From 099293a1d49cbe3981480ed5483c8745de06af97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Mon, 26 Aug 2024 15:26:07 +0200 Subject: [PATCH] Add support for COSE for VCs. (#593) * Update MSRV. --- Cargo.toml | 6 +- crates/claims/Cargo.toml | 9 +- crates/claims/core/src/signature.rs | 11 + crates/claims/core/src/verification/claims.rs | 26 +- crates/claims/crates/cose/Cargo.toml | 29 + crates/claims/crates/cose/src/algorithm.rs | 86 ++ crates/claims/crates/cose/src/key.rs | 732 ++++++++++++++++++ crates/claims/crates/cose/src/lib.rs | 189 +++++ crates/claims/crates/cose/src/sign1.rs | 331 ++++++++ crates/claims/crates/cose/src/signature.rs | 157 ++++ crates/claims/crates/cose/src/verification.rs | 135 ++++ .../sd-primitives/src/canonicalize.rs | 3 +- .../src/suites/w3c/rsa_signature_2018.rs | 6 +- crates/claims/crates/jws/src/compact/bytes.rs | 10 +- crates/claims/crates/jws/src/compact/str.rs | 6 +- crates/claims/crates/jws/src/lib.rs | 21 +- crates/claims/crates/sd-jwt/src/digest.rs | 4 +- crates/claims/crates/sd-jwt/src/disclosure.rs | 6 +- crates/claims/crates/sd-jwt/src/encode.rs | 6 +- crates/claims/crates/vc-jose-cose/Cargo.toml | 7 +- .../vc-jose-cose/src/cose/credential.rs | 233 ++++++ .../crates/vc-jose-cose/src/cose/mod.rs | 16 + .../vc-jose-cose/src/cose/presentation.rs | 164 ++++ crates/claims/crates/vc-jose-cose/src/lib.rs | 4 +- .../claims/crates/vc/src/v1/revocation/mod.rs | 5 +- crates/claims/src/lib.rs | 5 + crates/crypto/Cargo.toml | 10 +- crates/crypto/src/key.rs | 182 +++++ crates/crypto/src/lib.rs | 16 + crates/crypto/src/signature.rs | 67 ++ crates/crypto/src/verification.rs | 75 ++ crates/dids/methods/ion/src/sidetree/mod.rs | 8 +- crates/jwk/src/der.rs | 6 +- crates/jwk/src/lib.rs | 15 +- .../status/src/impl/token_status_list/json.rs | 5 +- crates/ucan/src/lib.rs | 19 +- 36 files changed, 2527 insertions(+), 83 deletions(-) create mode 100644 crates/claims/crates/cose/Cargo.toml create mode 100644 crates/claims/crates/cose/src/algorithm.rs create mode 100644 crates/claims/crates/cose/src/key.rs create mode 100644 crates/claims/crates/cose/src/lib.rs create mode 100644 crates/claims/crates/cose/src/sign1.rs create mode 100644 crates/claims/crates/cose/src/signature.rs create mode 100644 crates/claims/crates/cose/src/verification.rs create mode 100644 crates/claims/crates/vc-jose-cose/src/cose/credential.rs create mode 100644 crates/claims/crates/vc-jose-cose/src/cose/mod.rs create mode 100644 crates/claims/crates/vc-jose-cose/src/cose/presentation.rs create mode 100644 crates/crypto/src/key.rs create mode 100644 crates/crypto/src/signature.rs create mode 100644 crates/crypto/src/verification.rs diff --git a/Cargo.toml b/Cargo.toml index c7a23f64c..8407fdc51 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ description = "Core library for Verifiable Credentials and Decentralized Identif repository = "https://github.com/spruceid/ssi/" documentation = "https://docs.rs/ssi/" keywords = ["ssi", "did", "vc", "vp", "jsonld"] -rust-version = "1.77" +rust-version = "1.80" [workspace] members = [ @@ -47,6 +47,7 @@ ssi-claims-core = { path = "./crates/claims/core", version = "0.1", default-feat ssi-jws = { path = "./crates/claims/crates/jws", version = "0.2", default-features = false } ssi-jwt = { path = "./crates/claims/crates/jwt", version = "0.2", default-features = false } ssi-sd-jwt = { path = "./crates/claims/crates/sd-jwt", version = "0.2", default-features = false } +ssi-cose = { path = "./crates/claims/crates/cose", version = "0.1", default-features = false } ssi-vc = { path = "./crates/claims/crates/vc", version = "0.3", default-features = false } ssi-vc-jose-cose = { path = "./crates/claims/crates/vc-jose-cose", version = "0.1", default-features = false } ssi-data-integrity-core = { path = "./crates/claims/crates/data-integrity/core", version = "0.1", default-features = false } @@ -95,8 +96,9 @@ multibase = "0.9.1" serde = "1.0" serde_json = { version = "1.0", features = ["arbitrary_precision"] } serde_jcs = "0.1.0" +ciborium = "0.2.2" bs58 = "0.4" -base64 = "0.12" +base64 = "0.22" hex = "0.4.3" derivative = "2.2.0" educe = "0.4.22" diff --git a/crates/claims/Cargo.toml b/crates/claims/Cargo.toml index cd4fccd53..d2b2f591a 100644 --- a/crates/claims/Cargo.toml +++ b/crates/claims/Cargo.toml @@ -19,20 +19,20 @@ dif = ["ssi-data-integrity/dif"] # - `Ed25519Signature2018` # - `Ed25519Signature2020` # - `EdDsa2022` -ed25519 = ["ssi-jws/ed25519", "ssi-data-integrity/ed25519", "ssi-verification-methods/ed25519"] +ed25519 = ["ssi-jws/ed25519", "ssi-cose/ed25519", "ssi-data-integrity/ed25519", "ssi-verification-methods/ed25519"] # Enables signature suites based on secp256k1: # - `EcdsaSecp256k1Signature2019` -secp256k1 = ["ssi-jws/secp256k1", "ssi-data-integrity/secp256k1", "ssi-verification-methods/secp256k1"] +secp256k1 = ["ssi-jws/secp256k1", "ssi-cose/secp256k1", "ssi-data-integrity/secp256k1", "ssi-verification-methods/secp256k1"] # Enables signature suites based on secp256r1: # - `EcdsaSecp256r1Signature2019` # - `EcdsaRdfc2019` -secp256r1 = ["ssi-jws/secp256r1", "ssi-data-integrity/secp256r1", "ssi-verification-methods/secp256r1"] +secp256r1 = ["ssi-jws/secp256r1", "ssi-cose/secp256r1", "ssi-data-integrity/secp256r1", "ssi-verification-methods/secp256r1"] # Enables signature suites based on secp384r1: # - `EcdsaRdfc2019` -secp384r1 = ["ssi-jws/secp384r1", "ssi-data-integrity/secp384r1", "ssi-verification-methods/secp384r1"] +secp384r1 = ["ssi-jws/secp384r1", "ssi-cose/secp384r1", "ssi-data-integrity/secp384r1", "ssi-verification-methods/secp384r1"] # Enables `RsaSignature2018` rsa = ["ssi-jws/rsa", "ssi-data-integrity/rsa", "ssi-verification-methods/rsa"] @@ -68,6 +68,7 @@ ssi-claims-core.workspace = true ssi-jws.workspace = true ssi-jwt.workspace = true ssi-sd-jwt.workspace = true +ssi-cose.workspace = true ssi-vc.workspace = true ssi-vc-jose-cose.workspace = true ssi-data-integrity.workspace = true diff --git a/crates/claims/core/src/signature.rs b/crates/claims/core/src/signature.rs index 6285a56e1..bd47fdf36 100644 --- a/crates/claims/core/src/signature.rs +++ b/crates/claims/core/src/signature.rs @@ -59,6 +59,17 @@ impl From for SignatureError { } } +impl From for SignatureError { + fn from(value: ssi_crypto::SignatureError) -> Self { + match value { + ssi_crypto::SignatureError::UnsupportedAlgorithm(a) => { + Self::UnsupportedAlgorithm(a.to_string()) + } + e => Self::other(e), + } + } +} + #[derive(Debug, thiserror::Error)] pub enum MessageSignatureError { #[error("0")] diff --git a/crates/claims/core/src/verification/claims.rs b/crates/claims/core/src/verification/claims.rs index 433c7b6cb..8e288e075 100644 --- a/crates/claims/core/src/verification/claims.rs +++ b/crates/claims/core/src/verification/claims.rs @@ -49,29 +49,15 @@ pub type ClaimsValidity = Result<(), InvalidClaims>; /// The `validate` function is also provided with the proof, as some claim type /// require information from the proof to be validated. pub trait ValidateClaims { - fn validate_claims(&self, environment: &E, proof: &P) -> ClaimsValidity; -} - -impl ValidateClaims for () { - fn validate_claims(&self, _env: &E, _proof: &P) -> ClaimsValidity { + fn validate_claims(&self, _environment: &E, _proof: &P) -> ClaimsValidity { Ok(()) } } -impl ValidateClaims for [u8] { - fn validate_claims(&self, _env: &E, _proof: &P) -> ClaimsValidity { - Ok(()) - } -} +impl ValidateClaims for () {} -impl ValidateClaims for Vec { - fn validate_claims(&self, _env: &E, _proof: &P) -> ClaimsValidity { - Ok(()) - } -} +impl ValidateClaims for [u8] {} -impl<'a, E, P, T: ?Sized + ToOwned + ValidateClaims> ValidateClaims for Cow<'a, T> { - fn validate_claims(&self, _env: &E, _proof: &P) -> ClaimsValidity { - Ok(()) - } -} +impl ValidateClaims for Vec {} + +impl<'a, E, P, T: ?Sized + ToOwned + ValidateClaims> ValidateClaims for Cow<'a, T> {} diff --git a/crates/claims/crates/cose/Cargo.toml b/crates/claims/crates/cose/Cargo.toml new file mode 100644 index 000000000..f01f746d8 --- /dev/null +++ b/crates/claims/crates/cose/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "ssi-cose" +version = "0.1.0" +edition = "2021" +authors = ["Spruce Systems, Inc."] +license = "Apache-2.0" +description = "CBOR Object Signing and Encryption for the `ssi` library." +repository = "https://github.com/spruceid/ssi/" +documentation = "https://docs.rs/ssi-cose/" + +[features] +default = ["secp256r1"] +ed25519 = ["ssi-crypto/ed25519"] +secp256k1 = ["ssi-crypto/secp256k1"] +secp256r1 = ["ssi-crypto/secp256r1"] +secp384r1 = ["ssi-crypto/secp384r1"] + +[dependencies] +ssi-crypto.workspace = true +ssi-claims-core.workspace = true +thiserror.workspace = true +ciborium.workspace = true +coset = { version = "0.3.8", features = ["std"] } +serde = { workspace = true, features = ["derive"] } + +[dev-dependencies] +serde_json.workspace = true +hex.workspace = true +async-std = { workspace = true, features = ["attributes"] } \ No newline at end of file diff --git a/crates/claims/crates/cose/src/algorithm.rs b/crates/claims/crates/cose/src/algorithm.rs new file mode 100644 index 000000000..c0140733d --- /dev/null +++ b/crates/claims/crates/cose/src/algorithm.rs @@ -0,0 +1,86 @@ +use std::borrow::Cow; + +use coset::{ + iana::{self, EnumI64}, + Algorithm, CoseKey, KeyType, +}; +use ssi_crypto::AlgorithmInstance; + +use crate::key::{CoseKeyDecode, EC2_CRV}; + +/// Converts a COSE algorithm into an SSI algorithm instance. +pub fn instantiate_algorithm(algorithm: &Algorithm) -> Option { + match algorithm { + Algorithm::Assigned(iana::Algorithm::PS256) => Some(AlgorithmInstance::PS256), + Algorithm::Assigned(iana::Algorithm::PS384) => Some(AlgorithmInstance::PS384), + Algorithm::Assigned(iana::Algorithm::PS512) => Some(AlgorithmInstance::PS512), + Algorithm::Assigned(iana::Algorithm::EdDSA) => Some(AlgorithmInstance::EdDSA), + Algorithm::Assigned(iana::Algorithm::ES256K) => Some(AlgorithmInstance::ES256K), + Algorithm::Assigned(iana::Algorithm::ES256) => Some(AlgorithmInstance::ES256), + Algorithm::Assigned(iana::Algorithm::ES384) => Some(AlgorithmInstance::ES384), + _ => None, + } +} + +/// Computes a proper display name for the give COSE algorithm. +pub fn algorithm_name(algorithm: &Algorithm) -> String { + match algorithm { + Algorithm::Assigned(iana::Algorithm::PS256) => "PS256".to_owned(), + Algorithm::Assigned(iana::Algorithm::PS384) => "PS384".to_owned(), + Algorithm::Assigned(iana::Algorithm::PS512) => "PS512".to_owned(), + Algorithm::Assigned(iana::Algorithm::EdDSA) => "EdDSA".to_owned(), + Algorithm::Assigned(iana::Algorithm::ES256K) => "ES256K".to_owned(), + Algorithm::Assigned(iana::Algorithm::ES256) => "ES256".to_owned(), + Algorithm::Assigned(iana::Algorithm::ES384) => "ES384".to_owned(), + Algorithm::Assigned(i) => format!("assigned({})", i.to_i64()), + Algorithm::PrivateUse(i) => format!("private_use({i})"), + Algorithm::Text(text) => text.to_owned(), + } +} + +/// Returns the preferred signature algorithm for the give COSE key. +pub fn preferred_algorithm(key: &CoseKey) -> Option> { + key.alg + .as_ref() + .map(Cow::Borrowed) + .or_else(|| match key.kty { + KeyType::Assigned(iana::KeyType::RSA) => { + Some(Cow::Owned(Algorithm::Assigned(iana::Algorithm::PS256))) + } + KeyType::Assigned(iana::KeyType::OKP) => { + let crv = key + .parse_required_param(&EC2_CRV, |v| { + v.as_integer().and_then(|i| i64::try_from(i).ok()) + }) + .ok()?; + + match iana::EllipticCurve::from_i64(crv)? { + iana::EllipticCurve::Ed25519 => { + Some(Cow::Owned(Algorithm::Assigned(iana::Algorithm::EdDSA))) + } + _ => None, + } + } + KeyType::Assigned(iana::KeyType::EC2) => { + let crv = key + .parse_required_param(&EC2_CRV, |v| { + v.as_integer().and_then(|i| i64::try_from(i).ok()) + }) + .ok()?; + + match iana::EllipticCurve::from_i64(crv)? { + iana::EllipticCurve::Secp256k1 => { + Some(Cow::Owned(Algorithm::Assigned(iana::Algorithm::ES256K))) + } + iana::EllipticCurve::P_256 => { + Some(Cow::Owned(Algorithm::Assigned(iana::Algorithm::ES256))) + } + iana::EllipticCurve::P_384 => { + Some(Cow::Owned(Algorithm::Assigned(iana::Algorithm::ES384))) + } + _ => None, + } + } + _ => None, + }) +} diff --git a/crates/claims/crates/cose/src/key.rs b/crates/claims/crates/cose/src/key.rs new file mode 100644 index 000000000..c2c879408 --- /dev/null +++ b/crates/claims/crates/cose/src/key.rs @@ -0,0 +1,732 @@ +use coset::{ + iana::{self, EnumI64}, + CoseKey, KeyType, Label, +}; +use ssi_claims_core::{ProofValidationError, SignatureError}; +use ssi_crypto::{ + rand::{CryptoRng, RngCore}, + PublicKey, SecretKey, +}; +use std::borrow::Cow; + +use crate::{ + algorithm::{algorithm_name, instantiate_algorithm, preferred_algorithm}, + CoseSigner, CoseSignerInfo, +}; + +/// COSE key resolver. +pub trait CoseKeyResolver { + /// Fetches the COSE key associated to the give identifier. + #[allow(async_fn_in_trait)] + async fn fetch_public_cose_key( + &self, + id: Option<&[u8]>, + ) -> Result, ProofValidationError>; +} + +impl<'a, T: CoseKeyResolver> CoseKeyResolver for &'a T { + async fn fetch_public_cose_key( + &self, + id: Option<&[u8]>, + ) -> Result, ProofValidationError> { + T::fetch_public_cose_key(*self, id).await + } +} + +impl CoseKeyResolver for CoseKey { + async fn fetch_public_cose_key( + &self, + _id: Option<&[u8]>, + ) -> Result, ProofValidationError> { + Ok(Cow::Borrowed(self)) + } +} + +impl CoseSigner for CoseKey { + async fn fetch_info(&self) -> Result { + Ok(CoseSignerInfo { + algorithm: preferred_algorithm(self).map(Cow::into_owned), + key_id: self.key_id.clone(), + }) + } + + async fn sign_bytes(&self, signing_bytes: &[u8]) -> Result, SignatureError> { + let algorithm = preferred_algorithm(self).ok_or(SignatureError::MissingAlgorithm)?; + let secret_key = self.decode_secret()?; + secret_key + .sign( + instantiate_algorithm(&algorithm).ok_or_else(|| { + SignatureError::UnsupportedAlgorithm(algorithm_name(&algorithm)) + })?, + signing_bytes, + ) + .map_err(Into::into) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum KeyDecodingError { + #[error("unsupported key type")] + UnsupportedKeyType(KeyType), + + #[error("missing parameter")] + MissingParam(Label), + + #[error("invalid parameter")] + InvalidParam(Label), + + #[error("unsupported parameter value")] + UnsupportedParam(Label, ciborium::Value), + + #[error("invalid key")] + InvalidKey, +} + +impl From for KeyDecodingError { + fn from(_value: ssi_crypto::key::InvalidPublicKey) -> Self { + Self::InvalidKey + } +} + +impl From for KeyDecodingError { + fn from(_value: ssi_crypto::key::InvalidSecretKey) -> Self { + Self::InvalidKey + } +} + +impl From for ssi_claims_core::SignatureError { + fn from(_value: KeyDecodingError) -> Self { + Self::InvalidSecretKey + } +} + +/// Decode COSE keys. +pub trait CoseKeyDecode { + /// Reads a key parameter, if it exists. + fn fetch_param(&self, label: &Label) -> Option<&ciborium::Value>; + + /// Requires the given key parameter. + /// + /// Returns an error if the key parameter is not present in the key. + fn require_param(&self, label: &Label) -> Result<&ciborium::Value, KeyDecodingError> { + self.fetch_param(label) + .ok_or_else(|| KeyDecodingError::MissingParam(label.clone())) + } + + /// Requires and parses the given key parameter. + /// + /// Returns an error if the key parameter is not present in the key, or + /// if the parsing function `f` returns `None`. + fn parse_required_param<'a, T>( + &'a self, + label: &Label, + f: impl FnOnce(&'a ciborium::Value) -> Option, + ) -> Result { + f(self.require_param(label)?).ok_or_else(|| KeyDecodingError::InvalidParam(label.clone())) + } + + /// Decodes the COSE key as a public key. + fn decode_public(&self) -> Result; + + /// Decodes the COSE key as a secret key. + fn decode_secret(&self) -> Result; +} + +impl CoseKeyDecode for CoseKey { + /// Fetch a key parameter. + fn fetch_param(&self, label: &Label) -> Option<&ciborium::Value> { + self.params + .iter() + .find_map(|(l, value)| if l == label { Some(value) } else { None }) + } + + fn decode_public(&self) -> Result { + match &self.kty { + t @ KeyType::Assigned(kty) => { + match kty { + // Octet Key Pair. + iana::KeyType::OKP => { + let crv = self.parse_required_param(&OKP_CRV, |v| { + v.as_integer().and_then(|i| i64::try_from(i).ok()) + })?; + + #[allow(unused_variables)] + let x = self.parse_required_param(&OKP_X, ciborium::Value::as_bytes)?; + + match iana::EllipticCurve::from_i64(crv) { + #[cfg(feature = "ed25519")] + Some(iana::EllipticCurve::Ed25519) => { + ssi_crypto::PublicKey::new_ed25519(x).map_err(Into::into) + } + _ => Err(KeyDecodingError::UnsupportedParam(EC2_CRV, crv.into())), + } + } + // Double Coordinate Curves. + // See: + iana::KeyType::EC2 => { + let crv = self.parse_required_param(&EC2_CRV, |v| { + v.as_integer().and_then(|i| i64::try_from(i).ok()) + })?; + + #[allow(unused_variables)] + let x = self.parse_required_param(&EC2_X, ciborium::Value::as_bytes)?; + + #[allow(unused_variables)] + let y = self.parse_required_param( + &EC2_Y, + ciborium::Value::as_bytes, // TODO: this can be a `bool` + )?; + + match iana::EllipticCurve::from_i64(crv) { + #[cfg(feature = "secp256k1")] + Some(iana::EllipticCurve::Secp256k1) => { + ssi_crypto::PublicKey::new_secp256k1(x, y).map_err(Into::into) + } + #[cfg(feature = "secp256r1")] + Some(iana::EllipticCurve::P_256) => { + ssi_crypto::PublicKey::new_p256(x, y).map_err(Into::into) + } + #[cfg(feature = "secp384r1")] + Some(iana::EllipticCurve::P_384) => { + ssi_crypto::PublicKey::new_p384(x, y).map_err(Into::into) + } + _ => Err(KeyDecodingError::UnsupportedParam(EC2_CRV, crv.into())), + } + } + _ => Err(KeyDecodingError::UnsupportedKeyType(t.clone())), + } + } + other => Err(KeyDecodingError::UnsupportedKeyType(other.clone())), + } + } + + fn decode_secret(&self) -> Result { + match &self.kty { + t @ KeyType::Assigned(kty) => { + match kty { + // Octet Key Pair. + iana::KeyType::OKP => { + let crv = self.parse_required_param(&OKP_CRV, |v| { + v.as_integer().and_then(|i| i64::try_from(i).ok()) + })?; + + #[allow(unused_variables)] + let d = self.parse_required_param(&OKP_D, ciborium::Value::as_bytes)?; + + match iana::EllipticCurve::from_i64(crv) { + #[cfg(feature = "ed25519")] + Some(iana::EllipticCurve::Ed25519) => { + ssi_crypto::SecretKey::new_ed25519(d).map_err(Into::into) + } + _ => Err(KeyDecodingError::UnsupportedParam(EC2_CRV, crv.into())), + } + } + // Double Coordinate Curves. + // See: + iana::KeyType::EC2 => { + let crv = self.parse_required_param(&EC2_CRV, |v| { + v.as_integer().and_then(|i| i64::try_from(i).ok()) + })?; + + #[allow(unused_variables)] + let d = self.parse_required_param(&EC2_D, ciborium::Value::as_bytes)?; + + match iana::EllipticCurve::from_i64(crv) { + #[cfg(feature = "secp256k1")] + Some(iana::EllipticCurve::Secp256k1) => { + ssi_crypto::SecretKey::new_secp256k1(d).map_err(Into::into) + } + #[cfg(feature = "secp256r1")] + Some(iana::EllipticCurve::P_256) => { + ssi_crypto::SecretKey::new_p256(d).map_err(Into::into) + } + #[cfg(feature = "secp384r1")] + Some(iana::EllipticCurve::P_384) => { + ssi_crypto::SecretKey::new_p384(d).map_err(Into::into) + } + _ => Err(KeyDecodingError::UnsupportedParam(EC2_CRV, crv.into())), + } + } + _ => Err(KeyDecodingError::UnsupportedKeyType(t.clone())), + } + } + other => Err(KeyDecodingError::UnsupportedKeyType(other.clone())), + } + } +} + +pub const OKP_CRV: Label = Label::Int(iana::OkpKeyParameter::Crv as i64); +pub const OKP_X: Label = Label::Int(iana::OkpKeyParameter::X as i64); +pub const OKP_D: Label = Label::Int(iana::OkpKeyParameter::D as i64); + +pub const EC2_CRV: Label = Label::Int(iana::Ec2KeyParameter::Crv as i64); +pub const EC2_X: Label = Label::Int(iana::Ec2KeyParameter::X as i64); +pub const EC2_Y: Label = Label::Int(iana::Ec2KeyParameter::Y as i64); +pub const EC2_D: Label = Label::Int(iana::Ec2KeyParameter::D as i64); + +#[derive(Debug, thiserror::Error)] +pub enum KeyEncodingError { + #[error("unsupported key type")] + UnsupportedKeyType, +} + +/// COSE key encoding +pub trait CoseKeyEncode: Sized { + fn encode_public(key: &PublicKey) -> Result; + + fn encode_public_with_id(key: &PublicKey, id: Vec) -> Result { + let mut cose_key = Self::encode_public(key)?; + cose_key.key_id = id; + Ok(cose_key) + } + + fn encode_secret(key: &SecretKey) -> Result; + + fn encode_secret_with_id(key: &SecretKey, id: Vec) -> Result { + let mut cose_key = Self::encode_secret(key)?; + cose_key.key_id = id; + Ok(cose_key) + } +} + +impl CoseKeyEncode for CoseKey { + fn encode_public(key: &PublicKey) -> Result { + match key { + #[cfg(feature = "ed25519")] + PublicKey::Ed25519(key) => Ok(Self { + kty: KeyType::Assigned(iana::KeyType::OKP), + params: vec![ + (OKP_CRV, iana::EllipticCurve::Ed25519.to_i64().into()), + (OKP_X, key.as_bytes().to_vec().into()), + ], + ..Default::default() + }), + #[cfg(feature = "secp256k1")] + PublicKey::Secp256k1(key) => { + use ssi_crypto::k256::elliptic_curve::sec1::ToEncodedPoint; + let encoded_point = key.to_encoded_point(false); + Ok(Self { + kty: KeyType::Assigned(iana::KeyType::EC2), + params: vec![ + (EC2_CRV, iana::EllipticCurve::Secp256k1.to_i64().into()), + (EC2_X, encoded_point.x().unwrap().to_vec().into()), + (EC2_Y, encoded_point.y().unwrap().to_vec().into()), + ], + ..Default::default() + }) + } + #[cfg(feature = "secp256r1")] + PublicKey::P256(key) => { + use ssi_crypto::p256::elliptic_curve::sec1::ToEncodedPoint; + let encoded_point = key.to_encoded_point(false); + Ok(Self { + kty: KeyType::Assigned(iana::KeyType::EC2), + params: vec![ + (EC2_CRV, iana::EllipticCurve::P_256.to_i64().into()), + (EC2_X, encoded_point.x().unwrap().to_vec().into()), + (EC2_Y, encoded_point.y().unwrap().to_vec().into()), + ], + ..Default::default() + }) + } + #[cfg(feature = "secp384r1")] + PublicKey::P384(key) => { + use ssi_crypto::p384::elliptic_curve::sec1::ToEncodedPoint; + let encoded_point = key.to_encoded_point(false); + Ok(Self { + kty: KeyType::Assigned(iana::KeyType::EC2), + params: vec![ + (EC2_CRV, iana::EllipticCurve::P_384.to_i64().into()), + (EC2_X, encoded_point.x().unwrap().to_vec().into()), + (EC2_Y, encoded_point.y().unwrap().to_vec().into()), + ], + ..Default::default() + }) + } + _ => Err(KeyEncodingError::UnsupportedKeyType), + } + } + + fn encode_secret(key: &SecretKey) -> Result { + match key { + #[cfg(feature = "ed25519")] + SecretKey::Ed25519(key) => { + let public_key = key.verifying_key(); + Ok(Self { + kty: KeyType::Assigned(iana::KeyType::OKP), + params: vec![ + (OKP_CRV, iana::EllipticCurve::Ed25519.to_i64().into()), + (OKP_X, public_key.as_bytes().to_vec().into()), + (OKP_D, key.to_bytes().to_vec().into()), + ], + ..Default::default() + }) + } + #[cfg(feature = "secp256k1")] + SecretKey::Secp256k1(key) => { + use ssi_crypto::k256::elliptic_curve::sec1::ToEncodedPoint; + let public_key = key.public_key(); + let encoded_point = public_key.to_encoded_point(false); + Ok(Self { + kty: KeyType::Assigned(iana::KeyType::EC2), + params: vec![ + (EC2_CRV, iana::EllipticCurve::Secp256k1.to_i64().into()), + (EC2_X, encoded_point.x().unwrap().to_vec().into()), + (EC2_Y, encoded_point.y().unwrap().to_vec().into()), + (EC2_D, key.to_bytes().to_vec().into()), + ], + ..Default::default() + }) + } + #[cfg(feature = "secp256r1")] + SecretKey::P256(key) => { + use ssi_crypto::p256::elliptic_curve::sec1::ToEncodedPoint; + let public_key = key.public_key(); + let encoded_point = public_key.to_encoded_point(false); + Ok(Self { + kty: KeyType::Assigned(iana::KeyType::EC2), + params: vec![ + (EC2_CRV, iana::EllipticCurve::P_256.to_i64().into()), + (EC2_X, encoded_point.x().unwrap().to_vec().into()), + (EC2_Y, encoded_point.y().unwrap().to_vec().into()), + (EC2_D, key.to_bytes().to_vec().into()), + ], + ..Default::default() + }) + } + #[cfg(feature = "secp384r1")] + SecretKey::P384(key) => { + use ssi_crypto::p384::elliptic_curve::sec1::ToEncodedPoint; + let public_key = key.public_key(); + let encoded_point = public_key.to_encoded_point(false); + Ok(Self { + kty: KeyType::Assigned(iana::KeyType::EC2), + params: vec![ + (EC2_CRV, iana::EllipticCurve::P_384.to_i64().into()), + (EC2_X, encoded_point.x().unwrap().to_vec().into()), + (EC2_Y, encoded_point.y().unwrap().to_vec().into()), + (EC2_D, key.to_bytes().to_vec().into()), + ], + ..Default::default() + }) + } + _ => Err(KeyEncodingError::UnsupportedKeyType), + } + } +} + +pub trait CoseKeyGenerate { + #[cfg(feature = "ed25519")] + fn generate_ed25519() -> Self; + + #[cfg(feature = "ed25519")] + fn generate_ed25519_from(rng: &mut (impl RngCore + CryptoRng)) -> Self; + + #[cfg(feature = "secp256k1")] + fn generate_secp256k1() -> Self; + + #[cfg(feature = "secp256k1")] + fn generate_secp256k1_from(rng: &mut (impl RngCore + CryptoRng)) -> Self; + + #[cfg(feature = "secp256r1")] + fn generate_p256() -> Self; + + #[cfg(feature = "secp256r1")] + fn generate_p256_from(rng: &mut (impl RngCore + CryptoRng)) -> Self; + + #[cfg(feature = "secp384r1")] + fn generate_p384() -> Self; + + #[cfg(feature = "secp384r1")] + fn generate_p384_from(rng: &mut (impl RngCore + CryptoRng)) -> Self; +} + +impl CoseKeyGenerate for CoseKey { + #[cfg(feature = "ed25519")] + fn generate_ed25519() -> Self { + Self::encode_secret(&ssi_crypto::SecretKey::generate_ed25519()).unwrap() + } + + #[cfg(feature = "ed25519")] + fn generate_ed25519_from(rng: &mut (impl RngCore + CryptoRng)) -> Self { + Self::encode_secret(&ssi_crypto::SecretKey::generate_ed25519_from(rng)).unwrap() + } + + #[cfg(feature = "secp256k1")] + fn generate_secp256k1() -> Self { + Self::encode_secret(&ssi_crypto::SecretKey::generate_secp256k1()).unwrap() + } + + #[cfg(feature = "secp256k1")] + fn generate_secp256k1_from(rng: &mut (impl RngCore + CryptoRng)) -> Self { + Self::encode_secret(&ssi_crypto::SecretKey::generate_secp256k1_from(rng)).unwrap() + } + + #[cfg(feature = "secp256r1")] + fn generate_p256() -> Self { + Self::encode_secret(&ssi_crypto::SecretKey::generate_p256()).unwrap() + } + + #[cfg(feature = "secp256r1")] + fn generate_p256_from(rng: &mut (impl RngCore + CryptoRng)) -> Self { + Self::encode_secret(&ssi_crypto::SecretKey::generate_p256_from(rng)).unwrap() + } + + #[cfg(feature = "secp384r1")] + fn generate_p384() -> Self { + Self::encode_secret(&ssi_crypto::SecretKey::generate_p384()).unwrap() + } + + #[cfg(feature = "secp384r1")] + fn generate_p384_from(rng: &mut (impl RngCore + CryptoRng)) -> Self { + Self::encode_secret(&ssi_crypto::SecretKey::generate_p384_from(rng)).unwrap() + } +} + +#[cfg(test)] +mod tests { + use super::{CoseKeyDecode, CoseKeyEncode}; + use coset::{CborSerializable, CoseKey}; + use ssi_crypto::{PublicKey, SecretKey}; + + /// Public secp256k1 key. + /// + /// ```cbor-diagnostic + /// { + /// 1: 1, + /// -1: 6, + /// -2: h'8816d41001dd1a9ddea1232381b2eede803161e88ebb19eaf573d393dec800a7' + /// } + /// ``` + #[cfg(feature = "ed25519")] + #[test] + fn public_ed25519_1() { + let input = hex::decode( + "a3010120062158208816d41001dd1a9ddea1232381b2eede803161e88ebb19eaf573d393dec800a7", + ) + .unwrap(); + let cose_key = CoseKey::from_slice(&input).unwrap(); + let key = cose_key.decode_public().unwrap(); + assert!(matches!(key, PublicKey::Ed25519(_))); + assert_eq!( + CoseKey::encode_public_with_id(&key, cose_key.key_id.clone()).unwrap(), + cose_key + ) + } + + /// Secret secp256k1 key. + /// + /// ```cbor-diagnostic + /// { + /// 1: 1, + /// -1: 6, + /// -2: h'8816d41001dd1a9ddea1232381b2eede803161e88ebb19eaf573d393dec800a7', + /// -4: h'e25df1249ab766fc5a8c9f98d5e311cd4f7d5fd1c6b6a2032adc973056c87dc3' + /// } + /// ``` + #[cfg(feature = "ed25519")] + #[test] + fn secret_ed25519_1() { + let input = hex::decode("a4010120062158208816d41001dd1a9ddea1232381b2eede803161e88ebb19eaf573d393dec800a7235820e25df1249ab766fc5a8c9f98d5e311cd4f7d5fd1c6b6a2032adc973056c87dc3").unwrap(); + let cose_key = CoseKey::from_slice(&input).unwrap(); + let key = cose_key.decode_secret().unwrap(); + assert!(matches!(key, SecretKey::Ed25519(_))); + assert_eq!( + CoseKey::encode_secret_with_id(&key, cose_key.key_id.clone()).unwrap(), + cose_key + ) + } + + /// Secret secp256k1 key. + /// + /// ```cbor-diagnostic + /// { + /// 1: 2, + /// -1: 8, + /// -2: h'394fd5a1e33b8a67d5fa9ddca42d261219dde202e65bbf07bf2f671e157ac41f', + /// -3: h'199d7db667e74905c8371168b815c267db76243fbfd387fa5f2d8a691099a89a' + /// } + /// ``` + #[cfg(feature = "secp256k1")] + #[test] + fn public_secp256k1_1() { + let input = hex::decode("a401022008215820394fd5a1e33b8a67d5fa9ddca42d261219dde202e65bbf07bf2f671e157ac41f225820199d7db667e74905c8371168b815c267db76243fbfd387fa5f2d8a691099a89a").unwrap(); + let cose_key = CoseKey::from_slice(&input).unwrap(); + let key = cose_key.decode_public().unwrap(); + assert!(matches!(key, PublicKey::Secp256k1(_))); + assert_eq!( + CoseKey::encode_public_with_id(&key, cose_key.key_id.clone()).unwrap(), + cose_key + ) + } + + /// Secret secp256k1 key. + /// + /// ```cbor-diagnostic + /// { + /// 1: 2, + /// -1: 8, + /// -2: h'394fd5a1e33b8a67d5fa9ddca42d261219dde202e65bbf07bf2f671e157ac41f', + /// -3: h'199d7db667e74905c8371168b815c267db76243fbfd387fa5f2d8a691099a89a', + /// -4: h'3e0fada8be75e5e47ab4c1c91c3f8f9185d1e18a2a16b3400a1eb33c9cdf8b96' + /// } + /// ``` + #[cfg(feature = "secp256k1")] + #[test] + fn secret_secp256k1_1() { + let input = hex::decode("a501022008215820394fd5a1e33b8a67d5fa9ddca42d261219dde202e65bbf07bf2f671e157ac41f225820199d7db667e74905c8371168b815c267db76243fbfd387fa5f2d8a691099a89a2358203e0fada8be75e5e47ab4c1c91c3f8f9185d1e18a2a16b3400a1eb33c9cdf8b96").unwrap(); + let cose_key = CoseKey::from_slice(&input).unwrap(); + let key = cose_key.decode_secret().unwrap(); + assert!(matches!(key, SecretKey::Secp256k1(_))); + assert_eq!( + CoseKey::encode_secret_with_id(&key, cose_key.key_id.clone()).unwrap(), + cose_key + ) + } + + /// A public EC (P-256) key with a `kid` of + /// "meriadoc.brandybuck@buckland.example". + /// + /// See: + #[cfg(feature = "secp256r1")] + #[test] + fn public_p256_1() { + let input = hex::decode("a5200121582065eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d2258201e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c01020258246d65726961646f632e6272616e64796275636b406275636b6c616e642e6578616d706c65").unwrap(); + let cose_key = CoseKey::from_slice(&input).unwrap(); + let key = cose_key.decode_public().unwrap(); + assert!(matches!(key, PublicKey::P256(_))); + assert_eq!( + CoseKey::encode_public_with_id(&key, cose_key.key_id.clone()).unwrap(), + cose_key + ) + } + + /// A secret EC (P-256) key with a kid of + /// "meriadoc.brandybuck@buckland.example". + /// + /// See: + #[cfg(feature = "secp256r1")] + #[test] + fn secret_p256_1() { + let input = hex::decode("a601020258246d65726961646f632e6272616e64796275636b406275636b6c616e642e6578616d706c65200121582065eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d2258201e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c235820aff907c99f9ad3aae6c4cdf21122bce2bd68b5283e6907154ad911840fa208cf").unwrap(); + let cose_key = CoseKey::from_slice(&input).unwrap(); + let key = cose_key.decode_secret().unwrap(); + assert!(matches!(key, SecretKey::P256(_))); + assert_eq!( + CoseKey::encode_secret_with_id(&key, cose_key.key_id.clone()).unwrap(), + cose_key + ) + } + + /// A public EC (P-256) key with a kid of "11". + /// + /// See: + #[cfg(feature = "secp256r1")] + #[test] + fn public_p256_2() { + let input = hex::decode("a52001215820bac5b11cad8f99f9c72b05cf4b9e26d244dc189f745228255a219a86d6a09eff22582020138bf82dc1b6d562be0fa54ab7804a3a64b6d72ccfed6b6fb6ed28bbfc117e010202423131").unwrap(); + let cose_key = CoseKey::from_slice(&input).unwrap(); + let key = cose_key.decode_public().unwrap(); + assert!(matches!(key, PublicKey::P256(_))); + assert_eq!( + CoseKey::encode_public_with_id(&key, cose_key.key_id.clone()).unwrap(), + cose_key + ) + } + + /// A secret EC (P-256) key with a kid of "11". + /// + /// See: + #[cfg(feature = "secp256r1")] + #[test] + fn secret_p256_2() { + let input = hex::decode("a60102024231312001215820bac5b11cad8f99f9c72b05cf4b9e26d244dc189f745228255a219a86d6a09eff22582020138bf82dc1b6d562be0fa54ab7804a3a64b6d72ccfed6b6fb6ed28bbfc117e23582057c92077664146e876760c9520d054aa93c3afb04e306705db6090308507b4d3").unwrap(); + let cose_key = CoseKey::from_slice(&input).unwrap(); + let key = cose_key.decode_secret().unwrap(); + assert!(matches!(key, SecretKey::P256(_))); + assert_eq!( + CoseKey::encode_secret_with_id(&key, cose_key.key_id.clone()).unwrap(), + cose_key + ) + } + + /// A public EC (P-256) key with a kid of "peregrin.took@tuckborough.example". + /// + /// See: + #[cfg(feature = "secp256r1")] + #[test] + fn public_p256_3() { + let input = hex::decode("a5200121582098f50a4ff6c05861c8860d13a638ea56c3f5ad7590bbfbf054e1c7b4d91d6280225820f01400b089867804b8e9fc96c3932161f1934f4223069170d924b7e03bf822bb0102025821706572656772696e2e746f6f6b407475636b626f726f7567682e6578616d706c65").unwrap(); + let cose_key = CoseKey::from_slice(&input).unwrap(); + let key = cose_key.decode_public().unwrap(); + assert!(matches!(key, PublicKey::P256(_))); + assert_eq!( + CoseKey::encode_public_with_id(&key, cose_key.key_id.clone()).unwrap(), + cose_key + ) + } + + /// A secret EC (P-256) key with a kid of + /// "peregrin.took@tuckborough.example". + /// + /// See: + #[cfg(feature = "secp256r1")] + #[test] + fn secret_p256_3() { + let input = hex::decode("a601022001025821706572656772696e2e746f6f6b407475636b626f726f7567682e6578616d706c6521582098f50a4ff6c05861c8860d13a638ea56c3f5ad7590bbfbf054e1c7b4d91d6280225820f01400b089867804b8e9fc96c3932161f1934f4223069170d924b7e03bf822bb23582002d1f7e6f26c43d4868d87ceb2353161740aacf1f7163647984b522a848df1c3").unwrap(); + let cose_key = CoseKey::from_slice(&input).unwrap(); + let key = cose_key.decode_secret().unwrap(); + assert!(matches!(key, SecretKey::P256(_))); + assert_eq!( + CoseKey::encode_secret_with_id(&key, cose_key.key_id.clone()).unwrap(), + cose_key + ) + } + + /// A public EC (P-384) key. + /// + /// ```cbor-diagnostic + /// { + /// 1: 2, + /// -1: 2, + /// -2: h'fa1d31d39853d37fbfd145675635d52795f5feb3eacf11371ad8c6eb30c6f2493b0ec74d8c5b5a20ebf68ce3e0bd2c07', + /// -3: h'7c2b27b366e4fc73b79d28bac0b18ae2f2b0c4e7849656a71aac8987e60af5af57a9af3faf206afc798fa5fb06db15aa' + /// } + /// ``` + #[cfg(feature = "secp384r1")] + #[test] + fn public_p384_1() { + let input = hex::decode("a401022002215830fa1d31d39853d37fbfd145675635d52795f5feb3eacf11371ad8c6eb30c6f2493b0ec74d8c5b5a20ebf68ce3e0bd2c072258307c2b27b366e4fc73b79d28bac0b18ae2f2b0c4e7849656a71aac8987e60af5af57a9af3faf206afc798fa5fb06db15aa").unwrap(); + let cose_key = CoseKey::from_slice(&input).unwrap(); + let key = cose_key.decode_public().unwrap(); + assert!(matches!(key, PublicKey::P384(_))); + assert_eq!( + CoseKey::encode_public_with_id(&key, cose_key.key_id.clone()).unwrap(), + cose_key + ) + } + + /// A secret EC (P-384) key. + /// + /// ```cbor-diagnostic + /// { + /// 1: 2, + /// -1: 2, + /// -2: h'fa1d31d39853d37fbfd145675635d52795f5feb3eacf11371ad8c6eb30c6f2493b0ec74d8c5b5a20ebf68ce3e0bd2c07', + /// -3: h'7c2b27b366e4fc73b79d28bac0b18ae2f2b0c4e7849656a71aac8987e60af5af57a9af3faf206afc798fa5fb06db15aa', + /// -4: h'21d8eb2250cdaa19bfb01f03211be11a70ef4739650ed954166531808aa254c1d6d968b36d16184d350600253fa672c0' + /// } + /// ``` + #[cfg(feature = "secp384r1")] + #[test] + fn secret_p384_1() { + let input = hex::decode("a501022002215830fa1d31d39853d37fbfd145675635d52795f5feb3eacf11371ad8c6eb30c6f2493b0ec74d8c5b5a20ebf68ce3e0bd2c072258307c2b27b366e4fc73b79d28bac0b18ae2f2b0c4e7849656a71aac8987e60af5af57a9af3faf206afc798fa5fb06db15aa23583021d8eb2250cdaa19bfb01f03211be11a70ef4739650ed954166531808aa254c1d6d968b36d16184d350600253fa672c0").unwrap(); + let cose_key = CoseKey::from_slice(&input).unwrap(); + let key = cose_key.decode_secret().unwrap(); + assert!(matches!(key, SecretKey::P384(_))); + assert_eq!( + CoseKey::encode_secret_with_id(&key, cose_key.key_id.clone()).unwrap(), + cose_key + ) + } +} diff --git a/crates/claims/crates/cose/src/lib.rs b/crates/claims/crates/cose/src/lib.rs new file mode 100644 index 000000000..abb81c15c --- /dev/null +++ b/crates/claims/crates/cose/src/lib.rs @@ -0,0 +1,189 @@ +//! CBOR Object Signing and Encryption ([COSE]) implementation based on +//! [`coset`]. +//! +//! [COSE]: +//! [`coset`]: +//! +//! # Usage +//! +//! ``` +//! # #[async_std::main] +//! # async fn main() { +//! # #[cfg(feature = "secp256r1")] { +//! use std::borrow::Cow; +//! use serde::{Serialize, Deserialize}; +//! use ssi_claims_core::{VerifiableClaims, ValidateClaims, VerificationParameters}; +//! use ssi_cose::{CosePayload, ValidateCoseHeader, CoseSignatureBytes, DecodedCoseSign1, CoseKey, key::CoseKeyGenerate}; +//! +//! // Our custom payload type. +//! #[derive(Serialize, Deserialize)] +//! struct CustomPayload { +//! data: String +//! } +//! +//! // Define how the payload is encoded in COSE. +//! impl CosePayload for CustomPayload { +//! // Serialize the payload as JSON. +//! fn payload_bytes(&self) -> Cow<[u8]> { +//! Cow::Owned(serde_json::to_vec(self).unwrap()) +//! } +//! } +//! +//! // Define how to validate the COSE header (always valid by default). +//! impl

ValidateCoseHeader

for CustomPayload {} +//! +//! // Define how to validate the payload (always valid by default). +//! impl

ValidateClaims for CustomPayload {} +//! +//! // Create a payload. +//! let payload = CustomPayload { +//! data: "Some Data".to_owned() +//! }; +//! +//! // Create a signature key. +//! let key = CoseKey::generate_p256(); // requires the `secp256r1` feature. +//! +//! // Sign the payload! +//! let bytes = payload.sign( +//! &key, +//! true // should the `COSE_Sign1` object be tagged or not. +//! ).await.unwrap(); +//! +//! // Decode the signed COSE object. +//! let decoded: DecodedCoseSign1 = bytes +//! .decode(true) +//! .unwrap() +//! .try_map(|_, bytes| serde_json::from_slice(bytes)) +//! .unwrap(); +//! +//! assert_eq!(decoded.signing_bytes.payload.data, "Some Data"); +//! +//! // Verify the signature. +//! let params = VerificationParameters::from_resolver(&key); +//! decoded.verify(¶ms).await.unwrap(); +//! # } } +//! ``` +use ssi_claims_core::SignatureError; +use std::borrow::Cow; + +pub use coset; +pub use coset::{ContentType, CoseError, CoseKey, CoseSign1, Header, Label, ProtectedHeader}; + +pub use ciborium; +pub use ciborium::Value as CborValue; + +pub mod key; + +mod signature; +pub use signature::*; + +mod verification; +pub use verification::*; + +pub mod algorithm; + +mod sign1; +pub use sign1::*; + +/// COSE payload. +/// +/// This trait defines how a custom type can be encoded and signed using COSE. +/// +/// # Example +/// +/// ``` +/// use std::borrow::Cow; +/// use serde::{Serialize, Deserialize}; +/// use ssi_cose::{CosePayload, CosePayloadType, ContentType}; +/// +/// // Our custom payload type. +/// #[derive(Serialize, Deserialize)] +/// struct CustomPayload { +/// data: String +/// } +/// +/// // Define how the payload is encoded in COSE. +/// impl CosePayload for CustomPayload { +/// fn typ(&self) -> Option { +/// Some(CosePayloadType::Text( +/// "application/json+cose".to_owned(), +/// )) +/// } +/// +/// fn content_type(&self) -> Option { +/// Some(ContentType::Text("application/json".to_owned())) +/// } +/// +/// // Serialize the payload as JSON. +/// fn payload_bytes(&self) -> Cow<[u8]> { +/// Cow::Owned(serde_json::to_vec(self).unwrap()) +/// } +/// } +/// ``` +pub trait CosePayload { + /// `typ` header parameter. + /// + /// See: + fn typ(&self) -> Option { + None + } + + /// Content type header parameter. + fn content_type(&self) -> Option { + None + } + + /// Payload bytes. + /// + /// Returns the payload bytes representing this value. + fn payload_bytes(&self) -> Cow<[u8]>; + + /// Sign the payload to produce a serialized `COSE_Sign1` object. + /// + /// The `tagged` flag specifies if the COSE object should be tagged or + /// not. + #[allow(async_fn_in_trait)] + async fn sign( + &self, + signer: impl CoseSigner, + tagged: bool, + ) -> Result { + signer.sign(self, None, tagged).await + } +} + +impl CosePayload for [u8] { + fn payload_bytes(&self) -> Cow<[u8]> { + Cow::Borrowed(self) + } +} + +pub const TYP_LABEL: Label = Label::Int(16); + +/// COSE payload type. +/// +/// Value of the `typ` header parameter. +/// +/// See: +pub enum CosePayloadType { + UInt(u64), + Text(String), +} + +impl From for CborValue { + fn from(ty: CosePayloadType) -> Self { + match ty { + CosePayloadType::UInt(i) => Self::Integer(i.into()), + CosePayloadType::Text(t) => Self::Text(t), + } + } +} + +/// COSE signature bytes. +pub struct CoseSignatureBytes(pub Vec); + +impl CoseSignatureBytes { + pub fn into_bytes(self) -> Vec { + self.0 + } +} diff --git a/crates/claims/crates/cose/src/sign1.rs b/crates/claims/crates/cose/src/sign1.rs new file mode 100644 index 000000000..ee7a802b7 --- /dev/null +++ b/crates/claims/crates/cose/src/sign1.rs @@ -0,0 +1,331 @@ +use crate::{CborValue, CosePayload, CoseSignatureBytes}; +use coset::{ + sig_structure_data, CborSerializable, CoseError, CoseSign1, Header, ProtectedHeader, + TaggedCborSerializable, +}; +use serde::{Deserialize, Serialize}; +use std::{borrow::Borrow, ops::Deref}; + +/// CBOR-encoded `COSE_Sign1` object. +/// +/// This represents the raw CBOR bytes encoding a [`CoseSign1`] object. The +/// [`Self::decode`] method can be used to decode into a [`DecodedCoseSign1`] +/// (similar to `CoseSign1` but with extra information about the payload). +/// +/// This is the borrowed equivalent of [`CoseSign1BytesBuf`]. +#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +#[serde(transparent)] +pub struct CoseSign1Bytes([u8]); + +impl CoseSign1Bytes { + /// Creates a new CBOR-encoded `COSE_Sign1` object from a byte slice. + /// + /// The bytes are not actually checked. If the bytes are not describing + /// a CBOR-encoded `COSE_Sign1` object it will be detected when the + /// [`Self::decode`] method is called. + pub fn new(bytes: &[u8]) -> &Self { + unsafe { std::mem::transmute(bytes) } + } + + /// Decodes the CBOR bytes into a [`DecodedCoseSign1`]. + pub fn decode(&self, tagged: bool) -> Result { + let cose = if tagged { + CoseSign1::from_tagged_slice(&self.0)? + } else { + CoseSign1::from_slice(&self.0)? + }; + + Ok(cose.into()) + } + + /// Returns the raw CBOR bytes. + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +impl AsRef<[u8]> for CoseSign1Bytes { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl ToOwned for CoseSign1Bytes { + type Owned = CoseSign1BytesBuf; + + fn to_owned(&self) -> Self::Owned { + CoseSign1BytesBuf(self.0.to_owned()) + } +} + +/// CBOR-encoded `COSE_Sign1` object buffer. +/// +/// This represents the raw CBOR bytes encoding a [`CoseSign1`] object. The +/// [`CoseSign1Bytes::decode`] method can be used to decode into a +/// [`DecodedCoseSign1`] (similar to `CoseSign1` but with extra information +/// about the payload). +/// +/// This is the owned equivalent of [`CoseSign1Bytes`]. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[serde(transparent)] +pub struct CoseSign1BytesBuf(Vec); + +impl CoseSign1BytesBuf { + /// Creates a new CBOR-encoded `COSE_Sign1` object from a byte buffer. + /// + /// The bytes are not actually checked. If the bytes are not describing + /// a CBOR-encoded `COSE_Sign1` object it will be detected when the + /// [`CoseSign1Bytes::decode`] method is called. + pub fn new(bytes: Vec) -> Self { + Self(bytes) + } + + /// Creates a new CBOR-encoded `COSE_Sign1` object by encoding the give + /// [`CoseSign1`] value. + /// + /// If `tagged` is set to `true`, the CBOR value will be tagged. + pub fn encode(object: impl Into, tagged: bool) -> Self { + if tagged { + Self(TaggedCborSerializable::to_tagged_vec(object.into()).unwrap()) + } else { + Self(CborSerializable::to_vec(object.into()).unwrap()) + } + } + + /// Borrows the value as a [`CoseSign1Bytes`]. + pub fn as_compact(&self) -> &CoseSign1Bytes { + CoseSign1Bytes::new(self.0.as_slice()) + } +} + +impl Deref for CoseSign1BytesBuf { + type Target = CoseSign1Bytes; + + fn deref(&self) -> &Self::Target { + self.as_compact() + } +} + +impl Borrow for CoseSign1BytesBuf { + fn borrow(&self) -> &CoseSign1Bytes { + self.as_compact() + } +} + +impl From for CoseSign1BytesBuf { + fn from(value: CborValue) -> Self { + let mut buffer = Vec::new(); + ciborium::into_writer(&value, &mut buffer).unwrap(); + Self(buffer) + } +} + +impl AsRef<[u8]> for CoseSign1BytesBuf { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl From> for CoseSign1BytesBuf { + fn from(value: Vec) -> Self { + Self(value) + } +} + +/// Decoded `COSE_Sign1` object. +pub struct DecodedCoseSign1 { + /// Signing bytes. + pub signing_bytes: UnsignedCoseSign1, + + /// Signature. + pub signature: CoseSignatureBytes, +} + +impl DecodedCoseSign1 { + /// Maps the payload interpretation. + /// + /// This function can be used to decode the raw payload bytes into a + /// proper typed value the application can work with. + pub fn map(self, f: impl FnOnce(T, &[u8]) -> U) -> DecodedCoseSign1 { + DecodedCoseSign1 { + signing_bytes: self.signing_bytes.map(f), + signature: self.signature, + } + } + + /// Tries to map the payload interpretation. + /// + /// This function can be used to decode the raw payload bytes into a + /// proper typed value the application can work with. + pub fn try_map( + self, + f: impl FnOnce(T, &[u8]) -> Result, + ) -> Result, E> { + Ok(DecodedCoseSign1 { + signing_bytes: self.signing_bytes.try_map(f)?, + signature: self.signature, + }) + } +} + +impl From for DecodedCoseSign1 { + fn from(value: CoseSign1) -> Self { + Self { + signing_bytes: UnsignedCoseSign1 { + protected: value.protected, + unprotected: value.unprotected, + payload: PayloadBytes::from_bytes(value.payload.unwrap_or_default()), + }, + signature: CoseSignatureBytes(value.signature), + } + } +} + +impl From> for CoseSign1 { + fn from(value: DecodedCoseSign1) -> Self { + Self { + protected: value.signing_bytes.protected, + unprotected: value.signing_bytes.unprotected, + payload: Some(value.signing_bytes.payload.into_bytes()), + signature: value.signature.into_bytes(), + } + } +} + +/// Payload and bytes. +/// +/// Stores the payload value as interpreted by the application (type `T`) and +/// the original payload bytes. +/// +/// The original payload bytes are always preserved since they can not always +/// be deterministically (or cheaply) reconstructed from the typed payload +/// value. +#[derive(Clone, PartialEq)] +pub struct PayloadBytes { + /// Original payload bytes. + bytes: Vec, + + /// Interpretation of the payload bytes. + value: T, +} + +impl PayloadBytes { + /// Creates a new `PayloadBytes` from the bytes. + /// + /// The interpretation of the bytes will be unit `()`. + pub fn from_bytes(bytes: Vec) -> Self { + Self { bytes, value: () } + } +} + +impl PayloadBytes { + /// Creates a new `PayloadBytes` from the payload, using + /// [`CosePayload::payload_bytes`] to reconstruct the payload bytes. + pub fn new(value: T) -> Self { + Self { + bytes: value.payload_bytes().into_owned(), + value, + } + } +} + +impl PayloadBytes { + /// Returns the bytes as a slice. + pub fn as_bytes(&self) -> &[u8] { + &self.bytes + } + + /// Maps the payload interpretation. + /// + /// This function can be used to decode the raw payload bytes into a + /// proper typed value the application can work with. + pub fn map(self, f: impl FnOnce(T, &[u8]) -> U) -> PayloadBytes { + let value = f(self.value, &self.bytes); + PayloadBytes { + bytes: self.bytes, + value, + } + } + + /// Tries to map the payload interpretation. + /// + /// This function can be used to decode the raw payload bytes into a + /// proper typed value the application can work with. + pub fn try_map( + self, + f: impl FnOnce(T, &[u8]) -> Result, + ) -> Result, E> { + let value = f(self.value, &self.bytes)?; + Ok(PayloadBytes { + bytes: self.bytes, + value, + }) + } + + /// Forgets about the payload interpretation and returns the raw bytes. + pub fn into_bytes(self) -> Vec { + self.bytes + } +} + +impl Deref for PayloadBytes { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl Borrow for PayloadBytes { + fn borrow(&self) -> &T { + &self.value + } +} + +/// `COSE_Sign1` object without the signature. +#[derive(Clone, PartialEq)] +pub struct UnsignedCoseSign1 { + /// Protected header. + pub protected: ProtectedHeader, + + /// Unprotected header. + pub unprotected: Header, + + /// Payload. + pub payload: PayloadBytes, +} + +impl UnsignedCoseSign1 { + /// Returns the bytes that will be signed. + pub fn tbs_data(&self, aad: &[u8]) -> Vec { + sig_structure_data( + coset::SignatureContext::CoseSign1, + self.protected.clone(), + None, + aad, + self.payload.as_bytes(), + ) + } + + /// Maps the payload interpretation. + pub fn map(self, f: impl FnOnce(T, &[u8]) -> U) -> UnsignedCoseSign1 { + UnsignedCoseSign1 { + protected: self.protected, + unprotected: self.unprotected, + payload: self.payload.map(f), + } + } + + /// Tries to map the payload interpretation. + pub fn try_map( + self, + f: impl FnOnce(T, &[u8]) -> Result, + ) -> Result, E> { + Ok(UnsignedCoseSign1 { + protected: self.protected, + unprotected: self.unprotected, + payload: self.payload.try_map(f)?, + }) + } +} diff --git a/crates/claims/crates/cose/src/signature.rs b/crates/claims/crates/cose/src/signature.rs new file mode 100644 index 000000000..550310f5e --- /dev/null +++ b/crates/claims/crates/cose/src/signature.rs @@ -0,0 +1,157 @@ +use coset::{ + Algorithm, CborSerializable, CoseSign1, Header, ProtectedHeader, TaggedCborSerializable, +}; +use ssi_claims_core::SignatureError; + +use crate::{CosePayload, CoseSign1BytesBuf, TYP_LABEL}; + +/// COSE signer information. +pub struct CoseSignerInfo { + /// Signature algorithm. + pub algorithm: Option, + + /// Signing key identifier. + pub key_id: Vec, +} + +/// COSE signer. +/// +/// Any type with the ability to sign a COSE payload. +pub trait CoseSigner { + /// Fetches the information about the signing key. + /// + /// This information will be included in the COSE header. + #[allow(async_fn_in_trait)] + async fn fetch_info(&self) -> Result; + + /// Signs the given bytes. + #[allow(async_fn_in_trait)] + async fn sign_bytes(&self, signing_bytes: &[u8]) -> Result, SignatureError>; + + /// Signs the given payload. + /// + /// Returns a serialized `COSE_Sign1` object, tagged or not according to + /// `tagged`. + #[allow(async_fn_in_trait)] + async fn sign( + &self, + payload: &(impl ?Sized + CosePayload), + additional_data: Option<&[u8]>, + tagged: bool, + ) -> Result { + let info = self.fetch_info().await?; + + let mut result = CoseSign1 { + protected: ProtectedHeader { + header: Header { + alg: info.algorithm, + key_id: info.key_id, + content_type: payload.content_type(), + rest: match payload.typ() { + Some(typ) => vec![(TYP_LABEL, typ.into())], + None => Vec::new(), + }, + ..Default::default() + }, + ..Default::default() + }, + unprotected: Header::default(), + payload: Some(payload.payload_bytes().into_owned()), + signature: Vec::new(), + }; + + let tbs = result.tbs_data(additional_data.unwrap_or_default()); + + result.signature = self.sign_bytes(&tbs).await?; + + Ok(if tagged { + result.to_tagged_vec().unwrap().into() + } else { + result.to_vec().unwrap().into() + }) + } +} + +impl<'a, T: CoseSigner> CoseSigner for &'a T { + async fn fetch_info(&self) -> Result { + T::fetch_info(*self).await + } + + async fn sign_bytes(&self, signing_bytes: &[u8]) -> Result, SignatureError> { + T::sign_bytes(*self, signing_bytes).await + } + + async fn sign( + &self, + payload: &(impl ?Sized + CosePayload), + additional_data: Option<&[u8]>, + tagged: bool, + ) -> Result { + T::sign(*self, payload, additional_data, tagged).await + } +} + +#[cfg(test)] +mod tests { + use crate::{key::CoseKeyGenerate, CosePayload, DecodedCoseSign1}; + use coset::CoseKey; + use ssi_claims_core::VerificationParameters; + + async fn sign_with(key: &CoseKey, tagged: bool) { + let bytes = b"PAYLOAD".sign(key, tagged).await.unwrap(); + let decoded: DecodedCoseSign1 = bytes.decode(tagged).unwrap(); + + assert_eq!(decoded.signing_bytes.payload.as_bytes(), b"PAYLOAD"); + + let params = VerificationParameters::from_resolver(key); + assert_eq!(decoded.verify(params).await.unwrap(), Ok(())); + } + + #[cfg(feature = "ed25519")] + #[async_std::test] + async fn sign_ed25519() { + sign_with(&CoseKey::generate_ed25519(), false).await + } + + #[cfg(feature = "ed25519")] + #[async_std::test] + async fn sign_ed25519_tagged() { + sign_with(&CoseKey::generate_ed25519(), true).await + } + + #[cfg(feature = "secp256k1")] + #[async_std::test] + async fn sign_secp256k1() { + sign_with(&CoseKey::generate_secp256k1(), false).await + } + + #[cfg(feature = "secp256k1")] + #[async_std::test] + async fn sign_secp256k1_tagged() { + sign_with(&CoseKey::generate_secp256k1(), true).await + } + + #[cfg(feature = "secp256r1")] + #[async_std::test] + async fn sign_p256() { + sign_with(&CoseKey::generate_p256(), false).await + } + + #[cfg(feature = "secp256r1")] + #[async_std::test] + async fn sign_p256_tagged() { + sign_with(&CoseKey::generate_p256(), true).await + } + + #[cfg(feature = "secp384r1")] + #[async_std::test] + async fn sign_p384() { + sign_with(&CoseKey::generate_p384(), false).await + } + + #[cfg(feature = "secp384r1")] + #[async_std::test] + async fn sign_p384_tagged() { + sign_with(&CoseKey::generate_p384(), true).await + } +} diff --git a/crates/claims/crates/cose/src/verification.rs b/crates/claims/crates/cose/src/verification.rs new file mode 100644 index 000000000..b627394ca --- /dev/null +++ b/crates/claims/crates/cose/src/verification.rs @@ -0,0 +1,135 @@ +use coset::{CoseKey, Header, ProtectedHeader}; +use ssi_claims_core::{ + ClaimsValidity, InvalidProof, ProofValidationError, ProofValidity, ResolverProvider, + ValidateClaims, ValidateProof, VerifiableClaims, Verification, +}; +use ssi_crypto::VerificationError; + +use crate::{ + algorithm::instantiate_algorithm, + key::{CoseKeyDecode, CoseKeyResolver, KeyDecodingError}, + CoseSignatureBytes, DecodedCoseSign1, UnsignedCoseSign1, +}; + +impl DecodedCoseSign1 { + /// Verify. + pub async fn verify

(&self, params: P) -> Result + where + T: ValidateCoseHeader

+ ValidateClaims, + P: ResolverProvider, + { + VerifiableClaims::verify(self, params).await + } +} + +impl VerifiableClaims for DecodedCoseSign1 { + type Claims = UnsignedCoseSign1; + type Proof = CoseSignatureBytes; + + fn claims(&self) -> &Self::Claims { + &self.signing_bytes + } + + fn proof(&self) -> &Self::Proof { + &self.signature + } +} + +pub trait ValidateCoseHeader

