Merge 020f9917e9c69fddac2eca54612ec6727eaf1230 on remote branch
Change-Id: I9adb17328f0ffe8ed10bbac37a510c0ca0ff8d46
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..20370f0
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,7 @@
+# This is the list of significant contributors.
+#
+# This does not necessarily list everyone who has contributed code,
+# especially since many employees of one corporation may be contributing.
+# To see the full list of contributors, see the revision history in
+# source control.
+Google LLC
\ No newline at end of file
diff --git a/Android.bp b/Android.bp
new file mode 100644
index 0000000..45509ce
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,18 @@
+// This file is generated by cargo2android.py --run --device --dependencies.
+// Do not modify this file as changes will be overridden on upgrade.
+
+
+
+rust_library {
+ name: "libcoset",
+ host_supported: true,
+ crate_name: "coset",
+ cargo_env_compat: true,
+ cargo_pkg_version: "0.3.0",
+ srcs: ["src/lib.rs"],
+ edition: "2018",
+ rustlibs: [
+ "libciborium",
+ "libciborium_io",
+ ],
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..97eb7ff
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,61 @@
+# Change Log
+
+## 0.3.0 - 2022-01-19
+
+- Change to use `ciborium` as CBOR library. Breaking change with many knock-on effects:
+ - Re-export `ciborium` as `coset::cbor` (rather than `sk-cbor`).
+ - Use `ciborium`'s `Value` type rather than `sk-cbor`'s version.
+ - Change `CoseError` to no longer wrap `sk-cbor` errors.
+ - Drop `derive` of `Eq` for data types (`ciborium` supports float values, which are inherently non-`Eq`)
+ - Add `#[must_use]` attributes to builder methods.
+ - Update MSRV to 1.56.0, as `ciborium` is `edition=2021`
+- Use new `ProtectedHeader` type for protected headers (breaking change). This variant of `Header` preserves any
+ originally-parsed data, so that calculations (signatures, decryption, etc.) over the data can use the bit-for-bit wire
+ data instead of a reconstituted (and potentially different) version.
+- Add more specific error cases to `CoseError` (breaking change):
+ - Use new `OutOfRangeIntegerValue` error when an integer value is too large for the representation used in this
+ crate.
+ - Use new `DuplicateMapKey` error when a CBOR map contains duplicate keys (and is thus invalid).
+ - Extend `DecodeFailed` error to include the underlying `ciborium::de::Error` value.
+ - Use new `ExtraneousData` error when data remains after reading a CBOR value.
+ - Rename `UnexpectedType` error to `UnexpectedItem` to reflect broader usage than type.
+- Add a crate-specific `Result` type whose `E` field defaults to `CoseError`.
+
+## 0.2.0 - 2021-12-09
+
+- Change to use `sk-cbor` as CBOR library, due to deprecation of `serde-cbor`. Breaking change with many knock-on
+ effects:
+ - Re-export `sk-cbor` as `coset::cbor`.
+ - Use `sk-cbor`'s `Value` type rather than `serde-cbor`'s version.
+ - Change encoding methods to consume `self`.
+ - Change encoding methods to be fallible.
+ - Move to be `no_std` (but still using `alloc`)
+ - Add `CoseError` error type and use throughout.
+ - Use `Vec` of pairs not `BTreeMap`s for CBOR map values.
+ - Use `i64` not `i128` for integer values throughout.
+ - Drop use of `serde`'s `Serialize` and `Deserialize` traits; instead…
+ - Add `CborSerializable` extension trait for conversion to/from bytes.
+ - Drop `from_tagged_reader` / `to_tagged_writer` methods from `TaggedCborSerializable` trait.
+ - Derive `Debug` for builders.
+ - Convert `CoseKeySet` to a newtype, and add standard traits.
+
+## 0.1.2 - 2021-08-24
+
+- Add fallible variants of builder methods that invoke closures (#20):
+ - `CoseRecipientBuilder::try_create_ciphertext()`
+ - `CoseEncryptBuilder::try_create_ciphertext()`
+ - `CoseEncrypt0Builder::try_create_ciphertext()`
+ - `CoseMacBuilder::try_create_tag()`
+ - `CoseMac0Builder::try_create_tag()`
+ - `CoseSignBuilder::try_add_created_signature()`
+ - `CoseSign1Builder::try_create_signature()`
+- Upgrade dependencies.
+
+## 0.1.1 - 2021-06-24
+
+- Make `KeyType` and `KeyOperation` public.
+- Upgrade dependencies.
+
+## 0.1.0 - 2021-05-18
+
+- Initial version, using `serde-cbor` as CBOR library.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..654a071
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,28 @@
+# How to Contribute
+
+We'd love to accept your patches and contributions to this project. There are
+just a few small guidelines you need to follow.
+
+## Contributor License Agreement
+
+Contributions to this project must be accompanied by a Contributor License
+Agreement. You (or your employer) retain the copyright to your contribution;
+this simply gives us permission to use and redistribute your contributions as
+part of the project. Head over to <https://cla.developers.google.com/> to see
+your current agreements on file or to sign a new one.
+
+You generally only need to submit a CLA once, so if you've already submitted one
+(even if it was for a different project), you probably don't need to do it
+again.
+
+## Code reviews
+
+All submissions, including submissions by project members, require review. We
+use GitHub pull requests for this purpose. Consult
+[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
+information on using pull requests.
+
+## Community Guidelines
+
+This project follows [Google's Open Source Community
+Guidelines](https://opensource.google/conduct/).
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..774a9b7
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "coset"
+version = "0.3.0"
+authors = ["David Drysdale <drysdale@google.com>", "Paul Crowley <paulcrowley@google.com>"]
+edition = "2018"
+license = "Apache-2.0"
+description = "Set of types for supporting COSE"
+repository = "https://github.com/google/coset"
+keywords = ["cryptography", "cose"]
+categories = ["cryptography"]
+
+[dependencies]
+ciborium = { version = "^0.2.0", default-features = false }
+ciborium-io = "^0.2.0"
+
+[dev-dependencies]
+hex = "^0.4.2"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..1faae87
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,18 @@
+name: "coset"
+description:
+ "Set of types for supporting CBOR Object Signing and Encryption (COSE)."
+
+third_party {
+ url {
+ type: HOMEPAGE
+ value: "https://crates.io/crates/coset"
+ }
+ url {
+ type: ARCHIVE
+ value: "https://static.crates.io/crates/coset/coset-0.3.0.crate"
+ }
+ version: "0.3.0"
+ last_upgrade_date { year: 2022 month: 1 day: 26 }
+ license_type: NOTICE
+}
+
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..f6f0a7f
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,6 @@
+include platform/prebuilts/rust:master:/OWNERS
+# Android Hardware Security
+paulcrowley@google.com
+drysdale@google.com
+asbel@google.com
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5b51f27
--- /dev/null
+++ b/README.md
@@ -0,0 +1,69 @@
+# COSET
+
+[![Docs](https://img.shields.io/badge/docs-rust-brightgreen?style=for-the-badge)](https://google.github.io/coset)
+[![CI Status](https://img.shields.io/github/workflow/status/google/coset/CI?color=blue&style=for-the-badge)](https://github.com/google/coset/actions?query=workflow%3ACI)
+[![codecov](https://img.shields.io/codecov/c/github/google/coset?style=for-the-badge)](https://codecov.io/gh/google/coset)
+
+This crate holds a set of Rust types for working with CBOR Object Signing and Encryption (COSE) objects, as defined in
+[RFC 8152](https://tools.ietf.org/html/rfc8152). It builds on the core [CBOR](https://tools.ietf.org/html/rfc7049)
+parsing functionality from the [`ciborium` crate](https://docs.rs/ciborium).
+
+See [crate docs](https://google.github.io/coset/rust/coset/index.html), or the [signature
+example](examples/signature.rs) for documentation on how to use the code.
+
+**This repo is under construction** and so details of the API and the code may change without warning.
+
+## `no_std` Support
+
+This crate supports `no_std`, but uses the `alloc` crate.
+
+## Minimum Supported Rust Version
+
+MSRV is 1.56 (the main `ciborium` dependency is `edition="2021"`)
+
+## Integer Ranges
+
+CBOR supports integers in the range:
+
+```text
+[-18_446_744_073_709_551_616, -1] ∪ [0, 18_446_744_073_709_551_615]
+```
+
+which is [-2<sup>64</sup>, -1] ∪ [0, 2<sup>64</sup> - 1].
+
+This does not map onto a single Rust integer type, so different CBOR crates take different approaches.
+
+- The [`serde_cbor`](https://docs.rs/serde_cbor) crate uses a single `i128` integer type for all integer values, which
+ means that all CBOR integer values can be expressed, but there are also `i128` values that cannot be encoded in CBOR.
+ This also means that data size is larger.
+- The [`ciborium`](https://docs.rs/ciborium) also uses a single `i128` integer type internally, but wraps it in its own
+ [`Integer`](https://docs.rs/ciborium/latest/ciborium/value/struct.Integer.html) type and only implements `TryFrom`
+ (not `From`) for `i128` / `u128` conversions so that unrepresentable numbers can be rejected.
+- The [`sk-cbor`](https://docs.rs/sk-cbor) crate uses distinct types:
+ - positive numbers as u64, covering [0, 2<sup>64</sup> - 1]
+ - negative numbers as i64, covering [-2<sup>63</sup>, -1] (which means that some theoretically-valid large negative
+ values are not represented).
+
+This crate uses a single type to encompass both positive and negative values, but uses `i64` for that type to keep data
+sizes smaller. This means that:
+
+- positive numbers in `i64` cover [0, 2<sup>63</sup> - 1]
+- negative numbers in `i64` cover [-2<sup>63</sup>, -1]
+
+and so there are large values – both positive and negative – which are not supported by this crate.
+
+## Working on the Code
+
+Local coding conventions are enforced by the [continuous integration jobs](.github/workflows) and include:
+
+- Build cleanly and pass all tests.
+- Free of [Clippy](https://github.com/rust-lang/rust-clippy) warnings.
+- Formatted with `rustfmt` using the local [rustfmt.toml](.rustfmt.toml) settings.
+- Compliance with local conventions:
+ - All `TODO` markers should be of form `TODO(#99)` and refer to an open GitHub issue.
+ - Calls to functions that can panic (`panic!`, `unwrap`, `expect`) should have a comment on the same line in the
+ form `// safe: reason` (or `/* safe: reason */`) to document the reason why panicking is acceptable.
+
+## Disclaimer
+
+This is not an officially supported Google product.
diff --git a/deny.toml b/deny.toml
new file mode 100644
index 0000000..849416c
--- /dev/null
+++ b/deny.toml
@@ -0,0 +1,32 @@
+# Configuration used for dependency checking with cargo-deny.
+#
+# For further details on all configuration options see:
+# https://embarkstudios.github.io/cargo-deny/checks/cfg.html
+targets = [
+ { triple = "x86_64-unknown-linux-gnu" },
+ { triple = "x86_64-apple-darwin" },
+ { triple = "x86_64-pc-windows-msvc" },
+]
+
+# Deny all advisories unless explicitly ignored.
+[advisories]
+vulnerability = "deny"
+unmaintained = "deny"
+yanked = "deny"
+notice = "deny"
+ignore = []
+
+# Deny multiple versions unless explicitly skipped.
+[bans]
+multiple-versions = "deny"
+wildcards = "allow"
+
+######################################
+
+# List of allowed licenses.
+[licenses]
+allow = [
+ "Apache-2.0",
+ "MIT",
+]
+copyleft = "deny"
diff --git a/dependabot.yml b/dependabot.yml
new file mode 100644
index 0000000..7d87c2f
--- /dev/null
+++ b/dependabot.yml
@@ -0,0 +1,9 @@
+# Please see the documentation for all configuration options:
+# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
+
+version: 2
+updates:
+ - package-ecosystem: "cargo"
+ directory: "/"
+ schedule:
+ interval: "daily"
diff --git a/examples/signature.rs b/examples/signature.rs
new file mode 100644
index 0000000..4512df7
--- /dev/null
+++ b/examples/signature.rs
@@ -0,0 +1,91 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+//! Example program demonstrating signature creation.
+use coset::{iana, CborSerializable};
+
+#[derive(Copy, Clone)]
+struct FakeSigner {}
+
+// Use a fake signer/verifier (to avoid pulling in lots of dependencies).
+impl FakeSigner {
+ fn sign(&self, data: &[u8]) -> Vec<u8> {
+ data.to_vec()
+ }
+
+ fn verify(&self, sig: &[u8], data: &[u8]) -> Result<(), String> {
+ if sig != self.sign(data) {
+ Err("failed to verify".to_owned())
+ } else {
+ Ok(())
+ }
+ }
+}
+
+fn main() {
+ // Build a fake signer/verifier (to avoid pulling in lots of dependencies).
+ let signer = FakeSigner {};
+ let verifier = signer;
+
+ // Inputs.
+ let pt = b"This is the content";
+ let aad = b"this is additional data";
+
+ // Build a `CoseSign1` object.
+ let protected = coset::HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ES256)
+ .key_id(b"11".to_vec())
+ .build();
+ let sign1 = coset::CoseSign1Builder::new()
+ .protected(protected)
+ .payload(pt.to_vec())
+ .create_signature(aad, |pt| signer.sign(pt))
+ .build();
+
+ // Serialize to bytes.
+ let sign1_data = sign1.to_vec().unwrap();
+ println!(
+ "'{}' + '{}' => {}",
+ String::from_utf8_lossy(pt),
+ String::from_utf8_lossy(aad),
+ hex::encode(&sign1_data)
+ );
+
+ // At the receiving end, deserialize the bytes back to a `CoseSign1` object.
+ let mut sign1 = coset::CoseSign1::from_slice(&sign1_data).unwrap();
+
+ // Check the signature, which needs to have the same `aad` provided.
+ let result = sign1.verify_signature(aad, |sig, data| verifier.verify(sig, data));
+ println!("Signature verified: {:?}.", result);
+ assert!(result.is_ok());
+
+ // Changing an unprotected header leaves the signature valid.
+ sign1.unprotected.content_type = Some(coset::ContentType::Text("text/plain".to_owned()));
+ assert!(sign1
+ .verify_signature(aad, |sig, data| verifier.verify(sig, data))
+ .is_ok());
+
+ // Providing a different `aad` means the signature won't validate.
+ assert!(sign1
+ .verify_signature(b"not aad", |sig, data| verifier.verify(sig, data))
+ .is_err());
+
+ // Changing a protected header invalidates the signature.
+ sign1.protected.header.content_type = Some(coset::ContentType::Text("text/plain".to_owned()));
+ assert!(sign1
+ .verify_signature(aad, |sig, data| verifier.verify(sig, data))
+ .is_err());
+}
diff --git a/fuzz/.gitignore b/fuzz/.gitignore
new file mode 100644
index 0000000..572e03b
--- /dev/null
+++ b/fuzz/.gitignore
@@ -0,0 +1,4 @@
+
+target
+corpus
+artifacts
diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock
new file mode 100644
index 0000000..443dab6
--- /dev/null
+++ b/fuzz/Cargo.lock
@@ -0,0 +1,46 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "arbitrary"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "698b65a961a9d730fb45b6b0327e20207810c9f61ee421b082b27ba003f49e2b"
+
+[[package]]
+name = "cc"
+version = "1.0.67"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
+
+[[package]]
+name = "coset"
+version = "0.2.0"
+dependencies = [
+ "sk-cbor",
+]
+
+[[package]]
+name = "coset-fuzz"
+version = "0.0.0"
+dependencies = [
+ "coset",
+ "libfuzzer-sys",
+]
+
+[[package]]
+name = "libfuzzer-sys"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86c975d637bc2a2f99440932b731491fc34c7f785d239e38af3addd3c2fd0e46"
+dependencies = [
+ "arbitrary",
+ "cc",
+]
+
+[[package]]
+name = "sk-cbor"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e94879a793aba6e65d691f345cfd172c4cc924a78259d5f9612a2cbfb78847a"
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
new file mode 100644
index 0000000..435f2c0
--- /dev/null
+++ b/fuzz/Cargo.toml
@@ -0,0 +1,44 @@
+
+[package]
+name = "coset-fuzz"
+version = "0.0.0"
+authors = ["Automatically generated"]
+publish = false
+edition = "2018"
+
+[package.metadata]
+cargo-fuzz = true
+
+[dependencies]
+libfuzzer-sys = "0.4"
+
+[dependencies.coset]
+path = ".."
+
+# Prevent this from interfering with workspaces
+[workspace]
+members = ["."]
+
+[[bin]]
+name = "encrypt"
+path = "fuzz_targets/encrypt.rs"
+test = false
+doc = false
+
+[[bin]]
+name = "key"
+path = "fuzz_targets/key.rs"
+test = false
+doc = false
+
+[[bin]]
+name = "mac"
+path = "fuzz_targets/mac.rs"
+test = false
+doc = false
+
+[[bin]]
+name = "sign"
+path = "fuzz_targets/sign.rs"
+test = false
+doc = false
diff --git a/fuzz/fuzz_targets/encrypt.rs b/fuzz/fuzz_targets/encrypt.rs
new file mode 100644
index 0000000..b579053
--- /dev/null
+++ b/fuzz/fuzz_targets/encrypt.rs
@@ -0,0 +1,28 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+//! Fuzz COSE_Encrypt* parsing.
+
+#![no_main]
+use libfuzzer_sys::fuzz_target;
+
+use coset::CborSerializable;
+
+fuzz_target!(|data: &[u8]| {
+ let _recipient = coset::CoseRecipient::from_slice(data);
+ let _encrypt = coset::CoseEncrypt::from_slice(data);
+ let _encrypt0 = coset::CoseEncrypt0::from_slice(data);
+});
diff --git a/fuzz/fuzz_targets/key.rs b/fuzz/fuzz_targets/key.rs
new file mode 100644
index 0000000..24217e9
--- /dev/null
+++ b/fuzz/fuzz_targets/key.rs
@@ -0,0 +1,26 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+//! Fuzz COSE_Key parsing.
+
+#![no_main]
+use libfuzzer_sys::fuzz_target;
+
+use coset::CborSerializable;
+
+fuzz_target!(|data: &[u8]| {
+ let _key = coset::CoseKey::from_slice(data);
+});
diff --git a/fuzz/fuzz_targets/mac.rs b/fuzz/fuzz_targets/mac.rs
new file mode 100644
index 0000000..5d5ec09
--- /dev/null
+++ b/fuzz/fuzz_targets/mac.rs
@@ -0,0 +1,27 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+//! Fuzz COSE_Mac* parsing.
+
+#![no_main]
+use libfuzzer_sys::fuzz_target;
+
+use coset::CborSerializable;
+
+fuzz_target!(|data: &[u8]| {
+ let _mac = coset::CoseMac::from_slice(data);
+ let _mac0 = coset::CoseMac0::from_slice(data);
+});
diff --git a/fuzz/fuzz_targets/sign.rs b/fuzz/fuzz_targets/sign.rs
new file mode 100644
index 0000000..56242d4
--- /dev/null
+++ b/fuzz/fuzz_targets/sign.rs
@@ -0,0 +1,28 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+//! Fuzz COSE_Sign* parsing.
+
+#![no_main]
+use libfuzzer_sys::fuzz_target;
+
+use coset::CborSerializable;
+
+fuzz_target!(|data: &[u8]| {
+ let _signature = coset::CoseSignature::from_slice(data);
+ let _sign = coset::CoseSign::from_slice(data);
+ let _sign1 = coset::CoseSign1::from_slice(data);
+});
diff --git a/patches/std.diff b/patches/std.diff
new file mode 100644
index 0000000..dd24015
--- /dev/null
+++ b/patches/std.diff
@@ -0,0 +1,15 @@
+diff --git a/src/lib.rs b/src/lib.rs
+index 2a8ceb3..3c46fcf 100644
+--- a/src/lib.rs
++++ b/src/lib.rs
+@@ -100,6 +100,9 @@
+ #![deny(rustdoc::broken_intra_doc_links)]
+ extern crate alloc;
+
++/// Use std to allow building as a dylib.
++extern crate std;
++
+ /// Re-export of the `ciborium` crate used for underlying CBOR encoding.
+ pub use ciborium as cbor;
+
+
diff --git a/scripts/build-gh-pages.sh b/scripts/build-gh-pages.sh
new file mode 100755
index 0000000..fb191ad
--- /dev/null
+++ b/scripts/build-gh-pages.sh
@@ -0,0 +1,70 @@
+#!/usr/bin/env bash
+# Copyright 2020 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -o errexit
+set -o nounset
+set -o xtrace
+set -o pipefail
+
+# Update the gh-pages branch. Note that `cargo doc` is **not deterministic** so
+# this should only be done when there is a real change.
+readonly RUST_BRANCH=${1:-main}
+readonly RUST_GH_BRANCH=gh-pages
+
+if [ -z "${FORCE+x}" ]; then
+ readonly PREV_COMMIT=$(git log --oneline -n 1 ${RUST_GH_BRANCH} | sed 's/.*branch at \([0-9a-f]*\)/\1/')
+ readonly CHANGES=$(git diff "${PREV_COMMIT}..${RUST_BRANCH}" | grep -e '[+-]//[/!]')
+
+ if [ -z "${CHANGES}" ]; then
+ echo "No doc comment changes found in ${PREV_COMMIT}..${RUST_BRANCH} subdir rust/"
+ exit 0
+ fi
+fi
+
+git switch "${RUST_BRANCH}"
+readonly RUST_BRANCH_SHA1=$(git rev-parse --short HEAD)
+readonly RUST_BRANCH_SUBJECT=$(git log -n 1 --format=format:%s)
+readonly COMMIT_MESSAGE=$(cat <<-END
+Update Rust docs to ${RUST_BRANCH} branch at ${RUST_BRANCH_SHA1}
+
+Auto-generated from commit ${RUST_BRANCH_SHA1} ("${RUST_BRANCH_SUBJECT}").
+END
+)
+
+readonly TGZ_FILE="/tmp/coset-doc-${RUST_BRANCH_SHA1}.tgz"
+# Build Cargo docs and save them off outside the repo
+(
+ rm -rf target/doc
+ cargo doc --no-deps
+ cargo deadlinks
+ cd target/doc || exit
+ tar czf "${TGZ_FILE}" ./*
+)
+
+# Shift to ${RUST_GH_BRANCH} branch and replace contents of (just) ./rust/
+git switch ${RUST_GH_BRANCH}
+
+readonly DOC_DIR=rust
+rm -rf ${DOC_DIR}
+mkdir ${DOC_DIR}
+(
+ cd "${DOC_DIR}" || exit
+ tar xzf "${TGZ_FILE}"
+)
+
+# Commit any differences
+git add "${DOC_DIR}"
+git commit --message="${COMMIT_MESSAGE}"
+git switch "${RUST_BRANCH}"
diff --git a/scripts/check-format.sh b/scripts/check-format.sh
new file mode 100755
index 0000000..445a564
--- /dev/null
+++ b/scripts/check-format.sh
@@ -0,0 +1,107 @@
+#!/usr/bin/env bash
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Find code files.
+CODE_FILES=()
+while IFS= read -r -d $'\0'; do
+ CODE_FILES+=("$REPLY")
+done < <(find . -not \( -path '*/target' -prune \) -and -name '*.rs' -print0)
+
+# Find markdown files.
+MD_FILES=()
+while IFS= read -r -d $'\0'; do
+ MD_FILES+=("$REPLY")
+done < <(find . -not \( -path '*/target' -prune \) -and -not \( -path '*/wycheproof' -prune \) -and -name '*.md' -print0)
+
+# Check that source files have the Apache License header.
+# Automatically skips generated files.
+check_license() {
+ local path="$1"
+
+ if head -1 "$path" | grep -iq -e 'generated' -e '::prost::message'; then
+ return 0
+ fi
+
+ if echo "$path" | grep -q "/proto/"; then
+ return 0
+ fi
+
+ # Look for "Apache License" on the file header
+ if ! head -10 "$path" | grep -q 'Apache License'; then
+ # Format: $path:$line:$message
+ echo "$path:1:license header not found"
+ return 1
+ fi
+ return 0
+}
+
+# Check that any TODO markers in files have associated issue numbers
+check_todo() {
+ local path="$1"
+ local result
+ result=$(grep --with-filename --line-number TODO "$path" | grep --invert-match --regexp='TODO(#[0-9][0-9]*)')
+ if [[ -n $result ]]; then
+ echo "TODO marker without issue number:"
+ echo "$result"
+ return 1
+ fi
+ return 0
+}
+
+# Check that any calls that might panic have a comment noting why they're safe
+check_panic() {
+ local path="$1"
+ if [[ $path =~ "test" || $path =~ "examples/" || $path =~ "rinkey/" || $path =~ "benches/" ]]; then
+ return 0
+ fi
+ for needle in "panic!(" "unwrap(" "expect(" "unwrap_err(" "expect_err(" "unwrap_none(" "expect_none(" "unreachable!"; do
+ local result
+ result=$(grep --with-filename --line-number "$needle" "$path" | grep --invert-match --regexp='safe:'| grep --invert-match --regexp=':[0-9]*://')
+ if [[ -n $result ]]; then
+ echo "Un-annotated panic code:"
+ echo "$result"
+ return 1
+ fi
+ done
+ return 0
+}
+
+errcount=0
+for f in "${CODE_FILES[@]}"; do
+ check_license "$f"
+ errcount=$((errcount + $?))
+ check_todo "$f"
+ errcount=$((errcount + $?))
+ check_panic "$f"
+ errcount=$((errcount + $?))
+done
+
+EMBEDMD="$(go env GOPATH)/bin/embedmd"
+if [[ ! -x "$EMBEDMD" ]]; then
+ go get github.com/campoy/embedmd
+fi
+for f in "${MD_FILES[@]}"; do
+ "$EMBEDMD" -d "$f"
+ errcount=$((errcount + $?))
+ check_todo "$f"
+ errcount=$((errcount + $?))
+ mdl "$f"
+ errcount=$((errcount + $?))
+done
+
+if [ $errcount -gt 0 ]; then
+ echo "$errcount errors detected"
+ exit 1
+fi
diff --git a/src/common/mod.rs b/src/common/mod.rs
new file mode 100644
index 0000000..bc602e6
--- /dev/null
+++ b/src/common/mod.rs
@@ -0,0 +1,359 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+//! Common types.
+
+use crate::{
+ cbor,
+ cbor::value::Value,
+ iana,
+ iana::{EnumI64, WithPrivateRange},
+ util::{cbor_type_error, AsCborValue, ValueTryAs},
+};
+use alloc::{boxed::Box, string::String, vec::Vec};
+use core::{cmp::Ordering, convert::TryInto};
+
+#[cfg(test)]
+mod tests;
+
+/// Marker structure indicating that the EOF was encountered when reading CBOR data.
+#[derive(Debug)]
+pub struct EndOfFile;
+
+/// Error type for failures in encoding or decoding COSE types.
+pub enum CoseError {
+ /// CBOR decoding failure.
+ DecodeFailed(cbor::de::Error<EndOfFile>),
+ /// Duplicate map key detected.
+ DuplicateMapKey,
+ /// CBOR encoding failure.
+ EncodeFailed,
+ /// CBOR input had extra data.
+ ExtraneousData,
+ /// Integer value on the wire is outside the range of integers representable in this crate.
+ /// See <https://crates.io/crates/coset/#integer-ranges>.
+ OutOfRangeIntegerValue,
+ /// Unexpected CBOR item encountered (got, want).
+ UnexpectedItem(&'static str, &'static str),
+ /// Unrecognized value in IANA-controlled range (with no private range).
+ UnregisteredIanaValue,
+ /// Unrecognized value in neither IANA-controlled range nor private range.
+ UnregisteredIanaNonPrivateValue,
+}
+
+/// Crate-specific Result type
+pub type Result<T, E = CoseError> = core::result::Result<T, E>;
+
+impl core::convert::From<cbor::de::Error<EndOfFile>> for CoseError {
+ fn from(e: cbor::de::Error<EndOfFile>) -> Self {
+ CoseError::DecodeFailed(e)
+ }
+}
+
+impl<T> core::convert::From<cbor::ser::Error<T>> for CoseError {
+ fn from(_e: cbor::ser::Error<T>) -> Self {
+ CoseError::EncodeFailed
+ }
+}
+
+impl core::convert::From<core::num::TryFromIntError> for CoseError {
+ fn from(_: core::num::TryFromIntError) -> Self {
+ CoseError::OutOfRangeIntegerValue
+ }
+}
+
+impl core::fmt::Debug for CoseError {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ match self {
+ CoseError::DecodeFailed(e) => write!(f, "decode CBOR failure: {}", e),
+ CoseError::DuplicateMapKey => write!(f, "duplicate map key"),
+ CoseError::EncodeFailed => write!(f, "encode CBOR failure"),
+ CoseError::ExtraneousData => write!(f, "extraneous data in CBOR input"),
+ CoseError::OutOfRangeIntegerValue => write!(f, "out of range integer value"),
+ CoseError::UnexpectedItem(got, want) => write!(f, "got {}, expected {}", got, want),
+ CoseError::UnregisteredIanaValue => write!(f, "expected recognized IANA value"),
+ CoseError::UnregisteredIanaNonPrivateValue => {
+ write!(f, "expected value in IANA or private use range")
+ }
+ }
+ }
+}
+
+/// Newtype wrapper around a byte slice to allow left-over data to be detected.
+struct MeasuringReader<'a>(&'a [u8]);
+
+impl<'a> MeasuringReader<'a> {
+ fn new(buf: &'a [u8]) -> MeasuringReader<'a> {
+ MeasuringReader(buf)
+ }
+
+ fn is_empty(&self) -> bool {
+ self.0.is_empty()
+ }
+}
+
+impl<'a> ciborium_io::Read for &mut MeasuringReader<'a> {
+ type Error = EndOfFile;
+
+ fn read_exact(&mut self, data: &mut [u8]) -> Result<(), Self::Error> {
+ if data.len() > self.0.len() {
+ return Err(EndOfFile);
+ }
+
+ let (prefix, suffix) = self.0.split_at(data.len());
+ data.copy_from_slice(prefix);
+ self.0 = suffix;
+ Ok(())
+ }
+}
+
+/// Read a CBOR [`Value`] from a byte slice, failing if any extra data remains after the `Value` has
+/// been read.
+fn read_to_value(slice: &[u8]) -> Result<Value> {
+ let mut mr = MeasuringReader::new(slice);
+ let value = cbor::de::from_reader(&mut mr)?;
+ if mr.is_empty() {
+ Ok(value)
+ } else {
+ Err(CoseError::ExtraneousData)
+ }
+}
+
+/// Extension trait that adds serialization/deserialization methods.
+pub trait CborSerializable: AsCborValue {
+ /// Create an object instance from serialized CBOR data in a slice.
+ fn from_slice(slice: &[u8]) -> Result<Self> {
+ Self::from_cbor_value(read_to_value(slice)?)
+ }
+
+ /// Serialize this object to a vector, consuming it along the way.
+ fn to_vec(self) -> Result<Vec<u8>> {
+ let mut data = Vec::new();
+ cbor::ser::into_writer(&self.to_cbor_value()?, &mut data)?;
+ Ok(data)
+ }
+}
+
+/// Extension trait that adds tagged serialization/deserialization methods.
+pub trait TaggedCborSerializable: AsCborValue {
+ /// The associated tag value.
+ const TAG: u64;
+
+ /// Create an object instance from serialized CBOR data in a slice, expecting an initial
+ /// tag value.
+ fn from_tagged_slice(slice: &[u8]) -> Result<Self> {
+ let (t, v) = read_to_value(slice)?.try_as_tag()?;
+ if t != Self::TAG {
+ return Err(CoseError::UnexpectedItem("tag", "other tag"));
+ }
+ Self::from_cbor_value(*v)
+ }
+
+ /// Serialize this object to a vector, including initial tag, consuming the object along the
+ /// way.
+ fn to_tagged_vec(self) -> Result<Vec<u8>> {
+ let mut data = Vec::new();
+ cbor::ser::into_writer(
+ &Value::Tag(Self::TAG, Box::new(self.to_cbor_value()?)),
+ &mut data,
+ )?;
+ Ok(data)
+ }
+}
+
+/// Algorithm identifier.
+pub type Algorithm = crate::RegisteredLabelWithPrivate<iana::Algorithm>;
+
+impl Default for Algorithm {
+ fn default() -> Self {
+ Algorithm::Assigned(iana::Algorithm::Reserved)
+ }
+}
+
+/// A COSE label may be either a signed integer value or a string.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum Label {
+ Int(i64),
+ Text(String),
+}
+
+impl CborSerializable for Label {}
+
+/// Manual implementation of [`Ord`] to ensure that CBOR canonical ordering is respected.
+///
+/// Note that this uses the ordering given by RFC 8949 section 4.2.1 (lexicographic ordering of
+/// encoded form), which is *different* from the canonical ordering defined in RFC 7049 section 3.9
+/// (where the primary sorting criterion is the length of the encoded form)
+impl Ord for Label {
+ fn cmp(&self, other: &Self) -> Ordering {
+ match (self, other) {
+ (Label::Int(i1), Label::Int(i2)) => match (i1.signum(), i2.signum()) {
+ (-1, -1) => (-i1).cmp(&(-i2)),
+ (-1, 0) => Ordering::Greater,
+ (-1, 1) => Ordering::Greater,
+ (0, -1) => Ordering::Less,
+ (0, 0) => Ordering::Equal,
+ (0, 1) => Ordering::Less,
+ (1, -1) => Ordering::Less,
+ (1, 0) => Ordering::Greater,
+ (1, 1) => i1.cmp(i2),
+ (_, _) => unreachable!(), // safe: all possibilies covered
+ },
+ (Label::Int(_i1), Label::Text(_t2)) => Ordering::Less,
+ (Label::Text(_t1), Label::Int(_i2)) => Ordering::Greater,
+ (Label::Text(t1), Label::Text(t2)) => t1.len().cmp(&t2.len()).then(t1.cmp(t2)),
+ }
+ }
+}
+
+impl PartialOrd for Label {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl AsCborValue for Label {
+ fn from_cbor_value(value: Value) -> Result<Self> {
+ match value {
+ Value::Integer(i) => Ok(Label::Int(i.try_into()?)),
+ Value::Text(t) => Ok(Label::Text(t)),
+ v => cbor_type_error(&v, "int/tstr"),
+ }
+ }
+ fn to_cbor_value(self) -> Result<Value> {
+ Ok(match self {
+ Label::Int(i) => Value::from(i),
+ Label::Text(t) => Value::Text(t),
+ })
+ }
+}
+
+/// A COSE label which can be either a signed integer value or a string, but
+/// where the allowed integer values are governed by IANA.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum RegisteredLabel<T: EnumI64> {
+ Assigned(T),
+ Text(String),
+}
+
+impl<T: EnumI64> CborSerializable for RegisteredLabel<T> {}
+
+/// Manual implementation of [`Ord`] to ensure that CBOR canonical ordering is respected.
+impl<T: EnumI64> Ord for RegisteredLabel<T> {
+ fn cmp(&self, other: &Self) -> Ordering {
+ match (self, other) {
+ (RegisteredLabel::Assigned(i1), RegisteredLabel::Assigned(i2)) => {
+ Label::Int(i1.to_i64()).cmp(&Label::Int(i2.to_i64()))
+ }
+ (RegisteredLabel::Assigned(_i1), RegisteredLabel::Text(_t2)) => Ordering::Less,
+ (RegisteredLabel::Text(_t1), RegisteredLabel::Assigned(_i2)) => Ordering::Greater,
+ (RegisteredLabel::Text(t1), RegisteredLabel::Text(t2)) => {
+ t1.len().cmp(&t2.len()).then(t1.cmp(t2))
+ }
+ }
+ }
+}
+
+impl<T: EnumI64> PartialOrd for RegisteredLabel<T> {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl<T: EnumI64> AsCborValue for RegisteredLabel<T> {
+ fn from_cbor_value(value: Value) -> Result<Self> {
+ match value {
+ Value::Integer(i) => {
+ if let Some(a) = T::from_i64(i.try_into()?) {
+ Ok(RegisteredLabel::Assigned(a))
+ } else {
+ Err(CoseError::UnregisteredIanaValue)
+ }
+ }
+ Value::Text(t) => Ok(RegisteredLabel::Text(t)),
+ v => cbor_type_error(&v, "int/tstr"),
+ }
+ }
+
+ fn to_cbor_value(self) -> Result<Value> {
+ Ok(match self {
+ RegisteredLabel::Assigned(e) => Value::from(e.to_i64()),
+ RegisteredLabel::Text(t) => Value::Text(t),
+ })
+ }
+}
+
+/// A COSE label which can be either a signed integer value or a string, and
+/// where the allowed integer values are governed by IANA but include a private
+/// use range.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum RegisteredLabelWithPrivate<T: EnumI64 + WithPrivateRange> {
+ PrivateUse(i64),
+ Assigned(T),
+ Text(String),
+}
+
+impl<T: EnumI64 + WithPrivateRange> CborSerializable for RegisteredLabelWithPrivate<T> {}
+
+/// Manual implementation of [`Ord`] to ensure that CBOR canonical ordering is respected.
+impl<T: EnumI64 + WithPrivateRange> Ord for RegisteredLabelWithPrivate<T> {
+ fn cmp(&self, other: &Self) -> Ordering {
+ use RegisteredLabelWithPrivate::{Assigned, PrivateUse, Text};
+ match (self, other) {
+ (Assigned(i1), Assigned(i2)) => Label::Int(i1.to_i64()).cmp(&Label::Int(i2.to_i64())),
+ (Assigned(i1), PrivateUse(i2)) => Label::Int(i1.to_i64()).cmp(&Label::Int(*i2)),
+ (PrivateUse(i1), Assigned(i2)) => Label::Int(*i1).cmp(&Label::Int(i2.to_i64())),
+ (PrivateUse(i1), PrivateUse(i2)) => Label::Int(*i1).cmp(&Label::Int(*i2)),
+ (Assigned(_i1), Text(_t2)) => Ordering::Less,
+ (PrivateUse(_i1), Text(_t2)) => Ordering::Less,
+ (Text(_t1), Assigned(_i2)) => Ordering::Greater,
+ (Text(_t1), PrivateUse(_i2)) => Ordering::Greater,
+ (Text(t1), Text(t2)) => t1.len().cmp(&t2.len()).then(t1.cmp(t2)),
+ }
+ }
+}
+
+impl<T: EnumI64 + WithPrivateRange> PartialOrd for RegisteredLabelWithPrivate<T> {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl<T: EnumI64 + WithPrivateRange> AsCborValue for RegisteredLabelWithPrivate<T> {
+ fn from_cbor_value(value: Value) -> Result<Self> {
+ match value {
+ Value::Integer(i) => {
+ let i = i.try_into()?;
+ if let Some(a) = T::from_i64(i) {
+ Ok(RegisteredLabelWithPrivate::Assigned(a))
+ } else if T::is_private(i) {
+ Ok(RegisteredLabelWithPrivate::PrivateUse(i))
+ } else {
+ Err(CoseError::UnregisteredIanaNonPrivateValue)
+ }
+ }
+ Value::Text(t) => Ok(RegisteredLabelWithPrivate::Text(t)),
+ v => cbor_type_error(&v, "int/tstr"),
+ }
+ }
+ fn to_cbor_value(self) -> Result<Value> {
+ Ok(match self {
+ RegisteredLabelWithPrivate::PrivateUse(i) => Value::from(i),
+ RegisteredLabelWithPrivate::Assigned(i) => Value::from(i.to_i64()),
+ RegisteredLabelWithPrivate::Text(t) => Value::Text(t),
+ })
+ }
+}
diff --git a/src/common/tests.rs b/src/common/tests.rs
new file mode 100644
index 0000000..5cde798
--- /dev/null
+++ b/src/common/tests.rs
@@ -0,0 +1,372 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+use super::*;
+use crate::util::expect_err;
+use alloc::{borrow::ToOwned, format, vec};
+use core::cmp::Ordering;
+
+#[test]
+fn test_error_convert() {
+ let e = CoseError::from(crate::cbor::ser::Error::<String>::Value(
+ "error message lost".to_owned(),
+ ));
+ match e {
+ CoseError::EncodeFailed => {
+ assert!(format!("{:?}", e).contains("encode CBOR failure"));
+ }
+ _ => panic!("unexpected error enum after conversion"),
+ }
+}
+
+#[test]
+fn test_label_encode() {
+ let tests = vec![
+ (Label::Int(2), "02"),
+ (Label::Int(-1), "20"),
+ (Label::Text("abc".to_owned()), "63616263"),
+ ];
+
+ for (i, (label, label_data)) in tests.iter().enumerate() {
+ let got = label.clone().to_vec().unwrap();
+ assert_eq!(*label_data, hex::encode(&got), "case {}", i);
+
+ let got = Label::from_slice(&got).unwrap();
+ assert_eq!(*label, got);
+ }
+}
+
+#[test]
+fn test_label_sort() {
+ // Pairs of `Label`s with the "smaller" first.
+ let pairs = vec![
+ (Label::Int(0x1234), Label::Text("a".to_owned())),
+ (Label::Int(0x1234), Label::Text("ab".to_owned())),
+ (Label::Int(0), Label::Text("ab".to_owned())),
+ (Label::Int(-1), Label::Text("ab".to_owned())),
+ (Label::Int(0), Label::Int(10)),
+ (Label::Int(0), Label::Int(-10)),
+ (Label::Int(10), Label::Int(-1)),
+ (Label::Int(-1), Label::Int(-2)),
+ (Label::Int(0x12), Label::Int(0x1234)),
+ (Label::Int(0x99), Label::Int(0x1234)),
+ (Label::Int(0x1234), Label::Int(0x1235)),
+ (Label::Text("a".to_owned()), Label::Text("ab".to_owned())),
+ (Label::Text("aa".to_owned()), Label::Text("ab".to_owned())),
+ ];
+ for (left, right) in pairs.into_iter() {
+ let value_cmp = left.cmp(&right);
+ let value_partial_cmp = left.partial_cmp(&right);
+ let left_data = left.clone().to_vec().unwrap();
+ let right_data = right.clone().to_vec().unwrap();
+ let data_cmp = left_data.cmp(&right_data);
+ let reverse_cmp = right.cmp(&left);
+ let equal_cmp = left.cmp(&left);
+
+ assert_eq!(value_cmp, Ordering::Less, "{:?} < {:?}", left, right);
+ assert_eq!(
+ value_partial_cmp,
+ Some(Ordering::Less),
+ "{:?} < {:?}",
+ left,
+ right
+ );
+ assert_eq!(
+ data_cmp,
+ Ordering::Less,
+ "{:?}={} < {:?}={}",
+ left,
+ hex::encode(&left_data),
+ right,
+ hex::encode(&right_data)
+ );
+ assert_eq!(reverse_cmp, Ordering::Greater, "{:?} > {:?}", right, left);
+ assert_eq!(equal_cmp, Ordering::Equal, "{:?} = {:?}", left, left);
+ }
+}
+
+#[test]
+fn test_label_decode_fail() {
+ let tests = vec![
+ ("43010203", "expected int/tstr"),
+ ("", "decode CBOR failure: Io(EndOfFile"),
+ ("1e", "decode CBOR failure: Syntax"),
+ ("0202", "extraneous data"),
+ ];
+ for (label_data, err_msg) in tests.iter() {
+ let data = hex::decode(label_data).unwrap();
+ let result = Label::from_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
+
+#[test]
+fn test_registered_label_encode() {
+ let tests = vec![
+ (RegisteredLabel::Assigned(iana::Algorithm::A192GCM), "02"),
+ (RegisteredLabel::Assigned(iana::Algorithm::EdDSA), "27"),
+ (RegisteredLabel::Text("abc".to_owned()), "63616263"),
+ ];
+
+ for (i, (label, label_data)) in tests.iter().enumerate() {
+ let got = label.clone().to_vec().unwrap();
+ assert_eq!(*label_data, hex::encode(&got), "case {}", i);
+
+ let got = RegisteredLabel::from_slice(&got).unwrap();
+ assert_eq!(*label, got);
+ }
+}
+
+#[test]
+fn test_registered_label_sort() {
+ use RegisteredLabel::{Assigned, Text};
+ // Pairs of `RegisteredLabel`s with the "smaller" first.
+ let pairs = vec![
+ (Assigned(iana::Algorithm::A192GCM), Text("a".to_owned())),
+ (Assigned(iana::Algorithm::WalnutDSA), Text("ab".to_owned())),
+ (Text("ab".to_owned()), Text("cd".to_owned())),
+ (Text("ab".to_owned()), Text("abcd".to_owned())),
+ (
+ Assigned(iana::Algorithm::AES_CCM_16_64_128),
+ Assigned(iana::Algorithm::A128KW),
+ ),
+ (
+ Assigned(iana::Algorithm::A192GCM),
+ Assigned(iana::Algorithm::AES_CCM_16_64_128),
+ ),
+ ];
+ for (left, right) in pairs.into_iter() {
+ let value_cmp = left.cmp(&right);
+ let value_partial_cmp = left.partial_cmp(&right);
+ let left_data = left.clone().to_vec().unwrap();
+ let right_data = right.clone().to_vec().unwrap();
+ let data_cmp = left_data.cmp(&right_data);
+ let reverse_cmp = right.cmp(&left);
+ let equal_cmp = left.cmp(&left);
+
+ assert_eq!(value_cmp, Ordering::Less, "{:?} < {:?}", left, right);
+ assert_eq!(
+ value_partial_cmp,
+ Some(Ordering::Less),
+ "{:?} < {:?}",
+ left,
+ right
+ );
+ assert_eq!(
+ data_cmp,
+ Ordering::Less,
+ "{:?}={} < {:?}={}",
+ left,
+ hex::encode(&left_data),
+ right,
+ hex::encode(&right_data)
+ );
+ assert_eq!(reverse_cmp, Ordering::Greater, "{:?} > {:?}", right, left);
+ assert_eq!(equal_cmp, Ordering::Equal, "{:?} = {:?}", left, left);
+ }
+}
+
+#[test]
+fn test_registered_label_decode_fail() {
+ let tests = vec![
+ ("43010203", "expected int/tstr"),
+ ("", "decode CBOR failure: Io(EndOfFile"),
+ ("09", "expected recognized IANA value"),
+ ("394e1f", "expected recognized IANA value"),
+ ];
+ for (label_data, err_msg) in tests.iter() {
+ let data = hex::decode(label_data).unwrap();
+ let result = RegisteredLabel::<iana::EllipticCurve>::from_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
+
+iana_registry! {
+ TestPrivateLabel {
+ Reserved: 0,
+ Something: 1,
+ }
+}
+
+impl WithPrivateRange for TestPrivateLabel {
+ fn is_private(i: i64) -> bool {
+ i > 10 || i < 1000
+ }
+}
+
+#[test]
+fn test_registered_label_with_private_encode() {
+ let tests = vec![
+ (
+ RegisteredLabelWithPrivate::Assigned(TestPrivateLabel::Something),
+ "01",
+ ),
+ (
+ RegisteredLabelWithPrivate::Text("abc".to_owned()),
+ "63616263",
+ ),
+ (
+ RegisteredLabelWithPrivate::PrivateUse(-70_000),
+ "3a0001116f",
+ ),
+ (RegisteredLabelWithPrivate::PrivateUse(11), "0b"),
+ ];
+
+ for (i, (label, label_data)) in tests.iter().enumerate() {
+ let got = label.clone().to_vec().unwrap();
+ assert_eq!(*label_data, hex::encode(&got), "case {}", i);
+
+ let got = RegisteredLabelWithPrivate::from_slice(&got).unwrap();
+ assert_eq!(*label, got);
+ }
+}
+
+#[test]
+fn test_registered_label_with_private_sort() {
+ use RegisteredLabelWithPrivate::{Assigned, PrivateUse, Text};
+ // Pairs of `RegisteredLabelWithPrivate`s with the "smaller" first.
+ let pairs = vec![
+ (Assigned(iana::Algorithm::A192GCM), Text("a".to_owned())),
+ (Assigned(iana::Algorithm::WalnutDSA), Text("ab".to_owned())),
+ (Text("ab".to_owned()), Text("cd".to_owned())),
+ (Text("ab".to_owned()), Text("abcd".to_owned())),
+ (
+ Assigned(iana::Algorithm::AES_CCM_16_64_128),
+ Assigned(iana::Algorithm::A128KW),
+ ),
+ (
+ Assigned(iana::Algorithm::A192GCM),
+ Assigned(iana::Algorithm::AES_CCM_16_64_128),
+ ),
+ (
+ Assigned(iana::Algorithm::AES_CCM_16_64_128),
+ PrivateUse(-70_000),
+ ),
+ (PrivateUse(-70_000), PrivateUse(-70_001)),
+ (PrivateUse(-70_000), Text("a".to_owned())),
+ ];
+ for (left, right) in pairs.into_iter() {
+ let value_cmp = left.cmp(&right);
+ let value_partial_cmp = left.partial_cmp(&right);
+ let left_data = left.clone().to_vec().unwrap();
+ let right_data = right.clone().to_vec().unwrap();
+ let data_cmp = left_data.cmp(&right_data);
+ let reverse_cmp = right.cmp(&left);
+ let equal_cmp = left.cmp(&left);
+
+ assert_eq!(value_cmp, Ordering::Less, "{:?} < {:?}", left, right);
+ assert_eq!(
+ value_partial_cmp,
+ Some(Ordering::Less),
+ "{:?} < {:?}",
+ left,
+ right
+ );
+ assert_eq!(
+ data_cmp,
+ Ordering::Less,
+ "{:?}={} < {:?}={}",
+ left,
+ hex::encode(&left_data),
+ right,
+ hex::encode(&right_data)
+ );
+ assert_eq!(reverse_cmp, Ordering::Greater, "{:?} > {:?}", right, left);
+ assert_eq!(equal_cmp, Ordering::Equal, "{:?} = {:?}", left, left);
+ }
+}
+
+#[test]
+fn test_registered_label_with_private_decode_fail() {
+ let tests = vec![
+ ("43010203", "expected int/tstr"),
+ ("", "decode CBOR failure: Io(EndOfFile"),
+ ("09", "expected value in IANA or private use range"),
+ ("394e1f", "expected value in IANA or private use range"),
+ ];
+ for (label_data, err_msg) in tests.iter() {
+ let data = hex::decode(label_data).unwrap();
+ let result = RegisteredLabelWithPrivate::<iana::Algorithm>::from_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
+
+// The most negative integer value that can be encoded in CBOR is:
+// 0x3B (0b001_11011) 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF
+// which is -18_446_744_073_709_551_616 (-1 - 18_446_744_073_709_551_615).
+//
+// However, this crate uses `i64` for all integers, which cannot hold
+// negative values below `i64::MIN` (=-2^63 = 0x8000000000000000).
+const CBOR_NINT_MIN_HEX: &str = "3b7fffffffffffffff";
+const CBOR_NINT_OUT_OF_RANGE_HEX: &str = "3b8000000000000000";
+
+// The largest positive integer value that can be encoded in CBOR is:
+// 0x1B (0b000_11011) 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF
+// which is 18_446_744_073_709_551_615.
+//
+// However, this crate uses `i64` for all integers, which cannot hold
+// positive values above `i64::MAX` (=-2^63 - 1 = 0x7fffffffffffffff).
+const CBOR_INT_MAX_HEX: &str = "1b7fffffffffffffff";
+const CBOR_INT_OUT_OF_RANGE_HEX: &str = "1b8000000000000000";
+
+#[test]
+fn test_large_label_decode() {
+ let tests = vec![(CBOR_NINT_MIN_HEX, i64::MIN), (CBOR_INT_MAX_HEX, i64::MAX)];
+ for (label_data, want) in tests.iter() {
+ let data = hex::decode(label_data).unwrap();
+ let got = Label::from_slice(&data).unwrap();
+ assert_eq!(got, Label::Int(*want))
+ }
+}
+
+#[test]
+fn test_large_label_decode_fail() {
+ let tests = vec![
+ (CBOR_NINT_OUT_OF_RANGE_HEX, "out of range integer value"),
+ (CBOR_INT_OUT_OF_RANGE_HEX, "out of range integer value"),
+ ];
+ for (label_data, err_msg) in tests.iter() {
+ let data = hex::decode(label_data).unwrap();
+ let result = Label::from_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
+
+#[test]
+fn test_large_registered_label_decode_fail() {
+ let tests = vec![
+ (CBOR_NINT_OUT_OF_RANGE_HEX, "out of range integer value"),
+ (CBOR_INT_OUT_OF_RANGE_HEX, "out of range integer value"),
+ ];
+ for (label_data, err_msg) in tests.iter() {
+ let data = hex::decode(label_data).unwrap();
+ let result = RegisteredLabel::<crate::iana::HeaderParameter>::from_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
+
+#[test]
+fn test_large_registered_label_with_private_decode_fail() {
+ let tests = vec![
+ (CBOR_NINT_OUT_OF_RANGE_HEX, "out of range integer value"),
+ (CBOR_INT_OUT_OF_RANGE_HEX, "out of range integer value"),
+ ];
+ for (label_data, err_msg) in tests.iter() {
+ let data = hex::decode(label_data).unwrap();
+ let result = RegisteredLabelWithPrivate::<crate::iana::HeaderParameter>::from_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
diff --git a/src/context/mod.rs b/src/context/mod.rs
new file mode 100644
index 0000000..ee53afe
--- /dev/null
+++ b/src/context/mod.rs
@@ -0,0 +1,267 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+//! COSE_KDF_Context functionality.
+
+use crate::{
+ cbor::value::Value,
+ iana,
+ util::{cbor_type_error, AsCborValue, ValueTryAs},
+ Algorithm, CoseError, ProtectedHeader, Result,
+};
+use alloc::{vec, vec::Vec};
+use core::convert::TryInto;
+
+#[cfg(test)]
+mod tests;
+
+/// A nonce value.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum Nonce {
+ Bytes(Vec<u8>),
+ Integer(i64),
+}
+
+/// Structure representing a party involved in key derivation.
+///
+/// ```cddl
+/// PartyInfo = (
+/// identity : bstr / nil,
+/// nonce : bstr / int / nil,
+/// other : bstr / nil
+/// )
+/// ```
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+pub struct PartyInfo {
+ pub identity: Option<Vec<u8>>,
+ pub nonce: Option<Nonce>,
+ pub other: Option<Vec<u8>>,
+}
+
+impl crate::CborSerializable for PartyInfo {}
+
+impl AsCborValue for PartyInfo {
+ fn from_cbor_value(value: Value) -> Result<Self> {
+ let mut a = value.try_as_array()?;
+ if a.len() != 3 {
+ return Err(CoseError::UnexpectedItem("array", "array with 3 items"));
+ }
+
+ // Remove array elements in reverse order to avoid shifts.
+ Ok(Self {
+ other: match a.remove(2) {
+ Value::Null => None,
+ Value::Bytes(b) => Some(b),
+ v => return cbor_type_error(&v, "bstr / nil"),
+ },
+ nonce: match a.remove(1) {
+ Value::Null => None,
+ Value::Bytes(b) => Some(Nonce::Bytes(b)),
+ Value::Integer(u) => Some(Nonce::Integer(u.try_into()?)),
+ v => return cbor_type_error(&v, "bstr / int / nil"),
+ },
+ identity: match a.remove(0) {
+ Value::Null => None,
+ Value::Bytes(b) => Some(b),
+ v => return cbor_type_error(&v, "bstr / nil"),
+ },
+ })
+ }
+
+ fn to_cbor_value(self) -> Result<Value> {
+ Ok(Value::Array(vec![
+ match self.identity {
+ None => Value::Null,
+ Some(b) => Value::Bytes(b),
+ },
+ match self.nonce {
+ None => Value::Null,
+ Some(Nonce::Bytes(b)) => Value::Bytes(b),
+ Some(Nonce::Integer(i)) => Value::from(i),
+ },
+ match self.other {
+ None => Value::Null,
+ Some(b) => Value::Bytes(b),
+ },
+ ]))
+ }
+}
+
+/// Builder for [`PartyInfo`] objects.
+#[derive(Debug, Default)]
+pub struct PartyInfoBuilder(PartyInfo);
+
+impl PartyInfoBuilder {
+ builder! {PartyInfo}
+ builder_set_optional! {identity: Vec<u8>}
+ builder_set_optional! {nonce: Nonce}
+ builder_set_optional! {other: Vec<u8>}
+}
+
+/// Structure representing supplemental public information.
+///
+/// ```cddl
+/// SuppPubInfo : [
+/// keyDataLength : uint,
+/// protected : empty_or_serialized_map,
+/// ? other : bstr
+/// ],
+/// ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct SuppPubInfo {
+ pub key_data_length: u64,
+ pub protected: ProtectedHeader,
+ pub other: Option<Vec<u8>>,
+}
+
+impl crate::CborSerializable for SuppPubInfo {}
+
+impl AsCborValue for SuppPubInfo {
+ fn from_cbor_value(value: Value) -> Result<Self> {
+ let mut a = value.try_as_array()?;
+ if a.len() != 2 && a.len() != 3 {
+ return Err(CoseError::UnexpectedItem(
+ "array",
+ "array with 2 or 3 items",
+ ));
+ }
+
+ // Remove array elements in reverse order to avoid shifts.
+ Ok(Self {
+ other: {
+ if a.len() == 3 {
+ Some(a.remove(2).try_as_bytes()?)
+ } else {
+ None
+ }
+ },
+ protected: ProtectedHeader::from_cbor_bstr(a.remove(1))?,
+ key_data_length: a.remove(0).try_as_integer()?.try_into()?,
+ })
+ }
+
+ fn to_cbor_value(self) -> Result<Value> {
+ let mut v = vec![
+ Value::from(self.key_data_length),
+ self.protected.cbor_bstr()?,
+ ];
+ if let Some(other) = self.other {
+ v.push(Value::Bytes(other));
+ }
+ Ok(Value::Array(v))
+ }
+}
+
+/// Builder for [`SuppPubInfo`] objects.
+#[derive(Debug, Default)]
+pub struct SuppPubInfoBuilder(SuppPubInfo);
+
+impl SuppPubInfoBuilder {
+ builder! {SuppPubInfo}
+ builder_set! {key_data_length: u64}
+ builder_set_protected! {protected}
+ builder_set_optional! {other: Vec<u8>}
+}
+
+/// Structure representing a a key derivation context.
+/// ```cdl
+/// COSE_KDF_Context = [
+/// AlgorithmID : int / tstr,
+/// PartyUInfo : [ PartyInfo ],
+/// PartyVInfo : [ PartyInfo ],
+/// SuppPubInfo : [
+/// keyDataLength : uint,
+/// protected : empty_or_serialized_map,
+/// ? other : bstr
+/// ],
+/// ? SuppPrivInfo : bstr
+/// ]
+/// ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct CoseKdfContext {
+ algorithm_id: Algorithm,
+ party_u_info: PartyInfo,
+ party_v_info: PartyInfo,
+ supp_pub_info: SuppPubInfo,
+ supp_priv_info: Vec<Vec<u8>>,
+}
+
+impl crate::CborSerializable for CoseKdfContext {}
+
+impl AsCborValue for CoseKdfContext {
+ fn from_cbor_value(value: Value) -> Result<Self> {
+ let mut a = value.try_as_array()?;
+ if a.len() < 4 {
+ return Err(CoseError::UnexpectedItem(
+ "array",
+ "array with at least 4 items",
+ ));
+ }
+
+ // Remove array elements in reverse order to avoid shifts.
+ let mut supp_priv_info = Vec::with_capacity(a.len() - 4);
+ for i in (4..a.len()).rev() {
+ supp_priv_info.push(a.remove(i).try_as_bytes()?);
+ }
+ supp_priv_info.reverse();
+
+ Ok(Self {
+ supp_priv_info,
+ supp_pub_info: SuppPubInfo::from_cbor_value(a.remove(3))?,
+ party_v_info: PartyInfo::from_cbor_value(a.remove(2))?,
+ party_u_info: PartyInfo::from_cbor_value(a.remove(1))?,
+ algorithm_id: Algorithm::from_cbor_value(a.remove(0))?,
+ })
+ }
+
+ fn to_cbor_value(self) -> Result<Value> {
+ let mut v = vec![
+ self.algorithm_id.to_cbor_value()?,
+ self.party_u_info.to_cbor_value()?,
+ self.party_v_info.to_cbor_value()?,
+ self.supp_pub_info.to_cbor_value()?,
+ ];
+ for supp_priv_info in self.supp_priv_info {
+ v.push(Value::Bytes(supp_priv_info));
+ }
+ Ok(Value::Array(v))
+ }
+}
+
+/// Builder for [`CoseKdfContext`] objects.
+#[derive(Debug, Default)]
+pub struct CoseKdfContextBuilder(CoseKdfContext);
+
+impl CoseKdfContextBuilder {
+ builder! {CoseKdfContext}
+ builder_set! {party_u_info: PartyInfo}
+ builder_set! {party_v_info: PartyInfo}
+ builder_set! {supp_pub_info: SuppPubInfo}
+
+ /// Set the algorithm.
+ #[must_use]
+ pub fn algorithm(mut self, alg: iana::Algorithm) -> Self {
+ self.0.algorithm_id = Algorithm::Assigned(alg);
+ self
+ }
+
+ /// Add supplemental private info.
+ #[must_use]
+ pub fn add_supp_priv_info(mut self, supp_priv_info: Vec<u8>) -> Self {
+ self.0.supp_priv_info.push(supp_priv_info);
+ self
+ }
+}
diff --git a/src/context/tests.rs b/src/context/tests.rs
new file mode 100644
index 0000000..0ad2b36
--- /dev/null
+++ b/src/context/tests.rs
@@ -0,0 +1,391 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+use super::*;
+use crate::{iana, util::expect_err, CborSerializable, HeaderBuilder};
+use alloc::vec;
+
+#[test]
+fn test_context_encode() {
+ let tests = vec![
+ (
+ CoseKdfContext::default(),
+ concat!(
+ "84", // 4-tuple
+ "00", // int : reserved
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "82", "0040", // 2-tuple: [0, 0-bstr]
+ ),
+ ),
+ (
+ CoseKdfContextBuilder::new()
+ .algorithm(iana::Algorithm::A128GCM)
+ .build(),
+ concat!(
+ "84", // 4-tuple
+ "01", // int : AES-128-GCM
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "82", "0040", // 2-tuple: [0, 0-bstr]
+ ),
+ ),
+ (
+ CoseKdfContextBuilder::new()
+ .algorithm(iana::Algorithm::A128GCM)
+ .party_u_info(PartyInfoBuilder::new().identity(vec![]).build())
+ .build(),
+ concat!(
+ "84", // 4-tuple
+ "01", // int : AES-128-GCM
+ "83", "40f6f6", // 3-tuple: [0-bstr, nil, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "82", "0040", // 2-tuple: [0, 0-bstr]
+ ),
+ ),
+ (
+ CoseKdfContextBuilder::new()
+ .algorithm(iana::Algorithm::A128GCM)
+ .party_u_info(
+ PartyInfoBuilder::new()
+ .identity(vec![3, 6])
+ .nonce(Nonce::Integer(7))
+ .build(),
+ )
+ .build(),
+ concat!(
+ "84", // 4-tuple
+ "01", // int : AES-128-GCM
+ "83", "420306", "07f6", // 3-tuple: [2-bstr, int, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "82", "0040", // 2-tuple: [0, 0-bstr]
+ ),
+ ),
+ (
+ CoseKdfContextBuilder::new()
+ .algorithm(iana::Algorithm::A128GCM)
+ .party_u_info(
+ PartyInfoBuilder::new()
+ .identity(vec![3, 6])
+ .nonce(Nonce::Integer(-2))
+ .build(),
+ )
+ .build(),
+ concat!(
+ "84", // 4-tuple
+ "01", // int : AES-128-GCM
+ "83", "420306", "21f6", // 3-tuple: [2-bstr, nint, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "82", "0040", // 2-tuple: [0, 0-bstr]
+ ),
+ ),
+ (
+ CoseKdfContextBuilder::new()
+ .algorithm(iana::Algorithm::A128GCM)
+ .party_v_info(
+ PartyInfoBuilder::new()
+ .identity(vec![3, 6])
+ .nonce(Nonce::Bytes(vec![7, 3]))
+ .build(),
+ )
+ .build(),
+ concat!(
+ "84", // 4-tuple
+ "01", // int : AES-128-GCM
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "420306", "420703", "f6", // 3-tuple: [2-bstr, 2-bstr, nil]
+ "82", "0040", // 2-tuple: [0, 0-bstr]
+ ),
+ ),
+ (
+ CoseKdfContextBuilder::new()
+ .algorithm(iana::Algorithm::A128GCM)
+ .party_v_info(
+ PartyInfoBuilder::new()
+ .identity(vec![3, 6])
+ .other(vec![7, 3])
+ .build(),
+ )
+ .build(),
+ concat!(
+ "84", // 4-tuple
+ "01", // int : AES-128-GCM
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "420306", "f6", "420703", // 3-tuple: [2-bstr, nil, 2-bstr]
+ "82", "0040", // 2-tuple: [0, 0-bstr]
+ ),
+ ),
+ (
+ CoseKdfContextBuilder::new()
+ .supp_pub_info(
+ SuppPubInfoBuilder::new()
+ .key_data_length(10)
+ .protected(
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::A128GCM)
+ .build(),
+ )
+ .build(),
+ )
+ .build(),
+ concat!(
+ "84", // 4-tuple
+ "00", // int : reserved
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "82", "0a43", "a10101" // 2-tuple: [10, 3-bstr]
+ ),
+ ),
+ (
+ CoseKdfContextBuilder::new()
+ .supp_pub_info(
+ SuppPubInfoBuilder::new()
+ .key_data_length(10)
+ .other(vec![1, 3, 5])
+ .build(),
+ )
+ .build(),
+ concat!(
+ "84", // 4-tuple
+ "00", // int : reserved
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "0a40", "43010305", // 3-tuple: [10, 0-bstr, 3-bstr]
+ ),
+ ),
+ (
+ CoseKdfContextBuilder::new()
+ .add_supp_priv_info(vec![1, 2, 3])
+ .build(),
+ concat!(
+ "85", // 5-tuple
+ "00", // int : reserved
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "82", "0040", // 2-tuple: [0, 0-bstr]
+ "43", "010203", // 3-bstr
+ ),
+ ),
+ (
+ CoseKdfContextBuilder::new()
+ .add_supp_priv_info(vec![1, 2, 3])
+ .add_supp_priv_info(vec![2, 3])
+ .add_supp_priv_info(vec![3])
+ .build(),
+ concat!(
+ "87", // 7-tuple
+ "00", // int : reserved
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "82", "0040", // 2-tuple: [0, 0-bstr]
+ "43", "010203", // 3-bstr
+ "42", "0203", // 2-bstr
+ "41", "03", // 1-bstr
+ ),
+ ),
+ ];
+ for (i, (key, key_data)) in tests.iter().enumerate() {
+ let got = key.clone().to_vec().unwrap();
+ assert_eq!(*key_data, hex::encode(&got), "case {}", i);
+
+ let mut got = CoseKdfContext::from_slice(&got).unwrap();
+ got.supp_pub_info.protected.original_data = None;
+ assert_eq!(*key, got);
+ }
+}
+
+#[test]
+fn test_context_decode_fail() {
+ let tests = vec![
+ (
+ concat!(
+ "a2", // 2-map
+ "00", // int : reserved
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "82", "0040", // 2-tuple: [0, 0-bstr]
+ ),
+ "expected array",
+ ),
+ (
+ concat!(
+ "83", // 3-tuple
+ "00", // int : reserved
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ ),
+ "expected array with at least 4 items",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "00", // int : reserved
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ ),
+ "decode CBOR failure: Io(EndOfFile",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "08", // int : unassigned value
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "82", "0040", // 2-tuple: [0, 0-bstr]
+ ),
+ "expected value in IANA or private use range",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr : invalid
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "82", "0040", // 2-tuple: [0, 0-bstr]
+ ),
+ "expected int/tstr",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "00", // int : reserved
+ "a1", "f6f6", // 1-map
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "82", "0040", // 2-tuple: [0, 0-bstr]
+ ),
+ "expected array",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "00", // int : reserved
+ "84", "f6f6f6f6", // 4-tuple: [nil, nil, nil, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "82", "0040", // 2-tuple: [0, 0-bstr]
+ ),
+ "expected array with 3 items",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "00", // int : reserved
+ "83", "f660f6", // 3-tuple: [nil, 0-tstr, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "82", "0040", // 2-tuple: [0, 0-bstr]
+ ),
+ "expected bstr / int / nil",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "00", // int : reserved
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "f6f660", // 3-tuple: [nil, nil, 0-tstr]
+ "82", "0040", // 2-tuple: [0, 0-bstr]
+ ),
+ "expected bstr / nil",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "00", // int : reserved
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "60f6f6", // 3-tuple: [0-tstr, nil, nil]
+ "82", "0040", // 2-tuple: [0, 0-bstr]
+ ),
+ "expected bstr / nil",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "00", // int : reserved
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "a1", "0040", // 1-map: {0: 0-bstr}
+ ),
+ "expected array",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "00", // int : reserved
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "81", "00", // 2-tuple: [0]
+ ),
+ "expected array with 2 or 3 items",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "00", // int : reserved
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "82", "4040", // 2-tuple: [0-bstr, 0-bstr]
+ ),
+ "expected int",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "00", // int : reserved
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "82", "0060", // 2-tuple: [0, 0-tstr]
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "00", // int : reserved
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "004060", // 3-tuple: [0, 0-bstr, 0-tstr]
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "85", // 5-tuple
+ "00", // int : reserved
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+ "82", "0040", // 2-tuple: [0, 0-bstr]
+ "60", // 0-tstr
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "01", // int : AES-128-GCM
+ "83", // 3-tuple: [0-bstr, out-of-range int, nil]
+ "401b8000000000000000f6",
+ "83", // 3-tuple: [nil, nil, nil]
+ "f6f6f6",
+ "82", // 2-tuple: [0, 0-bstr]
+ "0040",
+ ),
+ "out of range integer value",
+ ),
+ ];
+ for (context_data, err_msg) in tests.iter() {
+ let data = hex::decode(context_data).unwrap();
+ let result = CoseKdfContext::from_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
diff --git a/src/encrypt/mod.rs b/src/encrypt/mod.rs
new file mode 100644
index 0000000..589f611
--- /dev/null
+++ b/src/encrypt/mod.rs
@@ -0,0 +1,505 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+//! COSE_Encrypt functionality.
+
+use crate::{
+ cbor,
+ cbor::value::Value,
+ iana,
+ util::{cbor_type_error, to_cbor_array, AsCborValue, ValueTryAs},
+ CoseError, Header, ProtectedHeader, Result,
+};
+use alloc::{borrow::ToOwned, vec, vec::Vec};
+
+#[cfg(test)]
+mod tests;
+
+/// Structure representing the recipient of encrypted data.
+///
+/// ```cddl
+/// COSE_Recipient = [
+/// Headers,
+/// ciphertext : bstr / nil,
+/// ? recipients : [+COSE_recipient]
+/// ]
+/// ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct CoseRecipient {
+ pub protected: ProtectedHeader,
+ pub unprotected: Header,
+ pub ciphertext: Option<Vec<u8>>,
+ pub recipients: Vec<CoseRecipient>,
+}
+
+impl crate::CborSerializable for CoseRecipient {}
+
+impl AsCborValue for CoseRecipient {
+ fn from_cbor_value(value: Value) -> Result<Self> {
+ let mut a = value.try_as_array()?;
+ if a.len() != 3 && a.len() != 4 {
+ return Err(CoseError::UnexpectedItem(
+ "array",
+ "array with 3 or 4 items",
+ ));
+ }
+
+ // Remove array elements in reverse order to avoid shifts.
+ let recipients = if a.len() == 4 {
+ a.remove(3)
+ .try_as_array_then_convert(CoseRecipient::from_cbor_value)?
+ } else {
+ Vec::new()
+ };
+
+ Ok(Self {
+ recipients,
+ ciphertext: match a.remove(2) {
+ Value::Bytes(b) => Some(b),
+ Value::Null => None,
+ v => return cbor_type_error(&v, "bstr / null"),
+ },
+ unprotected: Header::from_cbor_value(a.remove(1))?,
+ protected: ProtectedHeader::from_cbor_bstr(a.remove(0))?,
+ })
+ }
+
+ fn to_cbor_value(self) -> Result<Value> {
+ let mut v = vec![
+ self.protected.cbor_bstr()?,
+ self.unprotected.to_cbor_value()?,
+ match self.ciphertext {
+ None => Value::Null,
+ Some(b) => Value::Bytes(b),
+ },
+ ];
+ if !self.recipients.is_empty() {
+ v.push(to_cbor_array(self.recipients)?);
+ }
+ Ok(Value::Array(v))
+ }
+}
+
+impl CoseRecipient {
+ /// Decrypt the `ciphertext` value, using `cipher` to decrypt the cipher text and
+ /// combined AAD.
+ ///
+ /// # Panics
+ ///
+ /// This function will panic if no `ciphertext` is available. It will also panic
+ /// if the `context` parameter does not refer to a recipient context.
+ pub fn decrypt<F, E>(
+ &self,
+ context: EncryptionContext,
+ external_aad: &[u8],
+ cipher: F,
+ ) -> Result<Vec<u8>, E>
+ where
+ F: FnOnce(&[u8], &[u8]) -> Result<Vec<u8>, E>,
+ {
+ let ct = self.ciphertext.as_ref().unwrap(/* safe: documented */);
+ match context {
+ EncryptionContext::EncRecipient
+ | EncryptionContext::MacRecipient
+ | EncryptionContext::RecRecipient => {}
+ _ => panic!("unsupported encryption context {:?}", context), // safe: documented
+ }
+ let aad = enc_structure_data(context, self.protected.clone(), external_aad);
+ cipher(ct, &aad)
+ }
+}
+
+/// Builder for [`CoseRecipient`] objects.
+#[derive(Debug, Default)]
+pub struct CoseRecipientBuilder(CoseRecipient);
+
+impl CoseRecipientBuilder {
+ builder! {CoseRecipient}
+ builder_set_protected! {protected}
+ builder_set! {unprotected: Header}
+ builder_set_optional! {ciphertext: Vec<u8>}
+
+ /// Add a [`CoseRecipient`].
+ #[must_use]
+ pub fn add_recipient(mut self, recipient: CoseRecipient) -> Self {
+ self.0.recipients.push(recipient);
+ self
+ }
+
+ /// Calculate the ciphertext value, using `cipher` to generate the encrypted bytes from the
+ /// plaintext and combined AAD (in that order). Any protected header values should be set
+ /// before using this method.
+ ///
+ /// # Panics
+ ///
+ /// This function will panic if the `context` parameter does not refer to a recipient context.
+ #[must_use]
+ pub fn create_ciphertext<F>(
+ self,
+ context: EncryptionContext,
+ plaintext: &[u8],
+ external_aad: &[u8],
+ cipher: F,
+ ) -> Self
+ where
+ F: FnOnce(&[u8], &[u8]) -> Vec<u8>,
+ {
+ let aad = self.aad(context, external_aad);
+ self.ciphertext(cipher(plaintext, &aad))
+ }
+
+ /// Calculate the ciphertext value, using `cipher` to generate the encrypted bytes from the
+ /// plaintext and combined AAD (in that order). Any protected header values should be set
+ /// before using this method.
+ ///
+ /// # Panics
+ ///
+ /// This function will panic if the `context` parameter does not refer to a recipient context.
+ pub fn try_create_ciphertext<F, E>(
+ self,
+ context: EncryptionContext,
+ plaintext: &[u8],
+ external_aad: &[u8],
+ cipher: F,
+ ) -> Result<Self, E>
+ where
+ F: FnOnce(&[u8], &[u8]) -> Result<Vec<u8>, E>,
+ {
+ let aad = self.aad(context, external_aad);
+ Ok(self.ciphertext(cipher(plaintext, &aad)?))
+ }
+
+ /// Construct the combined AAD data needed for encryption. Any protected header values should be
+ /// set before using this method.
+ ///
+ /// # Panics
+ ///
+ /// This function will panic if the `context` parameter does not refer to a recipient context.
+ #[must_use]
+ fn aad(&self, context: EncryptionContext, external_aad: &[u8]) -> Vec<u8> {
+ match context {
+ EncryptionContext::EncRecipient
+ | EncryptionContext::MacRecipient
+ | EncryptionContext::RecRecipient => {}
+ _ => panic!("unsupported encryption context {:?}", context), // safe: documented
+ }
+ enc_structure_data(context, self.0.protected.clone(), external_aad)
+ }
+}
+
+/// Structure representing an encrypted object.
+///
+/// ```cddl
+/// COSE_Encrypt = [
+/// Headers,
+/// ciphertext : bstr / nil,
+/// recipients : [+COSE_recipient]
+/// ]
+/// ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct CoseEncrypt {
+ pub protected: ProtectedHeader,
+ pub unprotected: Header,
+ pub ciphertext: Option<Vec<u8>>,
+ pub recipients: Vec<CoseRecipient>,
+}
+
+impl crate::CborSerializable for CoseEncrypt {}
+
+impl crate::TaggedCborSerializable for CoseEncrypt {
+ const TAG: u64 = iana::CborTag::CoseEncrypt as u64;
+}
+
+impl AsCborValue for CoseEncrypt {
+ fn from_cbor_value(value: Value) -> Result<Self> {
+ let mut a = value.try_as_array()?;
+ if a.len() != 4 {
+ return Err(CoseError::UnexpectedItem("array", "array with 4 items"));
+ }
+
+ // Remove array elements in reverse order to avoid shifts.
+ let recipients = a
+ .remove(3)
+ .try_as_array_then_convert(CoseRecipient::from_cbor_value)?;
+ Ok(Self {
+ recipients,
+ ciphertext: match a.remove(2) {
+ Value::Bytes(b) => Some(b),
+ Value::Null => None,
+ v => return cbor_type_error(&v, "bstr"),
+ },
+ unprotected: Header::from_cbor_value(a.remove(1))?,
+ protected: ProtectedHeader::from_cbor_bstr(a.remove(0))?,
+ })
+ }
+
+ fn to_cbor_value(self) -> Result<Value> {
+ Ok(Value::Array(vec![
+ self.protected.cbor_bstr()?,
+ self.unprotected.to_cbor_value()?,
+ match self.ciphertext {
+ None => Value::Null,
+ Some(b) => Value::Bytes(b),
+ },
+ to_cbor_array(self.recipients)?,
+ ]))
+ }
+}
+
+impl CoseEncrypt {
+ /// Decrypt the `ciphertext` value, using `cipher` to decrypt the cipher text and
+ /// combined AAD.
+ ///
+ /// # Panics
+ ///
+ /// This function will panic if no `ciphertext` is available.
+ pub fn decrypt<F, E>(&self, external_aad: &[u8], cipher: F) -> Result<Vec<u8>, E>
+ where
+ F: FnOnce(&[u8], &[u8]) -> Result<Vec<u8>, E>,
+ {
+ let ct = self.ciphertext.as_ref().unwrap(/* safe: documented */);
+ let aad = enc_structure_data(
+ EncryptionContext::CoseEncrypt,
+ self.protected.clone(),
+ external_aad,
+ );
+ cipher(ct, &aad)
+ }
+}
+
+/// Builder for [`CoseEncrypt`] objects.
+#[derive(Debug, Default)]
+pub struct CoseEncryptBuilder(CoseEncrypt);
+
+impl CoseEncryptBuilder {
+ builder! {CoseEncrypt}
+ builder_set_protected! {protected}
+ builder_set! {unprotected: Header}
+ builder_set_optional! {ciphertext: Vec<u8>}
+
+ /// Calculate the ciphertext value, using `cipher` to generate the encrypted bytes from the
+ /// plaintext and combined AAD (in that order). Any protected header values should be set
+ /// before using this method.
+ #[must_use]
+ pub fn create_ciphertext<F>(self, plaintext: &[u8], external_aad: &[u8], cipher: F) -> Self
+ where
+ F: FnOnce(&[u8], &[u8]) -> Vec<u8>,
+ {
+ let aad = enc_structure_data(
+ EncryptionContext::CoseEncrypt,
+ self.0.protected.clone(),
+ external_aad,
+ );
+ self.ciphertext(cipher(plaintext, &aad))
+ }
+
+ /// Calculate the ciphertext value, using `cipher` to generate the encrypted bytes from the
+ /// plaintext and combined AAD (in that order). Any protected header values should be set
+ /// before using this method.
+ pub fn try_create_ciphertext<F, E>(
+ self,
+ plaintext: &[u8],
+ external_aad: &[u8],
+ cipher: F,
+ ) -> Result<Self, E>
+ where
+ F: FnOnce(&[u8], &[u8]) -> Result<Vec<u8>, E>,
+ {
+ let aad = enc_structure_data(
+ EncryptionContext::CoseEncrypt,
+ self.0.protected.clone(),
+ external_aad,
+ );
+ Ok(self.ciphertext(cipher(plaintext, &aad)?))
+ }
+
+ /// Add a [`CoseRecipient`].
+ #[must_use]
+ pub fn add_recipient(mut self, recipient: CoseRecipient) -> Self {
+ self.0.recipients.push(recipient);
+ self
+ }
+}
+
+/// Structure representing an encrypted object.
+///
+/// ```cddl
+/// COSE_Encrypt0 = [
+/// Headers,
+/// ciphertext : bstr / nil,
+/// ]
+/// ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct CoseEncrypt0 {
+ pub protected: ProtectedHeader,
+ pub unprotected: Header,
+ pub ciphertext: Option<Vec<u8>>,
+}
+
+impl crate::CborSerializable for CoseEncrypt0 {}
+
+impl crate::TaggedCborSerializable for CoseEncrypt0 {
+ const TAG: u64 = iana::CborTag::CoseEncrypt0 as u64;
+}
+
+impl AsCborValue for CoseEncrypt0 {
+ fn from_cbor_value(value: Value) -> Result<Self> {
+ let mut a = value.try_as_array()?;
+ if a.len() != 3 {
+ return Err(CoseError::UnexpectedItem("array", "array with 3 items"));
+ }
+
+ // Remove array elements in reverse order to avoid shifts.
+ Ok(Self {
+ ciphertext: match a.remove(2) {
+ Value::Bytes(b) => Some(b),
+ Value::Null => None,
+ v => return cbor_type_error(&v, "bstr"),
+ },
+
+ unprotected: Header::from_cbor_value(a.remove(1))?,
+ protected: ProtectedHeader::from_cbor_bstr(a.remove(0))?,
+ })
+ }
+
+ fn to_cbor_value(self) -> Result<Value> {
+ Ok(Value::Array(vec![
+ self.protected.cbor_bstr()?,
+ self.unprotected.to_cbor_value()?,
+ match self.ciphertext {
+ None => Value::Null,
+ Some(b) => Value::Bytes(b),
+ },
+ ]))
+ }
+}
+
+impl CoseEncrypt0 {
+ /// Decrypt the `ciphertext` value, using `cipher` to decrypt the cipher text and
+ /// combined AAD.
+ ///
+ /// # Panics
+ ///
+ /// This function will panic if no `ciphertext` is available.
+ pub fn decrypt<F, E>(&self, external_aad: &[u8], cipher: F) -> Result<Vec<u8>, E>
+ where
+ F: FnOnce(&[u8], &[u8]) -> Result<Vec<u8>, E>,
+ {
+ let ct = self.ciphertext.as_ref().unwrap(/* safe: documented */);
+ let aad = enc_structure_data(
+ EncryptionContext::CoseEncrypt0,
+ self.protected.clone(),
+ external_aad,
+ );
+ cipher(ct, &aad)
+ }
+}
+
+/// Builder for [`CoseEncrypt0`] objects.
+#[derive(Debug, Default)]
+pub struct CoseEncrypt0Builder(CoseEncrypt0);
+
+impl CoseEncrypt0Builder {
+ builder! {CoseEncrypt0}
+ builder_set_protected! {protected}
+ builder_set! {unprotected: Header}
+ builder_set_optional! {ciphertext: Vec<u8>}
+
+ /// Calculate the ciphertext value, using `cipher` to generate the encrypted bytes from the
+ /// plaintext and combined AAD (in that order). Any protected header values should be set
+ /// before using this method.
+ #[must_use]
+ pub fn create_ciphertext<F>(self, plaintext: &[u8], external_aad: &[u8], cipher: F) -> Self
+ where
+ F: FnOnce(&[u8], &[u8]) -> Vec<u8>,
+ {
+ let aad = enc_structure_data(
+ EncryptionContext::CoseEncrypt0,
+ self.0.protected.clone(),
+ external_aad,
+ );
+ self.ciphertext(cipher(plaintext, &aad))
+ }
+
+ /// Calculate the ciphertext value, using `cipher` to generate the encrypted bytes from the
+ /// plaintext and combined AAD (in that order). Any protected header values should be set
+ /// before using this method.
+ pub fn try_create_ciphertext<F, E>(
+ self,
+ plaintext: &[u8],
+ external_aad: &[u8],
+ cipher: F,
+ ) -> Result<Self, E>
+ where
+ F: FnOnce(&[u8], &[u8]) -> Result<Vec<u8>, E>,
+ {
+ let aad = enc_structure_data(
+ EncryptionContext::CoseEncrypt0,
+ self.0.protected.clone(),
+ external_aad,
+ );
+ Ok(self.ciphertext(cipher(plaintext, &aad)?))
+ }
+}
+
+/// Possible encryption contexts.
+#[derive(Clone, Copy, Debug)]
+pub enum EncryptionContext {
+ CoseEncrypt,
+ CoseEncrypt0,
+ EncRecipient,
+ MacRecipient,
+ RecRecipient,
+}
+
+impl EncryptionContext {
+ /// Return the context string as per RFC 8152 section 5.3.
+ fn text(&self) -> &'static str {
+ match self {
+ EncryptionContext::CoseEncrypt => "Encrypt",
+ EncryptionContext::CoseEncrypt0 => "Encrypt0",
+ EncryptionContext::EncRecipient => "Enc_Recipient",
+ EncryptionContext::MacRecipient => "Mac_Recipient",
+ EncryptionContext::RecRecipient => "Rec_Recipient",
+ }
+ }
+}
+
+/// Create a binary blob that will be signed.
+//
+/// ```cddl
+/// Enc_structure = [
+/// context : "Encrypt" / "Encrypt0" / "Enc_Recipient" /
+/// "Mac_Recipient" / "Rec_Recipient",
+/// protected : empty_or_serialized_map,
+/// external_aad : bstr
+/// ]
+/// ```
+pub fn enc_structure_data(
+ context: EncryptionContext,
+ protected: ProtectedHeader,
+ external_aad: &[u8],
+) -> Vec<u8> {
+ let arr = vec![
+ Value::Text(context.text().to_owned()),
+ protected.cbor_bstr().expect("failed to serialize header"), // safe: always serializable
+ Value::Bytes(external_aad.to_vec()),
+ ];
+
+ let mut data = Vec::new();
+ cbor::ser::into_writer(&Value::Array(arr), &mut data).unwrap(); // safe: always serializable
+ data
+}
diff --git a/src/encrypt/tests.rs b/src/encrypt/tests.rs
new file mode 100644
index 0000000..56e24c4
--- /dev/null
+++ b/src/encrypt/tests.rs
@@ -0,0 +1,1084 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+use super::*;
+use crate::{
+ cbor::value::Value, iana, util::expect_err, CborSerializable, ContentType, CoseKeyBuilder,
+ CoseRecipientBuilder, CoseSignatureBuilder, HeaderBuilder, TaggedCborSerializable,
+};
+use alloc::{
+ string::{String, ToString},
+ vec,
+ vec::Vec,
+};
+
+#[test]
+fn test_cose_recipient_decode() {
+ let tests: Vec<(CoseRecipient, &'static str)> = vec![
+ (
+ CoseRecipientBuilder::new().build(),
+ concat!(
+ "83", // 3-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "f6", // null
+ ),
+ ),
+ (
+ CoseRecipientBuilder::new().ciphertext(vec![]).build(),
+ concat!(
+ "83", // 3-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "40", // 0-bstr
+ ),
+ ),
+ (
+ CoseRecipientBuilder::new()
+ .ciphertext(vec![])
+ .add_recipient(CoseRecipientBuilder::new().build())
+ .build(),
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "40", // 0-bstr
+ "81", // 1-tuple
+ "83", // 3-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "f6", // null
+ ),
+ ),
+ ];
+
+ for (i, (recipient, recipient_data)) in tests.iter().enumerate() {
+ let got = recipient.clone().to_vec().unwrap();
+ assert_eq!(*recipient_data, hex::encode(&got), "case {}", i);
+
+ let mut got = CoseRecipient::from_slice(&got).unwrap();
+ got.protected.original_data = None;
+ for mut recip in &mut got.recipients {
+ recip.protected.original_data = None;
+ }
+ assert_eq!(*recipient, got);
+ }
+}
+
+#[test]
+fn test_cose_recipient_decode_fail() {
+ let tests = vec![
+ (
+ concat!(
+ "a2", // 2-map (should be tuple)
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "4161", // 1-bstr
+ "40", // 0-bstr
+ ),
+ "expected array",
+ ),
+ (
+ concat!(
+ "82", // 2-tuple (should be 4-tuple)
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ ),
+ "expected array with 3 or 4 items",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "80", // 0-tuple (should be bstr)
+ "a0", // 0-map
+ "40", // 0-bstr
+ "80", // 0-arr
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "40", // 0-bstr (should be map)
+ "40", // 0-bstr
+ "80", // 0-arr
+ ),
+ "expected map",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "60", // 0-tstr (should be bstr)
+ "80", // 0-arr
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "40", // 0-bstr
+ "40", // 0-bstr
+ ),
+ "expected array",
+ ),
+ ];
+ for (recipient_data, err_msg) in tests.iter() {
+ let data = hex::decode(recipient_data).unwrap();
+ let result = CoseRecipient::from_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
+
+#[test]
+fn test_cose_encrypt_decode() {
+ let tests: Vec<(CoseEncrypt, &'static str)> = vec![
+ (
+ CoseEncryptBuilder::new().build(),
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "f6", // null
+ "80", // 0-tuple
+ ),
+ ),
+ (
+ CoseEncryptBuilder::new().ciphertext(vec![]).build(),
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "40", // 0-bstr
+ "80", // 0-tuple
+ ),
+ ),
+ ];
+
+ for (i, (encrypt, encrypt_data)) in tests.iter().enumerate() {
+ let got = encrypt.clone().to_vec().unwrap();
+ assert_eq!(*encrypt_data, hex::encode(&got), "case {}", i);
+
+ let mut got = CoseEncrypt::from_slice(&got).unwrap();
+ got.protected.original_data = None;
+ assert_eq!(*encrypt, got);
+ }
+}
+
+#[test]
+fn test_cose_encrypt_decode_fail() {
+ let tests = vec![
+ (
+ concat!(
+ "a2", // 2-map (should be tuple)
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "4161", // 1-bstr
+ "40", // 0-bstr
+ ),
+ "expected array",
+ ),
+ (
+ concat!(
+ "83", // 3-tuple (should be 4-tuple)
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "40", // 0-bstr
+ ),
+ "expected array with 4 items",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "80", // 0-tuple (should be bstr)
+ "a0", // 0-map
+ "40", // 0-bstr
+ "80", // 0-arr
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "40", // 0-bstr (should be map)
+ "40", // 0-bstr
+ "80", // 0-arr
+ ),
+ "expected map",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "60", // 0-tstr (should be bstr)
+ "80", // 0-arr
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "40", // 0-bstr
+ "40", // 0-bstr
+ ),
+ "expected array",
+ ),
+ ];
+ for (encrypt_data, err_msg) in tests.iter() {
+ let data = hex::decode(encrypt_data).unwrap();
+ let result = CoseEncrypt::from_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
+#[test]
+fn test_rfc8152_cose_encrypt_decode() {
+ // COSE_Encrypt structures from RFC 8152 section C.3.
+ let tests: Vec<(CoseEncrypt, &'static str)> = vec![
+ (
+ CoseEncryptBuilder::new()
+ .protected(
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::A128GCM)
+ .build(),
+ )
+ .unprotected(
+ HeaderBuilder::new()
+ .iv(hex::decode("c9cf4df2fe6c632bf7886413").unwrap())
+ .build(),
+ )
+ .ciphertext(
+ hex::decode(
+ "7adbe2709ca818fb415f1e5df66f4e1a51053ba6d65a1a0c52a357da7a644b8070a151b0",
+ )
+ .unwrap(),
+ )
+ .add_recipient(
+ CoseRecipientBuilder::new()
+ .protected(
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ECDH_ES_HKDF_256)
+ .build(),
+ )
+ .unprotected(
+ HeaderBuilder::new()
+ .value(iana::HeaderAlgorithmParameter::EphemeralKey as i64,
+ CoseKeyBuilder::new_ec2_pub_key_y_sign(iana::EllipticCurve::P_256,
+ hex::decode("98f50a4ff6c05861c8860d13a638ea56c3f5ad7590bbfbf054e1c7b4d91d6280").unwrap(),
+ true)
+ .build().to_cbor_value().unwrap())
+ .key_id(b"meriadoc.brandybuck@buckland.example".to_vec())
+ .build(),
+ )
+ .ciphertext(vec![])
+ .build(),
+ )
+ .build(),
+ // Note: contents of maps have been re-ordered from the RFC to canonical ordering.
+ concat!(
+ "d860",
+ "84", "43", "a10101",
+ "a1", "05", "4c", "c9cf4df2fe6c632bf7886413",
+ "5824", "7adbe2709ca818fb415f1e5df66f4e1a51053ba6d65a1a0c52a357da7a644b8070a151b0",
+ "81",
+ "83",
+ "44", "a1013818",
+ "a2",
+ "04",
+ "5824", "6d65726961646f632e6272616e64796275636b406275636b6c616e642e6578616d706c65",
+ "20",
+ "a4",
+ "01", "02",
+ "20", "01",
+ "21", "5820", "98f50a4ff6c05861c8860d13a638ea56c3f5ad7590bbfbf054e1c7b4d91d6280",
+ "22", "f5",
+ "40",
+ ),
+ ),
+ (
+ CoseEncryptBuilder::new()
+ .protected(HeaderBuilder::new().algorithm(iana::Algorithm::AES_CCM_16_64_128).build())
+ .unprotected(HeaderBuilder::new().iv(hex::decode("89f52f65a1c580933b5261a76c").unwrap()).build())
+ .ciphertext(hex::decode("753548a19b1307084ca7b2056924ed95f2e3b17006dfe931b687b847").unwrap())
+ .add_recipient(CoseRecipientBuilder::new()
+ .protected(HeaderBuilder::new().algorithm(iana::Algorithm::Direct_HKDF_SHA_256).build())
+ .unprotected(
+ HeaderBuilder::new()
+ .key_id(b"our-secret".to_vec())
+ .value(iana::HeaderAlgorithmParameter::Salt as i64,
+ Value::Bytes(b"aabbccddeeffgghh".to_vec()))
+ .build())
+ .ciphertext(vec![])
+ .build())
+ .build(),
+ // Note: contents of maps have been re-ordered from the RFC to canonical ordering.
+ concat!(
+ "d860",
+ "84",
+ "43",
+ "a1010a",
+ "a1",
+ "05",
+ "4d", "89f52f65a1c580933b5261a76c",
+ "581c", "753548a19b1307084ca7b2056924ed95f2e3b17006dfe931b687b847",
+ "81",
+ "83",
+ "43",
+ "a10129",
+ "a2",
+ "04", "4a", "6f75722d736563726574",
+ "33", "50", "61616262636364646565666667676868",
+ "40",
+ ),
+ ),
+
+ (
+ CoseEncryptBuilder::new()
+ .protected(HeaderBuilder::new().algorithm(iana::Algorithm::A128GCM).build())
+ .unprotected(HeaderBuilder::new()
+ .iv(hex::decode("c9cf4df2fe6c632bf7886413").unwrap())
+ .add_counter_signature(CoseSignatureBuilder::new()
+ .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ES512).build())
+ .unprotected(HeaderBuilder::new().key_id(b"bilbo.baggins@hobbiton.example".to_vec()).build())
+ .signature(hex::decode("00929663c8789bb28177ae28467e66377da12302d7f9594d2999afa5dfa531294f8896f2b6cdf1740014f4c7f1a358e3a6cf57f4ed6fb02fcf8f7aa989f5dfd07f0700a3a7d8f3c604ba70fa9411bd10c2591b483e1d2c31de003183e434d8fba18f17a4c7e3dfa003ac1cf3d30d44d2533c4989d3ac38c38b71481cc3430c9d65e7ddff").unwrap())
+ .build())
+ .build())
+ .ciphertext(hex::decode("7adbe2709ca818fb415f1e5df66f4e1a51053ba6d65a1a0c52a357da7a644b8070a151b0").unwrap())
+ .add_recipient(CoseRecipientBuilder::new()
+ .protected(
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ECDH_ES_HKDF_256)
+ .build())
+ .unprotected(
+ HeaderBuilder::new()
+ .value(iana::HeaderAlgorithmParameter::EphemeralKey as i64,
+ CoseKeyBuilder::new_ec2_pub_key_y_sign(iana::EllipticCurve::P_256,
+ hex::decode("98f50a4ff6c05861c8860d13a638ea56c3f5ad7590bbfbf054e1c7b4d91d6280").unwrap(),
+ true)
+ .build().to_cbor_value().unwrap())
+ .key_id(b"meriadoc.brandybuck@buckland.example".to_vec())
+ .build())
+ .ciphertext(vec![])
+ .build())
+ .build(),
+ // Note: contents of maps have been re-ordered from the RFC to canonical ordering.
+ concat!(
+ "d860",
+ "84",
+ "43",
+ "a10101",
+ "a2",
+ "05",
+ "4c", "c9cf4df2fe6c632bf7886413",
+ "07",
+ "83",
+ "44",
+ "a1013823",
+ "a1",
+ "04",
+ "581e", "62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c65",
+ "5884",
+ "00929663c8789bb28177ae28467e66377da12302d7f9594d2999afa5dfa531294f8896f2b6cdf1740014f4c7f1a358e3a6cf57f4ed6fb02fcf8f7aa989f5dfd07f0700a3a7d8f3c604ba70fa9411bd10c2591b483e1d2c31de003183e434d8fba18f17a4c7e3dfa003ac1cf3d30d44d2533c4989d3ac38c38b71481cc3430c9d65e7ddff",
+ "5824",
+ "7adbe2709ca818fb415f1e5df66f4e1a51053ba6d65a1a0c52a357da7a644b8070a151b0",
+ "81",
+ "83",
+ "44", "a1013818",
+ "a2",
+ "04",
+ "5824", "6d65726961646f632e6272616e64796275636b406275636b6c616e642e6578616d706c65",
+ "20",
+ "a4",
+ "01",
+ "02",
+ "20",
+ "01",
+ "21",
+ "5820", "98f50a4ff6c05861c8860d13a638ea56c3f5ad7590bbfbf054e1c7b4d91d6280",
+ "22",
+ "f5",
+ "40",
+ ),
+ ),
+ (
+ CoseEncryptBuilder::new()
+ .protected(HeaderBuilder::new().algorithm(iana::Algorithm::A128GCM).build())
+ .unprotected(HeaderBuilder::new().iv(hex::decode("02d1f7e6f26c43d4868d87ce").unwrap()).build())
+ .ciphertext(hex::decode("64f84d913ba60a76070a9a48f26e97e863e28529d8f5335e5f0165eee976b4a5f6c6f09d").unwrap())
+ .add_recipient(CoseRecipientBuilder::new()
+ .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ECDH_SS_A128KW).build())
+ .unprotected(HeaderBuilder::new()
+ .key_id(b"meriadoc.brandybuck@buckland.example".to_vec())
+ .value(
+ iana::HeaderAlgorithmParameter::StaticKeyId as i64,
+ Value::Bytes(b"peregrin.took@tuckborough.example".to_vec())
+ )
+ .value(
+ iana::HeaderAlgorithmParameter::PartyUNonce as i64,
+ Value::Bytes(hex::decode("0101").unwrap())
+ )
+ .build())
+ .ciphertext(hex::decode("41e0d76f579dbd0d936a662d54d8582037de2e366fde1c62").unwrap())
+ .build())
+ .build(),
+ // Note: contents of maps have been re-ordered from the RFC to canonical ordering.
+ concat!(
+ "d860",
+ "84",
+ "43",
+ "a10101",
+ "a1",
+ "05",
+ "4c", "02d1f7e6f26c43d4868d87ce",
+ "5824", "64f84d913ba60a76070a9a48f26e97e863e28529d8f5335e5f0165eee976b4a5f6c6f09d",
+ "81",
+ "83",
+ "44", "a101381f",
+ "a3",
+ "04",
+ "5824", "6d65726961646f632e6272616e64796275636b406275636b6c616e642e6578616d706c65",
+ "22",
+ "5821", "706572656772696e2e746f6f6b407475636b626f726f7567682e6578616d706c65",
+ "35",
+ "42",
+ "0101",
+ "5818", "41e0d76f579dbd0d936a662d54d8582037de2e366fde1c62",
+ ),
+ ),
+ ];
+
+ for (i, (encrypt, encrypt_data)) in tests.iter().enumerate() {
+ let got = encrypt.clone().to_tagged_vec().unwrap();
+ assert_eq!(*encrypt_data, hex::encode(&got), "case {}", i);
+
+ let mut got = CoseEncrypt::from_tagged_slice(&got).unwrap();
+ got.protected.original_data = None;
+ for mut recip in &mut got.recipients {
+ recip.protected.original_data = None;
+ }
+ for mut sig in &mut got.unprotected.counter_signatures {
+ sig.protected.original_data = None;
+ }
+ assert_eq!(*encrypt, got);
+ }
+}
+
+#[test]
+fn test_cose_encrypt0_decode() {
+ let tests: Vec<(CoseEncrypt0, &'static str)> = vec![
+ (
+ CoseEncrypt0Builder::new().build(),
+ concat!(
+ "83", // 3-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "f6", // null
+ ),
+ ),
+ (
+ CoseEncrypt0Builder::new().ciphertext(vec![]).build(),
+ concat!(
+ "83", // 3-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "40", // 0-bstr
+ ),
+ ),
+ ];
+
+ for (i, (encrypt, encrypt_data)) in tests.iter().enumerate() {
+ let got = encrypt.clone().to_vec().unwrap();
+ assert_eq!(*encrypt_data, hex::encode(&got), "case {}", i);
+
+ let mut got = CoseEncrypt0::from_slice(&got).unwrap();
+ got.protected.original_data = None;
+ assert_eq!(*encrypt, got);
+ }
+}
+
+#[test]
+fn test_cose_encrypt0_decode_fail() {
+ let tests = vec![
+ (
+ concat!(
+ "a2", // 2-map (should be tuple)
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "4100", // 1-bstr
+ "40", // 0-bstr
+ ),
+ "expected array",
+ ),
+ (
+ concat!(
+ "82", // 2-tuple (should be 3-tuple)
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ ),
+ "expected array with 3 items",
+ ),
+ (
+ concat!(
+ "83", // 3-tuple
+ "80", // 0-tuple (should be bstr)
+ "a0", // 0-map
+ "40", // 0-bstr
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "83", // 3-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "40", // 0-bstr (should be map)
+ "40", // 0-bstr
+ ),
+ "expected map",
+ ),
+ (
+ concat!(
+ "83", // 3-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "60", // 0-tstr (should be bstr)
+ ),
+ "expected bstr",
+ ),
+ ];
+ for (encrypt_data, err_msg) in tests.iter() {
+ let data = hex::decode(encrypt_data).unwrap();
+ let result = CoseEncrypt0::from_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
+
+#[test]
+fn test_rfc8152_cose_encrypt0_decode() {
+ // COSE_Encrypt0 structures from RFC 8152 section C.4.
+ let tests: Vec<(CoseEncrypt0, &'static str)> = vec![
+ (
+ CoseEncrypt0Builder::new()
+ .protected(
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::AES_CCM_16_64_128)
+ .build(),
+ )
+ .unprotected(
+ HeaderBuilder::new()
+ .iv(hex::decode("89f52f65a1c580933b5261a78c").unwrap())
+ .build(),
+ )
+ .ciphertext(
+ hex::decode("5974e1b99a3a4cc09a659aa2e9e7fff161d38ce71cb45ce460ffb569")
+ .unwrap(),
+ )
+ .build(),
+ concat!(
+ "d0",
+ "83",
+ "43",
+ "a1010a",
+ "a1",
+ "05",
+ "4d",
+ "89f52f65a1c580933b5261a78c",
+ "581c",
+ "5974e1b99a3a4cc09a659aa2e9e7fff161d38ce71cb45ce460ffb569",
+ ),
+ ),
+ (
+ CoseEncrypt0Builder::new()
+ .protected(
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::AES_CCM_16_64_128)
+ .build(),
+ )
+ .unprotected(
+ HeaderBuilder::new()
+ .partial_iv(hex::decode("61a7").unwrap())
+ .build(),
+ )
+ .ciphertext(
+ hex::decode("252a8911d465c125b6764739700f0141ed09192de139e053bd09abca")
+ .unwrap(),
+ )
+ .build(),
+ concat!(
+ "d0",
+ "83",
+ "43",
+ "a1010a",
+ "a1",
+ "06",
+ "42",
+ "61a7",
+ "581c",
+ "252a8911d465c125b6764739700f0141ed09192de139e053bd09abca",
+ ),
+ ),
+ ];
+
+ for (i, (encrypt, encrypt_data)) in tests.iter().enumerate() {
+ let got = encrypt.clone().to_tagged_vec().unwrap();
+ assert_eq!(*encrypt_data, hex::encode(&got), "case {}", i);
+
+ let mut got = CoseEncrypt0::from_tagged_slice(&got).unwrap();
+ got.protected.original_data = None;
+ assert_eq!(*encrypt, got);
+ }
+}
+
+struct FakeCipher {}
+
+impl FakeCipher {
+ fn encrypt(&self, plaintext: &[u8], additional_data: &[u8]) -> Result<Vec<u8>, String> {
+ let mut result = vec![];
+ result.extend_from_slice(&(plaintext.len() as u32).to_be_bytes());
+ result.extend_from_slice(plaintext);
+ result.extend_from_slice(additional_data);
+ Ok(result)
+ }
+
+ fn decrypt(&self, ciphertext: &[u8], additional_data: &[u8]) -> Result<Vec<u8>, String> {
+ if ciphertext.len() < 4 {
+ return Err("not long enough".to_owned());
+ }
+ let pt_len =
+ u32::from_be_bytes([ciphertext[0], ciphertext[1], ciphertext[2], ciphertext[3]])
+ as usize;
+ let pt = &ciphertext[4..4 + pt_len];
+ let recovered_aad = &ciphertext[4 + pt_len..];
+ if recovered_aad != additional_data {
+ return Err("aad doesn't match".to_owned());
+ }
+ Ok(pt.to_vec())
+ }
+ fn fail_encrypt(&self, _plaintext: &[u8], _additional_data: &[u8]) -> Result<Vec<u8>, String> {
+ Err("failed".to_string())
+ }
+}
+
+#[test]
+fn test_cose_recipient_roundtrip() {
+ let pt = b"This is the plaintext";
+ let external_aad = b"This is the external aad";
+ let cipher = FakeCipher {};
+
+ for context in &[
+ EncryptionContext::EncRecipient,
+ EncryptionContext::MacRecipient,
+ EncryptionContext::RecRecipient,
+ ] {
+ let protected = HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ES256)
+ .key_id(b"11".to_vec())
+ .build();
+
+ let mut recipient = CoseRecipientBuilder::new()
+ .protected(protected)
+ .create_ciphertext(*context, pt, external_aad, |pt, aad| {
+ cipher.encrypt(pt, aad).unwrap()
+ })
+ .build();
+
+ let recovered_pt = recipient
+ .decrypt(*context, external_aad, |ct, aad| cipher.decrypt(ct, aad))
+ .unwrap();
+ assert_eq!(&pt[..], recovered_pt);
+
+ // Changing an unprotected header leaves the ciphertext decipherable.
+ recipient.unprotected.content_type = Some(ContentType::Text("text/plain".to_owned()));
+ assert!(recipient
+ .decrypt(*context, external_aad, |ct, aad| {
+ cipher.decrypt(ct, aad)
+ })
+ .is_ok());
+
+ // Providing a different `aad` means the ciphertext won't validate.
+ assert!(recipient
+ .decrypt(*context, b"not aad", |ct, aad| { cipher.decrypt(ct, aad) })
+ .is_err());
+
+ // Changing a protected header invalidates the ciphertext.
+ recipient.protected = ProtectedHeader::default();
+ assert!(recipient
+ .decrypt(*context, external_aad, |ct, aad| {
+ cipher.decrypt(ct, aad)
+ })
+ .is_err());
+ }
+}
+
+#[test]
+fn test_cose_recipient_noncanonical() {
+ let pt = b"aa";
+ let aad = b"bb";
+ let cipher = FakeCipher {};
+ let context = EncryptionContext::EncRecipient;
+
+ // Build an empty protected header from a non-canonical input of 41a0 rather than 40.
+ let protected = ProtectedHeader::from_cbor_bstr(Value::Bytes(vec![0xa0])).unwrap();
+ assert_eq!(protected.header, Header::default());
+ assert_eq!(protected.original_data, Some(vec![0xa0]));
+
+ let mut recipient = CoseRecipient {
+ protected: protected.clone(),
+ ..Default::default()
+ };
+ let internal_aad = crate::encrypt::enc_structure_data(context, protected, aad);
+ recipient.ciphertext = Some(cipher.encrypt(pt, &internal_aad).unwrap());
+
+ // Deciphering the ciphertext should still succeed, because the `ProtectedHeader`
+ // includes the wire data and uses it for building the decryption input.
+ let recovered_pt = recipient
+ .decrypt(context, aad, |ct, aad| cipher.decrypt(ct, aad))
+ .unwrap();
+ assert_eq!(&pt[..], recovered_pt);
+
+ // However, if we attempt to build the same decryption inputs by hand (thus not including the
+ // non-canonical wire data)...
+ let recreated_recipient = CoseRecipientBuilder::new()
+ .ciphertext(recipient.ciphertext.unwrap())
+ .build();
+
+ // ...then the transplanted cipher text will not decipher, because the re-building of the
+ // inputs will use the canonical encoding of the protected header, which is not what was
+ // originally used for the input.
+ assert!(recreated_recipient
+ .decrypt(context, aad, |ct, aad| cipher.decrypt(ct, aad))
+ .is_err());
+}
+
+#[test]
+fn test_cose_recipient_result() {
+ let pt = b"This is the plaintext";
+ let external_aad = b"This is the external aad";
+ let cipher = FakeCipher {};
+
+ let protected = HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ES256)
+ .key_id(b"11".to_vec())
+ .build();
+ let _recipient = CoseRecipientBuilder::new()
+ .protected(protected.clone())
+ .try_create_ciphertext(
+ EncryptionContext::EncRecipient,
+ pt,
+ external_aad,
+ |pt, aad| cipher.encrypt(pt, aad),
+ )
+ .unwrap()
+ .build();
+ let status = CoseRecipientBuilder::new()
+ .protected(protected)
+ .try_create_ciphertext(
+ EncryptionContext::EncRecipient,
+ pt,
+ external_aad,
+ |pt, aad| cipher.fail_encrypt(pt, aad),
+ );
+ expect_err(status, "failed");
+}
+
+#[test]
+#[should_panic]
+fn test_cose_recipient_missing_ciphertext() {
+ let external_aad = b"This is the external aad";
+ let cipher = FakeCipher {};
+
+ let recipient = CoseRecipient::default();
+
+ // No ciphertext has been set, do decryption will panic.
+ let _result = recipient.decrypt(EncryptionContext::EncRecipient, external_aad, |ct, aad| {
+ cipher.decrypt(ct, aad)
+ });
+}
+
+#[test]
+#[should_panic]
+fn test_cose_recipient_builder_invalid_context() {
+ let pt = b"This is the plaintext";
+ let external_aad = b"This is the external aad";
+ let cipher = FakeCipher {};
+
+ // Can't use a non-recipient context.
+ let _recipient = CoseRecipientBuilder::new()
+ .create_ciphertext(
+ EncryptionContext::CoseEncrypt,
+ pt,
+ external_aad,
+ |pt, aad| cipher.encrypt(pt, aad).unwrap(),
+ )
+ .build();
+}
+
+#[test]
+#[should_panic]
+fn test_cose_recipient_decrypt_invalid_context() {
+ let pt = b"This is the plaintext";
+ let external_aad = b"This is the external aad";
+ let cipher = FakeCipher {};
+
+ let recipient = CoseRecipientBuilder::new()
+ .create_ciphertext(
+ EncryptionContext::EncRecipient,
+ pt,
+ external_aad,
+ |pt, aad| cipher.encrypt(pt, aad).unwrap(),
+ )
+ .build();
+
+ // Can't use a non-recipient context.
+ let _result = recipient.decrypt(EncryptionContext::CoseEncrypt, external_aad, |ct, aad| {
+ cipher.decrypt(ct, aad)
+ });
+}
+
+#[test]
+fn test_cose_encrypt_roundtrip() {
+ let pt = b"This is the plaintext";
+ let external_aad = b"This is the external aad";
+ let cipher = FakeCipher {};
+
+ let protected = HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ES256)
+ .key_id(b"11".to_vec())
+ .build();
+ let mut encrypt = CoseEncryptBuilder::new()
+ .protected(protected)
+ .create_ciphertext(pt, external_aad, |pt, aad| cipher.encrypt(pt, aad).unwrap())
+ .build();
+
+ let recovered_pt = encrypt
+ .decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad))
+ .unwrap();
+ assert_eq!(&pt[..], recovered_pt);
+
+ // Changing an unprotected header leaves the ciphertext decipherable.
+ encrypt.unprotected.content_type = Some(ContentType::Text("text/plain".to_owned()));
+ assert!(encrypt
+ .decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad))
+ .is_ok());
+
+ // Providing a different `aad` means the signature won't validate.
+ assert!(encrypt
+ .decrypt(b"not aad", |ct, aad| cipher.decrypt(ct, aad))
+ .is_err());
+
+ // Changing a protected header invalidates the ciphertext.
+ encrypt.protected = ProtectedHeader::default();
+ assert!(encrypt
+ .decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad))
+ .is_err());
+}
+
+#[test]
+fn test_cose_encrypt_noncanonical() {
+ let pt = b"aa";
+ let external_aad = b"bb";
+ let cipher = FakeCipher {};
+
+ // Build an empty protected header from a non-canonical input of 41a0 rather than 40.
+ let protected = ProtectedHeader::from_cbor_bstr(Value::Bytes(vec![0xa0])).unwrap();
+ assert_eq!(protected.header, Header::default());
+ assert_eq!(protected.original_data, Some(vec![0xa0]));
+
+ let mut encrypt = CoseEncrypt {
+ protected: protected.clone(),
+ ..Default::default()
+ };
+ let aad = enc_structure_data(
+ EncryptionContext::CoseEncrypt,
+ protected.clone(),
+ external_aad,
+ );
+ encrypt.ciphertext = Some(cipher.encrypt(pt, &aad).unwrap());
+
+ // Deciphering the ciphertext should still succeed, because the `ProtectedHeader`
+ // includes the wire data and uses it for building the decryption input.
+ let recovered_pt = encrypt
+ .decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad))
+ .unwrap();
+ assert_eq!(&pt[..], recovered_pt);
+
+ // However, if we attempt to build the same decryption inputs by hand (thus not including the
+ // non-canonical wire data)...
+ let recreated_encrypt = CoseEncryptBuilder::new()
+ .protected(protected.header)
+ .ciphertext(encrypt.ciphertext.unwrap())
+ .build();
+
+ // ...then the transplanted cipher text will not decipher, because the re-building of the
+ // inputs will use the canonical encoding of the protected header, which is not what was
+ // originally used for the input.
+ assert!(recreated_encrypt
+ .decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad))
+ .is_err());
+}
+
+#[test]
+fn test_cose_encrypt_status() {
+ let pt = b"This is the plaintext";
+ let external_aad = b"This is the external aad";
+ let cipher = FakeCipher {};
+
+ let protected = HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ES256)
+ .key_id(b"11".to_vec())
+ .build();
+ let _encrypt = CoseEncryptBuilder::new()
+ .protected(protected.clone())
+ .try_create_ciphertext(pt, external_aad, |pt, aad| cipher.encrypt(pt, aad))
+ .unwrap()
+ .build();
+ let status = CoseEncryptBuilder::new()
+ .protected(protected)
+ .try_create_ciphertext(pt, external_aad, |pt, aad| cipher.fail_encrypt(pt, aad));
+ expect_err(status, "failed");
+}
+
+#[test]
+#[should_panic]
+fn test_cose_encrypt_missing_ciphertext() {
+ let external_aad = b"This is the external aad";
+ let cipher = FakeCipher {};
+
+ let encrypt = CoseEncrypt::default();
+
+ // No ciphertext has been set, do decryption will panic.
+ let _result = encrypt.decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad));
+}
+
+#[test]
+fn test_cose_encrypt0_roundtrip() {
+ let pt = b"This is the plaintext";
+ let external_aad = b"This is the external aad";
+ let cipher = FakeCipher {};
+
+ let protected = HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ES256)
+ .key_id(b"11".to_vec())
+ .build();
+ let mut encrypt = CoseEncrypt0Builder::new()
+ .protected(protected)
+ .create_ciphertext(pt, external_aad, |pt, aad| cipher.encrypt(pt, aad).unwrap())
+ .build();
+
+ let recovered_pt = encrypt
+ .decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad))
+ .unwrap();
+ assert_eq!(&pt[..], recovered_pt);
+
+ // Changing an unprotected header leaves the ciphertext decipherable.
+ encrypt.unprotected.content_type = Some(ContentType::Text("text/plain".to_owned()));
+ assert!(encrypt
+ .decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad))
+ .is_ok());
+
+ // Providing a different `aad` means the ciphertext won't decrypt.
+ assert!(encrypt
+ .decrypt(b"not aad", |ct, aad| cipher.decrypt(ct, aad))
+ .is_err());
+
+ // Changing a protected header invalidates the ciphertext.
+ encrypt.protected = ProtectedHeader::default();
+ assert!(encrypt
+ .decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad))
+ .is_err());
+}
+
+#[test]
+fn test_cose_encrypt0_noncanonical() {
+ let pt = b"aa";
+ let external_aad = b"bb";
+ let cipher = FakeCipher {};
+
+ // Build an empty protected header from a non-canonical input of 41a0 rather than 40.
+ let protected = ProtectedHeader::from_cbor_bstr(Value::Bytes(vec![0xa0])).unwrap();
+ assert_eq!(protected.header, Header::default());
+ assert_eq!(protected.original_data, Some(vec![0xa0]));
+
+ let mut encrypt = CoseEncrypt0 {
+ protected: protected.clone(),
+ ..Default::default()
+ };
+ let aad = enc_structure_data(
+ EncryptionContext::CoseEncrypt0,
+ protected.clone(),
+ external_aad,
+ );
+ encrypt.ciphertext = Some(cipher.encrypt(pt, &aad).unwrap());
+
+ // Deciphering the ciphertext should still succeed, because the `ProtectedHeader`
+ // includes the wire data and uses it for building the decryption input.
+ let recovered_pt = encrypt
+ .decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad))
+ .unwrap();
+ assert_eq!(&pt[..], recovered_pt);
+
+ // However, if we attempt to build the same decryption inputs by hand (thus not including the
+ // non-canonical wire data)...
+ let recreated_encrypt = CoseEncrypt0Builder::new()
+ .protected(protected.header)
+ .ciphertext(encrypt.ciphertext.unwrap())
+ .build();
+
+ // ...then the transplanted cipher text will not decipher, because the re-building of the
+ // inputs will use the canonical encoding of the protected header, which is not what was
+ // originally used for the input.
+ assert!(recreated_encrypt
+ .decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad))
+ .is_err());
+}
+#[test]
+fn test_cose_encrypt0_status() {
+ let pt = b"This is the plaintext";
+ let external_aad = b"This is the external aad";
+ let cipher = FakeCipher {};
+
+ let protected = HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ES256)
+ .key_id(b"11".to_vec())
+ .build();
+ let _encrypt = CoseEncrypt0Builder::new()
+ .protected(protected.clone())
+ .try_create_ciphertext(pt, external_aad, |pt, aad| cipher.encrypt(pt, aad))
+ .unwrap()
+ .build();
+ let status = CoseEncrypt0Builder::new()
+ .protected(protected)
+ .try_create_ciphertext(pt, external_aad, |pt, aad| cipher.fail_encrypt(pt, aad));
+ expect_err(status, "failed");
+}
+
+#[test]
+#[should_panic]
+fn test_cose_encrypt0_missing_ciphertext() {
+ let external_aad = b"This is the external aad";
+ let cipher = FakeCipher {};
+
+ let encrypt = CoseEncrypt0::default();
+
+ // No ciphertext has been set, do decryption will panic.
+ let _result = encrypt.decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad));
+}
diff --git a/src/header/mod.rs b/src/header/mod.rs
new file mode 100644
index 0000000..aa11174
--- /dev/null
+++ b/src/header/mod.rs
@@ -0,0 +1,406 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+//! COSE Headers functionality.
+
+use crate::{
+ cbor::value::Value,
+ iana,
+ iana::EnumI64,
+ util::{cbor_type_error, to_cbor_array, AsCborValue, ValueTryAs},
+ Algorithm, CborSerializable, CoseError, CoseSignature, Label, RegisteredLabel, Result,
+};
+use alloc::{collections::BTreeSet, string::String, vec, vec::Vec};
+
+#[cfg(test)]
+mod tests;
+
+/// Content type.
+pub type ContentType = crate::RegisteredLabel<iana::CoapContentFormat>;
+
+/// Structure representing a common COSE header map.
+///
+/// ```cddl
+/// header_map = {
+/// Generic_Headers,
+/// * label => values
+/// }
+///
+/// Generic_Headers = (
+/// ? 1 => int / tstr, ; algorithm identifier
+/// ? 2 => [+label], ; criticality
+/// ? 3 => tstr / int, ; content type
+/// ? 4 => bstr, ; key identifier
+/// ? 5 => bstr, ; IV
+/// ? 6 => bstr, ; Partial IV
+/// ? 7 => COSE_Signature / [+COSE_Signature] ; Counter signature
+/// )
+/// ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct Header {
+ /// Cryptographic algorithm to use
+ pub alg: Option<Algorithm>,
+ /// Critical headers to be understood
+ pub crit: Vec<RegisteredLabel<iana::HeaderParameter>>,
+ /// Content type of the payload
+ pub content_type: Option<ContentType>,
+ /// Key identifier.
+ pub key_id: Vec<u8>,
+ /// Full initialization vector
+ pub iv: Vec<u8>,
+ /// Partial initialization vector
+ pub partial_iv: Vec<u8>,
+ /// Counter signature
+ pub counter_signatures: Vec<CoseSignature>,
+ /// Any additional header (label,value) pairs. If duplicate labels are present, CBOR-encoding
+ /// will fail.
+ pub rest: Vec<(Label, Value)>,
+}
+
+impl Header {
+ /// Indicate whether the `Header` is empty.
+ pub fn is_empty(&self) -> bool {
+ self.alg.is_none()
+ && self.crit.is_empty()
+ && self.content_type.is_none()
+ && self.key_id.is_empty()
+ && self.iv.is_empty()
+ && self.partial_iv.is_empty()
+ && self.counter_signatures.is_empty()
+ && self.rest.is_empty()
+ }
+}
+
+impl crate::CborSerializable for Header {}
+
+const ALG: Label = Label::Int(iana::HeaderParameter::Alg as i64);
+const CRIT: Label = Label::Int(iana::HeaderParameter::Crit as i64);
+const CONTENT_TYPE: Label = Label::Int(iana::HeaderParameter::ContentType as i64);
+const KID: Label = Label::Int(iana::HeaderParameter::Kid as i64);
+const IV: Label = Label::Int(iana::HeaderParameter::Iv as i64);
+const PARTIAL_IV: Label = Label::Int(iana::HeaderParameter::PartialIv as i64);
+const COUNTER_SIG: Label = Label::Int(iana::HeaderParameter::CounterSignature as i64);
+
+impl AsCborValue for Header {
+ fn from_cbor_value(value: Value) -> Result<Self> {
+ let m = value.try_as_map()?;
+ let mut headers = Self::default();
+ let mut seen = BTreeSet::new();
+ for (l, value) in m.into_iter() {
+ // The `ciborium` CBOR library does not police duplicate map keys.
+ // RFC 8152 section 14 requires that COSE does police duplicates, so do it here.
+ let label = Label::from_cbor_value(l)?;
+ if seen.contains(&label) {
+ return Err(CoseError::DuplicateMapKey);
+ }
+ seen.insert(label.clone());
+ match label {
+ ALG => headers.alg = Some(Algorithm::from_cbor_value(value)?),
+
+ CRIT => match value {
+ Value::Array(a) => {
+ if a.is_empty() {
+ return Err(CoseError::UnexpectedItem(
+ "empty array",
+ "non-empty array",
+ ));
+ }
+ for v in a {
+ headers.crit.push(
+ RegisteredLabel::<iana::HeaderParameter>::from_cbor_value(v)?,
+ );
+ }
+ }
+ v => return cbor_type_error(&v, "array value"),
+ },
+
+ CONTENT_TYPE => {
+ headers.content_type = Some(ContentType::from_cbor_value(value)?);
+ if let Some(ContentType::Text(text)) = &headers.content_type {
+ if text.is_empty() {
+ return Err(CoseError::UnexpectedItem("empty tstr", "non-empty tstr"));
+ }
+ if text.trim() != text {
+ return Err(CoseError::UnexpectedItem(
+ "leading/trailing whitespace",
+ "no leading/trailing whitespace",
+ ));
+ }
+ // Basic check that the content type is of form type/subtype.
+ // We don't check the precise definition though (RFC 6838 s4.2)
+ if text.matches('/').count() != 1 {
+ return Err(CoseError::UnexpectedItem(
+ "arbitrary text",
+ "text of form type/subtype",
+ ));
+ }
+ }
+ }
+
+ KID => {
+ headers.key_id = value.try_as_nonempty_bytes()?;
+ }
+
+ IV => {
+ headers.iv = value.try_as_nonempty_bytes()?;
+ }
+
+ PARTIAL_IV => {
+ headers.partial_iv = value.try_as_nonempty_bytes()?;
+ }
+ COUNTER_SIG => {
+ let sig_or_sigs = value.try_as_array()?;
+ if sig_or_sigs.is_empty() {
+ return Err(CoseError::UnexpectedItem(
+ "empty sig array",
+ "non-empty sig array",
+ ));
+ }
+ // The encoding of counter signature[s] is pesky:
+ // - a single counter signature is encoded as `COSE_Signature` (a 3-tuple)
+ // - multiple counter signatures are encoded as `[+ COSE_Signature]`
+ //
+ // Determine which is which by looking at the first entry of the array:
+ // - If it's a bstr, sig_or_sigs is a single signature.
+ // - If it's an array, sig_or_sigs is an array of signatures
+ match &sig_or_sigs[0] {
+ Value::Bytes(_) => headers
+ .counter_signatures
+ .push(CoseSignature::from_cbor_value(Value::Array(sig_or_sigs))?),
+ Value::Array(_) => {
+ for sig in sig_or_sigs.into_iter() {
+ headers
+ .counter_signatures
+ .push(CoseSignature::from_cbor_value(sig)?);
+ }
+ }
+ v => return cbor_type_error(v, "array or bstr value"),
+ }
+ }
+
+ label => headers.rest.push((label, value)),
+ }
+ // RFC 8152 section 3.1: "The 'Initialization Vector' and 'Partial Initialization
+ // Vector' parameters MUST NOT both be present in the same security layer."
+ if !headers.iv.is_empty() && !headers.partial_iv.is_empty() {
+ return Err(CoseError::UnexpectedItem(
+ "IV and partial-IV specified",
+ "only one of IV and partial IV",
+ ));
+ }
+ }
+ Ok(headers)
+ }
+
+ fn to_cbor_value(mut self) -> Result<Value> {
+ let mut map = Vec::<(Value, Value)>::new();
+ if let Some(alg) = self.alg {
+ map.push((ALG.to_cbor_value()?, alg.to_cbor_value()?));
+ }
+ if !self.crit.is_empty() {
+ map.push((CRIT.to_cbor_value()?, to_cbor_array(self.crit)?));
+ }
+ if let Some(content_type) = self.content_type {
+ map.push((CONTENT_TYPE.to_cbor_value()?, content_type.to_cbor_value()?));
+ }
+ if !self.key_id.is_empty() {
+ map.push((KID.to_cbor_value()?, Value::Bytes(self.key_id)));
+ }
+ if !self.iv.is_empty() {
+ map.push((IV.to_cbor_value()?, Value::Bytes(self.iv)));
+ }
+ if !self.partial_iv.is_empty() {
+ map.push((PARTIAL_IV.to_cbor_value()?, Value::Bytes(self.partial_iv)));
+ }
+ if !self.counter_signatures.is_empty() {
+ if self.counter_signatures.len() == 1 {
+ // A single counter signature is encoded differently.
+ map.push((
+ COUNTER_SIG.to_cbor_value()?,
+ self.counter_signatures.remove(0).to_cbor_value()?,
+ ));
+ } else {
+ map.push((
+ COUNTER_SIG.to_cbor_value()?,
+ to_cbor_array(self.counter_signatures)?,
+ ));
+ }
+ }
+ let mut seen = BTreeSet::new();
+ for (label, value) in self.rest.into_iter() {
+ if seen.contains(&label) {
+ return Err(CoseError::DuplicateMapKey);
+ }
+ seen.insert(label.clone());
+ map.push((label.to_cbor_value()?, value));
+ }
+ Ok(Value::Map(map))
+ }
+}
+
+/// Builder for [`Header`] objects.
+#[derive(Debug, Default)]
+pub struct HeaderBuilder(Header);
+
+impl HeaderBuilder {
+ builder! {Header}
+ builder_set! {key_id: Vec<u8>}
+
+ /// Set the algorithm.
+ #[must_use]
+ pub fn algorithm(mut self, alg: iana::Algorithm) -> Self {
+ self.0.alg = Some(Algorithm::Assigned(alg));
+ self
+ }
+
+ /// Add a critical header.
+ #[must_use]
+ pub fn add_critical(mut self, param: iana::HeaderParameter) -> Self {
+ self.0.crit.push(RegisteredLabel::Assigned(param));
+ self
+ }
+
+ /// Add a critical header.
+ #[must_use]
+ pub fn add_critical_label(mut self, label: RegisteredLabel<iana::HeaderParameter>) -> Self {
+ self.0.crit.push(label);
+ self
+ }
+
+ /// Set the content type to a numeric value.
+ #[must_use]
+ pub fn content_format(mut self, content_type: iana::CoapContentFormat) -> Self {
+ self.0.content_type = Some(ContentType::Assigned(content_type));
+ self
+ }
+
+ /// Set the content type to a text value.
+ #[must_use]
+ pub fn content_type(mut self, content_type: String) -> Self {
+ self.0.content_type = Some(ContentType::Text(content_type));
+ self
+ }
+
+ /// Set the IV, and clear any partial IV already set.
+ #[must_use]
+ pub fn iv(mut self, iv: Vec<u8>) -> Self {
+ self.0.iv = iv;
+ self.0.partial_iv.clear();
+ self
+ }
+
+ /// Set the partial IV, and clear any IV already set.
+ #[must_use]
+ pub fn partial_iv(mut self, iv: Vec<u8>) -> Self {
+ self.0.partial_iv = iv;
+ self.0.iv.clear();
+ self
+ }
+
+ /// Add a counter signature.
+ #[must_use]
+ pub fn add_counter_signature(mut self, sig: CoseSignature) -> Self {
+ self.0.counter_signatures.push(sig);
+ self
+ }
+
+ /// Set a header label:value pair. If duplicate labels are added to a [`Header`],
+ /// subsequent attempts to CBOR-encode the header will fail.
+ ///
+ /// # Panics
+ ///
+ /// This function will panic if it used to set a header label from the range [1, 6].
+ #[must_use]
+ pub fn value(mut self, label: i64, value: Value) -> Self {
+ if label >= iana::HeaderParameter::Alg.to_i64()
+ && label <= iana::HeaderParameter::CounterSignature.to_i64()
+ {
+ panic!("value() method used to set core header parameter"); // safe: invalid input
+ }
+ self.0.rest.push((Label::Int(label), value));
+ self
+ }
+
+ /// Set a header label:value pair where the `label` is text.
+ #[must_use]
+ pub fn text_value(mut self, label: String, value: Value) -> Self {
+ self.0.rest.push((Label::Text(label), value));
+ self
+ }
+}
+
+/// Structure representing a protected COSE header map.
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct ProtectedHeader {
+ /// If this structure was created by parsing serialized data, this field
+ /// holds the entire contents of the original `bstr` data.
+ pub original_data: Option<Vec<u8>>,
+ /// Parsed header information.
+ pub header: Header,
+}
+
+impl ProtectedHeader {
+ /// Constructor from a [`Value`] that holds a `bstr` encoded header.
+ #[inline]
+ pub fn from_cbor_bstr(val: Value) -> Result<Self> {
+ let data = val.try_as_bytes()?;
+ let header = if data.is_empty() {
+ // An empty bstr is used as a short cut for an empty header map.
+ Header::default()
+ } else {
+ Header::from_slice(&data)?
+ };
+ Ok(ProtectedHeader {
+ original_data: Some(data),
+ header,
+ })
+ }
+
+ /// Convert this header to a `bstr` encoded map, as a [`Value`], consuming the object along the
+ /// way.
+ #[inline]
+ pub fn cbor_bstr(self) -> Result<Value> {
+ Ok(Value::Bytes(
+ if let Some(original_data) = self.original_data {
+ original_data
+ } else if self.is_empty() {
+ vec![]
+ } else {
+ self.to_vec()?
+ },
+ ))
+ }
+
+ /// Indicate whether the `ProtectedHeader` is empty.
+ pub fn is_empty(&self) -> bool {
+ self.header.is_empty()
+ }
+}
+
+impl crate::CborSerializable for ProtectedHeader {}
+
+impl AsCborValue for ProtectedHeader {
+ fn from_cbor_value(value: Value) -> Result<Self> {
+ Ok(ProtectedHeader {
+ original_data: None,
+ header: Header::from_cbor_value(value)?,
+ })
+ }
+
+ fn to_cbor_value(self) -> Result<Value> {
+ self.header.to_cbor_value()
+ }
+}
diff --git a/src/header/tests.rs b/src/header/tests.rs
new file mode 100644
index 0000000..6ff901a
--- /dev/null
+++ b/src/header/tests.rs
@@ -0,0 +1,487 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+use super::*;
+use crate::{cbor::value::Value, iana, util::expect_err, CborSerializable, Label};
+use alloc::{borrow::ToOwned, vec};
+
+#[test]
+fn test_header_encode() {
+ let tests = vec![
+ (
+ Header {
+ alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)),
+ key_id: vec![1, 2, 3],
+ partial_iv: vec![1, 2, 3],
+ ..Default::default()
+ },
+ concat!(
+ "a3", // 3-map
+ "01", "01", // 1 (alg) => A128GCM
+ "04", "43", "010203", // 4 (kid) => 3-bstr
+ "06", "43", "010203", // 6 (partial-iv) => 3-bstr
+ ),
+ ),
+ (
+ Header {
+ alg: Some(Algorithm::PrivateUse(i64::MIN)),
+ ..Default::default()
+ },
+ concat!(
+ "a1", // 1-map
+ "01",
+ "3b7fffffffffffffff", // 1 (alg) => -lots
+ ),
+ ),
+ (
+ Header {
+ alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)),
+ crit: vec![RegisteredLabel::Assigned(iana::HeaderParameter::Alg)],
+ content_type: Some(ContentType::Assigned(iana::CoapContentFormat::CoseEncrypt0)),
+ key_id: vec![1, 2, 3],
+ iv: vec![1, 2, 3],
+ rest: vec![
+ (Label::Int(0x46), Value::from(0x47)),
+ (Label::Int(0x66), Value::from(0x67)),
+ ],
+ ..Default::default()
+ },
+ concat!(
+ "a7", // 7-map
+ "01", "01", // 1 (alg) => A128GCM
+ "02", "81", "01", // 2 (crit) => 1-arr [x01]
+ "03", "10", // 3 (content-type) => 16
+ "04", "43", "010203", // 4 (kid) => 3-bstr
+ "05", "43", "010203", // 5 (iv) => 3-bstr
+ "1846", "1847", // 46 => 47 (note canonical ordering)
+ "1866", "1867", // 66 => 67
+ ),
+ ),
+ (
+ Header {
+ alg: Some(Algorithm::Text("abc".to_owned())),
+ crit: vec![RegisteredLabel::Text("d".to_owned())],
+ content_type: Some(ContentType::Text("a/b".to_owned())),
+ key_id: vec![1, 2, 3],
+ iv: vec![1, 2, 3],
+ rest: vec![
+ (Label::Int(0x46), Value::from(0x47)),
+ (Label::Text("a".to_owned()), Value::from(0x47)),
+ ],
+ counter_signatures: vec![CoseSignature {
+ signature: vec![1, 2, 3],
+ ..Default::default()
+ }],
+ ..Default::default()
+ },
+ concat!(
+ "a8", // 8-map
+ "01", "63616263", // 1 (alg) => "abc"
+ "02", "81", "6164", // 2 (crit) => 1-arr ["d"]
+ "03", "63612f62", // 3 (content-type) => "a/b"
+ "04", "43", "010203", // 4 (kid) => 3-bstr
+ "05", "43", "010203", // 5 (iv) => 3-bstr
+ "07", "83", // 7 (sig) => [3-arr for COSE_Signature
+ "40", "a0", "43010203", // ]
+ "1846", "1847", // 46 => 47 (note canonical ordering)
+ "6161", "1847", // "a" => 47
+ ),
+ ),
+ (
+ Header {
+ alg: Some(Algorithm::Text("abc".to_owned())),
+ crit: vec![RegisteredLabel::Text("d".to_owned())],
+ content_type: Some(ContentType::Text("a/b".to_owned())),
+ key_id: vec![1, 2, 3],
+ iv: vec![1, 2, 3],
+ rest: vec![
+ (Label::Int(0x46), Value::from(0x47)),
+ (Label::Text("a".to_owned()), Value::from(0x47)),
+ ],
+ counter_signatures: vec![
+ CoseSignature {
+ signature: vec![1, 2, 3],
+ ..Default::default()
+ },
+ CoseSignature {
+ signature: vec![3, 4, 5],
+ ..Default::default()
+ },
+ ],
+ ..Default::default()
+ },
+ concat!(
+ "a8", // 8-map
+ "01", "63616263", // 1 (alg) => "abc"
+ "02", "81", "6164", // 2 (crit) => 1-arr ["d"]
+ "03", "63612f62", // 3 (content-type) => "a/b"
+ "04", "43", "010203", // 4 (kid) => 3-bstr
+ "05", "43", "010203", // 5 (iv) => 3-bstr
+ "07", "82", // 7 (sig) => 2-array
+ "83", "40", "a0", "43010203", // [3-arr for COSE_Signature]
+ "83", "40", "a0", "43030405", // [3-arr for COSE_Signature]
+ "1846", "1847", // 46 => 47 (note canonical ordering)
+ "6161", "1847", // "a" => 47
+ ),
+ ),
+ (
+ HeaderBuilder::new()
+ .add_critical(iana::HeaderParameter::Alg)
+ .add_critical(iana::HeaderParameter::Alg)
+ .build(),
+ concat!(
+ "a1", // 1-map
+ "02", "820101", // crit => 2-arr [1, 1]
+ ),
+ ),
+ ];
+ for (i, (header, header_data)) in tests.iter().enumerate() {
+ let got = header.clone().to_vec().unwrap();
+ assert_eq!(*header_data, hex::encode(&got), "case {}", i);
+
+ let mut got = Header::from_slice(&got).unwrap();
+ for mut sig in &mut got.counter_signatures {
+ sig.protected.original_data = None;
+ }
+ assert_eq!(*header, got);
+ assert!(!got.is_empty());
+
+ // The same data also parses as a `ProtectedHeader`
+ let protected = ProtectedHeader {
+ original_data: None,
+ header: header.clone(),
+ };
+ let protected_data = protected.clone().to_vec().unwrap();
+ assert_eq!(*header_data, hex::encode(&protected_data), "case {}", i);
+
+ let mut got = ProtectedHeader::from_slice(&protected_data).unwrap();
+ for mut sig in &mut got.header.counter_signatures {
+ sig.protected.original_data = None;
+ }
+ assert!(!got.is_empty());
+ assert_eq!(*header, got.header);
+
+ // Also try parsing as a protected header inside a `bstr`
+ let prot_bstr_val = protected.cbor_bstr().unwrap();
+ let mut got = ProtectedHeader::from_cbor_bstr(prot_bstr_val).unwrap();
+ for mut sig in &mut got.header.counter_signatures {
+ sig.protected.original_data = None;
+ }
+ assert!(!got.is_empty());
+ assert_eq!(*header, got.header);
+ assert_eq!(
+ *header_data,
+ hex::encode(&got.original_data.expect("missing original data"))
+ );
+ }
+}
+
+#[test]
+fn test_header_decode_fail() {
+ let tests = vec![
+ (
+ concat!(
+ "a1", // 1-map
+ "01", "01", // 1 (alg) => 01
+ "01", // extraneous data
+ ),
+ "extraneous data in CBOR input",
+ ),
+ (
+ concat!(
+ "a1", // 1-map
+ "01", "08", // 1 (alg) => invalid value
+ ),
+ "expected value in IANA or private use range",
+ ),
+ (
+ concat!(
+ "a1", // 1-map
+ "01", "4101", // 1 (alg) => bstr (invalid value type)
+ ),
+ "expected int/tstr",
+ ),
+ (
+ concat!(
+ "a1", // 1-map
+ "02", "4101", // 2 (crit) => bstr (invalid value type)
+ ),
+ "expected array",
+ ),
+ (
+ concat!(
+ "a1", // 1-map
+ "02", "81", "4101", // 2 (crit) => [bstr] (invalid value type)
+ ),
+ "expected int/tstr",
+ ),
+ (
+ concat!(
+ "a1", // 1-map
+ "02", "80", // 2 (crit) => []
+ ),
+ "expected non-empty array",
+ ),
+ (
+ concat!(
+ "a1", // 1-map
+ "03", "81", "4101", // 3 (content-type) => [bstr] (invalid value type)
+ ),
+ "expected int/tstr",
+ ),
+ (
+ concat!(
+ "a1", // 1-map
+ "03", "19", "0606", // 3 (content-type) => invalid value 1542
+ ),
+ "expected recognized IANA value",
+ ),
+ (
+ concat!(
+ "a1", // 1-map
+ "03", "64", "20612f62" // 3 (content-type) => invalid value " a/b"
+ ),
+ "expected no leading/trailing whitespace",
+ ),
+ (
+ concat!(
+ "a1", // 1-map
+ "03", "64", "612f6220" // 3 (content-type) => invalid value "a/b "
+ ),
+ "expected no leading/trailing whitespace",
+ ),
+ (
+ concat!(
+ "a1", // 1-map
+ "03", "62", "6162" // 3 (content-type) => invalid value "ab"
+ ),
+ "expected text of form type/subtype",
+ ),
+ (
+ concat!(
+ "a1", // 1-map
+ "03", "60", // 3 (content-type) => invalid value ""
+ ),
+ "expected non-empty tstr",
+ ),
+ (
+ concat!(
+ "a1", // 1-map
+ "04", "40", // 4 (key-id) => 0-bstr
+ ),
+ "expected non-empty bstr",
+ ),
+ (
+ concat!(
+ "a1", // 1-map
+ "04", "01", // 4 (key-id) => invalid value type
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "a1", // 1-map
+ "05", "40", // 5 (iv) => 0-bstr
+ ),
+ "expected non-empty bstr",
+ ),
+ (
+ concat!(
+ "a1", // 1-map
+ "05", "01", // 5 (iv) => invalid value type
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "a1", // 1-map
+ "06", "40", // 6 (partial-iv) => 0-bstr
+ ),
+ "expected non-empty bstr",
+ ),
+ (
+ concat!(
+ "a1", // 1-map
+ "06", "01", // 6 (partial-iv) => invalid value type
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "a1", // 1-map
+ "07", "01", // 7 (counter-sig) => invalid value type
+ ),
+ "expected array",
+ ),
+ (
+ concat!(
+ "a1", // 1-map
+ "07", "80", // 7 (counter-sig) => 0-arr
+ ),
+ "expected non-empty sig array",
+ ),
+ (
+ concat!(
+ "a2", // 1-map
+ "05", "4101", // 5 (iv) => 1-bstr
+ "06", "4101", // 6 (partial-iv) => 1-bstr
+ ),
+ "expected only one of IV and partial IV",
+ ),
+ (
+ concat!(
+ "a2", // 2-map
+ "01", "63616263", // 1 (alg) => "abc"
+ "07", "82", // 7 (sig) => 2-array
+ "63616263", // tstr (invalid)
+ "83", "40", "a0", "43010203", // [3-arr for COSE_Signature]
+ ),
+ "array or bstr value",
+ ),
+ ];
+ for (header_data, err_msg) in tests.iter() {
+ let data = hex::decode(header_data).unwrap();
+ let result = Header::from_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
+
+#[test]
+fn test_header_decode_dup_fail() {
+ let tests = vec![
+ (
+ concat!(
+ "a3", // 3-map
+ "01", "01", // 1 (alg) => A128GCM
+ "1866", "1867", // 66 => 67
+ "1866", "1847", // 66 => 47
+ ),
+ "duplicate map key",
+ ),
+ (
+ concat!(
+ "a3", // 3-map
+ "01", "01", // 1 (alg) => A128GCM
+ "1866", "1867", // 66 => 67
+ "01", "01", // 1 (alg) => A128GCM (duplicate label)
+ ),
+ "duplicate map key",
+ ),
+ ];
+ for (header_data, err_msg) in tests.iter() {
+ let data = hex::decode(header_data).unwrap();
+ let result = Header::from_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
+
+#[test]
+fn test_header_encode_dup_fail() {
+ let tests = vec![
+ Header {
+ alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)),
+ crit: vec![RegisteredLabel::Assigned(iana::HeaderParameter::Alg)],
+ content_type: Some(ContentType::Assigned(iana::CoapContentFormat::CoseEncrypt0)),
+ key_id: vec![1, 2, 3],
+ iv: vec![1, 2, 3],
+ rest: vec![
+ (Label::Int(0x46), Value::from(0x47)),
+ (Label::Int(0x46), Value::from(0x67)),
+ ],
+ ..Default::default()
+ },
+ HeaderBuilder::new()
+ .text_value("doop".to_owned(), Value::from(1))
+ .text_value("doop".to_owned(), Value::from(2))
+ .build(),
+ ];
+ for header in tests {
+ let result = header.clone().to_vec();
+ expect_err(result, "duplicate map key");
+ }
+}
+
+#[test]
+fn test_header_builder() {
+ let tests = vec![
+ (
+ HeaderBuilder::new().build(),
+ Header {
+ ..Default::default()
+ },
+ ),
+ (
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::A128GCM)
+ .add_critical(iana::HeaderParameter::Alg)
+ .add_critical_label(RegisteredLabel::Text("abc".to_owned()))
+ .content_format(iana::CoapContentFormat::CoseEncrypt0)
+ .key_id(vec![1, 2, 3])
+ .partial_iv(vec![4, 5, 6]) // removed by .iv() call
+ .iv(vec![1, 2, 3])
+ .value(0x46, Value::from(0x47))
+ .value(0x66, Value::from(0x67))
+ .build(),
+ Header {
+ alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)),
+ crit: vec![
+ RegisteredLabel::Assigned(iana::HeaderParameter::Alg),
+ RegisteredLabel::Text("abc".to_owned()),
+ ],
+ content_type: Some(ContentType::Assigned(iana::CoapContentFormat::CoseEncrypt0)),
+ key_id: vec![1, 2, 3],
+ iv: vec![1, 2, 3],
+ rest: vec![
+ (Label::Int(0x46), Value::from(0x47)),
+ (Label::Int(0x66), Value::from(0x67)),
+ ],
+ ..Default::default()
+ },
+ ),
+ (
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::A128GCM)
+ .add_critical(iana::HeaderParameter::Alg)
+ .add_critical_label(RegisteredLabel::Text("abc".to_owned()))
+ .content_type("type/subtype".to_owned())
+ .key_id(vec![1, 2, 3])
+ .iv(vec![1, 2, 3]) // removed by .partial_iv() call
+ .partial_iv(vec![4, 5, 6])
+ .build(),
+ Header {
+ alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)),
+ crit: vec![
+ RegisteredLabel::Assigned(iana::HeaderParameter::Alg),
+ RegisteredLabel::Text("abc".to_owned()),
+ ],
+ content_type: Some(ContentType::Text("type/subtype".to_owned())),
+ key_id: vec![1, 2, 3],
+ partial_iv: vec![4, 5, 6],
+ ..Default::default()
+ },
+ ),
+ ];
+ for (got, want) in tests {
+ assert_eq!(got, want);
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_header_builder_core_param_panic() {
+ // Attempting to set a core header parameter (in range [1,7]) via `.param()` panics.
+ let _hdr = HeaderBuilder::new().value(1, Value::Null).build();
+}
diff --git a/src/iana/mod.rs b/src/iana/mod.rs
new file mode 100644
index 0000000..41c2ef8
--- /dev/null
+++ b/src/iana/mod.rs
@@ -0,0 +1,745 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+//! Enumerations for IANA-managed values.
+//!
+//! Sources:
+//! - <https://www.iana.org/assignments/cose/cose.xhtml>
+//! - <https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml>
+//! - <https://www.iana.org/assignments/core-parameters/core-parameters.xhtml#content-formats>
+
+#[cfg(test)]
+mod tests;
+
+/// Trait indicating an enum that can be constructed from `i64` values.
+pub trait EnumI64: Sized + Eq {
+ fn from_i64(i: i64) -> Option<Self>;
+ fn to_i64(&self) -> i64;
+}
+
+/// Trait indicating an enum with a range of private values.
+pub trait WithPrivateRange {
+ fn is_private(i: i64) -> bool;
+}
+
+/// Generate an enum with associated values, plus a `from_i64` method.
+macro_rules! iana_registry {
+ ( $(#[$attr:meta])* $enum_name:ident {$($(#[$fattr:meta])* $name:ident: $val:expr,)* } ) => {
+ #[allow(non_camel_case_types)]
+ $(#[$attr])*
+ #[non_exhaustive]
+ #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
+ pub enum $enum_name {
+ $($(#[$fattr])* $name = $val,)*
+ }
+ impl EnumI64 for $enum_name {
+ fn from_i64(i: i64) -> Option<Self> {
+ match i {
+ $(x if x == Self::$name as i64 => Some(Self::$name),)*
+ _ => None,
+ }
+ }
+ #[inline]
+ fn to_i64(&self) -> i64 {
+ *self as i64
+ }
+ }
+ }
+}
+
+iana_registry! {
+ /// IANA-registered COSE header parameters.
+ ///
+ /// From IANA registry <https://www.iana.org/assignments/cose/cose.xhtml#header-parameters>
+ /// as of 2021-03-19.
+ HeaderParameter {
+ /// Reserved
+ Reserved: 0,
+ /// Cryptographic algorithm to use
+ ///
+ /// Associated value of type int / tstr
+ Alg: 1,
+ /// Critical headers to be understood
+ ///
+ /// Associated value of type [+ label]
+ Crit: 2,
+ /// Content type of the payload
+ ///
+ /// Associated value of type tstr / uint
+ ContentType: 3,
+ /// Key identifier
+ ///
+ /// Associated value of type bstr
+ Kid: 4,
+ /// Full Initialization Vector
+ ///
+ /// Associated value of type bstr
+ Iv: 5,
+ /// Partial Initialization Vector
+ ///
+ /// Associated value of type bstr
+ PartialIv: 6,
+ /// CBOR-encoded signature structure
+ ///
+ /// Associated value of type COSE_Signature / [+ COSE_Signature ]
+ CounterSignature: 7,
+ /// Counter signature with implied signer and headers
+ ///
+ /// Associated value of type bstr
+ CounterSignature0: 9,
+ /// Identifies the context for the key identifier
+ ///
+ /// Associated value of type bstr
+ KidContext: 10,
+ /// An unordered bag of X.509 certificates
+ ///
+ /// Associated value of type COSE_X509
+ X5Bag: 32,
+ /// An ordered chain of X.509 certificates
+ ///
+ /// Associated value of type COSE_X509
+ X5Chain: 33,
+ /// Hash of an X.509 certificate
+ ///
+ /// Associated value of type COSE_CertHash
+ X5T: 34,
+ /// URI pointing to an X.509 certificate
+ ///
+ /// Associated value of type uri
+ X5U: 35,
+ /// Challenge Nonce
+ ///
+ /// Associated value of type bstr
+ CuphNonce: 256,
+ /// Public Key
+ ///
+ /// Associated value of type array
+ CuphOwnerPubKey: 257,
+ }
+}
+
+/// Integer values for COSE header parameters below this value are reserved for private use.
+pub const HEADER_PARAMETER_PRIVATE_USE_MAX: i64 = -65536;
+
+impl WithPrivateRange for HeaderParameter {
+ fn is_private(i: i64) -> bool {
+ i < HEADER_PARAMETER_PRIVATE_USE_MAX
+ }
+}
+
+iana_registry! {
+ /// IANA-registered COSE header algorithm parameters.
+ ///
+ /// From IANA registry <https://www.iana.org/assignments/cose/cose.xhtml#header-algorithm-parameters>
+ /// as of 2021-03-19.
+ HeaderAlgorithmParameter {
+ /// Party V other provided information
+ ///
+ /// Associated value of type bstr
+ PartyVOther: -26,
+ /// Party V provided nonce
+ ///
+ /// Associated value of type bstr / int
+ PartyVNonce: -25,
+ /// Party V identity information
+ ///
+ /// Associated value of type bstr
+ PartyVIdentity: -24,
+ /// Party U other provided information
+ ///
+ /// Associated value of type bstr
+ PartyUOther: -23,
+ /// Party U provided nonce
+ ///
+ /// Associated value of type bstr / int
+ PartyUNonce: -22,
+ /// Party U identity information
+ ///
+ /// Associated value of type bstr
+ PartyUIdentity: -21,
+ /// Random salt
+ ///
+ /// Associated value of type bstr
+ Salt: -20,
+ /// Static public key identifier for the sender
+ ///
+ /// Associated value of type bstr
+ StaticKeyId: -3,
+ /// Static public key for the sender
+ ///
+ /// Associated value of type COSE_Key
+ StaticKey: -2,
+ /// Ephemeral public key for the sender
+ ///
+ /// Associated value of type COSE_Key
+ EphemeralKey: -1,
+ }
+}
+
+iana_registry! {
+ /// IANA-registered COSE algorithms.
+ ///
+ /// From IANA registry <https://www.iana.org/assignments/cose/cose.xhtml#algorithms>
+ /// as of 2021-03-19.
+ Algorithm {
+ /// RSASSA-PKCS1-v1_5 using SHA-1
+ RS1: -65535,
+ /// WalnutDSA signature
+ WalnutDSA: -260,
+ /// RSASSA-PKCS1-v1_5 using SHA-512
+ RS512: -259,
+ /// RSASSA-PKCS1-v1_5 using SHA-384
+ RS384: -258,
+ /// RSASSA-PKCS1-v1_5 using SHA-256
+ RS256: -257,
+ /// ECDSA using secp256k1 curve and SHA-256
+ ES256K: -47,
+ /// HSS/LMS hash-based digital signature
+ HSS_LMS: -46,
+ /// SHAKE-256 512-bit Hash Value
+ SHAKE256: -45,
+ /// SHA-2 512-bit Hash
+ SHA_512: -44,
+ /// SHA-2 384-bit Hash
+ SHA_384: -43,
+ /// RSAES-OAEP w/ SHA-512
+ RSAES_OAEP_SHA_512: -42,
+ /// RSAES-OAEP w/ SHA-256
+ RSAES_OAEP_SHA_256: -41,
+ /// RSAES-OAEP w/ SHA-1
+ RSAES_OAEP_RFC_8017_default: -40,
+ /// RSASSA-PSS w/ SHA-512
+ PS512: -39,
+ /// RSASSA-PSS_SHA-384
+ PS384: -38,
+ /// RSASSA-PSS w/ SHA-256
+ PS256: -37,
+ /// ECDSA w/ SHA-512
+ ES512: -36,
+ /// ECDSA w/ SHA-384
+ ES384: -35,
+ /// ECDH SS w/ Concat KDF and AES Key Wrap w/ 256-bit key
+ ECDH_SS_A256KW: -34,
+ /// ECDH SS w/ Concat KDF and AES Key Wrap w/ 192-bit key
+ ECDH_SS_A192KW: -33,
+ /// ECDH SS w/ Concat KDF and AES Key Wrap w/ 128-bit key
+ ECDH_SS_A128KW: -32,
+ /// ECDH ES w/ Concat KDF and AES Key Wrap w/ 256-bit key
+ ECDH_ES_A256KW: -31,
+ /// ECDH ES w/ Concat KDF and AES Key Wrap w/ 192-bit key
+ ECDH_ES_A192KW: -30,
+ /// ECDH ES w/ Concat KDF and AES Key Wrap w/ 128-bit key
+ ECDH_ES_A128KW: -29,
+ /// ECDH SS w/ HKDF - generate key directly
+ ECDH_SS_HKDF_512: -28,
+ /// ECDH SS w/ HKDF - generate key directly
+ ECDH_SS_HKDF_256: -27,
+ /// ECDH ES w/ HKDF - generate key directly
+ ECDH_ES_HKDF_512: -26,
+ /// ECDH ES w/ HKDF - generate key directly
+ ECDH_ES_HKDF_256: -25,
+ /// SHAKE-128 256-bit Hash Value
+ SHAKE128: -18,
+ /// SHA-2 512-bit Hash truncated to 256-bits
+ SHA_512_256: -17,
+ /// SHA-2 256-bit Hash
+ SHA_256: -16,
+ /// SHA-2 256-bit Hash truncated to 64-bits
+ SHA_256_64: -15,
+ /// SHA-1 Hash
+ SHA_1: -14,
+ /// Shared secret w/ AES-MAC 256-bit key
+ Direct_HKDF_AES_256: -13,
+ /// Shared secret w/ AES-MAC 128-bit key
+ Direct_HKDF_AES_128: -12,
+ /// Shared secret w/ HKDF and SHA-512
+ Direct_HKDF_SHA_512: -11,
+ /// Shared secret w/ HKDF and SHA-256
+ Direct_HKDF_SHA_256: -10,
+ /// EdDSA
+ EdDSA: -8,
+ /// ECDSA w/ SHA-256
+ ES256: -7,
+ /// Direct use of CEK
+ Direct: -6,
+ /// AES Key Wrap w/ 256-bit key
+ A256KW: -5,
+ /// AES Key Wrap w/ 192-bit key
+ A192KW: -4,
+ /// AES Key Wrap w/ 128-bit key
+ A128KW: -3,
+ /// Reserved
+ Reserved: 0,
+ /// AES-GCM mode w/ 128-bit key, 128-bit tag
+ A128GCM: 1,
+ /// AES-GCM mode w/ 192-bit key, 128-bit tag
+ A192GCM: 2,
+ /// AES-GCM mode w/ 256-bit key, 128-bit tag
+ A256GCM: 3,
+ /// HMAC w/ SHA-256 truncated to 64 bits
+ HMAC_256_64: 4,
+ /// HMAC w/ SHA-256
+ HMAC_256_256: 5,
+ /// HMAC w/ SHA-384
+ HMAC_384_384: 6,
+ /// HMAC w/ SHA-512
+ HMAC_512_512: 7,
+ /// AES-CCM mode 128-bit key, 64-bit tag, 13-byte nonce
+ AES_CCM_16_64_128: 10,
+ /// AES-CCM mode 256-bit key, 64-bit tag, 13-byte nonce
+ AES_CCM_16_64_256: 11,
+ /// AES-CCM mode 128-bit key, 64-bit tag, 7-byte nonce
+ AES_CCM_64_64_128: 12,
+ /// AES-CCM mode 256-bit key, 64-bit tag, 7-byte nonce
+ AES_CCM_64_64_256: 13,
+ /// AES-MAC 128-bit key, 64-bit tag
+ AES_MAC_128_64: 14,
+ /// AES-MAC 256-bit key, 64-bit tag
+ AES_MAC_256_64: 15,
+ /// ChaCha20/Poly1305 w/ 256-bit key, 128-bit tag
+ ChaCha20Poly1305: 24,
+ /// AES-MAC 128-bit key, 128-bit tag
+ AES_MAC_128_128: 25,
+ /// AES-MAC 256-bit key, 128-bit tag
+ AES_MAC_256_128: 26,
+ /// AES-CCM mode 128-bit key, 128-bit tag, 13-byte nonce
+ AES_CCM_16_128_128: 30,
+ /// AES-CCM mode 256-bit key, 128-bit tag, 13-byte nonce
+ AES_CCM_16_128_256: 31,
+ /// AES-CCM mode 128-bit key, 128-bit tag, 7-byte nonce
+ AES_CCM_64_128_128: 32,
+ /// AES-CCM mode 256-bit key, 128-bit tag, 7-byte nonce
+ AES_CCM_64_128_256: 33,
+ /// For doing IV generation for symmetric algorithms.
+ IV_GENERATION: 34,
+ }
+}
+
+/// Integer values for COSE algorithms below this value are reserved for private use.
+pub const ALGORITHM_PRIVATE_USE_MAX: i64 = -65536;
+
+impl WithPrivateRange for Algorithm {
+ fn is_private(i: i64) -> bool {
+ i < ALGORITHM_PRIVATE_USE_MAX
+ }
+}
+
+iana_registry! {
+ /// IANA-registered COSE common key parameters.
+ ///
+ /// From IANA registry <https://www.iana.org/assignments/cose/cose.xhtml#key-common-parameters>
+ /// as of 2021-03-19.
+ KeyParameter {
+ /// Reserved value.
+ Reserved: 0,
+ /// Identification of the key type
+ ///
+ /// Associated value of type tstr / int
+ Kty: 1,
+ /// Key identification value - match to kid in message
+ ///
+ /// Associated value of type bstr
+ Kid: 2,
+ /// Key usage restriction to this algorithm
+ ///
+ /// Associated value of type tstr / int
+ Alg: 3,
+ /// Restrict set of permissible operations
+ ///
+ /// Associated value of type [+ (tstr / int)]
+ KeyOps: 4,
+ /// Base IV to be XORed with Partial IVs
+ ///
+ /// Associated value of type bstr
+ BaseIv: 5,
+ }
+}
+
+iana_registry! {
+ /// IANA-registered COSE key parameters for keys of type [`KeyType::OKP`].
+ ///
+ /// From IANA registry <https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters>
+ /// as of 2021-03-19.
+ OkpKeyParameter {
+ /// EC identifier - Taken from the "COSE Elliptic Curves" registry
+ ///
+ /// Associated value of type tstr / int
+ Crv: -1,
+ /// x-coordinate
+ ///
+ /// Associated value of type bstr
+ X: -2,
+ /// Private key
+ ///
+ /// Associated value of type bstr
+ D: -4,
+ }
+}
+
+iana_registry! {
+ /// IANA-registered COSE key parameters for keys of type [`KeyType::EC2`].
+ ///
+ /// From IANA registry <https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters>
+ /// as of 2021-03-19.
+ Ec2KeyParameter {
+ /// EC identifier - Taken from the "COSE Elliptic Curves" registry
+ ///
+ /// Associated value of type tstr / int
+ Crv: -1,
+ /// Public Key
+ ///
+ /// Associated value of type bstr
+ X: -2,
+ /// y-coordinate
+ ///
+ /// Associated value of type bstr / bool
+ Y: -3,
+ /// Private key
+ ///
+ /// Associated value of type bstr
+ D: -4,
+ }
+}
+
+iana_registry! {
+ /// IANA-registered COSE key parameters for keys of type [`KeyType::RSA`].
+ ///
+ /// From IANA registry <https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters>
+ /// as of 2021-03-19.
+ RsaKeyParameter {
+ /// The RSA modulus n
+ ///
+ /// Associated value of type bstr
+ N: -1,
+ /// The RSA public exponent e
+ ///
+ /// Associated value of type bstr
+ E: -2,
+ /// The RSA private exponent d
+ ///
+ /// Associated value of type bstr
+ D: -3,
+ /// The prime factor p of n
+ ///
+ /// Associated value of type bstr
+ P: -4,
+ /// The prime factor q of n
+ ///
+ /// Associated value of type bstr
+ Q: -5,
+ /// dP is d mod (p - 1)
+ ///
+ /// Associated value of type bstr
+ DP: -6,
+ /// dQ is d mod (q - 1)
+ ///
+ /// Associated value of type bstr
+ DQ: -7,
+ /// qInv is the CRT coefficient q^(-1) mod p
+ ///
+ /// Associated value of type bstr
+ QInv: -8,
+ /// Other prime infos, an array
+ ///
+ /// Associated value of type array
+ Other: -9,
+ /// a prime factor r_i of n, where i >= 3
+ ///
+ /// Associated value of type bstr
+ RI: -10,
+ /// d_i = d mod (r_i - 1)
+ ///
+ /// Associated value of type bstr
+ DI: -11,
+ /// The CRT coefficient t_i = (r_1 * r_2 * ... * r_(i-1))^(-1) mod r_i
+ ///
+ /// Associated value of type bstr
+ TI: -12,
+ }
+}
+
+iana_registry! {
+ /// IANA-registered COSE key parameters for keys of type [`KeyType::Symmetric`].
+ ///
+ /// From IANA registry <https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters>
+ /// as of 2021-03-19.
+ SymmetricKeyParameter {
+ /// Key Value
+ ///
+ /// Associated value of type bstr
+ K: -1,
+ }
+}
+
+iana_registry! {
+ /// IANA-registered COSE key parameters for keys of type [`KeyType::HSS_LMS`].
+ ///
+ /// From IANA registry <https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters>
+ /// as of 2021-03-19.
+ HssLmsKeyParameter {
+ /// Public key for HSS/LMS hash-based digital signature
+ ///
+ /// Associated value of type bstr
+ Pub: -1,
+ }
+}
+
+iana_registry! {
+ /// IANA-registered COSE key parameters for keys of type [`KeyType::WalnutDSA`].
+ ///
+ /// From IANA registry <https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters>
+ /// as of 2021-03-19.
+ WalnutDsaKeyParameter {
+ /// Group and Matrix (NxN) size
+ ///
+ /// Associated value of type uint
+ N: -1,
+ /// Finite field F_q
+ ///
+ /// Associated value of type uint
+ Q: -2,
+ /// List of T-values, enties in F_q
+ ///
+ /// Associated value of type array of uint
+ TValues: -3,
+ /// NxN Matrix of enties in F_q in column-major form
+ ///
+ /// Associated value of type array of array of uint
+ Matrix1: -4,
+ /// Permutation associated with matrix 1
+ ///
+ /// Associated value of type array of uint
+ Permutation1: -5,
+ /// NxN Matrix of enties in F_q in column-major form
+ ///
+ /// Associated value of type array of array of uint
+ Matrix2: -6,
+ }
+}
+
+iana_registry! {
+ /// IANA-registered COSE key types.
+ ///
+ /// From IANA registry <https://www.iana.org/assignments/cose/cose.xhtml#key-type>
+ /// as of 2021-03-19.
+ KeyType {
+ /// This value is reserved
+ Reserved: 0,
+ /// Octet Key Pair
+ OKP: 1,
+ /// Elliptic Curve Keys w/ x- and y-coordinate pair
+ EC2: 2,
+ /// RSA Key
+ RSA: 3,
+ /// Symmetric Keys
+ Symmetric: 4,
+ /// Public key for HSS/LMS hash-based digital signature
+ HSS_LMS: 5,
+ /// WalnutDSA public key
+ WalnutDSA: 6,
+ }
+}
+
+iana_registry! {
+ /// IANA-registered COSE elliptic curves.
+ ///
+ /// From IANA registry <https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves>
+ /// as of 2021-03-19.
+ EllipticCurve {
+ Reserved: 0,
+ /// EC2: NIST P-256 also known as secp256r1
+ P_256: 1,
+ /// EC2: NIST P-384 also known as secp384r1
+ P_384: 2,
+ /// EC2: NIST P-521 also known as secp521r1
+ P_521: 3,
+ /// OKP: X25519 for use w/ ECDH only
+ X25519: 4,
+ /// OKP: X448 for use w/ ECDH only
+ X448: 5,
+ /// OKP: Ed25519 for use w/ EdDSA only
+ Ed25519: 6,
+ /// OKP: Ed448 for use w/ EdDSA only
+ Ed448: 7,
+ /// EC2: SECG secp256k1 curve
+ Secp256k1: 8,
+ }
+}
+
+/// Integer values for COSE elliptic curves below this value are reserved for private use.
+pub const ELLIPTIC_CURVE_PRIVATE_USE_MAX: i64 = -65536;
+
+impl WithPrivateRange for EllipticCurve {
+ fn is_private(i: i64) -> bool {
+ i < ELLIPTIC_CURVE_PRIVATE_USE_MAX
+ }
+}
+
+iana_registry! {
+ /// Key operation values.
+ ///
+ /// See RFC 8152 section 7.1 table 4.
+ KeyOperation {
+ /// Key is used to create signatures. Requires private key fields.
+ Sign: 1,
+ /// Key is used for verification of signatures.
+ Verify: 2,
+ /// Key is used for key transport encryption.
+ Encrypt: 3,
+ /// Key is used for key transport decryption. Requires private key fields.
+ Decrypt: 4,
+ /// Key is used for key wrap encryption.
+ WrapKey: 5,
+ /// Key is used for key wrap decryption. Requires private key fields.
+ UnwrapKey: 6,
+ /// Key is used for deriving keys. Requires private key fields.
+ DeriveKey: 7,
+ /// Key is used for deriving bits not to be used as a key. Requires private key fields.
+ DeriveBits: 8,
+ /// Key is used for creating MACs.
+ MacCreate: 9,
+ /// Key is used for validating MACs.
+ MacVerify: 10,
+ }
+}
+
+iana_registry! {
+ /// CBOR tag values for COSE structures.
+ ///
+ /// From IANA registry <https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml>
+ /// as of 2021-03-19.
+ CborTag {
+ /// COSE Single Recipient Encrypted Data Object
+ CoseEncrypt0: 16,
+ /// COSE Mac w/o Recipients Object
+ CoseMac0: 17,
+ /// COSE Single Signer Data Object
+ CoseSign1: 18,
+ /// CBOR Web Token (CWT)
+ Cwt: 61,
+ /// COSE Encrypted Data Object
+ CoseEncrypt: 96,
+ /// COSE MACed Data Object
+ CoseMac: 97,
+ /// COSE Signed Data Object
+ CoseSign: 98,
+ }
+}
+
+iana_registry! {
+ /// CoAP Content Formats
+ ///
+ /// From IANA registry <https://www.iana.org/assignments/core-parameters/core-parameters.xhtml#content-formats>
+ /// as of 2021-03-19.
+ CoapContentFormat {
+ /// text/plain; charset=utf-8
+ TextPlainUtf8: 0,
+ /// application/cose; cose-type="cose-encrypt0"
+ CoseEncrypt0: 16,
+ /// application/cose; cose-type="cose-mac0"
+ CoseMac0: 17,
+ /// application/cose; cose-type="cose-sign1"
+ CoseSign1: 18,
+ /// application/link-format
+ LinkFormat: 40,
+ /// application/xml
+ Xml: 41,
+ /// application/octet-stream
+ OctetStream: 42,
+ /// application/exi
+ Exi: 47,
+ /// application/json
+ Json: 50,
+ /// application/json-patch+json
+ JsonPatchJson: 51,
+ /// application/merge-patch+json
+ MergePatchJson: 52,
+ /// application/cbor
+ Cbor: 60,
+ /// application/cwt
+ Cwt: 61,
+ /// application/multipart-core
+ MultipartCore: 62,
+ /// application/cbor-seq
+ CborSeq: 63,
+ /// application/cose; cose-type="cose-encrypt"
+ CoseEncrypt: 96,
+ /// application/cose; cose-type="cose-mac"
+ CoseMac: 97,
+ /// application/cose; cose-type="cose-sign"
+ CoseSign: 98,
+ /// application/cose-key
+ CoseKey: 101,
+ /// application/cose-key-set
+ CoseKeySet: 102,
+ /// application/senml+json
+ SenmlJson: 110,
+ /// application/sensml+json
+ SensmlJson: 111,
+ /// application/senml+cbor
+ SenmlCbor: 112,
+ /// application/sensml+cbor
+ SensmlCbor: 113,
+ /// application/senml-exi
+ SenmlExi: 114,
+ /// application/sensml-exi
+ SensmlExi: 115,
+ /// application/coap-group+json
+ CoapGroupJson: 256,
+ /// application/dots+cbor
+ DotsCbor: 271,
+ /// application/pkcs7-mime; smime-type=server-generated-key
+ Pkcs7MimeSmimeTypeServerGeneratedKey: 280,
+ /// application/pkcs7-mime; smime-type=certs-only
+ Pkcs7MimeSmimeTypeCertsOnly: 281,
+ /// application/pkcs7-mime; smime-type=CMC-Request
+ Pkcs7MimeSmimeTypeCmcRequest: 282,
+ /// application/pkcs7-mime; smime-type=CMC-Response
+ Pkcs7MimeSmimeTypeCmcResponse: 283,
+ /// application/pkcs8
+ Pkcs8: 284,
+ /// application/csrattrs
+ Csrattrs: 285,
+ /// application/pkcs10
+ Pkcs10: 286,
+ /// application/pkix-cert
+ PkixCert: 287,
+ /// application/senml+xml
+ SenmlXml: 310,
+ /// application/sensml+xml
+ SensmlXml: 311,
+ /// application/senml-etch+json
+ SenmlEtchJson: 320,
+ /// application/senml-etch+cbor
+ SenmlEtchCbor: 322,
+ /// application/td+json
+ TdJson: 432,
+ /// application/vnd.ocf+cbor
+ VndOcfCbor: 10000,
+ /// application/oscore
+ Oscore: 10001,
+ // application/json deflate
+ JsonDeflate: 11050,
+ // application/cbor deflate
+ CborDeflate: 11060,
+ /// application/vnd.oma.lwm2m+tlv
+ VndOmaLwm2mTlv: 11542,
+ /// application/vnd.oma.lwm2m+json
+ VndOmaLwm2mJson: 11543,
+ /// application/vnd.oma.lwm2m+cbor
+ VndOmaLwm2mCbor: 11544,
+ }
+}
diff --git a/src/iana/tests.rs b/src/iana/tests.rs
new file mode 100644
index 0000000..52cb203
--- /dev/null
+++ b/src/iana/tests.rs
@@ -0,0 +1,38 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+use super::*;
+
+#[test]
+fn test_algorithm_conversion() {
+ assert_eq!(Some(Algorithm::ES256), Algorithm::from_i64(-7));
+ assert_eq!(Some(Algorithm::A128GCM), Algorithm::from_i64(1));
+ assert_eq!(Algorithm::A128GCM as i64, 1);
+ assert_eq!(None, Algorithm::from_i64(8));
+ assert_eq!(None, Algorithm::from_i64(-65538));
+}
+
+#[test]
+fn test_header_param_private_range() {
+ assert!(!HeaderParameter::is_private(1));
+ assert!(HeaderParameter::is_private(-70_000));
+}
+
+#[test]
+fn test_elliptic_curve_private_range() {
+ assert!(!EllipticCurve::is_private(1));
+ assert!(EllipticCurve::is_private(-70_000));
+}
diff --git a/src/key/mod.rs b/src/key/mod.rs
new file mode 100644
index 0000000..f0c1eea
--- /dev/null
+++ b/src/key/mod.rs
@@ -0,0 +1,280 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+//! COSE_Key functionality.
+
+use crate::{
+ cbor::value::Value,
+ iana,
+ iana::EnumI64,
+ util::{to_cbor_array, AsCborValue, ValueTryAs},
+ Algorithm, CoseError, Label, Result,
+};
+use alloc::{collections::BTreeSet, vec, vec::Vec};
+
+#[cfg(test)]
+mod tests;
+
+/// Key type.
+pub type KeyType = crate::RegisteredLabel<iana::KeyType>;
+
+impl Default for KeyType {
+ fn default() -> Self {
+ KeyType::Assigned(iana::KeyType::Reserved)
+ }
+}
+
+/// Key operation.
+pub type KeyOperation = crate::RegisteredLabel<iana::KeyOperation>;
+
+/// A collection of [`CoseKey`] objects.
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct CoseKeySet(pub Vec<CoseKey>);
+
+impl crate::CborSerializable for CoseKeySet {}
+
+impl AsCborValue for CoseKeySet {
+ fn from_cbor_value(value: Value) -> Result<Self> {
+ Ok(Self(
+ value.try_as_array_then_convert(CoseKey::from_cbor_value)?,
+ ))
+ }
+
+ fn to_cbor_value(self) -> Result<Value> {
+ to_cbor_array(self.0)
+ }
+}
+
+/// Structure representing a cryptographic key.
+///
+/// ```cddl
+/// COSE_Key = {
+/// 1 => tstr / int, ; kty
+/// ? 2 => bstr, ; kid
+/// ? 3 => tstr / int, ; alg
+/// ? 4 => [+ (tstr / int) ], ; key_ops
+/// ? 5 => bstr, ; Base IV
+/// * label => values
+/// }
+/// ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct CoseKey {
+ /// Key type identification.
+ pub kty: KeyType,
+ /// Key identification.
+ pub key_id: Vec<u8>,
+ /// Key use restriction to this algorithm.
+ pub alg: Option<Algorithm>,
+ /// Restrict set of possible operations.
+ pub key_ops: BTreeSet<KeyOperation>,
+ /// Base IV to be xor-ed with partial IVs.
+ pub base_iv: Vec<u8>,
+ /// Any additional parameter (label,value) pairs. If duplicate labels are present,
+ /// CBOR-encoding will fail.
+ pub params: Vec<(Label, Value)>,
+}
+
+impl crate::CborSerializable for CoseKey {}
+
+const KTY: Label = Label::Int(iana::KeyParameter::Kty as i64);
+const KID: Label = Label::Int(iana::KeyParameter::Kid as i64);
+const ALG: Label = Label::Int(iana::KeyParameter::Alg as i64);
+const KEY_OPS: Label = Label::Int(iana::KeyParameter::KeyOps as i64);
+const BASE_IV: Label = Label::Int(iana::KeyParameter::BaseIv as i64);
+
+impl AsCborValue for CoseKey {
+ fn from_cbor_value(value: Value) -> Result<Self> {
+ let m = value.try_as_map()?;
+ let mut key = Self::default();
+ let mut seen = BTreeSet::new();
+ for (l, value) in m.into_iter() {
+ // The `ciborium` CBOR library does not police duplicate map keys.
+ // RFC 8152 section 14 requires that COSE does police duplicates, so do it here.
+ let label = Label::from_cbor_value(l)?;
+ if seen.contains(&label) {
+ return Err(CoseError::DuplicateMapKey);
+ }
+ seen.insert(label.clone());
+ match label {
+ KTY => key.kty = KeyType::from_cbor_value(value)?,
+
+ KID => {
+ key.key_id = value.try_as_nonempty_bytes()?;
+ }
+
+ ALG => key.alg = Some(Algorithm::from_cbor_value(value)?),
+
+ KEY_OPS => {
+ let key_ops = value.try_as_array()?;
+ for key_op in key_ops.into_iter() {
+ if !key.key_ops.insert(KeyOperation::from_cbor_value(key_op)?) {
+ return Err(CoseError::UnexpectedItem(
+ "repeated array entry",
+ "unique array label",
+ ));
+ }
+ }
+ if key.key_ops.is_empty() {
+ return Err(CoseError::UnexpectedItem("empty array", "non-empty array"));
+ }
+ }
+
+ BASE_IV => {
+ key.base_iv = value.try_as_nonempty_bytes()?;
+ }
+
+ label => key.params.push((label, value)),
+ }
+ }
+ // Check that key type has been set.
+ if key.kty == KeyType::Assigned(iana::KeyType::Reserved) {
+ return Err(CoseError::UnexpectedItem(
+ "no kty label",
+ "mandatory kty label",
+ ));
+ }
+
+ Ok(key)
+ }
+
+ fn to_cbor_value(self) -> Result<Value> {
+ let mut map: Vec<(Value, Value)> = vec![(KTY.to_cbor_value()?, self.kty.to_cbor_value()?)];
+ if !self.key_id.is_empty() {
+ map.push((KID.to_cbor_value()?, Value::Bytes(self.key_id)));
+ }
+ if let Some(alg) = self.alg {
+ map.push((ALG.to_cbor_value()?, alg.to_cbor_value()?));
+ }
+ if !self.key_ops.is_empty() {
+ map.push((KEY_OPS.to_cbor_value()?, to_cbor_array(self.key_ops)?));
+ }
+ if !self.base_iv.is_empty() {
+ map.push((BASE_IV.to_cbor_value()?, Value::Bytes(self.base_iv)));
+ }
+ let mut seen = BTreeSet::new();
+ for (label, value) in self.params {
+ if seen.contains(&label) {
+ return Err(CoseError::DuplicateMapKey);
+ }
+ seen.insert(label.clone());
+ map.push((label.to_cbor_value()?, value));
+ }
+ Ok(Value::Map(map))
+ }
+}
+
+/// Builder for [`CoseKey`] objects.
+#[derive(Debug, Default)]
+pub struct CoseKeyBuilder(CoseKey);
+
+impl CoseKeyBuilder {
+ builder! {CoseKey}
+ builder_set! {key_id: Vec<u8>}
+ builder_set! {base_iv: Vec<u8>}
+
+ /// Constructor for an elliptic curve public key specified by `x` and `y` coordinates.
+ pub fn new_ec2_pub_key(curve: iana::EllipticCurve, x: Vec<u8>, y: Vec<u8>) -> Self {
+ Self(CoseKey {
+ kty: KeyType::Assigned(iana::KeyType::EC2),
+ params: vec![
+ (
+ Label::Int(iana::Ec2KeyParameter::Crv as i64),
+ Value::from(curve as u64),
+ ),
+ (Label::Int(iana::Ec2KeyParameter::X as i64), Value::Bytes(x)),
+ (Label::Int(iana::Ec2KeyParameter::Y as i64), Value::Bytes(y)),
+ ],
+ ..Default::default()
+ })
+ }
+
+ /// Constructor for an elliptic curve public key specified by `x` coordinate plus sign of `y`
+ /// coordinate.
+ pub fn new_ec2_pub_key_y_sign(curve: iana::EllipticCurve, x: Vec<u8>, y_sign: bool) -> Self {
+ Self(CoseKey {
+ kty: KeyType::Assigned(iana::KeyType::EC2),
+ params: vec![
+ (
+ Label::Int(iana::Ec2KeyParameter::Crv as i64),
+ Value::from(curve as u64),
+ ),
+ (Label::Int(iana::Ec2KeyParameter::X as i64), Value::Bytes(x)),
+ (
+ Label::Int(iana::Ec2KeyParameter::Y as i64),
+ Value::Bool(y_sign),
+ ),
+ ],
+ ..Default::default()
+ })
+ }
+
+ /// Constructor for an elliptic curve private key specified by `d`, together with public `x` and
+ /// `y` coordinates.
+ pub fn new_ec2_priv_key(
+ curve: iana::EllipticCurve,
+ x: Vec<u8>,
+ y: Vec<u8>,
+ d: Vec<u8>,
+ ) -> Self {
+ let mut builder = Self::new_ec2_pub_key(curve, x, y);
+ builder
+ .0
+ .params
+ .push((Label::Int(iana::Ec2KeyParameter::D as i64), Value::Bytes(d)));
+ builder
+ }
+
+ /// Constructor for a symmetric key specified by `k`.
+ pub fn new_symmetric_key(k: Vec<u8>) -> Self {
+ Self(CoseKey {
+ kty: KeyType::Assigned(iana::KeyType::Symmetric),
+ params: vec![(
+ Label::Int(iana::SymmetricKeyParameter::K as i64),
+ Value::Bytes(k),
+ )],
+ ..Default::default()
+ })
+ }
+
+ /// Set the algorithm.
+ #[must_use]
+ pub fn algorithm(mut self, alg: iana::Algorithm) -> Self {
+ self.0.alg = Some(Algorithm::Assigned(alg));
+ self
+ }
+
+ /// Add a key operation.
+ #[must_use]
+ pub fn add_key_op(mut self, op: iana::KeyOperation) -> Self {
+ self.0.key_ops.insert(KeyOperation::Assigned(op));
+ self
+ }
+
+ /// Set a parameter value.
+ ///
+ /// # Panics
+ ///
+ /// This function will panic if it used to set a parameter label from the [`iana::KeyParameter`]
+ /// range.
+ #[must_use]
+ pub fn param(mut self, label: i64, value: Value) -> Self {
+ if iana::KeyParameter::from_i64(label).is_some() {
+ panic!("param() method used to set KeyParameter"); // safe: invalid input
+ }
+ self.0.params.push((Label::Int(label), value));
+ self
+ }
+}
diff --git a/src/key/tests.rs b/src/key/tests.rs
new file mode 100644
index 0000000..713fe1a
--- /dev/null
+++ b/src/key/tests.rs
@@ -0,0 +1,719 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+use super::*;
+use crate::{cbor::value::Value, iana, util::expect_err, CborSerializable};
+use alloc::{borrow::ToOwned, vec};
+
+#[test]
+fn test_cose_key_encode() {
+ let tests = vec![
+ (
+ CoseKey {
+ kty: KeyType::Assigned(iana::KeyType::OKP),
+ key_id: vec![1, 2, 3],
+ ..Default::default()
+ },
+ concat!(
+ "a2", // 2-map
+ "01", "01", // 1 (kty) => OKP
+ "02", "43", "010203" // 2 (kid) => 3-bstr
+ ),
+ ),
+ (
+ CoseKey {
+ kty: KeyType::Assigned(iana::KeyType::OKP),
+ ..Default::default()
+ },
+ concat!(
+ "a1", // 1-map
+ "01", "01", // 1 (kty) => OKP
+ ),
+ ),
+ (
+ CoseKey {
+ kty: KeyType::Text("bc".to_owned()),
+ ..Default::default()
+ },
+ concat!(
+ "a1", // 1-map
+ "01", "62", "6263" // 1 (kty) => "bc"
+ ),
+ ),
+ (
+ CoseKey {
+ kty: KeyType::Assigned(iana::KeyType::OKP),
+ base_iv: vec![3, 2, 1],
+ ..Default::default()
+ },
+ concat!(
+ "a2", // 2-map
+ "01", "01", // 1 (kty) => OKP
+ "05", "43", "030201", // 5 (base_iv) => 3-bstr
+ ),
+ ),
+ (
+ CoseKey {
+ kty: KeyType::Assigned(iana::KeyType::OKP),
+ alg: Some(Algorithm::Assigned(iana::Algorithm::ES256)),
+ ..Default::default()
+ },
+ concat!(
+ "a2", // 2-map
+ "01", "01", // 1 (kty) => OKP
+ "03", "26", // 3 (alg) => -7
+ ),
+ ),
+ (
+ CoseKey {
+ kty: KeyType::Assigned(iana::KeyType::OKP),
+ alg: Some(Algorithm::PrivateUse(-70_000)),
+ ..Default::default()
+ },
+ concat!(
+ "a2", // 2-map
+ "01", "01", // 1 (kty) => OKP
+ "03", "3a", "0001116f", // 3 (alg) => -70000
+ ),
+ ),
+ (
+ CoseKey {
+ kty: KeyType::Assigned(iana::KeyType::OKP),
+ alg: Some(Algorithm::Text("abc".to_owned())),
+ ..Default::default()
+ },
+ concat!(
+ "a2", // 2-map
+ "01", "01", // 1 (kty) => OKP
+ "03", "63", "616263", // 3 (alg) => "abc"
+ ),
+ ),
+ (
+ CoseKey {
+ kty: KeyType::Assigned(iana::KeyType::OKP),
+ key_id: vec![1, 2, 3],
+ key_ops: vec![
+ KeyOperation::Assigned(iana::KeyOperation::Encrypt),
+ KeyOperation::Assigned(iana::KeyOperation::Decrypt),
+ KeyOperation::Text("abc".to_owned()),
+ ]
+ .into_iter()
+ .collect(),
+ ..Default::default()
+ },
+ concat!(
+ "a3", // 3-map
+ "01", "01", // 1 (kty) => OKP
+ "02", "43", "010203", // 2 (kid) => 3-bstr
+ "04", "83", "03", "04", "63616263", // 4 (key_ops) => 3-tuple [3,4,"abc"]
+ ),
+ ),
+ (
+ CoseKey {
+ kty: KeyType::Assigned(iana::KeyType::OKP),
+ params: vec![
+ (Label::Int(0x46), Value::from(0x47)),
+ (Label::Int(0x66), Value::from(0x67)),
+ ],
+ ..Default::default()
+ },
+ concat!(
+ "a3", // 3-map
+ "01", "01", // 1 (kty) => OKP
+ "1846", "1847", // 46 => 47 (note canonical ordering)
+ "1866", "1867", // 66 => 67
+ ),
+ ),
+ (
+ CoseKey {
+ kty: KeyType::Assigned(iana::KeyType::OKP),
+ params: vec![
+ (Label::Int(0x1234), Value::from(0x47)),
+ (Label::Text("a".to_owned()), Value::from(0x67)),
+ ],
+ ..Default::default()
+ },
+ concat!(
+ "a3", // 3-map
+ "01", "01", // 1 (kty) => OKP
+ // note canonical ordering: lexicographic
+ "191234", "1847", // 0x1234 => 47
+ "6161", "1867", // "a" => 67
+ ),
+ ),
+ (
+ CoseKey {
+ kty: KeyType::Assigned(iana::KeyType::OKP),
+ params: vec![
+ (Label::Int(0x66), Value::from(0x67)),
+ (Label::Text("a".to_owned()), Value::from(0x47)),
+ ],
+ ..Default::default()
+ },
+ concat!(
+ "a3", // 3-map
+ "01", "01", // 1 (kty) => OKP
+ "1866", "1867", // 66 => 67
+ "6161", "1847", // "a" => 47
+ ),
+ ),
+ (
+ CoseKeyBuilder::new_ec2_pub_key(
+ iana::EllipticCurve::P_256,
+ hex::decode("6b4ad240073b99cad65ab8417ce29c6844ad0ae77ce8b3f7e41233f5b9129465")
+ .unwrap(),
+ hex::decode("a7dc1c39391ab300f7b1787b6e569a031dd0750fe2509b880a41f06666fff785")
+ .unwrap(),
+ )
+ .algorithm(iana::Algorithm::ES256)
+ .param(-70000, Value::Null)
+ .build(),
+ concat!(
+ "a60102032620012158206b4ad240073b",
+ "99cad65ab8417ce29c6844ad0ae77ce8",
+ "b3f7e41233f5b9129465225820a7dc1c",
+ "39391ab300f7b1787b6e569a031dd075",
+ "0fe2509b880a41f06666fff7853a0001",
+ "116ff6"
+ ),
+ ),
+ (
+ CoseKeyBuilder::new_ec2_pub_key_y_sign(
+ iana::EllipticCurve::P_256,
+ hex::decode("aabbcc").unwrap(),
+ false,
+ )
+ .build(),
+ concat!(
+ "a4", // 3-map
+ "01", "02", // 1 (kty) => 2 (EC2)
+ "20", "01", // -1 (crv) => 1 (P_256)
+ "21", "43", "aabbcc", // -2 (x) => 3-bstr
+ "22", "f4" // -3 (y) => false
+ ),
+ ),
+ ];
+ for (i, (key, key_data)) in tests.iter().enumerate() {
+ let got = key.clone().to_vec().unwrap();
+ assert_eq!(*key_data, hex::encode(&got), "case {}", i);
+
+ let got = CoseKey::from_slice(&got).unwrap();
+ assert_eq!(*key, got);
+ }
+
+ // Now combine all of the keys into a `CoseKeySet`
+ let keyset = CoseKeySet(tests.iter().map(|(l, _v)| l.clone()).collect());
+ let mut keyset_data: Vec<u8> = vec![0x80u8 + (tests.len() as u8)]; // assumes fewer than 24 keys
+ for (_, key_data) in tests.iter() {
+ keyset_data.extend_from_slice(&hex::decode(key_data).unwrap());
+ }
+ let got_data = keyset.clone().to_vec().unwrap();
+ assert_eq!(hex::encode(keyset_data), hex::encode(&got_data));
+
+ let got = CoseKeySet::from_slice(&got_data).unwrap();
+ assert_eq!(got, keyset);
+}
+
+#[test]
+fn test_rfc8152_public_cose_key_decode() {
+ // Public keys from RFC8152 section 6.7.1.
+ // Note that map contents have been reordered into canonical order.
+ let tests = vec![
+ (
+ CoseKeyBuilder::new_ec2_pub_key(
+ iana::EllipticCurve::P_256,
+ hex::decode("65eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d").unwrap(),
+ hex::decode("1e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c").unwrap(),
+ ).key_id(b"meriadoc.brandybuck@buckland.example".to_vec()).build(),
+ concat!(
+ "a5",
+ "0102",
+ "0258246d65726961646f632e6272616e64796275636b406275636b6c616e642e6578616d706c65",
+ "2001",
+ "21582065eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d",
+ "2258201e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c",
+ ),
+ ),
+ (
+ CoseKeyBuilder::new_ec2_pub_key(
+ iana::EllipticCurve::P_256,
+ hex::decode("bac5b11cad8f99f9c72b05cf4b9e26d244dc189f745228255a219a86d6a09eff").unwrap(),
+ hex::decode("20138bf82dc1b6d562be0fa54ab7804a3a64b6d72ccfed6b6fb6ed28bbfc117e").unwrap(),
+ ).key_id(b"11".to_vec()).build(),
+ concat!("a5",
+ "0102",
+ "02423131",
+ "2001",
+ "215820bac5b11cad8f99f9c72b05cf4b9e26d244dc189f745228255a219a86d6a09eff",
+ "22582020138bf82dc1b6d562be0fa54ab7804a3a64b6d72ccfed6b6fb6ed28bbfc117e",
+ ),
+ ),
+ (
+ CoseKeyBuilder::new_ec2_pub_key(
+ iana::EllipticCurve::P_521,
+ hex::decode("0072992cb3ac08ecf3e5c63dedec0d51a8c1f79ef2f82f94f3c737bf5de7986671eac625fe8257bbd0394644caaa3aaf8f27a4585fbbcad0f2457620085e5c8f42ad").unwrap(),
+ hex::decode("01dca6947bce88bc5790485ac97427342bc35f887d86d65a089377e247e60baa55e4e8501e2ada5724ac51d6909008033ebc10ac999b9d7f5cc2519f3fe1ea1d9475").unwrap(),
+ ).key_id(
+ b"bilbo.baggins@hobbiton.example".to_vec()).build(),
+ concat!("a5",
+ "0102",
+ "02581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c65",
+ "2003",
+ "2158420072992cb3ac08ecf3e5c63dedec0d51a8c1f79ef2f82f94f3c737bf5de7986671eac625fe8257bbd0394644caaa3aaf8f27a4585fbbcad0f2457620085e5c8f42ad",
+ "22584201dca6947bce88bc5790485ac97427342bc35f887d86d65a089377e247e60baa55e4e8501e2ada5724ac51d6909008033ebc10ac999b9d7f5cc2519f3fe1ea1d9475",
+ ),
+ ),
+ (
+ CoseKeyBuilder::new_ec2_pub_key(
+ iana::EllipticCurve::P_256,
+ hex::decode("98f50a4ff6c05861c8860d13a638ea56c3f5ad7590bbfbf054e1c7b4d91d6280").unwrap(),
+ hex::decode("f01400b089867804b8e9fc96c3932161f1934f4223069170d924b7e03bf822bb").unwrap(),
+ ).key_id(b"peregrin.took@tuckborough.example".to_vec()).build(),
+ concat!("a5",
+ "0102",
+ "025821706572656772696e2e746f6f6b407475636b626f726f7567682e6578616d706c65",
+ "2001",
+ "21582098f50a4ff6c05861c8860d13a638ea56c3f5ad7590bbfbf054e1c7b4d91d6280",
+ "225820f01400b089867804b8e9fc96c3932161f1934f4223069170d924b7e03bf822bb",
+ )
+ ),
+ ];
+ for (i, (key, key_data)) in tests.iter().enumerate() {
+ let got = key.clone().to_vec().unwrap();
+ assert_eq!(*key_data, hex::encode(&got), "case {}", i);
+
+ let got = CoseKey::from_slice(&got).unwrap();
+ assert_eq!(*key, got);
+ }
+
+ // Now combine all of the keys into a `CoseKeySet`
+ let keyset = CoseKeySet(tests.iter().map(|(l, _v)| l.clone()).collect());
+ let mut keyset_data: Vec<u8> = vec![0x80u8 + (tests.len() as u8)]; // assumes fewer than 24 keys
+ for (_, key_data) in tests.iter() {
+ keyset_data.extend_from_slice(&hex::decode(key_data).unwrap());
+ }
+ let got = keyset.to_vec().unwrap();
+ assert_eq!(hex::encode(keyset_data), hex::encode(got));
+}
+
+#[test]
+fn test_rfc8152_private_cose_key_decode() {
+ // Private keys from RFC8152 section 6.7.2.
+ // Note that map contents have been reordered into canonical order.
+ let tests = vec![
+ (
+ CoseKeyBuilder::new_ec2_priv_key(
+ iana::EllipticCurve::P_256,
+ hex::decode("65eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d").unwrap(),
+ hex::decode("1e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c").unwrap(),
+ hex::decode("aff907c99f9ad3aae6c4cdf21122bce2bd68b5283e6907154ad911840fa208cf").unwrap(),
+ ).key_id(b"meriadoc.brandybuck@buckland.example".to_vec()).build(),
+ concat!(
+ "a6",
+ "0102",
+ "0258246d65726961646f632e6272616e64796275636b406275636b6c616e642e6578616d706c65",
+ "2001",
+ "21582065eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d",
+ "2258201e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c",
+ "235820aff907c99f9ad3aae6c4cdf21122bce2bd68b5283e6907154ad911840fa208cf",
+ ),
+ ),
+ (
+ CoseKeyBuilder::new_ec2_priv_key(
+ iana::EllipticCurve::P_256,
+ hex::decode("bac5b11cad8f99f9c72b05cf4b9e26d244dc189f745228255a219a86d6a09eff").unwrap(),
+ hex::decode("20138bf82dc1b6d562be0fa54ab7804a3a64b6d72ccfed6b6fb6ed28bbfc117e").unwrap(),
+ hex::decode("57c92077664146e876760c9520d054aa93c3afb04e306705db6090308507b4d3").unwrap(),
+ ).key_id(b"11".to_vec()).build(),
+ concat!("a6",
+ "0102",
+ "02423131",
+ "2001",
+ "215820bac5b11cad8f99f9c72b05cf4b9e26d244dc189f745228255a219a86d6a09eff",
+ "22582020138bf82dc1b6d562be0fa54ab7804a3a64b6d72ccfed6b6fb6ed28bbfc117e",
+ "23582057c92077664146e876760c9520d054aa93c3afb04e306705db6090308507b4d3",
+ ),
+ ),
+ (
+ CoseKeyBuilder::new_ec2_priv_key(
+ iana::EllipticCurve::P_521,
+ hex::decode("0072992cb3ac08ecf3e5c63dedec0d51a8c1f79ef2f82f94f3c737bf5de7986671eac625fe8257bbd0394644caaa3aaf8f27a4585fbbcad0f2457620085e5c8f42ad").unwrap(),
+ hex::decode("01dca6947bce88bc5790485ac97427342bc35f887d86d65a089377e247e60baa55e4e8501e2ada5724ac51d6909008033ebc10ac999b9d7f5cc2519f3fe1ea1d9475").unwrap(),
+ hex::decode("00085138ddabf5ca975f5860f91a08e91d6d5f9a76ad4018766a476680b55cd339e8ab6c72b5facdb2a2a50ac25bd086647dd3e2e6e99e84ca2c3609fdf177feb26d").unwrap(),
+ ).key_id(b"bilbo.baggins@hobbiton.example".to_vec()).build(),
+ concat!("a6",
+ "0102",
+ "02581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c65",
+ "2003",
+ "2158420072992cb3ac08ecf3e5c63dedec0d51a8c1f79ef2f82f94f3c737bf5de7986671eac625fe8257bbd0394644caaa3aaf8f27a4585fbbcad0f2457620085e5c8f42ad",
+ "22584201dca6947bce88bc5790485ac97427342bc35f887d86d65a089377e247e60baa55e4e8501e2ada5724ac51d6909008033ebc10ac999b9d7f5cc2519f3fe1ea1d9475",
+ "23584200085138ddabf5ca975f5860f91a08e91d6d5f9a76ad4018766a476680b55cd339e8ab6c72b5facdb2a2a50ac25bd086647dd3e2e6e99e84ca2c3609fdf177feb26d",
+ ),
+ ),
+ (
+ CoseKey {
+ kty: KeyType::Assigned(iana::KeyType::Symmetric),
+ key_id: b"our-secret".to_vec(),
+ params: vec![
+ (Label::Int(iana::SymmetricKeyParameter::K as i64) ,
+ Value::Bytes(hex::decode("849b57219dae48de646d07dbb533566e976686457c1491be3a76dcea6c427188").unwrap())),
+ ],
+ ..Default::default()
+ },
+ concat!("a3",
+ "0104",
+ "024a6f75722d736563726574",
+ "205820849b57219dae48de646d07dbb533566e976686457c1491be3a76dcea6c427188",
+ ),
+ ),
+ (
+ CoseKeyBuilder::new_ec2_priv_key(
+ iana::EllipticCurve::P_256,
+ hex::decode("98f50a4ff6c05861c8860d13a638ea56c3f5ad7590bbfbf054e1c7b4d91d6280").unwrap(),
+ hex::decode("f01400b089867804b8e9fc96c3932161f1934f4223069170d924b7e03bf822bb").unwrap(),
+ hex::decode("02d1f7e6f26c43d4868d87ceb2353161740aacf1f7163647984b522a848df1c3").unwrap(),
+ ).key_id(b"peregrin.took@tuckborough.example".to_vec()).build(),
+ concat!("a6",
+ "0102",
+ "025821706572656772696e2e746f6f6b407475636b626f726f7567682e6578616d706c65",
+ "2001",
+ "21582098f50a4ff6c05861c8860d13a638ea56c3f5ad7590bbfbf054e1c7b4d91d6280",
+ "225820f01400b089867804b8e9fc96c3932161f1934f4223069170d924b7e03bf822bb",
+ "23582002d1f7e6f26c43d4868d87ceb2353161740aacf1f7163647984b522a848df1c3",
+ )
+ ),
+ (
+ CoseKey {
+ kty: KeyType::Assigned(iana::KeyType::Symmetric),
+ key_id: b"our-secret2".to_vec(),
+ params: vec![(
+ Label::Int(iana::SymmetricKeyParameter::K as i64) ,
+ Value::Bytes(hex::decode("849b5786457c1491be3a76dcea6c4271").unwrap()),
+ )],
+ ..Default::default()
+ },
+ concat!("a3",
+ "0104",
+ "024b6f75722d73656372657432",
+ "2050849b5786457c1491be3a76dcea6c4271",
+ ),
+ ),
+ (
+ CoseKey {
+ kty: KeyType::Assigned(iana::KeyType::Symmetric),
+ key_id: b"018c0ae5-4d9b-471b-bfd6-eef314bc7037".to_vec(),
+ params: vec![(
+ Label::Int(iana::SymmetricKeyParameter::K as i64) ,
+ Value::Bytes(hex::decode("849b57219dae48de646d07dbb533566e976686457c1491be3a76dcea6c427188").unwrap()),
+ )],
+ ..Default::default()
+ },
+ concat!("a3",
+ "0104",
+ "02582430313863306165352d346439622d343731622d626664362d656566333134626337303337",
+ "205820849b57219dae48de646d07dbb533566e976686457c1491be3a76dcea6c427188",
+ ),
+ ),
+ ];
+ for (i, (key, key_data)) in tests.iter().enumerate() {
+ let got = key.clone().to_vec().unwrap();
+ assert_eq!(*key_data, hex::encode(&got), "case {}", i);
+
+ let got = CoseKey::from_slice(&got).unwrap();
+ assert_eq!(*key, got);
+ }
+
+ // Now combine all of the keys into a `CoseKeySet`
+ let keyset = CoseKeySet(tests.iter().map(|(l, _v)| l.clone()).collect());
+ let mut keyset_data: Vec<u8> = vec![0x80u8 + (tests.len() as u8)]; // assumes fewer than 24 keys
+ for (_, key_data) in tests.iter() {
+ keyset_data.extend_from_slice(&hex::decode(key_data).unwrap());
+ }
+ let got = keyset.to_vec().unwrap();
+ assert_eq!(hex::encode(keyset_data), hex::encode(got));
+}
+
+#[test]
+fn test_cose_key_decode_fail() {
+ let tests = vec![
+ (
+ concat!(
+ "82", // 2-tuple (invalid)
+ "01", "01", // 1 (kty) => OKP
+ ),
+ "expected map",
+ ),
+ (
+ concat!(
+ "a2", // 2-map
+ "01", "11", // 1 (kty) => invalid value
+ "02", "43", "010203" // 2 (kid) => 3-bstr
+ ),
+ "expected recognized IANA value",
+ ),
+ (
+ concat!(
+ "a2", // 2-map
+ "01", "4101", // 1 (kty) => 1-bstr (invalid value type)
+ "02", "43", "010203" // 2 (kid) => 3-bstr
+ ),
+ "expected int/tstr",
+ ),
+ (
+ concat!(
+ "a1", // 1-map (no kty value)
+ "02", "41", "01", // 2 (kid) => 1-bstr
+ ),
+ "expected mandatory kty label",
+ ),
+ (
+ concat!(
+ "a2", // 2-map
+ "01", "01", // 1 (kty) => OKP
+ "02", "40", // 2 (kid) => 0-bstr
+ ),
+ "expected non-empty bstr",
+ ),
+ (
+ concat!(
+ "a2", // 2-map
+ "01", "01", // 1 (kty) => OKP
+ "02", "01", // 2 (kid) => int (invalid value type)
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "a2", // 2-map
+ "01", "01", // 1 (kty) => OKP
+ "03", "1899", // 3 (alg) => 0x99
+ ),
+ "expected value in IANA or private use range",
+ ),
+ (
+ concat!(
+ "a2", // 2-map
+ "01", "01", // 1 (kty) => OKP
+ "03", "4101", // 3 (alg) => 1-bstr (invalid value type)
+ ),
+ "expected int/tstr",
+ ),
+ (
+ concat!(
+ "a2", // 2-map
+ "01", "01", // 1 (kty) => OKP
+ "04", "4101", // 4 (key_ops) => 1-bstr (invalid value type)
+ ),
+ "expected array",
+ ),
+ (
+ concat!(
+ "a2", // 2-map
+ "01", "01", // 1 (kty) => OKP
+ "04", "82", "03", "03", // 4 (key_ops) => 3-tuple [3,3]
+ ),
+ "expected unique array label",
+ ),
+ (
+ concat!(
+ "a2", // 2-map
+ "01", "01", // 1 (kty) => OKP
+ "04", "80", // 4 (key_ops) => 0-tuple []
+ ),
+ "expected non-empty array",
+ ),
+ (
+ concat!(
+ "a2", // 2-map
+ "01", "01", // 1 (kty) => OKP
+ "04", "82", "03", "0b", // 4 (key_ops) => 3-tuple [3,11]
+ ),
+ "expected recognized IANA value",
+ ),
+ (
+ concat!(
+ "a2", // 2-map
+ "01", "01", // 1 (kty) => OKP
+ "05", "40", // 5 (base_iv) => 0-bstr
+ ),
+ "expected non-empty bstr",
+ ),
+ (
+ concat!(
+ "a2", // 2-map
+ "01", "01", // 1 (kty) => OKP
+ "05", "01", // 5 (base_iv) => int (invalid value type)
+ ),
+ "expected bstr",
+ ),
+ ];
+ for (key_data, err_msg) in tests.iter() {
+ let data = hex::decode(key_data).unwrap();
+ let result = CoseKey::from_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
+
+#[test]
+fn test_cose_keyset_decode_fail() {
+ let tests = vec![(
+ concat!(
+ "a1", // 1-map
+ "a1", // 1-map
+ "01", "01", // 1 (kty) => OKP
+ "00"
+ ),
+ "expected array",
+ )];
+ for (keyset_data, err_msg) in tests.iter() {
+ let data = hex::decode(keyset_data).unwrap();
+ let result = CoseKeySet::from_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
+
+#[test]
+fn test_cose_key_decode_dup_fail() {
+ let tests = vec![
+ (
+ concat!(
+ "a3", // 3-map
+ "01", "01", // 1 (kty) => OKP
+ "1866", "1867", // 66 => 67
+ "1866", "1847", // 66 => 47
+ ),
+ "duplicate map key",
+ ),
+ (
+ concat!(
+ "a3", // 3-map
+ "01", "01", // 1 (kty) => OKP
+ "02", "41", "01", // 2 (kid) => 1-bstr
+ "01", "01", // 1 (kty) => OKP (duplicate label)
+ ),
+ "duplicate map key",
+ ),
+ ];
+ for (key_data, err_msg) in tests.iter() {
+ let data = hex::decode(key_data).unwrap();
+ let result = CoseKey::from_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
+
+#[test]
+fn test_cose_key_encode_dup_fail() {
+ let tests = vec![CoseKeyBuilder::new()
+ .param(10, Value::from(0))
+ .param(10, Value::from(0))
+ .build()];
+ for key in tests {
+ let result = key.clone().to_vec();
+ expect_err(result, "duplicate map key");
+ }
+}
+
+#[test]
+fn test_key_builder() {
+ let tests = vec![
+ (
+ CoseKeyBuilder::new_symmetric_key(vec![1, 2, 3]).build(),
+ CoseKey {
+ kty: KeyType::Assigned(iana::KeyType::Symmetric),
+ params: vec![(
+ Label::Int(iana::SymmetricKeyParameter::K as i64),
+ Value::Bytes(vec![1, 2, 3]),
+ )],
+ ..Default::default()
+ },
+ ),
+ (
+ CoseKeyBuilder::new_symmetric_key(vec![1, 2, 3])
+ .algorithm(iana::Algorithm::A128GCM)
+ .build(),
+ CoseKey {
+ kty: KeyType::Assigned(iana::KeyType::Symmetric),
+ alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)),
+ params: vec![(
+ Label::Int(iana::SymmetricKeyParameter::K as i64),
+ Value::Bytes(vec![1, 2, 3]),
+ )],
+ ..Default::default()
+ },
+ ),
+ (
+ CoseKeyBuilder::new_symmetric_key(vec![1, 2, 3])
+ .key_id(vec![4, 5])
+ .build(),
+ CoseKey {
+ kty: KeyType::Assigned(iana::KeyType::Symmetric),
+ key_id: vec![4, 5],
+ params: vec![(
+ Label::Int(iana::SymmetricKeyParameter::K as i64),
+ Value::Bytes(vec![1, 2, 3]),
+ )],
+ ..Default::default()
+ },
+ ),
+ (
+ CoseKeyBuilder::new_symmetric_key(vec![1, 2, 3])
+ .add_key_op(iana::KeyOperation::Encrypt)
+ .add_key_op(iana::KeyOperation::Decrypt)
+ .build(),
+ CoseKey {
+ kty: KeyType::Assigned(iana::KeyType::Symmetric),
+ key_ops: vec![
+ KeyOperation::Assigned(iana::KeyOperation::Encrypt),
+ KeyOperation::Assigned(iana::KeyOperation::Decrypt),
+ ]
+ .into_iter()
+ .collect(),
+ params: vec![(
+ Label::Int(iana::SymmetricKeyParameter::K as i64),
+ Value::Bytes(vec![1, 2, 3]),
+ )],
+ ..Default::default()
+ },
+ ),
+ (
+ CoseKeyBuilder::new_symmetric_key(vec![1, 2, 3])
+ .base_iv(vec![4, 5])
+ .build(),
+ CoseKey {
+ kty: KeyType::Assigned(iana::KeyType::Symmetric),
+ base_iv: vec![4, 5],
+ params: vec![(
+ Label::Int(iana::SymmetricKeyParameter::K as i64),
+ Value::Bytes(vec![1, 2, 3]),
+ )],
+ ..Default::default()
+ },
+ ),
+ ];
+ for (got, want) in tests {
+ assert_eq!(got, want);
+ }
+}
+
+#[test]
+#[should_panic]
+fn test_key_builder_core_param_panic() {
+ // Attempting to set a core `KeyParameter` (in range [1,5]) via `.param()` panics.
+ let _key =
+ CoseKeyBuilder::new_ec2_pub_key(iana::EllipticCurve::P_256, vec![1, 2, 3], vec![2, 3, 4])
+ .param(1, Value::Null)
+ .build();
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..3c46fcf
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,128 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+//! Set of types for supporting [CBOR Object Signing and Encryption (COSE)][COSE].
+//!
+//! Builds on the [`ciborium`](https://docs.rs/ciborium) crate for underlying [CBOR][CBOR] support.
+//!
+//! ## Usage
+//!
+//! ```
+//! # #[derive(Copy, Clone)]
+//! # struct FakeSigner {}
+//! # impl FakeSigner {
+//! # fn sign(&self, data: &[u8]) -> Vec<u8> {
+//! # data.to_vec()
+//! # }
+//! # fn verify(&self, sig: &[u8], data: &[u8]) -> Result<(), String> {
+//! # if sig != self.sign(data) {
+//! # Err("failed to verify".to_owned())
+//! # } else {
+//! # Ok(())
+//! # }
+//! # }
+//! # }
+//! # let signer = FakeSigner {};
+//! # let verifier = signer;
+//! use coset::{iana, CborSerializable};
+//!
+//! // Inputs.
+//! let pt = b"This is the content";
+//! let aad = b"this is additional data";
+//!
+//! // Build a `CoseSign1` object.
+//! let protected = coset::HeaderBuilder::new()
+//! .algorithm(iana::Algorithm::ES256)
+//! .key_id(b"11".to_vec())
+//! .build();
+//! let sign1 = coset::CoseSign1Builder::new()
+//! .protected(protected)
+//! .payload(pt.to_vec())
+//! .create_signature(aad, |pt| signer.sign(pt)) // closure to do sign operation
+//! .build();
+//!
+//! // Serialize to bytes.
+//! let sign1_data = sign1.to_vec().unwrap();
+//! println!(
+//! "'{}' + '{}' => {}",
+//! String::from_utf8_lossy(pt),
+//! String::from_utf8_lossy(aad),
+//! hex::encode(&sign1_data)
+//! );
+//!
+//! // At the receiving end, deserialize the bytes back to a `CoseSign1` object.
+//! let mut sign1 = coset::CoseSign1::from_slice(&sign1_data).unwrap();
+//!
+//! // At this point, real code would validate the protected headers.
+//!
+//! // Check the signature, which needs to have the same `aad` provided, by
+//! // providing a closure that can do the verify operation.
+//! let result = sign1.verify_signature(aad, |sig, data| verifier.verify(sig, data));
+//! println!("Signature verified: {:?}.", result);
+//! assert!(result.is_ok());
+//!
+//! // Changing an unprotected header leaves the signature valid.
+//! sign1.unprotected.content_type = Some(coset::ContentType::Text("text/plain".to_owned()));
+//! assert!(sign1
+//! .verify_signature(aad, |sig, data| verifier.verify(sig, data))
+//! .is_ok());
+//!
+//! // Providing a different `aad` means the signature won't validate.
+//! assert!(sign1
+//! .verify_signature(b"not aad", |sig, data| verifier.verify(sig, data))
+//! .is_err());
+//!
+//! // Changing a protected header invalidates the signature.
+//! sign1.protected.original_data = None;
+//! sign1.protected.header.content_type = Some(coset::ContentType::Text("text/plain".to_owned()));
+//! assert!(sign1
+//! .verify_signature(aad, |sig, data| verifier.verify(sig, data))
+//! .is_err());
+//! ```
+//!
+//! [COSE]: https://tools.ietf.org/html/rfc8152
+//! [CBOR]: https://tools.ietf.org/html/rfc7049
+
+#![no_std]
+#![deny(rustdoc::broken_intra_doc_links)]
+extern crate alloc;
+
+/// Use std to allow building as a dylib.
+extern crate std;
+
+/// Re-export of the `ciborium` crate used for underlying CBOR encoding.
+pub use ciborium as cbor;
+
+#[macro_use]
+pub(crate) mod util;
+
+#[macro_use]
+pub mod iana;
+
+mod common;
+pub use common::*;
+mod context;
+pub use context::*;
+mod encrypt;
+pub use encrypt::*;
+mod header;
+pub use header::*;
+mod key;
+pub use key::*;
+mod mac;
+pub use mac::*;
+mod sign;
+pub use sign::*;
diff --git a/src/mac/mod.rs b/src/mac/mod.rs
new file mode 100644
index 0000000..1fb47b1
--- /dev/null
+++ b/src/mac/mod.rs
@@ -0,0 +1,346 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+//! COSE_Mac functionality.
+
+use crate::{
+ cbor,
+ cbor::value::Value,
+ iana,
+ util::{cbor_type_error, to_cbor_array, AsCborValue, ValueTryAs},
+ CoseError, CoseRecipient, Header, ProtectedHeader, Result,
+};
+use alloc::{borrow::ToOwned, vec, vec::Vec};
+
+#[cfg(test)]
+mod tests;
+
+/// Structure representing a message with authentication code (MAC).
+///
+/// ```cddl
+/// COSE_Mac = [
+/// Headers,
+/// payload : bstr / nil,
+/// tag : bstr,
+/// recipients :[+COSE_recipient]
+/// ]
+/// ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct CoseMac {
+ pub protected: ProtectedHeader,
+ pub unprotected: Header,
+ pub payload: Option<Vec<u8>>,
+ pub tag: Vec<u8>,
+ pub recipients: Vec<CoseRecipient>,
+}
+
+impl crate::CborSerializable for CoseMac {}
+
+impl crate::TaggedCborSerializable for CoseMac {
+ const TAG: u64 = iana::CborTag::CoseMac as u64;
+}
+
+impl AsCborValue for CoseMac {
+ fn from_cbor_value(value: Value) -> Result<Self> {
+ let mut a = value.try_as_array()?;
+ if a.len() != 5 {
+ return Err(CoseError::UnexpectedItem("array", "array with 5 items"));
+ }
+
+ // Remove array elements in reverse order to avoid shifts.
+ let recipients = a
+ .remove(4)
+ .try_as_array_then_convert(CoseRecipient::from_cbor_value)?;
+
+ Ok(Self {
+ recipients,
+ tag: a.remove(3).try_as_bytes()?,
+ payload: match a.remove(2) {
+ Value::Bytes(b) => Some(b),
+ Value::Null => None,
+ v => return cbor_type_error(&v, "bstr"),
+ },
+ unprotected: Header::from_cbor_value(a.remove(1))?,
+ protected: ProtectedHeader::from_cbor_bstr(a.remove(0))?,
+ })
+ }
+
+ fn to_cbor_value(self) -> Result<Value> {
+ Ok(Value::Array(vec![
+ self.protected.cbor_bstr()?,
+ self.unprotected.to_cbor_value()?,
+ match self.payload {
+ None => Value::Null,
+ Some(b) => Value::Bytes(b),
+ },
+ Value::Bytes(self.tag),
+ to_cbor_array(self.recipients)?,
+ ]))
+ }
+}
+
+impl CoseMac {
+ /// Verify the `tag` value using the provided `mac` function, feeding it
+ /// the `tag` value and the combined to-be-MACed data (in that order).
+ ///
+ /// # Panics
+ ///
+ /// This function will panic if the `payload` has not been set.
+ pub fn verify_tag<F, E>(&self, external_aad: &[u8], verify: F) -> Result<(), E>
+ where
+ F: FnOnce(&[u8], &[u8]) -> Result<(), E>,
+ {
+ let tbm = self.tbm(external_aad);
+ verify(&self.tag, &tbm)
+ }
+
+ /// Construct the to-be-MAC-ed data for this object. Any protected header values should be set
+ /// before using this method, as should the `payload`.
+ ///
+ /// # Panics
+ ///
+ /// This function will panic if the `payload` has not been set.
+ fn tbm(&self, external_aad: &[u8]) -> Vec<u8> {
+ mac_structure_data(
+ MacContext::CoseMac,
+ self.protected.clone(),
+ external_aad,
+ self.payload.as_ref().expect("payload missing"), // safe: documented
+ )
+ }
+}
+
+/// Builder for [`CoseMac`] objects.
+#[derive(Debug, Default)]
+pub struct CoseMacBuilder(CoseMac);
+
+impl CoseMacBuilder {
+ builder! {CoseMac}
+ builder_set_protected! {protected}
+ builder_set! {unprotected: Header}
+ builder_set! {tag: Vec<u8>}
+ builder_set_optional! {payload: Vec<u8>}
+
+ /// Add a [`CoseRecipient`].
+ #[must_use]
+ pub fn add_recipient(mut self, recipient: CoseRecipient) -> Self {
+ self.0.recipients.push(recipient);
+ self
+ }
+
+ /// Calculate the tag value, using `mac`. Any protected header values should be set
+ /// before using this method, as should the `payload`.
+ ///
+ /// # Panics
+ ///
+ /// This function will panic if the `payload` has not been set.
+ #[must_use]
+ pub fn create_tag<F>(self, external_aad: &[u8], create: F) -> Self
+ where
+ F: FnOnce(&[u8]) -> Vec<u8>,
+ {
+ let tbm = self.0.tbm(external_aad);
+ self.tag(create(&tbm))
+ }
+
+ /// Calculate the tag value, using `mac`. Any protected header values should be set
+ /// before using this method, as should the `payload`.
+ ///
+ /// # Panics
+ ///
+ /// This function will panic if the `payload` has not been set.
+ pub fn try_create_tag<F, E>(self, external_aad: &[u8], create: F) -> Result<Self, E>
+ where
+ F: FnOnce(&[u8]) -> Result<Vec<u8>, E>,
+ {
+ let tbm = self.0.tbm(external_aad);
+ Ok(self.tag(create(&tbm)?))
+ }
+}
+
+/// Structure representing a message with authentication code (MAC)
+/// where the relevant key is implicit.
+///
+/// ```cddl
+/// COSE_Mac0 = [
+/// Headers,
+/// payload : bstr / nil,
+/// tag : bstr,
+/// ]
+/// ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct CoseMac0 {
+ pub protected: ProtectedHeader,
+ pub unprotected: Header,
+ pub payload: Option<Vec<u8>>,
+ pub tag: Vec<u8>,
+}
+
+impl crate::CborSerializable for CoseMac0 {}
+
+impl crate::TaggedCborSerializable for CoseMac0 {
+ const TAG: u64 = iana::CborTag::CoseMac0 as u64;
+}
+
+impl AsCborValue for CoseMac0 {
+ fn from_cbor_value(value: Value) -> Result<Self> {
+ let mut a = value.try_as_array()?;
+ if a.len() != 4 {
+ return Err(CoseError::UnexpectedItem("array", "array with 4 items"));
+ }
+
+ // Remove array elements in reverse order to avoid shifts.
+ Ok(Self {
+ tag: a.remove(3).try_as_bytes()?,
+ payload: match a.remove(2) {
+ Value::Bytes(b) => Some(b),
+ Value::Null => None,
+ v => return cbor_type_error(&v, "bstr"),
+ },
+ unprotected: Header::from_cbor_value(a.remove(1))?,
+ protected: ProtectedHeader::from_cbor_bstr(a.remove(0))?,
+ })
+ }
+
+ fn to_cbor_value(self) -> Result<Value> {
+ Ok(Value::Array(vec![
+ self.protected.cbor_bstr()?,
+ self.unprotected.to_cbor_value()?,
+ match self.payload {
+ None => Value::Null,
+ Some(b) => Value::Bytes(b),
+ },
+ Value::Bytes(self.tag),
+ ]))
+ }
+}
+
+impl CoseMac0 {
+ /// Verify the `tag` value using the provided `mac` function, feeding it
+ /// the `tag` value and the combined to-be-MACed data (in that order).
+ ///
+ /// # Panics
+ ///
+ /// This function will panic if the `payload` has not been set.
+ pub fn verify_tag<F, E>(&self, external_aad: &[u8], verify: F) -> Result<(), E>
+ where
+ F: FnOnce(&[u8], &[u8]) -> Result<(), E>,
+ {
+ let tbm = self.tbm(external_aad);
+ verify(&self.tag, &tbm)
+ }
+
+ /// Construct the to-be-MAC-ed data for this object. Any protected header values should be set
+ /// before using this method, as should the `payload`.
+ ///
+ /// # Panics
+ ///
+ /// This function will panic if the `payload` has not been set.
+ fn tbm(&self, external_aad: &[u8]) -> Vec<u8> {
+ mac_structure_data(
+ MacContext::CoseMac0,
+ self.protected.clone(),
+ external_aad,
+ self.payload.as_ref().expect("payload missing"), // safe: documented
+ )
+ }
+}
+
+/// Builder for [`CoseMac0`] objects.
+#[derive(Debug, Default)]
+pub struct CoseMac0Builder(CoseMac0);
+
+impl CoseMac0Builder {
+ builder! {CoseMac0}
+ builder_set_protected! {protected}
+ builder_set! {unprotected: Header}
+ builder_set! {tag: Vec<u8>}
+ builder_set_optional! {payload: Vec<u8>}
+
+ /// Calculate the tag value, using `mac`. Any protected header values should be set
+ /// before using this method, as should the `payload`.
+ ///
+ /// # Panics
+ ///
+ /// This function will panic if the `payload` has not been set.
+ #[must_use]
+ pub fn create_tag<F>(self, external_aad: &[u8], create: F) -> Self
+ where
+ F: FnOnce(&[u8]) -> Vec<u8>,
+ {
+ let tbm = self.0.tbm(external_aad);
+ self.tag(create(&tbm))
+ }
+
+ /// Calculate the tag value, using `mac`. Any protected header values should be set
+ /// before using this method, as should the `payload`.
+ ///
+ /// # Panics
+ ///
+ /// This function will panic if the `payload` has not been set.
+ pub fn try_create_tag<F, E>(self, external_aad: &[u8], create: F) -> Result<Self, E>
+ where
+ F: FnOnce(&[u8]) -> Result<Vec<u8>, E>,
+ {
+ let tbm = self.0.tbm(external_aad);
+ Ok(self.tag(create(&tbm)?))
+ }
+}
+
+/// Possible MAC contexts.
+#[derive(Clone, Copy, Debug)]
+pub enum MacContext {
+ CoseMac,
+ CoseMac0,
+}
+
+impl MacContext {
+ /// Return the context string as per RFC 8152 section 6.3.
+ fn text(&self) -> &'static str {
+ match self {
+ MacContext::CoseMac => "MAC",
+ MacContext::CoseMac0 => "MAC0",
+ }
+ }
+}
+
+/// Create a binary blob that will be signed.
+//
+/// ```cddl
+/// MAC_structure = [
+/// context : "MAC" / "MAC0",
+/// protected : empty_or_serialized_map,
+/// external_aad : bstr,
+/// payload : bstr
+/// ]
+/// ```
+pub fn mac_structure_data(
+ context: MacContext,
+ protected: ProtectedHeader,
+ external_aad: &[u8],
+ payload: &[u8],
+) -> Vec<u8> {
+ let arr = vec![
+ Value::Text(context.text().to_owned()),
+ protected.cbor_bstr().expect("failed to serialize header"), // safe: always serializable
+ Value::Bytes(external_aad.to_vec()),
+ Value::Bytes(payload.to_vec()),
+ ];
+
+ let mut data = Vec::new();
+ cbor::ser::into_writer(&Value::Array(arr), &mut data).unwrap(); // safe: always serializable
+ data
+}
diff --git a/src/mac/tests.rs b/src/mac/tests.rs
new file mode 100644
index 0000000..9a5fffc
--- /dev/null
+++ b/src/mac/tests.rs
@@ -0,0 +1,763 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+use super::*;
+use crate::{
+ cbor::value::Value, util::expect_err, CborSerializable, ContentType, CoseKeyBuilder,
+ CoseRecipientBuilder, HeaderBuilder, TaggedCborSerializable,
+};
+use alloc::{
+ string::{String, ToString},
+ vec,
+ vec::Vec,
+};
+
+#[test]
+fn test_cose_mac_decode() {
+ let tests: Vec<(CoseMac, &'static str)> = vec![
+ (
+ CoseMacBuilder::new().build(),
+ concat!(
+ "85", // 5-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "f6", // null
+ "40", // 0-bstr
+ "80", // 0-arr
+ ),
+ ),
+ (
+ CoseMacBuilder::new().payload(vec![]).build(),
+ concat!(
+ "85", // 5-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "40", // 0-bstr
+ "40", // 0-bstr
+ "80", // 0-arr
+ ),
+ ),
+ ];
+ for (i, (mac, mac_data)) in tests.iter().enumerate() {
+ let got = mac.clone().to_vec().unwrap();
+ assert_eq!(*mac_data, hex::encode(&got), "case {}", i);
+
+ let mut got = CoseMac::from_slice(&got).unwrap();
+ got.protected.original_data = None;
+ assert_eq!(*mac, got);
+ }
+}
+
+#[test]
+fn test_cose_mac_decode_fail() {
+ let tests = vec![
+ (
+ concat!(
+ "a2", // 2-map (should be tuple)
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "4100", // 1-bstr
+ "40", // 0-bstr
+ ),
+ "expected array",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple (should be 5-tuple)
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "40", // 0-bstr
+ "40", // 0-bstr
+ ),
+ "expected array with 5 items",
+ ),
+ (
+ concat!(
+ "85", // 5-tuple
+ "80", // 0-tuple (should be bstr)
+ "a0", // 0-map
+ "40", // 0-bstr
+ "40", // 0-bstr
+ "80", // 0-arr
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "85", // 5-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "40", // 0-bstr (should be map)
+ "40", // 0-bstr
+ "40", // 0-bstr
+ "80", // 0-arr
+ ),
+ "expected map",
+ ),
+ (
+ concat!(
+ "85", // 5-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "60", // 0-tstr (should be bstr)
+ "40", // 0-bstr
+ "80", // 0-arr
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "85", // 5-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "40", // 0-bstr
+ "60", // 0-tstr
+ "80", // 0-arr
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "85", // 5-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "40", // 0-bstr
+ "40", // 0-bstr
+ "40", // 0-bstr
+ ),
+ "expected array",
+ ),
+ ];
+ for (mac_data, err_msg) in tests.iter() {
+ let data = hex::decode(mac_data).unwrap();
+ let result = CoseMac::from_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
+
+#[test]
+fn test_rfc8152_cose_mac_decode() {
+ // COSE_Mac structures from RFC 8152 section C.5.
+ let tests: Vec<(CoseMac, &'static str)> = vec![
+ (
+ CoseMacBuilder::new()
+ .protected(
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::AES_MAC_256_64)
+ .build(),
+ )
+ .payload(b"This is the content.".to_vec())
+ .tag(hex::decode("9e1226ba1f81b848").unwrap())
+ .add_recipient(
+ CoseRecipientBuilder::new()
+ .unprotected(
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::Direct)
+ .key_id(b"our-secret".to_vec())
+ .build(),
+ )
+ .ciphertext(vec![])
+ .build(),
+ )
+ .build(),
+ concat!(
+ "d861",
+ "85",
+ "43",
+ "a1010f",
+ "a0",
+ "54",
+ "546869732069732074686520636f6e74656e742e",
+ "48",
+ "9e1226ba1f81b848",
+ "81",
+ "83",
+ "40",
+ "a2",
+ "01",
+ "25",
+ "04",
+ "4a",
+ "6f75722d736563726574",
+ "40",
+ ),
+ ),
+ (
+ CoseMacBuilder::new()
+ .protected(HeaderBuilder::new().algorithm(iana::Algorithm::HMAC_256_256).build())
+ .payload(b"This is the content.".to_vec())
+ .tag(hex::decode("81a03448acd3d305376eaa11fb3fe416a955be2cbe7ec96f012c994bc3f16a41").unwrap())
+ .add_recipient(
+ CoseRecipientBuilder::new()
+ .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ECDH_SS_HKDF_256).build())
+ .unprotected(
+ HeaderBuilder::new()
+ .key_id(b"meriadoc.brandybuck@buckland.example".to_vec())
+ .value(
+ iana::HeaderAlgorithmParameter::StaticKeyId as i64,
+ Value::Bytes(b"peregrin.took@tuckborough.example".to_vec())
+ )
+ .value(
+ iana::HeaderAlgorithmParameter::PartyUNonce as i64,
+ Value::Bytes(hex::decode("4d8553e7e74f3c6a3a9dd3ef286a8195cbf8a23d19558ccfec7d34b824f42d92bd06bd2c7f0271f0214e141fb779ae2856abf585a58368b017e7f2a9e5ce4db5").unwrap())
+ )
+ .build(),
+ )
+ .ciphertext(vec![])
+ .build(),
+ )
+ .build(),
+ // Note: contents of maps have been re-ordered from the RFC to canonical ordering.
+ concat!(
+ "d861",
+ "85",
+ "43", "a10105",
+ "a0",
+ "54", "546869732069732074686520636f6e74656e742e",
+ "5820", "81a03448acd3d305376eaa11fb3fe416a955be2cbe7ec96f012c994bc3f16a41",
+ "81",
+ "83",
+ "44", "a101381a",
+ "a3",
+ "04",
+ "5824", "6d65726961646f632e6272616e64796275636b406275636b6c616e642e6578616d706c65",
+ "22",
+ "5821", "706572656772696e2e746f6f6b407475636b626f726f7567682e6578616d706c65",
+ "35",
+ "5840", "4d8553e7e74f3c6a3a9dd3ef286a8195cbf8a23d19558ccfec7d34b824f42d92bd06bd2c7f0271f0214e141fb779ae2856abf585a58368b017e7f2a9e5ce4db5",
+ "40",
+ ),
+ ),
+ (
+ CoseMacBuilder::new()
+ .protected(HeaderBuilder::new().algorithm(iana::Algorithm::AES_MAC_128_64).build())
+ .payload(b"This is the content.".to_vec())
+ .tag(hex::decode("36f5afaf0bab5d43").unwrap())
+ .add_recipient(
+ CoseRecipientBuilder::new()
+ .unprotected(
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::A256KW)
+ .key_id(b"018c0ae5-4d9b-471b-bfd6-eef314bc7037".to_vec())
+ .build(),
+ )
+ .ciphertext(hex::decode("711ab0dc2fc4585dce27effa6781c8093eba906f227b6eb0").unwrap())
+ .build(),
+ )
+ .build(),
+ concat!(
+ "d861",
+ "85",
+ "43", "a1010e",
+ "a0",
+ "54", "546869732069732074686520636f6e74656e742e",
+ "48", "36f5afaf0bab5d43",
+ "81",
+ "83",
+ "40",
+ "a2",
+ "01",
+ "24",
+ "04",
+ "5824", "30313863306165352d346439622d343731622d626664362d656566333134626337303337",
+ "5818", "711ab0dc2fc4585dce27effa6781c8093eba906f227b6eb0",
+ ),
+ ),
+ (
+ CoseMacBuilder::new()
+ .protected(HeaderBuilder::new().algorithm(iana::Algorithm::HMAC_256_256).build())
+ .payload(b"This is the content.".to_vec())
+ .tag(hex::decode("bf48235e809b5c42e995f2b7d5fa13620e7ed834e337f6aa43df161e49e9323e").unwrap())
+ .add_recipient(
+ CoseRecipientBuilder::new()
+ .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ECDH_ES_A128KW).build())
+ .unprotected(
+ HeaderBuilder::new()
+ .value(iana::HeaderAlgorithmParameter::EphemeralKey as i64,
+ CoseKeyBuilder::new_ec2_pub_key_y_sign(iana::EllipticCurve::P_521,
+ hex::decode("0043b12669acac3fd27898ffba0bcd2e6c366d53bc4db71f909a759304acfb5e18cdc7ba0b13ff8c7636271a6924b1ac63c02688075b55ef2d613574e7dc242f79c3").unwrap(),
+ true)
+ .build().to_cbor_value().unwrap())
+ .key_id(b"bilbo.baggins@hobbiton.example".to_vec())
+ .build(),
+ )
+ .ciphertext(hex::decode("339bc4f79984cdc6b3e6ce5f315a4c7d2b0ac466fcea69e8c07dfbca5bb1f661bc5f8e0df9e3eff5").unwrap())
+ .build(),
+ )
+ .add_recipient(
+ CoseRecipientBuilder::new()
+ .unprotected(
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::A256KW)
+ .key_id(b"018c0ae5-4d9b-471b-bfd6-eef314bc7037".to_vec())
+ .build(),
+ )
+ .ciphertext(hex::decode("0b2c7cfce04e98276342d6476a7723c090dfdd15f9a518e7736549e998370695e6d6a83b4ae507bb").unwrap())
+ .build(),
+ )
+ .build(),
+ // Note: contents of maps have been re-ordered from the RFC to canonical ordering.
+ concat!(
+ "d861",
+ "85",
+ "43", "a10105",
+ "a0",
+ "54", "546869732069732074686520636f6e74656e742e",
+ "5820", "bf48235e809b5c42e995f2b7d5fa13620e7ed834e337f6aa43df161e49e9323e",
+ "82",
+ "83",
+ "44", "a101381c",
+ "a2",
+ "04",
+ "581e", "62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c65",
+ "20",
+ "a4",
+ "01",
+ "02",
+ "20",
+ "03",
+ "21",
+ "5842", "0043b12669acac3fd27898ffba0bcd2e6c366d53bc4db71f909a759304acfb5e18cdc7ba0b13ff8c7636271a6924b1ac63c02688075b55ef2d613574e7dc242f79c3",
+ "22",
+ "f5",
+ "5828", "339bc4f79984cdc6b3e6ce5f315a4c7d2b0ac466fcea69e8c07dfbca5bb1f661bc5f8e0df9e3eff5",
+ "83",
+ "40",
+ "a2",
+ "01",
+ "24",
+ "04",
+ "5824", "30313863306165352d346439622d343731622d626664362d656566333134626337303337",
+ "5828", "0b2c7cfce04e98276342d6476a7723c090dfdd15f9a518e7736549e998370695e6d6a83b4ae507bb",
+ ),
+ ),
+ ];
+
+ for (i, (mac, mac_data)) in tests.iter().enumerate() {
+ let got = mac.clone().to_tagged_vec().unwrap();
+ assert_eq!(*mac_data, hex::encode(&got), "case {}", i);
+
+ let mut got = CoseMac::from_tagged_slice(&got).unwrap();
+ got.protected.original_data = None;
+ for mut recip in &mut got.recipients {
+ recip.protected.original_data = None;
+ }
+ for mut sig in &mut got.unprotected.counter_signatures {
+ sig.protected.original_data = None;
+ }
+ assert_eq!(*mac, got);
+ }
+}
+
+#[test]
+fn test_cose_mac0_decode() {
+ let tests: Vec<(CoseMac0, &'static str)> = vec![
+ (
+ CoseMac0Builder::new().build(),
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "f6", // null
+ "40", // 0-bstr
+ ),
+ ),
+ (
+ CoseMac0Builder::new().payload(vec![]).build(),
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "40", // 0-bstr
+ "40", // 0-bstr
+ ),
+ ),
+ ];
+ for (i, (mac, mac_data)) in tests.iter().enumerate() {
+ let got = mac.clone().to_vec().unwrap();
+ assert_eq!(*mac_data, hex::encode(&got), "case {}", i);
+
+ let mut got = CoseMac0::from_slice(&got).unwrap();
+ got.protected.original_data = None;
+ assert_eq!(*mac, got);
+ }
+}
+#[test]
+fn test_cose_mac0_decode_fail() {
+ let tests = vec![
+ (
+ concat!(
+ "a2", // 2-map (should be tuple)
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "4100", // 0-bstr
+ "40", // 0-bstr
+ ),
+ "expected array",
+ ),
+ (
+ concat!(
+ "83", // 3-tuple (should be 4-tuple)
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "40", // 0-bstr
+ ),
+ "expected array with 4 items",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "80", // 0-tuple (should be bstr)
+ "a0", // 0-map
+ "40", // 0-bstr
+ "40", // 0-bstr
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "40", // 0-bstr (should be map)
+ "40", // 0-bstr
+ "40", // 0-bstr
+ ),
+ "expected map",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "60", // 0-tstr (should be bstr)
+ "40", // 0-bstr
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "40", // 0-bstr
+ "60", // 0-tstr
+ ),
+ "expected bstr",
+ ),
+ ];
+ for (mac_data, err_msg) in tests.iter() {
+ let data = hex::decode(mac_data).unwrap();
+ let result = CoseMac0::from_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
+
+#[test]
+fn test_rfc8152_cose_mac0_decode() {
+ // COSE_Mac0 structures from RFC 8152 section C.5.
+ let tests: Vec<(CoseMac0, &'static str)> = vec![(
+ CoseMac0Builder::new()
+ .protected(
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::AES_MAC_256_64)
+ .build(),
+ )
+ .payload(b"This is the content.".to_vec())
+ .tag(hex::decode("726043745027214f").unwrap())
+ .build(),
+ concat!(
+ "d1",
+ "84",
+ "43",
+ "a1010f",
+ "a0",
+ "54",
+ "546869732069732074686520636f6e74656e742e",
+ "48",
+ "726043745027214f",
+ ),
+ )];
+
+ for (i, (mac, mac_data)) in tests.iter().enumerate() {
+ let got = mac.clone().to_tagged_vec().unwrap();
+ assert_eq!(*mac_data, hex::encode(&got), "case {}", i);
+
+ let mut got = CoseMac0::from_tagged_slice(&got).unwrap();
+ got.protected.original_data = None;
+ assert_eq!(*mac, got);
+ }
+}
+
+struct FakeMac {}
+impl FakeMac {
+ fn compute(&self, data: &[u8]) -> Vec<u8> {
+ let mut val = 0u8;
+ for b in data {
+ val ^= b;
+ }
+ vec![val]
+ }
+ fn verify(&self, tag: &[u8], data: &[u8]) -> Result<(), String> {
+ if self.compute(data) == tag {
+ Ok(())
+ } else {
+ Err("mismatch".to_owned())
+ }
+ }
+ fn try_compute(&self, data: &[u8]) -> Result<Vec<u8>, String> {
+ Ok(self.compute(data))
+ }
+ fn fail_compute(&self, _data: &[u8]) -> Result<Vec<u8>, String> {
+ Err("failed".to_string())
+ }
+}
+
+#[test]
+fn test_cose_mac_roundtrip() {
+ let tagger = FakeMac {};
+ let external_aad = b"This is the external aad";
+ let mut mac = CoseMacBuilder::new()
+ .protected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+ .payload(b"This is the data".to_vec())
+ .create_tag(external_aad, |data| tagger.compute(data))
+ .build();
+
+ assert!(mac
+ .verify_tag(external_aad, |tag, data| tagger.verify(tag, data))
+ .is_ok());
+
+ // Changing an unprotected header leaves a correct tag.
+ mac.unprotected.content_type = Some(ContentType::Text("text/plain".to_owned()));
+ assert!(mac
+ .verify_tag(external_aad, |tag, data| tagger.verify(tag, data))
+ .is_ok());
+
+ // Providing a different `aad` means the tag won't validate
+ assert!(mac
+ .verify_tag(b"not aad", |tag, data| tagger.verify(tag, data))
+ .is_err());
+
+ // Changing a protected header invalidates the tag.
+ mac.protected = ProtectedHeader::default();
+ assert!(mac
+ .verify_tag(external_aad, |tag, data| tagger.verify(tag, data))
+ .is_err());
+}
+
+#[test]
+fn test_cose_mac_noncanonical() {
+ let tagger = FakeMac {};
+ let external_aad = b"aad";
+
+ // Build an empty protected header from a non-canonical input of 41a0 rather than 40.
+ let protected = ProtectedHeader::from_cbor_bstr(Value::Bytes(vec![0xa0])).unwrap();
+ assert_eq!(protected.header, Header::default());
+ assert_eq!(protected.original_data, Some(vec![0xa0]));
+
+ let mut mac = CoseMac {
+ protected: protected.clone(),
+ payload: Some(b"data".to_vec()),
+ ..Default::default()
+ };
+ let tbm = mac.tbm(external_aad);
+ mac.tag = tagger.compute(&tbm);
+
+ // Checking the MAC should still succeed, because the `ProtectedHeader`
+ // includes the wire data and uses it for building the input.
+ assert!(mac
+ .verify_tag(external_aad, |tag, data| tagger.verify(tag, data))
+ .is_ok());
+
+ // However, if we attempt to build the same decryption inputs by hand (thus not including the
+ // non-canonical wire data)...
+ let recreated_mac = CoseMacBuilder::new()
+ .protected(protected.header)
+ .payload(b"data".to_vec())
+ .tag(mac.tag)
+ .build();
+
+ // ...then the transplanted tag will not verify, because the re-building of the
+ // inputs will use the canonical encoding of the protected header, which is not what was
+ // originally used for the input.
+ assert!(recreated_mac
+ .verify_tag(external_aad, |tag, data| tagger.verify(tag, data))
+ .is_err());
+}
+
+#[test]
+fn test_cose_mac_tag_result() {
+ let tagger = FakeMac {};
+ let external_aad = b"This is the external aad";
+ let mut _mac = CoseMacBuilder::new()
+ .protected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+ .payload(b"This is the data".to_vec())
+ .try_create_tag(external_aad, |data| tagger.try_compute(data))
+ .unwrap()
+ .build();
+
+ // Cope with MAC creation failure.
+ let result = CoseMacBuilder::new()
+ .protected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+ .payload(b"This is the data".to_vec())
+ .try_create_tag(external_aad, |data| tagger.fail_compute(data));
+ expect_err(result, "failed");
+}
+
+#[test]
+#[should_panic]
+fn test_cose_mac_create_tag_no_payload() {
+ let tagger = FakeMac {};
+ let external_aad = b"This is the external aad";
+ let _mac = CoseMacBuilder::new()
+ .protected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+ // Creating a tag before a payload has been set will panic.
+ .create_tag(external_aad, |data| tagger.compute(data))
+ .build();
+}
+
+#[test]
+#[should_panic]
+fn test_cose_mac_verify_tag_no_payload() {
+ let tagger = FakeMac {};
+ let external_aad = b"This is the external aad";
+ let mut mac = CoseMacBuilder::new()
+ .protected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+ .payload(b"This is the data".to_vec())
+ .create_tag(external_aad, |data| tagger.compute(data))
+ .build();
+
+ mac.payload = None;
+ // Trying to verify with no payload available panics.
+ let _result = mac.verify_tag(external_aad, |tag, data| tagger.verify(tag, data));
+}
+
+#[test]
+fn test_cose_mac0_roundtrip() {
+ let tagger = FakeMac {};
+ let external_aad = b"This is the external aad";
+ let mut mac = CoseMac0Builder::new()
+ .protected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+ .payload(b"This is the data".to_vec())
+ .create_tag(external_aad, |data| tagger.compute(data))
+ .build();
+
+ assert!(mac
+ .verify_tag(external_aad, |tag, data| tagger.verify(tag, data))
+ .is_ok());
+
+ // Changing an unprotected header leaves a correct tag.
+ mac.unprotected.content_type = Some(ContentType::Text("text/plain".to_owned()));
+ assert!(mac
+ .verify_tag(external_aad, |tag, data| tagger.verify(tag, data))
+ .is_ok());
+
+ // Providing a different `aad` means the tag won't validate
+ assert!(mac
+ .verify_tag(b"not aad", |tag, data| tagger.verify(tag, data))
+ .is_err());
+
+ // Changing a protected header invalidates the tag.
+ mac.protected = ProtectedHeader::default();
+ assert!(mac
+ .verify_tag(external_aad, |tag, data| tagger.verify(tag, data))
+ .is_err());
+}
+
+#[test]
+fn test_cose_mac0_noncanonical() {
+ let tagger = FakeMac {};
+ let external_aad = b"aad";
+
+ // Build an empty protected header from a non-canonical input of 41a0 rather than 40.
+ let protected = ProtectedHeader::from_cbor_bstr(Value::Bytes(vec![0xa0])).unwrap();
+ assert_eq!(protected.header, Header::default());
+ assert_eq!(protected.original_data, Some(vec![0xa0]));
+
+ let mut mac = CoseMac0 {
+ protected: protected.clone(),
+ payload: Some(b"data".to_vec()),
+ ..Default::default()
+ };
+ let tbm = mac.tbm(external_aad);
+ mac.tag = tagger.compute(&tbm);
+
+ // Checking the MAC should still succeed, because the `ProtectedHeader`
+ // includes the wire data and uses it for building the input.
+ assert!(mac
+ .verify_tag(external_aad, |tag, data| tagger.verify(tag, data))
+ .is_ok());
+
+ // However, if we attempt to build the same decryption inputs by hand (thus not including the
+ // non-canonical wire data)...
+ let recreated_mac = CoseMac0Builder::new()
+ .protected(protected.header)
+ .payload(b"data".to_vec())
+ .tag(mac.tag)
+ .build();
+
+ // ...then the transplanted tag will not verify, because the re-building of the
+ // inputs will use the canonical encoding of the protected header, which is not what was
+ // originally used for the input.
+ assert!(recreated_mac
+ .verify_tag(external_aad, |tag, data| tagger.verify(tag, data))
+ .is_err());
+}
+
+#[test]
+fn test_cose_mac0_tag_result() {
+ let tagger = FakeMac {};
+ let external_aad = b"This is the external aad";
+ let mut _mac = CoseMac0Builder::new()
+ .protected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+ .payload(b"This is the data".to_vec())
+ .try_create_tag(external_aad, |data| tagger.try_compute(data))
+ .unwrap()
+ .build();
+
+ // Cope with MAC creation failure.
+ let result = CoseMac0Builder::new()
+ .protected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+ .payload(b"This is the data".to_vec())
+ .try_create_tag(external_aad, |data| tagger.fail_compute(data));
+ expect_err(result, "failed");
+}
+
+#[test]
+#[should_panic]
+fn test_cose_mac0_create_tag_no_payload() {
+ let tagger = FakeMac {};
+ let external_aad = b"This is the external aad";
+ let _mac = CoseMac0Builder::new()
+ .protected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+ // Creating a tag before a payload has been set will panic.
+ .create_tag(external_aad, |data| tagger.compute(data))
+ .build();
+}
+
+#[test]
+#[should_panic]
+fn test_cose_mac0_verify_tag_no_payload() {
+ let tagger = FakeMac {};
+ let external_aad = b"This is the external aad";
+ let mut mac = CoseMac0Builder::new()
+ .protected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+ .payload(b"This is the data".to_vec())
+ .create_tag(external_aad, |data| tagger.compute(data))
+ .build();
+
+ mac.payload = None;
+ // Trying to verify with no payload available panics.
+ let _result = mac.verify_tag(external_aad, |tag, data| tagger.verify(tag, data));
+}
diff --git a/src/sign/mod.rs b/src/sign/mod.rs
new file mode 100644
index 0000000..d945244
--- /dev/null
+++ b/src/sign/mod.rs
@@ -0,0 +1,380 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+//! COSE_Sign* functionality.
+
+use crate::{
+ cbor,
+ cbor::value::Value,
+ iana,
+ util::{cbor_type_error, to_cbor_array, AsCborValue, ValueTryAs},
+ CoseError, Header, ProtectedHeader, Result,
+};
+use alloc::{borrow::ToOwned, vec, vec::Vec};
+
+#[cfg(test)]
+mod tests;
+
+/// Structure representing a cryptographic signature.
+///
+/// ```cddl
+/// COSE_Signature = [
+/// Headers,
+/// signature : bstr
+/// ]
+/// ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct CoseSignature {
+ pub protected: ProtectedHeader,
+ pub unprotected: Header,
+ pub signature: Vec<u8>,
+}
+
+impl crate::CborSerializable for CoseSignature {}
+
+impl AsCborValue for CoseSignature {
+ fn from_cbor_value(value: Value) -> Result<Self> {
+ let mut a = value.try_as_array()?;
+ if a.len() != 3 {
+ return Err(CoseError::UnexpectedItem("array", "array with 3 items"));
+ }
+
+ // Remove array elements in reverse order to avoid shifts.
+ Ok(Self {
+ signature: a.remove(2).try_as_bytes()?,
+ unprotected: Header::from_cbor_value(a.remove(1))?,
+ protected: ProtectedHeader::from_cbor_bstr(a.remove(0))?,
+ })
+ }
+
+ fn to_cbor_value(self) -> Result<Value> {
+ Ok(Value::Array(vec![
+ self.protected.cbor_bstr()?,
+ self.unprotected.to_cbor_value()?,
+ Value::Bytes(self.signature),
+ ]))
+ }
+}
+
+/// Builder for [`CoseSignature`] objects.
+#[derive(Debug, Default)]
+pub struct CoseSignatureBuilder(CoseSignature);
+
+impl CoseSignatureBuilder {
+ builder! {CoseSignature}
+ builder_set_protected! {protected}
+ builder_set! {unprotected: Header}
+ builder_set! {signature: Vec<u8>}
+}
+
+/// Signed payload with signatures.
+///
+/// ```cdl
+/// COSE_Sign = [
+/// Headers,
+/// payload : bstr / nil,
+/// signatures : [+ COSE_Signature]
+/// ]
+/// ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct CoseSign {
+ pub protected: ProtectedHeader,
+ pub unprotected: Header,
+ pub payload: Option<Vec<u8>>,
+ pub signatures: Vec<CoseSignature>,
+}
+
+impl crate::CborSerializable for CoseSign {}
+impl crate::TaggedCborSerializable for CoseSign {
+ const TAG: u64 = iana::CborTag::CoseSign as u64;
+}
+
+impl AsCborValue for CoseSign {
+ fn from_cbor_value(value: Value) -> Result<Self> {
+ let mut a = value.try_as_array()?;
+ if a.len() != 4 {
+ return Err(CoseError::UnexpectedItem("array", "array with 4 items"));
+ }
+
+ // Remove array elements in reverse order to avoid shifts.
+ let signatures = a.remove(3).try_as_array_then_convert(|v| {
+ CoseSignature::from_cbor_value(v)
+ .map_err(|_e| CoseError::UnexpectedItem("non-signature", "map for COSE_Signature"))
+ })?;
+
+ Ok(Self {
+ signatures,
+ payload: match a.remove(2) {
+ Value::Bytes(b) => Some(b),
+ Value::Null => None,
+ v => return cbor_type_error(&v, "bstr or nil"),
+ },
+ unprotected: Header::from_cbor_value(a.remove(1))?,
+ protected: ProtectedHeader::from_cbor_bstr(a.remove(0))?,
+ })
+ }
+
+ fn to_cbor_value(self) -> Result<Value> {
+ Ok(Value::Array(vec![
+ self.protected.cbor_bstr()?,
+ self.unprotected.to_cbor_value()?,
+ match self.payload {
+ Some(b) => Value::Bytes(b),
+ None => Value::Null,
+ },
+ to_cbor_array(self.signatures)?,
+ ]))
+ }
+}
+
+impl CoseSign {
+ /// Verify the indidated signature value, using `verifier` on the signature value and serialized
+ /// data (in that order).
+ ///
+ /// # Panics
+ ///
+ /// This method will panic if `which` is >= `self.signatures.len()`.
+ pub fn verify_signature<F, E>(&self, which: usize, aad: &[u8], verifier: F) -> Result<(), E>
+ where
+ F: FnOnce(&[u8], &[u8]) -> Result<(), E>,
+ {
+ let sig = &self.signatures[which];
+ let tbs_data = self.tbs_data(aad, sig);
+ verifier(&sig.signature, &tbs_data)
+ }
+
+ /// Construct the to-be-signed data for this object.
+ fn tbs_data(&self, aad: &[u8], sig: &CoseSignature) -> Vec<u8> {
+ sig_structure_data(
+ SignatureContext::CoseSignature,
+ self.protected.clone(),
+ Some(sig.protected.clone()),
+ aad,
+ self.payload.as_ref().unwrap_or(&vec![]),
+ )
+ }
+}
+
+/// Builder for [`CoseSign`] objects.
+#[derive(Debug, Default)]
+pub struct CoseSignBuilder(CoseSign);
+
+impl CoseSignBuilder {
+ builder! {CoseSign}
+ builder_set_protected! {protected}
+ builder_set! {unprotected: Header}
+ builder_set_optional! {payload: Vec<u8>}
+
+ /// Add a signature value.
+ #[must_use]
+ pub fn add_signature(mut self, sig: CoseSignature) -> Self {
+ self.0.signatures.push(sig);
+ self
+ }
+
+ /// Calculate the signature value, using `signer` to generate the signature bytes that will be
+ /// used to complete `sig`. Any protected header values should be set before using this
+ /// method.
+ #[must_use]
+ pub fn add_created_signature<F>(self, mut sig: CoseSignature, aad: &[u8], signer: F) -> Self
+ where
+ F: FnOnce(&[u8]) -> Vec<u8>,
+ {
+ let tbs_data = self.0.tbs_data(aad, &sig);
+ sig.signature = signer(&tbs_data);
+ self.add_signature(sig)
+ }
+
+ /// Calculate the signature value, using `signer` to generate the signature bytes that will be
+ /// used to complete `sig`. Any protected header values should be set before using this
+ /// method.
+ pub fn try_add_created_signature<F, E>(
+ self,
+ mut sig: CoseSignature,
+ aad: &[u8],
+ signer: F,
+ ) -> Result<Self, E>
+ where
+ F: FnOnce(&[u8]) -> Result<Vec<u8>, E>,
+ {
+ let tbs_data = self.0.tbs_data(aad, &sig);
+ sig.signature = signer(&tbs_data)?;
+ Ok(self.add_signature(sig))
+ }
+}
+
+/// Signed payload with a single signature.
+///
+/// ```cddl
+/// COSE_Sign1 = [
+/// Headers,
+/// payload : bstr / nil,
+/// signature : bstr
+/// ]
+/// ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct CoseSign1 {
+ pub protected: ProtectedHeader,
+ pub unprotected: Header,
+ pub payload: Option<Vec<u8>>,
+ pub signature: Vec<u8>,
+}
+
+impl crate::CborSerializable for CoseSign1 {}
+impl crate::TaggedCborSerializable for CoseSign1 {
+ const TAG: u64 = iana::CborTag::CoseSign1 as u64;
+}
+
+impl AsCborValue for CoseSign1 {
+ fn from_cbor_value(value: Value) -> Result<Self> {
+ let mut a = value.try_as_array()?;
+ if a.len() != 4 {
+ return Err(CoseError::UnexpectedItem("array", "array with 4 items"));
+ }
+
+ // Remove array elements in reverse order to avoid shifts.
+ Ok(Self {
+ signature: a.remove(3).try_as_bytes()?,
+ payload: match a.remove(2) {
+ Value::Bytes(b) => Some(b),
+ Value::Null => None,
+ v => return cbor_type_error(&v, "bstr or nil"),
+ },
+ unprotected: Header::from_cbor_value(a.remove(1))?,
+ protected: ProtectedHeader::from_cbor_bstr(a.remove(0))?,
+ })
+ }
+
+ fn to_cbor_value(self) -> Result<Value> {
+ Ok(Value::Array(vec![
+ self.protected.cbor_bstr()?,
+ self.unprotected.to_cbor_value()?,
+ match self.payload {
+ Some(b) => Value::Bytes(b),
+ None => Value::Null,
+ },
+ Value::Bytes(self.signature),
+ ]))
+ }
+}
+
+impl CoseSign1 {
+ /// Verify the signature value, using `verifier` on the signature value and serialized data (in
+ /// that order).
+ pub fn verify_signature<F, E>(&self, aad: &[u8], verifier: F) -> Result<(), E>
+ where
+ F: FnOnce(&[u8], &[u8]) -> Result<(), E>,
+ {
+ let tbs_data = self.tbs_data(aad);
+ verifier(&self.signature, &tbs_data)
+ }
+
+ /// Construct the to-be-signed data for this object.
+ fn tbs_data(&self, aad: &[u8]) -> Vec<u8> {
+ sig_structure_data(
+ SignatureContext::CoseSign1,
+ self.protected.clone(),
+ None,
+ aad,
+ self.payload.as_ref().unwrap_or(&vec![]),
+ )
+ }
+}
+
+/// Builder for [`CoseSign1`] objects.
+#[derive(Debug, Default)]
+pub struct CoseSign1Builder(CoseSign1);
+
+impl CoseSign1Builder {
+ builder! {CoseSign1}
+ builder_set_protected! {protected}
+ builder_set! {unprotected: Header}
+ builder_set! {signature: Vec<u8>}
+ builder_set_optional! {payload: Vec<u8>}
+
+ /// Calculate the signature value, using `signer` to generate the signature bytes. Any
+ /// protected header values should be set before using this method.
+ #[must_use]
+ pub fn create_signature<F>(self, aad: &[u8], signer: F) -> Self
+ where
+ F: FnOnce(&[u8]) -> Vec<u8>,
+ {
+ let sig_data = signer(&self.0.tbs_data(aad));
+ self.signature(sig_data)
+ }
+
+ /// Calculate the signature value, using `signer` to generate the signature bytes. Any
+ /// protected header values should be set before using this method.
+ pub fn try_create_signature<F, E>(self, aad: &[u8], signer: F) -> Result<Self, E>
+ where
+ F: FnOnce(&[u8]) -> Result<Vec<u8>, E>,
+ {
+ let sig_data = signer(&self.0.tbs_data(aad))?;
+ Ok(self.signature(sig_data))
+ }
+}
+
+/// Possible signature contexts.
+#[derive(Clone, Copy)]
+pub enum SignatureContext {
+ CoseSignature,
+ CoseSign1,
+ CounterSignature,
+}
+
+impl SignatureContext {
+ /// Return the context string as per RFC 8152 section 4.4.
+ fn text(&self) -> &'static str {
+ match self {
+ SignatureContext::CoseSignature => "Signature",
+ SignatureContext::CoseSign1 => "Signature1",
+ SignatureContext::CounterSignature => "CounterSignature",
+ }
+ }
+}
+
+/// Create a binary blob that will be signed.
+///
+/// ```cddl
+/// Sig_structure = [
+/// context : "Signature" / "Signature1" / "CounterSignature",
+/// body_protected : empty_or_serialized_map,
+/// ? sign_protected : empty_or_serialized_map,
+/// external_aad : bstr,
+/// payload : bstr
+/// ]
+/// ```
+pub fn sig_structure_data(
+ context: SignatureContext,
+ body: ProtectedHeader,
+ sign: Option<ProtectedHeader>,
+ aad: &[u8],
+ payload: &[u8],
+) -> Vec<u8> {
+ let mut arr = vec![
+ Value::Text(context.text().to_owned()),
+ body.cbor_bstr().expect("failed to serialize header"), // safe: always serializable
+ ];
+ if let Some(sign) = sign {
+ arr.push(sign.cbor_bstr().expect("failed to serialize header")); // safe: always
+ // serializable
+ }
+ arr.push(Value::Bytes(aad.to_vec()));
+ arr.push(Value::Bytes(payload.to_vec()));
+ let mut data = Vec::new();
+ cbor::ser::into_writer(&Value::Array(arr), &mut data).unwrap(); // safe: always serializable
+ data
+}
diff --git a/src/sign/tests.rs b/src/sign/tests.rs
new file mode 100644
index 0000000..d802b90
--- /dev/null
+++ b/src/sign/tests.rs
@@ -0,0 +1,1427 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+use super::*;
+use crate::{
+ cbor::value::Value, iana, util::expect_err, Algorithm, CborSerializable, ContentType,
+ HeaderBuilder, RegisteredLabel, TaggedCborSerializable,
+};
+use alloc::{
+ format,
+ string::{String, ToString},
+ vec,
+};
+
+#[test]
+fn test_cose_signature_encode() {
+ let tests = vec![
+ (
+ CoseSignature::default(),
+ concat!(
+ "83", // 3-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "40", // 0-bstr
+ ),
+ ),
+ (
+ CoseSignature {
+ signature: vec![1, 2, 3],
+ ..Default::default()
+ },
+ concat!(
+ "83", // 3-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ ),
+ ),
+ (
+ CoseSignature {
+ protected: ProtectedHeader {
+ original_data: None,
+ header: Header {
+ alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)),
+ key_id: vec![1, 2, 3],
+ partial_iv: vec![1, 2, 3],
+ ..Default::default()
+ },
+ },
+ signature: vec![1, 2, 3],
+ ..Default::default()
+ },
+ concat!(
+ "83", // 3-tuple
+ "4d", // 13-bstr
+ "a3", // 3-map
+ "01", "01", // 1 (alg) => A128GCM
+ "04", "43", "010203", // 4 (kid) => 3-bstr
+ "06", "43", "010203", // 6 (partial-iv) => 3-bstr
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ ),
+ ),
+ (
+ CoseSignature {
+ unprotected: Header {
+ alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)),
+ key_id: vec![1, 2, 3],
+ partial_iv: vec![1, 2, 3],
+ ..Default::default()
+ },
+ signature: vec![1, 2, 3],
+ ..Default::default()
+ },
+ concat!(
+ "83", // 3-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a3", // 3-map
+ "01", "01", // 1 (alg) => A128GCM
+ "04", "43", "010203", // 4 (kid) => 3-bstr
+ "06", "43", "010203", // 6 (partial-iv) => 3-bstr
+ "43010203", // 3-bstr
+ ),
+ ),
+ ];
+ for (i, (sig, sig_data)) in tests.iter().enumerate() {
+ let got = sig.clone().to_vec().unwrap();
+ assert_eq!(*sig_data, hex::encode(&got), "case {}", i);
+
+ let mut got = CoseSignature::from_slice(&got).unwrap();
+ got.protected.original_data = None;
+ assert_eq!(*sig, got);
+ }
+}
+
+#[test]
+fn test_cose_signature_decode_noncanonical() {
+ // RFC8152 section 3: "Recipients MUST accept both a zero-length binary value and a zero-length
+ // map encoded in the binary value."
+ let sig_data = hex::decode(concat!(
+ "83", // 3-tuple
+ "41a0", // 1-bstr holding 0-map (not a 0-bstr)
+ "a0", // 0-map
+ "40", // 0-bstr
+ ))
+ .unwrap();
+ let sig = CoseSignature::default();
+ let mut got = CoseSignature::from_slice(&sig_data).unwrap();
+ got.protected.original_data = None;
+ assert_eq!(sig, got);
+}
+
+#[test]
+fn test_cose_signature_decode_fail() {
+ let tests = vec![
+ (
+ concat!(
+ "a2", // 2-map
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ "40", // 0-bstr
+ ),
+ "expected array",
+ ),
+ (
+ concat!(
+ "83", // 3-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "40", // 0-bstr (invalid: should be map)
+ "43010203", // 3-bstr
+ ),
+ "expected map",
+ ),
+ (
+ concat!(
+ "83", // 3-tuple
+ "a0", // 0-map (invalid: should be bstr)
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ "43010203", // 3-bstr
+ ),
+ "expected array with 3 items",
+ ),
+ (
+ concat!(
+ "82", // 4-tuple
+ "40", // 0-bstr
+ "a0", // 0-map
+ ),
+ "expected array with 3 items",
+ ),
+ (
+ concat!(
+ "83", // 3-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "40", // 0-bstr (invalid: should be map)
+ "63616263", // 3-tstr
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "83", // 3-tuple
+ "45", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a1", // 1-map
+ "03", "81", "4101", // 3 (content-type) => [bstr] (invalid value type)
+ "a0", // 0-map
+ "43616263", // 0-bstr
+ ),
+ "expected int/tstr",
+ ),
+ ];
+ for (sig_data, err_msg) in tests.iter() {
+ let data = hex::decode(sig_data).unwrap();
+ let result = CoseSignature::from_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
+
+#[test]
+fn test_cose_signature_builder() {
+ let tests = vec![
+ (
+ CoseSignatureBuilder::new().build(),
+ CoseSignature::default(),
+ ),
+ (
+ CoseSignatureBuilder::new().signature(vec![1, 2, 3]).build(),
+ CoseSignature {
+ signature: vec![1, 2, 3],
+ ..Default::default()
+ },
+ ),
+ (
+ CoseSignatureBuilder::new()
+ .signature(vec![1, 2, 3])
+ .protected(
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::A128GCM)
+ .key_id(vec![1, 2, 3])
+ .iv(vec![1, 2, 3])
+ .build(),
+ )
+ .build(),
+ CoseSignature {
+ protected: ProtectedHeader {
+ original_data: None,
+ header: Header {
+ alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)),
+ key_id: vec![1, 2, 3],
+ iv: vec![1, 2, 3],
+ ..Default::default()
+ },
+ },
+ signature: vec![1, 2, 3],
+ ..Default::default()
+ },
+ ),
+ (
+ CoseSignatureBuilder::new()
+ .signature(vec![1, 2, 3])
+ .unprotected(
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::A128GCM)
+ .key_id(vec![1, 2, 3])
+ .partial_iv(vec![1, 2, 3])
+ .build(),
+ )
+ .build(),
+ CoseSignature {
+ unprotected: Header {
+ alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)),
+ key_id: vec![1, 2, 3],
+ partial_iv: vec![1, 2, 3],
+ ..Default::default()
+ },
+ signature: vec![1, 2, 3],
+ ..Default::default()
+ },
+ ),
+ ];
+ for (got, want) in tests {
+ assert_eq!(got, want);
+ }
+}
+
+#[test]
+fn test_cose_sign_encode() {
+ let tests = vec![
+ (
+ CoseSign::default(),
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "f6", // null
+ "80", // 0-tuple
+ ),
+ ),
+ (
+ CoseSignBuilder::new()
+ .add_signature(CoseSignature::default())
+ .build(),
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "f6", // null
+ "81", // 1-tuple
+ "83", "40a040", // 3-tuple
+ ),
+ ),
+ (
+ CoseSignBuilder::new()
+ .protected(
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::A128GCM)
+ .key_id(vec![1, 2, 3])
+ .build(),
+ )
+ .payload(vec![4, 5, 6])
+ .add_signature(
+ CoseSignatureBuilder::new()
+ .signature(vec![1, 2, 3])
+ .protected(
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::A128GCM)
+ .key_id(vec![1, 2, 3])
+ .iv(vec![1, 2, 3])
+ .build(),
+ )
+ .build(),
+ )
+ .build(),
+ concat!(
+ "84", // 4-tuple
+ "48", // 8-bstr (protected)
+ "a2", // 2-map
+ "01", "01", // 1 (alg) => A128GCM
+ "04", "43", "010203", // 4 (kid) => 3-bstr
+ "a0", // 0-map (unprotected)
+ "43", "040506", // 3-bstr (payload)
+ "81", // 1-tuple (signatures)
+ "83", // 3-tuple (COSE_Signature)
+ "4d", // 14-bstr (protected)
+ "a3", // 3-map
+ "01", "01", // 1 (alg) => A128GCM
+ "04", "43", "010203", // 4 (kid) => 3-bstr
+ "05", "43", "010203", // 5 (iv) => 3-bstr
+ "a0", // 0-map (unprotected)
+ "43", "010203", // 0-bstr (signature)
+ ),
+ ),
+ (
+ CoseSignBuilder::new()
+ .unprotected(
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::A128GCM)
+ .key_id(vec![1, 2, 3])
+ .build(),
+ )
+ .payload(vec![4, 5, 6])
+ .add_signature(
+ CoseSignatureBuilder::new()
+ .signature(vec![1, 2, 3])
+ .protected(
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::A128GCM)
+ .key_id(vec![1, 2, 3])
+ .iv(vec![1, 2, 3])
+ .build(),
+ )
+ .build(),
+ )
+ .build(),
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (protected)
+ "a2", // 2-map (unprotected)
+ "01", "01", // 1 (alg) => A128GCM
+ "04", "43", "010203", // 4 (kid) => 3-bstr
+ "43", "040506", // 3-bstr (payload)
+ "81", // 1-tuple (signatures)
+ "83", // 3-tuple (COSE_Signature)
+ "4d", // 14-bstr (protected)
+ "a3", // 3-map
+ "01", "01", // 1 (alg) => A128GCM
+ "04", "43", "010203", // 4 (kid) => 3-bstr
+ "05", "43", "010203", // 5 (iv) => 3-bstr
+ "a0", // 0-map (unprotected)
+ "43", "010203", // 0-bstr (signature)
+ ),
+ ),
+ ];
+ for (i, (sign, sign_data)) in tests.iter().enumerate() {
+ let got = sign.clone().to_vec().unwrap();
+ assert_eq!(*sign_data, hex::encode(&got), "case {}", i);
+
+ let mut got = CoseSign::from_slice(&got).unwrap();
+ got.protected.original_data = None;
+ for mut sig in &mut got.signatures {
+ sig.protected.original_data = None;
+ }
+ assert_eq!(*sign, got);
+
+ // Repeat with tagged variant.
+ let got = sign.clone().to_tagged_vec().unwrap();
+ let tagged_sign_data = format!("d862{}", sign_data);
+ assert_eq!(tagged_sign_data, hex::encode(&got), "tagged case {}", i);
+
+ let mut got = CoseSign::from_tagged_slice(&got).unwrap();
+ got.protected.original_data = None;
+ for mut sig in &mut got.signatures {
+ sig.protected.original_data = None;
+ }
+ assert_eq!(*sign, got);
+ }
+}
+
+#[test]
+fn test_cose_sign_decode_fail() {
+ let tests = vec![
+ (
+ concat!(
+ "a2", // 2-map
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "40", // 0-bstr (invalid: should be map)
+ "43010203", // 3-bstr
+ "80", // 0-tuple
+ ),
+ "expected array",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "40", // 0-bstr (invalid: should be map)
+ "43010203", // 3-bstr
+ "80", // 0-tuple
+ ),
+ "expected map",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "a0", // 0-map (invalid: should be bstr)
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ "80", // 0-tuple
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "85", // 5-tuple
+ "40", // 0-bstr
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ "80", // 0-tuple
+ "43010203", // 3-bstr
+ ),
+ "expected array with 4 items",
+ ),
+ (
+ concat!(
+ "83", // 3-tuple
+ "40", // 0-bstr
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ ),
+ "expected array with 4 items",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ "43010203", // 3-bstr
+ ),
+ "expected array",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr
+ "a0", // 0-map
+ "63616263", // 3-tstr
+ "80", // 0-tuple
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ "81", // 1-tuple
+ "83", // 3-tuple
+ "a0", // 0-map (invalid: should be bstr)
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ ),
+ "expected map for COSE_Signature",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "45", // 5-bstr
+ "a1", // 1-map
+ "03", "81", "4101", // 3 (content-type) => [bstr] (invalid value type)
+ "a0", // 0-map
+ "43616263", // 3-bstr
+ "80", // 0-tuple
+ ),
+ "expected int/tstr",
+ ),
+ ];
+ for (sign_data, err_msg) in tests.iter() {
+ let data = hex::decode(sign_data).unwrap();
+ let result = CoseSign::from_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
+
+#[test]
+fn test_cose_sign_tagged_decode_fail() {
+ let tests = vec![
+ (
+ concat!(
+ "d862", // tag(98)
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "40", // 0-bstr (invalid: should be map)
+ "43010203", // 3-bstr
+ "80", // 0-tuple
+ ),
+ "expected map",
+ ),
+ (
+ concat!(
+ "d862", // tag(98)
+ "84", // 4-tuple
+ "a0", // 0-map (invalid: should be bstr)
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ "80", // 0-tuple
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "d862", // tag(98)
+ "85", // 5-tuple
+ "40", // 0-bstr
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ "80", // 0-tuple
+ "43010203", // 3-bstr
+ ),
+ "expected array with 4 items",
+ ),
+ (
+ concat!(
+ "d862", // tag(98)
+ "83", // 3-tuple
+ "40", // 0-bstr
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ ),
+ "expected array with 4 items",
+ ),
+ (
+ concat!(
+ "1862", // int instead of tag
+ ),
+ "expected tag",
+ ),
+ (
+ concat!(
+ "d861", // tag(97) : wrong tag
+ "84", // 4-tuple
+ "40", // 0-bstr
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ "80", // 0-tuple
+ ),
+ "expected other tag",
+ ),
+ (
+ concat!(
+ "1861", // int (97) : not a tag
+ "84", // 4-tuple
+ "40", // 0-bstr
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ "80", // 0-tuple
+ ),
+ "extraneous data",
+ ),
+ (
+ concat!(
+ "18", // incomplete int
+ ),
+ "decode CBOR failure: Io(EndOfFile",
+ ),
+ ];
+ for (sign_data, err_msg) in tests.iter() {
+ let data = hex::decode(sign_data).unwrap();
+ let result = CoseSign::from_tagged_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
+
+#[test]
+fn test_rfc8152_cose_sign_decode() {
+ // COSE_Sign structures from RFC 8152 section C.1.
+ let tests = vec![
+ (
+ CoseSignBuilder::new()
+ .payload(b"This is the content.".to_vec())
+ .add_signature(
+ CoseSignatureBuilder::new()
+ .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ES256).build())
+ .unprotected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+ .signature(hex::decode(
+ "e2aeafd40d69d19dfe6e52077c5d7ff4e408282cbefb5d06cbf414af2e19d982ac45ac98b8544c908b4507de1e90b717c3d34816fe926a2b98f53afd2fa0f30a"
+ ).unwrap())
+ .build()
+ )
+ .build(),
+ concat!(
+ "d862",
+ "84",
+ "40",
+ "a0",
+ "54", "546869732069732074686520636f6e74656e742e",
+ "81", "83",
+ "43", "a10126",
+ "a1", "04", "42", "3131",
+ "5840", "e2aeafd40d69d19dfe6e52077c5d7ff4e408282cbefb5d06cbf414af2e19d982ac45ac98b8544c908b4507de1e90b717c3d34816fe926a2b98f53afd2fa0f30a"
+ ),
+ ),
+ (
+ CoseSignBuilder::new()
+ .payload(b"This is the content.".to_vec())
+ .add_signature(
+ CoseSignatureBuilder::new()
+ .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ES256).build())
+ .unprotected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+ .signature(hex::decode(
+ "e2aeafd40d69d19dfe6e52077c5d7ff4e408282cbefb5d06cbf414af2e19d982ac45ac98b8544c908b4507de1e90b717c3d34816fe926a2b98f53afd2fa0f30a"
+ ).unwrap())
+ .build()
+ )
+ .add_signature(
+ CoseSignatureBuilder::new()
+ .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ES512).build())
+ .unprotected(HeaderBuilder::new().key_id(b"bilbo.baggins@hobbiton.example".to_vec()).build())
+ .signature(hex::decode(
+ "00a2d28a7c2bdb1587877420f65adf7d0b9a06635dd1de64bb62974c863f0b160dd2163734034e6ac003b01e8705524c5c4ca479a952f0247ee8cb0b4fb7397ba08d009e0c8bf482270cc5771aa143966e5a469a09f613488030c5b07ec6d722e3835adb5b2d8c44e95ffb13877dd2582866883535de3bb03d01753f83ab87bb4f7a0297"
+ ).unwrap())
+ .build()
+ )
+ .build(),
+ concat!(
+ "d862",
+ "84",
+ "40",
+ "a0", "54", "546869732069732074686520636f6e74656e742e",
+ "82",
+ "83",
+ "43", "a10126",
+ "a1", "04", "42", "3131",
+ "5840", "e2aeafd40d69d19dfe6e52077c5d7ff4e408282cbefb5d06cbf414af2e19d982ac45ac98b8544c908b4507de1e90b717c3d34816fe926a2b98f53afd2fa0f30a",
+ "83",
+ "44", "a1013823",
+ "a1", "04", "581e", "62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c65",
+ "5884", "00a2d28a7c2bdb1587877420f65adf7d0b9a06635dd1de64bb62974c863f0b160dd2163734034e6ac003b01e8705524c5c4ca479a952f0247ee8cb0b4fb7397ba08d009e0c8bf482270cc5771aa143966e5a469a09f613488030c5b07ec6d722e3835adb5b2d8c44e95ffb13877dd2582866883535de3bb03d01753f83ab87bb4f7a0297",
+ )
+ ),
+ (
+ CoseSignBuilder::new()
+ .unprotected(HeaderBuilder::new()
+ .add_counter_signature(
+ CoseSignatureBuilder::new()
+ .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ES256).build())
+ .unprotected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+ .signature(hex::decode(
+ "5ac05e289d5d0e1b0a7f048a5d2b643813ded50bc9e49220f4f7278f85f19d4a77d655c9d3b51e805a74b099e1e085aacd97fc29d72f887e8802bb6650cceb2c"
+ ).unwrap())
+ .build()
+ )
+ .build())
+ .payload(b"This is the content.".to_vec())
+ .add_signature(
+ CoseSignatureBuilder::new()
+ .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ES256).build())
+ .unprotected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+ .signature(hex::decode(
+ "e2aeafd40d69d19dfe6e52077c5d7ff4e408282cbefb5d06cbf414af2e19d982ac45ac98b8544c908b4507de1e90b717c3d34816fe926a2b98f53afd2fa0f30a"
+ ).unwrap())
+ .build()
+ )
+ .build(),
+ concat!(
+ "d862",
+ "84",
+ "40",
+ "a1", "07",
+ "83",
+ "43", "a10126",
+ "a1", "04", "42", "3131",
+ "5840", "5ac05e289d5d0e1b0a7f048a5d2b643813ded50bc9e49220f4f7278f85f19d4a77d655c9d3b51e805a74b099e1e085aacd97fc29d72f887e8802bb6650cceb2c",
+ "54", "546869732069732074686520636f6e74656e742e",
+ "81",
+ "83",
+ "43", "a10126",
+ "a1", "04", "42", "3131",
+ "5840", "e2aeafd40d69d19dfe6e52077c5d7ff4e408282cbefb5d06cbf414af2e19d982ac45ac98b8544c908b4507de1e90b717c3d34816fe926a2b98f53afd2fa0f30a",
+ ),
+ ),
+ (
+ CoseSignBuilder::new()
+ .protected(HeaderBuilder::new()
+ .text_value("reserved".to_owned(), Value::Bool(false))
+ .add_critical_label(RegisteredLabel::Text("reserved".to_owned()))
+ .build())
+ .payload(b"This is the content.".to_vec())
+ .add_signature(
+ CoseSignatureBuilder::new()
+ .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ES256).build())
+ .unprotected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+ .signature(hex::decode(
+ "3fc54702aa56e1b2cb20284294c9106a63f91bac658d69351210a031d8fc7c5ff3e4be39445b1a3e83e1510d1aca2f2e8a7c081c7645042b18aba9d1fad1bd9c"
+ ).unwrap())
+ .build()
+ )
+ .build(),
+ // Note: contents of protected header changed from RFC to be put in canonical order.
+ concat!(
+ "d862",
+ "84",
+ "56",
+ "a2",
+ "02", "81687265736572766564",
+ "687265736572766564", "f4",
+ "a0",
+ "54", "546869732069732074686520636f6e74656e742e",
+ "81", "83",
+ "43", "a10126",
+ "a1", "04", "42", "3131",
+ "5840", "3fc54702aa56e1b2cb20284294c9106a63f91bac658d69351210a031d8fc7c5ff3e4be39445b1a3e83e1510d1aca2f2e8a7c081c7645042b18aba9d1fad1bd9c",
+ ),
+ ),
+ ];
+
+ for (i, (sign, sign_data)) in tests.iter().enumerate() {
+ let got = sign.clone().to_tagged_vec().unwrap();
+ assert_eq!(
+ *sign_data,
+ hex::encode(&got),
+ "case {}: encode {:?}",
+ i,
+ sign
+ );
+
+ let mut got = CoseSign::from_tagged_slice(&got).unwrap();
+ got.protected.original_data = None;
+ for mut sig in &mut got.signatures {
+ sig.protected.original_data = None;
+ }
+ for mut sig in &mut got.unprotected.counter_signatures {
+ sig.protected.original_data = None;
+ }
+ assert_eq!(*sign, got);
+ }
+}
+
+#[test]
+fn test_cose_sign1_encode() {
+ let tests = vec![
+ (
+ CoseSign1Builder::new().payload(vec![]).build(),
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "40", // 0-bstr
+ "40", // 0-bstr
+ ),
+ ),
+ (
+ CoseSign1Builder::new().signature(vec![1, 2, 3]).build(),
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "f6", // null
+ "43", "010203", // 3-bstr
+ ),
+ ),
+ (
+ CoseSign1Builder::new()
+ .protected(
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::A128GCM)
+ .key_id(vec![1, 2, 3])
+ .build(),
+ )
+ .payload(vec![])
+ .signature(vec![1, 2, 3])
+ .build(),
+ concat!(
+ "84", // 4-tuple
+ "48", // 8-bstr (protected)
+ "a2", // 2-map
+ "01", "01", // 1 (alg) => A128GCM
+ "04", "43", "010203", // 4 (kid) => 3-bstr
+ "a0", // 0-map
+ "40", // 0-bstr
+ "43", "010203", // 3-bstr
+ ),
+ ),
+ (
+ CoseSign1Builder::new()
+ .unprotected(
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::A128GCM)
+ .key_id(vec![1, 2, 3])
+ .build(),
+ )
+ .payload(vec![])
+ .signature(vec![1, 2, 3])
+ .build(),
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a2", // 2-map
+ "01", "01", // 1 (alg) => A128GCM
+ "04", "43", "010203", // 4 (kid) => 3-bstr
+ "40", // 0-bstr
+ "43", "010203", // 3-bstr
+ ),
+ ),
+ ];
+ for (i, (sign, sign_data)) in tests.iter().enumerate() {
+ let got = sign.clone().to_vec().unwrap();
+ assert_eq!(*sign_data, hex::encode(&got), "case {}", i);
+
+ let mut got = CoseSign1::from_slice(&got).unwrap();
+ got.protected.original_data = None;
+ assert_eq!(*sign, got);
+
+ // Repeat with tagged variant.
+ let got = sign.clone().to_tagged_vec().unwrap();
+ let want_hex = format!("d2{}", sign_data);
+ assert_eq!(want_hex, hex::encode(&got), "tagged case {}", i);
+
+ let mut got = CoseSign1::from_tagged_slice(&got).unwrap();
+ got.protected.original_data = None;
+ assert_eq!(*sign, got);
+ }
+}
+
+#[test]
+fn test_cose_sign1_decode_fail() {
+ let tests = vec![
+ (
+ concat!(
+ "a2", // 2-map
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ "40", // 0-bstr
+ ),
+ "expected array",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "40", // 0-bstr (invalid: should be map)
+ "43010203", // 3-bstr
+ "40", // 0-bstr
+ ),
+ "expected map",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "a0", // 0-map (invalid: should be bstr)
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ "40", // 0-bstr
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "a0", // 0-map
+ "63616263", // 3-tstr
+ "40", // 0-bstr
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "40", // 0-bstr
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ "80", // 0-arr (invalid: should be bstr)
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "85", // 5-tuple
+ "40", // 0-bstr
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ "40", // 0-bstr
+ "43010203", // 3-bstr
+ ),
+ "expected array with 4 items",
+ ),
+ (
+ concat!(
+ "83", // 3-tuple
+ "40", // 0-bstr
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ ),
+ "expected array with 4 items",
+ ),
+ (
+ concat!(
+ "84", // 4-tuple
+ "45", // 5-bstr
+ "a1", // 1-map
+ "03", "81", "4101", // 3 (content-type) => [bstr] (invalid value type)
+ "a0", // 0-map
+ "43616263", // 3-bstr
+ "40", // 0-bstr
+ ),
+ "expected int/tstr",
+ ),
+ ];
+ for (sign_data, err_msg) in tests.iter() {
+ let data = hex::decode(sign_data).unwrap();
+ let result = CoseSign1::from_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
+
+#[test]
+fn test_cose_sign1_decode_noncanonical() {
+ let tests = vec![(
+ CoseSign1Builder::new()
+ .protected(
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ES256)
+ .key_id(vec![0x31, 0x31])
+ .build(),
+ )
+ .payload(vec![0x61, 0x61])
+ .build(),
+ concat!(
+ "84", // 4-tuple
+ "47", // bstr len 7 (protected)
+ concat!(
+ "a2", // 2-map
+ // The contents of the bstr-encoded header are not in canonical order.
+ "04", "42", "3131", // 4 (kid) => 2-bstr "11"
+ "01", "26", // 1 (alg) => ES256
+ ),
+ "a0", // 0-map (unprotected)
+ "42", // 2-bstr (payload)
+ "6161", // "aa"
+ "40", // 0-bstr
+ ),
+ )];
+ for (sign, sign_data) in tests.iter() {
+ let data = hex::decode(sign_data).unwrap();
+ let mut got = CoseSign1::from_slice(&data).unwrap();
+ got.protected.original_data = None;
+ assert_eq!(*sign, got);
+
+ // Repeat with tagged variant.
+ let mut tagged_data = vec![0xd2];
+ tagged_data.extend_from_slice(&data);
+ let mut got = CoseSign1::from_tagged_slice(&tagged_data).unwrap();
+ got.protected.original_data = None;
+ assert_eq!(*sign, got);
+ }
+}
+
+#[test]
+fn test_cose_sign1_tagged_decode_fail() {
+ let tests = vec![
+ (
+ concat!(
+ "d2", // tag(18)
+ "84", // 4-tuple
+ "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+ "40", // 0-bstr (invalid: should be map)
+ "43010203", // 3-bstr
+ "40", // 0-bstr
+ ),
+ "expected map",
+ ),
+ (
+ concat!(
+ "d2", // tag(18)
+ "84", // 4-tuple
+ "a0", // 0-map (invalid: should be bstr)
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ "40", // 0-bstr
+ ),
+ "expected bstr",
+ ),
+ (
+ concat!(
+ "d2", // tag(18)
+ "85", // 5-tuple
+ "40", // 0-bstr
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ "80", // 0-tuple
+ "40", // 0-bstr
+ ),
+ "expected array with 4 items",
+ ),
+ (
+ concat!(
+ "d2", // tag(18)
+ "83", // 3-tuple
+ "40", // 0-bstr
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ ),
+ "expected array with 4 items",
+ ),
+ (
+ concat!(
+ "d1", // tag(17) : wrong tag
+ "84", // 4-tuple
+ "40", // 0-bstr
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ "40", // 0-bstr
+ ),
+ "expected other tag",
+ ),
+ (
+ concat!(
+ "12", // int (18) : not a tag
+ "84", // 4-tuple
+ "40", // 0-bstr
+ "a0", // 0-map
+ "43010203", // 3-bstr
+ "40", // 0-bstr
+ ),
+ "extraneous data",
+ ),
+ (
+ concat!(
+ "12", // incomplete int
+ ),
+ "expected tag",
+ ),
+ ];
+ for (sign_data, err_msg) in tests.iter() {
+ let data = hex::decode(sign_data).unwrap();
+ let result = CoseSign1::from_tagged_slice(&data);
+ expect_err(result, err_msg);
+ }
+}
+
+#[test]
+fn test_rfc8152_cose_sign1_decode() {
+ // COSE_Sign1 structures from RFC 8152 section C.2.
+ let tests = vec![
+ (
+ CoseSign1Builder::new()
+ .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ES256).build())
+ .unprotected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+ .payload(b"This is the content.".to_vec())
+ .signature(hex::decode(
+ "8eb33e4ca31d1c465ab05aac34cc6b23d58fef5c083106c4d25a91aef0b0117e2af9a291aa32e14ab834dc56ed2a223444547e01f11d3b0916e5a4c345cacb36",
+ ).unwrap())
+ .build(),
+ concat!(
+ "d2",
+ "84",
+ "43", "a10126",
+ "a1", "04", "42", "3131",
+ "54", "546869732069732074686520636f6e74656e742e",
+ "5840", "8eb33e4ca31d1c465ab05aac34cc6b23d58fef5c083106c4d25a91aef0b0117e2af9a291aa32e14ab834dc56ed2a223444547e01f11d3b0916e5a4c345cacb36",
+ ),
+ ),
+ ];
+
+ for (i, (sign, sign_data)) in tests.iter().enumerate() {
+ let got = sign.clone().to_tagged_vec().unwrap();
+ assert_eq!(*sign_data, hex::encode(&got), "case {}", i);
+
+ let mut got = CoseSign1::from_tagged_slice(&got).unwrap();
+ got.protected.original_data = None;
+ assert_eq!(*sign, got);
+ }
+}
+
+#[derive(Copy, Clone)]
+struct FakeSigner {}
+
+extern crate std;
+impl FakeSigner {
+ fn sign(&self, data: &[u8]) -> Vec<u8> {
+ data.to_vec()
+ }
+
+ fn verify(&self, sig: &[u8], data: &[u8]) -> Result<(), String> {
+ if sig != self.sign(data) {
+ Err("failed to verify".to_owned())
+ } else {
+ Ok(())
+ }
+ }
+ fn try_sign(&self, data: &[u8]) -> Result<Vec<u8>, String> {
+ Ok(self.sign(data))
+ }
+
+ fn fail_sign(&self, _data: &[u8]) -> Result<Vec<u8>, String> {
+ Err("failed".to_string())
+ }
+}
+
+#[test]
+fn test_sign_roundtrip() {
+ let signer = FakeSigner {};
+ let verifier = signer;
+
+ let pt = b"This is the content";
+ let aad = b"this is additional data";
+
+ let protected = HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ES256)
+ .key_id(b"11".to_vec())
+ .build();
+ let sign = CoseSignBuilder::new()
+ .protected(protected.clone())
+ .payload(pt.to_vec())
+ .add_created_signature(
+ CoseSignatureBuilder::new().protected(protected).build(),
+ aad,
+ |pt| signer.sign(pt),
+ )
+ .build();
+
+ let sign_data = sign.to_vec().unwrap();
+ let mut sign = CoseSign::from_slice(&sign_data).unwrap();
+
+ assert!(sign
+ .verify_signature(0, aad, |sig, data| verifier.verify(sig, data))
+ .is_ok());
+
+ // Changing an unprotected header leaves the signature valid.
+ sign.unprotected.content_type = Some(ContentType::Text("text/plain".to_owned()));
+ assert!(sign
+ .verify_signature(0, aad, |sig, data| verifier.verify(sig, data))
+ .is_ok());
+
+ // Providing a different `aad` means the signature won't validate.
+ assert!(sign
+ .verify_signature(0, b"not aad", |sig, data| verifier.verify(sig, data))
+ .is_err());
+
+ // Changing a protected header invalidates the signature.
+ let mut sign2 = sign.clone();
+ sign2.protected = ProtectedHeader::default();
+ assert!(sign2
+ .verify_signature(0, aad, |sig, data| verifier.verify(sig, data))
+ .is_err());
+ let mut sign3 = sign;
+ sign3.signatures[0].protected = ProtectedHeader::default();
+ assert!(sign2
+ .verify_signature(0, aad, |sig, data| verifier.verify(sig, data))
+ .is_err());
+}
+
+#[test]
+fn test_sign_noncanonical() {
+ let signer = FakeSigner {};
+ let verifier = signer;
+ let pt = b"aa";
+ let aad = b"bb";
+
+ let tests = vec![
+ // Non-canonical: empty map can just be an empty bstr, not a bstr holding an empty map.
+ ("a0", Header::default()),
+ // Non-canonical: the map length (of 0) is non-minimally encoded as the 0x00 following
+ // 0xb8; it is short enough that it would normally be folded into the type byte
+ // (0xa0).
+ ("b800", Header::default()),
+ // Non-canonical: map not in canonical order.
+ (
+ concat!(
+ "a2", // 2-map
+ // The contents of the bstr-encoded header are not in canonical order.
+ "04", "42", "3131", // 4 (kid) => 2-bstr "11"
+ "01", "26", // 1 (alg) => ES256
+ ),
+ HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ES256)
+ .key_id(vec![0x31, 0x31])
+ .build(),
+ ),
+ ];
+
+ for (protected_data, header) in tests {
+ // Build a protected header from a non-canonically encoded input.
+ let protected_data = hex::decode(protected_data).unwrap();
+ let protected =
+ ProtectedHeader::from_cbor_bstr(Value::Bytes(protected_data.clone())).unwrap();
+ assert_eq!(protected.header, header);
+ assert_eq!(protected.original_data, Some(protected_data));
+
+ // Build a signature whose inputs include the non-canonically encoded protected header.
+ let mut sign = CoseSign {
+ payload: Some(pt.to_vec()),
+ protected: protected.clone(),
+ ..Default::default()
+ };
+ let mut sig = CoseSignature {
+ protected: protected.clone(),
+ ..Default::default()
+ };
+ sig.protected = protected.clone();
+ sig.signature = signer.sign(&sign.tbs_data(aad, &sig));
+ sign.signatures.push(sig.clone());
+ let sign_data = sign.to_vec().unwrap();
+
+ // Parsing and verifying this signature should still succeed, because the `ProtectedHeader`
+ // includes the wire data and uses it for building the signature input.
+ let sign = CoseSign::from_slice(&sign_data).unwrap();
+ assert!(sign
+ .verify_signature(0, aad, |sig, data| verifier.verify(sig, data))
+ .is_ok());
+
+ // However, if we attempt to build the same signature inputs by hand (thus not including the
+ // non-canonical wire data)...
+ let recreated_sign = CoseSignBuilder::new()
+ .protected(protected.header)
+ .payload(pt.to_vec())
+ .add_signature(sig)
+ .build();
+
+ // ...then the transplanted signature will not verify, because the re-building of the
+ // signature inputs will use the canonical encoding of the protected header, which
+ // is not what was originally used for the signature input.
+ assert!(recreated_sign
+ .verify_signature(0, aad, |sig, data| verifier.verify(sig, data))
+ .is_err());
+ }
+}
+
+#[test]
+fn test_sign_create_result() {
+ let signer = FakeSigner {};
+
+ let pt = b"This is the content";
+ let aad = b"this is additional data";
+
+ let protected = HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ES256)
+ .key_id(b"11".to_vec())
+ .build();
+ let _sign = CoseSignBuilder::new()
+ .protected(protected.clone())
+ .payload(pt.to_vec())
+ .try_add_created_signature(
+ CoseSignatureBuilder::new()
+ .protected(protected.clone())
+ .build(),
+ aad,
+ |pt| signer.try_sign(pt),
+ )
+ .unwrap()
+ .build();
+
+ let result = CoseSignBuilder::new()
+ .protected(protected.clone())
+ .payload(pt.to_vec())
+ .try_add_created_signature(
+ CoseSignatureBuilder::new().protected(protected).build(),
+ aad,
+ |pt| signer.fail_sign(pt),
+ );
+ expect_err(result, "failed");
+}
+
+#[test]
+#[should_panic]
+fn test_sign_sig_index_invalid() {
+ let signer = FakeSigner {};
+ let verifier = signer;
+
+ let pt = b"This is the content";
+ let aad = b"this is additional data";
+
+ let protected = HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ES256)
+ .key_id(b"11".to_vec())
+ .build();
+ let sign = CoseSignBuilder::new()
+ .protected(protected)
+ .payload(pt.to_vec())
+ .add_created_signature(CoseSignatureBuilder::new().build(), aad, |pt| {
+ signer.sign(pt)
+ })
+ .build();
+
+ // Attempt to verify an out-of-range signature
+ let _ = sign.verify_signature(sign.signatures.len(), aad, |sig, data| {
+ verifier.verify(sig, data)
+ });
+}
+
+#[test]
+fn test_sign1_roundtrip() {
+ let signer = FakeSigner {};
+ let verifier = signer;
+
+ let pt = b"This is the content";
+ let aad = b"this is additional data";
+
+ let protected = HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ES256)
+ .key_id(b"11".to_vec())
+ .build();
+ let sign1 = CoseSign1Builder::new()
+ .protected(protected)
+ .payload(pt.to_vec())
+ .create_signature(aad, |pt| signer.sign(pt))
+ .build();
+
+ let sign1_data = sign1.to_vec().unwrap();
+ let mut sign1 = CoseSign1::from_slice(&sign1_data).unwrap();
+
+ assert!(sign1
+ .verify_signature(aad, |sig, data| verifier.verify(sig, data))
+ .is_ok());
+
+ // Changing an unprotected header leaves the signature valid.
+ sign1.unprotected.content_type = Some(ContentType::Text("text/plain".to_owned()));
+ assert!(sign1
+ .verify_signature(aad, |sig, data| verifier.verify(sig, data))
+ .is_ok());
+
+ // Providing a different `aad` means the signature won't validate.
+ assert!(sign1
+ .verify_signature(b"not aad", |sig, data| verifier.verify(sig, data))
+ .is_err());
+
+ // Changing a protected header invalidates the signature.
+ sign1.protected.original_data = None;
+ sign1.protected.header.content_type = Some(ContentType::Text("text/plain".to_owned()));
+ assert!(sign1
+ .verify_signature(aad, |sig, data| verifier.verify(sig, data))
+ .is_err());
+}
+
+#[test]
+fn test_sign1_create_result() {
+ let signer = FakeSigner {};
+
+ let pt = b"This is the content";
+ let aad = b"this is additional data";
+
+ let protected = HeaderBuilder::new()
+ .algorithm(iana::Algorithm::ES256)
+ .key_id(b"11".to_vec())
+ .build();
+ let _sign = CoseSign1Builder::new()
+ .protected(protected.clone())
+ .payload(pt.to_vec())
+ .try_create_signature(aad, |pt| signer.try_sign(pt))
+ .unwrap()
+ .build();
+
+ let result = CoseSign1Builder::new()
+ .protected(protected)
+ .payload(pt.to_vec())
+ .try_create_signature(aad, |pt| signer.fail_sign(pt));
+ expect_err(result, "failed");
+}
+
+#[test]
+fn test_sign1_noncanonical() {
+ let signer = FakeSigner {};
+ let verifier = signer;
+ let pt = b"aa";
+ let aad = b"bb";
+
+ // Build an empty protected header from a non-canonical input of 41a0 rather than 40.
+ let protected = ProtectedHeader::from_cbor_bstr(Value::Bytes(vec![0xa0])).unwrap();
+ assert_eq!(protected.header, Header::default());
+ assert_eq!(protected.original_data, Some(vec![0xa0]));
+
+ // Build a signature whose inputs include the non-canonically encoded protected header.
+ let mut sign1 = CoseSign1::default();
+ sign1.payload = Some(pt.to_vec());
+ sign1.protected = protected.clone();
+ sign1.signature = signer.sign(&sign1.tbs_data(aad));
+ let sign1_data = sign1.to_vec().unwrap();
+
+ // Parsing and verifying this signature should still succeed, because the `ProtectedHeader`
+ // includes the wire data and uses it for building the signature input.
+ let sign1 = CoseSign1::from_slice(&sign1_data).unwrap();
+ assert!(sign1
+ .verify_signature(aad, |sig, data| verifier.verify(sig, data))
+ .is_ok());
+
+ // However, if we attempt to build the same signature inputs by hand (thus not including the
+ // non-canonical wire data)...
+ let recreated_sign1 = CoseSign1Builder::new()
+ .protected(protected.header)
+ .payload(pt.to_vec())
+ .signature(sign1.signature)
+ .build();
+
+ // ...then the transplanted signature will not verify, because the re-building of the signature
+ // inputs will use the canonical encoding of the protected header, which is not what was
+ // originally used for the signature input.
+ assert!(recreated_sign1
+ .verify_signature(aad, |sig, data| verifier.verify(sig, data))
+ .is_err());
+}
diff --git a/src/util/mod.rs b/src/util/mod.rs
new file mode 100644
index 0000000..b24ae7b
--- /dev/null
+++ b/src/util/mod.rs
@@ -0,0 +1,229 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+//! Common internal utilities.
+
+use crate::{
+ cbor::value::{Integer, Value},
+ CoseError, Result,
+};
+use alloc::{boxed::Box, vec::Vec};
+
+#[cfg(test)]
+mod tests;
+
+/// Return an error indicating that an unexpected CBOR type was encountered.
+pub(crate) fn cbor_type_error<T>(value: &Value, want: &'static str) -> Result<T> {
+ let got = match value {
+ Value::Integer(_) => "int",
+ Value::Bytes(_) => "bstr",
+ Value::Float(_) => "float",
+ Value::Text(_) => "tstr",
+ Value::Bool(_) => "bool",
+ Value::Null => "nul",
+ Value::Tag(_, _) => "tag",
+ Value::Array(_) => "array",
+ Value::Map(_) => "map",
+ _ => "other",
+ };
+ Err(CoseError::UnexpectedItem(got, want))
+}
+
+/// Trait which augments the [`Value`] type with methods for convenient conversions to contained
+/// types which throw a [`CoseError`] if the Value is not of the expected type.
+pub(crate) trait ValueTryAs
+where
+ Self: Sized,
+{
+ /// Extractor for [`Value::Integer`]
+ fn try_as_integer(self) -> Result<Integer>;
+
+ /// Extractor for [`Value::Bytes`]
+ fn try_as_bytes(self) -> Result<Vec<u8>>;
+
+ /// Extractor for [`Value::Bytes`] which also throws an error if the byte string is zero length
+ fn try_as_nonempty_bytes(self) -> Result<Vec<u8>>;
+
+ /// Extractor for [`Value::Array`]
+ fn try_as_array(self) -> Result<Vec<Self>>;
+
+ /// Extractor for [`Value::Array`] which applies `f` to each item to build a new [`Vec`]
+ fn try_as_array_then_convert<F, T>(self, f: F) -> Result<Vec<T>>
+ where
+ F: Fn(Value) -> Result<T>;
+
+ /// Extractor for [`Value::Map`]
+ fn try_as_map(self) -> Result<Vec<(Self, Self)>>;
+
+ /// Extractor for [`Value::Tag`]
+ fn try_as_tag(self) -> Result<(u64, Box<Value>)>;
+}
+
+impl ValueTryAs for Value {
+ fn try_as_integer(self) -> Result<Integer> {
+ if let Value::Integer(i) = self {
+ Ok(i)
+ } else {
+ cbor_type_error(&self, "int")
+ }
+ }
+
+ fn try_as_bytes(self) -> Result<Vec<u8>> {
+ if let Value::Bytes(b) = self {
+ Ok(b)
+ } else {
+ cbor_type_error(&self, "bstr")
+ }
+ }
+
+ fn try_as_nonempty_bytes(self) -> Result<Vec<u8>> {
+ let v = self.try_as_bytes()?;
+ if v.is_empty() {
+ return Err(CoseError::UnexpectedItem("empty bstr", "non-empty bstr"));
+ }
+ Ok(v)
+ }
+
+ fn try_as_array(self) -> Result<Vec<Self>> {
+ if let Value::Array(a) = self {
+ Ok(a)
+ } else {
+ cbor_type_error(&self, "array")
+ }
+ }
+
+ fn try_as_array_then_convert<F, T>(self, f: F) -> Result<Vec<T>>
+ where
+ F: Fn(Value) -> Result<T>,
+ {
+ self.try_as_array()?
+ .into_iter()
+ .map(f)
+ .collect::<Result<Vec<_>, _>>()
+ }
+
+ fn try_as_map(self) -> Result<Vec<(Self, Self)>> {
+ if let Value::Map(a) = self {
+ Ok(a)
+ } else {
+ cbor_type_error(&self, "map")
+ }
+ }
+
+ fn try_as_tag(self) -> Result<(u64, Box<Value>)> {
+ if let Value::Tag(a, v) = self {
+ Ok((a, v))
+ } else {
+ cbor_type_error(&self, "tag")
+ }
+ }
+}
+
+/// Trait for types that can be converted to/from a [`Value`].
+pub trait AsCborValue: Sized {
+ /// Convert a [`Value`] into an instance of the type.
+ fn from_cbor_value(value: Value) -> Result<Self>;
+ /// Convert the object into a [`Value`], consuming it along the way.
+ fn to_cbor_value(self) -> Result<Value>;
+}
+
+/// Convert each item of an iterator to CBOR, and wrap the lot in
+/// a [`Value::Array`]
+pub fn to_cbor_array<C>(c: C) -> Result<Value>
+where
+ C: IntoIterator,
+ C::Item: AsCborValue,
+{
+ Ok(Value::Array(
+ c.into_iter()
+ .map(|e| e.to_cbor_value())
+ .collect::<Result<Vec<_>, _>>()?,
+ ))
+}
+
+/// Check for an expected error.
+#[cfg(test)]
+pub fn expect_err<T: core::fmt::Debug, E: core::fmt::Debug>(result: Result<T, E>, err_msg: &str) {
+ use alloc::format;
+ assert!(
+ result.is_err(),
+ "expected error containing '{}', got success {:?}",
+ err_msg,
+ result
+ );
+ let err = result.err();
+ assert!(
+ format!("{:?}", err).contains(err_msg),
+ "unexpected error {:?}, doesn't contain '{}'",
+ err,
+ err_msg
+ );
+}
+
+// Macros to reduce boilerplate when creating `CoseSomethingBuilder` structures.
+
+/// Add `new()` and `build()` methods to the builder.
+macro_rules! builder {
+ ( $otype: ty ) => {
+ /// Constructor for builder.
+ pub fn new() -> Self {
+ Self(<$otype>::default())
+ }
+ /// Build the completed object.
+ pub fn build(self) -> $otype {
+ self.0
+ }
+ };
+}
+
+/// Add a setter function for a field to the builder.
+macro_rules! builder_set {
+ ( $name:ident: $ftype:ty ) => {
+ /// Set the associated field.
+ #[must_use]
+ pub fn $name(mut self, $name: $ftype) -> Self {
+ self.0.$name = $name;
+ self
+ }
+ };
+}
+
+/// Add a setter function for an optional field to the builder.
+macro_rules! builder_set_optional {
+ ( $name:ident: $ftype:ty ) => {
+ /// Set the associated field.
+ #[must_use]
+ pub fn $name(mut self, $name: $ftype) -> Self {
+ self.0.$name = Some($name);
+ self
+ }
+ };
+}
+
+/// Add a setter function that fills out a `ProtectedHeader` from `Header` contents.
+macro_rules! builder_set_protected {
+ ( $name:ident ) => {
+ /// Set the associated field.
+ #[must_use]
+ pub fn $name(mut self, hdr: $crate::Header) -> Self {
+ self.0.$name = $crate::ProtectedHeader {
+ original_data: None,
+ header: hdr,
+ };
+ self
+ }
+ };
+}
diff --git a/src/util/tests.rs b/src/util/tests.rs
new file mode 100644
index 0000000..b75d248
--- /dev/null
+++ b/src/util/tests.rs
@@ -0,0 +1,40 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+use super::*;
+use crate::{cbor::value::Value, util::expect_err};
+use alloc::{borrow::ToOwned, boxed::Box, vec};
+
+#[test]
+fn test_cbor_type_error() {
+ let cases = vec![
+ (Value::Null, "nul"),
+ (Value::Bool(true), "bool"),
+ (Value::Bool(false), "bool"),
+ (Value::from(128), "int"),
+ (Value::from(-1), "int"),
+ (Value::Bytes(vec![1, 2]), "bstr"),
+ (Value::Text("string".to_owned()), "tstr"),
+ (Value::Array(vec![Value::from(0)]), "array"),
+ (Value::Map(vec![]), "map"),
+ (Value::Tag(1, Box::new(Value::from(0))), "tag"),
+ (Value::Float(1.054571817), "float"),
+ ];
+ for (val, want) in cases {
+ let e = cbor_type_error::<()>(&val, "a");
+ expect_err(e, want);
+ }
+}