diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-07-07 04:58:17 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-07-07 04:58:17 +0000 |
commit | f55619e0447595170080b5e2bbfcf664f95e8c8f (patch) | |
tree | ba4e327477ad1386a0449ff67c60cd1f1c05a890 | |
parent | acba5c45f25c9fd43da34fa383448eb0a4279de7 (diff) | |
parent | 924690d88ac076d17921eb8ba110233b7ea12e03 (diff) | |
download | coset-android14-mainline-extservices-release.tar.gz |
Snap for 10453563 from 924690d88ac076d17921eb8ba110233b7ea12e03 to mainline-extservices-releaseaml_ext_341716000aml_ext_341620040aml_ext_341518010aml_ext_341414010aml_ext_341317010aml_ext_341131030aml_ext_341027030android14-mainline-extservices-release
Change-Id: Ia662a511a4c38755094631842c2bd7cbab547409
-rw-r--r-- | .cargo_vcs_info.json | 2 | ||||
-rw-r--r-- | .github/workflows/ci.yml | 27 | ||||
-rw-r--r-- | Android.bp | 32 | ||||
-rw-r--r-- | CHANGELOG.md | 14 | ||||
-rw-r--r-- | Cargo.lock | 2 | ||||
-rw-r--r-- | Cargo.toml | 19 | ||||
-rw-r--r-- | Cargo.toml.orig | 7 | ||||
-rw-r--r-- | METADATA | 14 | ||||
-rw-r--r-- | TEST_MAPPING | 12 | ||||
-rw-r--r-- | cargo2android.json | 7 | ||||
-rw-r--r-- | examples/cwt.rs | 96 | ||||
-rw-r--r-- | examples/signature.rs | 10 | ||||
-rw-r--r-- | patches/std.diff | 8 | ||||
-rwxr-xr-x | scripts/check-format.sh | 2 | ||||
-rw-r--r-- | src/common/mod.rs | 3 | ||||
-rw-r--r-- | src/cwt/mod.rs | 199 | ||||
-rw-r--r-- | src/cwt/tests.rs | 235 | ||||
-rw-r--r-- | src/encrypt/mod.rs | 48 | ||||
-rw-r--r-- | src/iana/mod.rs | 52 | ||||
-rw-r--r-- | src/key/mod.rs | 16 | ||||
-rw-r--r-- | src/key/tests.rs | 27 | ||||
-rw-r--r-- | src/lib.rs | 4 | ||||
-rw-r--r-- | src/util/mod.rs | 14 |
23 files changed, 798 insertions, 52 deletions
diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json index 2fcab48..79d70ec 100644 --- a/.cargo_vcs_info.json +++ b/.cargo_vcs_info.json @@ -1,6 +1,6 @@ { "git": { - "sha1": "818bd1e3b3ff44e9b179532d2b4d942ea34bd1a1" + "sha1": "8a8552a8b57f004d08d081230659b47578c86b66" }, "path_in_vcs": "" }
\ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3430ad1..14bc1f5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,7 @@ jobs: components: rustfmt override: true - run: cargo build --release --workspace + - run: cargo build --release --workspace --features=std test: runs-on: ubuntu-latest @@ -48,6 +49,28 @@ jobs: components: rustfmt override: true - run: cargo test --workspace -- --nocapture + - run: cargo test --workspace --features=std -- --nocapture + + examples: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + - beta + - nightly-2022-01-01 + steps: + - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 + with: + submodules: true + - uses: actions-rs/toolchain@63eb9591781c46a70274cb3ebdf190fce92702e8 # v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + components: rustfmt + override: true + - run: cargo test --examples + - run: cargo test --features=std --examples no_std: name: Build for a no_std target @@ -87,6 +110,7 @@ jobs: components: rustfmt override: true - run: rustc --version + - run: cargo build --release --workspace - run: cargo build --release --workspace --all-features formatting: @@ -94,7 +118,7 @@ jobs: steps: - uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2 - uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4 # v2 - - run: go get github.com/campoy/embedmd + - run: go install github.com/campoy/embedmd@97c13d6 - uses: actions/setup-ruby@b007fae6f1ffbe3a51c00a6df6f5ff01184d5340 # v1 - run: gem install mdl - uses: actions-rs/toolchain@63eb9591781c46a70274cb3ebdf190fce92702e8 # v1 @@ -175,6 +199,7 @@ jobs: - uses: actions-rs/install@69ec87709ffb5b19a7b5ddbf610cb221498bb1eb # v0.1.2 with: crate: cargo-tarpaulin + version: 0.20.1 use-tool-cache: true - run: cargo tarpaulin --verbose --ignore-tests --all-features --timeout=600 --out Xml - name: Upload to codecov.io @@ -1,4 +1,4 @@ -// This file is generated by cargo2android.py --run --device --dependencies. +// This file is generated by cargo2android.py --config cargo2android.json. // Do not modify this file as changes will be overridden on upgrade. package { @@ -18,16 +18,44 @@ license { ], } +rust_test { + name: "coset_test_src_lib", + host_supported: true, + crate_name: "coset", + cargo_env_compat: true, + cargo_pkg_version: "0.3.4", + srcs: ["src/lib.rs"], + test_suites: ["general-tests"], + auto_gen_config: true, + test_options: { + unit_test: true, + }, + edition: "2018", + features: ["default"], + rustlibs: [ + "libciborium", + "libciborium_io", + "libhex", + ], +} + rust_library { name: "libcoset", host_supported: true, crate_name: "coset", cargo_env_compat: true, - cargo_pkg_version: "0.3.1", + cargo_pkg_version: "0.3.4", srcs: ["src/lib.rs"], edition: "2018", + features: ["default"], rustlibs: [ "libciborium", "libciborium_io", ], + apex_available: [ + "//apex_available:platform", + "//apex_available:anyapex", + ], + product_available: true, + vendor_available: true, } diff --git a/CHANGELOG.md b/CHANGELOG.md index f8d4915..df2b2cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Change Log +## 0.3.4 - 2023-01-25 + +- Add non-default `std` feature that turns on `impl Error for CoseError`. +- Add `cwt::ClaimsSetBuilder::private_claim` method. +- Update documentation for existing encryption methods to make it clear that they only support AEAD encryption. + +## 0.3.3 - 2022-09-30 + +- Add `CoseKeyBuilder` methods `kty`, `key_type` and `new_okp_key`. + +## 0.3.2 - 2022-04-02 + +- Add basic [CWT](https://datatracker.ietf.org/doc/html/rfc8392) support in `cwt` module, via the `ClaimsSet` type. + ## 0.3.1 - 2022-02-23 - Implement `Display` for `CoseError`. @@ -31,7 +31,7 @@ dependencies = [ [[package]] name = "coset" -version = "0.3.1" +version = "0.3.4" dependencies = [ "ciborium", "ciborium-io", @@ -12,13 +12,21 @@ [package] edition = "2018" name = "coset" -version = "0.3.1" -authors = ["David Drysdale <drysdale@google.com>", "Paul Crowley <paulcrowley@google.com>"] +version = "0.3.4" +authors = [ + "David Drysdale <drysdale@google.com>", + "Paul Crowley <paulcrowley@google.com>", +] description = "Set of types for supporting COSE" -keywords = ["cryptography", "cose"] +readme = "README.md" +keywords = [ + "cryptography", + "cose", +] categories = ["cryptography"] license = "Apache-2.0" repository = "https://github.com/google/coset" + [dependencies.ciborium] version = "^0.2.0" default-features = false @@ -26,5 +34,10 @@ default-features = false [dependencies.ciborium-io] version = "^0.2.0" features = ["alloc"] + [dev-dependencies.hex] version = "^0.4.2" + +[features] +default = [] +std = [] diff --git a/Cargo.toml.orig b/Cargo.toml.orig index 9555fdf..e011996 100644 --- a/Cargo.toml.orig +++ b/Cargo.toml.orig @@ -1,6 +1,6 @@ [package] name = "coset" -version = "0.3.1" +version = "0.3.4" authors = ["David Drysdale <drysdale@google.com>", "Paul Crowley <paulcrowley@google.com>"] edition = "2018" license = "Apache-2.0" @@ -9,6 +9,11 @@ repository = "https://github.com/google/coset" keywords = ["cryptography", "cose"] categories = ["cryptography"] +[features] +default = [] +# `std` feature enables an `Error` impl for `CoseError` +std = [] + [dependencies] ciborium = { version = "^0.2.0", default-features = false } ciborium-io = { version = "^0.2.0", features = ["alloc"] } @@ -1,3 +1,7 @@ +# This project was upgraded with external_updater. +# Usage: tools/external_updater/updater.sh update rust/crates/coset +# For more info, check https://cs.android.com/android/platform/superproject/+/master:tools/external_updater/README.md + name: "coset" description: "Set of types for supporting COSE" third_party { @@ -7,13 +11,13 @@ third_party { } url { type: ARCHIVE - value: "https://static.crates.io/crates/coset/coset-0.3.1.crate" + value: "https://static.crates.io/crates/coset/coset-0.3.4.crate" } - version: "0.3.1" + version: "0.3.4" license_type: NOTICE last_upgrade_date { - year: 2022 - month: 3 - day: 1 + year: 2023 + month: 2 + day: 15 } } diff --git a/TEST_MAPPING b/TEST_MAPPING index 2f7de78..c1f672e 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -1,13 +1,21 @@ // Generated by update_crate_tests.py for tests that depend on this crate. { + "imports": [ + { + "path": "system/keymint/derive" + }, + { + "path": "system/keymint/hal" + } + ], "presubmit": [ { - "name": "libcert_request_validator_tests" + "name": "coset_test_src_lib" } ], "presubmit-rust": [ { - "name": "libcert_request_validator_tests" + "name": "coset_test_src_lib" } ] } diff --git a/cargo2android.json b/cargo2android.json new file mode 100644 index 0000000..cf7ea4a --- /dev/null +++ b/cargo2android.json @@ -0,0 +1,7 @@ +{ + "device": true, + "run": true, + "dependencies": true, + "vendor-available": true, + "tests": true +} diff --git a/examples/cwt.rs b/examples/cwt.rs new file mode 100644 index 0000000..5972cf9 --- /dev/null +++ b/examples/cwt.rs @@ -0,0 +1,96 @@ +// Copyright 2022 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 signed CWT processing. +use coset::{cbor::value::Value, cwt, iana, CborSerializable, CoseError}; + +#[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() -> Result<(), CoseError> { + // Build a fake signer/verifier (to avoid pulling in lots of dependencies). + let signer = FakeSigner {}; + let verifier = signer; + + // Build a CWT ClaimsSet (cf. RFC 8392 A.3). + let claims = cwt::ClaimsSetBuilder::new() + .issuer("coap://as.example.com".to_string()) + .subject("erikw".to_string()) + .audience("coap://light.example.com".to_string()) + .expiration_time(cwt::Timestamp::WholeSeconds(1444064944)) + .not_before(cwt::Timestamp::WholeSeconds(1443944944)) + .issued_at(cwt::Timestamp::WholeSeconds(1443944944)) + .cwt_id(vec![0x0b, 0x71]) + // Add additional standard claim. + .claim( + iana::CwtClaimName::Scope, + Value::Text("email phone".to_string()), + ) + // Add additional private-use claim. + .private_claim(-70_000, Value::Integer(42.into())) + .build(); + let aad = b""; + + // Build a `CoseSign1` object. + let protected = coset::HeaderBuilder::new() + .algorithm(iana::Algorithm::ES256) + .build(); + let unprotected = coset::HeaderBuilder::new() + .key_id(b"AsymmetricECDSA256".to_vec()) + .build(); + let sign1 = coset::CoseSign1Builder::new() + .protected(protected) + .unprotected(unprotected) + .payload(claims.clone().to_vec()?) + .create_signature(aad, |pt| signer.sign(pt)) + .build(); + + // Serialize to bytes. + let sign1_data = sign1.to_vec()?; + + // At the receiving end, deserialize the bytes back to a `CoseSign1` object. + let sign1 = coset::CoseSign1::from_slice(&sign1_data)?; + + // Real code would: + // - Use the key ID to identify the relevant local key. + // - Check that the key is of the same type as `sign1.protected.algorithm`. + + // Check the signature. + let result = sign1.verify_signature(aad, |sig, data| verifier.verify(sig, data)); + println!("Signature verified: {:?}.", result); + assert!(result.is_ok()); + + // Now it's safe to parse the payload. + let recovered_claims = cwt::ClaimsSet::from_slice(&sign1.payload.unwrap())?; + + assert_eq!(recovered_claims, claims); + Ok(()) +} diff --git a/examples/signature.rs b/examples/signature.rs index 4512df7..cf8b91a 100644 --- a/examples/signature.rs +++ b/examples/signature.rs @@ -15,7 +15,7 @@ //////////////////////////////////////////////////////////////////////////////// //! Example program demonstrating signature creation. -use coset::{iana, CborSerializable}; +use coset::{iana, CborSerializable, CoseError}; #[derive(Copy, Clone)] struct FakeSigner {} @@ -35,7 +35,7 @@ impl FakeSigner { } } -fn main() { +fn main() -> Result<(), CoseError> { // Build a fake signer/verifier (to avoid pulling in lots of dependencies). let signer = FakeSigner {}; let verifier = signer; @@ -56,7 +56,7 @@ fn main() { .build(); // Serialize to bytes. - let sign1_data = sign1.to_vec().unwrap(); + let sign1_data = sign1.to_vec()?; println!( "'{}' + '{}' => {}", String::from_utf8_lossy(pt), @@ -65,7 +65,7 @@ fn main() { ); // At the receiving end, deserialize the bytes back to a `CoseSign1` object. - let mut sign1 = coset::CoseSign1::from_slice(&sign1_data).unwrap(); + let mut sign1 = coset::CoseSign1::from_slice(&sign1_data)?; // Check the signature, which needs to have the same `aad` provided. let result = sign1.verify_signature(aad, |sig, data| verifier.verify(sig, data)); @@ -85,7 +85,9 @@ fn main() { // Changing a protected header invalidates the signature. sign1.protected.header.content_type = Some(coset::ContentType::Text("text/plain".to_owned())); + sign1.protected.original_data = None; assert!(sign1 .verify_signature(aad, |sig, data| verifier.verify(sig, data)) .is_err()); + Ok(()) } diff --git a/patches/std.diff b/patches/std.diff index dd24015..244b78c 100644 --- a/patches/std.diff +++ b/patches/std.diff @@ -1,15 +1,15 @@ diff --git a/src/lib.rs b/src/lib.rs -index 2a8ceb3..3c46fcf 100644 +index 4ce9c93..a800c89 100644 --- a/src/lib.rs +++ b/src/lib.rs -@@ -100,6 +100,9 @@ +@@ -100,6 +100,10 @@ #![deny(rustdoc::broken_intra_doc_links)] extern crate alloc; +/// Use std to allow building as a dylib. ++#[cfg(android_dylib)] +extern crate std; + /// Re-export of the `ciborium` crate used for underlying CBOR encoding. pub use ciborium as cbor; - - +
\ No newline at end of file diff --git a/scripts/check-format.sh b/scripts/check-format.sh index 445a564..220022c 100755 --- a/scripts/check-format.sh +++ b/scripts/check-format.sh @@ -90,7 +90,7 @@ done EMBEDMD="$(go env GOPATH)/bin/embedmd" if [[ ! -x "$EMBEDMD" ]]; then - go get github.com/campoy/embedmd + go install github.com/campoy/embedmd@97c13d6 fi for f in "${MD_FILES[@]}"; do "$EMBEDMD" -d "$f" diff --git a/src/common/mod.rs b/src/common/mod.rs index c1f2973..106a3a3 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -87,6 +87,9 @@ impl core::fmt::Display for CoseError { } } +#[cfg(feature = "std")] +impl std::error::Error for CoseError {} + impl CoseError { fn fmt_msg(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { diff --git a/src/cwt/mod.rs b/src/cwt/mod.rs new file mode 100644 index 0000000..d12531a --- /dev/null +++ b/src/cwt/mod.rs @@ -0,0 +1,199 @@ +// 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. +// +//////////////////////////////////////////////////////////////////////////////// + +//! CBOR Web Token functionality. + +use crate::{ + cbor::value::Value, + common::AsCborValue, + iana, + iana::{EnumI64, WithPrivateRange}, + util::{cbor_type_error, ValueTryAs}, + CoseError, +}; +use alloc::{collections::BTreeSet, string::String, vec::Vec}; +use core::convert::TryInto; + +#[cfg(test)] +mod tests; + +/// Number of seconds since UNIX epoch. +#[derive(Clone, Debug, PartialEq)] +pub enum Timestamp { + WholeSeconds(i64), + FractionalSeconds(f64), +} + +impl AsCborValue for Timestamp { + fn from_cbor_value(value: Value) -> Result<Self, CoseError> { + match value { + Value::Integer(i) => Ok(Timestamp::WholeSeconds(i.try_into()?)), + Value::Float(f) => Ok(Timestamp::FractionalSeconds(f)), + _ => cbor_type_error(&value, "int/float"), + } + } + fn to_cbor_value(self) -> Result<Value, CoseError> { + Ok(match self { + Timestamp::WholeSeconds(t) => Value::Integer(t.into()), + Timestamp::FractionalSeconds(f) => Value::Float(f), + }) + } +} + +/// Claim name. +pub type ClaimName = crate::RegisteredLabelWithPrivate<iana::CwtClaimName>; + +/// Structure representing a CWT Claims Set. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct ClaimsSet { + /// Issuer + pub issuer: Option<String>, + /// Subject + pub subject: Option<String>, + /// Audience + pub audience: Option<String>, + /// Expiration Time + pub expiration_time: Option<Timestamp>, + /// Not Before + pub not_before: Option<Timestamp>, + /// Issued At + pub issued_at: Option<Timestamp>, + /// CWT ID + pub cwt_id: Option<Vec<u8>>, + /// Any additional claims. + pub rest: Vec<(ClaimName, Value)>, +} + +impl crate::CborSerializable for ClaimsSet {} + +const ISS: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Iss); +const SUB: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Sub); +const AUD: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Aud); +const EXP: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Exp); +const NBF: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Nbf); +const IAT: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Iat); +const CTI: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Cti); + +impl AsCborValue for ClaimsSet { + fn from_cbor_value(value: Value) -> Result<Self, CoseError> { + let m = match value { + Value::Map(m) => m, + v => return cbor_type_error(&v, "map"), + }; + + let mut claims = Self::default(); + let mut seen = BTreeSet::new(); + for (n, value) in m.into_iter() { + // The `ciborium` CBOR library does not police duplicate map keys, so do it here. + let name = ClaimName::from_cbor_value(n)?; + if seen.contains(&name) { + return Err(CoseError::DuplicateMapKey); + } + seen.insert(name.clone()); + match name { + x if x == ISS => claims.issuer = Some(value.try_as_string()?), + x if x == SUB => claims.subject = Some(value.try_as_string()?), + x if x == AUD => claims.audience = Some(value.try_as_string()?), + x if x == EXP => claims.expiration_time = Some(Timestamp::from_cbor_value(value)?), + x if x == NBF => claims.not_before = Some(Timestamp::from_cbor_value(value)?), + x if x == IAT => claims.issued_at = Some(Timestamp::from_cbor_value(value)?), + x if x == CTI => claims.cwt_id = Some(value.try_as_bytes()?), + name => claims.rest.push((name, value)), + } + } + Ok(claims) + } + + fn to_cbor_value(self) -> Result<Value, CoseError> { + let mut map = Vec::new(); + if let Some(iss) = self.issuer { + map.push((ISS.to_cbor_value()?, Value::Text(iss))); + } + if let Some(sub) = self.subject { + map.push((SUB.to_cbor_value()?, Value::Text(sub))); + } + if let Some(aud) = self.audience { + map.push((AUD.to_cbor_value()?, Value::Text(aud))); + } + if let Some(exp) = self.expiration_time { + map.push((EXP.to_cbor_value()?, exp.to_cbor_value()?)); + } + if let Some(nbf) = self.not_before { + map.push((NBF.to_cbor_value()?, nbf.to_cbor_value()?)); + } + if let Some(iat) = self.issued_at { + map.push((IAT.to_cbor_value()?, iat.to_cbor_value()?)); + } + if let Some(cti) = self.cwt_id { + map.push((CTI.to_cbor_value()?, Value::Bytes(cti))); + } + for (label, value) in self.rest { + map.push((label.to_cbor_value()?, value)); + } + Ok(Value::Map(map)) + } +} + +/// Builder for [`ClaimsSet`] objects. +#[derive(Default)] +pub struct ClaimsSetBuilder(ClaimsSet); + +impl ClaimsSetBuilder { + builder! {ClaimsSet} + builder_set_optional! {issuer: String} + builder_set_optional! {subject: String} + builder_set_optional! {audience: String} + builder_set_optional! {expiration_time: Timestamp} + builder_set_optional! {not_before: Timestamp} + builder_set_optional! {issued_at: Timestamp} + builder_set_optional! {cwt_id: Vec<u8>} + + /// Set a claim name:value pair. + /// + /// # Panics + /// + /// This function will panic if it used to set a claim with name from the range [1, 7]. + #[must_use] + pub fn claim(mut self, name: iana::CwtClaimName, value: Value) -> Self { + if name.to_i64() >= iana::CwtClaimName::Iss.to_i64() + && name.to_i64() <= iana::CwtClaimName::Cti.to_i64() + { + panic!("claim() method used to set core claim"); // safe: invalid input + } + self.0.rest.push((ClaimName::Assigned(name), value)); + self + } + + /// Set a claim name:value pair where the `name` is text. + #[must_use] + pub fn text_claim(mut self, name: String, value: Value) -> Self { + self.0.rest.push((ClaimName::Text(name), value)); + self + } + + /// Set a claim where the claim key is a numeric value from the private use range. + /// + /// # Panics + /// + /// This function will panic if it is used to set a claim with a key value outside of the + /// private use range. + #[must_use] + pub fn private_claim(mut self, id: i64, value: Value) -> Self { + assert!(iana::CwtClaimName::is_private(id)); + self.0.rest.push((ClaimName::PrivateUse(id), value)); + self + } +} diff --git a/src/cwt/tests.rs b/src/cwt/tests.rs new file mode 100644 index 0000000..ff59dd6 --- /dev/null +++ b/src/cwt/tests.rs @@ -0,0 +1,235 @@ +// 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, iana::WithPrivateRange, util::expect_err, CborSerializable}; +use alloc::{borrow::ToOwned, vec}; + +#[test] +fn test_cwt_encode() { + let tests = vec![ + ( + ClaimsSet { + issuer: Some("abc".to_owned()), + ..Default::default() + }, + concat!( + "a1", // 1-map + "01", "63", "616263" // 1 (iss) => 3-tstr + ), + ), + (ClaimsSetBuilder::new().build(), concat!("a0")), + ( + ClaimsSetBuilder::new() + .issuer("aaa".to_owned()) + .subject("bb".to_owned()) + .audience("c".to_owned()) + .expiration_time(Timestamp::WholeSeconds(0x100)) + .not_before(Timestamp::WholeSeconds(0x200)) + .issued_at(Timestamp::WholeSeconds(0x10)) + .cwt_id(vec![1, 2, 3, 4]) + .private_claim(-70_000, Value::Integer(0.into())) + .build(), + concat!( + "a8", // 8-map + "01", + "63", + "616161", // 1 (iss) => 3-tstr + "02", + "62", + "6262", // 2 (sub) => 2-tstr + "03", + "61", + "63", // 3 (aud) => 1-tstr + "04", + "19", + "0100", // 4 (exp) => uint + "05", + "19", + "0200", // 5 (nbf) => uint + "06", + "10", // 6 (iat) => uint + "07", + "44", + "01020304", // 7 => bstr + "3a0001116f", + "00" // -70000 => uint + ), + ), + ( + ClaimsSetBuilder::new() + .claim( + iana::CwtClaimName::Cnf, + Value::Map(vec![(Value::Integer(0.into()), Value::Integer(0.into()))]), + ) + .build(), + concat!( + "a1", // 1-map + "08", "a1", "00", "00" + ), + ), + ( + ClaimsSetBuilder::new() + .text_claim("aa".to_owned(), Value::Integer(0.into())) + .build(), + concat!( + "a1", // 1-map + "62", "6161", "00", + ), + ), + ( + ClaimsSetBuilder::new() + .expiration_time(Timestamp::FractionalSeconds(1.5)) + .build(), + concat!( + "a1", // 1-map + "04", // 4 (exp) => + // Note: ciborium serializes floats as the smallest float type that + // will parse back to the original f64! As a result, 1.5 is encoded + // as an f16. + "f9", "3e00", + ), + ), + ]; + for (i, (claims, claims_data)) in tests.iter().enumerate() { + let got = claims.clone().to_vec().unwrap(); + assert_eq!(*claims_data, hex::encode(&got), "case {}", i); + + let got = ClaimsSet::from_slice(&got).unwrap(); + assert_eq!(*claims, got); + } +} + +#[test] +fn test_cwt_decode_fail() { + let tests = vec![ + ( + concat!( + "81", // 1-arr + "01", + ), + "expected map", + ), + ( + concat!( + "a1", // 1-map + "01", "08", // 1 (iss) => int (invalid value type) + ), + "expected tstr", + ), + ( + concat!( + "a1", // 1-map + "02", "08", // 2 (sub) => int (invalid value type) + ), + "expected tstr", + ), + ( + concat!( + "a1", // 1-map + "03", "08", // 3 (aud) => int (invalid value type) + ), + "expected tstr", + ), + ( + concat!( + "a1", // 1-map + "04", "40", // 4 (exp) => bstr (invalid value type) + ), + "expected int/float", + ), + ( + concat!( + "a1", // 1-map + "05", "40", // 5 (nbf) => bstr (invalid value type) + ), + "expected int/float", + ), + ( + concat!( + "a1", // 1-map + "06", "40", // 6 (iat) => bstr (invalid value type) + ), + "expected int/float", + ), + ( + concat!( + "a1", // 1-map + "07", "01", // 5 (cti) => uint (invalid value type) + ), + "expected bstr", + ), + ( + concat!( + "a1", // 1-map + "07", "40", // 5 (cti) => 0-bstr + "06", "01", // 6 (iat) => 1 + ), + "extraneous data", + ), + ( + concat!( + "a2", // 1-map + "07", "40", // 5 (cti) => 0-bstr + "07", "40", // 5 (cti) => 0-bstr + ), + "duplicate map key", + ), + ]; + for (claims_data, err_msg) in tests.iter() { + let data = hex::decode(claims_data).unwrap(); + let result = ClaimsSet::from_slice(&data); + expect_err(result, err_msg); + } +} + +#[test] +fn test_cwt_is_private() { + assert!(!iana::CwtClaimName::is_private(1)); + assert!(iana::CwtClaimName::is_private(-500_000)); +} + +#[test] +#[should_panic] +fn test_cwt_claims_builder_core_param_panic() { + // Attempting to set a core claim (in range [1,7]) via `.claim()` panics. + let _claims = ClaimsSetBuilder::new() + .claim(iana::CwtClaimName::Iss, Value::Null) + .build(); +} + +#[test] +#[should_panic] +fn test_cwt_claims_builder_non_private_panic() { + // Attempting to set a claim outside of private range via `.private_claim()` panics. + let _claims = ClaimsSetBuilder::new() + .private_claim(100, Value::Null) + .build(); +} + +#[test] +fn test_cwt_dup_claim() { + // Set a duplicate map key. + let claims = ClaimsSetBuilder::new() + .claim(iana::CwtClaimName::AceProfile, Value::Integer(1.into())) + .claim(iana::CwtClaimName::AceProfile, Value::Integer(2.into())) + .build(); + // Encoding succeeds. + let data = claims.to_vec().unwrap(); + // But an attempt to parse the encoded data fails. + let result = ClaimsSet::from_slice(&data); + expect_err(result, "duplicate map key"); +} diff --git a/src/encrypt/mod.rs b/src/encrypt/mod.rs index 22f9777..db38de5 100644 --- a/src/encrypt/mod.rs +++ b/src/encrypt/mod.rs @@ -95,8 +95,8 @@ impl AsCborValue for CoseRecipient { } impl CoseRecipient { - /// Decrypt the `ciphertext` value, using `cipher` to decrypt the cipher text and - /// combined AAD. + /// Decrypt the `ciphertext` value with an AEAD, using `cipher` to decrypt the cipher text and + /// combined AAD as per RFC 8152 section 5.3. /// /// # Panics /// @@ -140,9 +140,9 @@ impl CoseRecipientBuilder { 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. + /// Calculate the ciphertext value with an AEAD, using `cipher` to generate the encrypted bytes + /// from the plaintext and combined AAD (in that order) as per RFC 8152 section 5.3. Any + /// protected header values should be set before using this method. /// /// # Panics /// @@ -162,9 +162,9 @@ impl CoseRecipientBuilder { 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. + /// Calculate the ciphertext value with an AEAD, using `cipher` to generate the encrypted bytes + /// from the plaintext and combined AAD (in that order) as per RFC 8152 section 5.3. Any + /// protected header values should be set before using this method. /// /// # Panics /// @@ -183,8 +183,8 @@ impl CoseRecipientBuilder { 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. + /// Construct the combined AAD data needed for encryption with an AEAD. Any protected header + /// values should be set before using this method. /// /// # Panics /// @@ -261,7 +261,7 @@ impl AsCborValue for CoseEncrypt { } impl CoseEncrypt { - /// Decrypt the `ciphertext` value, using `cipher` to decrypt the cipher text and + /// Decrypt the `ciphertext` value with an AEAD, using `cipher` to decrypt the cipher text and /// combined AAD. /// /// # Panics @@ -291,9 +291,9 @@ impl CoseEncryptBuilder { 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. + /// Calculate the ciphertext value with an AEAD, using `cipher` to generate the encrypted bytes + /// from the plaintext and combined AAD (in that order) as per RFC 8152 section 5.3. 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 @@ -307,9 +307,9 @@ impl CoseEncryptBuilder { 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. + /// Calculate the ciphertext value with an AEAD, using `cipher` to generate the encrypted bytes + /// from the plaintext and combined AAD (in that order) as per RFC 8152 section 5.3. Any + /// protected header values should be set before using this method. pub fn try_create_ciphertext<F, E>( self, plaintext: &[u8], @@ -389,7 +389,7 @@ impl AsCborValue for CoseEncrypt0 { } impl CoseEncrypt0 { - /// Decrypt the `ciphertext` value, using `cipher` to decrypt the cipher text and + /// Decrypt the `ciphertext` value with an AEAD, using `cipher` to decrypt the cipher text and /// combined AAD. /// /// # Panics @@ -419,9 +419,9 @@ impl CoseEncrypt0Builder { 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. + /// Calculate the ciphertext value with an AEAD, using `cipher` to generate the encrypted bytes + /// from the plaintext and combined AAD (in that order) as per RFC 8152 section 5.3. 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 @@ -435,9 +435,9 @@ impl CoseEncrypt0Builder { 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. + /// Calculate the ciphertext value with an AEAD, using `cipher` to generate the encrypted bytes + /// from the plaintext and combined AAD (in that order) as per RFC 8152 section 5.3. Any + /// protected header values should be set before using this method. pub fn try_create_ciphertext<F, E>( self, plaintext: &[u8], diff --git a/src/iana/mod.rs b/src/iana/mod.rs index 41c2ef8..3702014 100644 --- a/src/iana/mod.rs +++ b/src/iana/mod.rs @@ -20,6 +20,7 @@ //! - <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> +//! - <https://www.iana.org/assignments/cwt/cwt.xhtml> #[cfg(test)] mod tests; @@ -743,3 +744,54 @@ iana_registry! { VndOmaLwm2mCbor: 11544, } } + +iana_registry! { + /// CBOR Web Token (CWT) Claims + /// From IANA registry <https://www.iana.org/assignments/cwt/cwt.xhtml> + /// as of 2021-10-21. + CwtClaimName { + /// Health certificate ("hcert": map). + Hcert: -260, + /// Challenge nonce ("EUPHNonce": bstr). + EuphNonce: -259, + /// Signing prefix for multi-app restricted operating environment ("EATMAROEPrefix": bstr). + EatMaroePrefix: -258, + /// FIDO Device Onboarding EAT ("EAT-FDO": array). + EatFido: -257, + /// Reserved value. + Reserved: 0, + /// Issuer ("iss": tstr). + Iss: 1, + /// Subject ("sub": tstr) + Sub: 2, + /// Audience ("aud": tstr) + Aud: 3, + /// Expiration Time, as seconds since UNIX epoch ("exp": int/float) + Exp: 4, + /// Not Before, as seconds since UNIX epoch ("nbf": int/float) + Nbf: 5, + /// Issued at, as seconds since UNIX epoch ("iat": int/float) + Iat: 6, + /// CWT ID ("cti": bstr) + Cti: 7, + /// Confirmation ("cnf": map) + Cnf: 8, + /// Scope of an access token ("scope": bstr/tstr) + Scope: 9, + /// The ACE profile a token is supposed to be used with ("ace_profile": int) + AceProfile: 38, + /// The client-nonce sent to the AS by the RS via the client ("cnonce": bstr) + CNonce: 39, + /// The expiration time of a token measured from when it was received at the RS in seconds ("exi": int) + Exi: 40, + } +} + +/// Integer values for CWT claims below this value are reserved for private use. +pub const CWT_CLAIM_PRIVATE_USE_MAX: i64 = -65536; + +impl WithPrivateRange for CwtClaimName { + fn is_private(i: i64) -> bool { + i < CWT_CLAIM_PRIVATE_USE_MAX + } +} diff --git a/src/key/mod.rs b/src/key/mod.rs index b81ea8c..07ee7a7 100644 --- a/src/key/mod.rs +++ b/src/key/mod.rs @@ -183,6 +183,7 @@ pub struct CoseKeyBuilder(CoseKey); impl CoseKeyBuilder { builder! {CoseKey} + builder_set! {kty: KeyType} builder_set! {key_id: Vec<u8>} builder_set! {base_iv: Vec<u8>} @@ -250,6 +251,21 @@ impl CoseKeyBuilder { }) } + /// Constructor for a octet keypair key. + pub fn new_okp_key() -> Self { + Self(CoseKey { + kty: KeyType::Assigned(iana::KeyType::OKP), + ..Default::default() + }) + } + + /// Set the key type. + #[must_use] + pub fn key_type(mut self, key_type: iana::KeyType) -> Self { + self.0.kty = KeyType::Assigned(key_type); + self + } + /// Set the algorithm. #[must_use] pub fn algorithm(mut self, alg: iana::Algorithm) -> Self { diff --git a/src/key/tests.rs b/src/key/tests.rs index 713fe1a..45f8eef 100644 --- a/src/key/tests.rs +++ b/src/key/tests.rs @@ -16,7 +16,7 @@ use super::*; use crate::{cbor::value::Value, iana, util::expect_err, CborSerializable}; -use alloc::{borrow::ToOwned, vec}; +use alloc::{borrow::ToOwned, string::ToString, vec}; #[test] fn test_cose_key_encode() { @@ -702,6 +702,31 @@ fn test_key_builder() { ..Default::default() }, ), + ( + CoseKeyBuilder::new_okp_key().build(), + CoseKey { + kty: KeyType::Assigned(iana::KeyType::OKP), + ..Default::default() + }, + ), + ( + CoseKeyBuilder::new() + .key_type(iana::KeyType::WalnutDSA) + .build(), + CoseKey { + kty: KeyType::Assigned(iana::KeyType::WalnutDSA), + ..Default::default() + }, + ), + ( + CoseKeyBuilder::new() + .kty(KeyType::Text("test".to_string())) + .build(), + CoseKey { + kty: KeyType::Text("test".to_string()), + ..Default::default() + }, + ), ]; for (got, want) in tests { assert_eq!(got, want); @@ -96,11 +96,12 @@ //! [COSE]: https://tools.ietf.org/html/rfc8152 //! [CBOR]: https://tools.ietf.org/html/rfc7049 -#![no_std] +#![cfg_attr(not(feature = "std"), no_std)] #![deny(rustdoc::broken_intra_doc_links)] extern crate alloc; /// Use std to allow building as a dylib. +#[cfg(android_dylib)] extern crate std; /// Re-export of the `ciborium` crate used for underlying CBOR encoding. @@ -109,6 +110,7 @@ pub use ciborium as cbor; #[macro_use] pub(crate) mod util; +pub mod cwt; #[macro_use] pub mod iana; diff --git a/src/util/mod.rs b/src/util/mod.rs index d12337c..5dde295 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -21,7 +21,7 @@ use crate::{ common::AsCborValue, CoseError, Result, }; -use alloc::{boxed::Box, vec::Vec}; +use alloc::{boxed::Box, string::String, vec::Vec}; #[cfg(test)] mod tests; @@ -71,6 +71,9 @@ where /// Extractor for [`Value::Tag`] fn try_as_tag(self) -> Result<(u64, Box<Value>)>; + + /// Extractor for [`Value::Text`] + fn try_as_string(self) -> Result<String>; } impl ValueTryAs for Value { @@ -131,6 +134,14 @@ impl ValueTryAs for Value { cbor_type_error(&self, "tag") } } + + fn try_as_string(self) -> Result<String> { + if let Value::Text(s) = self { + Ok(s) + } else { + cbor_type_error(&self, "tstr") + } + } } /// Convert each item of an iterator to CBOR, and wrap the lot in @@ -153,6 +164,7 @@ pub fn expect_err<T: core::fmt::Debug, E: core::fmt::Debug + core::fmt::Display> result: Result<T, E>, err_msg: &str, ) { + #[cfg(not(feature = "std"))] use alloc::format; match result { Ok(_) => { |