{ + fn validate_cose_headers( + &self, + _params: &P, + _protected: &ProtectedHeader, + _unprotected: &Header, + ) -> ClaimsValidity { + Ok(()) + } +} + +impl

ValidateCoseHeader

for () {} + +impl ValidateClaims for UnsignedCoseSign1 +where + T: ValidateClaims + ValidateCoseHeader, +{ + fn validate_claims(&self, params: &E, signature: &CoseSignatureBytes) -> ClaimsValidity { + self.payload + .validate_cose_headers(params, &self.protected, &self.unprotected)?; + self.payload.validate_claims(params, signature) + } +} + +impl ValidateProof> for CoseSignatureBytes +where + P: ResolverProvider, +{ + async fn validate_proof<'a>( + &'a self, + params: &'a P, + claims: &'a UnsignedCoseSign1, + ) -> Result { + let key = params + .resolver() + .fetch_public_cose_key(Some(&claims.protected.header.key_id)) + .await?; + + let signing_bytes = claims.tbs_data(&[]); + + verify_bytes( + claims + .protected + .header + .alg + .as_ref() + .ok_or(ProofValidationError::MissingAlgorithm)?, + &key, + &signing_bytes, + &self.0, + ) + .map(|b| { + if b { + Ok(()) + } else { + Err(InvalidProof::Signature) + } + }) + .map_err(Into::into) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum CoseVerificationError { + #[error("unsupported COSE algorithm")] + UnsupportedAlgorithm(coset::Algorithm), + + #[error(transparent)] + PublicKey(#[from] KeyDecodingError), + + #[error(transparent)] + Verification(#[from] VerificationError), +} + +impl From for ProofValidationError { + fn from(value: CoseVerificationError) -> Self { + match value { + CoseVerificationError::PublicKey(_) => Self::InvalidKey, + e => ProofValidationError::other(e), + } + } +} + +/// Verify a signature using a COSE key and algorithm. +pub fn verify_bytes( + algorithm: &coset::Algorithm, + key: &CoseKey, + signing_bytes: &[u8], + signature_bytes: &[u8], +) -> Result { + let instance = instantiate_algorithm(algorithm) + .ok_or_else(|| CoseVerificationError::UnsupportedAlgorithm(algorithm.clone()))?; + let public_key = key.decode_public()?; + + public_key + .verify(instance, signing_bytes, signature_bytes) + .map_err(Into::into) +} diff --git a/crates/claims/crates/data-integrity/sd-primitives/src/canonicalize.rs b/crates/claims/crates/data-integrity/sd-primitives/src/canonicalize.rs index e5d4eee51..0fa05d5a1 100644 --- a/crates/claims/crates/data-integrity/sd-primitives/src/canonicalize.rs +++ b/crates/claims/crates/data-integrity/sd-primitives/src/canonicalize.rs @@ -1,3 +1,4 @@ +use base64::Engine; use iref::Iri; use rdf_types::{BlankId, BlankIdBuf, Id, LexicalQuad, LexicalQuadRef, Literal, Quad, Term}; use ssi_rdf::urdna2015::NormalizingSubstitution; @@ -16,7 +17,7 @@ pub fn create_hmac_id_label_map_function( let digest = hmac.finalize_reset(); let b64_url_digest = BlankIdBuf::new(format!( "_:u{}", - base64::encode_config(digest, base64::URL_SAFE_NO_PAD) + base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(digest) )) .unwrap(); (key.clone(), b64_url_digest) diff --git a/crates/claims/crates/data-integrity/suites/src/suites/w3c/rsa_signature_2018.rs b/crates/claims/crates/data-integrity/suites/src/suites/w3c/rsa_signature_2018.rs index 3f592e2f3..a4962958e 100644 --- a/crates/claims/crates/data-integrity/suites/src/suites/w3c/rsa_signature_2018.rs +++ b/crates/claims/crates/data-integrity/suites/src/suites/w3c/rsa_signature_2018.rs @@ -1,3 +1,4 @@ +use base64::Engine; use k256::sha2::Sha256; use serde::{Deserialize, Serialize}; use ssi_claims_core::{ProofValidationError, ProofValidity, SignatureError}; @@ -87,7 +88,7 @@ where .await?; Ok(Signature { - signature_value: base64::encode(signature), + signature_value: base64::prelude::BASE64_STANDARD.encode(signature), }) } } @@ -98,7 +99,8 @@ impl VerificationAlgorithm for RsaSignatureAlgorithm { prepared_claims: [u8; 64], proof: ProofRef, ) -> Result { - let signature = base64::decode(&proof.signature.signature_value) + let signature = base64::prelude::BASE64_STANDARD + .decode(&proof.signature.signature_value) .map_err(|_| ProofValidationError::InvalidSignature)?; method .verify_bytes(&prepared_claims, &signature) diff --git a/crates/claims/crates/jws/src/compact/bytes.rs b/crates/claims/crates/jws/src/compact/bytes.rs index 90e3e0cb4..e52731afa 100644 --- a/crates/claims/crates/jws/src/compact/bytes.rs +++ b/crates/claims/crates/jws/src/compact/bytes.rs @@ -1,5 +1,6 @@ use crate::{DecodeError, DecodedJWS, DecodedSigningBytes, Header, InvalidHeader, JWS}; pub use base64::DecodeError as Base64DecodeError; +use base64::Engine; use ssi_claims_core::{ProofValidationError, ResolverProvider, Verification}; use ssi_jwk::JWKResolver; use std::{borrow::Cow, ops::Deref}; @@ -105,10 +106,9 @@ impl CompactJWS { /// The header is necessary to know how the payload is encoded. pub fn decode_payload(&self, header: &Header) -> Result, Base64DecodeError> { if header.base64urlencode_payload.unwrap_or(true) { - Ok(Cow::Owned(base64::decode_config( - self.payload(), - base64::URL_SAFE_NO_PAD, - )?)) + Ok(Cow::Owned( + base64::prelude::BASE64_URL_SAFE_NO_PAD.decode(self.payload())?, + )) } else { Ok(Cow::Borrowed(self.payload())) } @@ -120,7 +120,7 @@ impl CompactJWS { } pub fn decode_signature(&self) -> Result, Base64DecodeError> { - base64::decode_config(self.signature(), base64::URL_SAFE_NO_PAD) + base64::prelude::BASE64_URL_SAFE_NO_PAD.decode(self.signature()) } /// Decodes the entire JWS. diff --git a/crates/claims/crates/jws/src/compact/str.rs b/crates/claims/crates/jws/src/compact/str.rs index a2b7de4d9..150d9a8c9 100644 --- a/crates/claims/crates/jws/src/compact/str.rs +++ b/crates/claims/crates/jws/src/compact/str.rs @@ -1,6 +1,8 @@ use core::fmt; use std::{ops::Deref, str::FromStr}; +use base64::Engine; + use crate::{CompactJWS, DecodeError, DecodedJWS, DecodedSigningBytes, Header, InvalidCompactJWS}; /// JWS in UTF-8 compact serialized form. @@ -164,7 +166,7 @@ impl CompactJWSString { /// /// Detached means the payload will not appear in the JWS. pub fn encode_detached(header: Header, signature: &[u8]) -> Self { - let b64_signature = base64::encode_config(signature, base64::URL_SAFE_NO_PAD); + let b64_signature = base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(signature); Self::new_detached(header, b64_signature.as_bytes()).unwrap() } @@ -173,7 +175,7 @@ impl CompactJWSString { signing_bytes: Vec, signature: &[u8], ) -> Result>> { - let b64_signature = base64::encode_config(signature, base64::URL_SAFE_NO_PAD); + let b64_signature = base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(signature); let mut bytes = signing_bytes; bytes.push(b'.'); bytes.extend_from_slice(b64_signature.as_bytes()); diff --git a/crates/claims/crates/jws/src/lib.rs b/crates/claims/crates/jws/src/lib.rs index 7420dc3b0..9542741b8 100644 --- a/crates/claims/crates/jws/src/lib.rs +++ b/crates/claims/crates/jws/src/lib.rs @@ -75,6 +75,7 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod error; pub use base64::DecodeError as Base64DecodeError; +use base64::Engine; pub use error::Error; use serde::{Deserialize, Serialize}; use ssi_claims_core::{ @@ -381,7 +382,7 @@ impl Header { /// Decode a JWS Protected Header. pub fn decode(base_64: &[u8]) -> Result { - let header_json = base64::decode_config(base_64, base64::URL_SAFE_NO_PAD)?; + let header_json = base64::prelude::BASE64_URL_SAFE_NO_PAD.decode(base_64)?; Ok(serde_json::from_slice(&header_json)?) } @@ -390,7 +391,7 @@ impl Header { } pub fn encode(&self) -> String { - base64::encode_config(self.to_json_string(), base64::URL_SAFE_NO_PAD) + base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(self.to_json_string()) } pub fn encode_signing_bytes(&self, payload: &[u8]) -> Vec { @@ -398,7 +399,7 @@ impl Header { result.push(b'.'); if self.base64urlencode_payload.unwrap_or(true) { - let encoded_payload = base64::encode_config(payload, base64::URL_SAFE_NO_PAD); + let encoded_payload = base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(payload); result.extend(encoded_payload.into_bytes()) } else { result.extend(payload) @@ -410,7 +411,7 @@ impl Header { fn base64_encode_json(object: &T) -> Result { let json = serde_json::to_string(&object)?; - Ok(base64::encode_config(json, base64::URL_SAFE_NO_PAD)) + Ok(base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(json)) } #[allow(unreachable_code, unused_variables)] @@ -599,7 +600,7 @@ pub fn sign_bytes(algorithm: Algorithm, data: &[u8], key: &JWK) -> Result Result { let signature = sign_bytes(algorithm, data, key)?; - let sig_b64 = base64::encode_config(signature, base64::URL_SAFE_NO_PAD); + let sig_b64 = base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(signature); Ok(sig_b64) } @@ -715,7 +716,7 @@ pub fn verify_bytes_warnable( let normalized_sig = if let Some(s) = sig.normalize_s() { // For user convenience, output the normalized signature. let sig_normalized_b64 = - base64::encode_config(s.to_bytes(), base64::URL_SAFE_NO_PAD); + base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(s.to_bytes()); warnings.push(format!( "Non-normalized ES256K signature. Normalized: {sig_normalized_b64}" )); @@ -989,7 +990,7 @@ pub fn encode_sign_custom_header( header: &Header, ) -> Result { let header_b64 = base64_encode_json(header)?; - let payload_b64 = base64::encode_config(payload, base64::URL_SAFE_NO_PAD); + let payload_b64 = base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(payload); let signing_input = header_b64 + "." + &payload_b64; let sig_b64 = sign_bytes_b64(header.algorithm, signing_input.as_bytes(), key)?; let jws = [signing_input, sig_b64].join("."); @@ -1002,7 +1003,7 @@ pub fn encode_unsigned(payload: &str) -> Result { ..Default::default() }; let header_b64 = base64_encode_json(&header)?; - let payload_b64 = base64::encode_config(payload, base64::URL_SAFE_NO_PAD); + let payload_b64 = base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(payload); Ok(header_b64 + "." + &payload_b64 + ".") } @@ -1032,10 +1033,10 @@ pub fn decode_jws_parts( payload_enc: &[u8], signature_b64: &str, ) -> Result { - let signature = base64::decode_config(signature_b64, base64::URL_SAFE_NO_PAD)?; + let signature = base64::prelude::BASE64_URL_SAFE_NO_PAD.decode(signature_b64)?; let header = Header::decode(header_b64.as_bytes())?; let payload = if header.base64urlencode_payload.unwrap_or(true) { - base64::decode_config(payload_enc, base64::URL_SAFE_NO_PAD)? + base64::prelude::BASE64_URL_SAFE_NO_PAD.decode(payload_enc)? } else { payload_enc.to_vec() }; diff --git a/crates/claims/crates/sd-jwt/src/digest.rs b/crates/claims/crates/sd-jwt/src/digest.rs index 34c9e6421..523218a16 100644 --- a/crates/claims/crates/sd-jwt/src/digest.rs +++ b/crates/claims/crates/sd-jwt/src/digest.rs @@ -1,4 +1,4 @@ -use base64::URL_SAFE_NO_PAD; +use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine}; use sha2::Digest; use crate::DecodeError; @@ -47,7 +47,7 @@ pub fn hash_encoded_disclosure(digest_algo: SdAlg, disclosure: &str) -> String { match digest_algo { SdAlg::Sha256 => { let digest = sha2::Sha256::digest(disclosure.as_bytes()); - base64::encode_config(digest, URL_SAFE_NO_PAD) + BASE64_URL_SAFE_NO_PAD.encode(digest) } } } diff --git a/crates/claims/crates/sd-jwt/src/disclosure.rs b/crates/claims/crates/sd-jwt/src/disclosure.rs index c428dff96..e4d685184 100644 --- a/crates/claims/crates/sd-jwt/src/disclosure.rs +++ b/crates/claims/crates/sd-jwt/src/disclosure.rs @@ -1,6 +1,5 @@ -use base64::URL_SAFE_NO_PAD; - use crate::DecodeError; +use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine}; #[derive(Debug, PartialEq)] pub struct DecodedDisclosure { @@ -19,7 +18,8 @@ pub enum DisclosureKind { impl DecodedDisclosure { pub fn new(encoded: &str) -> Result { - let bytes = base64::decode_config(encoded, URL_SAFE_NO_PAD) + let bytes = BASE64_URL_SAFE_NO_PAD + .decode(encoded) .map_err(|_| DecodeError::DisclosureMalformed)?; let json: serde_json::Value = serde_json::from_slice(&bytes)?; diff --git a/crates/claims/crates/sd-jwt/src/encode.rs b/crates/claims/crates/sd-jwt/src/encode.rs index 6b89f44a7..6318b8125 100644 --- a/crates/claims/crates/sd-jwt/src/encode.rs +++ b/crates/claims/crates/sd-jwt/src/encode.rs @@ -1,4 +1,4 @@ -use base64::URL_SAFE_NO_PAD; +use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine}; use rand::{CryptoRng, Rng}; use serde::Serialize; use ssi_jwk::{Algorithm, JWK}; @@ -27,7 +27,7 @@ fn encode_disclosure_with_salt( let json_string = serde_json::to_string(&disclosure)?; - Ok(base64::encode_config(json_string, URL_SAFE_NO_PAD)) + Ok(BASE64_URL_SAFE_NO_PAD.encode(json_string)) } pub fn encode_disclosure_with_rng( @@ -42,7 +42,7 @@ pub fn encode_disclosure_with_rng( rng.fill_bytes(&mut salt_bytes); - let salt = base64::encode_config(salt_bytes, URL_SAFE_NO_PAD); + let salt = BASE64_URL_SAFE_NO_PAD.encode(salt_bytes); encode_disclosure_with_salt(&salt, claim_name, claim_value) } diff --git a/crates/claims/crates/vc-jose-cose/Cargo.toml b/crates/claims/crates/vc-jose-cose/Cargo.toml index 93bf9b29a..af0bb4924 100644 --- a/crates/claims/crates/vc-jose-cose/Cargo.toml +++ b/crates/claims/crates/vc-jose-cose/Cargo.toml @@ -11,14 +11,19 @@ documentation = "https://docs.rs/vc-jose-cose/" [dependencies] ssi-claims-core.workspace = true ssi-jws.workspace = true +ssi-cose.workspace = true ssi-vc.workspace = true ssi-json-ld.workspace = true xsd-types.workspace = true serde.workspace = true serde_json.workspace = true +ciborium.workspace = true thiserror.workspace = true +base64.workspace = true [dev-dependencies] ssi-jws = { workspace = true, features = ["secp256r1"] } ssi-jwk.workspace = true -async-std.workspace = true \ No newline at end of file +ssi-cose = { workspace = true, features = ["secp256r1"] } +async-std.workspace = true +hex.workspace = true \ No newline at end of file diff --git a/crates/claims/crates/vc-jose-cose/src/cose/credential.rs b/crates/claims/crates/vc-jose-cose/src/cose/credential.rs new file mode 100644 index 000000000..c34f1a7d8 --- /dev/null +++ b/crates/claims/crates/vc-jose-cose/src/cose/credential.rs @@ -0,0 +1,233 @@ +use base64::Engine; +use serde::{de::DeserializeOwned, Serialize}; +use ssi_claims_core::{ClaimsValidity, DateTimeProvider, SignatureError, ValidateClaims}; +use ssi_cose::{CosePayload, CoseSign1Bytes, CoseSigner, DecodedCoseSign1, ValidateCoseHeader}; +use ssi_json_ld::{iref::Uri, syntax::Context}; +use ssi_vc::{ + enveloped::EnvelopedVerifiableCredential, + v2::{Credential, CredentialTypes, JsonCredential}, + MaybeIdentified, +}; +use std::borrow::Cow; +use xsd_types::DateTimeStamp; + +use super::CoseDecodeError; + +/// Payload of a COSE_Sign1-secured Verifiable Credential. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct CoseVc(pub T); + +impl CoseVc { + /// Sign a COSE VC into an enveloped verifiable credential. + pub async fn sign_into_enveloped( + &self, + signer: impl CoseSigner, + ) -> Result { + let cose = CosePayload::sign(self, signer, true).await?; + let base64_cose = base64::prelude::BASE64_STANDARD.encode(&cose); + Ok(EnvelopedVerifiableCredential { + context: Context::iri_ref(ssi_vc::v2::CREDENTIALS_V2_CONTEXT_IRI.to_owned().into()), + id: format!("data:application/vc-ld+cose;base64,{base64_cose}") + .parse() + .unwrap(), + }) + } +} + +impl CoseVc { + /// Decode a COSE VC. + pub fn decode( + cose: &CoseSign1Bytes, + tagged: bool, + ) -> Result, CoseDecodeError> { + cose.decode(tagged)? + .try_map(|_, payload| serde_json::from_slice(payload).map(Self)) + .map_err(Into::into) + } +} + +impl CoseVc { + /// Decode a JOSE VC with an arbitrary credential type. + pub fn decode_any( + cose: &CoseSign1Bytes, + tagged: bool, + ) -> Result, CoseDecodeError> { + Self::decode(cose, tagged) + } +} + +impl CosePayload for CoseVc { + fn typ(&self) -> Option { + Some(ssi_cose::CosePayloadType::Text( + "application/vc-ld+cose".to_owned(), + )) + } + + fn content_type(&self) -> Option { + Some(ssi_cose::ContentType::Text("application/vc".to_owned())) + } + + fn payload_bytes(&self) -> Cow<[u8]> { + Cow::Owned(serde_json::to_vec(&self.0).unwrap()) + } +} + +impl ValidateCoseHeader for CoseVc { + fn validate_cose_headers( + &self, + _params: &E, + _protected: &ssi_cose::ProtectedHeader, + _unprotected: &ssi_cose::Header, + ) -> ClaimsValidity { + Ok(()) + } +} + +impl MaybeIdentified for CoseVc { + fn id(&self) -> Option<&Uri> { + self.0.id() + } +} + +impl Credential for CoseVc { + type Description = T::Description; + type Subject = T::Subject; + type Issuer = T::Issuer; + type Status = T::Status; + type Schema = T::Schema; + type RelatedResource = T::RelatedResource; + type RefreshService = T::RefreshService; + type TermsOfUse = T::TermsOfUse; + type Evidence = T::Evidence; + + fn id(&self) -> Option<&Uri> { + Credential::id(&self.0) + } + + fn additional_types(&self) -> &[String] { + self.0.additional_types() + } + + fn types(&self) -> CredentialTypes { + self.0.types() + } + + fn name(&self) -> Option<&str> { + self.0.name() + } + + fn description(&self) -> Option<&Self::Description> { + self.0.description() + } + + fn credential_subjects(&self) -> &[Self::Subject] { + self.0.credential_subjects() + } + + fn issuer(&self) -> &Self::Issuer { + self.0.issuer() + } + + fn valid_from(&self) -> Option { + self.0.valid_from() + } + + fn valid_until(&self) -> Option { + self.0.valid_until() + } + + fn credential_status(&self) -> &[Self::Status] { + self.0.credential_status() + } + + fn credential_schemas(&self) -> &[Self::Schema] { + self.0.credential_schemas() + } + + fn related_resources(&self) -> &[Self::RelatedResource] { + self.0.related_resources() + } + + fn refresh_services(&self) -> &[Self::RefreshService] { + self.0.refresh_services() + } + + fn terms_of_use(&self) -> &[Self::TermsOfUse] { + self.0.terms_of_use() + } + + fn evidence(&self) -> &[Self::Evidence] { + self.0.evidence() + } + + fn validate_credential(&self, env: &E) -> ClaimsValidity + where + E: DateTimeProvider, + { + self.0.validate_credential(env) + } +} + +impl> ValidateClaims for CoseVc { + fn validate_claims(&self, environment: &E, proof: &P) -> ClaimsValidity { + self.0.validate_claims(environment, proof) + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + use ssi_claims_core::VerificationParameters; + use ssi_cose::{coset::CoseKey, key::CoseKeyGenerate, CoseSign1Bytes, CoseSign1BytesBuf}; + use ssi_vc::v2::JsonCredential; + + use super::CoseVc; + + async fn verify(input: &CoseSign1Bytes, key: &CoseKey) { + let vc = CoseVc::decode_any(input, true).unwrap(); + let params = VerificationParameters::from_resolver(key); + let result = vc.verify(params).await.unwrap(); + assert_eq!(result, Ok(())) + } + + #[async_std::test] + async fn cose_vc_roundtrip() { + let vc: JsonCredential = serde_json::from_value(json!({ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://www.w3.org/ns/credentials/examples/v2" + ], + "id": "http://university.example/credentials/1872", + "type": [ + "VerifiableCredential", + "ExampleAlumniCredential" + ], + "issuer": "https://university.example/issuers/565049", + "validFrom": "2010-01-01T19:23:24Z", + "credentialSchema": { + "id": "https://example.org/examples/degree.json", + "type": "JsonSchema" + }, + "credentialSubject": { + "id": "did:example:123", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts" + } + } + })) + .unwrap(); + + let key = CoseKey::generate_p256(); + let enveloped = CoseVc(vc).sign_into_enveloped(&key).await.unwrap(); + let jws = CoseSign1BytesBuf::new(enveloped.id.decoded_data().unwrap().into_owned()); + verify(&jws, &key).await + } + + #[test] + fn example7() { + let input_hex = "d28444a1013822a05901f87b2240636f6e74657874223a5b2268747470733a2f2f7777772e77332e6f72672f6e732f63726564656e7469616c732f7632222c2268747470733a2f2f7777772e77332e6f72672f6e732f63726564656e7469616c732f6578616d706c65732f7632225d2c226964223a22687474703a2f2f756e69766572736974792e6578616d706c652f63726564656e7469616c732f31383732222c2274797065223a5b2256657269666961626c6543726564656e7469616c222c224578616d706c65416c756d6e6943726564656e7469616c225d2c22697373756572223a2268747470733a2f2f756e69766572736974792e6578616d706c652f697373756572732f353635303439222c2276616c696446726f6d223a22323031302d30312d30315431393a32333a32345a222c2263726564656e7469616c536368656d61223a7b226964223a2268747470733a2f2f6578616d706c652e6f72672f6578616d706c65732f6465677265652e6a736f6e222c2274797065223a224a736f6e536368656d61227d2c2263726564656e7469616c5375626a656374223a7b226964223a226469643a6578616d706c653a313233222c22646567726565223a7b2274797065223a2242616368656c6f72446567726565222c226e616d65223a2242616368656c6f72206f6620536369656e636520616e642041727473227d7d7d58405731e67b84ce95105ea78d49b97f90f962c7e247ebaf4c057b2d8ef16b11882cea11170fcf7b566fd7d8932a597885599d7e010b15d1aa639bcceaf114325a01"; + let input = CoseSign1BytesBuf::new(hex::decode(input_hex).unwrap()); + let _ = CoseVc::decode_any(&input, true).unwrap(); + } +} diff --git a/crates/claims/crates/vc-jose-cose/src/cose/mod.rs b/crates/claims/crates/vc-jose-cose/src/cose/mod.rs new file mode 100644 index 000000000..47eb8dfab --- /dev/null +++ b/crates/claims/crates/vc-jose-cose/src/cose/mod.rs @@ -0,0 +1,16 @@ +use ssi_cose::CoseError; + +mod credential; +pub use credential::*; + +mod presentation; +pub use presentation::*; + +#[derive(Debug, thiserror::Error)] +pub enum CoseDecodeError { + #[error(transparent)] + Decode(#[from] CoseError), + + #[error(transparent)] + Payload(#[from] serde_json::Error), +} diff --git a/crates/claims/crates/vc-jose-cose/src/cose/presentation.rs b/crates/claims/crates/vc-jose-cose/src/cose/presentation.rs new file mode 100644 index 000000000..b59e6d186 --- /dev/null +++ b/crates/claims/crates/vc-jose-cose/src/cose/presentation.rs @@ -0,0 +1,164 @@ +use super::CoseDecodeError; +use base64::Engine; +use serde::{de::DeserializeOwned, Serialize}; +use ssi_claims_core::{ClaimsValidity, SignatureError, ValidateClaims}; +use ssi_cose::{CosePayload, CoseSign1Bytes, CoseSigner, DecodedCoseSign1, ValidateCoseHeader}; +use ssi_json_ld::{iref::Uri, syntax::Context}; +use ssi_vc::{ + enveloped::{EnvelopedVerifiableCredential, EnvelopedVerifiablePresentation}, + v2::{syntax::JsonPresentation, Presentation, PresentationTypes}, + MaybeIdentified, +}; +use std::borrow::Cow; + +/// Payload of a COSE-secured Verifiable Presentation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct CoseVp>(pub T); + +impl CosePayload for CoseVp { + fn typ(&self) -> Option { + Some(ssi_cose::CosePayloadType::Text( + "application/vp-ld+cose".to_owned(), + )) + } + + fn content_type(&self) -> Option { + Some(ssi_cose::ContentType::Text("application/vp".to_owned())) + } + + fn payload_bytes(&self) -> Cow<[u8]> { + Cow::Owned(serde_json::to_vec(&self.0).unwrap()) + } +} + +impl ValidateCoseHeader for CoseVp { + fn validate_cose_headers( + &self, + _params: &E, + _protected: &ssi_cose::ProtectedHeader, + _unprotected: &ssi_cose::Header, + ) -> ClaimsValidity { + Ok(()) + } +} + +impl CoseVp { + /// Sign a COSE VP into an enveloped verifiable presentation. + pub async fn sign_into_enveloped( + &self, + signer: &impl CoseSigner, + ) -> Result { + let cose = CosePayload::sign(self, signer, true).await?; + let base64_cose = base64::prelude::BASE64_STANDARD.encode(&cose); + Ok(EnvelopedVerifiablePresentation { + context: Context::iri_ref(ssi_vc::v2::CREDENTIALS_V2_CONTEXT_IRI.to_owned().into()), + id: format!("data:application/vp-ld+cose;base64,{base64_cose}") + .parse() + .unwrap(), + }) + } +} + +impl CoseVp { + /// Decode a JOSE VP. + pub fn decode( + cose: &CoseSign1Bytes, + tagged: bool, + ) -> Result, CoseDecodeError> { + cose.decode(tagged)? + .try_map(|_, payload| serde_json::from_slice(payload).map(Self)) + .map_err(Into::into) + } +} + +impl CoseVp { + /// Decode a JOSE VP with an arbitrary presentation type. + pub fn decode_any( + jws: &CoseSign1Bytes, + tagged: bool, + ) -> Result, CoseDecodeError> { + Self::decode(jws, tagged) + } +} + +impl MaybeIdentified for CoseVp { + fn id(&self) -> Option<&ssi_json_ld::iref::Uri> { + self.0.id() + } +} + +impl Presentation for CoseVp { + type Credential = T::Credential; + type Holder = T::Holder; + + fn id(&self) -> Option<&Uri> { + Presentation::id(&self.0) + } + + fn additional_types(&self) -> &[String] { + self.0.additional_types() + } + + fn types(&self) -> PresentationTypes { + self.0.types() + } + + fn verifiable_credentials(&self) -> &[Self::Credential] { + self.0.verifiable_credentials() + } + + fn holders(&self) -> &[Self::Holder] { + self.0.holders() + } +} + +impl> ValidateClaims for CoseVp { + fn validate_claims(&self, environment: &E, proof: &P) -> ClaimsValidity { + self.0.validate_claims(environment, proof) + } +} + +#[cfg(test)] +mod tests { + use super::CoseVp; + use serde_json::json; + use ssi_claims_core::VerificationParameters; + use ssi_cose::{key::CoseKeyGenerate, CoseKey, CoseSign1Bytes, CoseSign1BytesBuf}; + use ssi_vc::{enveloped::EnvelopedVerifiableCredential, v2::syntax::JsonPresentation}; + + async fn verify(input: &CoseSign1Bytes, key: &CoseKey) { + let vp = CoseVp::decode_any(input, true).unwrap(); + let params = VerificationParameters::from_resolver(key); + let result = vp.verify(params).await.unwrap(); + assert_eq!(result, Ok(())) + } + + #[async_std::test] + async fn cose_vp_roundtrip() { + let vp: JsonPresentation = serde_json::from_value(json!({ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://www.w3.org/ns/credentials/examples/v2" + ], + "type": "VerifiablePresentation", + "verifiableCredential": [{ + "@context": ["https://www.w3.org/ns/credentials/v2"], + "type": ["EnvelopedVerifiableCredential"], + "id": "data:application/vc-ld+jwt,eyJraWQiOiJFeEhrQk1XOWZtYmt2VjI2Nm1ScHVQMnNVWV9OX0VXSU4xbGFwVXpPOHJvIiwiYWxnIjoiRVMzODQifQ.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjIiXSwiaWQiOiJodHRwOi8vdW5pdmVyc2l0eS5leGFtcGxlL2NyZWRlbnRpYWxzLzE4NzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRXhhbXBsZUFsdW1uaUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly91bml2ZXJzaXR5LmV4YW1wbGUvaXNzdWVycy81NjUwNDkiLCJ2YWxpZEZyb20iOiIyMDEwLTAxLTAxVDE5OjIzOjI0WiIsImNyZWRlbnRpYWxTY2hlbWEiOnsiaWQiOiJodHRwczovL2V4YW1wbGUub3JnL2V4YW1wbGVzL2RlZ3JlZS5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifSwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZToxMjMiLCJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX19.d2k4O3FytQJf83kLh-HsXuPvh6yeOlhJELVo5TF71gu7elslQyOf2ZItAXrtbXF4Kz9WivNdztOayz4VUQ0Mwa8yCDZkP9B2pH-9S_tcAFxeoeJ6Z4XnFuL_DOfkR1fP" + }] + })).unwrap(); + + let key = CoseKey::generate_p256(); + let enveloped = CoseVp(vp).sign_into_enveloped(&key).await.unwrap(); + let jws = CoseSign1BytesBuf::new(enveloped.id.decoded_data().unwrap().into_owned()); + verify(&jws, &key).await + } + + // NOTE: the example is incorrect because of the invalid data URL. + // #[test] + // fn example8() { + // let input_hex = "d28444a1013822a05908d67b2240636f6e74657874223a5b2268747470733a2f2f7777772e77332e6f72672f6e732f63726564656e7469616c732f7632222c2268747470733a2f2f7777772e77332e6f72672f6e732f63726564656e7469616c732f6578616d706c65732f7632225d2c2274797065223a2256657269666961626c6550726573656e746174696f6e222c2276657269666961626c6543726564656e7469616c223a5b7b2240636f6e74657874223a2268747470733a2f2f7777772e77332e6f72672f6e732f63726564656e7469616c732f7632222c226964223a22646174613a6170706c69636174696f6e2f76632d6c642b73642d6a77743b65794a68624763694f694a46557a4d344e434973496d74705a434936496c565254563966626c4530557a5a43547a68755554527554303559654842346148526f62336c4f654749314d30785a5a316c364c544a42516e4d694c434a30655841694f694a32634374735a437471633239754b334e6b4c57703364434973496d4e3065534936496e5a774b32786b4b32707a6232346966512e65794a4159323975644756346443493657794a6f64485277637a6f764c336433647935334d793576636d6376626e4d7659334a6c5a47567564476c6862484d76646a49694c434a6f64485277637a6f764c336433647935334d793576636d6376626e4d7659334a6c5a47567564476c6862484d765a586868625842735a584d76646a496958537769646d567961575a7059574a735a554e795a57526c626e5270595777694f6c7437496b426a623235305a586830496a7062496d68306448427a4f693876643364334c6e637a4c6d39795a7939756379396a636d566b5a57353061574673637939324d694973496d68306448427a4f693876643364334c6e637a4c6d39795a7939756379396a636d566b5a573530615746736379396c654746746347786c637939324d694a644c434a7063334e315a5849694f694a6f64485277637a6f764c33567561585a6c636e4e7064486b755a586868625842735a53397063334e315a584a7a4c7a55324e5441304f534973496e5a6862476c6b526e4a7662534936496a49774d5441744d4445744d4446554d546b364d6a4d364d6a52614969776959334a6c5a47567564476c6862464e31596d706c593351694f6e73695957783162573570543259694f6e7369626d46745a534936496b5634595731776247556756573570646d567963326c3065534973496c397a5a43493657794a6f656b394c527a55326344493563314279544746444e554534526e64466455637a5655303564556c5a5531703163553959637a4a6c56474a42496c31394c434a66633251694f6c736957566458566d5644526e6478516d6b34574442715346396a56304e575755313653544e684f48426a5445565952575a6963464e5351566c6e64794a646653776958334e6b496a7062496a4a4a5a6a68686155733452455a7756574a346445633163474d77656c395361464a7a626d3179624746524d45687a63546b3457464e79595773694c434a5565445a345a575a4d565564555a557066595774565546644765484e766255686f62477457566e70664e7a566f61565a3665577079596d567a496c31395853776958334e6b496a7062496a6432616e6c3056564e335a454a304d585135526b746c4f56466653334a49525868465747787254454661547a424b4d304a7064323030646c6b695853776958334e6b583246735a794936496e4e6f595330794e5459694c434a70595851694f6a45334d4459314e6a49344e446b73496d5634634349364d54637a4f4445344e5449304f5377695932356d496a7037496d70336179493665794a7264486b694f694a4651794973496d4e7964694936496c41744d7a6730496977695957786e496a6f6952564d7a4f4451694c434a34496a6f6964577445643155325a7a6c51555652465557685961456779636b525a4e6e644d516c67335548466c556a5a4263476c685648424555586f77636c387464446c3655584e78656d35345a3068456345356f656b5a6c51794973496e6b694f694a4d516e6856596e425664464e474d56564b56545670596e4a49646b70494e6a4255534735594d6b3178613078485a476c7455316c3055475234526c6b784f456468636c64695333465a5630646a556b5a4856453942496e313966512e6b594436335974424e596e4c55547736537a663176735f556733554258685077437971704e6d506e5044613372585a5168514c6442314267616f4f387a67512d6333423431667861584d6e4c485956392d42323075626f53704a5030422d325672653931376551743163534473774447415f5974766e3442537159564242324a7e57794a464d6b4673527a68735932703051564672636c6c49626a6c49626e565249697767496e5235634755694c434169566d567961575a7059574a735a5642795a584e6c626e526864476c7662694a647e577949354e6c64594d44526e656e6f3463565a7a4f565a4c553277775954566e49697767496d6c6b49697767496d6830644841364c793931626d6c325a584a7a615852354c6d5634595731776247557659334a6c5a47567564476c6862484d764d5467334d694a647e57794a61656b553256465661616d74484d5731445758424b4d45686e63306c3349697767496e5235634755694c434262496c5a6c636d6c6d6157466962475644636d566b5a5735306157467349697767496b5634595731776247564262485674626d6c44636d566b5a57353061574673496c31647e5779497451334e73533235475a47465962324a695157737955304a425647523349697767496d6c6b49697767496d52705a44706c654746746347786c4f6d56695a6d56694d5759334d544a6c596d4d325a6a466a4d6a63325a5445795a574d794d534a647e57794a75526d314f576c3949637a423357574e6f4f46646b6554646e51554e5249697767496d6c6b49697767496d52705a44706c654746746347786c4f6d4d794e7a5a6c4d544a6c597a49785a574a6d5a5749785a6a63784d6d5669597a5a6d4d534a64222c2274797065223a22456e76656c6f70656456657269666961626c6543726564656e7469616c227d5d7d5840710a23eba256305aaadd73655219bc38acf04714ce3310823f3ad2288a58f9b40e8764cbe28fe20a60e415e63fba71f352160dd2c812c2dd794cd67f420999fd"; + // let input = CoseSign1BytesBuf::new(hex::decode(input_hex).unwrap()); + // let _ = CoseVp::decode_any(&input, true).unwrap(); + // } +} diff --git a/crates/claims/crates/vc-jose-cose/src/lib.rs b/crates/claims/crates/vc-jose-cose/src/lib.rs index 14d7d86dd..ace24fe0b 100644 --- a/crates/claims/crates/vc-jose-cose/src/lib.rs +++ b/crates/claims/crates/vc-jose-cose/src/lib.rs @@ -3,5 +3,7 @@ //! //! [1]: mod jose; - pub use jose::*; + +mod cose; +pub use cose::*; diff --git a/crates/claims/crates/vc/src/v1/revocation/mod.rs b/crates/claims/crates/vc/src/v1/revocation/mod.rs index e9783a24a..cced5c3e9 100644 --- a/crates/claims/crates/vc/src/v1/revocation/mod.rs +++ b/crates/claims/crates/vc/src/v1/revocation/mod.rs @@ -2,6 +2,7 @@ //! Credentials. //! //! See: +use base64::Engine; use bitvec::prelude::Lsb0; use bitvec::slice::BitSlice; use core::convert::TryFrom; @@ -146,7 +147,7 @@ impl TryFrom<&EncodedList> for List { type Error = DecodeListError; fn try_from(encoded_list: &EncodedList) -> Result { let string = &encoded_list.0; - let bytes = base64::decode_config(string, base64::URL_SAFE)?; + let bytes = base64::prelude::BASE64_URL_SAFE_NO_PAD.decode(string)?; let mut data = Vec::new(); use flate2::bufread::GzDecoder; use std::io::Read; @@ -164,7 +165,7 @@ impl TryFrom<&List> for EncodedList { let mut e = GzEncoder::new(Vec::new(), Compression::default()); e.write_all(&list.0)?; let bytes = e.finish()?; - let string = base64::encode_config(bytes, base64::URL_SAFE_NO_PAD); + let string = base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(bytes); Ok(EncodedList(string)) } } diff --git a/crates/claims/src/lib.rs b/crates/claims/src/lib.rs index 23c206ccf..9fd4dc0d8 100644 --- a/crates/claims/src/lib.rs +++ b/crates/claims/src/lib.rs @@ -26,6 +26,11 @@ pub use jwt::JWTClaims; /// See: pub use ssi_sd_jwt as sd_jwt; +/// CBOR Object Signing and Encryption (COSE). +/// +/// See: +pub use ssi_cose as cose; + /// W3C Verifiable Credentials (VC). /// /// See: diff --git a/crates/crypto/Cargo.toml b/crates/crypto/Cargo.toml index d4e1bb981..a99756f78 100644 --- a/crates/crypto/Cargo.toml +++ b/crates/crypto/Cargo.toml @@ -9,26 +9,30 @@ repository = "https://github.com/spruceid/ssi/" documentation = "https://docs.rs/ssi-crypto/" [features] -default = ["secp256k1", "ripemd-160"] +default = ["secp256k1", "secp256r1", "ripemd-160"] +ed25519 = ["ed25519-dalek"] secp256k1 = ["k256", "getrandom", "keccak"] +secp256r1 = ["p256"] +secp384r1 = ["p384"] bbs = ["dep:bbs", "pairing-plus", "rand_old", "getrandom", "sha2_old", "hkdf"] ripemd-160 = ["ripemd160", "secp256k1"] keccak = ["keccak-hash"] ring = ["dep:ring"] - [dependencies] thiserror.workspace = true sha2 = { workspace = true } ring = { version = "0.16", optional = true } k256 = { workspace = true, optional = true, features = ["ecdsa"] } p256 = { workspace = true, optional = true, features = ["ecdsa"] } +p384 = { workspace = true, optional = true, features = ["ecdsa"] } hkdf = { version = "0.8", optional = true } +rand.workspace = true rand_old = { package = "rand", version = "0.7", optional = true } getrandom = { workspace = true, optional = true } # Required for wasm targets. sha2_old = { package = "sha2", version = "0.8", optional = true } keccak-hash = { version = "0.7", optional = true } -ed25519-dalek = { workspace = true, optional = true } +ed25519-dalek = { workspace = true, optional = true, features = ["rand_core"] } ripemd160 = { version = "0.9", optional = true } bbs = { version = "=0.4.1", optional = true } pairing-plus = { version = "=0.19.0", optional = true } diff --git a/crates/crypto/src/key.rs b/crates/crypto/src/key.rs new file mode 100644 index 000000000..38d1e6e02 --- /dev/null +++ b/crates/crypto/src/key.rs @@ -0,0 +1,182 @@ +use zeroize::ZeroizeOnDrop; + +use crate::{AlgorithmInstance, SignatureError, VerificationError}; + +#[derive(Debug, thiserror::Error)] +#[error("invalid public key")] +pub struct InvalidPublicKey; + +/// Public key. +#[non_exhaustive] +pub enum PublicKey { + #[cfg(feature = "ed25519")] + Ed25519(ed25519_dalek::VerifyingKey), + + #[cfg(feature = "secp256k1")] + Secp256k1(k256::PublicKey), + + #[cfg(feature = "secp256r1")] + P256(p256::PublicKey), + + #[cfg(feature = "secp384r1")] + P384(p384::PublicKey), +} + +impl PublicKey { + #[cfg(feature = "ed25519")] + pub fn new_ed25519(bytes: &[u8]) -> Result { + bytes + .try_into() + .map(Self::Ed25519) + .map_err(|_| InvalidPublicKey) + } + + #[cfg(feature = "secp256k1")] + pub fn new_secp256k1(x: &[u8], y: &[u8]) -> Result { + let mut bytes = Vec::new(); + bytes.push(0x04); + bytes.extend(x); + bytes.extend(y); + + k256::PublicKey::from_sec1_bytes(&bytes) + .map(Self::Secp256k1) + .map_err(|_| InvalidPublicKey) + } + + #[cfg(feature = "secp256r1")] + pub fn new_p256(x: &[u8], y: &[u8]) -> Result { + let mut bytes = Vec::new(); + bytes.push(0x04); + bytes.extend(x); + bytes.extend(y); + + p256::PublicKey::from_sec1_bytes(&bytes) + .map(Self::P256) + .map_err(|_| InvalidPublicKey) + } + + #[cfg(feature = "secp384r1")] + pub fn new_p384(x: &[u8], y: &[u8]) -> Result { + let mut bytes = Vec::new(); + bytes.push(0x04); + bytes.extend(x); + bytes.extend(y); + + p384::PublicKey::from_sec1_bytes(&bytes) + .map(Self::P384) + .map_err(|_| InvalidPublicKey) + } + + pub fn verify( + &self, + algorithm: AlgorithmInstance, + signing_bytes: &[u8], + signature_bytes: &[u8], + ) -> Result { + algorithm.verify(self, signing_bytes, signature_bytes) + } +} + +#[derive(Debug, thiserror::Error)] +#[error("invalid secret key")] +pub struct InvalidSecretKey; + +/// Secret key. +#[derive(ZeroizeOnDrop)] +#[non_exhaustive] +pub enum SecretKey { + #[cfg(feature = "ed25519")] + Ed25519(ed25519_dalek::SigningKey), + + #[cfg(feature = "secp256k1")] + Secp256k1(k256::SecretKey), + + #[cfg(feature = "secp256r1")] + P256(p256::SecretKey), + + #[cfg(feature = "secp384r1")] + P384(p384::SecretKey), +} + +impl SecretKey { + #[cfg(feature = "ed25519")] + pub fn new_ed25519(bytes: &[u8]) -> Result { + bytes + .try_into() + .map(Self::Ed25519) + .map_err(|_| InvalidSecretKey) + } + + #[cfg(feature = "ed25519")] + pub fn generate_ed25519() -> Self { + let mut rng = rand::rngs::OsRng {}; + Self::generate_ed25519_from(&mut rng) + } + + #[cfg(feature = "ed25519")] + pub fn generate_ed25519_from(rng: &mut (impl rand::CryptoRng + rand::RngCore)) -> Self { + Self::Ed25519(ed25519_dalek::SigningKey::generate(rng)) + } + + #[cfg(feature = "secp256k1")] + pub fn new_secp256k1(d: &[u8]) -> Result { + k256::SecretKey::from_bytes(d.into()) + .map(Self::Secp256k1) + .map_err(|_| InvalidSecretKey) + } + + #[cfg(feature = "secp256k1")] + pub fn generate_secp256k1() -> Self { + let mut rng = rand::rngs::OsRng {}; + Self::generate_secp256k1_from(&mut rng) + } + + #[cfg(feature = "secp256k1")] + pub fn generate_secp256k1_from(rng: &mut (impl rand::CryptoRng + rand::RngCore)) -> Self { + Self::Secp256k1(k256::SecretKey::random(rng)) + } + + #[cfg(feature = "secp256r1")] + pub fn new_p256(d: &[u8]) -> Result { + p256::SecretKey::from_bytes(d.into()) + .map(Self::P256) + .map_err(|_| InvalidSecretKey) + } + + #[cfg(feature = "secp256r1")] + pub fn generate_p256() -> Self { + let mut rng = rand::rngs::OsRng {}; + Self::generate_p256_from(&mut rng) + } + + #[cfg(feature = "secp256r1")] + pub fn generate_p256_from(rng: &mut (impl rand::CryptoRng + rand::RngCore)) -> Self { + Self::P256(p256::SecretKey::random(rng)) + } + + #[cfg(feature = "secp384r1")] + pub fn new_p384(d: &[u8]) -> Result { + p384::SecretKey::from_bytes(d.into()) + .map(Self::P384) + .map_err(|_| InvalidSecretKey) + } + + #[cfg(feature = "secp384r1")] + pub fn generate_p384() -> Self { + let mut rng = rand::rngs::OsRng {}; + Self::generate_p384_from(&mut rng) + } + + #[cfg(feature = "secp384r1")] + pub fn generate_p384_from(rng: &mut (impl rand::CryptoRng + rand::RngCore)) -> Self { + Self::P384(p384::SecretKey::random(rng)) + } + + pub fn sign( + &self, + algorithm: AlgorithmInstance, + signing_bytes: &[u8], + ) -> Result, SignatureError> { + algorithm.sign(self, signing_bytes) + } +} diff --git a/crates/crypto/src/lib.rs b/crates/crypto/src/lib.rs index e4ba73857..ee03b877d 100644 --- a/crates/crypto/src/lib.rs +++ b/crates/crypto/src/lib.rs @@ -1,7 +1,23 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#[cfg(feature = "ed25519")] +pub use ed25519_dalek as ed25519; +#[cfg(feature = "secp256k1")] +pub use k256; +#[cfg(feature = "secp256r1")] +pub use p256; +#[cfg(feature = "secp384r1")] +pub use p384; +pub use rand; + pub mod algorithm; pub mod hashes; +pub mod key; +mod signature; pub mod signatures; +mod verification; pub use algorithm::{Algorithm, AlgorithmError, AlgorithmInstance, UnsupportedAlgorithm}; +pub use key::{PublicKey, SecretKey}; +pub use signature::*; +pub use verification::*; diff --git a/crates/crypto/src/signature.rs b/crates/crypto/src/signature.rs new file mode 100644 index 000000000..a6424786b --- /dev/null +++ b/crates/crypto/src/signature.rs @@ -0,0 +1,67 @@ +use crate::{Algorithm, AlgorithmInstance, SecretKey}; + +#[derive(Debug, thiserror::Error)] +pub enum SignatureError { + #[error("unsupported algorithm `{0}`")] + UnsupportedAlgorithm(Algorithm), + + #[error("secret key is not compatible with the signature algorithm")] + IncompatibleKey, +} + +impl AlgorithmInstance { + #[allow(unused)] + pub fn sign(&self, key: &SecretKey, signing_bytes: &[u8]) -> Result, SignatureError> { + match self { + #[cfg(feature = "ed25519")] + Self::EdDSA => match key { + SecretKey::Ed25519(key) => { + use ed25519_dalek::Signer; + Ok(key.sign(signing_bytes).to_bytes().to_vec()) + } + #[allow(unreachable_patterns)] + _ => Err(SignatureError::IncompatibleKey), + }, + #[cfg(feature = "secp256k1")] + Self::ES256K => { + match key { + SecretKey::Secp256k1(key) => { + use k256::ecdsa::{signature::Signer, Signature}; + let signing_key = k256::ecdsa::SigningKey::from(key); + let signature: Signature = signing_key.try_sign(signing_bytes).unwrap(); // Uses SHA-256 by default. + Ok(signature.to_bytes().to_vec()) + } + #[allow(unreachable_patterns)] + _ => Err(SignatureError::IncompatibleKey), + } + } + #[cfg(feature = "secp256r1")] + Self::ES256 => { + match key { + SecretKey::P256(key) => { + use p256::ecdsa::{signature::Signer, Signature}; + let signing_key = p256::ecdsa::SigningKey::from(key); + let signature: Signature = signing_key.try_sign(signing_bytes).unwrap(); // Uses SHA-256 by default. + Ok(signature.to_bytes().to_vec()) + } + #[allow(unreachable_patterns)] + _ => Err(SignatureError::IncompatibleKey), + } + } + #[cfg(feature = "secp384r1")] + Self::ES384 => { + match key { + SecretKey::P384(key) => { + use p384::ecdsa::{signature::Signer, Signature}; + let signing_key = p384::ecdsa::SigningKey::from(key); + let signature: Signature = signing_key.try_sign(signing_bytes).unwrap(); // Uses SHA-384 by default. + Ok(signature.to_bytes().to_vec()) + } + #[allow(unreachable_patterns)] + _ => Err(SignatureError::IncompatibleKey), + } + } + other => Err(SignatureError::UnsupportedAlgorithm(other.algorithm())), + } + } +} diff --git a/crates/crypto/src/verification.rs b/crates/crypto/src/verification.rs new file mode 100644 index 000000000..614dc42d4 --- /dev/null +++ b/crates/crypto/src/verification.rs @@ -0,0 +1,75 @@ +use crate::{Algorithm, AlgorithmInstance, PublicKey}; + +#[derive(Debug, thiserror::Error)] +pub enum VerificationError { + #[error("unsupported algorithm `{0}`")] + UnsupportedAlgorithm(Algorithm), + + #[error("secret key is not compatible with the signature algorithm")] + IncompatibleKey, + + #[error("invalid signature")] + InvalidSignature, +} + +impl AlgorithmInstance { + #[allow(unused)] + pub fn verify( + &self, + key: &PublicKey, + signing_bytes: &[u8], + signature_bytes: &[u8], + ) -> Result { + match self { + #[cfg(feature = "ed25519")] + Self::EdDSA => match key { + PublicKey::Ed25519(key) => { + use ed25519_dalek::Verifier; + let signature: ed25519_dalek::Signature = signature_bytes + .try_into() + .map_err(|_| VerificationError::InvalidSignature)?; + Ok(key.verify(signing_bytes, &signature).is_ok()) + } + #[allow(unreachable_patterns)] + _ => Err(VerificationError::IncompatibleKey), + }, + #[cfg(feature = "secp256k1")] + Self::ES256K => match key { + PublicKey::Secp256k1(key) => { + use k256::ecdsa::signature::Verifier; + let verifying_key = k256::ecdsa::VerifyingKey::from(key); + let sig = k256::ecdsa::Signature::try_from(signature_bytes) + .map_err(|_| VerificationError::InvalidSignature)?; + Ok(verifying_key.verify(signing_bytes, &sig).is_ok()) + } + #[allow(unreachable_patterns)] + _ => Err(VerificationError::IncompatibleKey), + }, + #[cfg(feature = "secp256r1")] + Self::ES256 => match key { + PublicKey::P256(key) => { + use p256::ecdsa::signature::Verifier; + let verifying_key = p256::ecdsa::VerifyingKey::from(key); + let sig = p256::ecdsa::Signature::try_from(signature_bytes) + .map_err(|_| VerificationError::InvalidSignature)?; + Ok(verifying_key.verify(signing_bytes, &sig).is_ok()) + } + #[allow(unreachable_patterns)] + _ => Err(VerificationError::IncompatibleKey), + }, + #[cfg(feature = "secp384r1")] + Self::ES384 => match key { + PublicKey::P384(key) => { + use p384::ecdsa::signature::Verifier; + let verifying_key = p384::ecdsa::VerifyingKey::from(key); + let sig = p384::ecdsa::Signature::try_from(signature_bytes) + .map_err(|_| VerificationError::InvalidSignature)?; + Ok(verifying_key.verify(signing_bytes, &sig).is_ok()) + } + #[allow(unreachable_patterns)] + _ => Err(VerificationError::IncompatibleKey), + }, + other => Err(VerificationError::UnsupportedAlgorithm(other.algorithm())), + } + } +} diff --git a/crates/dids/methods/ion/src/sidetree/mod.rs b/crates/dids/methods/ion/src/sidetree/mod.rs index 351f27777..502443751 100644 --- a/crates/dids/methods/ion/src/sidetree/mod.rs +++ b/crates/dids/methods/ion/src/sidetree/mod.rs @@ -1,6 +1,7 @@ use core::fmt; use std::borrow::Cow; +use base64::Engine; use json_patch::Patch; use serde::{Deserialize, Serialize}; use ssi_dids_core::{ @@ -197,7 +198,7 @@ pub trait Sidetree { /// [`DATA_ENCODING_SCHEME`](https://identity.foundation/sidetree/spec/v1.0.0/#data-encoding-scheme) fn data_encoding_scheme(data: &[u8]) -> String { - base64::encode_config(data, base64::URL_SAFE_NO_PAD) + base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(data) } /// Generate a new keypair ([KEY_ALGORITHM][ka]) @@ -522,7 +523,8 @@ pub trait Sidetree { /// Check that a DID Suffix looks valid fn validate_did_suffix(suffix: &DIDSuffix) -> Result<(), InvalidSidetreeDIDSuffix> { - let bytes = base64::decode_config(&suffix.0, base64::URL_SAFE_NO_PAD) + let bytes = base64::prelude::BASE64_URL_SAFE_NO_PAD + .decode(&suffix.0) .map_err(|_| InvalidSidetreeDIDSuffix::Base64)?; if bytes.len() != 34 { @@ -827,7 +829,7 @@ impl TryFrom for JWK { } fn b64len(s: &str) -> usize { - base64::encode_config(s, base64::URL_SAFE_NO_PAD).len() + base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(s).len() } impl DIDStatePatch { diff --git a/crates/jwk/src/der.rs b/crates/jwk/src/der.rs index 3d285aaf8..dfba16ba7 100644 --- a/crates/jwk/src/der.rs +++ b/crates/jwk/src/der.rs @@ -261,6 +261,8 @@ impl ToASN1 for OtherPrimeInfo { #[cfg(test)] mod tests { + use base64::Engine; + use super::*; #[test] @@ -293,7 +295,9 @@ mod tests { ]), }; let expected_b64 = "MC4CAQAwBQYDK2VwBCIEINTuctv5E1hK1bbY8fdp+K06/nwoy/HU++CXqI9EdVhC"; - let expected_key = base64::decode(expected_b64).unwrap(); + let expected_key = base64::prelude::BASE64_STANDARD + .decode(expected_b64) + .unwrap(); let key_der = der_encode(&key).unwrap(); assert_eq!(key_der, expected_key); } diff --git a/crates/jwk/src/lib.rs b/crates/jwk/src/lib.rs index 7e3323f75..fb087a315 100644 --- a/crates/jwk/src/lib.rs +++ b/crates/jwk/src/lib.rs @@ -1,5 +1,6 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] +use base64::Engine; use core::fmt; use num_bigint::{BigInt, Sign}; use simple_asn1::{ASN1Block, ASN1Class, ToASN1}; @@ -1284,19 +1285,23 @@ impl From<&p384::SecretKey> for ECParams { } } +const BASE64_URL_SAFE_INDIFFERENT_PAD: base64::engine::GeneralPurpose = + base64::engine::GeneralPurpose::new( + &base64::alphabet::URL_SAFE, + base64::engine::GeneralPurposeConfig::new() + .with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent), + ); + impl TryFrom for Base64urlUInt { type Error = base64::DecodeError; fn try_from(data: String) -> Result { - Ok(Base64urlUInt(base64::decode_config( - data, - base64::URL_SAFE, - )?)) + Ok(Base64urlUInt(BASE64_URL_SAFE_INDIFFERENT_PAD.decode(data)?)) } } impl From<&Base64urlUInt> for String { fn from(data: &Base64urlUInt) -> String { - base64::encode_config(&data.0, base64::URL_SAFE_NO_PAD) + base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(&data.0) } } diff --git a/crates/status/src/impl/token_status_list/json.rs b/crates/status/src/impl/token_status_list/json.rs index 8bc471277..06c361f59 100644 --- a/crates/status/src/impl/token_status_list/json.rs +++ b/crates/status/src/impl/token_status_list/json.rs @@ -1,6 +1,7 @@ //! JWT encoding of a Status Lists. use std::borrow::Cow; +use base64::prelude::{Engine, BASE64_URL_SAFE}; use flate2::Compression; use iref::UriBuf; use serde::{Deserialize, Serialize}; @@ -185,12 +186,12 @@ impl JsonStatusList { let bytes = bit_string.to_compressed_bytes(compression); Self { bits: bit_string.status_size(), - lst: base64::encode_config(bytes, base64::URL_SAFE), + lst: BASE64_URL_SAFE.encode(bytes), } } pub fn decode(&self, limit: Option) -> Result { - let bytes = base64::decode_config(&self.lst, base64::URL_SAFE)?; + let bytes = BASE64_URL_SAFE.decode(&self.lst)?; Ok(BitString::from_compressed_bytes(self.bits, &bytes, limit)?) } } diff --git a/crates/ucan/src/lib.rs b/crates/ucan/src/lib.rs index 856389b06..32ecdbab9 100644 --- a/crates/ucan/src/lib.rs +++ b/crates/ucan/src/lib.rs @@ -1,4 +1,5 @@ pub mod error; +use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine}; pub use error::Error; use iref::UriBuf; use libipld::{ @@ -157,12 +158,10 @@ impl Ucan { Ok(match &self.codec { UcanCodec::Raw(r) => r.clone(), UcanCodec::DagJson => [ - base64::encode_config( - DagJsonCodec.encode(&to_ipld(&self.header).map_err(IpldError::new)?)?, - base64::URL_SAFE_NO_PAD, - ), - base64::encode_config(DagJsonCodec.encode(&self.payload)?, base64::URL_SAFE_NO_PAD), - base64::encode_config(&self.signature, base64::URL_SAFE_NO_PAD), + BASE64_URL_SAFE_NO_PAD + .encode(DagJsonCodec.encode(&to_ipld(&self.header).map_err(IpldError::new)?)?), + BASE64_URL_SAFE_NO_PAD.encode(DagJsonCodec.encode(&self.payload)?), + BASE64_URL_SAFE_NO_PAD.encode(&self.signature), ] .join("."), }) @@ -304,11 +303,9 @@ impl Payload { let signature = sign_bytes( algorithm, [ - base64::encode_config( - DagJsonCodec.encode(&to_ipld(&header).map_err(IpldError::new)?)?, - base64::URL_SAFE_NO_PAD, - ), - base64::encode_config(DagJsonCodec.encode(&self)?, base64::URL_SAFE_NO_PAD), + BASE64_URL_SAFE_NO_PAD + .encode(DagJsonCodec.encode(&to_ipld(&header).map_err(IpldError::new)?)?), + BASE64_URL_SAFE_NO_PAD.encode(DagJsonCodec.encode(&self)?), ] .join(".") .as_bytes(),