aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-07-07 04:58:17 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-07-07 04:58:17 +0000
commitf55619e0447595170080b5e2bbfcf664f95e8c8f (patch)
treeba4e327477ad1386a0449ff67c60cd1f1c05a890
parentacba5c45f25c9fd43da34fa383448eb0a4279de7 (diff)
parent924690d88ac076d17921eb8ba110233b7ea12e03 (diff)
downloadcoset-android14-mainline-extservices-release.tar.gz
Change-Id: Ia662a511a4c38755094631842c2bd7cbab547409
-rw-r--r--.cargo_vcs_info.json2
-rw-r--r--.github/workflows/ci.yml27
-rw-r--r--Android.bp32
-rw-r--r--CHANGELOG.md14
-rw-r--r--Cargo.lock2
-rw-r--r--Cargo.toml19
-rw-r--r--Cargo.toml.orig7
-rw-r--r--METADATA14
-rw-r--r--TEST_MAPPING12
-rw-r--r--cargo2android.json7
-rw-r--r--examples/cwt.rs96
-rw-r--r--examples/signature.rs10
-rw-r--r--patches/std.diff8
-rwxr-xr-xscripts/check-format.sh2
-rw-r--r--src/common/mod.rs3
-rw-r--r--src/cwt/mod.rs199
-rw-r--r--src/cwt/tests.rs235
-rw-r--r--src/encrypt/mod.rs48
-rw-r--r--src/iana/mod.rs52
-rw-r--r--src/key/mod.rs16
-rw-r--r--src/key/tests.rs27
-rw-r--r--src/lib.rs4
-rw-r--r--src/util/mod.rs14
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
diff --git a/Android.bp b/Android.bp
index 4c0f293..7dcf870 100644
--- a/Android.bp
+++ b/Android.bp
@@ -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`.
diff --git a/Cargo.lock b/Cargo.lock
index d257ebb..328bb43 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -31,7 +31,7 @@ dependencies = [
[[package]]
name = "coset"
-version = "0.3.1"
+version = "0.3.4"
dependencies = [
"ciborium",
"ciborium-io",
diff --git a/Cargo.toml b/Cargo.toml
index eb4661b..5f31443 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"] }
diff --git a/METADATA b/METADATA
index c3bb0c2..16e9249 100644
--- a/METADATA
+++ b/METADATA
@@ -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);
diff --git a/src/lib.rs b/src/lib.rs
index 3c46fcf..5ee10f3 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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(_) => {