diff --git a/README.md b/README.md index caa824b18..771fc6768 100644 --- a/README.md +++ b/README.md @@ -45,16 +45,18 @@ with JSON Web Signatures (or Tokens) and Verifiable Credentials. The simplest type of claim to load and verify is probably JSON Web Signatures (JWSs), often use to encode JSON Web Tokens (JWTs). To represent -such claims SSI provides the `CompactJWSString` type representing a JWS -in compact textual form. One can load a JWS using `from_string` and verify -it using `verify`. +such claims SSI provides the `JwsBuf` type representing a JWS +in compact textual form. One can load a JWS using [`new`] and verify +it using [`verify`]. +[`new`]: claims::JwsBuf::new +[`verify`]: claims::JwsSlice::verify ```rust use ssi::prelude::*; // Load a JWT from the file system. -let jwt = CompactJWSString::from_string( +let jwt = JwsBuf::new( std::fs::read_to_string("examples/files/claims.jwt") .expect("unable to load JWT") ).expect("invalid JWS"); @@ -78,10 +80,11 @@ Verifiable Credential are much more complex as they require interpreting the input claims and proofs, such as Data-Integrity proofs as Linked-Data using JSON-LD. This operation is highly configurable. SSI provide functions exposing various levels of implementation details that you can -tweak as needed. The simplest of them is `any_credential_from_json_str` +tweak as needed. The simplest of them is [`any_credential_from_json_str`] that will simply load a VC from a string, assuming it is signed using any Data-Integrity proof supported by SSI. +[`any_credential_from_json_str`]: claims::vc::v1::data_integrity::any_credential_from_json_str ```rust use ssi::prelude::*; @@ -155,10 +158,11 @@ println!("{jwt}") #### Verifiable Credential We can use a similar technique to sign a VC with custom claims. -The `SpecializedJsonCredential` type provides a customizable +The [`SpecializedJsonCredential`] type provides a customizable implementation of the VC data-model 1.1 where you can set the credential type yourself. +[`SpecializedJsonCredential`]: claims::vc::v1::SpecializedJsonCredential ```rust use static_iref::uri; @@ -217,16 +221,19 @@ It is critical that custom claims can be interpreted as Linked-Data. In the above example this is done by specifying a serialization URL for each field of `MyCredentialSubject`. This can also be done by creating a custom JSON-LD context and embed it to `credential` using either -`SpecializedJsonCredential`'s `context` field or leveraging its context type +[`SpecializedJsonCredential`]'s [`context`] field or leveraging its context type parameter. +[`context`]: claims::vc::v1::SpecializedJsonCredential::context ## Data-Models The examples above are using the VC data-model 1.1, but you ssi also has support for: -- `VC data-model 2.0` -- `A wrapper type to accept both` +- [`VC data-model 2.0`] +- [`A wrapper type to accept both`] +[`VC data-model 2.0`]: claims::vc::v2 +[`A wrapper type to accept both`]: claims::vc::syntax::AnySpecializedJsonCredential ## Features diff --git a/crates/claims/core/src/verification/proof.rs b/crates/claims/core/src/verification/proof.rs index 80842cbbb..43ab951af 100644 --- a/crates/claims/core/src/verification/proof.rs +++ b/crates/claims/core/src/verification/proof.rs @@ -75,6 +75,10 @@ pub enum ProofValidationError { } impl ProofValidationError { + pub fn input_data(e: impl ToString) -> Self { + Self::InvalidInputData(e.to_string()) + } + pub fn other(e: impl ToString) -> Self { Self::Other(e.to_string()) } diff --git a/crates/claims/crates/data-integrity/core/src/signing/jws.rs b/crates/claims/crates/data-integrity/core/src/signing/jws.rs index 40dc5defa..6118a1b44 100644 --- a/crates/claims/crates/data-integrity/core/src/signing/jws.rs +++ b/crates/claims/crates/data-integrity/core/src/signing/jws.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, marker::PhantomData}; use ssi_claims_core::{ProofValidationError, SignatureError}; use ssi_crypto::algorithm::{SignatureAlgorithmInstance, SignatureAlgorithmType}; use ssi_jwk::{Algorithm, JWK}; -use ssi_jws::{CompactJWSString, JWSSignature, JWS}; +use ssi_jws::{DecodedJws, JwsSignature, JwsString}; use ssi_verification_methods::{MessageSigner, VerifyBytes, VerifyBytesWithRecoveryJwk}; use crate::{ @@ -24,13 +24,13 @@ use super::AlgorithmSelection; linked_data::Deserialize, )] #[ld(prefix("sec" = "https://w3id.org/security#"))] -pub struct JwsSignature { +pub struct DetachedJwsSignature { #[ld("sec:jws")] - pub jws: CompactJWSString, + pub jws: JwsString, } -impl JwsSignature { - pub fn new(jws: CompactJWSString) -> Self { +impl DetachedJwsSignature { + pub fn new(jws: JwsString) -> Self { Self { jws } } @@ -40,15 +40,20 @@ impl JwsSignature { pub fn decode( &self, message: &[u8], - ) -> Result<(Vec, JWSSignature, Algorithm), ProofValidationError> { - let JWS { - header, signature, .. + ) -> Result<(Vec, JwsSignature, Algorithm), ProofValidationError> { + let DecodedJws { + signing_bytes: detached_signing_bytes, + signature, } = self .jws .decode() .map_err(|_| ProofValidationError::InvalidSignature)?; - let signing_bytes = header.encode_signing_bytes(message); - Ok((signing_bytes, signature, header.algorithm)) + let signing_bytes = detached_signing_bytes.header.encode_signing_bytes(message); + Ok(( + signing_bytes, + signature, + detached_signing_bytes.header.algorithm, + )) } pub async fn sign_detached, S: MessageSigner>( @@ -60,27 +65,27 @@ impl JwsSignature { let header = ssi_jws::Header::new_unencoded(algorithm_instance.algorithm().into(), key_id); let signing_bytes = header.encode_signing_bytes(payload); let signature = signer.sign(algorithm_instance, &signing_bytes).await?; - let jws = ssi_jws::CompactJWSString::encode_detached(header, &signature); - Ok(JwsSignature::new(jws)) + let jws = ssi_jws::JwsString::encode_detached(header, &signature); + Ok(Self::new(jws)) } } -impl AsRef for JwsSignature { +impl AsRef for DetachedJwsSignature { fn as_ref(&self) -> &str { self.jws.as_str() } } -impl super::AlterSignature for JwsSignature { +impl super::AlterSignature for DetachedJwsSignature { fn alter(&mut self) { - self.jws = CompactJWSString::from_string(format!("ff{}", self.jws)).unwrap(); + self.jws = JwsString::from_string(format!("ff{}", self.jws)).unwrap(); } } pub struct DetachedJwsSigning(PhantomData); impl SignatureAndVerificationAlgorithm for DetachedJwsSigning { - type Signature = JwsSignature; + type Signature = DetachedJwsSignature; } impl SignatureAlgorithm for DetachedJwsSigning @@ -98,7 +103,7 @@ where prepared_claims: S::PreparedClaims, proof_configuration: ProofConfigurationRef<'_, S>, ) -> Result { - JwsSignature::sign_detached( + DetachedJwsSignature::sign_detached( prepared_claims.as_ref(), signer, None, @@ -110,7 +115,7 @@ where impl VerificationAlgorithm for DetachedJwsSigning where - S: CryptographicSuite, + S: CryptographicSuite, S::PreparedClaims: AsRef<[u8]>, S::VerificationMethod: VerifyBytes, A: TryFrom, @@ -120,17 +125,21 @@ where prepared_claims: S::PreparedClaims, proof: ProofRef, ) -> Result { - let JWS { - header, signature, .. + let DecodedJws { + signing_bytes: detached_signing_bytes, + signature, } = proof .signature .jws .decode() .map_err(|_| ProofValidationError::InvalidSignature)?; - let signing_bytes = header.encode_signing_bytes(prepared_claims.as_ref()); + let signing_bytes = detached_signing_bytes + .header + .encode_signing_bytes(prepared_claims.as_ref()); - let algorithm = header + let algorithm = detached_signing_bytes + .header .algorithm .try_into() .map_err(|_| ProofValidationError::InvalidSignature)?; @@ -142,7 +151,7 @@ where pub struct DetachedJwsRecoverySigning(PhantomData); impl SignatureAndVerificationAlgorithm for DetachedJwsRecoverySigning { - type Signature = JwsSignature; + type Signature = DetachedJwsSignature; } impl SignatureAlgorithm for DetachedJwsRecoverySigning @@ -159,7 +168,7 @@ where prepared_claims: S::PreparedClaims, proof_configuration: ProofConfigurationRef<'_, S>, ) -> Result { - JwsSignature::sign_detached( + DetachedJwsSignature::sign_detached( prepared_claims.as_ref(), signer, proof_configuration.options.public_jwk().key_id.clone(), @@ -171,7 +180,7 @@ where impl VerificationAlgorithm for DetachedJwsRecoverySigning where - S: CryptographicSuite, + S: CryptographicSuite, S::PreparedClaims: AsRef<[u8]>, S::ProofOptions: RecoverPublicJwk, S::VerificationMethod: VerifyBytesWithRecoveryJwk, @@ -182,17 +191,21 @@ where prepared_claims: S::PreparedClaims, proof: ProofRef, ) -> Result { - let JWS { - header, signature, .. + let DecodedJws { + signing_bytes: detached_signing_bytes, + signature, } = proof .signature .jws .decode() .map_err(|_| ProofValidationError::InvalidSignature)?; - let signing_bytes = header.encode_signing_bytes(prepared_claims.as_ref()); + let signing_bytes = detached_signing_bytes + .header + .encode_signing_bytes(prepared_claims.as_ref()); - let found_algorithm = header + let found_algorithm = detached_signing_bytes + .header .algorithm .try_into() .map_err(|_| ProofValidationError::InvalidSignature)?; diff --git a/crates/claims/crates/data-integrity/sd-primitives/Cargo.toml b/crates/claims/crates/data-integrity/sd-primitives/Cargo.toml index 29d62a51f..8aa1f51ef 100644 --- a/crates/claims/crates/data-integrity/sd-primitives/Cargo.toml +++ b/crates/claims/crates/data-integrity/sd-primitives/Cargo.toml @@ -9,6 +9,7 @@ repository = "https://github.com/spruceid/ssi/" documentation = "https://docs.rs/ssi-di-sd-primitives/" [dependencies] +ssi-core.workspace = true ssi-rdf.workspace = true ssi-json-ld.workspace = true linked-data.workspace = true diff --git a/crates/claims/crates/data-integrity/sd-primitives/src/group.rs b/crates/claims/crates/data-integrity/sd-primitives/src/group.rs index 58e6552cf..7959a26fa 100644 --- a/crates/claims/crates/data-integrity/sd-primitives/src/group.rs +++ b/crates/claims/crates/data-integrity/sd-primitives/src/group.rs @@ -6,6 +6,7 @@ use std::{ use linked_data::IntoQuadsError; use rdf_types::{BlankIdBuf, LexicalQuad}; +use ssi_core::JsonPointerBuf; use ssi_json_ld::{Expandable, ExpandedDocument, JsonLdObject}; use ssi_rdf::{urdna2015::NormalizingSubstitution, LexicalInterpretation}; @@ -13,7 +14,6 @@ use crate::{ canonicalize::label_replacement_canonicalize_nquads, select::{select_canonical_nquads, SelectError}, skolemize::{expanded_to_deskolemized_nquads, SkolemError, Skolemize}, - JsonPointerBuf, }; #[derive(Debug, thiserror::Error)] @@ -126,10 +126,11 @@ mod tests { use hmac::{Hmac, Mac}; use lazy_static::lazy_static; + use ssi_core::JsonPointerBuf; use ssi_json_ld::CompactJsonLd; use ssi_rdf::IntoNQuads; - use crate::{canonicalize::create_hmac_id_label_map_function, HmacShaAny, JsonPointerBuf}; + use crate::{canonicalize::create_hmac_id_label_map_function, HmacShaAny}; use super::canonicalize_and_group; diff --git a/crates/claims/crates/data-integrity/sd-primitives/src/lib.rs b/crates/claims/crates/data-integrity/sd-primitives/src/lib.rs index 41013827a..8031dac89 100644 --- a/crates/claims/crates/data-integrity/sd-primitives/src/lib.rs +++ b/crates/claims/crates/data-integrity/sd-primitives/src/lib.rs @@ -6,6 +6,8 @@ use hmac::Mac; use serde::{Deserialize, Serialize}; use sha2::{Sha256, Sha384}; +pub use ssi_core::{JsonPointer, JsonPointerBuf}; + pub type HmacSha256 = Hmac; pub type HmacSha384 = Hmac; @@ -228,8 +230,5 @@ impl<'de> Deserialize<'de> for HmacShaAnyKey { pub mod canonicalize; pub mod group; -pub mod json_pointer; pub mod select; pub mod skolemize; - -pub use json_pointer::{JsonPointer, JsonPointerBuf}; diff --git a/crates/claims/crates/data-integrity/sd-primitives/src/select.rs b/crates/claims/crates/data-integrity/sd-primitives/src/select.rs index 4af4df26a..dd98a123a 100644 --- a/crates/claims/crates/data-integrity/sd-primitives/src/select.rs +++ b/crates/claims/crates/data-integrity/sd-primitives/src/select.rs @@ -1,12 +1,12 @@ use std::collections::{BTreeMap, HashMap}; use rdf_types::{BlankId, BlankIdBuf, LexicalQuad}; +use ssi_core::{JsonPointer, JsonPointerBuf}; use ssi_json_ld::syntax::Value; use crate::{ canonicalize::relabel_quads, skolemize::{compact_to_deskolemized_nquads, SkolemError}, - JsonPointer, JsonPointerBuf, }; #[derive(Debug, thiserror::Error)] @@ -276,7 +276,7 @@ impl Select for ssi_json_ld::syntax::Object { ) -> Result<(), DanglingJsonPointer> { match pointer.split_first() { Some((token, rest)) => { - let key = token.to_str(); + let key = token.to_decoded(); let a_item = self.get(key.as_ref()).next().ok_or(DanglingJsonPointer)?; let b_item = selection.get_mut_or_insert_with(&key, || create_initial_selection(a_item)); diff --git a/crates/claims/crates/data-integrity/src/any/sd.rs b/crates/claims/crates/data-integrity/src/any/sd.rs index e074f91bc..593a7430d 100644 --- a/crates/claims/crates/data-integrity/src/any/sd.rs +++ b/crates/claims/crates/data-integrity/src/any/sd.rs @@ -1,10 +1,10 @@ use serde::{Deserialize, Serialize}; use ssi_claims_core::ResolverProvider; +use ssi_core::JsonPointerBuf; use ssi_data_integrity_core::{ suite::{CryptographicSuiteSelect, SelectionError, SelectiveCryptographicSuite}, DataIntegrity, ProofRef, }; -use ssi_di_sd_primitives::JsonPointerBuf; use ssi_json_ld::{Expandable, ExpandedDocument, JsonLdLoaderProvider, JsonLdNodeObject}; use ssi_rdf::LexicalInterpretation; use ssi_verification_methods::{AnyMethod, VerificationMethodResolver}; diff --git a/crates/claims/crates/data-integrity/src/any/signature_options.rs b/crates/claims/crates/data-integrity/src/any/signature_options.rs index 163bb9924..0c6d49800 100644 --- a/crates/claims/crates/data-integrity/src/any/signature_options.rs +++ b/crates/claims/crates/data-integrity/src/any/signature_options.rs @@ -1,5 +1,6 @@ use serde::Deserialize; -use ssi_di_sd_primitives::{HmacShaAnyKey, JsonPointerBuf}; +use ssi_core::JsonPointerBuf; +use ssi_di_sd_primitives::HmacShaAnyKey; use ssi_verification_methods::multikey::MultikeyPair; #[derive(Debug, Default, Deserialize)] diff --git a/crates/claims/crates/data-integrity/src/lib.rs b/crates/claims/crates/data-integrity/src/lib.rs index d4596d263..2681c6021 100644 --- a/crates/claims/crates/data-integrity/src/lib.rs +++ b/crates/claims/crates/data-integrity/src/lib.rs @@ -1,8 +1,7 @@ +pub use ssi_core::{JsonPointer, JsonPointerBuf}; pub use ssi_data_integrity_core::*; pub use ssi_data_integrity_suites as suites; -pub use ssi_di_sd_primitives::{JsonPointer, JsonPointerBuf}; - mod any; pub use any::*; diff --git a/crates/claims/crates/data-integrity/suites/src/suites/w3c/ecdsa_sd_2023/configuration.rs b/crates/claims/crates/data-integrity/suites/src/suites/w3c/ecdsa_sd_2023/configuration.rs index a03ed173b..cd6b8403d 100644 --- a/crates/claims/crates/data-integrity/suites/src/suites/w3c/ecdsa_sd_2023/configuration.rs +++ b/crates/claims/crates/data-integrity/suites/src/suites/w3c/ecdsa_sd_2023/configuration.rs @@ -1,8 +1,9 @@ +use ssi_core::JsonPointerBuf; use ssi_data_integrity_core::{ suite::{ConfigurationError, InputSignatureOptions, InputVerificationOptions}, ProofConfiguration, ProofOptions, }; -use ssi_di_sd_primitives::{HmacShaAnyKey, JsonPointerBuf}; +use ssi_di_sd_primitives::HmacShaAnyKey; use ssi_verification_methods::{multikey::MultikeyPair, Multikey}; use crate::EcdsaSd2023; diff --git a/crates/claims/crates/data-integrity/suites/src/suites/w3c/ecdsa_sd_2023/derive.rs b/crates/claims/crates/data-integrity/suites/src/suites/w3c/ecdsa_sd_2023/derive.rs index 13442c2ee..e00cd9a33 100644 --- a/crates/claims/crates/data-integrity/suites/src/suites/w3c/ecdsa_sd_2023/derive.rs +++ b/crates/claims/crates/data-integrity/suites/src/suites/w3c/ecdsa_sd_2023/derive.rs @@ -2,12 +2,12 @@ use std::{borrow::Cow, collections::HashMap}; use rdf_types::{BlankIdBuf, Quad}; use serde::Serialize; +use ssi_core::JsonPointerBuf; use ssi_data_integrity_core::{DataIntegrity, Proof, ProofRef}; use ssi_di_sd_primitives::{ canonicalize::create_hmac_id_label_map_function, group::{canonicalize_and_group, GroupError}, select::{select_json_ld, DanglingJsonPointer}, - JsonPointerBuf, }; use ssi_json_ld::{Expandable, ExpandedDocument, JsonLdNodeObject}; use ssi_multicodec::MultiEncodedBuf; diff --git a/crates/claims/crates/data-integrity/suites/src/suites/w3c/ecdsa_sd_2023/signature/base.rs b/crates/claims/crates/data-integrity/suites/src/suites/w3c/ecdsa_sd_2023/signature/base.rs index 99b9a079b..e2db0d490 100644 --- a/crates/claims/crates/data-integrity/suites/src/suites/w3c/ecdsa_sd_2023/signature/base.rs +++ b/crates/claims/crates/data-integrity/suites/src/suites/w3c/ecdsa_sd_2023/signature/base.rs @@ -1,7 +1,8 @@ use multibase::Base; use ssi_claims_core::SignatureError; +use ssi_core::JsonPointerBuf; use ssi_crypto::algorithm::ES256OrES384; -use ssi_di_sd_primitives::{HmacShaAnyKey, JsonPointerBuf, ShaAny, ShaAnyBytes}; +use ssi_di_sd_primitives::{HmacShaAnyKey, ShaAny, ShaAnyBytes}; use ssi_multicodec::{MultiEncoded, MultiEncodedBuf}; use ssi_rdf::IntoNQuads; use ssi_security::MultibaseBuf; diff --git a/crates/claims/crates/jws/src/compact/bytes.rs b/crates/claims/crates/jws/src/compact/bytes.rs index e52731afa..2a5737141 100644 --- a/crates/claims/crates/jws/src/compact/bytes.rs +++ b/crates/claims/crates/jws/src/compact/bytes.rs @@ -1,24 +1,39 @@ -use crate::{DecodeError, DecodedJWS, DecodedSigningBytes, Header, InvalidHeader, JWS}; +use crate::{ + utils::is_url_safe_base64_char, DecodeError, DecodedJws, DecodedSigningBytes, Header, + InvalidHeader, InvalidJws, JwsBuf, JwsSignature, JwsString, +}; 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}; -/// JWS in compact serialized form. +/// Borrowed JWS without any encoding guaranties. +/// +/// This is an unsized type borrowing the JWS and meant to be referenced as +/// `&JwsSlice`, just like `&[u8]`. +/// Use [`JwsVec`] if you need to own the JWS. +/// +/// This type is similar to the [`Jws`](crate::Jws) type. +/// However contrarily to `Jws`, there is no guarantee that the JWS is a valid +/// UTF-8 string (and even less URL-safe). +/// +/// Use [`JwsStr`](crate::JwsStr) if you expect UTF-8 encoded JWSs. +/// Use [`Jws`](crate::Jws) if you expect URL-safe JWSs. +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(transparent)] -pub struct CompactJWS([u8]); +pub struct JwsSlice([u8]); -impl CompactJWS { - pub fn new(data: &T) -> Result<&Self, InvalidCompactJWS<&T>> +impl JwsSlice { + pub fn new(data: &T) -> Result<&Self, InvalidJws<&T>> where T: ?Sized + AsRef<[u8]>, { let bytes = data.as_ref(); - if Self::check(bytes) { + if Self::validate(bytes) { Ok(unsafe { Self::new_unchecked(bytes) }) } else { - Err(InvalidCompactJWS(data)) + Err(InvalidJws(data)) } } @@ -31,44 +46,80 @@ impl CompactJWS { std::mem::transmute(data) } - pub fn check(data: &[u8]) -> bool { - enum State { - Header, - Payload, - Signature, + pub const fn validate(bytes: &[u8]) -> bool { + Self::validate_range(bytes, 0, bytes.len()) + } + + pub const fn validate_range(bytes: &[u8], mut i: usize, end: usize) -> bool { + let mut j = if end > bytes.len() { bytes.len() } else { end }; + + // Header. + loop { + if i >= j { + // Missing `.` + return false; + } + + if bytes[i] == b'.' { + break; + } + + if !is_url_safe_base64_char(bytes[i]) { + return false; + } + + i += 1 } - let mut state = State::Header; - - for &b in data { - match state { - State::Header => match b { - b'.' => state = State::Payload, - b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'+' | b'/' => (), - _ => return false, - }, - State::Payload => { - if b == b'.' { - state = State::Signature - } - } - State::Signature => (), + // Signature. + if i >= j { + return false; + } + j -= 1; + loop { + if i >= j { + // Missing `.` + return false; } + + if bytes[j] == b'.' { + break; + } + + if !is_url_safe_base64_char(bytes[j]) { + return false; + } + + j -= 1 } - matches!(state, State::Signature) + true } - pub fn check_signing_bytes(data: &[u8]) -> bool { - for &b in data { - match b { - b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'+' | b'/' => (), - b'.' => return true, - _ => return false, + pub fn check_signing_bytes(bytes: &[u8]) -> bool { + let mut i = 0; + + loop { + if i >= bytes.len() { + // Missing `.` + break false; + } + + if bytes[i] == b'.' { + break true; + } + + if !is_url_safe_base64_char(bytes[i]) { + return false; } + + i += 1 } + } - false + #[allow(clippy::len_without_is_empty)] // A JWS slice cannot be empty. + pub fn len(&self) -> usize { + self.0.len() } fn header_end(&self) -> usize { @@ -119,31 +170,26 @@ impl CompactJWS { unsafe { std::str::from_utf8_unchecked(&self.0[self.signature_start()..]) } } - pub fn decode_signature(&self) -> Result, Base64DecodeError> { - base64::prelude::BASE64_URL_SAFE_NO_PAD.decode(self.signature()) + pub fn decode_signature(&self) -> Result { + base64::prelude::BASE64_URL_SAFE_NO_PAD + .decode(self.signature()) + .map(JwsSignature::new) } /// Decodes the entire JWS. - pub fn decode(&self) -> Result>, DecodeError> { + pub fn decode(&self) -> Result>, DecodeError> { let header = self.decode_header().map_err(DecodeError::Header)?; let payload = self.decode_payload(&header).map_err(DecodeError::Payload)?; let signature = self.decode_signature().map_err(DecodeError::Signature)?; - Ok(JWS::new(header, payload, signature.into())) - } - - /// Decodes the entire JWS while preserving the signing bytes so they can - /// be verified. - pub fn to_decoded(&self) -> Result>, DecodeError> { - let signing_bytes = self.signing_bytes().to_owned(); - let jws = self.decode()?; + let signing_bytes = self.signing_bytes(); - Ok(DecodedJWS::new( + Ok(DecodedJws::new( DecodedSigningBytes { - bytes: signing_bytes, - header: jws.header, - payload: jws.payload, + bytes: Cow::Borrowed(signing_bytes), + header, + payload, }, - jws.signature, + signature, )) } @@ -192,20 +238,27 @@ impl CompactJWS { V: ResolverProvider, V::Resolver: JWKResolver, { - let jws = self.to_decoded().unwrap(); + let jws = self.decode().unwrap(); jws.verify(params).await } } -/// JWS in compact serialized form. -pub struct CompactJWSBuf(Vec); - -impl CompactJWSBuf { - pub fn new(bytes: Vec) -> Result>> { - if CompactJWS::check(&bytes) { +/// Owned JWS without any encoding guaranties. +/// +/// This type is similar to the [`JwsBuf`](crate::JwsBuf) type. +/// However contrarily to `JwsBuf`, there is no guarantee that the JWS is a +/// valid UTF-8 string (and even less URL-safe). +/// +/// Use [`JwsString`](crate::JwsString) if you expect UTF-8 encoded JWSs. +/// Use [`JwsBuf`](crate::JwsBuf) if you expect URL-safe JWSs. +pub struct JwsVec(Vec); + +impl JwsVec { + pub fn new(bytes: Vec) -> Result>> { + if JwsSlice::validate(&bytes) { Ok(Self(bytes)) } else { - Err(InvalidCompactJWS(bytes)) + Err(InvalidJws(bytes)) } } @@ -219,10 +272,10 @@ impl CompactJWSBuf { pub fn from_signing_bytes_and_signature( signing_bytes: Vec, signature: &[u8], - ) -> Result>> { + ) -> Result>> { let mut bytes = signing_bytes; bytes.push(b'.'); - bytes.extend(signature.iter().copied()); + bytes.extend_from_slice(signature); Self::new(bytes) } @@ -250,8 +303,8 @@ impl CompactJWSBuf { Self::new_unchecked(bytes) } - pub fn as_compact_jws(&self) -> &CompactJWS { - unsafe { CompactJWS::new_unchecked(&self.0) } + pub fn as_compact_jws(&self) -> &JwsSlice { + unsafe { JwsSlice::new_unchecked(&self.0) } } pub fn into_signing_bytes(mut self) -> Vec { @@ -259,32 +312,29 @@ impl CompactJWSBuf { self.0 } + pub fn into_bytes(self) -> Vec { + self.0 + } + /// Decodes the entire JWS while preserving the signing bytes so they can /// be verified. - pub fn into_decoded(self) -> Result>, DecodeError> { - let decoded = self.decode()?.into_owned(); - let signing_bytes = self.into_signing_bytes(); - Ok(DecodedJWS::new( - DecodedSigningBytes::new(signing_bytes, decoded.header, decoded.payload), - decoded.signature, - )) + pub fn into_decoded(self) -> Result, DecodeError> { + Ok(self.decode()?.into_owned()) } -} -impl Deref for CompactJWSBuf { - type Target = CompactJWS; + pub fn into_url_safe(self) -> Result { + JwsBuf::new(self.0).map_err(|InvalidJws(bytes)| Self(bytes)) + } - fn deref(&self) -> &Self::Target { - self.as_compact_jws() + pub fn into_jws_string(self) -> Result { + JwsString::new(self.0).map_err(|InvalidJws(bytes)| Self(bytes)) } } -#[derive(Debug, thiserror::Error)] -#[error("invalid compact JWS")] -pub struct InvalidCompactJWS(pub B); +impl Deref for JwsVec { + type Target = JwsSlice; -impl<'a> InvalidCompactJWS<&'a [u8]> { - pub fn into_owned(self) -> InvalidCompactJWS> { - InvalidCompactJWS(self.0.to_owned()) + fn deref(&self) -> &Self::Target { + self.as_compact_jws() } } diff --git a/crates/claims/crates/jws/src/compact/mod.rs b/crates/claims/crates/jws/src/compact/mod.rs index d1710b660..bd3b67379 100644 --- a/crates/claims/crates/jws/src/compact/mod.rs +++ b/crates/claims/crates/jws/src/compact/mod.rs @@ -3,3 +3,16 @@ pub use bytes::*; mod str; pub use str::*; + +mod url_safe; +pub use url_safe::*; + +#[derive(Debug, thiserror::Error)] +#[error("invalid JWS")] +pub struct InvalidJws(pub B); + +impl<'a> InvalidJws<&'a [u8]> { + pub fn into_owned(self) -> InvalidJws> { + InvalidJws(self.0.to_owned()) + } +} diff --git a/crates/claims/crates/jws/src/compact/str.rs b/crates/claims/crates/jws/src/compact/str.rs index 150d9a8c9..5e7018521 100644 --- a/crates/claims/crates/jws/src/compact/str.rs +++ b/crates/claims/crates/jws/src/compact/str.rs @@ -1,29 +1,89 @@ +use base64::Engine; use core::fmt; use std::{ops::Deref, str::FromStr}; -use base64::Engine; - -use crate::{CompactJWS, DecodeError, DecodedJWS, DecodedSigningBytes, Header, InvalidCompactJWS}; +use crate::{ + utils::is_url_safe_base64_char, DecodeError, DecodedJws, Header, InvalidJws, JwsSlice, +}; -/// JWS in UTF-8 compact serialized form. +/// Borrowed UTF-8 encoded JWS. +/// +/// This is an unsized type borrowing the JWS and meant to be referenced as +/// `&JwsStr`, just like `&str`. +/// Use [`JwsString`] if you need to own the JWS. /// -/// Contrarily to [`CompactJWS`], this type guarantees that the payload is -/// a valid UTF-8 string, meaning the whole compact JWS is an UTF-8 string. -/// This does not necessarily mean the payload is base64 encoded. +/// This type is similar to the [`Jws`](crate::Jws) type. +/// However contrarily to `Jws`, there is no guarantee that the JWS is URL-safe. +/// +/// Use [`Jws`](crate::Jws) if you expect URL-safe JWSs. +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(transparent)] -pub struct CompactJWSStr(CompactJWS); - -impl CompactJWSStr { - pub fn new(data: &[u8]) -> Result<&Self, InvalidCompactJWS<&[u8]>> { - match std::str::from_utf8(data) { - Ok(s) => Self::from_string(s).map_err(|_| InvalidCompactJWS(data)), - Err(_) => Err(InvalidCompactJWS(data)), +pub struct JwsStr(JwsSlice); + +impl JwsStr { + pub fn new>(data: &T) -> Result<&Self, InvalidJws<&T>> { + let bytes = data.as_ref(); + match std::str::from_utf8(bytes) { + Ok(_) => { + let _ = JwsSlice::new(bytes).map_err(|_| InvalidJws(data))?; + Ok(unsafe { Self::new_unchecked(bytes) }) + } + Err(_) => Err(InvalidJws(data)), } } - pub fn from_string(data: &str) -> Result<&Self, InvalidCompactJWS<&str>> { - let inner = CompactJWS::new(data.as_bytes()).map_err(|_| InvalidCompactJWS(data))?; - Ok(unsafe { std::mem::transmute::<&CompactJWS, &Self>(inner) }) + pub const fn validate(bytes: &[u8]) -> bool { + Self::validate_range(bytes, 0, bytes.len()) + } + + pub const fn validate_range(bytes: &[u8], mut i: usize, end: usize) -> bool { + let mut j = if end > bytes.len() { bytes.len() } else { end }; + + // Header. + loop { + if i >= j { + // Missing `.` + return false; + } + + if bytes[i] == b'.' { + break; + } + + if !is_url_safe_base64_char(bytes[i]) { + return false; + } + + i += 1 + } + + // Signature. + if i >= j { + return false; + } + j -= 1; + loop { + if i >= j { + // Missing `.` + return false; + } + + if bytes[j] == b'.' { + break; + } + + if !is_url_safe_base64_char(bytes[j]) { + return false; + } + + j -= 1 + } + + // Payload. + i += 1; + let payload_bytes = unsafe { std::slice::from_raw_parts(bytes.as_ptr().add(i), j - i) }; + + std::str::from_utf8(payload_bytes).is_ok() } /// Creates a new compact JWS without checking the data. @@ -32,7 +92,7 @@ impl CompactJWSStr { /// /// The input `data` must represent a valid compact JWS where the payload /// is an UTF-8 string. - pub unsafe fn new_unchecked(data: &[u8]) -> &Self { + pub const unsafe fn new_unchecked(data: &[u8]) -> &Self { std::mem::transmute(data) } @@ -45,27 +105,27 @@ impl CompactJWSStr { } } -impl Deref for CompactJWSStr { - type Target = CompactJWS; +impl Deref for JwsStr { + type Target = JwsSlice; fn deref(&self) -> &Self::Target { &self.0 } } -impl fmt::Display for CompactJWSStr { +impl fmt::Display for JwsStr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.as_str().fmt(f) } } -impl fmt::Debug for CompactJWSStr { +impl fmt::Debug for JwsStr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.as_str().fmt(f) } } -impl serde::Serialize for CompactJWSStr { +impl serde::Serialize for JwsStr { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, @@ -74,70 +134,72 @@ impl serde::Serialize for CompactJWSStr { } } -impl PartialEq for CompactJWSStr { +impl PartialEq for JwsStr { fn eq(&self, other: &str) -> bool { self.as_str() == other } } -impl PartialEq for CompactJWSStr { +impl PartialEq for JwsStr { fn eq(&self, other: &String) -> bool { self.as_str() == other } } -impl<'a> PartialEq for &'a CompactJWSStr { +impl<'a> PartialEq for &'a JwsStr { fn eq(&self, other: &String) -> bool { self.as_str() == other } } -impl PartialEq for str { - fn eq(&self, other: &CompactJWSStr) -> bool { +impl PartialEq for str { + fn eq(&self, other: &JwsStr) -> bool { self == other.as_str() } } -impl PartialEq for String { - fn eq(&self, other: &CompactJWSStr) -> bool { +impl PartialEq for String { + fn eq(&self, other: &JwsStr) -> bool { self == other.as_str() } } -impl<'a> PartialEq<&'a CompactJWSStr> for String { - fn eq(&self, other: &&'a CompactJWSStr) -> bool { +impl<'a> PartialEq<&'a JwsStr> for String { + fn eq(&self, other: &&'a JwsStr) -> bool { self == other.as_str() } } -/// JWS in compact serialized form, with an UTF-8 encoded payload. +/// Owned UTF-8 encoded JWS. +/// +/// This type is similar to the [`JwsBuf`](crate::JwsBuf) type. +/// However contrarily to `JwsBuf`, there is no guarantee that the JWS is +/// URL-safe. /// -/// Contrarily to [`CompactJWS`], this type guarantees that the payload is -/// a valid UTF-8 string, meaning the whole compact JWS is an UTF-8 string. -/// This does not necessarily mean the payload is base64 encoded. +/// Use [`JwsBuf`](crate::JwsBuf) if you expect URL-safe JWSs. #[derive(Clone, serde::Serialize)] #[serde(transparent)] -pub struct CompactJWSString(String); +pub struct JwsString(String); -impl CompactJWSString { - pub fn new(bytes: Vec) -> Result>> { +impl JwsString { + pub fn new(bytes: Vec) -> Result>> { match String::from_utf8(bytes) { Ok(string) => { - if CompactJWS::check(string.as_bytes()) { + if JwsSlice::validate(string.as_bytes()) { Ok(Self(string)) } else { - Err(InvalidCompactJWS(string.into_bytes())) + Err(InvalidJws(string.into_bytes())) } } - Err(e) => Err(InvalidCompactJWS(e.into_bytes())), + Err(e) => Err(InvalidJws(e.into_bytes())), } } - pub fn from_string(string: String) -> Result> { - if CompactJWS::check(string.as_bytes()) { + pub fn from_string(string: String) -> Result> { + if JwsSlice::validate(string.as_bytes()) { Ok(Self(string)) } else { - Err(InvalidCompactJWS(string)) + Err(InvalidJws(string)) } } @@ -152,10 +214,7 @@ impl CompactJWSString { /// Creates a new detached JWS from a header and base64-encoded signature. /// /// Detached means the payload will not appear in the JWS. - pub fn new_detached( - header: Header, - b64_signature: &[u8], - ) -> Result>> { + pub fn new_detached(header: Header, b64_signature: &[u8]) -> Result>> { let mut bytes = header.encode().into_bytes(); bytes.extend(b".."); bytes.extend(b64_signature.iter().copied()); @@ -174,7 +233,7 @@ impl CompactJWSString { pub fn encode_from_signing_bytes_and_signature( signing_bytes: Vec, signature: &[u8], - ) -> Result>> { + ) -> Result>> { let b64_signature = base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(signature); let mut bytes = signing_bytes; bytes.push(b'.'); @@ -185,7 +244,7 @@ impl CompactJWSString { pub fn from_signing_bytes_and_signature( signing_bytes: Vec, signature: impl IntoIterator, - ) -> Result>> { + ) -> Result>> { let mut bytes = signing_bytes; bytes.push(b'.'); bytes.extend(signature); @@ -206,8 +265,8 @@ impl CompactJWSString { Self::new_unchecked(bytes) } - pub fn as_compact_jws_str(&self) -> &CompactJWSStr { - unsafe { CompactJWSStr::new_unchecked(self.0.as_bytes()) } + pub fn as_compact_jws_str(&self) -> &JwsStr { + unsafe { JwsStr::new_unchecked(self.0.as_bytes()) } } pub fn into_signing_bytes(mut self) -> String { @@ -221,53 +280,48 @@ impl CompactJWSString { /// Decodes the entire JWS while preserving the signing bytes so they can /// be verified. - pub fn into_decoded(self) -> Result>, DecodeError> { - let decoded = self.decode()?.into_owned(); - let signing_bytes = self.into_signing_bytes().into_bytes(); - Ok(DecodedJWS::new( - DecodedSigningBytes::new(signing_bytes, decoded.header, decoded.payload), - decoded.signature, - )) + pub fn into_decoded(self) -> Result, DecodeError> { + Ok(self.decode()?.into_owned()) } } -impl Deref for CompactJWSString { - type Target = CompactJWSStr; +impl Deref for JwsString { + type Target = JwsStr; fn deref(&self) -> &Self::Target { self.as_compact_jws_str() } } -impl fmt::Display for CompactJWSString { +impl fmt::Display for JwsString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.as_str().fmt(f) } } -impl fmt::Debug for CompactJWSString { +impl fmt::Debug for JwsString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.as_str().fmt(f) } } -impl FromStr for CompactJWSString { - type Err = InvalidCompactJWS; +impl FromStr for JwsString { + type Err = InvalidJws; fn from_str(s: &str) -> Result { Self::from_string(s.to_owned()) } } -impl TryFrom for CompactJWSString { - type Error = InvalidCompactJWS; +impl TryFrom for JwsString { + type Error = InvalidJws; fn try_from(value: String) -> Result { Self::from_string(value) } } -impl<'de> serde::Deserialize<'de> for CompactJWSString { +impl<'de> serde::Deserialize<'de> for JwsString { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -275,7 +329,7 @@ impl<'de> serde::Deserialize<'de> for CompactJWSString { struct Visitor; impl<'de> serde::de::Visitor<'de> for Visitor { - type Value = CompactJWSString; + type Value = JwsString; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { formatter.write_str("compact JWS") @@ -292,7 +346,7 @@ impl<'de> serde::Deserialize<'de> for CompactJWSString { where E: serde::de::Error, { - CompactJWSString::from_string(v).map_err(|e| E::custom(e)) + JwsString::from_string(v).map_err(|e| E::custom(e)) } } @@ -300,38 +354,38 @@ impl<'de> serde::Deserialize<'de> for CompactJWSString { } } -impl PartialEq for CompactJWSString { +impl PartialEq for JwsString { fn eq(&self, other: &str) -> bool { self.as_str() == other } } -impl<'a> PartialEq<&'a str> for CompactJWSString { +impl<'a> PartialEq<&'a str> for JwsString { fn eq(&self, other: &&'a str) -> bool { self.as_str() == *other } } -impl PartialEq for CompactJWSString { +impl PartialEq for JwsString { fn eq(&self, other: &String) -> bool { self.as_str() == other } } -impl PartialEq for str { - fn eq(&self, other: &CompactJWSString) -> bool { +impl PartialEq for str { + fn eq(&self, other: &JwsString) -> bool { self == other.as_str() } } -impl<'a> PartialEq for &'a str { - fn eq(&self, other: &CompactJWSString) -> bool { +impl<'a> PartialEq for &'a str { + fn eq(&self, other: &JwsString) -> bool { *self == other.as_str() } } -impl PartialEq for String { - fn eq(&self, other: &CompactJWSString) -> bool { +impl PartialEq for String { + fn eq(&self, other: &JwsString) -> bool { self == other.as_str() } } diff --git a/crates/claims/crates/jws/src/compact/url_safe.rs b/crates/claims/crates/jws/src/compact/url_safe.rs new file mode 100644 index 000000000..0c90f1a7b --- /dev/null +++ b/crates/claims/crates/jws/src/compact/url_safe.rs @@ -0,0 +1,412 @@ +use base64::Engine; +use core::fmt; +use ssi_core::BytesBuf; +use std::{ops::Deref, str::FromStr}; + +use crate::{ + utils::is_url_safe_base64_char, DecodeError, DecodedJws, Header, InvalidJws, JwsSlice, JwsStr, +}; + +/// Creates a new static URL-safe JWS reference from a string literal. +#[macro_export] +macro_rules! jws { + ($value:literal) => { + match $crate::Jws::from_str_const($value) { + Ok(value) => value, + Err(_) => panic!("invalid URL-safe JWS"), + } + }; +} + +/// Borrowed URL-safe JWS. +/// +/// This is an unsized type borrowing the JWS and meant to be referenced as +/// `&Jws`, just like `&str`. +/// Use [`JwsBuf`] if you need to own the JWS. +/// +/// Contrarily to [`JwsSlice`] or [`JwsStr`], this type guarantees that +/// the payload is URL-safe, even if it is unencoded. +/// +/// Use [`JwsStr`] if you expect an UTF-8 encoded JWS but don't know if it is +/// URL-safe. +/// Use [`JwsSlice`] if you don't have any expectations about the encoding. +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)] +#[repr(transparent)] +pub struct Jws(JwsStr); + +impl Jws { + pub fn new>(data: &T) -> Result<&Self, InvalidJws<&T>> { + let bytes = data.as_ref(); + match std::str::from_utf8(bytes) { + Ok(_) => { + let _ = JwsSlice::new(bytes).map_err(|_| InvalidJws(data))?; + Ok(unsafe { Self::new_unchecked(bytes) }) + } + Err(_) => Err(InvalidJws(data)), + } + } + + /// Parses the given `input` string as an URL-safe JWS. + /// + /// Returns an error if it is not a valid URL-safe JWS. + pub const fn from_str_const(input: &str) -> Result<&Self, InvalidJws<&str>> { + let bytes = input.as_bytes(); + if Self::validate(bytes) { + Ok(unsafe { Self::new_unchecked(bytes) }) + } else { + Err(InvalidJws(input)) + } + } + + pub const fn validate(bytes: &[u8]) -> bool { + Self::validate_range(bytes, 0, bytes.len()) + } + + pub const fn validate_range(bytes: &[u8], mut i: usize, end: usize) -> bool { + // Header. + loop { + if i >= end { + // Missing `.` + return false; + } + + if bytes[i] == b'.' { + break; + } + + if !is_url_safe_base64_char(bytes[i]) { + return false; + } + + i += 1 + } + + i += 1; + + // Payload. + loop { + if i >= end { + // Missing `.` + return false; + } + + if bytes[i] == b'.' { + break; + } + + if !is_url_safe_base64_char(bytes[i]) { + return false; + } + + i += 1 + } + + i += 1; + + // Signature. + while i < end { + if !is_url_safe_base64_char(bytes[i]) { + return false; + } + + i += 1 + } + + true + } + + /// Creates a new compact JWS without checking the data. + /// + /// # Safety + /// + /// The input `data` must represent a valid compact JWS where the payload + /// is an UTF-8 string. + pub const unsafe fn new_unchecked(data: &[u8]) -> &Self { + std::mem::transmute(data) + } + + pub fn as_str(&self) -> &str { + unsafe { + // Safety: we already checked that the bytes are a valid UTF-8 + // string. + std::str::from_utf8_unchecked(self.0.as_bytes()) + } + } +} + +impl Deref for Jws { + type Target = JwsSlice; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl fmt::Display for Jws { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_str().fmt(f) + } +} + +impl fmt::Debug for Jws { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_str().fmt(f) + } +} + +impl serde::Serialize for Jws { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.as_str().serialize(serializer) + } +} + +impl PartialEq for Jws { + fn eq(&self, other: &str) -> bool { + self.as_str() == other + } +} + +impl PartialEq for Jws { + fn eq(&self, other: &String) -> bool { + self.as_str() == other + } +} + +impl<'a> PartialEq for &'a Jws { + fn eq(&self, other: &String) -> bool { + self.as_str() == other + } +} + +impl PartialEq for str { + fn eq(&self, other: &Jws) -> bool { + self == other.as_str() + } +} + +impl PartialEq for String { + fn eq(&self, other: &Jws) -> bool { + self == other.as_str() + } +} + +impl<'a> PartialEq<&'a Jws> for String { + fn eq(&self, other: &&'a Jws) -> bool { + self == other.as_str() + } +} + +/// Owned URL-safe JWS. +/// +/// Contrarily to [`JwsVec`](crate::JwsVec) or [`JwsString`](crate::JwsString), +/// this type guarantees that the payload is URL-safe, even if it is unencoded. +/// +/// Use [`JwsString`](crate::JwsString) if you expect an UTF-8 encoded JWS but +/// don't know if it is URL-safe. +/// Use [`JwsVec`](crate::JwsVec) if you don't have any expectations about the +/// encoding. +#[derive(Clone, serde::Serialize)] +#[serde(transparent)] +pub struct JwsBuf(String); + +impl JwsBuf { + pub fn new(bytes: B) -> Result> { + if Jws::validate(bytes.as_ref()) { + Ok(unsafe { + // SAFETY: we just validated the bytes. + Self::new_unchecked(bytes.into()) + }) + } else { + Err(InvalidJws(bytes)) + } + } + + /// # Safety + /// + /// The input `bytes` must represent a valid compact JWS where the payload + /// is UTF-8 encoded. + pub unsafe fn new_unchecked(bytes: Vec) -> Self { + Self(String::from_utf8_unchecked(bytes)) + } + + /// Creates a new detached JWS from a header and base64-encoded signature. + /// + /// Detached means the payload will not appear in the JWS. + pub fn new_detached(header: Header, b64_signature: &[u8]) -> Result>> { + let mut bytes = header.encode().into_bytes(); + bytes.extend(b".."); + bytes.extend(b64_signature.iter().copied()); + Self::new(bytes) + } + + /// Creates a new detached JWS from a header and unencoded signature. + /// + /// Detached means the payload will not appear in the JWS. + pub fn encode_detached(header: Header, signature: &[u8]) -> Self { + let b64_signature = base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(signature); + Self::new_detached(header, b64_signature.as_bytes()).unwrap() + } + + /// Encodes the given signature in base64 and returns a compact JWS. + pub fn encode_from_signing_bytes_and_signature( + signing_bytes: Vec, + signature: &[u8], + ) -> Result>> { + 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()); + Self::new(bytes) + } + + pub fn from_signing_bytes_and_signature( + signing_bytes: Vec, + signature: impl IntoIterator, + ) -> Result>> { + let mut bytes = signing_bytes; + bytes.push(b'.'); + bytes.extend(signature); + Self::new(bytes) + } + + /// # Safety + /// + /// The input `signing_bytes` and `signature` must form a valid compact JWS + /// once concatenated with a `.`. + pub unsafe fn from_signing_bytes_and_signature_unchecked( + signing_bytes: Vec, + signature: Vec, + ) -> Self { + let mut bytes = signing_bytes; + bytes.push(b'.'); + bytes.extend(signature); + Self::new_unchecked(bytes) + } + + pub fn as_compact_jws_str(&self) -> &Jws { + unsafe { Jws::new_unchecked(self.0.as_bytes()) } + } + + pub fn into_signing_bytes(mut self) -> String { + self.0.truncate(self.payload_end()); // remove the signature. + self.0 + } + + pub fn into_string(self) -> String { + self.0 + } + + /// Decodes the entire JWS while preserving the signing bytes so they can + /// be verified. + pub fn into_decoded(self) -> Result, DecodeError> { + Ok(self.decode()?.into_owned()) + } +} + +impl Deref for JwsBuf { + type Target = Jws; + + fn deref(&self) -> &Self::Target { + self.as_compact_jws_str() + } +} + +impl fmt::Display for JwsBuf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_str().fmt(f) + } +} + +impl fmt::Debug for JwsBuf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_str().fmt(f) + } +} + +impl FromStr for JwsBuf { + type Err = InvalidJws; + + fn from_str(s: &str) -> Result { + Self::new(s.to_owned()) + } +} + +impl TryFrom for JwsBuf { + type Error = InvalidJws; + + fn try_from(value: String) -> Result { + Self::new(value) + } +} + +impl<'de> serde::Deserialize<'de> for JwsBuf { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Visitor; + + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = JwsBuf; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("compact JWS") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + self.visit_string(v.to_owned()) + } + + fn visit_string(self, v: String) -> Result + where + E: serde::de::Error, + { + JwsBuf::new(v).map_err(|e| E::custom(e)) + } + } + + deserializer.deserialize_string(Visitor) + } +} + +impl PartialEq for JwsBuf { + fn eq(&self, other: &str) -> bool { + self.as_str() == other + } +} + +impl<'a> PartialEq<&'a str> for JwsBuf { + fn eq(&self, other: &&'a str) -> bool { + self.as_str() == *other + } +} + +impl PartialEq for JwsBuf { + fn eq(&self, other: &String) -> bool { + self.as_str() == other + } +} + +impl PartialEq for str { + fn eq(&self, other: &JwsBuf) -> bool { + self == other.as_str() + } +} + +impl<'a> PartialEq for &'a str { + fn eq(&self, other: &JwsBuf) -> bool { + *self == other.as_str() + } +} + +impl PartialEq for String { + fn eq(&self, other: &JwsBuf) -> bool { + self == other.as_str() + } +} diff --git a/crates/claims/crates/jws/src/error.rs b/crates/claims/crates/jws/src/error.rs index 535c3a491..1c05ae174 100644 --- a/crates/claims/crates/jws/src/error.rs +++ b/crates/claims/crates/jws/src/error.rs @@ -21,7 +21,7 @@ pub enum Error { #[error(transparent)] CryptoErr(#[from] p384::ecdsa::Error), #[error(transparent)] - JWK(#[from] ssi_jwk::Error), + Jwk(#[from] ssi_jwk::Error), #[error(transparent)] Json(#[from] serde_json::Error), #[error(transparent)] @@ -37,7 +37,7 @@ pub enum Error { AlgorithmMismatch, /// Invalid JWS #[error("Invalid JWS")] - InvalidJWS, + InvalidJws, /// Unsupported algorithm #[error("Unsupported algorithm `{0}`")] UnsupportedAlgorithm(String), diff --git a/crates/claims/crates/jws/src/lib.rs b/crates/claims/crates/jws/src/lib.rs index 9542741b8..5a1709020 100644 --- a/crates/claims/crates/jws/src/lib.rs +++ b/crates/claims/crates/jws/src/lib.rs @@ -1,23 +1,25 @@ //! JSON Web Signature (JWS) implementation following [RFC 7515] and [RFC 7797] //! (Unencoded Payload Option). //! -//! [RFC 7515]: -//! [RFC 7797]: -//! //! # Usage //! +//! The entry point to store and verify JWS is the [`&Jws`][Jws] type, borrowing +//! the JWS, just like a `&str` borrows a text string. +//! The [`JwsBuf`] type is the owned version of this type, owning the JWS, +//! just like a [`String`] owns a text string. +//! //! # Decoding & Verification //! -//! Use [`CompactJWS::verify`] to decode a JWS. +//! Use [`JwsSlice::verify`] to decode a JWS. //! //! ``` //! # #[cfg(feature = "secp256r1")] //! # async_std::task::block_on(async { //! use serde_json::json; //! use ssi_jwk::JWK; -//! use ssi_jws::CompactJWSStr; +//! use ssi_jws::Jws; //! -//! let jws = CompactJWSStr::new(b"eyJhbGciOiJFUzI1NiJ9.cGF5bG9hZA.LW6XkHmgfNnb2CA-2qdeMVGpekAoxRNsAHoeLpnton3QMaQ3dMj-5G9SlP8dHj7cHf2HtRPdy6-9LbxYKvumKw").unwrap(); +//! let jws = Jws::new(b"eyJhbGciOiJFUzI1NiJ9.cGF5bG9hZA.LW6XkHmgfNnb2CA-2qdeMVGpekAoxRNsAHoeLpnton3QMaQ3dMj-5G9SlP8dHj7cHf2HtRPdy6-9LbxYKvumKw").unwrap(); //! //! let jwk: JWK = json!({ //! "kty": "EC", @@ -32,10 +34,10 @@ //! # }) //! ``` //! -//! Internally [`CompactJWS::verify`] uses [`CompactJWS::to_decoded`] to decode -//! the JWS, then [`DecodedJWS::verify`] to validate the signature. +//! Internally [`JwsSlice::verify`] uses [`JwsSlice::decode`] to decode +//! the JWS, then [`DecodedJws::verify`] to validate the signature. //! -//! [`DecodedJWS::verify`]: DecodedJWS::verify +//! [`DecodedJws::verify`]: DecodedJws::verify //! //! ```ignore //! let decoded_jws = jws.to_decoded().unwrap(); @@ -44,19 +46,19 @@ //! ``` //! //! You can use this method to decode the payload before the verification -//! (using [`DecodedJWS::try_map`] for instance) so it can be verified along the +//! (using [`DecodedJws::try_map`] for instance) so it can be verified along the //! signature. //! //! # Signature //! -//! Use the [`JWSPayload::sign`] method to sign a payload into a compact JWS. +//! Use the [`JwsPayload::sign`] method to sign a payload into a compact JWS. //! //! ``` //! # #[cfg(feature = "secp256r1")] //! # async_std::task::block_on(async { //! use serde_json::json; //! use ssi_jwk::JWK; -//! use ssi_jws::JWSPayload; +//! use ssi_jws::JwsPayload; //! //! let jwk: JWK = json!({ //! "kty": "EC", @@ -72,6 +74,32 @@ //! assert_eq!(jwt, "eyJhbGciOiJFUzI1NiJ9.cGF5bG9hZA.LW6XkHmgfNnb2CA-2qdeMVGpekAoxRNsAHoeLpnton3QMaQ3dMj-5G9SlP8dHj7cHf2HtRPdy6-9LbxYKvumKw") //! # }) //! ``` +//! +//! # URL safety and JWS types +//! +//! [RFC 7515] originally defines JWS as URL safe strings due to the payload +//! being base64 URL-safe encoded. +//! However, [RFC 7797] introduces a `b64` header option that makes this +//! encoding optional. If set to `false`, the JWS may not be URL-safe. In fact +//! it may not be UTF-8 encoded at all. +//! +//! To deal with these different encoding expectations this library provides +//! three families of types for representing JWS: +//! - [`Jws`] and [`JwsBuf`]: This is the most common type family that follows +//! [RFC 7515] to the letter, expecting an URL-safe JWS. +//! It is still possible to use the `b64` header to embed unencoded payloads +//! but those payloads *must* use URL-safe base64 bytes/characters. +//! - [`JwsStr`] and [`JwsString`]: This family relaxes the URL-safe payload +//! constraint. +//! Unencoded payloads may use bytes outside of the URL-safe base64 alphabet, +//! but they *must* be valid UTF-8 strings. This guarantees that the overall +//! JWS is a valid UTF-8 string, even if it is not URL-safe. +//! - [`JwsSlice`] and [`JwsVec`]: This family does not imposes any constraint +//! on unencoded payloads. +//! There is no guaranty that the overall JWS will be an UTF-8 string. +//! +//! [RFC 7515]: +//! [RFC 7797]: #![cfg_attr(docsrs, feature(doc_auto_cfg))] pub mod error; pub use base64::DecodeError as Base64DecodeError; @@ -86,6 +114,8 @@ use std::{borrow::Cow, collections::BTreeMap}; pub type VerificationWarnings = Vec; +pub(crate) mod utils; + mod compact; pub use compact::*; @@ -95,9 +125,9 @@ pub use signature::*; mod verification; pub use verification::*; -/// Decoded JWS. +/// Decoded JWS parts. #[derive(Clone, PartialEq, Eq)] -pub struct JWS> { +pub struct JwsParts> { /// JOSE Header. pub header: Header, @@ -105,11 +135,11 @@ pub struct JWS> { pub payload: T, /// Signature. - pub signature: JWSSignature, + pub signature: JwsSignature, } -impl JWS { - pub fn new(header: Header, payload: T, signature: JWSSignature) -> Self { +impl JwsParts { + pub fn new(header: Header, payload: T, signature: JwsSignature) -> Self { Self { header, payload, @@ -117,16 +147,16 @@ impl JWS { } } - pub fn map(self, f: impl FnOnce(T) -> U) -> JWS { - JWS { + pub fn map(self, f: impl FnOnce(T) -> U) -> JwsParts { + JwsParts { header: self.header, payload: f(self.payload), signature: self.signature, } } - pub fn try_map(self, f: impl FnOnce(T) -> Result) -> Result, E> { - Ok(JWS { + pub fn try_map(self, f: impl FnOnce(T) -> Result) -> Result, E> { + Ok(JwsParts { header: self.header, payload: f(self.payload)?, signature: self.signature, @@ -134,51 +164,55 @@ impl JWS { } } -impl<'a, T: ?Sized + ToOwned> JWS> { - pub fn into_owned(self) -> JWS { - JWS::new(self.header, self.payload.into_owned(), self.signature) +impl<'a, T: ?Sized + ToOwned> JwsParts> { + pub fn into_owned(self) -> JwsParts { + JwsParts::new(self.header, self.payload.into_owned(), self.signature) } } /// Decoded JWS. /// /// JWS with its signing bytes. -#[derive(Clone, PartialEq, Eq)] -pub struct DecodedJWS> { - pub signing_bytes: DecodedSigningBytes, - pub signature: JWSSignature, +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DecodedJws<'a, T = Vec> { + pub signing_bytes: DecodedSigningBytes<'a, T>, + pub signature: JwsSignature, } -impl DecodedJWS { - pub fn new(signing_bytes: DecodedSigningBytes, signature: JWSSignature) -> Self { +impl<'a, T> DecodedJws<'a, T> { + pub fn new(signing_bytes: DecodedSigningBytes<'a, T>, signature: JwsSignature) -> Self { Self { signing_bytes, signature, } } - pub fn map(self, f: impl FnOnce(T) -> U) -> DecodedJWS { - DecodedJWS::new(self.signing_bytes.map(f), self.signature) + pub fn header(&self) -> &Header { + &self.signing_bytes.header + } + + pub fn map(self, f: impl FnOnce(T) -> U) -> DecodedJws<'a, U> { + DecodedJws::new(self.signing_bytes.map(f), self.signature) } - pub fn try_map(self, f: impl FnOnce(T) -> Result) -> Result, E> { - Ok(DecodedJWS::new( + pub fn try_map(self, f: impl FnOnce(T) -> Result) -> Result, E> { + Ok(DecodedJws::new( self.signing_bytes.try_map(f)?, self.signature, )) } - pub fn into_jws(self) -> JWS { - JWS::new( + pub fn into_jws(self) -> JwsParts { + JwsParts::new( self.signing_bytes.header, self.signing_bytes.payload, self.signature, ) } - pub fn into_jws_and_signing_bytes(self) -> (JWS, Vec) { + pub fn into_jws_and_signing_bytes(self) -> (JwsParts, Cow<'a, [u8]>) { ( - JWS::new( + JwsParts::new( self.signing_bytes.header, self.signing_bytes.payload, self.signature, @@ -187,6 +221,14 @@ impl DecodedJWS { ) } + pub fn into_encoded(self) -> JwsVec { + JwsVec::from_signing_bytes_and_signature( + self.signing_bytes.bytes.into_owned(), + self.signature.encode().as_bytes(), + ) + .unwrap() + } + /// Verify the JWS signature. /// /// This will check the signature and the validity of the decoded payload. @@ -213,7 +255,7 @@ impl DecodedJWS { /// by move *or* by reference. pub async fn verify

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

+ ValidateClaims, + T: ValidateJwsHeader

+ ValidateClaims, P: ResolverProvider, P::Resolver: JWKResolver, { @@ -221,17 +263,26 @@ impl DecodedJWS { } } -impl<'a, T: ?Sized + ToOwned> DecodedJWS> { - pub fn into_owned(self) -> DecodedJWS { - DecodedJWS::new(self.signing_bytes.into_owned(), self.signature) +impl<'a, 'b, T: ?Sized + ToOwned> DecodedJws<'a, &'b T> { + pub fn to_owned(&self) -> DecodedJws<'static, T::Owned> { + DecodedJws { + signing_bytes: self.signing_bytes.to_owned(), + signature: self.signature.to_owned(), + } + } +} + +impl<'a, 'b, T: ?Sized + ToOwned> DecodedJws<'a, Cow<'b, T>> { + pub fn into_owned(self) -> DecodedJws<'static, T::Owned> { + DecodedJws::new(self.signing_bytes.into_owned(), self.signature) } } /// JWS decoded signing bytes. -#[derive(Clone, PartialEq, Eq)] -pub struct DecodedSigningBytes> { +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DecodedSigningBytes<'a, T = Vec> { /// Encoded bytes. - pub bytes: Vec, + pub bytes: Cow<'a, [u8]>, /// Decoded JOSE Header. pub header: Header, @@ -240,8 +291,8 @@ pub struct DecodedSigningBytes> { pub payload: T, } -impl DecodedSigningBytes { - pub fn new(bytes: Vec, header: Header, payload: T) -> Self { +impl<'a, T> DecodedSigningBytes<'a, T> { + pub fn new(bytes: Cow<'a, [u8]>, header: Header, payload: T) -> Self { Self { bytes, header, @@ -249,7 +300,7 @@ impl DecodedSigningBytes { } } - pub fn map(self, f: impl FnOnce(T) -> U) -> DecodedSigningBytes { + pub fn map(self, f: impl FnOnce(T) -> U) -> DecodedSigningBytes<'a, U> { DecodedSigningBytes { bytes: self.bytes, header: self.header, @@ -260,7 +311,7 @@ impl DecodedSigningBytes { pub fn try_map( self, f: impl FnOnce(T) -> Result, - ) -> Result, E> { + ) -> Result, E> { Ok(DecodedSigningBytes { bytes: self.bytes, header: self.header, @@ -269,9 +320,23 @@ impl DecodedSigningBytes { } } -impl<'a, T: ?Sized + ToOwned> DecodedSigningBytes> { - pub fn into_owned(self) -> DecodedSigningBytes { - self.map(Cow::into_owned) +impl<'a, 'b, T: ?Sized + ToOwned> DecodedSigningBytes<'a, &'b T> { + pub fn to_owned(&self) -> DecodedSigningBytes<'static, T::Owned> { + DecodedSigningBytes { + bytes: Cow::Owned(self.bytes.as_ref().to_owned()), + header: self.header.clone(), + payload: self.payload.to_owned(), + } + } +} + +impl<'a, 'b, T: ?Sized + ToOwned> DecodedSigningBytes<'a, Cow<'b, T>> { + pub fn into_owned(self) -> DecodedSigningBytes<'static, T::Owned> { + DecodedSigningBytes { + bytes: Cow::Owned(self.bytes.into_owned()), + header: self.header, + payload: self.payload.into_owned(), + } } } @@ -589,7 +654,7 @@ pub fn sign_bytes(algorithm: Algorithm, data: &[u8], key: &JWK) -> Result { - return Err(Error::JWK(ssi_jwk::Error::KeyTypeNotImplemented(Box::new( + return Err(Error::Jwk(ssi_jwk::Error::KeyTypeNotImplemented(Box::new( key.to_public(), )))) } @@ -851,7 +916,7 @@ pub fn verify_bytes_warnable( } }, _ => { - return Err(Error::JWK(ssi_jwk::Error::KeyTypeNotImplemented(Box::new( + return Err(Error::Jwk(ssi_jwk::Error::KeyTypeNotImplemented(Box::new( key.to_public(), )))) } @@ -1012,7 +1077,7 @@ pub fn split_jws(jws: &str) -> Result<(&str, &str, &str), Error> { Ok( match (parts.next(), parts.next(), parts.next(), parts.next()) { (Some(a), Some(b), Some(c), None) => (a, b, c), - _ => return Err(Error::InvalidJWS), + _ => return Err(Error::InvalidJws), }, ) } @@ -1020,7 +1085,7 @@ pub fn split_jws(jws: &str) -> Result<(&str, &str, &str), Error> { pub fn split_detached_jws(jws: &str) -> Result<(&str, &str), Error> { let (header_b64, omitted_payload, signature_b64) = split_jws(jws)?; if !omitted_payload.is_empty() { - return Err(Error::InvalidJWS); + return Err(Error::InvalidJws); } Ok((header_b64, signature_b64)) } @@ -1032,7 +1097,7 @@ pub fn decode_jws_parts( header_b64: &str, payload_enc: &[u8], signature_b64: &str, -) -> Result { +) -> Result, Error> { 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) { @@ -1049,8 +1114,8 @@ pub fn decode_jws_parts( } } let signing_input = [header_b64.as_bytes(), b".", payload_enc].concat(); - Ok(DecodedJWS::new( - DecodedSigningBytes::new(signing_input, header, payload), + Ok(DecodedJws::new( + DecodedSigningBytes::new(Cow::Owned(signing_input), header, payload), signature.into(), )) } @@ -1060,7 +1125,12 @@ pub fn detached_verify(jws: &str, payload_enc: &[u8], key: &JWK) -> Result Result<(Header, JWK), let (header_b64, signature_b64) = split_detached_jws(jws)?; let (jws, signing_bytes) = decode_jws_parts(header_b64, payload_enc, signature_b64)?.into_jws_and_signing_bytes(); - let key = recover(jws.header.algorithm, &signing_bytes, &jws.signature)?; + let key = recover( + jws.header.algorithm, + &signing_bytes, + jws.signature.as_bytes(), + )?; Ok((jws.header, key)) } @@ -1085,7 +1159,11 @@ pub fn detached_recover_legacy_keccak_es256kr( return Err(Error::AlgorithmMismatch); } jws.header.algorithm = Algorithm::ESKeccakKR; - let key = recover(jws.header.algorithm, &signing_bytes, &jws.signature)?; + let key = recover( + jws.header.algorithm, + &signing_bytes, + jws.signature.as_bytes(), + )?; Ok((jws.header, key)) } @@ -1093,7 +1171,12 @@ pub fn decode_verify(jws: &str, key: &JWK) -> Result<(Header, Vec), Error> { let (header_b64, payload_enc, signature_b64) = split_jws(jws)?; let (jws, signing_bytes) = decode_jws_parts(header_b64, payload_enc.as_bytes(), signature_b64)? .into_jws_and_signing_bytes(); - verify_bytes(jws.header.algorithm, &signing_bytes, key, &jws.signature)?; + verify_bytes( + jws.header.algorithm, + &signing_bytes, + key, + jws.signature.as_bytes(), + )?; Ok((jws.header, jws.payload)) } diff --git a/crates/claims/crates/jws/src/signature.rs b/crates/claims/crates/jws/src/signature.rs index 5f7f5a83a..bed0d2e76 100644 --- a/crates/claims/crates/jws/src/signature.rs +++ b/crates/claims/crates/jws/src/signature.rs @@ -2,12 +2,12 @@ use ssi_claims_core::SignatureError; use ssi_jwk::{Algorithm, JWK}; use std::borrow::Cow; -use crate::{CompactJWSString, Header}; +use crate::{DecodedJws, DecodedSigningBytes, Header, JwsBuf, JwsSignature}; /// JWS payload type. /// /// Any type that can be serialized with a give JWS type. -pub trait JWSPayload { +pub trait JwsPayload { /// JWS type. /// /// Value of the `typ` field in the JWS header. @@ -24,36 +24,60 @@ pub trait JWSPayload { /// Signs the payload and returns a compact JWS. #[allow(async_fn_in_trait)] - async fn sign(&self, signer: &impl JWSSigner) -> Result { + async fn sign(&self, signer: impl JwsSigner) -> Result { signer.sign(self).await } } -impl JWSPayload for [u8] { +impl<'a, P: ?Sized + JwsPayload> JwsPayload for &'a P { + fn typ(&self) -> Option<&str> { + P::typ(*self) + } + + fn cty(&self) -> Option<&str> { + P::cty(*self) + } + + fn payload_bytes(&self) -> Cow<[u8]> { + P::payload_bytes(*self) + } + + async fn sign(&self, signer: impl JwsSigner) -> Result { + P::sign(*self, signer).await + } +} + +impl JwsPayload for [u8] { fn payload_bytes(&self) -> Cow<[u8]> { Cow::Borrowed(self) } } -impl JWSPayload for Vec { +impl JwsPayload for Vec { fn payload_bytes(&self) -> Cow<[u8]> { Cow::Borrowed(self) } } -impl JWSPayload for str { +impl JwsPayload for str { fn payload_bytes(&self) -> Cow<[u8]> { Cow::Borrowed(self.as_bytes()) } } -impl JWSPayload for String { +impl JwsPayload for String { fn payload_bytes(&self) -> Cow<[u8]> { Cow::Borrowed(self.as_bytes()) } } -pub struct JWSSignerInfo { +impl JwsPayload for serde_json::Value { + fn payload_bytes(&self) -> Cow<[u8]> { + Cow::Owned(serde_json::to_vec(self).unwrap()) + } +} + +pub struct JwsSignerInfo { pub key_id: Option, pub algorithm: Algorithm, } @@ -62,18 +86,18 @@ pub struct JWSSignerInfo { /// /// Any type that can fetch a JWK using the `kid` parameter of a JWS JOSE /// header and sign bytes. -pub trait JWSSigner { +pub trait JwsSigner { #[allow(async_fn_in_trait)] - async fn fetch_info(&self) -> Result; + async fn fetch_info(&self) -> Result; #[allow(async_fn_in_trait)] async fn sign_bytes(&self, signing_bytes: &[u8]) -> Result, SignatureError>; #[allow(async_fn_in_trait)] - async fn sign( + async fn sign_into_decoded( &self, - payload: &(impl ?Sized + JWSPayload), - ) -> Result { + payload: P, + ) -> Result, SignatureError> { let info = self.fetch_info().await?; let payload_bytes = payload.payload_bytes(); @@ -86,17 +110,32 @@ pub trait JWSSigner { }; let signing_bytes = header.encode_signing_bytes(&payload_bytes); - let signature = self.sign_bytes(&signing_bytes).await?; + let signature = JwsSignature::new(self.sign_bytes(&signing_bytes).await?); + + Ok(DecodedJws { + signing_bytes: DecodedSigningBytes { + bytes: Cow::Owned(signing_bytes), + header, + payload, + }, + signature, + }) + } - Ok( - CompactJWSString::encode_from_signing_bytes_and_signature(signing_bytes, &signature) - .unwrap(), - ) + #[allow(async_fn_in_trait)] + async fn sign(&self, payload: impl JwsPayload) -> Result { + Ok(self + .sign_into_decoded(payload) + .await? + .into_encoded() + .into_url_safe() + .ok() + .unwrap()) } } -impl<'a, T: JWSSigner> JWSSigner for &'a T { - async fn fetch_info(&self) -> Result { +impl<'a, T: JwsSigner> JwsSigner for &'a T { + async fn fetch_info(&self) -> Result { T::fetch_info(*self).await } @@ -104,16 +143,13 @@ impl<'a, T: JWSSigner> JWSSigner for &'a T { T::sign_bytes(*self, signing_bytes).await } - async fn sign( - &self, - payload: &(impl ?Sized + JWSPayload), - ) -> Result { + async fn sign(&self, payload: impl JwsPayload) -> Result { T::sign(*self, payload).await } } -impl<'a, T: JWSSigner + Clone> JWSSigner for Cow<'a, T> { - async fn fetch_info(&self) -> Result { +impl<'a, T: JwsSigner + Clone> JwsSigner for Cow<'a, T> { + async fn fetch_info(&self) -> Result { T::fetch_info(self.as_ref()).await } @@ -121,17 +157,14 @@ impl<'a, T: JWSSigner + Clone> JWSSigner for Cow<'a, T> { T::sign_bytes(self.as_ref(), signing_bytes).await } - async fn sign( - &self, - payload: &(impl ?Sized + JWSPayload), - ) -> Result { + async fn sign(&self, payload: impl JwsPayload) -> Result { T::sign(self.as_ref(), payload).await } } -impl JWSSigner for JWK { - async fn fetch_info(&self) -> Result { - Ok(JWSSignerInfo { +impl JwsSigner for JWK { + async fn fetch_info(&self) -> Result { + Ok(JwsSignerInfo { key_id: self.key_id.clone(), algorithm: self .get_algorithm() @@ -147,20 +180,20 @@ impl JWSSigner for JWK { } } -pub struct JWKWithAlgorithm<'a> { +pub struct JwkWithAlgorithm<'a> { pub jwk: &'a JWK, pub algorithm: Algorithm, } -impl<'a> JWKWithAlgorithm<'a> { +impl<'a> JwkWithAlgorithm<'a> { pub fn new(jwk: &'a JWK, algorithm: Algorithm) -> Self { Self { jwk, algorithm } } } -impl<'a> JWSSigner for JWKWithAlgorithm<'a> { - async fn fetch_info(&self) -> Result { - Ok(JWSSignerInfo { +impl<'a> JwsSigner for JwkWithAlgorithm<'a> { + async fn fetch_info(&self) -> Result { + Ok(JwsSignerInfo { key_id: self.jwk.key_id.clone(), algorithm: self.algorithm, }) diff --git a/crates/claims/crates/jws/src/utils.rs b/crates/claims/crates/jws/src/utils.rs new file mode 100644 index 000000000..2d7f0b229 --- /dev/null +++ b/crates/claims/crates/jws/src/utils.rs @@ -0,0 +1,4 @@ +/// Checks if the give byte is part of the base64 URL-safe alphabet. +pub const fn is_url_safe_base64_char(b: u8) -> bool { + b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_') +} diff --git a/crates/claims/crates/jws/src/verification.rs b/crates/claims/crates/jws/src/verification.rs index 1e7d866da..c11435c50 100644 --- a/crates/claims/crates/jws/src/verification.rs +++ b/crates/claims/crates/jws/src/verification.rs @@ -1,4 +1,5 @@ -use crate::{verify_bytes, DecodedJWS, DecodedSigningBytes, Error, Header}; +use crate::{verify_bytes, DecodedJws, DecodedSigningBytes, Error, Header}; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use ssi_claims_core::{ ClaimsValidity, InvalidProof, ProofValidationError, ProofValidity, ResolverProvider, ValidateClaims, ValidateProof, VerifiableClaims, @@ -9,35 +10,37 @@ use std::{ ops::Deref, }; -pub trait ValidateJWSHeader { - fn validate_jws_header(&self, env: &E, header: &Header) -> ClaimsValidity; -} - -impl ValidateJWSHeader for [u8] { +pub trait ValidateJwsHeader { fn validate_jws_header(&self, _env: &E, _header: &Header) -> ClaimsValidity { Ok(()) } } -impl<'a, E, T: ?Sized + ToOwned + ValidateJWSHeader> ValidateJWSHeader for Cow<'a, T> { +impl ValidateJwsHeader for [u8] {} + +impl<'a, E, T: ?Sized + ToOwned + ValidateJwsHeader> ValidateJwsHeader for Cow<'a, T> { fn validate_jws_header(&self, env: &E, header: &Header) -> ClaimsValidity { self.as_ref().validate_jws_header(env, header) } } -impl + ValidateJWSHeader> ValidateClaims - for DecodedSigningBytes +impl<'a, E, T: ValidateClaims + ValidateJwsHeader> + ValidateClaims for DecodedSigningBytes<'a, T> { - fn validate_claims(&self, env: &E, signature: &JWSSignature) -> ClaimsValidity { + fn validate_claims(&self, env: &E, signature: &JwsSignature) -> ClaimsValidity { self.payload.validate_jws_header(env, &self.header)?; self.payload.validate_claims(env, signature) } } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct JWSSignature(Vec); +pub struct JwsSignature(Vec); + +impl JwsSignature { + pub fn new(bytes: Vec) -> Self { + Self(bytes) + } -impl JWSSignature { pub fn as_bytes(&self) -> &[u8] { &self.0 } @@ -45,43 +48,47 @@ impl JWSSignature { pub fn into_bytes(self) -> Vec { self.0 } + + pub fn encode(&self) -> String { + URL_SAFE_NO_PAD.encode(&self.0) + } } -impl From> for JWSSignature { +impl From> for JwsSignature { fn from(value: Vec) -> Self { Self(value) } } -impl From for Vec { - fn from(value: JWSSignature) -> Self { +impl From for Vec { + fn from(value: JwsSignature) -> Self { value.into_bytes() } } -impl Deref for JWSSignature { +impl Deref for JwsSignature { type Target = [u8]; fn deref(&self) -> &Self::Target { - &self.0 + self.as_bytes() } } -impl AsRef<[u8]> for JWSSignature { +impl AsRef<[u8]> for JwsSignature { fn as_ref(&self) -> &[u8] { - &self.0 + self.as_bytes() } } -impl Borrow<[u8]> for JWSSignature { +impl Borrow<[u8]> for JwsSignature { fn borrow(&self) -> &[u8] { - &self.0 + self.as_bytes() } } -impl VerifiableClaims for DecodedJWS { - type Claims = DecodedSigningBytes; - type Proof = JWSSignature; +impl<'a, T> VerifiableClaims for DecodedJws<'a, T> { + type Claims = DecodedSigningBytes<'a, T>; + type Proof = JwsSignature; fn claims(&self) -> &Self::Claims { &self.signing_bytes @@ -92,7 +99,7 @@ impl VerifiableClaims for DecodedJWS { } } -impl ValidateProof> for JWSSignature +impl<'b, V, T> ValidateProof> for JwsSignature where V: ResolverProvider, V::Resolver: JWKResolver, @@ -100,7 +107,7 @@ where async fn validate_proof<'a>( &'a self, verifier: &'a V, - claims: &'a DecodedSigningBytes, + claims: &'a DecodedSigningBytes<'b, T>, ) -> Result { let key = verifier .resolver() diff --git a/crates/claims/crates/jwt/src/claims/any.rs b/crates/claims/crates/jwt/src/claims/any.rs index 78051d025..7c4b33045 100644 --- a/crates/claims/crates/jwt/src/claims/any.rs +++ b/crates/claims/crates/jwt/src/claims/any.rs @@ -2,7 +2,7 @@ use std::{borrow::Cow, collections::BTreeMap}; use ssi_claims_core::{ClaimsValidity, DateTimeProvider, ValidateClaims}; -use crate::{Claim, ClaimSet}; +use crate::{Claim, ClaimSet, InfallibleClaimSet, InvalidClaimValue}; /// Any set of JWT claims. #[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -50,31 +50,33 @@ impl IntoIterator for AnyClaims { } impl ClaimSet for AnyClaims { - type Error = serde_json::Error; - fn contains(&self) -> bool { self.contains(C::JWT_CLAIM_NAME) } - fn try_get(&self) -> Result>, Self::Error> { + fn try_get(&self) -> Result>, InvalidClaimValue> { self.get(C::JWT_CLAIM_NAME) .cloned() .map(serde_json::from_value) .transpose() + .map_err(Into::into) } - fn try_set(&mut self, claim: C) -> Result, Self::Error> { + fn try_set(&mut self, claim: C) -> Result, InvalidClaimValue> { self.set(C::JWT_CLAIM_NAME.to_owned(), serde_json::to_value(claim)?); Ok(Ok(())) } - fn try_remove(&mut self) -> Result, Self::Error> { + fn try_remove(&mut self) -> Result, InvalidClaimValue> { self.remove(C::JWT_CLAIM_NAME) .map(serde_json::from_value) .transpose() + .map_err(Into::into) } } +impl InfallibleClaimSet for AnyClaims {} + impl ValidateClaims for AnyClaims where E: DateTimeProvider, diff --git a/crates/claims/crates/jwt/src/claims/matching.rs b/crates/claims/crates/jwt/src/claims/matching.rs index 693cefc08..d4e396c65 100644 --- a/crates/claims/crates/jwt/src/claims/matching.rs +++ b/crates/claims/crates/jwt/src/claims/matching.rs @@ -162,7 +162,7 @@ mod tests { use serde::{Deserialize, Serialize}; use std::borrow::Cow; - use crate::{AnyClaims, Claim, ClaimSet}; + use crate::{AnyClaims, Claim, ClaimSet, InvalidClaimValue}; #[derive(Clone, Serialize, Deserialize)] struct CustomClaim; @@ -171,14 +171,13 @@ mod tests { const JWT_CLAIM_NAME: &'static str = "custom"; } + #[allow(unused)] struct CustomClaimSet { custom: Option, other_claims: AnyClaims, } impl ClaimSet for CustomClaimSet { - type Error = serde_json::Error; - fn contains(&self) -> bool { match_claim_type! { match C { @@ -188,7 +187,7 @@ mod tests { } } - fn try_get(&self) -> Result>, Self::Error> { + fn try_get(&self) -> Result>, InvalidClaimValue> { match_claim_type! { match C { CustomClaim => { @@ -201,7 +200,7 @@ mod tests { } } - fn try_set(&mut self, claim: C) -> Result, Self::Error> { + fn try_set(&mut self, claim: C) -> Result, InvalidClaimValue> { match_claim_type! { match claim: C { CustomClaim => { @@ -215,7 +214,7 @@ mod tests { } } - fn try_remove(&mut self) -> Result, Self::Error> { + fn try_remove(&mut self) -> Result, InvalidClaimValue> { match_claim_type! { match C { CustomClaim => { diff --git a/crates/claims/crates/jwt/src/claims/mixed/mod.rs b/crates/claims/crates/jwt/src/claims/mixed/mod.rs index 94f56606b..80aa2f8eb 100644 --- a/crates/claims/crates/jwt/src/claims/mixed/mod.rs +++ b/crates/claims/crates/jwt/src/claims/mixed/mod.rs @@ -1,10 +1,13 @@ use serde::Serialize; use ssi_claims_core::{ClaimsValidity, DateTimeProvider, ValidateClaims}; -use ssi_jws::{JWSPayload, ValidateJWSHeader}; +use ssi_jws::{JwsPayload, ValidateJwsHeader}; use std::borrow::Cow; use super::{Claim, InfallibleClaimSet, RegisteredClaims}; -use crate::{AnyClaims, ClaimSet}; +use crate::{ + AnyClaims, ClaimSet, ExpirationTime, InvalidClaimValue, IssuedAt, Issuer, RegisteredClaim, + Subject, TryIntoClaim, +}; mod de; @@ -20,6 +23,12 @@ pub struct JWTClaims { pub private: T, } +impl JWTClaims { + pub fn builder() -> JWTClaimsBuilder { + JWTClaimsBuilder::default() + } +} + impl JWTClaims { pub fn new() -> Self where @@ -37,27 +46,25 @@ impl JWTClaims { } impl ClaimSet for JWTClaims { - type Error = T::Error; - fn contains(&self) -> bool { ClaimSet::contains::(&self.registered) || self.private.contains::() } - fn try_get(&self) -> Result>, Self::Error> { + fn try_get(&self) -> Result>, InvalidClaimValue> { match InfallibleClaimSet::get(&self.registered) { Some(claim) => Ok(Some(claim)), None => self.private.try_get(), } } - fn try_set(&mut self, claim: C) -> Result, Self::Error> { + fn try_set(&mut self, claim: C) -> Result, InvalidClaimValue> { match InfallibleClaimSet::set(&mut self.registered, claim) { Ok(()) => Ok(Ok(())), Err(claim) => self.private.try_set(claim), } } - fn try_remove(&mut self) -> Result, Self::Error> { + fn try_remove(&mut self) -> Result, InvalidClaimValue> { match InfallibleClaimSet::remove(&mut self.registered) { Some(claim) => Ok(Some(claim)), None => self.private.try_remove(), @@ -65,7 +72,7 @@ impl ClaimSet for JWTClaims { } } -impl JWSPayload for JWTClaims { +impl JwsPayload for JWTClaims { fn typ(&self) -> Option<&'static str> { Some("JWT") } @@ -75,7 +82,7 @@ impl JWSPayload for JWTClaims { } } -impl ValidateJWSHeader for JWTClaims { +impl ValidateJwsHeader for JWTClaims { fn validate_jws_header(&self, _env: &E, _header: &ssi_jws::Header) -> ClaimsValidity { Ok(()) } @@ -90,3 +97,54 @@ where self.private.validate_claims(env, proof) } } + +#[derive(Default)] +pub struct JWTClaimsBuilder { + registered: RegisteredClaims, + error: bool, +} + +impl JWTClaimsBuilder { + pub fn set(mut self, value: impl TryIntoClaim) -> Self { + match value.try_into_claim() { + Ok(value) => { + self.registered.set(value); + } + Err(_) => self.error = true, + } + + self + } + + pub fn iss(self, value: impl TryIntoClaim) -> Self { + self.set(value) + } + + pub fn iat(self, value: impl TryIntoClaim) -> Self { + self.set(value) + } + + pub fn exp(self, value: impl TryIntoClaim) -> Self { + self.set(value) + } + + #[allow(clippy::should_implement_trait)] + pub fn sub(self, value: impl TryIntoClaim) -> Self { + self.set(value) + } + + pub fn with_private_claims(self, private: T) -> Result, InvalidJWTClaims> { + if self.error { + Err(InvalidJWTClaims) + } else { + Ok(JWTClaims { + registered: self.registered, + private, + }) + } + } +} + +#[derive(Debug, thiserror::Error)] +#[error("invalid JWT claims")] +pub struct InvalidJWTClaims; diff --git a/crates/claims/crates/jwt/src/claims/mod.rs b/crates/claims/crates/jwt/src/claims/mod.rs index 4cf443201..21c6f91ec 100644 --- a/crates/claims/crates/jwt/src/claims/mod.rs +++ b/crates/claims/crates/jwt/src/claims/mod.rs @@ -1,5 +1,4 @@ mod registered; -use core::fmt; use std::borrow::Cow; use chrono::Utc; @@ -13,6 +12,22 @@ pub use any::*; use serde::{de::DeserializeOwned, Serialize}; use ssi_claims_core::{ClaimsValidity, DateTimeProvider, InvalidClaims}; +#[derive(Debug, thiserror::Error)] +#[error("invalid claim value")] +pub struct InvalidClaimValue(String); + +impl InvalidClaimValue { + pub fn new(e: impl ToString) -> Self { + Self(e.to_string()) + } +} + +impl From for InvalidClaimValue { + fn from(value: serde_json::Error) -> Self { + Self::new(value) + } +} + /// JWT claim. pub trait Claim: 'static + Clone + Serialize + DeserializeOwned { /// Claim name, used as key in the JSON representation. @@ -20,15 +35,21 @@ pub trait Claim: 'static + Clone + Serialize + DeserializeOwned { } pub trait ClaimSet { - type Error: fmt::Display; - - fn contains(&self) -> bool; + fn contains(&self) -> bool { + false + } - fn try_get(&self) -> Result>, Self::Error>; + fn try_get(&self) -> Result>, InvalidClaimValue> { + Ok(None) + } - fn try_set(&mut self, claim: C) -> Result, Self::Error>; + fn try_set(&mut self, claim: C) -> Result, InvalidClaimValue> { + Ok(Err(claim)) + } - fn try_remove(&mut self) -> Result, Self::Error>; + fn try_remove(&mut self) -> Result, InvalidClaimValue> { + Ok(None) + } fn validate_registered_claims(&self, env: &E) -> ClaimsValidity where @@ -66,14 +87,6 @@ pub trait ClaimSet { /// Set of JWT claims. pub trait InfallibleClaimSet: ClaimSet { - fn get(&self) -> Option>; - - fn set(&mut self, claim: C) -> Result<(), C>; - - fn remove(&mut self) -> Option; -} - -impl> InfallibleClaimSet for T { fn get(&self) -> Option> { Self::try_get(self).unwrap() } diff --git a/crates/claims/crates/jwt/src/claims/registered.rs b/crates/claims/crates/jwt/src/claims/registered.rs index f4baed5b6..cc6e379ea 100644 --- a/crates/claims/crates/jwt/src/claims/registered.rs +++ b/crates/claims/crates/jwt/src/claims/registered.rs @@ -1,8 +1,8 @@ -use super::{Claim, JWTClaims}; -use crate::{CastClaim, ClaimSet, NumericDate, StringOrURI}; +use super::{Claim, InvalidClaimValue, JWTClaims}; +use crate::{CastClaim, ClaimSet, InfallibleClaimSet, NumericDate, StringOrURI}; use ssi_claims_core::{ClaimsValidity, DateTimeProvider, ValidateClaims}; use ssi_core::OneOrMany; -use ssi_jws::JWSPayload; +use ssi_jws::JwsPayload; use std::{borrow::Cow, collections::BTreeMap}; pub trait RegisteredClaim: Claim + Into { @@ -75,7 +75,9 @@ impl RegisteredClaims { } } -impl JWSPayload for RegisteredClaims { +impl InfallibleClaimSet for RegisteredClaims {} + +impl JwsPayload for RegisteredClaims { fn typ(&self) -> Option<&'static str> { Some("JWT") } @@ -153,6 +155,12 @@ impl<'de> serde::Deserialize<'de> for RegisteredClaims { } } +pub trait TryIntoClaim { + type Error; + + fn try_into_claim(self) -> Result; +} + macro_rules! registered_claims { ($($(#[$meta:meta])* $name:literal: $variant:ident ( $ty:ty )),*) => { $( @@ -166,6 +174,17 @@ macro_rules! registered_claims { const JWT_CLAIM_NAME: &'static str = $name; } + impl TryIntoClaim<$variant> for T + where + T: TryInto<$ty> + { + type Error = T::Error; + + fn try_into_claim(self) -> Result<$variant, Self::Error> { + self.try_into().map($variant) + } + } + impl RegisteredClaim for $variant { const JWT_REGISTERED_CLAIM_KIND: RegisteredClaimKind = RegisteredClaimKind::$variant; @@ -193,8 +212,6 @@ macro_rules! registered_claims { )* impl ClaimSet for RegisteredClaims { - type Error = std::convert::Infallible; - fn contains(&self) -> bool { $( if std::any::TypeId::of::() == std::any::TypeId::of::<$variant>() { @@ -205,7 +222,7 @@ macro_rules! registered_claims { false } - fn try_get(&self) -> Result>, Self::Error> { + fn try_get(&self) -> Result>, InvalidClaimValue> { $( if std::any::TypeId::of::() == std::any::TypeId::of::<$variant>() { return Ok(unsafe { CastClaim::cast_claim(self.get::<$variant>()) }.map(Cow::Borrowed)); @@ -215,7 +232,7 @@ macro_rules! registered_claims { Ok(None) } - fn try_set(&mut self, claim: C) -> Result, Self::Error> { + fn try_set(&mut self, claim: C) -> Result, InvalidClaimValue> { $( if std::any::TypeId::of::() == std::any::TypeId::of::<$variant>() { self.set::<$variant>(unsafe { CastClaim::cast_claim(claim) }); @@ -226,7 +243,7 @@ macro_rules! registered_claims { Ok(Err(claim)) } - fn try_remove(&mut self) -> Result, Self::Error> { + fn try_remove(&mut self) -> Result, InvalidClaimValue> { $( if std::any::TypeId::of::() == std::any::TypeId::of::<$variant>() { return Ok(unsafe { CastClaim::cast_claim(self.remove::<$variant>()) }); diff --git a/crates/claims/crates/jwt/src/datatype/numeric_date.rs b/crates/claims/crates/jwt/src/datatype/numeric_date.rs index 69495b4dd..9796fcbc2 100644 --- a/crates/claims/crates/jwt/src/datatype/numeric_date.rs +++ b/crates/claims/crates/jwt/src/datatype/numeric_date.rs @@ -111,8 +111,31 @@ impl std::ops::Sub for NumericDate { } } +impl From for NumericDate { + fn from(value: i32) -> Self { + Self(NotNan::new(value as f64).unwrap()) + } +} + +impl TryFrom for NumericDate { + type Error = NumericDateConversionError; + + fn try_from(value: i64) -> Result { + Self::try_from_seconds(value as f64) + } +} + +impl TryFrom for NumericDate { + type Error = NumericDateConversionError; + + fn try_from(value: f64) -> Result { + Self::try_from_seconds(value) + } +} + impl TryFrom> for NumericDate { type Error = NumericDateConversionError; + fn try_from(dtu: DateTime) -> Result { // Have to take seconds and nanoseconds separately in order to get the full allowable // range of microsecond-precision values as described above. diff --git a/crates/claims/crates/jwt/src/datatype/string_or_uri.rs b/crates/claims/crates/jwt/src/datatype/string_or_uri.rs index 8456b7f71..3e8e276f4 100644 --- a/crates/claims/crates/jwt/src/datatype/string_or_uri.rs +++ b/crates/claims/crates/jwt/src/datatype/string_or_uri.rs @@ -47,6 +47,7 @@ impl TryFrom for StringOrURI { } } } + impl TryFrom<&str> for StringOrURI { type Error = iref::InvalidUri; diff --git a/crates/claims/crates/jwt/src/decoding.rs b/crates/claims/crates/jwt/src/decoding.rs index 27e4e213f..d293dcd21 100644 --- a/crates/claims/crates/jwt/src/decoding.rs +++ b/crates/claims/crates/jwt/src/decoding.rs @@ -1,10 +1,7 @@ use serde::de::DeserializeOwned; use ssi_claims_core::{DateTimeProvider, ProofValidationError, ResolverProvider, Verification}; use ssi_jwk::JWKResolver; -use ssi_jws::{ - CompactJWS, CompactJWSBuf, CompactJWSStr, CompactJWSString, DecodeError as JWSDecodeError, - DecodedJWS, -}; +use ssi_jws::{DecodeError as JWSDecodeError, DecodedJws, JwsSlice, JwsStr, JwsString, JwsVec}; use crate::{AnyClaims, JWTClaims}; @@ -26,15 +23,15 @@ impl From for ProofValidationError { /// Decoded JWT. /// /// By definition this is a decoded JWS with JWT claims as payload. -pub type DecodedJWT = DecodedJWS>; +pub type DecodedJwt<'a, T = AnyClaims> = DecodedJws<'a, JWTClaims>; /// JWT borrowing decoding. -pub trait ToDecodedJWT { +pub trait ToDecodedJwt { /// Decodes a JWT with custom claims. - fn to_decoded_custom_jwt(&self) -> Result, DecodeError>; + fn to_decoded_custom_jwt(&self) -> Result, DecodeError>; /// Decodes a JWT. - fn to_decoded_jwt(&self) -> Result { + fn to_decoded_jwt(&self) -> Result { self.to_decoded_custom_jwt::() } @@ -52,49 +49,55 @@ pub trait ToDecodedJWT { } /// JWT consuming decoding. -pub trait IntoDecodedJWT: Sized { +pub trait IntoDecodedJwt: Sized { /// Decodes a JWT with custom claims. - fn into_decoded_custom_jwt(self) -> Result, DecodeError>; + fn into_decoded_custom_jwt( + self, + ) -> Result, DecodeError>; - fn into_decoded_jwt(self) -> Result { + fn into_decoded_jwt(self) -> Result, DecodeError> { self.into_decoded_custom_jwt::() } } -impl ToDecodedJWT for CompactJWS { - fn to_decoded_custom_jwt(&self) -> Result, DecodeError> { - self.to_decoded()? +impl ToDecodedJwt for JwsSlice { + fn to_decoded_custom_jwt(&self) -> Result, DecodeError> { + self.decode()? .try_map(|bytes| serde_json::from_slice(&bytes).map_err(Into::into)) } } -impl ToDecodedJWT for CompactJWSStr { - fn to_decoded_custom_jwt(&self) -> Result, DecodeError> { - CompactJWS::to_decoded_custom_jwt(self) +impl ToDecodedJwt for JwsStr { + fn to_decoded_custom_jwt(&self) -> Result, DecodeError> { + JwsSlice::to_decoded_custom_jwt(self) } } -impl ToDecodedJWT for CompactJWSBuf { - fn to_decoded_custom_jwt(&self) -> Result, DecodeError> { - CompactJWS::to_decoded_custom_jwt(self) +impl ToDecodedJwt for JwsVec { + fn to_decoded_custom_jwt(&self) -> Result, DecodeError> { + JwsSlice::to_decoded_custom_jwt(self) } } -impl IntoDecodedJWT for CompactJWSBuf { - fn into_decoded_custom_jwt(self) -> Result, DecodeError> { +impl IntoDecodedJwt for JwsVec { + fn into_decoded_custom_jwt( + self, + ) -> Result, DecodeError> { self.into_decoded()? .try_map(|bytes| serde_json::from_slice(&bytes).map_err(Into::into)) } } -impl ToDecodedJWT for CompactJWSString { - fn to_decoded_custom_jwt(&self) -> Result, DecodeError> { - CompactJWS::to_decoded_custom_jwt(self) +impl ToDecodedJwt for JwsString { + fn to_decoded_custom_jwt(&self) -> Result, DecodeError> { + JwsSlice::to_decoded_custom_jwt(self) } } -impl IntoDecodedJWT for CompactJWSString { - fn into_decoded_custom_jwt(self) -> Result, DecodeError> { +impl IntoDecodedJwt for JwsString { + fn into_decoded_custom_jwt( + self, + ) -> Result, DecodeError> { self.into_decoded()? .try_map(|bytes| serde_json::from_slice(&bytes).map_err(Into::into)) } diff --git a/crates/claims/crates/jwt/src/lib.rs b/crates/claims/crates/jwt/src/lib.rs index 9162dc44f..4a27ceac7 100644 --- a/crates/claims/crates/jwt/src/lib.rs +++ b/crates/claims/crates/jwt/src/lib.rs @@ -10,10 +10,10 @@ //! # async_std::task::block_on(async { //! use serde_json::json; //! use ssi_jwk::JWK; -//! use ssi_jws::CompactJWSStr; -//! use ssi_jwt::ToDecodedJWT; +//! use ssi_jws::Jws; +//! use ssi_jwt::ToDecodedJwt; //! -//! let jws = CompactJWSStr::new(b"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBTbWl0aCIsImlhdCI6MTcxNTM0Mjc5MCwiaXNzIjoiaHR0cDovL2V4YW1wbGUub3JnLyNpc3N1ZXIifQ.S51Gmlkwy4UxOhhc4nVl4_sHHVPSrNmjZDwJCDXDbKp2MT8-UyhZLw03gVKe-JRUzcsteWoeRCUoA5rwnuTSoA").unwrap(); +//! let jws = Jws::new(b"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBTbWl0aCIsImlhdCI6MTcxNTM0Mjc5MCwiaXNzIjoiaHR0cDovL2V4YW1wbGUub3JnLyNpc3N1ZXIifQ.S51Gmlkwy4UxOhhc4nVl4_sHHVPSrNmjZDwJCDXDbKp2MT8-UyhZLw03gVKe-JRUzcsteWoeRCUoA5rwnuTSoA").unwrap(); //! //! let jwk: JWK = json!({ //! "kty": "EC", @@ -28,24 +28,24 @@ //! # }) //! ``` //! -//! Internally [`ToDecodedJWT::verify_jwt`] uses -//! [`ToDecodedJWT::to_decoded_jwt`] to decode the JWT, -//! then [`DecodedJWS::verify`] to validate the signature and +//! Internally [`ToDecodedJwt::verify_jwt`] uses +//! [`ToDecodedJwt::to_decoded_jwt`] to decode the JWT, +//! then [`DecodedJws::verify`] to validate the signature and //! registered claims. //! -//! [`DecodedJWS::verify`]: ssi_jws::DecodedJWS::verify +//! [`DecodedJws::verify`]: ssi_jws::DecodedJws::verify //! //! ## Signature //! -//! Use the [`JWSPayload::sign`] method to sign a payload into a JWT. +//! Use the [`JwsPayload::sign`] method to sign a payload into a JWT. //! -//! [`JWSPayload::sign`]: ssi_jws::JWSPayload::sign +//! [`JwsPayload::sign`]: ssi_jws::JwsPayload::sign //! //! ``` //! # async_std::task::block_on(async { //! use serde_json::json; //! use ssi_jwk::JWK; -//! use ssi_jws::JWSPayload; +//! use ssi_jws::JwsPayload; //! use ssi_jwt::{JWTClaims, Issuer, IssuedAt, ExpirationTime}; //! //! let mut claims: JWTClaims = Default::default(); diff --git a/crates/claims/crates/sd-jwt/Cargo.toml b/crates/claims/crates/sd-jwt/Cargo.toml index a9f1207d6..f70e8aedb 100644 --- a/crates/claims/crates/sd-jwt/Cargo.toml +++ b/crates/claims/crates/sd-jwt/Cargo.toml @@ -14,11 +14,15 @@ rand.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true sha2.workspace = true +ssi-core.workspace = true +ssi-claims-core.workspace = true ssi-jwk.workspace = true ssi-jws.workspace = true ssi-jwt.workspace = true thiserror.workspace = true +indexmap.workspace = true [dev-dependencies] hex-literal = "0.4.1" -ssi-jws = { workspace = true, features = ["secp256r1"] } \ No newline at end of file +ssi-jws = { workspace = true, features = ["secp256r1"] } +async-std.workspace = true \ No newline at end of file diff --git a/crates/claims/crates/sd-jwt/src/conceal.rs b/crates/claims/crates/sd-jwt/src/conceal.rs new file mode 100644 index 000000000..ca8f74c1f --- /dev/null +++ b/crates/claims/crates/sd-jwt/src/conceal.rs @@ -0,0 +1,274 @@ +use std::borrow::Borrow; + +use base64::Engine; +use rand::{thread_rng, CryptoRng, RngCore}; +use serde::Serialize; +use serde_json::Value; +use ssi_claims_core::SignatureError; +use ssi_core::JsonPointer; +use ssi_jws::JwsSigner; +use ssi_jwt::JWTClaims; + +use crate::{ + DecodedDisclosure, Disclosure, DisclosureDescription, SdAlg, SdJwtBuf, SdJwtPayload, + ARRAY_CLAIM_ITEM_PROPERTY_NAME, SD_CLAIM_NAME, +}; + +/// Error that can occur during concealing. +#[derive(Debug, thiserror::Error)] +pub enum ConcealError { + /// Serialization failed. + #[error(transparent)] + Serialization(#[from] serde_json::Error), + + /// Concealed JSON value is not an object. + #[error("concealed JSON value is not an object")] + NotAnObject, + + /// Tried to conceal the root object. + #[error("cannot conceal root")] + CannotConcealRoot, + + /// Value to conceal not found. + #[error("value not found")] + NotFound, + + /// The `_sd` entry is not an array. + #[error("the `_sd` entry is not an array")] + SdEntryNotAnArray, +} + +/// JWT claims concealing methods. +pub trait ConcealJwtClaims { + /// Conceals these JWT claims. + fn conceal( + &self, + sd_alg: SdAlg, + pointers: &[impl Borrow], + ) -> Result<(SdJwtPayload, Vec>), ConcealError>; + + /// Conceals these JWT claims with the given `rng`. + fn conceal_with( + &self, + sd_alg: SdAlg, + pointers: &[impl Borrow], + rng: impl CryptoRng + RngCore, + ) -> Result<(SdJwtPayload, Vec>), ConcealError>; + + /// Conceals and signs these JWT claims. + #[allow(async_fn_in_trait)] + async fn conceal_and_sign( + &self, + sd_alg: SdAlg, + pointers: &[impl Borrow], + signer: impl JwsSigner, + ) -> Result; + + /// Conceals and signs these JWT claims with the given `rng`. + #[allow(async_fn_in_trait)] + async fn conceal_and_sign_with( + &self, + sd_alg: SdAlg, + pointers: &[impl Borrow], + signer: impl JwsSigner, + rng: impl CryptoRng + RngCore, + ) -> Result; +} + +impl ConcealJwtClaims for JWTClaims { + fn conceal( + &self, + sd_alg: SdAlg, + pointers: &[impl Borrow], + ) -> Result<(SdJwtPayload, Vec>), ConcealError> { + SdJwtPayload::conceal(self, sd_alg, pointers) + } + + fn conceal_with( + &self, + sd_alg: SdAlg, + pointers: &[impl Borrow], + rng: impl CryptoRng + RngCore, + ) -> Result<(SdJwtPayload, Vec>), ConcealError> { + SdJwtPayload::conceal_with(self, sd_alg, pointers, rng) + } + + async fn conceal_and_sign( + &self, + sd_alg: SdAlg, + pointers: &[impl Borrow], + signer: impl JwsSigner, + ) -> Result { + SdJwtBuf::conceal_and_sign(self, sd_alg, pointers, signer).await + } + + async fn conceal_and_sign_with( + &self, + sd_alg: SdAlg, + pointers: &[impl Borrow], + signer: impl JwsSigner, + rng: impl CryptoRng + RngCore, + ) -> Result { + SdJwtBuf::conceal_and_sign_with(self, sd_alg, pointers, signer, rng).await + } +} + +impl SdJwtPayload { + /// Conceal a value using the given JSON pointers, returning a SD-JWT + /// payload and disclosures. + pub fn conceal( + value: &T, + sd_alg: SdAlg, + pointers: &[impl Borrow], + ) -> Result<(Self, Vec>), ConcealError> { + Self::conceal_with(value, sd_alg, pointers, thread_rng()) + } + + /// Conceal a value using the given JSON pointers, returning a SD-JWT + /// payload and disclosures. + pub fn conceal_with( + value: &T, + sd_alg: SdAlg, + pointers: &[impl Borrow], + rng: impl CryptoRng + RngCore, + ) -> Result<(Self, Vec>), ConcealError> { + match serde_json::to_value(value)? { + Value::Object(obj) => Self::conceal_claims(obj, rng, sd_alg, pointers), + _ => Err(ConcealError::NotAnObject), + } + } + + /// Conceal a JSON object using the given JSON pointers, returning a SD-JWT + /// payload and disclosures. + pub fn conceal_claims( + mut claims: serde_json::Map, + mut rng: impl CryptoRng + RngCore, + sd_alg: SdAlg, + pointers: &[impl Borrow], + ) -> Result<(Self, Vec>), ConcealError> { + let mut disclosures = Vec::with_capacity(pointers.len()); + + // We sort the pointers here in order to visit parent pointers *after* + // child pointers (e.g. `/foo` after `/foo/bar`). Pointers are sorted + // parents-first in `sorted_pointers`, so we iterate over it in reverse. + let mut sorted_pointers: Vec<_> = pointers.iter().map(Borrow::borrow).collect(); + sorted_pointers.sort_unstable(); + + for pointer in pointers.iter().rev() { + disclosures.push(conceal_object_at( + &mut claims, + &mut rng, + sd_alg, + pointer.borrow(), + )?); + } + + let concealed = Self { sd_alg, claims }; + + Ok((concealed, disclosures)) + } +} + +fn generate_salt(rng: &mut (impl CryptoRng + RngCore)) -> String { + // TODO: link to rfc wrt suggested bit size of salt + const DEFAULT_SALT_SIZE: usize = 128 / 8; + let mut salt_bytes = [0u8; DEFAULT_SALT_SIZE]; + rng.fill_bytes(&mut salt_bytes); + base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(salt_bytes) +} + +fn conceal_at( + value: &mut Value, + rng: &mut (impl CryptoRng + RngCore), + sd_alg: SdAlg, + pointer: &JsonPointer, +) -> Result, ConcealError> { + match value { + Value::Object(object) => conceal_object_at(object, rng, sd_alg, pointer), + Value::Array(array) => conceal_array_at(array, rng, sd_alg, pointer), + _ => Err(ConcealError::CannotConcealRoot), + } +} + +fn conceal_object_at( + object: &mut serde_json::Map, + rng: &mut (impl CryptoRng + RngCore), + sd_alg: SdAlg, + pointer: &JsonPointer, +) -> Result, ConcealError> { + let (token, rest) = pointer + .split_first() + .ok_or(ConcealError::CannotConcealRoot)?; + + let key = token.to_decoded(); + + if rest.is_empty() { + let value = object.remove(&*key).ok_or(ConcealError::NotFound)?; + + let disclosure = DecodedDisclosure::from_parts( + generate_salt(rng), + DisclosureDescription::ObjectEntry { + key: key.into_owned(), + value, + }, + ); + + add_disclosure(object, sd_alg, &disclosure.encoded)?; + Ok(disclosure) + } else { + let value = object.get_mut(&*key).ok_or(ConcealError::NotFound)?; + + conceal_at(value, rng, sd_alg, rest) + } +} + +fn conceal_array_at( + array: &mut [Value], + rng: &mut (impl CryptoRng + RngCore), + sd_alg: SdAlg, + pointer: &JsonPointer, +) -> Result, ConcealError> { + let (token, rest) = pointer + .split_first() + .ok_or(ConcealError::CannotConcealRoot)?; + + let i = token.as_array_index().ok_or(ConcealError::NotFound)?; + + let value = array.get_mut(i).ok_or(ConcealError::NotFound)?; + + if rest.is_empty() { + let disclosure = DecodedDisclosure::from_parts( + generate_salt(rng), + DisclosureDescription::ArrayItem(value.take()), + ); + + *value = new_concealed_array_item(sd_alg, &disclosure.encoded); + Ok(disclosure) + } else { + conceal_at(value, rng, sd_alg, pointer) + } +} + +fn new_concealed_array_item(sd_alg: SdAlg, disclosure: &Disclosure) -> Value { + let mut object = serde_json::Map::new(); + object.insert( + ARRAY_CLAIM_ITEM_PROPERTY_NAME.into(), + sd_alg.hash(disclosure).into(), + ); + Value::Object(object) +} + +fn add_disclosure( + object: &mut serde_json::Map, + sd_alg: SdAlg, + disclosure: &Disclosure, +) -> Result<(), ConcealError> { + let sd = object + .entry(SD_CLAIM_NAME.to_owned()) + .or_insert_with(|| Value::Array(Vec::new())) + .as_array_mut() + .ok_or(ConcealError::SdEntryNotAnArray)?; + + sd.push(sd_alg.hash(disclosure).into()); + Ok(()) +} diff --git a/crates/claims/crates/sd-jwt/src/decode.rs b/crates/claims/crates/sd-jwt/src/decode.rs index 5f11c422a..44565027a 100644 --- a/crates/claims/crates/sd-jwt/src/decode.rs +++ b/crates/claims/crates/sd-jwt/src/decode.rs @@ -1,213 +1,97 @@ -use serde::de::DeserializeOwned; -use ssi_jwk::JWK; -use std::collections::BTreeMap; - -use crate::disclosure::{DecodedDisclosure, DisclosureKind}; -use crate::serialized::deserialize_string_format; -use crate::*; - -/// High level API to decode a fully encoded SD-JWT. That is a JWT and selective -/// disclosures separated by tildes -pub fn decode_verify( - serialized: &str, - key: &JWK, -) -> Result { - let deserialized = deserialize_string_format(serialized) - .ok_or(DecodeError::UnableToDeserializeStringFormat)?; - - decode_verify_disclosure_array(deserialized, key) +use crate::{DecodedDisclosure, DecodedSdJwt, Disclosure, PartsRef}; + +/// Errors in the decode pathway +#[derive(thiserror::Error, Debug)] +pub enum DecodeError { + /// Unable to decode undisclosed JWT. + #[error("Unable to decode undisclosed JWT: {0}")] + UndisclosedJWT(#[from] ssi_jws::DecodeError), + + /// Unable to deserialize string format of concatenated tildes + #[error("Unable to deserialize string format of concatenated tildes")] + UnableToDeserializeStringFormat, + + /// JWT payload claims were not a JSON object + #[error("JWT payload claims were not a JSON object")] + ClaimsWrongType, + + /// JWT is missing _sd_alg property + #[error("JWT is missing _sd_alg property")] + MissingSdAlg, + + /// Unknown value of _sd_alg + #[error("Unknown value of _sd_alg {0}")] + UnknownSdAlg(String), + + /// Type of _sd_alg was not string + #[error("Type of _sd_alg was not string")] + SdAlgWrongType, + + /// Multiple disclosures given with the same hash + #[error("Multiple disclosures given with the same hash")] + MultipleDisclosuresWithSameHash, + + /// An _sd claim wasn't a string + #[error("An _sd claim wasn't a string")] + SdClaimNotString, + + /// An _sd property was not an array type + #[error("An _sd property was not an array type")] + SdPropertyNotArray, + + /// A disclosure claim would collid with an existing JWT claim + #[error("A disclosure claim would collid with an existing JWT claim")] + DisclosureClaimCollidesWithJwtClaim, + + /// A disclosure is malformed + #[error("A disclosure is malformed")] + DisclosureMalformed, + + /// A single disclosure was used multiple times + #[error("A single disclosure was used multiple times")] + DisclosureUsedMultipleTimes, + + /// Found an array item disclosure when expecting a property type + #[error("Found an array item disclosure when expecting a property type")] + ArrayDisclosureWhenExpectingProperty, + + /// Found a property type disclosure when expecting an array item + #[error("Found a property type disclosure when expecting an array item")] + PropertyDisclosureWhenExpectingArray, + + /// A disclosure was not used during decoding + #[error("A disclosure was not used during decoding")] + UnusedDisclosure, + + /// Bubbled up error from ssi_jws + #[error(transparent)] + JWS(#[from] ssi_jws::Error), + + /// Bubbled up error from serde_json + #[error(transparent)] + JsonDeserialization(#[from] serde_json::Error), } -/// Lower level API to decode an SD-JWT that has already been split into its -/// JWT and disclosure components -pub fn decode_verify_disclosure_array( - deserialized: Deserialized<'_>, - key: &JWK, -) -> Result { - let mut payload_claims: serde_json::Value = ssi_jwt::decode_verify(deserialized.jwt, key)?; - - let sd_alg = extract_sd_alg(&mut payload_claims)?; - - let mut disclosures = translate_to_in_progress_disclosures(&deserialized.disclosures, sd_alg)?; - - visit_claims(&mut payload_claims, &mut disclosures)?; - - for (_, disclosure) in disclosures { - if !disclosure.found { - return Err(DecodeError::UnusedDisclosure); - } - } - - Ok(serde_json::from_value(payload_claims)?) -} - -fn extract_sd_alg(claims: &mut serde_json::Value) -> Result { - let claims = claims.as_object_mut().ok_or(DecodeError::ClaimsWrongType)?; - - let sd_alg_claim = claims - .remove(SD_ALG_CLAIM_NAME) - .ok_or(DecodeError::MissingSdAlg)?; - - let sd_alg = sd_alg_claim.as_str().ok_or(DecodeError::SdAlgWrongType)?; - - SdAlg::try_from(sd_alg) -} - -fn translate_to_in_progress_disclosures( - disclosures: &[&str], - sd_alg: SdAlg, -) -> Result, DecodeError> { - let disclosure_vec: Result, DecodeError> = disclosures - .iter() - .map(|disclosure| InProgressDisclosure::new(disclosure, sd_alg)) - .collect(); - - let disclosure_vec = disclosure_vec?; - - let mut disclosure_map = BTreeMap::new(); - for disclosure in disclosure_vec { - let prev = disclosure_map.insert(disclosure.hash.clone(), disclosure); - - if prev.is_some() { - return Err(DecodeError::MultipleDisclosuresWithSameHash); - } - } - - Ok(disclosure_map) -} - -#[derive(Debug)] -struct InProgressDisclosure { - decoded: DecodedDisclosure, - hash: String, - found: bool, -} - -impl InProgressDisclosure { - fn new(disclosure: &str, sd_alg: SdAlg) -> Result { - Ok(InProgressDisclosure { - decoded: DecodedDisclosure::new(disclosure)?, - hash: hash_encoded_disclosure(sd_alg, disclosure), - found: false, +impl<'a> PartsRef<'a> { + /// Decode the JWT-SD parts. + pub fn decode(self) -> Result, DecodeError> { + Ok(DecodedSdJwt { + jwt: self + .jwt + .decode()? + .try_map(|bytes| serde_json::from_slice(&bytes))?, + disclosures: self + .disclosures + .into_iter() + .map(Disclosure::decode) + .collect::>()?, }) } } -fn visit_claims( - payload_claims: &mut serde_json::Value, - disclosures: &mut BTreeMap, -) -> Result<(), DecodeError> { - let payload_claims = match payload_claims.as_object_mut() { - Some(obj) => obj, - None => return Ok(()), - }; - - // Visit children - for (_, child_claim) in payload_claims.iter_mut() { - visit_claims(child_claim, disclosures)? +impl Disclosure { + /// Decode this disclosure. + pub fn decode(&self) -> Result { + DecodedDisclosure::new(self) } - - // Process _sd claim - let new_claims = if let Some(sd_claims) = payload_claims.remove(SD_CLAIM_NAME) { - decode_sd_claims(&sd_claims, disclosures)? - } else { - vec![] - }; - - for (new_claim_name, mut new_claim_value) in new_claims { - visit_claims(&mut new_claim_value, disclosures)?; - - let prev = payload_claims.insert(new_claim_name, new_claim_value); - - if prev.is_some() { - return Err(DecodeError::DisclosureClaimCollidesWithJwtClaim); - } - } - - // Process array claims - for (_, item) in payload_claims.iter_mut() { - if let Some(array) = item.as_array_mut() { - let mut new_array_items = decode_array_claims(array, disclosures)?; - - for item in new_array_items.iter_mut() { - visit_claims(item, disclosures)?; - } - - *array = new_array_items; - } - } - - Ok(()) -} - -fn decode_sd_claims( - sd_claims: &serde_json::Value, - disclosures: &mut BTreeMap, -) -> Result, DecodeError> { - let sd_claims = sd_claims - .as_array() - .ok_or(DecodeError::SdPropertyNotArray)?; - let mut found_disclosures = vec![]; - for disclosure_hash in sd_claims { - let disclosure_hash = disclosure_hash - .as_str() - .ok_or(DecodeError::SdClaimNotString)?; - - if let Some(in_progress_disclosure) = disclosures.get_mut(disclosure_hash) { - if in_progress_disclosure.found { - return Err(DecodeError::DisclosureUsedMultipleTimes); - } - in_progress_disclosure.found = true; - match in_progress_disclosure.decoded.kind { - DisclosureKind::ArrayItem(_) => { - return Err(DecodeError::ArrayDisclosureWhenExpectingProperty) - } - DisclosureKind::Property { - ref name, - ref value, - } => found_disclosures.push((name.clone(), value.clone())), - } - } - } - - Ok(found_disclosures) -} - -fn decode_array_claims( - array: &[serde_json::Value], - disclosures: &mut BTreeMap, -) -> Result, DecodeError> { - let mut new_items = vec![]; - for item in array.iter() { - if let Some(hash) = array_item_is_disclosure(item) { - if let Some(in_progress_disclosure) = disclosures.get_mut(hash) { - if in_progress_disclosure.found { - return Err(DecodeError::DisclosureUsedMultipleTimes); - } - in_progress_disclosure.found = true; - match in_progress_disclosure.decoded.kind { - DisclosureKind::ArrayItem(ref value) => { - new_items.push(value.clone()); - } - DisclosureKind::Property { .. } => { - return Err(DecodeError::PropertyDisclosureWhenExpectingArray) - } - } - } - } else { - new_items.push(item.clone()); - } - } - - Ok(new_items) -} - -fn array_item_is_disclosure(item: &serde_json::Value) -> Option<&str> { - let obj = item.as_object()?; - - if obj.len() != 1 { - return None; - } - - obj.get(ARRAY_CLAIM_ITEM_PROPERTY_NAME)?.as_str() } diff --git a/crates/claims/crates/sd-jwt/src/digest.rs b/crates/claims/crates/sd-jwt/src/digest.rs index 523218a16..a69cb9dd2 100644 --- a/crates/claims/crates/sd-jwt/src/digest.rs +++ b/crates/claims/crates/sd-jwt/src/digest.rs @@ -1,7 +1,11 @@ +use std::str::FromStr; + use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine}; +use serde::{Deserialize, Serialize}; use sha2::Digest; +use ssi_jwt::Claim; -use crate::DecodeError; +use crate::{disclosure::Disclosure, DecodeError, SD_ALG_CLAIM_NAME}; /// Elements of the _sd_alg claim #[non_exhaustive] @@ -13,15 +17,35 @@ pub enum SdAlg { impl SdAlg { const SHA256_STR: &'static str = "sha-256"; -} -impl SdAlg { /// String encoding of _sd_alg field pub fn to_str(&self) -> &'static str { match self { SdAlg::Sha256 => Self::SHA256_STR, } } + + /// Hash the given disclosure. + pub fn hash(&self, disclosure: &Disclosure) -> String { + match self { + Self::Sha256 => { + let digest = sha2::Sha256::digest(disclosure.as_bytes()); + BASE64_URL_SAFE_NO_PAD.encode(digest) + } + } + } +} + +impl Claim for SdAlg { + const JWT_CLAIM_NAME: &'static str = SD_ALG_CLAIM_NAME; +} + +impl FromStr for SdAlg { + type Err = DecodeError; + + fn from_str(s: &str) -> Result { + SdAlg::try_from(s) + } } impl TryFrom<&str> for SdAlg { @@ -41,14 +65,23 @@ impl From for &'static str { } } -/// Lower level API to generate the hash of a given disclosure string already converted -/// into base 64 -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_URL_SAFE_NO_PAD.encode(digest) - } +impl Serialize for SdAlg { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.to_str().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for SdAlg { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + String::deserialize(deserializer)? + .parse() + .map_err(serde::de::Error::custom) } } @@ -59,9 +92,8 @@ mod tests { #[test] fn test_disclosure_hashing() { assert_eq!( - hash_encoded_disclosure( - SdAlg::Sha256, - "WyI2cU1RdlJMNWhhaiIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0" + SdAlg::Sha256.hash( + Disclosure::new("WyI2cU1RdlJMNWhhaiIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0").unwrap() ), "uutlBuYeMDyjLLTpf6Jxi7yNkEF35jdyWMn9U7b_RYY", ); diff --git a/crates/claims/crates/sd-jwt/src/disclosure.rs b/crates/claims/crates/sd-jwt/src/disclosure.rs index e4d685184..3faa406cc 100644 --- a/crates/claims/crates/sd-jwt/src/disclosure.rs +++ b/crates/claims/crates/sd-jwt/src/disclosure.rs @@ -1,74 +1,280 @@ -use crate::DecodeError; +use crate::{utils::is_url_safe_base64_char, DecodeError}; use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine}; +use serde_json::Value; +use std::{ + borrow::{Borrow, Cow}, + fmt, +}; -#[derive(Debug, PartialEq)] -pub struct DecodedDisclosure { - pub salt: String, - pub kind: DisclosureKind, +/// Invalid SD-JWT disclosure. +#[derive(Debug, thiserror::Error)] +#[error("invalid SD-JWT disclosure: `{0}`")] +pub struct InvalidDisclosure(pub T); + +/// Creates a static disclosure. +#[macro_export] +macro_rules! disclosure { + ($s:literal) => { + match $crate::Disclosure::from_str_const($s) { + Ok(d) => d, + Err(_) => panic!("invalid disclosure"), + } + }; } -#[derive(Debug, PartialEq)] -pub enum DisclosureKind { - Property { - name: String, - value: serde_json::Value, - }, - ArrayItem(serde_json::Value), +/// Encoded disclosure. +/// +/// An encoded disclosure is a url-safe base-64 string encoding (without +/// padding) an array containing the disclosure's parameters. +/// +/// See: +#[derive(PartialEq)] +pub struct Disclosure([u8]); + +impl Disclosure { + /// Parses the given `disclosure` bytes. + /// + /// Returns an error if the input value is not a valid url-safe base64 + /// string without padding. + pub fn new>(disclosure: &T) -> Result<&Self, InvalidDisclosure<&T>> { + let bytes = disclosure.as_ref(); + if bytes.iter().copied().all(is_url_safe_base64_char) { + Ok(unsafe { Self::new_unchecked(bytes) }) + } else { + Err(InvalidDisclosure(disclosure)) + } + } + + /// Parses the given `disclosure` string. + /// + /// Returns an error if the input string is not a valid url-safe base64 + /// string without padding. + /// + /// This function is limited to a `&str` input, but can be used in the const + /// context. + pub const fn from_str_const(disclosure: &str) -> Result<&Self, InvalidDisclosure<&str>> { + let bytes = disclosure.as_bytes(); + let mut i = 0; + + while i < bytes.len() { + if !is_url_safe_base64_char(bytes[i]) { + return Err(InvalidDisclosure(disclosure)); + } + + i += 1 + } + + Ok(unsafe { Self::new_unchecked(bytes) }) + } + + /// Creates a new disclosure out of the given `bytes` without validation. + /// + /// # Safety + /// + /// The input bytes **must** be a valid url-safe base64 string without + /// padding. + pub const unsafe fn new_unchecked(bytes: &[u8]) -> &Self { + std::mem::transmute(bytes) + } + + /// Returns underlying bytes of the disclosure. + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// Returns this disclosure as a string. + pub fn as_str(&self) -> &str { + unsafe { + // SAFETY: disclosures are url-safe base-64 strings. + std::str::from_utf8_unchecked(&self.0) + } + } +} + +impl AsRef<[u8]> for Disclosure { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl AsRef for Disclosure { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl fmt::Display for Disclosure { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_str().fmt(f) + } +} + +impl fmt::Debug for Disclosure { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_str().fmt(f) + } +} + +impl ToOwned for Disclosure { + type Owned = DisclosureBuf; + + fn to_owned(&self) -> Self::Owned { + DisclosureBuf(self.as_bytes().to_owned()) + } } -impl DecodedDisclosure { - pub fn new(encoded: &str) -> Result { +/// Owned disclosure. +pub struct DisclosureBuf(Vec); + +impl DisclosureBuf { + /// Creates a disclosure from its defining parts. + pub fn encode_from_parts(salt: &str, kind: &DisclosureDescription) -> Self { + Self( + BASE64_URL_SAFE_NO_PAD + .encode(kind.to_value(salt).to_string()) + .into_bytes(), + ) + } + + /// Borrows the disclosure. + pub fn as_disclosure(&self) -> &Disclosure { + unsafe { + // SAFETY: `self.0` is a disclosure by construction. + Disclosure::new_unchecked(&self.0) + } + } +} + +impl Borrow for DisclosureBuf { + fn borrow(&self) -> &Disclosure { + self.as_disclosure() + } +} + +impl fmt::Display for DisclosureBuf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_disclosure().fmt(f) + } +} + +impl fmt::Debug for DisclosureBuf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_disclosure().fmt(f) + } +} + +/// Decoded disclosure. +#[derive(Debug, Clone, PartialEq)] +pub struct DecodedDisclosure<'a> { + /// Encoded disclosure. + pub encoded: Cow<'a, Disclosure>, + + /// Salt. + pub salt: String, + + /// Disclosure description. + pub desc: DisclosureDescription, +} + +impl<'a> DecodedDisclosure<'a> { + /// Decodes the given encoded disclosure. + pub fn new(encoded: &'a (impl ?Sized + AsRef<[u8]>)) -> Result { + let base64 = encoded.as_ref(); let bytes = BASE64_URL_SAFE_NO_PAD - .decode(encoded) + .decode(base64) .map_err(|_| DecodeError::DisclosureMalformed)?; + + let encoded = unsafe { + // SAFETY: by decoding `base64` we validated the disclosure. + Disclosure::new_unchecked(base64) + }; + let json: serde_json::Value = serde_json::from_slice(&bytes)?; match json { serde_json::Value::Array(values) => match values.as_slice() { - [salt, name, value] => validate_property_disclosure(salt, name, value), - [salt, value] => validate_array_item_disclosure(salt, value), + [salt, name, value] => Ok(DecodedDisclosure { + encoded: Cow::Borrowed(encoded), + salt: salt + .as_str() + .ok_or(DecodeError::DisclosureMalformed)? + .to_owned(), + desc: DisclosureDescription::ObjectEntry { + key: name + .as_str() + .ok_or(DecodeError::DisclosureMalformed)? + .to_owned(), + value: value.clone(), + }, + }), + [salt, value] => Ok(DecodedDisclosure { + encoded: Cow::Borrowed(encoded), + salt: salt + .as_str() + .ok_or(DecodeError::DisclosureMalformed)? + .to_owned(), + desc: DisclosureDescription::ArrayItem(value.clone()), + }), _ => Err(DecodeError::DisclosureMalformed), }, _ => Err(DecodeError::DisclosureMalformed), } } + + /// Creates a decoded disclosure from its parts. + /// + /// The parts will be automatically encoded to populate the `encoded` + /// field. + pub fn from_parts(salt: String, kind: DisclosureDescription) -> Self { + Self { + encoded: Cow::Owned(DisclosureBuf::encode_from_parts(&salt, &kind)), + salt, + desc: kind, + } + } + + /// Clones the encoded disclosure to fully owned the decoded disclosure. + pub fn into_owned(self) -> DecodedDisclosure<'static> { + DecodedDisclosure { + encoded: Cow::Owned(self.encoded.into_owned()), + salt: self.salt, + desc: self.desc, + } + } } -fn validate_property_disclosure( - salt: &serde_json::Value, - name: &serde_json::Value, - value: &serde_json::Value, -) -> Result { - let salt = salt.as_str().ok_or(DecodeError::DisclosureMalformed)?; +/// Disclosure description. +#[derive(Debug, Clone, PartialEq)] +pub enum DisclosureDescription { + /// Object entry disclosure. + ObjectEntry { + /// Entry key. + key: String, - let name = name.as_str().ok_or(DecodeError::DisclosureMalformed)?; + /// Entry value. + value: serde_json::Value, + }, - Ok(DecodedDisclosure { - salt: salt.to_owned(), - kind: DisclosureKind::Property { - name: name.to_owned(), - value: value.clone(), - }, - }) + /// Array item disclosure. + ArrayItem(serde_json::Value), } -fn validate_array_item_disclosure( - salt: &serde_json::Value, - value: &serde_json::Value, -) -> Result { - let salt = salt.as_str().ok_or(DecodeError::DisclosureMalformed)?; - - Ok(DecodedDisclosure { - salt: salt.to_owned(), - kind: DisclosureKind::ArrayItem(value.clone()), - }) +impl DisclosureDescription { + /// Turns this disclosure description into a JSON value. + pub fn to_value(&self, salt: &str) -> Value { + match self { + Self::ObjectEntry { key, value } => { + Value::Array(vec![salt.into(), key.to_owned().into(), value.clone()]) + } + Self::ArrayItem(value) => Value::Array(vec![salt.into(), value.clone()]), + } + } } #[cfg(test)] mod tests { use super::*; - - use crate::digest::{hash_encoded_disclosure, SdAlg}; + use crate::SdAlg; fn verify_sd_disclosures_array( digest_algo: SdAlg, @@ -78,7 +284,7 @@ mod tests { let mut verfied_claims = serde_json::Map::new(); for disclosure in disclosures { - let disclosure_hash = hash_encoded_disclosure(digest_algo, disclosure); + let disclosure_hash = digest_algo.hash(Disclosure::new(disclosure).unwrap()); if !disclosure_hash_exists_in_sd_claims(&disclosure_hash, sd_claim) { continue; @@ -86,15 +292,15 @@ mod tests { let decoded = DecodedDisclosure::new(disclosure)?; - match decoded.kind { - DisclosureKind::Property { name, value } => { + match decoded.desc { + DisclosureDescription::ObjectEntry { key: name, value } => { let orig = verfied_claims.insert(name, value); if orig.is_some() { return Err(DecodeError::DisclosureUsedMultipleTimes); } } - DisclosureKind::ArrayItem(_) => { + DisclosureDescription::ArrayItem(_) => { return Err(DecodeError::ArrayDisclosureWhenExpectingProperty); } } @@ -182,11 +388,11 @@ mod tests { #[test] fn decode_array_disclosure() { assert_eq!( - DecodedDisclosure { - salt: "nPuoQnkRFq3BIeAm7AnXFA".to_owned(), - kind: DisclosureKind::ArrayItem(serde_json::json!("DE")) - }, - DecodedDisclosure::new("WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIkRFIl0").unwrap() + DecodedDisclosure::from_parts( + "nPuoQnkRFq3BIeAm7AnXFA".to_owned(), + DisclosureDescription::ArrayItem(serde_json::json!("DE")) + ), + DecodedDisclosure::new("WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwiREUiXQ").unwrap() ) } } diff --git a/crates/claims/crates/sd-jwt/src/encode.rs b/crates/claims/crates/sd-jwt/src/encode.rs deleted file mode 100644 index 6318b8125..000000000 --- a/crates/claims/crates/sd-jwt/src/encode.rs +++ /dev/null @@ -1,243 +0,0 @@ -use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine}; -use rand::{CryptoRng, Rng}; -use serde::Serialize; -use ssi_jwk::{Algorithm, JWK}; - -use crate::*; - -/// Disclosure as encoded -#[derive(Debug, PartialEq)] -pub struct Disclosure { - /// Base 64 of disclosure object - pub encoded: String, - - /// Base 64 of hash of disclosure object - pub hash: String, -} - -fn encode_disclosure_with_salt( - salt: &str, - claim_name: Option<&str>, - claim_value: &ClaimValue, -) -> Result { - let disclosure = match claim_name { - Some(claim_name) => serde_json::json!([salt, claim_name, claim_value]), - None => serde_json::json!([salt, claim_value]), - }; - - let json_string = serde_json::to_string(&disclosure)?; - - Ok(BASE64_URL_SAFE_NO_PAD.encode(json_string)) -} - -pub fn encode_disclosure_with_rng( - rng: &mut Rand, - claim_name: Option<&str>, - claim_value: &ClaimValue, -) -> Result { - // TODO: link to rfc wrt suggested bit size of salt - const DEFAULT_SALT_SIZE: usize = 128 / 8; - - let mut salt_bytes = [0u8; DEFAULT_SALT_SIZE]; - - rng.fill_bytes(&mut salt_bytes); - - let salt = BASE64_URL_SAFE_NO_PAD.encode(salt_bytes); - - encode_disclosure_with_salt(&salt, claim_name, claim_value) -} - -fn encode_disclosure( - claim_name: Option<&str>, - claim_value: &ClaimValue, -) -> Result { - let mut rng = rand::rngs::OsRng {}; - encode_disclosure_with_rng(&mut rng, claim_name, claim_value) -} - -/// Lower level API to create a property style disclosure -pub fn encode_property_disclosure( - sd_alg: SdAlg, - claim_name: &str, - claim_value: &ClaimValue, -) -> Result { - let encoded = encode_disclosure(Some(claim_name), claim_value)?; - let hash = hash_encoded_disclosure(sd_alg, &encoded); - - Ok(Disclosure { encoded, hash }) -} - -/// Lower level API to create an array style disclosure -pub fn encode_array_disclosure( - sd_alg: SdAlg, - claim_value: &ClaimValue, -) -> Result { - let encoded = encode_disclosure(None, claim_value)?; - let hash = hash_encoded_disclosure(sd_alg, &encoded); - - Ok(Disclosure { encoded, hash }) -} - -/// High level API to create most SD-JWTs -pub fn encode_sign( - algorithm: Algorithm, - base_claims: &Claims, - key: &JWK, - sd_alg: SdAlg, - disclosures: Vec, -) -> Result<(String, Vec), EncodeError> { - let mut base_claims_json = serde_json::to_value(base_claims)?; - - let post_encoded_disclosures: Result, EncodeError> = disclosures - .iter() - .map(|disclosure| { - let encoded = disclosure.encode()?; - let hash = hash_encoded_disclosure(sd_alg, &encoded); - Ok(FullDisclosure { - encoded, - hash, - unencoded: disclosure.clone(), - }) - }) - .collect(); - - let post_encoded_disclosures = post_encoded_disclosures?; - - { - let base_claims_obj = base_claims_json - .as_object_mut() - .ok_or(EncodeError::EncodedAsNonObject)?; - - let prev_sd_alg = base_claims_obj.insert( - SD_ALG_CLAIM_NAME.to_owned(), - serde_json::json!(sd_alg.to_str()), - ); - - if prev_sd_alg.is_some() { - return Err(EncodeError::EncodedClaimsContainsReservedProperty); - } - - let mut sd_claim = vec![]; - - for disclosure in post_encoded_disclosures.iter() { - match disclosure.unencoded { - UnencodedDisclosure::Property(ref claim_name, _) => { - sd_claim.push(serde_json::Value::String(disclosure.hash.clone())); - base_claims_obj.remove(claim_name); - } - UnencodedDisclosure::ArrayItem(ref claim_name, _) => { - if !base_claims_obj.contains_key(claim_name) { - let _ = base_claims_obj.insert(claim_name.clone(), serde_json::json!([])); - } - - // unwrap() justified as id statement above adds claim_name to the map if it - // doesn't previously exist - let array = base_claims_obj.get_mut(claim_name).unwrap(); - let array = array.as_array_mut().ok_or(EncodeError::ExpectedArray)?; - - array.push(serde_json::json!({ARRAY_CLAIM_ITEM_PROPERTY_NAME: disclosure.hash.clone()})); - } - } - } - - let prev_sd = - base_claims_obj.insert(SD_CLAIM_NAME.to_owned(), serde_json::Value::Array(sd_claim)); - - if prev_sd.is_some() { - return Err(EncodeError::EncodedClaimsContainsReservedProperty); - } - } - - let jwt = ssi_jwt::encode_sign(algorithm, &base_claims_json, key)?; - - Ok((jwt, post_encoded_disclosures)) -} - -/// Represents a disclosure before encoding -#[derive(Clone, Debug)] -pub enum UnencodedDisclosure { - /// Property style disclosure - Property(String, serde_json::Value), - - /// Array style disclosure - ArrayItem(String, serde_json::Value), -} - -impl UnencodedDisclosure { - /// Create a new property style UnencodedDisclosure - pub fn new_property, Value: Serialize>( - name: S, - value: &Value, - ) -> Result { - Ok(UnencodedDisclosure::Property( - name.as_ref().to_owned(), - serde_json::to_value(value)?, - )) - } - - /// Create a new array style UnencodedDisclosure - pub fn new_array_item, Value: Serialize>( - parent: S, - value: &Value, - ) -> Result { - Ok(UnencodedDisclosure::ArrayItem( - parent.as_ref().to_owned(), - serde_json::to_value(value)?, - )) - } - - /// Obtain reference to the disclosure's JSON object - pub fn claim_value_as_ref(&self) -> &serde_json::Value { - match self { - UnencodedDisclosure::ArrayItem(_, value) => value, - UnencodedDisclosure::Property(_, value) => value, - } - } - - /// Obtain reference to the disclosure's name if it is an array style - /// disclosure - pub fn encoded_claim_name(&self) -> Option<&str> { - match self { - UnencodedDisclosure::Property(name, _) => Some(name), - UnencodedDisclosure::ArrayItem(_, _) => None, - } - } - - /// Encode the disclosure into the plaintext base64 string encoding - pub fn encode(&self) -> Result { - encode_disclosure(self.encoded_claim_name(), self.claim_value_as_ref()) - } -} - -#[derive(Debug)] -pub struct FullDisclosure { - pub encoded: String, - pub hash: String, - pub unencoded: UnencodedDisclosure, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_encode_disclosure() { - assert_eq!( - encode_disclosure_with_salt( - "_26bc4LT-ac6q2KI6cBW5es", - Some("family_name"), - &"Möbius".to_owned(), - ) - .unwrap(), - "WyJfMjZiYzRMVC1hYzZxMktJNmNCVzVlcyIsImZhbWlseV9uYW1lIiwiTcO2Yml1cyJd", - ) - } - - #[test] - fn test_encode_array_disclosure() { - assert_eq!( - encode_disclosure_with_salt("nPuoQnkRFq3BIeAm7AnXFA", None, &"DE".to_owned()).unwrap(), - "WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwiREUiXQ" - ) - } -} diff --git a/crates/claims/crates/sd-jwt/src/error.rs b/crates/claims/crates/sd-jwt/src/error.rs deleted file mode 100644 index 37cbecf33..000000000 --- a/crates/claims/crates/sd-jwt/src/error.rs +++ /dev/null @@ -1,95 +0,0 @@ -/// Errors in the decode pathway -#[derive(thiserror::Error, Debug)] -pub enum DecodeError { - /// Unable to deserialize string format of concatenated tildes - #[error("Unable to deserialize string format of concatenated tildes")] - UnableToDeserializeStringFormat, - - /// JWT payload claims were not a JSON object - #[error("JWT payload claims were not a JSON object")] - ClaimsWrongType, - - /// JWT is missing _sd_alg property - #[error("JWT is missing _sd_alg property")] - MissingSdAlg, - - /// Unknown value of _sd_alg - #[error("Unknown value of _sd_alg {0}")] - UnknownSdAlg(String), - - /// Type of _sd_alg was not string - #[error("Type of _sd_alg was not string")] - SdAlgWrongType, - - /// Multiple disclosures given with the same hash - #[error("Multiple disclosures given with the same hash")] - MultipleDisclosuresWithSameHash, - - /// An _sd claim wasn't a string - #[error("An _sd claim wasn't a string")] - SdClaimNotString, - - /// An _sd property was not an array type - #[error("An _sd property was not an array type")] - SdPropertyNotArray, - - /// A disclosure claim would collid with an existing JWT claim - #[error("A disclosure claim would collid with an existing JWT claim")] - DisclosureClaimCollidesWithJwtClaim, - - /// A disclosure is malformed - #[error("A disclosure is malformed")] - DisclosureMalformed, - - /// A single disclosure was used multiple times - #[error("A single disclosure was used multiple times")] - DisclosureUsedMultipleTimes, - - /// Found an array item disclosure when expecting a property type - #[error("Found an array item disclosure when expecting a property type")] - ArrayDisclosureWhenExpectingProperty, - - /// Found a property type disclosure when expecting an array item - #[error("Found a property type disclosure when expecting an array item")] - PropertyDisclosureWhenExpectingArray, - - /// A disclosure was not used during decoding - #[error("A disclosure was not used during decoding")] - UnusedDisclosure, - - /// Bubbled up error from ssi_jws - #[error(transparent)] - JWS(#[from] ssi_jws::Error), - - /// Bubbled up error from serde_json - #[error(transparent)] - JsonDeserialization(#[from] serde_json::Error), -} - -/// Errors in the Encode pathway -#[derive(thiserror::Error, Debug)] -pub enum EncodeError { - /// The base claims to encode did not become a JSON object - #[error("The base claims to encode did not become a JSON object")] - EncodedAsNonObject, - - /// The base claims to encode contained a property reserved by SD-JWT - #[error("The base claims to encode contained a property reserved by SD-JWT")] - EncodedClaimsContainsReservedProperty, - - /// A property for an array sd claim was not an array - #[error("A property for an array sd claim was not an array")] - ExpectedArray, - - /// A disclosure was not used during decoding - #[error("A disclosure was not used during decoding")] - UnusedDisclosure, - - /// Bubbled up error from ssi_jws - #[error(transparent)] - JWS(#[from] ssi_jws::Error), - - /// Bubbled up error from serde_json - #[error(transparent)] - JsonSerialization(#[from] serde_json::Error), -} diff --git a/crates/claims/crates/sd-jwt/src/kb.rs b/crates/claims/crates/sd-jwt/src/kb.rs new file mode 100644 index 000000000..716d69126 --- /dev/null +++ b/crates/claims/crates/sd-jwt/src/kb.rs @@ -0,0 +1,67 @@ +pub struct SdJwtKb(str); + +impl SdJwtKb { + /// Returns references to each part of this SD-JWT. + pub fn parts(&self) -> PartsRef { + let mut chars = self.0.char_indices(); + + // Find issuer-signed JWT. + let jwt = loop { + if let Some((i, '~')) = chars.next() { + break unsafe { + // SAFETY: we already validated the SD-JWT and know it + // starts with a valid JWT. + Jws::new_unchecked(self.0[..i].as_bytes()) + } + } + }; + + let mut disclosures = Vec::new(); + let mut i = jwt.len() + 1; + + let key_binding_jwt = loop { + match chars.next() { + Some((j, '~')) => { + disclosures.push(unsafe { + // SAFETY: we already validated the SD-JWT and know + // it is composed of valid disclosures. + Disclosure::new_unchecked(self.0[i..j].as_bytes()) + }); + i = j + 1; + } + Some(_) => (), + None => { + break if i < self.0.len() { + Some(unsafe { + // SAFETY: we already validated the SD-JWT and know + // it ends with a valid JWT. + Jws::new_unchecked(self.0[i..].as_bytes()) + }) + } else { + None + } + } + } + }; + + PartsRef { + jwt, + disclosures, + key_binding_jwt + } + } +} + +/// SD-JWT components to be presented for decoding and validation whether coming +/// from a compact representation, enveloping JWT, etc. +#[derive(Debug, PartialEq)] +pub struct PartsRef<'a> { + /// JWT who's claims can be selectively disclosed. + pub jwt: &'a Jws, + + /// Disclosures for associated JWT + pub disclosures: Vec<&'a Disclosure>, + + /// Key binding JWT. + pub key_binding_jwt: Option<&'a Jws> +} \ No newline at end of file diff --git a/crates/claims/crates/sd-jwt/src/lib.rs b/crates/claims/crates/sd-jwt/src/lib.rs index 49ec746c1..dfb32979d 100644 --- a/crates/claims/crates/sd-jwt/src/lib.rs +++ b/crates/claims/crates/sd-jwt/src/lib.rs @@ -1,40 +1,947 @@ +//! Selective Disclosure for JWTs ([SD-JWT]). +//! +//! [SD-JWT]: +//! +//! # Usage +//! +//! Contrarily to regular JWTs or JWSs that can be verified directly after +//! being decoded, SD-JWTs claims need to be revealed before being validated. +//! The standard path looks like this: +//! ```text +//! ┌───────┐ ┌──────────────┐ ┌───────────────┐ +//! │ │ │ │ │ │ +//! │ SdJwt │ ─► SdJwt::decode ─► │ DecodedSdJwt │ ─► DecodedSdJwt::reveal ─► │ RevealedSdJwt │ +//! │ │ │ │ │ │ +//! └───────┘ └──────────────┘ └───────────────┘ +//! ``` +//! +//! The base SD-JWT type is [`SdJwt`] (or [`SdJwtBuf`] if you want to own the +//! SD-JWT). The [`SdJwt::decode`] function decodes the SD-JWT header, payload +//! and disclosures into a [`DecodedSdJwt`]. At this point the payload claims +//! are still concealed and cannot be validated. The [`DecodedSdJwt::reveal`] +//! function uses the disclosures to reveal the disclosed claims and discard +//! the non-disclosed claims. The result is a [`RevealedSdJwt`] containing the +//! revealed JWT, and a set of JSON pointers ([`JsonPointerBuf`]) mapping each +//! revealed claim to its disclosure. The [`RevealedSdJwt::verify`] function +//! can then be used to verify the JWT as usual. +//! +//! Alternatively, if you don't care about the byproducts of decoding and +//! revealing the claims, a [`SdJwt::decode_reveal_verify`] function is provided +//! to decode, reveal and verify the claims directly. #![warn(missing_docs)] +use rand::{CryptoRng, RngCore}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::Value; +use ssi_claims_core::{ + DateTimeProvider, ProofValidationError, ResolverProvider, SignatureError, ValidateClaims, + Verification, +}; +use ssi_core::{BytesBuf, JsonPointer, JsonPointerBuf}; +use ssi_jwk::JWKResolver; +use ssi_jws::{DecodedJws, Jws, JwsPayload, JwsSignature, JwsSigner, ValidateJwsHeader}; +use ssi_jwt::{AnyClaims, ClaimSet, DecodedJwt, JWTClaims}; +use std::{ + borrow::{Borrow, Cow}, + collections::BTreeMap, + fmt::{self, Write}, + ops::Deref, + str::FromStr, +}; -//! SSI library for processing SD-JWTs +pub(crate) mod utils; +use utils::is_url_safe_base64_char; + +mod digest; +pub use digest::*; mod decode; -pub(crate) mod digest; -pub(crate) mod disclosure; -pub(crate) mod encode; -mod error; -pub(crate) mod serialized; - -pub use decode::{decode_verify, decode_verify_disclosure_array}; -pub use digest::{hash_encoded_disclosure, SdAlg}; -pub use encode::{ - encode_array_disclosure, encode_property_disclosure, encode_sign, Disclosure, - UnencodedDisclosure, -}; -pub use error::{DecodeError, EncodeError}; -pub use serialized::{deserialize_string_format, serialize_string_format}; +pub use decode::*; + +mod disclosure; +pub use disclosure::*; + +mod conceal; +pub use conceal::*; + +mod reveal; +pub use reveal::*; const SD_CLAIM_NAME: &str = "_sd"; const SD_ALG_CLAIM_NAME: &str = "_sd_alg"; const ARRAY_CLAIM_ITEM_PROPERTY_NAME: &str = "..."; -/// SD-JWT components to be presented for decodindg and validation whtehre coming from -/// a compact representation, eveloping JWT, etc. +/// Invalid SD-JWT error. +#[derive(Debug, thiserror::Error)] +#[error("invalid SD-JWT: `{0}`")] +pub struct InvalidSdJwt(pub T); + +/// Creates a new static SD-JWT reference from a string literal. +#[macro_export] +#[collapse_debuginfo(no)] +macro_rules! sd_jwt { + ($value:literal) => { + match $crate::SdJwt::from_str_const($value) { + Ok(value) => value, + Err(_) => panic!("invalid SD-JWT"), + } + }; +} + +/// SD-JWT in compact form. +/// +/// # Grammar +/// +/// ```abnf +/// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z +/// DIGIT = %x30-39 ; 0-9 +/// BASE64URL = 1*(ALPHA / DIGIT / "-" / "_") +/// JWT = BASE64URL "." BASE64URL "." BASE64URL +/// DISCLOSURE = BASE64URL +/// SD-JWT = JWT "~" *[DISCLOSURE "~"] +/// ``` +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SdJwt([u8]); + +impl SdJwt { + /// Parses the given `input` as an SD-JWT. + /// + /// Returns an error if it is not a valid SD-JWT. + pub fn new>(input: &T) -> Result<&Self, InvalidSdJwt<&T>> { + let bytes = input.as_ref(); + if Self::validate(bytes) { + Ok(unsafe { Self::new_unchecked(bytes) }) + } else { + Err(InvalidSdJwt(input)) + } + } + + /// Parses the given `input` string as an SD-JWT. + /// + /// Returns an error if it is not a valid SD-JWT. + pub const fn from_str_const(input: &str) -> Result<&Self, InvalidSdJwt<&str>> { + let bytes = input.as_bytes(); + if Self::validate(bytes) { + Ok(unsafe { Self::new_unchecked(bytes) }) + } else { + Err(InvalidSdJwt(input)) + } + } + + /// Checks that the given input is a SD-JWT. + pub const fn validate(bytes: &[u8]) -> bool { + let mut i = 0; + + // Find the first `~`. + loop { + if i >= bytes.len() { + return false; + } + + if bytes[i] == b'~' { + break; + } + + i += 1 + } + + // Validate the JWS. + if !Jws::validate_range(bytes, 0, i) { + return false; + } + + // Parse disclosures. + loop { + // Skip the `~` + i += 1; + + // No more disclosures. + if i >= bytes.len() { + break true; + } + + loop { + if i >= bytes.len() { + // Missing terminating `~`. + return false; + } + + // End of disclosure. + if bytes[i] == b'~' { + break; + } + + // Not a disclosure. + if !is_url_safe_base64_char(bytes[i]) { + return false; + } + + i += 1 + } + } + } + + /// Creates a new SD-JWT from the given `input` without validation. + /// + /// # Safety + /// + /// The input value **must** be a valid SD-JWT. + pub const unsafe fn new_unchecked(input: &[u8]) -> &Self { + std::mem::transmute(input) + } + + /// Returns the underlying bytes of the SD-JWT. + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// Returns this SD-JWT as a string. + pub fn as_str(&self) -> &str { + unsafe { + // SAFETY: SD-JWT are valid UTF-8 strings by definition. + std::str::from_utf8_unchecked(&self.0) + } + } + + /// Returns the byte-position just after the issuer-signed JWT. + fn jwt_end(&self) -> usize { + self.0.iter().copied().position(|c| c == b'~').unwrap() + } + + /// Returns the issuer-signed JWT. + pub fn jwt(&self) -> &Jws { + unsafe { + // SAFETY: we already validated the SD-JWT and know it + // starts with a valid JWT. + Jws::new_unchecked(&self.0[..self.jwt_end()]) + } + } + + /// Returns an iterator over the disclosures of the SD-JWT. + pub fn disclosures(&self) -> Disclosures { + Disclosures { + bytes: &self.0, + offset: self.jwt_end() + 1, + } + } + + /// Returns references to each part of this SD-JWT. + pub fn parts(&self) -> PartsRef { + PartsRef { + jwt: self.jwt(), + disclosures: self.disclosures().collect(), + } + } + + /// Decode a compact SD-JWT. + pub fn decode(&self) -> Result { + self.parts().decode() + } + + /// Decodes and reveals the SD-JWT. + pub fn decode_reveal(&self) -> Result, RevealError> { + self.parts().decode_reveal() + } + + /// Decodes and reveals the SD-JWT. + pub fn decode_reveal_any(&self) -> Result { + self.parts().decode_reveal_any() + } + + /// Decode a compact SD-JWT. + pub async fn decode_verify_concealed

( + &self, + params: P, + ) -> Result<(DecodedSdJwt, Verification), ProofValidationError> + where + P: ResolverProvider, + { + self.parts().decode_verify_concealed(params).await + } + + /// Decodes, reveals and verify a compact SD-JWT. + /// + /// Only the registered JWT claims will be validated. + /// If you need to validate custom claims, use the + /// [`Self::decode_reveal_verify`] method with `T` defining the custom + /// claims. + /// + /// Returns the decoded JWT with the verification status. + pub async fn decode_reveal_verify_any

( + &self, + params: P, + ) -> Result<(RevealedSdJwt, Verification), ProofValidationError> + where + P: ResolverProvider + DateTimeProvider, + { + self.parts().decode_reveal_verify_any(params).await + } + + /// Decodes, reveals and verify a compact SD-JWT. + /// + /// The type parameter `T` corresponds to the set of private JWT claims + /// contained in the encoded SD-JWT. If you don't know what value to use + /// for this parameter, you can use the [`Self::decode_reveal_verify_any`] + /// function instead. + /// + /// Returns the decoded JWT with the verification status. + pub async fn decode_reveal_verify( + &self, + params: P, + ) -> Result<(RevealedSdJwt, Verification), ProofValidationError> + where + T: ClaimSet + DeserializeOwned + ValidateClaims, + P: ResolverProvider + DateTimeProvider, + { + self.parts().decode_reveal_verify(params).await + } +} + +impl AsRef for SdJwt { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl AsRef<[u8]> for SdJwt { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl fmt::Display for SdJwt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_str().fmt(f) + } +} + +impl fmt::Debug for SdJwt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_str().fmt(f) + } +} + +impl serde::Serialize for SdJwt { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.as_str().serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for &'de SdJwt { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + SdJwt::new(<&'de str>::deserialize(deserializer)?).map_err(serde::de::Error::custom) + } +} + +/// Owned SD-JWT. +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SdJwtBuf(Vec); + +impl SdJwtBuf { + /// Creates a new owned SD-JWT. + pub fn new(bytes: B) -> Result> { + if SdJwt::validate(bytes.as_ref()) { + Ok(Self(bytes.into())) + } else { + Err(InvalidSdJwt(bytes)) + } + } + + /// Creates a new owned SD-JWT without validating the input bytes. + /// + /// # Safety + /// + /// The input `bytes` **must** represent an SD-JWT. + pub unsafe fn new_unchecked(bytes: Vec) -> Self { + Self(bytes) + } + + /// Conceals and sign the given claims. + pub async fn conceal_and_sign( + claims: &JWTClaims, + sd_alg: SdAlg, + pointers: &[impl Borrow], + signer: impl JwsSigner, + ) -> Result { + DecodedSdJwt::conceal_and_sign(claims, sd_alg, pointers, signer) + .await + .map(DecodedSdJwt::into_encoded) + } + + /// Conceals and sign the given claims. + pub async fn conceal_and_sign_with( + claims: &JWTClaims, + sd_alg: SdAlg, + pointers: &[impl Borrow], + signer: impl JwsSigner, + rng: impl CryptoRng + RngCore, + ) -> Result { + DecodedSdJwt::conceal_and_sign_with(claims, sd_alg, pointers, signer, rng) + .await + .map(DecodedSdJwt::into_encoded) + } + + /// Borrows the SD-JWT. + pub fn as_sd_jwt(&self) -> &SdJwt { + unsafe { SdJwt::new_unchecked(&self.0) } + } +} + +impl Deref for SdJwtBuf { + type Target = SdJwt; + + fn deref(&self) -> &Self::Target { + self.as_sd_jwt() + } +} + +impl Borrow for SdJwtBuf { + fn borrow(&self) -> &SdJwt { + self.as_sd_jwt() + } +} + +impl AsRef for SdJwtBuf { + fn as_ref(&self) -> &SdJwt { + self.as_sd_jwt() + } +} + +impl AsRef for SdJwtBuf { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl AsRef<[u8]> for SdJwtBuf { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl fmt::Display for SdJwtBuf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_str().fmt(f) + } +} + +impl fmt::Debug for SdJwtBuf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_str().fmt(f) + } +} + +impl FromStr for SdJwtBuf { + type Err = InvalidSdJwt; + + fn from_str(s: &str) -> Result { + Self::new(s.to_owned()) + } +} + +impl serde::Serialize for SdJwtBuf { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.as_str().serialize(serializer) + } +} + +impl<'de> serde::Deserialize<'de> for SdJwtBuf { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + String::deserialize(deserializer)? + .parse() + .map_err(serde::de::Error::custom) + } +} + +/// Iterator over the disclosures of an SD-JWT. +pub struct Disclosures<'a> { + /// SD-JWT bytes. + bytes: &'a [u8], + + /// Offset of the beginning of the next disclosure (if any). + offset: usize, +} + +impl<'a> Iterator for Disclosures<'a> { + type Item = &'a Disclosure; + + fn next(&mut self) -> Option { + let mut i = self.offset; + + while i < self.bytes.len() { + if self.bytes[i] == b'~' { + let disclosure = unsafe { + // SAFETY: we already validated the SD-JWT and know + // it is composed of valid disclosures. + Disclosure::new_unchecked(&self.bytes[self.offset..i]) + }; + + self.offset = i + 1; + return Some(disclosure); + } + + i += 1 + } + + None + } +} + +/// SD-JWT components to be presented for decoding and validation whether coming +/// from a compact representation, enveloping JWT, etc. #[derive(Debug, PartialEq)] -pub struct Deserialized<'a> { - /// JWT who's claims can be selectively disclosed - pub jwt: &'a str, +pub struct PartsRef<'a> { + /// JWT who's claims can be selectively disclosed. + pub jwt: &'a Jws, + /// Disclosures for associated JWT - pub disclosures: Vec<&'a str>, + pub disclosures: Vec<&'a Disclosure>, +} + +impl<'a> PartsRef<'a> { + /// Creates a new `PartsRef`. + pub fn new(jwt: &'a Jws, disclosures: Vec<&'a Disclosure>) -> Self { + Self { jwt, disclosures } + } + + /// Decodes and reveals the SD-JWT. + pub fn decode_reveal(self) -> Result, RevealError> { + let decoded = self.decode()?; + decoded.reveal() + } + + /// Decodes and reveals the SD-JWT. + pub fn decode_reveal_any(self) -> Result, RevealError> { + let decoded = self.decode()?; + decoded.reveal_any() + } + + /// Decode a compact SD-JWT. + pub async fn decode_verify_concealed

( + self, + params: P, + ) -> Result<(DecodedSdJwt<'a>, Verification), ProofValidationError> + where + P: ResolverProvider, + { + let decoded = self.decode().map_err(ProofValidationError::input_data)?; + let verification = decoded.verify_concealed(params).await?; + Ok((decoded, verification)) + } + + /// Decodes, reveals and verify a compact SD-JWT. + /// + /// Only the registered JWT claims will be validated. + /// If you need to validate custom claims, use the + /// [`Self::decode_reveal_verify`] method with `T` defining the custom + /// claims. + /// + /// Returns the decoded JWT with the verification status. + pub async fn decode_reveal_verify_any

( + self, + params: P, + ) -> Result<(RevealedSdJwt<'a>, Verification), ProofValidationError> + where + P: ResolverProvider + DateTimeProvider, + { + let decoded = self.decode().map_err(ProofValidationError::input_data)?; + decoded.reveal_verify_any(params).await + } + + /// Decodes, reveals and verify a compact SD-JWT. + /// + /// The type parameter `T` corresponds to the set of private JWT claims + /// contained in the encoded SD-JWT. If you don't know what value to use + /// for this parameter, you can use the [`Self::decode_reveal_verify_any`] + /// function instead. + /// + /// Returns the decoded JWT with the verification status. + pub async fn decode_reveal_verify( + self, + params: P, + ) -> Result<(RevealedSdJwt<'a, T>, Verification), ProofValidationError> + where + T: ClaimSet + DeserializeOwned + ValidateClaims, + P: ResolverProvider + DateTimeProvider, + { + let decoded = self.decode().map_err(ProofValidationError::input_data)?; + decoded.reveal_verify(params).await + } +} + +impl<'a> fmt::Display for PartsRef<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.jwt.fmt(f)?; + f.write_char('~')?; + + for d in &self.disclosures { + d.fmt(f)?; + f.write_char('~')?; + } + + Ok(()) + } +} + +/// Undisclosed SD-JWT payload. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SdJwtPayload { + /// Hash algorithm used by the Issuer to generate the digests. + #[serde(rename = "_sd_alg")] + pub sd_alg: SdAlg, + + /// Other claims. + #[serde(flatten)] + pub claims: serde_json::Map, +} + +impl JwsPayload for SdJwtPayload { + fn payload_bytes(&self) -> Cow<[u8]> { + Cow::Owned(serde_json::to_vec(self).unwrap()) + } +} + +impl ValidateJwsHeader for SdJwtPayload {} + +impl ValidateClaims for SdJwtPayload {} + +/// Decoded SD-JWT. +pub struct DecodedSdJwt<'a> { + /// JWT who's claims can be selectively disclosed. + pub jwt: DecodedJws<'a, SdJwtPayload>, + + /// Disclosures for associated JWT. + pub disclosures: Vec>, +} + +impl<'a> DecodedSdJwt<'a> { + /// Verifies the decoded SD-JWT without revealing the concealed claims. + /// + /// No revealing the claims means only the registered JWT claims will be + /// validated. + pub async fn verify_concealed

(&self, params: P) -> Result + where + P: ResolverProvider, + { + self.jwt.verify(params).await + } + + /// Verifies the decoded SD-JWT after revealing the claims. + /// + /// Only the registered JWT claims will be validated. + /// If you need to validate custom claims, use the [`Self::reveal_verify`] + /// method with `T` defining the custom claims. + /// + /// Returns the decoded JWT with the verification status. + pub async fn reveal_verify_any

( + self, + params: P, + ) -> Result<(RevealedSdJwt<'a>, Verification), ProofValidationError> + where + P: ResolverProvider + DateTimeProvider, + { + let revealed = self + .reveal_any() + .map_err(ProofValidationError::input_data)?; + let verification = revealed.verify(params).await?; + Ok((revealed, verification)) + } + + /// Verifies the decoded SD-JWT after revealing the claims. + /// + /// The type parameter `T` corresponds to the set of private JWT claims. + /// If you don't know what value to use for this parameter, you can use the + /// [`Self::reveal_verify_any`] function instead. + /// + /// The `T` type parameter is the type of private claims. + pub async fn reveal_verify( + self, + params: P, + ) -> Result<(RevealedSdJwt<'a, T>, Verification), ProofValidationError> + where + T: ClaimSet + DeserializeOwned + ValidateClaims, + P: ResolverProvider + DateTimeProvider, + { + let revealed = self + .reveal::() + .map_err(ProofValidationError::input_data)?; + let verification = revealed.verify(params).await?; + Ok((revealed, verification)) + } } -impl<'a> Deserialized<'a> { - /// Convert Deserialized into a compact serialized format - pub fn compact_serialize(&self) -> String { - serialize_string_format(self.jwt, &self.disclosures) +impl DecodedSdJwt<'static> { + /// Conceal and sign the given claims. + pub async fn conceal_and_sign( + claims: &JWTClaims, + sd_alg: SdAlg, + pointers: &[impl Borrow], + signer: impl JwsSigner, + ) -> Result { + let (payload, disclosures) = + SdJwtPayload::conceal(claims, sd_alg, pointers).map_err(SignatureError::other)?; + + Ok(Self { + jwt: signer.sign_into_decoded(payload).await?, + disclosures, + }) + } + + /// Conceal and sign the given claims with a custom rng. + pub async fn conceal_and_sign_with( + claims: &JWTClaims, + sd_alg: SdAlg, + pointers: &[impl Borrow], + signer: impl JwsSigner, + rng: impl CryptoRng + RngCore, + ) -> Result { + let (payload, disclosures) = SdJwtPayload::conceal_with(claims, sd_alg, pointers, rng) + .map_err(SignatureError::other)?; + + Ok(Self { + jwt: signer.sign_into_decoded(payload).await?, + disclosures, + }) + } + + /// Encodes the SD-JWT. + pub fn into_encoded(self) -> SdJwtBuf { + let mut bytes = self.jwt.into_encoded().into_bytes(); + bytes.push(b'~'); + + for d in self.disclosures { + bytes.extend_from_slice(d.encoded.as_bytes()); + bytes.push(b'~'); + } + + unsafe { + // SAFETY: we just constructed those bytes following the SD-JWT + // syntax. + SdJwtBuf::new_unchecked(bytes) + } + } +} + +/// Revealed SD-JWT. +/// +/// This is similar to a [`DecodedSdJwt`] but with the JWT claims revealed. +/// You can use this type to access the revealed claims, and filter the +/// disclosures. +#[derive(Debug, Clone)] +pub struct RevealedSdJwt<'a, T = AnyClaims> { + /// Decoded JWT. + /// + /// The JWT bytes still contain the concealed SD-JWT claims, but the + /// decoded payload is revealed. + pub jwt: DecodedJwt<'a, T>, + + /// Disclosures bound to their JSON pointers. + pub disclosures: BTreeMap>, +} + +impl<'a, T> RevealedSdJwt<'a, T> { + /// Returns a reference to the revealed JWT claims. + pub fn claims(&self) -> &JWTClaims { + &self.jwt.signing_bytes.payload + } + + /// Turns this SD-JWT into its revealed JWT claims. + pub fn into_claims(self) -> JWTClaims { + self.jwt.signing_bytes.payload + } + + /// Verifies the SD-JWT, validating the revealed claims. + pub async fn verify

(&self, params: P) -> Result + where + T: ClaimSet + ValidateClaims, + P: ResolverProvider + DateTimeProvider, + { + self.jwt.verify(params).await + } + + /// Removes all the disclosures. + pub fn clear(&mut self) { + self.disclosures.clear() + } + + /// Removes all the disclosures. + pub fn cleared(mut self) -> Self { + self.clear(); + self + } + + /// Filter the disclosures, leaving only the ones targeting the given + /// JSON pointers. + /// + /// Returns a map containing the filtered-out disclosures and their + /// pointers. + pub fn retain( + &mut self, + pointers: &[impl Borrow], + ) -> BTreeMap> { + let mut disclosures = BTreeMap::new(); + + for p in pointers { + if let Some((p, d)) = self.disclosures.remove_entry(p.borrow()) { + disclosures.insert(p, d); + } + } + + std::mem::swap(&mut disclosures, &mut self.disclosures); + disclosures + } + + /// Filter the disclosures, leaving only the ones targeting the given + /// JSON pointers. + /// + /// Returns a map containing the filtered-out disclosures and their + /// pointers. + pub fn retaining(mut self, pointers: &[impl Borrow]) -> Self { + self.retain(pointers); + self + } + + /// Filter the disclosures, removing the ones targeting the given JSON + /// pointers. + /// + /// Returns a map containing the filtered-out disclosures and their + /// pointers. + pub fn reject( + &mut self, + pointers: &[impl Borrow], + ) -> BTreeMap> { + let mut disclosures = BTreeMap::new(); + + for p in pointers { + if let Some((p, d)) = self.disclosures.remove_entry(p.borrow()) { + disclosures.insert(p, d); + } + } + + disclosures + } + + /// Filter the disclosures, removing the ones targeting the given JSON + /// pointers. + pub fn rejecting(mut self, pointers: &[impl Borrow]) -> Self { + self.reject(pointers); + self + } + + /// Encodes the SD-JWT, re-concealing the claims. + pub fn into_encoded(self) -> SdJwtBuf { + let mut bytes = self.jwt.into_encoded().into_bytes(); + bytes.push(b'~'); + + for d in self.disclosures.into_values() { + bytes.extend_from_slice(d.encoded.as_bytes()); + bytes.push(b'~'); + } + + unsafe { + // SAFETY: we just constructed those bytes following the SD-JWT + // syntax. + SdJwtBuf::new_unchecked(bytes) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const ENCODED: &str = concat!( + "eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIkM5aW5wNllvUmFFWFI0Mjd6WUpQN1Fya", + "zFXSF84YmR3T0FfWVVyVW5HUVUiLCAiS3VldDF5QWEwSElRdlluT1ZkNTloY1ZpTzlVZ", + "zZKMmtTZnFZUkJlb3d2RSIsICJNTWxkT0ZGekIyZDB1bWxtcFRJYUdlcmhXZFVfUHBZZ", + "kx2S2hoX2ZfOWFZIiwgIlg2WkFZT0lJMnZQTjQwVjd4RXhad1Z3ejd5Um1MTmNWd3Q1R", + "Ew4Ukx2NGciLCAiWTM0em1JbzBRTExPdGRNcFhHd2pCZ0x2cjE3eUVoaFlUMEZHb2ZSL", + "WFJRSIsICJmeUdwMFdUd3dQdjJKRFFsbjFsU2lhZW9iWnNNV0ExMGJRNTk4OS05RFRzI", + "iwgIm9tbUZBaWNWVDhMR0hDQjB1eXd4N2ZZdW8zTUhZS08xNWN6LVJaRVlNNVEiLCAic", + "zBCS1lzTFd4UVFlVTh0VmxsdE03TUtzSVJUckVJYTFQa0ptcXhCQmY1VSJdLCAiaXNzI", + "jogImh0dHBzOi8vZXhhbXBsZS5jb20vaXNzdWVyIiwgImlhdCI6IDE2ODMwMDAwMDAsI", + "CJleHAiOiAxODgzMDAwMDAwLCAiYWRkcmVzcyI6IHsiX3NkIjogWyI2YVVoelloWjdTS", + "jFrVm1hZ1FBTzN1MkVUTjJDQzFhSGhlWnBLbmFGMF9FIiwgIkF6TGxGb2JrSjJ4aWF1c", + "FJFUHlvSnotOS1OU2xkQjZDZ2pyN2ZVeW9IemciLCAiUHp6Y1Z1MHFiTXVCR1NqdWxmZ", + "Xd6a2VzRDl6dXRPRXhuNUVXTndrclEtayIsICJiMkRrdzBqY0lGOXJHZzhfUEY4WmN2b", + "mNXN3p3Wmo1cnlCV3ZYZnJwemVrIiwgImNQWUpISVo4VnUtZjlDQ3lWdWIyVWZnRWs4a", + "nZ2WGV6d0sxcF9KbmVlWFEiLCAiZ2xUM2hyU1U3ZlNXZ3dGNVVEWm1Xd0JUdzMyZ25Vb", + "GRJaGk4aEdWQ2FWNCIsICJydkpkNmlxNlQ1ZWptc0JNb0d3dU5YaDlxQUFGQVRBY2k0M", + "G9pZEVlVnNBIiwgInVOSG9XWWhYc1poVkpDTkUyRHF5LXpxdDd0NjlnSkt5NVFhRnY3R", + "3JNWDQiXX0sICJfc2RfYWxnIjogInNoYS0yNTYifQ.rFsowW-KSZe7EITlWsGajR9nnG", + "BLlQ78qgtdGIZg3FZuZnxtapP0H8CUMnffJAwPQJmGnpFpulTkLWHiI1kMmw~WyJHMDJ", + "OU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICJcdTZlMmZcdTUzM2EiXQ~WyJs", + "a2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiSlAiXQ~" + ); + + const JWT: &str = concat!( + "eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIkM5aW5wNllvUmFFWFI0Mjd6WUpQN1Fya", + "zFXSF84YmR3T0FfWVVyVW5HUVUiLCAiS3VldDF5QWEwSElRdlluT1ZkNTloY1ZpTzlVZ", + "zZKMmtTZnFZUkJlb3d2RSIsICJNTWxkT0ZGekIyZDB1bWxtcFRJYUdlcmhXZFVfUHBZZ", + "kx2S2hoX2ZfOWFZIiwgIlg2WkFZT0lJMnZQTjQwVjd4RXhad1Z3ejd5Um1MTmNWd3Q1R", + "Ew4Ukx2NGciLCAiWTM0em1JbzBRTExPdGRNcFhHd2pCZ0x2cjE3eUVoaFlUMEZHb2ZSL", + "WFJRSIsICJmeUdwMFdUd3dQdjJKRFFsbjFsU2lhZW9iWnNNV0ExMGJRNTk4OS05RFRzI", + "iwgIm9tbUZBaWNWVDhMR0hDQjB1eXd4N2ZZdW8zTUhZS08xNWN6LVJaRVlNNVEiLCAic", + "zBCS1lzTFd4UVFlVTh0VmxsdE03TUtzSVJUckVJYTFQa0ptcXhCQmY1VSJdLCAiaXNzI", + "jogImh0dHBzOi8vZXhhbXBsZS5jb20vaXNzdWVyIiwgImlhdCI6IDE2ODMwMDAwMDAsI", + "CJleHAiOiAxODgzMDAwMDAwLCAiYWRkcmVzcyI6IHsiX3NkIjogWyI2YVVoelloWjdTS", + "jFrVm1hZ1FBTzN1MkVUTjJDQzFhSGhlWnBLbmFGMF9FIiwgIkF6TGxGb2JrSjJ4aWF1c", + "FJFUHlvSnotOS1OU2xkQjZDZ2pyN2ZVeW9IemciLCAiUHp6Y1Z1MHFiTXVCR1NqdWxmZ", + "Xd6a2VzRDl6dXRPRXhuNUVXTndrclEtayIsICJiMkRrdzBqY0lGOXJHZzhfUEY4WmN2b", + "mNXN3p3Wmo1cnlCV3ZYZnJwemVrIiwgImNQWUpISVo4VnUtZjlDQ3lWdWIyVWZnRWs4a", + "nZ2WGV6d0sxcF9KbmVlWFEiLCAiZ2xUM2hyU1U3ZlNXZ3dGNVVEWm1Xd0JUdzMyZ25Vb", + "GRJaGk4aEdWQ2FWNCIsICJydkpkNmlxNlQ1ZWptc0JNb0d3dU5YaDlxQUFGQVRBY2k0M", + "G9pZEVlVnNBIiwgInVOSG9XWWhYc1poVkpDTkUyRHF5LXpxdDd0NjlnSkt5NVFhRnY3R", + "3JNWDQiXX0sICJfc2RfYWxnIjogInNoYS0yNTYifQ.rFsowW-KSZe7EITlWsGajR9nnG", + "BLlQ78qgtdGIZg3FZuZnxtapP0H8CUMnffJAwPQJmGnpFpulTkLWHiI1kMmw" + ); + + const DISCLOSURE_0: &str = + "WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICJcdTZlMmZcdTUzM2EiXQ"; + const DISCLOSURE_1: &str = "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiSlAiXQ"; + + #[test] + fn deserialize() { + assert_eq!( + SdJwt::new(ENCODED).unwrap().parts(), + PartsRef::new( + Jws::new(JWT).unwrap(), + vec![ + Disclosure::new(DISCLOSURE_0).unwrap(), + Disclosure::new(DISCLOSURE_1).unwrap() + ] + ) + ) + } + + #[test] + fn deserialize_fails_with_emtpy() { + assert!(SdJwt::new("").is_err()) + } + + #[test] + fn serialize_parts() { + assert_eq!( + PartsRef::new( + Jws::new(JWT).unwrap(), + vec![ + Disclosure::new(DISCLOSURE_0).unwrap(), + Disclosure::new(DISCLOSURE_1).unwrap() + ] + ) + .to_string(), + ENCODED, + ) } } diff --git a/crates/claims/crates/sd-jwt/src/reveal.rs b/crates/claims/crates/sd-jwt/src/reveal.rs new file mode 100644 index 000000000..c344f7b9c --- /dev/null +++ b/crates/claims/crates/sd-jwt/src/reveal.rs @@ -0,0 +1,301 @@ +use crate::{ + disclosure::{DecodedDisclosure, DisclosureDescription}, + utils::TryRetainMut, + DecodeError, DecodedSdJwt, RevealedSdJwt, SdAlg, SdJwtPayload, ARRAY_CLAIM_ITEM_PROPERTY_NAME, + SD_CLAIM_NAME, +}; +use indexmap::IndexMap; +use serde::de::DeserializeOwned; +use serde_json::Value; +use ssi_core::{JsonPointer, JsonPointerBuf}; +use ssi_jwt::JWTClaims; + +/// Reveal error. +/// +/// Error type used by the [`DecodedSdJwt::reveal`] function. +#[derive(Debug, thiserror::Error)] +pub enum RevealError { + /// SD-JWT decoding failed. + #[error(transparent)] + Decode(#[from] DecodeError), + + /// Unused disclosure. + #[error("unused disclosure `{0:?}`")] + UnusedDisclosure(DecodedDisclosure<'static>), + + /// Claim collision. + #[error("claim collision")] + Collision, + + /// `_sd` claim value is not an array. + #[error("`_sd` claim value is not an array")] + SdClaimValueNotArray, + + /// Invalid disclosure hash. + #[error("invalid disclosure hash value")] + InvalidDisclosureHash, + + /// Disclosure used multiple times. + #[error("disclosure is used multiple times")] + DisclosureUsedMultipleTimes, + + /// Expected object entry, found array item disclosure. + #[error("expected object entry disclosure, found array item disclosure")] + ExpectedObjectEntryDisclosure, + + /// Expected array item disclosure, found object entry disclosure. + #[error("expected array item disclosure, found object entry disclosure")] + ExpectedArrayItemDisclosure, + + /// JSON deserialization failed. + #[error(transparent)] + Json(#[from] serde_json::Error), +} + +impl<'a> DecodedSdJwt<'a> { + /// Reveal the SD-JWT. + pub fn reveal(self) -> Result, RevealError> { + let mut pointers = Vec::with_capacity(self.disclosures.len()); + let jwt = self + .jwt + .try_map(|payload| payload.reveal(&self.disclosures, &mut pointers))?; + + Ok(RevealedSdJwt { + jwt, + disclosures: pointers.into_iter().zip(self.disclosures).collect(), + }) + } + + /// Reveal the SD-JWT. + pub fn reveal_any(self) -> Result, RevealError> { + self.reveal() + } +} + +impl SdJwtPayload { + /// Reveal the SD-JWT payload. + fn reveal( + &self, + disclosures: &[DecodedDisclosure], + pointers: &mut Vec, + ) -> Result, RevealError> { + eprintln!("payload: {}", serde_json::to_string_pretty(self).unwrap()); + eprintln!("disclosures: {disclosures:#?}"); + + let mut disclosures: IndexMap<_, _> = disclosures + .iter() + .map(|disclosure| { + let in_progress = InProgressDisclosure::new(disclosure, self.sd_alg); + (in_progress.hash.clone(), in_progress) + }) + .collect(); + + let mut disclosed_claims = self.claims.clone(); + reveal_object( + &JsonPointerBuf::default(), + &mut disclosed_claims, + &mut disclosures, + )?; + + for (_, disclosure) in disclosures { + pointers.push(disclosure.pointer.ok_or_else(|| { + RevealError::UnusedDisclosure(disclosure.disclosure.clone().into_owned()) + })?); + } + + serde_json::from_value(Value::Object(disclosed_claims)).map_err(Into::into) + } +} + +#[derive(Debug)] +struct InProgressDisclosure<'a> { + disclosure: &'a DecodedDisclosure<'a>, + hash: String, + pointer: Option, +} + +impl<'a> InProgressDisclosure<'a> { + fn new(disclosure: &'a DecodedDisclosure<'a>, sd_alg: SdAlg) -> Self { + InProgressDisclosure { + disclosure, + hash: sd_alg.hash(&disclosure.encoded), + pointer: None, + } + } +} + +fn reveal_value( + pointer: &JsonPointer, + value: &mut Value, + disclosures: &mut IndexMap, +) -> Result<(), RevealError> { + match value { + Value::Object(object) => reveal_object(pointer, object, disclosures), + Value::Array(array) => array.try_retain_mut(|i, item| { + let mut pointer = pointer.to_owned(); + pointer.push_index(i); + + match as_concealed_array_item(item) { + Some(hash) => match disclosures.get_mut(hash) { + Some(in_progress_disclosure) => match &in_progress_disclosure.disclosure.desc { + DisclosureDescription::ArrayItem(value) => { + if in_progress_disclosure + .pointer + .replace(pointer.clone()) + .is_some() + { + return Err(RevealError::DisclosureUsedMultipleTimes); + } + + *item = value.clone(); + reveal_value(&pointer, item, disclosures)?; + Ok(true) + } + DisclosureDescription::ObjectEntry { .. } => { + Err(RevealError::ExpectedArrayItemDisclosure) + } + }, + None => Ok(false), + }, + None => { + reveal_value(&pointer, item, disclosures)?; + Ok(true) + } + } + }), + _ => Ok(()), + } +} + +fn reveal_object( + pointer: &JsonPointer, + object: &mut serde_json::Map, + disclosures: &mut IndexMap, +) -> Result<(), RevealError> { + // Process `_sd` claim. + if let Some(sd_claims) = object.remove(SD_CLAIM_NAME) { + for (key, value) in reveal_sd_claim(pointer, &sd_claims, disclosures)? { + if object.insert(key, value).is_some() { + return Err(RevealError::Collision); + } + } + } + + // Visit sub-values. + for (key, sub_value) in object { + let mut pointer = pointer.to_owned(); + pointer.push(key); + reveal_value(&pointer, sub_value, disclosures)? + } + + Ok(()) +} + +fn reveal_sd_claim( + pointer: &JsonPointer, + sd_claim: &serde_json::Value, + disclosures: &mut IndexMap, +) -> Result, RevealError> { + let hashes = sd_claim + .as_array() + .ok_or(RevealError::SdClaimValueNotArray)?; + + let mut found_disclosures = vec![]; + + for disclosure_hash in hashes { + let disclosure_hash = disclosure_hash + .as_str() + .ok_or(RevealError::InvalidDisclosureHash)?; + + if let Some(in_progress_disclosure) = disclosures.get_mut(disclosure_hash) { + match &in_progress_disclosure.disclosure.desc { + DisclosureDescription::ArrayItem(_) => { + return Err(RevealError::ExpectedObjectEntryDisclosure) + } + DisclosureDescription::ObjectEntry { key, value } => { + let mut pointer = pointer.to_owned(); + pointer.push(key); + + if in_progress_disclosure.pointer.replace(pointer).is_some() { + return Err(RevealError::DisclosureUsedMultipleTimes); + } + + found_disclosures.push((key.clone(), value.clone())) + } + } + } + } + + Ok(found_disclosures) +} + +fn as_concealed_array_item(item: &serde_json::Value) -> Option<&str> { + let obj = item.as_object()?; + + if obj.len() != 1 { + return None; + } + + obj.get(ARRAY_CLAIM_ITEM_PROPERTY_NAME)?.as_str() +} + +#[cfg(test)] +mod tests { + use serde_json::json; + use std::cell::LazyCell; + + use crate::SdJwt; + + const SD_JWT: &str = concat!( + "eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIkM5aW5wNllvUmFFWFI0Mjd6WUpQN1Fya", + "zFXSF84YmR3T0FfWVVyVW5HUVUiLCAiS3VldDF5QWEwSElRdlluT1ZkNTloY1ZpTzlVZ", + "zZKMmtTZnFZUkJlb3d2RSIsICJNTWxkT0ZGekIyZDB1bWxtcFRJYUdlcmhXZFVfUHBZZ", + "kx2S2hoX2ZfOWFZIiwgIlg2WkFZT0lJMnZQTjQwVjd4RXhad1Z3ejd5Um1MTmNWd3Q1R", + "Ew4Ukx2NGciLCAiWTM0em1JbzBRTExPdGRNcFhHd2pCZ0x2cjE3eUVoaFlUMEZHb2ZSL", + "WFJRSIsICJmeUdwMFdUd3dQdjJKRFFsbjFsU2lhZW9iWnNNV0ExMGJRNTk4OS05RFRzI", + "iwgIm9tbUZBaWNWVDhMR0hDQjB1eXd4N2ZZdW8zTUhZS08xNWN6LVJaRVlNNVEiLCAic", + "zBCS1lzTFd4UVFlVTh0VmxsdE03TUtzSVJUckVJYTFQa0ptcXhCQmY1VSJdLCAiaXNzI", + "jogImh0dHBzOi8vZXhhbXBsZS5jb20vaXNzdWVyIiwgImlhdCI6IDE2ODMwMDAwMDAsI", + "CJleHAiOiAxODgzMDAwMDAwLCAiYWRkcmVzcyI6IHsiX3NkIjogWyI2YVVoelloWjdTS", + "jFrVm1hZ1FBTzN1MkVUTjJDQzFhSGhlWnBLbmFGMF9FIiwgIkF6TGxGb2JrSjJ4aWF1c", + "FJFUHlvSnotOS1OU2xkQjZDZ2pyN2ZVeW9IemciLCAiUHp6Y1Z1MHFiTXVCR1NqdWxmZ", + "Xd6a2VzRDl6dXRPRXhuNUVXTndrclEtayIsICJiMkRrdzBqY0lGOXJHZzhfUEY4WmN2b", + "mNXN3p3Wmo1cnlCV3ZYZnJwemVrIiwgImNQWUpISVo4VnUtZjlDQ3lWdWIyVWZnRWs4a", + "nZ2WGV6d0sxcF9KbmVlWFEiLCAiZ2xUM2hyU1U3ZlNXZ3dGNVVEWm1Xd0JUdzMyZ25Vb", + "GRJaGk4aEdWQ2FWNCIsICJydkpkNmlxNlQ1ZWptc0JNb0d3dU5YaDlxQUFGQVRBY2k0M", + "G9pZEVlVnNBIiwgInVOSG9XWWhYc1poVkpDTkUyRHF5LXpxdDd0NjlnSkt5NVFhRnY3R", + "3JNWDQiXX0sICJfc2RfYWxnIjogInNoYS0yNTYifQ.rFsowW-KSZe7EITlWsGajR9nnG", + "BLlQ78qgtdGIZg3FZuZnxtapP0H8CUMnffJAwPQJmGnpFpulTkLWHiI1kMmw~WyJHMDJ", + "OU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICJcdTZlMmZcdTUzM2EiXQ~WyJs", + "a2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiSlAiXQ~" + ); + + const DISCLOSED_CLAIMS: LazyCell = LazyCell::new(|| { + json!({ + "iss": "https://example.com/issuer", + "iat": 1683000000, + "exp": 1883000000, + // "sub": "6c5c0a49-b589-431d-bae7-219122a9ec2c", + // "given_name": "太郎", + // "family_name": "山田", + // "email": "\"unusual email address\"@example.jp", + // "phone_number": "+81-80-1234-5678", + "address": { + // "street_address": "東京都港区芝公園4丁目2−8", + // "locality": "東京都", + "region": "港区", + "country": "JP" + }, + // "birthdate": "1940-01-01" + }) + }); + + #[test] + fn disclose() { + let sd_jwt = SdJwt::new(SD_JWT).unwrap(); + let decoded = sd_jwt.decode().unwrap(); + let disclosed = decoded.reveal_any().unwrap(); + let output = serde_json::to_value(disclosed.claims()).unwrap(); + assert_eq!(output, *DISCLOSED_CLAIMS) + } +} diff --git a/crates/claims/crates/sd-jwt/src/serialized.rs b/crates/claims/crates/sd-jwt/src/serialized.rs deleted file mode 100644 index 034927d3b..000000000 --- a/crates/claims/crates/sd-jwt/src/serialized.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::Deserialized; - -/// Lower level API to encode a fully encoded SD-JWT given a JWT and disclosure array -/// already fully encoded into their string representations -pub fn serialize_string_format(jwt: &str, disclosures: &[&str]) -> String { - let mut serialized = format!("{}~", jwt); - - for disclosure in disclosures { - serialized = format!("{}{}~", serialized, disclosure) - } - - serialized -} - -/// Lower level API to seserialize a fully encoded SD-JWT into it's JWT and disclosure -/// components -pub fn deserialize_string_format(serialized: &str) -> Option> { - if !serialized.contains('~') { - return None; - } - let mut items = serialized.split('~'); - let jwt = items.next()?; - - let mut disclosures: Vec<_> = items.collect(); - - // Remove Key Binding JWT - if disclosures.len() > 1 { - disclosures.pop(); - } - - Some(Deserialized { jwt, disclosures }) -} - -#[cfg(test)] -mod tests { - use super::*; - - const ENCODED: &str = concat!( - "eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIkM5aW5wNllvUmFFWFI0Mjd6WUpQN1Fya", - "zFXSF84YmR3T0FfWVVyVW5HUVUiLCAiS3VldDF5QWEwSElRdlluT1ZkNTloY1ZpTzlVZ", - "zZKMmtTZnFZUkJlb3d2RSIsICJNTWxkT0ZGekIyZDB1bWxtcFRJYUdlcmhXZFVfUHBZZ", - "kx2S2hoX2ZfOWFZIiwgIlg2WkFZT0lJMnZQTjQwVjd4RXhad1Z3ejd5Um1MTmNWd3Q1R", - "Ew4Ukx2NGciLCAiWTM0em1JbzBRTExPdGRNcFhHd2pCZ0x2cjE3eUVoaFlUMEZHb2ZSL", - "WFJRSIsICJmeUdwMFdUd3dQdjJKRFFsbjFsU2lhZW9iWnNNV0ExMGJRNTk4OS05RFRzI", - "iwgIm9tbUZBaWNWVDhMR0hDQjB1eXd4N2ZZdW8zTUhZS08xNWN6LVJaRVlNNVEiLCAic", - "zBCS1lzTFd4UVFlVTh0VmxsdE03TUtzSVJUckVJYTFQa0ptcXhCQmY1VSJdLCAiaXNzI", - "jogImh0dHBzOi8vZXhhbXBsZS5jb20vaXNzdWVyIiwgImlhdCI6IDE2ODMwMDAwMDAsI", - "CJleHAiOiAxODgzMDAwMDAwLCAiYWRkcmVzcyI6IHsiX3NkIjogWyI2YVVoelloWjdTS", - "jFrVm1hZ1FBTzN1MkVUTjJDQzFhSGhlWnBLbmFGMF9FIiwgIkF6TGxGb2JrSjJ4aWF1c", - "FJFUHlvSnotOS1OU2xkQjZDZ2pyN2ZVeW9IemciLCAiUHp6Y1Z1MHFiTXVCR1NqdWxmZ", - "Xd6a2VzRDl6dXRPRXhuNUVXTndrclEtayIsICJiMkRrdzBqY0lGOXJHZzhfUEY4WmN2b", - "mNXN3p3Wmo1cnlCV3ZYZnJwemVrIiwgImNQWUpISVo4VnUtZjlDQ3lWdWIyVWZnRWs4a", - "nZ2WGV6d0sxcF9KbmVlWFEiLCAiZ2xUM2hyU1U3ZlNXZ3dGNVVEWm1Xd0JUdzMyZ25Vb", - "GRJaGk4aEdWQ2FWNCIsICJydkpkNmlxNlQ1ZWptc0JNb0d3dU5YaDlxQUFGQVRBY2k0M", - "G9pZEVlVnNBIiwgInVOSG9XWWhYc1poVkpDTkUyRHF5LXpxdDd0NjlnSkt5NVFhRnY3R", - "3JNWDQiXX0sICJfc2RfYWxnIjogInNoYS0yNTYifQ.rFsowW-KSZe7EITlWsGajR9nnG", - "BLlQ78qgtdGIZg3FZuZnxtapP0H8CUMnffJAwPQJmGnpFpulTkLWHiI1kMmw~WyJHMDJ", - "OU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICJcdTZlMmZcdTUzM2EiXQ~WyJs", - "a2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiSlAiXQ~" - ); - - const JWT: &str = concat!( - "eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIkM5aW5wNllvUmFFWFI0Mjd6WUpQN1Fya", - "zFXSF84YmR3T0FfWVVyVW5HUVUiLCAiS3VldDF5QWEwSElRdlluT1ZkNTloY1ZpTzlVZ", - "zZKMmtTZnFZUkJlb3d2RSIsICJNTWxkT0ZGekIyZDB1bWxtcFRJYUdlcmhXZFVfUHBZZ", - "kx2S2hoX2ZfOWFZIiwgIlg2WkFZT0lJMnZQTjQwVjd4RXhad1Z3ejd5Um1MTmNWd3Q1R", - "Ew4Ukx2NGciLCAiWTM0em1JbzBRTExPdGRNcFhHd2pCZ0x2cjE3eUVoaFlUMEZHb2ZSL", - "WFJRSIsICJmeUdwMFdUd3dQdjJKRFFsbjFsU2lhZW9iWnNNV0ExMGJRNTk4OS05RFRzI", - "iwgIm9tbUZBaWNWVDhMR0hDQjB1eXd4N2ZZdW8zTUhZS08xNWN6LVJaRVlNNVEiLCAic", - "zBCS1lzTFd4UVFlVTh0VmxsdE03TUtzSVJUckVJYTFQa0ptcXhCQmY1VSJdLCAiaXNzI", - "jogImh0dHBzOi8vZXhhbXBsZS5jb20vaXNzdWVyIiwgImlhdCI6IDE2ODMwMDAwMDAsI", - "CJleHAiOiAxODgzMDAwMDAwLCAiYWRkcmVzcyI6IHsiX3NkIjogWyI2YVVoelloWjdTS", - "jFrVm1hZ1FBTzN1MkVUTjJDQzFhSGhlWnBLbmFGMF9FIiwgIkF6TGxGb2JrSjJ4aWF1c", - "FJFUHlvSnotOS1OU2xkQjZDZ2pyN2ZVeW9IemciLCAiUHp6Y1Z1MHFiTXVCR1NqdWxmZ", - "Xd6a2VzRDl6dXRPRXhuNUVXTndrclEtayIsICJiMkRrdzBqY0lGOXJHZzhfUEY4WmN2b", - "mNXN3p3Wmo1cnlCV3ZYZnJwemVrIiwgImNQWUpISVo4VnUtZjlDQ3lWdWIyVWZnRWs4a", - "nZ2WGV6d0sxcF9KbmVlWFEiLCAiZ2xUM2hyU1U3ZlNXZ3dGNVVEWm1Xd0JUdzMyZ25Vb", - "GRJaGk4aEdWQ2FWNCIsICJydkpkNmlxNlQ1ZWptc0JNb0d3dU5YaDlxQUFGQVRBY2k0M", - "G9pZEVlVnNBIiwgInVOSG9XWWhYc1poVkpDTkUyRHF5LXpxdDd0NjlnSkt5NVFhRnY3R", - "3JNWDQiXX0sICJfc2RfYWxnIjogInNoYS0yNTYifQ.rFsowW-KSZe7EITlWsGajR9nnG", - "BLlQ78qgtdGIZg3FZuZnxtapP0H8CUMnffJAwPQJmGnpFpulTkLWHiI1kMmw" - ); - - const DISCLOSURE_0: &str = - "WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICJcdTZlMmZcdTUzM2EiXQ"; - const DISCLOSURE_1: &str = "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiSlAiXQ"; - - #[test] - fn serialize() { - assert_eq!( - serialize_string_format(JWT, &[DISCLOSURE_0, DISCLOSURE_1]), - ENCODED, - ) - } - - #[test] - fn deserialize() { - assert_eq!( - deserialize_string_format(ENCODED), - Some(Deserialized { - jwt: JWT, - disclosures: vec![DISCLOSURE_0, DISCLOSURE_1], - }) - ) - } - - #[test] - fn deserialize_fails_with_emtpy() { - assert_eq!(deserialize_string_format(""), None) - } -} diff --git a/crates/claims/crates/sd-jwt/src/utils.rs b/crates/claims/crates/sd-jwt/src/utils.rs new file mode 100644 index 000000000..db77e94a8 --- /dev/null +++ b/crates/claims/crates/sd-jwt/src/utils.rs @@ -0,0 +1,43 @@ +pub const fn is_url_safe_base64_char(b: u8) -> bool { + b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_') +} + +pub trait TryRetainMut { + type Item; + + fn try_retain_mut( + &mut self, + f: impl FnMut(usize, &mut Self::Item) -> Result, + ) -> Result<(), E>; +} + +impl TryRetainMut for Vec { + type Item = T; + + fn try_retain_mut( + &mut self, + mut f: impl FnMut(usize, &mut Self::Item) -> Result, + ) -> Result<(), E> { + let mut result = Ok(()); + + let mut i = 0; + self.retain_mut(|t| { + if result.is_ok() { + match f(i, t) { + Ok(retain) => { + i += 1; + retain + } + Err(e) => { + result = Err(e); + false + } + } + } else { + true + } + }); + + result + } +} diff --git a/crates/claims/crates/sd-jwt/tests/decode.rs b/crates/claims/crates/sd-jwt/tests/decode.rs index 93a388ad3..3e7c7f6e4 100644 --- a/crates/claims/crates/sd-jwt/tests/decode.rs +++ b/crates/claims/crates/sd-jwt/tests/decode.rs @@ -1,32 +1,59 @@ +use std::sync::LazyLock; + use serde::{Deserialize, Serialize}; -use ssi_jwk::{Algorithm, JWK}; -use ssi_jwt::NumericDate; -use ssi_sd_jwt::{decode_verify_disclosure_array, Deserialized}; +use serde_json::{json, Value}; +use ssi_jwk::JWK; +use ssi_jws::{JwsBuf, JwsPayload}; +use ssi_jwt::{JWTClaims, NumericDate}; +use ssi_sd_jwt::{disclosure, Disclosure, PartsRef}; #[derive(Debug, Default, Deserialize, Serialize, PartialEq)] struct ExampleClaims { - sub: Option, + #[serde(skip_serializing_if = "Option::is_none")] given_name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] family_name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] email: Option, + + #[serde(skip_serializing_if = "Option::is_none")] phone_number: Option, + + #[serde(skip_serializing_if = "Option::is_none")] phone_number_verified: Option, + + #[serde(skip_serializing_if = "Option::is_none")] address: Option, + + #[serde(skip_serializing_if = "Option::is_none")] birthdate: Option, + + #[serde(skip_serializing_if = "Option::is_none")] updated_at: Option, + + #[serde(skip_serializing_if = "Option::is_none")] nationalities: Option>, } #[derive(Debug, Default, Deserialize, Serialize, PartialEq)] struct AddressClaim { + #[serde(skip_serializing_if = "Option::is_none")] street_address: Option, + + #[serde(skip_serializing_if = "Option::is_none")] locality: Option, + + #[serde(skip_serializing_if = "Option::is_none")] region: Option, + + #[serde(skip_serializing_if = "Option::is_none")] country: Option, } -fn test_key() -> JWK { - serde_json::from_value(serde_json::json!({ +static JWK: LazyLock = LazyLock::new(|| { + serde_json::json!({ "kty": "EC", "d": "oYVImrMZjUclmWuhqa6bjzqGx5HFkbx76_00oWUHiLw", "use": "sig", @@ -35,13 +62,13 @@ fn test_key() -> JWK { "x": "UX7TC8uQ9sn06c3DxXy1Ua5V9BK-cb9fQfukVrCLD8s", "y": "yNXRKOnwBMTx536uajfNHklxpG9bAbdLlmVn6-XuK0Q", "alg": "ES256" - })) + }) + .try_into() .unwrap() -} +}); -fn test_standard_sd_jwt() -> String { - let key = test_key(); - let claims = serde_json::json!({ +static UNDISCLOSED_CLAIMS: LazyLock = LazyLock::new(|| { + json!({ "_sd": [ "CrQe7S5kqBAHt-nMYXgc6bdt2SH5aTY1sU_M-PgkjPI", "JzYjH4svliH0R3PyEMfeZu6Jt69u5qehZo7F7EPYlSE", @@ -61,9 +88,11 @@ fn test_standard_sd_jwt() -> String { { "...": "7Cf6JkPudry3lcbwHgeZ8khAv1U1OSlerP0VkBJrWZ0" } ], "_sd_alg": "sha-256" - }); + }) +}); - ssi_jwt::encode_sign(Algorithm::ES256, &claims, &key).unwrap() +async fn test_standard_sd_jwt() -> JwsBuf { + (*UNDISCLOSED_CLAIMS).sign(&*JWK).await.unwrap() } // *Claim email*: @@ -73,55 +102,68 @@ fn test_standard_sd_jwt() -> String { // ZXhhbXBsZS5jb20iXQ // * Contents: ["6Ij7tM-a5iVPGboS5tmvVA", "email", // "johndoe@example.com"] -const EMAIL_DISCLOSURE: &'static str = - "WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ"; +const EMAIL_DISCLOSURE: &Disclosure = + disclosure!("WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ"); // *Array Entry*: // * SHA-256 Hash: 7Cf6JkPudry3lcbwHgeZ8khAv1U1OSlerP0VkBJrWZ0 // * Disclosure: // WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIkRFIl0 // * Contents: ["nPuoQnkRFq3BIeAm7AnXFA", "DE"] -const NATIONALITY_DE_DISCLOSURE: &'static str = "WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIkRFIl0"; - -#[test] -fn decode_single() { - let claims = decode_verify_disclosure_array::( - Deserialized { - jwt: &test_standard_sd_jwt(), - disclosures: vec![EMAIL_DISCLOSURE], - }, - &test_key(), - ) - .unwrap(); +const NATIONALITY_DE_DISCLOSURE: &Disclosure = + disclosure!("WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIkRFIl0"); - assert_eq!( - claims, - ExampleClaims { - sub: Some("user_42".to_owned()), +#[async_std::test] +async fn disclose_single() { + let jwt = test_standard_sd_jwt().await; + + let sd_jwt = PartsRef::new(&jwt, vec![EMAIL_DISCLOSURE]); + + let disclosed = sd_jwt.decode().unwrap().reveal::().unwrap(); + + let expected = JWTClaims::builder() + .iss("https://example.com/issuer") + .iat(1683000000) + .exp(1883000000) + .sub("user_42") + .with_private_claims(ExampleClaims { email: Some("johndoe@example.com".to_owned()), nationalities: Some(vec![]), ..Default::default() - }, - ) + }) + .unwrap(); + + eprintln!( + "found = {}", + serde_json::to_string_pretty(disclosed.claims()).unwrap() + ); + eprintln!( + "expected = {}", + serde_json::to_string_pretty(&expected).unwrap() + ); + + assert_eq!(disclosed.into_claims(), expected); } -#[test] -fn decode_single_array_item() { - let claims = decode_verify_disclosure_array::( - Deserialized { - jwt: &test_standard_sd_jwt(), - disclosures: vec![NATIONALITY_DE_DISCLOSURE], - }, - &test_key(), - ) - .unwrap(); +#[async_std::test] +async fn decode_single_array_item() { + let jwt = test_standard_sd_jwt().await; + + let sd_jwt = PartsRef::new(&jwt, vec![NATIONALITY_DE_DISCLOSURE]); + + let disclosed = sd_jwt.decode().unwrap().reveal::().unwrap(); assert_eq!( - claims, - ExampleClaims { - sub: Some("user_42".to_owned()), - nationalities: Some(vec!["DE".to_owned()]), - ..Default::default() - }, + disclosed.into_claims(), + JWTClaims::builder() + .iss("https://example.com/issuer") + .iat(1683000000) + .exp(1883000000) + .sub("user_42") + .with_private_claims(ExampleClaims { + nationalities: Some(vec!["DE".to_owned()]), + ..Default::default() + }) + .unwrap() ) } diff --git a/crates/claims/crates/sd-jwt/tests/full_pathway.rs b/crates/claims/crates/sd-jwt/tests/full_pathway.rs index 983b83916..be492290c 100644 --- a/crates/claims/crates/sd-jwt/tests/full_pathway.rs +++ b/crates/claims/crates/sd-jwt/tests/full_pathway.rs @@ -1,10 +1,15 @@ +use std::sync::LazyLock; + use serde::{Deserialize, Serialize}; use serde_json::json; -use ssi_jwk::{Algorithm, JWK}; +use ssi_claims_core::{ValidateClaims, VerificationParameters}; +use ssi_core::json_pointer; +use ssi_jwk::JWK; +use ssi_jwt::{ClaimSet, JWTClaims}; use ssi_sd_jwt::*; -fn test_key() -> JWK { - serde_json::from_value(serde_json::json!({ +static JWK: LazyLock = LazyLock::new(|| { + json!({ "kty": "EC", "d": "oYVImrMZjUclmWuhqa6bjzqGx5HFkbx76_00oWUHiLw", "use": "sig", @@ -13,235 +18,236 @@ fn test_key() -> JWK { "x": "UX7TC8uQ9sn06c3DxXy1Ua5V9BK-cb9fQfukVrCLD8s", "y": "yNXRKOnwBMTx536uajfNHklxpG9bAbdLlmVn6-XuK0Q", "alg": "ES256" - })) + }) + .try_into() .unwrap() -} +}); -#[test] -fn full_pathway_regular_claim() { +#[async_std::test] +async fn full_pathway_regular_claim() { #[derive(Debug, Serialize, Deserialize, PartialEq)] struct BaseClaims { - sub: String, - disclosure0: Option, - disclosure1: Option, + property0: Option, + property1: Option, } - let base_claims = BaseClaims { - sub: "user".to_owned(), - disclosure0: None, - disclosure1: None, - }; - - let (jwt, disclosures) = encode_sign( - Algorithm::ES256, - &base_claims, - &test_key(), - SdAlg::Sha256, - vec![ - UnencodedDisclosure::new_property("disclosure0", &json!("value0")).unwrap(), - UnencodedDisclosure::new_property("disclosure1", &json!("value1")).unwrap(), - ], - ) - .unwrap(); - - let full_jwt_claims = decode_verify_disclosure_array::( - Deserialized { - jwt: &jwt, - disclosures: vec![&disclosures[0].encoded, &disclosures[1].encoded], - }, - &test_key(), - ) - .unwrap(); + impl ClaimSet for BaseClaims {} + impl ValidateClaims for BaseClaims {} - assert_eq!( - BaseClaims { - sub: "user".to_owned(), - disclosure0: Some("value0".to_owned()), - disclosure1: Some("value1".to_owned()), - }, - full_jwt_claims, - ); + let base_claims = JWTClaims::builder() + .sub("user") + .with_private_claims(BaseClaims { + property0: Some("value0".to_owned()), + property1: Some("value1".to_owned()), + }) + .unwrap(); + + let sd_jwt = base_claims + .conceal_and_sign( + SdAlg::Sha256, + &[json_pointer!("/property0"), json_pointer!("/property1")], + &*JWK, + ) + .await + .unwrap(); + + let params = VerificationParameters::from_resolver(&*JWK); - let one_sd_claim = decode_verify_disclosure_array::( - Deserialized { - jwt: &jwt, - disclosures: vec![&disclosures[1].encoded], - }, - &test_key(), - ) - .unwrap(); + let (mut revealed, verification) = sd_jwt + .decode_reveal_verify::(¶ms) + .await + .unwrap(); + assert_eq!(verification, Ok(())); + assert_eq!(*revealed.claims(), base_claims); + + // Retain only the `property1` property disclosure. + revealed.retain(&[json_pointer!("/property1")]); + + let sd_jwt = revealed.into_encoded(); + + let (revealed, verification) = sd_jwt + .decode_reveal_verify::(params) + .await + .unwrap(); + + assert_eq!(verification, Ok(())); assert_eq!( - BaseClaims { - sub: "user".to_owned(), - disclosure0: None, - disclosure1: Some("value1".to_owned()) - }, - one_sd_claim, + *revealed.claims(), + JWTClaims::builder() + .sub("user") + .with_private_claims(BaseClaims { + property0: None, // concealed + property1: Some("value1".to_owned()), + }) + .unwrap() ); } -#[test] -fn full_pathway_array() { +#[async_std::test] +async fn full_pathway_array() { #[derive(Debug, Serialize, Deserialize, PartialEq)] struct BaseClaims { - sub: String, array_disclosure: Vec, } - let base_claims = BaseClaims { - sub: "user".to_owned(), - array_disclosure: vec![], - }; - - let (jwt, disclosures) = encode_sign( - Algorithm::ES256, - &base_claims, - &test_key(), - SdAlg::Sha256, - vec![ - UnencodedDisclosure::new_array_item("array_disclosure", &json!("value0")).unwrap(), - UnencodedDisclosure::new_array_item("array_disclosure", &json!("value1")).unwrap(), - ], - ) - .unwrap(); - - let full_jwt_claims = decode_verify_disclosure_array::( - Deserialized { - jwt: &jwt, - disclosures: vec![&disclosures[0].encoded, &disclosures[1].encoded], - }, - &test_key(), - ) - .unwrap(); + impl ClaimSet for BaseClaims {} + impl ValidateClaims for BaseClaims {} - assert_eq!( - BaseClaims { - sub: "user".to_owned(), + let base_claims = JWTClaims::builder() + .sub("user") + .with_private_claims(BaseClaims { array_disclosure: vec!["value0".to_owned(), "value1".to_owned()], - }, - full_jwt_claims + }) + .unwrap(); + + let sd_jwt = base_claims + .conceal_and_sign( + SdAlg::Sha256, + &[ + json_pointer!("/array_disclosure/0"), + json_pointer!("/array_disclosure/1"), + ], + &*JWK, + ) + .await + .unwrap(); + + let params = VerificationParameters::from_resolver(&*JWK); + + let (mut revealed, verification) = sd_jwt + .decode_reveal_verify::(¶ms) + .await + .unwrap(); + + assert_eq!(verification, Ok(())); + assert_eq!(*revealed.claims(), base_claims); + + // Retain only the second item disclosure. + revealed.retain(&[json_pointer!("/array_disclosure/1")]); + + let sd_jwt = revealed.into_encoded(); + + let (revealed, verification) = sd_jwt + .decode_reveal_verify::(params) + .await + .unwrap(); + + assert_eq!(verification, Ok(())); + assert_eq!( + *revealed.claims(), + JWTClaims::builder() + .sub("user") + .with_private_claims(BaseClaims { + array_disclosure: vec!["value1".to_owned()] + }) + .unwrap() ); } -#[test] -fn nested_claims() { - const SD_ALG: SdAlg = SdAlg::Sha256; - +#[async_std::test] +async fn nested_claims() { // Decode types - #[derive(Debug, Deserialize, PartialEq)] + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] struct InnerNestedClaim { inner_property: String, } - #[derive(Debug, Deserialize, PartialEq)] + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] struct OuterNestedClaim { inner: Option, } - #[derive(Debug, Deserialize, PartialEq)] + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] struct Claims { - sub: String, outer: Option, } - // Manually encode - let inner_disclosure = encode_property_disclosure( - SD_ALG, - "inner", - &serde_json::json!({"inner_property": "value"}), - ) - .unwrap(); - - let outer_disclosure = encode_property_disclosure( - SD_ALG, - "outer", - &serde_json::json!({ - "_sd": [ - inner_disclosure.hash - ] - }), - ) - .unwrap(); - - let jwt = ssi_jwt::encode_sign( - Algorithm::ES256, - &serde_json::json!({ - "_sd": [ - outer_disclosure.hash - ], - "_sd_alg": SD_ALG.to_str(), - "sub": "user", - }), - &test_key(), - ) - .unwrap(); - - // No claims provided - let no_sd_claims = decode_verify_disclosure_array::( - Deserialized { - jwt: &jwt, - disclosures: vec![], - }, - &test_key(), - ) - .unwrap(); - assert_eq!( - no_sd_claims, - Claims { - sub: "user".to_owned(), - outer: None, - } - ); + impl ClaimSet for Claims {} + impl ValidateClaims for Claims {} + + let base_claims = JWTClaims::builder() + .sub("user") + .with_private_claims(Claims { + outer: Some(OuterNestedClaim { + inner: Some(InnerNestedClaim { + inner_property: "value".to_owned(), + }), + }), + }) + .unwrap(); + + let base_sd_jwt = base_claims + .conceal_and_sign( + SdAlg::Sha256, + &[json_pointer!("/outer"), json_pointer!("/outer/inner")], + &*JWK, + ) + .await + .unwrap(); + + let inner_revealed = base_sd_jwt.decode_reveal::().unwrap(); + + let params = VerificationParameters::from_resolver(&*JWK); - // Outer provided - let outer_provided = decode_verify_disclosure_array::( - Deserialized { - jwt: &jwt, - disclosures: vec![&outer_disclosure.encoded], - }, - &test_key(), - ) - .unwrap(); + let empty_sd_jwt = inner_revealed.clone().cleared().into_encoded(); + let (empty_revealed, verification) = empty_sd_jwt + .decode_reveal_verify::(¶ms) + .await + .unwrap(); + + assert_eq!(verification, Ok(())); assert_eq!( - outer_provided, - Claims { - sub: "user".to_owned(), - outer: Some(OuterNestedClaim { inner: None }) - } + *empty_revealed.claims(), + JWTClaims::builder() + .sub("user") + .with_private_claims(Claims { outer: None }) + .unwrap() ); - // Inner and outer provided - let inner_and_outer_provided = decode_verify_disclosure_array::( - Deserialized { - jwt: &jwt, - disclosures: vec![&outer_disclosure.encoded, &inner_disclosure.encoded], - }, - &test_key(), - ) - .unwrap(); + let full_sd_jwt = inner_revealed + .clone() + .retaining(&[json_pointer!("/outer"), json_pointer!("/outer/inner")]) + .into_encoded(); + + let (full_revealed, verification) = full_sd_jwt + .decode_reveal_verify::(¶ms) + .await + .unwrap(); + + assert_eq!(verification, Ok(())); + assert_eq!(*full_revealed.claims(), base_claims); + + let outer_sd_jwt = inner_revealed + .clone() + .retaining(&[json_pointer!("/outer")]) + .into_encoded(); + + let (full_revealed, verification) = outer_sd_jwt + .decode_reveal_verify::(¶ms) + .await + .unwrap(); + assert_eq!(verification, Ok(())); assert_eq!( - inner_and_outer_provided, - Claims { - sub: "user".to_owned(), - outer: Some(OuterNestedClaim { - inner: Some(InnerNestedClaim { - inner_property: "value".to_owned(), - }) + *full_revealed.claims(), + JWTClaims::builder() + .sub("user") + .with_private_claims(Claims { + outer: Some(OuterNestedClaim { inner: None }) }) - } + .unwrap() ); - // Inner without outer errors - let result = decode_verify_disclosure_array::( - Deserialized { - jwt: &jwt, - disclosures: vec![&inner_disclosure.encoded], - }, - &test_key(), - ); + let inner_sd_jwt = inner_revealed + .clone() + .retaining(&[json_pointer!("/outer/inner")]) + .into_encoded(); + + let result = inner_sd_jwt + .decode_reveal_verify::(¶ms) + .await; assert!(result.is_err()); } diff --git a/crates/claims/crates/sd-jwt/tests/rfc_examples.rs b/crates/claims/crates/sd-jwt/tests/rfc_examples.rs index b2a5476b1..d0a8dfb5f 100644 --- a/crates/claims/crates/sd-jwt/tests/rfc_examples.rs +++ b/crates/claims/crates/sd-jwt/tests/rfc_examples.rs @@ -1,175 +1,121 @@ +use std::sync::LazyLock; + use serde::{Deserialize, Serialize}; -use ssi_sd_jwt::{decode_verify_disclosure_array, Deserialized}; +use serde_json::json; +use ssi_claims_core::{ValidateClaims, VerificationParameters}; +use ssi_core::json_pointer; +use ssi_jwk::JWK; +use ssi_jws::{jws, Jws}; +use ssi_jwt::{ClaimSet, JWTClaims}; +use ssi_sd_jwt::{disclosure, Disclosure, PartsRef}; -fn rfc_a_5_key() -> ssi_jwk::JWK { - serde_json::from_value(serde_json::json!({ +static JWK: LazyLock = LazyLock::new(|| { + json!({ "kty": "EC", "crv": "P-256", "x": "b28d4MwZMjw8-00CG4xfnn9SLMVMM19SlqZpVb_uNtQ", "y": "Xv5zWwuoaTgdS6hV43yI6gBwTnjukmFQQnJ_kCxzqk8" - })) + }) + .try_into() .unwrap() -} +}); -#[test] -fn rfc_a_1_example_2_verification() { - #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +#[async_std::test] +async fn rfc_a_1_example_2_verification() { + #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)] struct Example2Address { + #[serde(skip_serializing_if = "Option::is_none")] street_address: Option, + + #[serde(skip_serializing_if = "Option::is_none")] locality: Option, + + #[serde(skip_serializing_if = "Option::is_none")] region: Option, + + #[serde(skip_serializing_if = "Option::is_none")] country: Option, } - #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] + #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)] struct Example2Claims { - sub: Option, + #[serde(skip_serializing_if = "Option::is_none")] given_name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] family_name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] email: Option, + + #[serde(skip_serializing_if = "Option::is_none")] phone_number: Option, + address: Example2Address, + + #[serde(skip_serializing_if = "Option::is_none")] birthdate: Option, - iss: String, - iat: u32, - exp: u32, } - const EXAMPLE_2_JWT: &str = concat!( - "eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIkM5aW5wNllvUmFFWFI0Mjd6WUpQN1Fya", - "zFXSF84YmR3T0FfWVVyVW5HUVUiLCAiS3VldDF5QWEwSElRdlluT1ZkNTloY1ZpTzlVZ", - "zZKMmtTZnFZUkJlb3d2RSIsICJNTWxkT0ZGekIyZDB1bWxtcFRJYUdlcmhXZFVfUHBZZ", - "kx2S2hoX2ZfOWFZIiwgIlg2WkFZT0lJMnZQTjQwVjd4RXhad1Z3ejd5Um1MTmNWd3Q1R", - "Ew4Ukx2NGciLCAiWTM0em1JbzBRTExPdGRNcFhHd2pCZ0x2cjE3eUVoaFlUMEZHb2ZSL", - "WFJRSIsICJmeUdwMFdUd3dQdjJKRFFsbjFsU2lhZW9iWnNNV0ExMGJRNTk4OS05RFRzI", - "iwgIm9tbUZBaWNWVDhMR0hDQjB1eXd4N2ZZdW8zTUhZS08xNWN6LVJaRVlNNVEiLCAic", - "zBCS1lzTFd4UVFlVTh0VmxsdE03TUtzSVJUckVJYTFQa0ptcXhCQmY1VSJdLCAiaXNzI", - "jogImh0dHBzOi8vZXhhbXBsZS5jb20vaXNzdWVyIiwgImlhdCI6IDE2ODMwMDAwMDAsI", - "CJleHAiOiAxODgzMDAwMDAwLCAiYWRkcmVzcyI6IHsiX3NkIjogWyI2YVVoelloWjdTS", - "jFrVm1hZ1FBTzN1MkVUTjJDQzFhSGhlWnBLbmFGMF9FIiwgIkF6TGxGb2JrSjJ4aWF1c", - "FJFUHlvSnotOS1OU2xkQjZDZ2pyN2ZVeW9IemciLCAiUHp6Y1Z1MHFiTXVCR1NqdWxmZ", - "Xd6a2VzRDl6dXRPRXhuNUVXTndrclEtayIsICJiMkRrdzBqY0lGOXJHZzhfUEY4WmN2b", - "mNXN3p3Wmo1cnlCV3ZYZnJwemVrIiwgImNQWUpISVo4VnUtZjlDQ3lWdWIyVWZnRWs4a", - "nZ2WGV6d0sxcF9KbmVlWFEiLCAiZ2xUM2hyU1U3ZlNXZ3dGNVVEWm1Xd0JUdzMyZ25Vb", - "GRJaGk4aEdWQ2FWNCIsICJydkpkNmlxNlQ1ZWptc0JNb0d3dU5YaDlxQUFGQVRBY2k0M", - "G9pZEVlVnNBIiwgInVOSG9XWWhYc1poVkpDTkUyRHF5LXpxdDd0NjlnSkt5NVFhRnY3R", - "3JNWDQiXX0sICJfc2RfYWxnIjogInNoYS0yNTYifQ.rFsowW-KSZe7EITlWsGajR9nnG", - "BLlQ78qgtdGIZg3FZuZnxtapP0H8CUMnffJAwPQJmGnpFpulTkLWHiI1kMmw" - ); + impl ClaimSet for Example2Claims {} + impl ValidateClaims for Example2Claims {} - const SUB_CLAIM_DISCLOSURE: &str = - "WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInN1YiIsICI2YzVjMGE0OS1iNTg5LTQzMWQtYmFlNy0yMTkxMjJhOWVjMmMiXQ"; - const GIVEN_NAME_DISCLOSURE: &str = - "WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImdpdmVuX25hbWUiLCAiXHU1OTJhXHU5MGNlIl0"; - const FAMILY_NAME_DISCLOSURE: &str = - "WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImZhbWlseV9uYW1lIiwgIlx1NWM3MVx1NzUzMCJd"; - const EMAIL_CLAIM_DISCLOSURE: &str = - "WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgImVtYWlsIiwgIlwidW51c3VhbCBlbWFpbCBhZGRyZXNzXCJAZXhhbXBsZS5qcCJd"; - const PHONE_NUMBER_DISCLOSURE: &str = - "WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgInBob25lX251bWJlciIsICIrODEtODAtMTIzNC01Njc4Il0"; - const ADDRESS_STREET_ADDRESS_DISCLOSURES: &str = - "WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgInN0cmVldF9hZGRyZXNzIiwgIlx1Njc3MVx1NGVhY1x1OTBmZFx1NmUyZlx1NTMzYVx1ODI5ZFx1NTE2Y1x1NTcxMlx1ZmYxNFx1NGUwMVx1NzZlZVx1ZmYxMlx1MjIxMlx1ZmYxOCJd"; - const ADDRESS_LOCALITY_DISCLOSURE: &str = - "WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImxvY2FsaXR5IiwgIlx1Njc3MVx1NGVhY1x1OTBmZCJd"; - const ADDRESS_REGION_DISCLOSURE: &str = - "WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICJcdTZlMmZcdTUzM2EiXQ"; - const ADDRESS_COUNTRY_DISCLOSURE: &str = - "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiSlAiXQ"; - const BIRTHDATE_DISCLOSURE: &str = - "WyJ5eXRWYmRBUEdjZ2wyckk0QzlHU29nIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0"; - - // Raw with no disclosures - let no_disclosures = decode_verify_disclosure_array::( - Deserialized { - jwt: EXAMPLE_2_JWT, - disclosures: vec![], - }, - &rfc_a_5_key(), - ) - .unwrap(); + const JWT: &Jws = jws!("eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIkM5aW5wNllvUmFFWFI0Mjd6WUpQN1FyazFXSF84YmR3T0FfWVVyVW5HUVUiLCAiS3VldDF5QWEwSElRdlluT1ZkNTloY1ZpTzlVZzZKMmtTZnFZUkJlb3d2RSIsICJNTWxkT0ZGekIyZDB1bWxtcFRJYUdlcmhXZFVfUHBZZkx2S2hoX2ZfOWFZIiwgIlg2WkFZT0lJMnZQTjQwVjd4RXhad1Z3ejd5Um1MTmNWd3Q1REw4Ukx2NGciLCAiWTM0em1JbzBRTExPdGRNcFhHd2pCZ0x2cjE3eUVoaFlUMEZHb2ZSLWFJRSIsICJmeUdwMFdUd3dQdjJKRFFsbjFsU2lhZW9iWnNNV0ExMGJRNTk4OS05RFRzIiwgIm9tbUZBaWNWVDhMR0hDQjB1eXd4N2ZZdW8zTUhZS08xNWN6LVJaRVlNNVEiLCAiczBCS1lzTFd4UVFlVTh0VmxsdE03TUtzSVJUckVJYTFQa0ptcXhCQmY1VSJdLCAiaXNzIjogImh0dHBzOi8vaXNzdWVyLmV4YW1wbGUuY29tIiwgImlhdCI6IDE2ODMwMDAwMDAsICJleHAiOiAxODgzMDAwMDAwLCAiYWRkcmVzcyI6IHsiX3NkIjogWyI2YVVoelloWjdTSjFrVm1hZ1FBTzN1MkVUTjJDQzFhSGhlWnBLbmFGMF9FIiwgIkF6TGxGb2JrSjJ4aWF1cFJFUHlvSnotOS1OU2xkQjZDZ2pyN2ZVeW9IemciLCAiUHp6Y1Z1MHFiTXVCR1NqdWxmZXd6a2VzRDl6dXRPRXhuNUVXTndrclEtayIsICJiMkRrdzBqY0lGOXJHZzhfUEY4WmN2bmNXN3p3Wmo1cnlCV3ZYZnJwemVrIiwgImNQWUpISVo4VnUtZjlDQ3lWdWIyVWZnRWs4anZ2WGV6d0sxcF9KbmVlWFEiLCAiZ2xUM2hyU1U3ZlNXZ3dGNVVEWm1Xd0JUdzMyZ25VbGRJaGk4aEdWQ2FWNCIsICJydkpkNmlxNlQ1ZWptc0JNb0d3dU5YaDlxQUFGQVRBY2k0MG9pZEVlVnNBIiwgInVOSG9XWWhYc1poVkpDTkUyRHF5LXpxdDd0NjlnSkt5NVFhRnY3R3JNWDQiXX0sICJfc2RfYWxnIjogInNoYS0yNTYifQ.H0uI6M5t7BDfAt_a3Rw5zq4IiYNtkMORENcQFXYkW1LURRp66baOXRcxb164snsUJneI-XLM2-COCTX1y7Sedw"); - assert_eq!( - no_disclosures, - Example2Claims { - address: Default::default(), - iss: "https://example.com/issuer".to_owned(), - iat: 1683000000, - exp: 1883000000, - ..Default::default() - } + const SUB_CLAIM_DISCLOSURE: &Disclosure = + disclosure!("WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInN1YiIsICI2YzVjMGE0OS1iNTg5LTQzMWQtYmFlNy0yMTkxMjJhOWVjMmMiXQ"); + const GIVEN_NAME_DISCLOSURE: &Disclosure = + disclosure!("WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImdpdmVuX25hbWUiLCAiXHU1OTJhXHU5MGNlIl0"); + const FAMILY_NAME_DISCLOSURE: &Disclosure = + disclosure!("WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImZhbWlseV9uYW1lIiwgIlx1NWM3MVx1NzUzMCJd"); + const EMAIL_CLAIM_DISCLOSURE: &Disclosure = + disclosure!("WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgImVtYWlsIiwgIlwidW51c3VhbCBlbWFpbCBhZGRyZXNzXCJAZXhhbXBsZS5qcCJd"); + const PHONE_NUMBER_DISCLOSURE: &Disclosure = disclosure!( + "WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgInBob25lX251bWJlciIsICIrODEtODAtMTIzNC01Njc4Il0" ); - - // Top level claim disclosed - let sub_claim_disclosed = decode_verify_disclosure_array::( - Deserialized { - jwt: EXAMPLE_2_JWT, - disclosures: vec![SUB_CLAIM_DISCLOSURE], - }, - &rfc_a_5_key(), - ) - .unwrap(); - - assert_eq!( - sub_claim_disclosed, - Example2Claims { - sub: Some("6c5c0a49-b589-431d-bae7-219122a9ec2c".to_owned()), - address: Default::default(), - iss: "https://example.com/issuer".to_owned(), - iat: 1683000000, - exp: 1883000000, - ..Default::default() - } + const ADDRESS_STREET_ADDRESS_DISCLOSURES: &Disclosure = + disclosure!("WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgInN0cmVldF9hZGRyZXNzIiwgIlx1Njc3MVx1NGVhY1x1OTBmZFx1NmUyZlx1NTMzYVx1ODI5ZFx1NTE2Y1x1NTcxMlx1ZmYxNFx1NGUwMVx1NzZlZVx1ZmYxMlx1MjIxMlx1ZmYxOCJd"); + const ADDRESS_LOCALITY_DISCLOSURE: &Disclosure = disclosure!( + "WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImxvY2FsaXR5IiwgIlx1Njc3MVx1NGVhY1x1OTBmZCJd" ); + const ADDRESS_REGION_DISCLOSURE: &Disclosure = + disclosure!("WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgInJlZ2lvbiIsICJcdTZlMmZcdTUzM2EiXQ"); + const ADDRESS_COUNTRY_DISCLOSURE: &Disclosure = + disclosure!("WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImNvdW50cnkiLCAiSlAiXQ"); + const BIRTHDATE_DISCLOSURE: &Disclosure = + disclosure!("WyJ5eXRWYmRBUEdjZ2wyckk0QzlHU29nIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0"); - // Address claim disclosed - let address_country_disclosed = decode_verify_disclosure_array::( - Deserialized { - jwt: EXAMPLE_2_JWT, - disclosures: vec![ADDRESS_COUNTRY_DISCLOSURE], - }, - &rfc_a_5_key(), - ) - .unwrap(); + let sd_jwt = PartsRef { + jwt: JWT, + disclosures: vec![ + SUB_CLAIM_DISCLOSURE, + GIVEN_NAME_DISCLOSURE, + FAMILY_NAME_DISCLOSURE, + EMAIL_CLAIM_DISCLOSURE, + PHONE_NUMBER_DISCLOSURE, + ADDRESS_STREET_ADDRESS_DISCLOSURES, + ADDRESS_LOCALITY_DISCLOSURE, + ADDRESS_REGION_DISCLOSURE, + ADDRESS_COUNTRY_DISCLOSURE, + BIRTHDATE_DISCLOSURE, + ], + }; - assert_eq!( - address_country_disclosed, - Example2Claims { - address: Example2Address { - country: Some("JP".to_owned()), - ..Default::default() - }, - iss: "https://example.com/issuer".to_owned(), - iat: 1683000000, - exp: 1883000000, - ..Default::default() - } - ); + let params = VerificationParameters::from_resolver(&*JWK); - // All claims disclosed - let all_claims = decode_verify_disclosure_array::( - Deserialized { - jwt: EXAMPLE_2_JWT, - disclosures: vec![ - SUB_CLAIM_DISCLOSURE, - GIVEN_NAME_DISCLOSURE, - FAMILY_NAME_DISCLOSURE, - EMAIL_CLAIM_DISCLOSURE, - PHONE_NUMBER_DISCLOSURE, - ADDRESS_STREET_ADDRESS_DISCLOSURES, - ADDRESS_LOCALITY_DISCLOSURE, - ADDRESS_REGION_DISCLOSURE, - ADDRESS_COUNTRY_DISCLOSURE, - BIRTHDATE_DISCLOSURE, - ], - }, - &rfc_a_5_key(), - ) - .unwrap(); + let (revealed, verification) = sd_jwt + .decode_reveal_verify::(¶ms) + .await + .unwrap(); - assert_eq!( - all_claims, - Example2Claims { - sub: Some("6c5c0a49-b589-431d-bae7-219122a9ec2c".to_owned()), + assert_eq!(verification, Ok(())); + + let expected = JWTClaims::builder() + .sub("6c5c0a49-b589-431d-bae7-219122a9ec2c") + .iss("https://issuer.example.com") + .iat(1683000000) + .exp(1883000000) + .with_private_claims(Example2Claims { given_name: Some("太郎".to_owned()), family_name: Some("山田".to_owned()), email: Some("\"unusual email address\"@example.jp".to_owned()), @@ -181,36 +127,100 @@ fn rfc_a_1_example_2_verification() { country: Some("JP".to_owned()), }, birthdate: Some("1940-01-01".to_owned()), - iss: "https://example.com/issuer".to_owned(), - iat: 1683000000, - exp: 1883000000 - } + }) + .unwrap(); + + eprintln!( + "expected = {}", + serde_json::to_string_pretty(&expected).unwrap() ); -} + eprintln!( + "found = {}", + serde_json::to_string_pretty(revealed.claims()).unwrap() + ); + + assert_eq!(*revealed.claims(), expected); + + let empty_sd_jwt = revealed.clone().cleared().into_encoded(); + + let (empty_revealed, verification) = empty_sd_jwt.decode_reveal_verify(¶ms).await.unwrap(); + + assert_eq!(verification, Ok(())); + + let expected = JWTClaims::builder() + .iss("https://issuer.example.com") + .iat(1683000000) + .exp(1883000000) + .with_private_claims(Example2Claims::default()) + .unwrap(); -#[test] -fn rfc_a_2_example_3_verification() { - const EXAMPLE_3_JWT: &str = concat!( - "eyJhbGciOiAiRVMyNTYifQ.eyJfc2QiOiBbIi1hU3puSWQ5bVdNOG9jdVFvbENsbHN4V", - "mdncTEtdkhXNE90bmhVdFZtV3ciLCAiSUticllObjN2QTdXRUZyeXN2YmRCSmpERFVfR", - "XZRSXIwVzE4dlRScFVTZyIsICJvdGt4dVQxNG5CaXd6TkozTVBhT2l0T2w5cFZuWE9hR", - "UhhbF94a3lOZktJIl0sICJpc3MiOiAiaHR0cHM6Ly9leGFtcGxlLmNvbS9pc3N1ZXIiL", - "CAiaWF0IjogMTY4MzAwMDAwMCwgImV4cCI6IDE4ODMwMDAwMDAsICJ2ZXJpZmllZF9jb", - "GFpbXMiOiB7InZlcmlmaWNhdGlvbiI6IHsiX3NkIjogWyI3aDRVRTlxU2N2REtvZFhWQ", - "3VvS2ZLQkpwVkJmWE1GX1RtQUdWYVplM1NjIiwgInZUd2UzcmFISUZZZ0ZBM3hhVUQyY", - "U14Rno1b0RvOGlCdTA1cUtsT2c5THciXSwgInRydXN0X2ZyYW1ld29yayI6ICJkZV9hb", - "WwiLCAiZXZpZGVuY2UiOiBbeyIuLi4iOiAidFlKMFREdWN5WlpDUk1iUk9HNHFSTzV2a", - "1BTRlJ4RmhVRUxjMThDU2wzayJ9XX0sICJjbGFpbXMiOiB7Il9zZCI6IFsiUmlPaUNuN", - "l93NVpIYWFka1FNcmNRSmYwSnRlNVJ3dXJSczU0MjMxRFRsbyIsICJTXzQ5OGJicEt6Q", - "jZFYW5mdHNzMHhjN2NPYW9uZVJyM3BLcjdOZFJtc01vIiwgIldOQS1VTks3Rl96aHNBY", - "jlzeVdPNklJUTF1SGxUbU9VOHI4Q3ZKMGNJTWsiLCAiV3hoX3NWM2lSSDliZ3JUQkppL", - "WFZSE5DTHQtdmpoWDFzZC1pZ09mXzlsayIsICJfTy13SmlIM2VuU0I0Uk9IbnRUb1FUO", - "EptTHR6LW1oTzJmMWM4OVhvZXJRIiwgImh2RFhod21HY0pRc0JDQTJPdGp1TEFjd0FNc", - "ERzYVUwbmtvdmNLT3FXTkUiXX19LCAiX3NkX2FsZyI6ICJzaGEtMjU2In0.Xtpp8nvAq", - "22k6wNRiYHGRoRnkn3EBaHdjcaa0sf0sYjCiyZnmSRlxv_C72gRwfVQkSA36ID_I46QS", - "TZvBrgm3g" + eprintln!( + "expected = {}", + serde_json::to_string_pretty(&expected).unwrap() ); + eprintln!( + "found = {}", + serde_json::to_string_pretty(empty_revealed.claims()).unwrap() + ); + + assert_eq!(*empty_revealed.claims(), expected); + + let sub_sd_jwt = revealed + .clone() + .retaining(&[json_pointer!("/sub")]) + .into_encoded(); + + let (sub_revealed, verification) = sub_sd_jwt.decode_reveal_verify(¶ms).await.unwrap(); + + assert_eq!(verification, Ok(())); + assert_eq!( + *sub_revealed.claims(), + JWTClaims::builder() + .sub("6c5c0a49-b589-431d-bae7-219122a9ec2c") + .iss("https://issuer.example.com") + .iat(1683000000) + .exp(1883000000) + .with_private_claims(Example2Claims::default()) + .unwrap() + ); + + let country_sd_jwt = revealed + .clone() + .retaining(&[json_pointer!("/address/country")]) + .into_encoded(); + + let (country_revealed, verification) = + country_sd_jwt.decode_reveal_verify(¶ms).await.unwrap(); + + assert_eq!(verification, Ok(())); + + let expected = JWTClaims::builder() + .iss("https://issuer.example.com") + .iat(1683000000) + .exp(1883000000) + .with_private_claims(Example2Claims { + address: Example2Address { + country: Some("JP".to_owned()), + ..Default::default() + }, + ..Default::default() + }) + .unwrap(); + eprintln!( + "expected = {}", + serde_json::to_string_pretty(&expected).unwrap() + ); + eprintln!( + "found = {}", + serde_json::to_string_pretty(country_revealed.claims()).unwrap() + ); + + assert_eq!(*country_revealed.claims(), expected); +} + +#[async_std::test] +async fn rfc_a_2_example_3_verification() { #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] struct VerificationEvidenceDocumentIssuer { name: String, @@ -229,17 +239,27 @@ fn rfc_a_2_example_3_verification() { #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] struct VerificationEvidence { - #[serde(rename = "type")] + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] _type: Option, + + #[serde(skip_serializing_if = "Option::is_none")] method: Option, + + #[serde(skip_serializing_if = "Option::is_none")] time: Option, + + #[serde(skip_serializing_if = "Option::is_none")] document: Option, } #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] struct Verification { trust_framework: String, + + #[serde(skip_serializing_if = "Option::is_none")] time: Option, + + #[serde(skip_serializing_if = "Option::is_none")] verification_process: Option, evidence: Vec, } @@ -260,11 +280,22 @@ fn rfc_a_2_example_3_verification() { #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] struct VerifiedClaimsClaims { + #[serde(skip_serializing_if = "Option::is_none")] given_name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] family_name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] nationalities: Option>, + + #[serde(skip_serializing_if = "Option::is_none")] birthdate: Option, + + #[serde(skip_serializing_if = "Option::is_none")] place_of_birth: Option, + + #[serde(skip_serializing_if = "Option::is_none")] address: Option

, } @@ -277,121 +308,133 @@ fn rfc_a_2_example_3_verification() { #[derive(Debug, Default, Serialize, Deserialize, PartialEq)] struct Example3Claims { verified_claims: VerifiedClaims, - iss: String, - iat: u32, - exp: u32, + + #[serde(skip_serializing_if = "Option::is_none")] birth_middle_name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] salutation: Option, + + #[serde(skip_serializing_if = "Option::is_none")] msisdn: Option, } - const VERIFIED_CLAIMS_TIME_DISCLOSURE: &str = - "WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInRpbWUiLCAiMjAxMi0wNC0yM1QxODoyNVoiXQ"; - const VERIFIED_CLAIMS_VERIFICATION_PROCESS_DISCLOSURE: &str = - "WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgInZlcmlmaWNhdGlvbl9wcm9jZXNzIiwgImYyNGM2Zi02ZDNmLTRlYzUtOTczZS1iMGQ4NTA2ZjNiYzciXQ"; - const VERIFIED_CLAIMS_EVIDENCE_0_TYPE_DISCLOSURE: &str = - "WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgInR5cGUiLCAiZG9jdW1lbnQiXQ"; - const VERIFIED_CLAIMS_EVIDENCE_0_METHOD_DISCLOSURE: &str = - "WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgIm1ldGhvZCIsICJwaXBwIl0"; - const VERIFIED_CLAIMS_EVIDENCE_0_TIME_DISCLOSURE: &str = - "WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgInRpbWUiLCAiMjAxMi0wNC0yMlQxMTozMFoiXQ"; - const VERIFIED_CLAIMS_EVIDENCE_0_DOCUMENT_DISCLOSURE: &str = - "WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImRvY3VtZW50IiwgeyJ0eXBlIjogImlkY2FyZCIsICJpc3N1ZXIiOiB7Im5hbWUiOiAiU3RhZHQgQXVnc2J1cmciLCAiY291bnRyeSI6ICJERSJ9LCAibnVtYmVyIjogIjUzNTU0NTU0IiwgImRhdGVfb2ZfaXNzdWFuY2UiOiAiMjAxMC0wMy0yMyIsICJkYXRlX29mX2V4cGlyeSI6ICIyMDIwLTAzLTIyIn1d"; - const VERIFIED_CLAIMS_EVIDENCE_0_DISCLOSURE: &str = - "WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgeyJfc2QiOiBbIjl3cGpWUFd1RDdQSzBuc1FETDhCMDZsbWRnVjNMVnliaEh5ZFFwVE55TEkiLCAiRzVFbmhPQU9vVTlYXzZRTU52ekZYanBFQV9SYy1BRXRtMWJHX3djYUtJayIsICJJaHdGcldVQjYzUmNacTl5dmdaMFhQYzdHb3doM08ya3FYZUJJc3dnMUI0IiwgIldweFE0SFNvRXRjVG1DQ0tPZURzbEJfZW11Y1lMejJvTzhvSE5yMWJFVlEiXX1d"; - const VERIFIED_CLAIMS_CLAIMS_GIVEN_NAME_DISCLOSURE: &str = - "WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgImdpdmVuX25hbWUiLCAiTWF4Il0"; - const VERIFIED_CLAIMS_CLAIMS_FAMILY_NAME_DISCLOSURE: &str = - "WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImZhbWlseV9uYW1lIiwgIk1cdTAwZmNsbGVyIl0"; - const VERIFIED_CLAIMS_CLAIMS_NATIONALITIES_DISCLOSURE: &str = - "WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIm5hdGlvbmFsaXRpZXMiLCBbIkRFIl1d"; - const VERIFIED_CLAIMS_CLAIMS_BIRTHDATE_DISCLOSURE: &str = - "WyI1YlBzMUlxdVpOYTBoa2FGenp6Wk53IiwgImJpcnRoZGF0ZSIsICIxOTU2LTAxLTI4Il0"; - const VERIFIED_CLAIMS_CLAIMS_PLACE_OF_BIRTH_DISCLOSURE: &str = - "WyI1YTJXMF9OcmxFWnpmcW1rXzdQcS13IiwgInBsYWNlX29mX2JpcnRoIiwgeyJjb3VudHJ5IjogIklTIiwgImxvY2FsaXR5IjogIlx1MDBkZXlra3ZhYlx1MDBlNmphcmtsYXVzdHVyIn1d"; - const VERIFIED_CLAIMS_CLAIMS_ADDRESS_DISCLOSURE: &str = - "WyJ5MXNWVTV3ZGZKYWhWZGd3UGdTN1JRIiwgImFkZHJlc3MiLCB7ImxvY2FsaXR5IjogIk1heHN0YWR0IiwgInBvc3RhbF9jb2RlIjogIjEyMzQ0IiwgImNvdW50cnkiOiAiREUiLCAic3RyZWV0X2FkZHJlc3MiOiAiV2VpZGVuc3RyYVx1MDBkZmUgMjIifV0"; - const BIRTH_MIDDLE_NAME_DISCLOSURE: &str = - "WyJIYlE0WDhzclZXM1FEeG5JSmRxeU9BIiwgImJpcnRoX21pZGRsZV9uYW1lIiwgIlRpbW90aGV1cyJd"; - const SALUTATION_DISCLOSURE: &str = - "WyJDOUdTb3VqdmlKcXVFZ1lmb2pDYjFBIiwgInNhbHV0YXRpb24iLCAiRHIuIl0"; - const MSISDN_DISCLOSURE: &str = - "WyJreDVrRjE3Vi14MEptd1V4OXZndnR3IiwgIm1zaXNkbiIsICI0OTEyMzQ1Njc4OSJd"; - - // All Claims - let all_claims = decode_verify_disclosure_array::( - Deserialized { - jwt: EXAMPLE_3_JWT, - disclosures: vec![ - VERIFIED_CLAIMS_TIME_DISCLOSURE, - VERIFIED_CLAIMS_VERIFICATION_PROCESS_DISCLOSURE, - VERIFIED_CLAIMS_EVIDENCE_0_TYPE_DISCLOSURE, - VERIFIED_CLAIMS_EVIDENCE_0_METHOD_DISCLOSURE, - VERIFIED_CLAIMS_EVIDENCE_0_TIME_DISCLOSURE, - VERIFIED_CLAIMS_EVIDENCE_0_DOCUMENT_DISCLOSURE, - VERIFIED_CLAIMS_EVIDENCE_0_DISCLOSURE, - VERIFIED_CLAIMS_CLAIMS_GIVEN_NAME_DISCLOSURE, - VERIFIED_CLAIMS_CLAIMS_FAMILY_NAME_DISCLOSURE, - VERIFIED_CLAIMS_CLAIMS_NATIONALITIES_DISCLOSURE, - VERIFIED_CLAIMS_CLAIMS_BIRTHDATE_DISCLOSURE, - VERIFIED_CLAIMS_CLAIMS_PLACE_OF_BIRTH_DISCLOSURE, - VERIFIED_CLAIMS_CLAIMS_ADDRESS_DISCLOSURE, - BIRTH_MIDDLE_NAME_DISCLOSURE, - SALUTATION_DISCLOSURE, - MSISDN_DISCLOSURE, - ], - }, - &rfc_a_5_key(), - ) + impl ClaimSet for Example3Claims {} + impl ValidateClaims for Example3Claims {} + + const JWT: &Jws = jws!("eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0.eyJfc2QiOiBbIi1hU3puSWQ5bVdNOG9jdVFvbENsbHN4VmdncTEtdkhXNE90bmhVdFZtV3ciLCAiSUticllObjN2QTdXRUZyeXN2YmRCSmpERFVfRXZRSXIwVzE4dlRScFVTZyIsICJvdGt4dVQxNG5CaXd6TkozTVBhT2l0T2w5cFZuWE9hRUhhbF94a3lOZktJIl0sICJpc3MiOiAiaHR0cHM6Ly9pc3N1ZXIuZXhhbXBsZS5jb20iLCAiaWF0IjogMTY4MzAwMDAwMCwgImV4cCI6IDE4ODMwMDAwMDAsICJ2ZXJpZmllZF9jbGFpbXMiOiB7InZlcmlmaWNhdGlvbiI6IHsiX3NkIjogWyI3aDRVRTlxU2N2REtvZFhWQ3VvS2ZLQkpwVkJmWE1GX1RtQUdWYVplM1NjIiwgInZUd2UzcmFISUZZZ0ZBM3hhVUQyYU14Rno1b0RvOGlCdTA1cUtsT2c5THciXSwgInRydXN0X2ZyYW1ld29yayI6ICJkZV9hbWwiLCAiZXZpZGVuY2UiOiBbeyIuLi4iOiAidFlKMFREdWN5WlpDUk1iUk9HNHFSTzV2a1BTRlJ4RmhVRUxjMThDU2wzayJ9XX0sICJjbGFpbXMiOiB7Il9zZCI6IFsiUmlPaUNuNl93NVpIYWFka1FNcmNRSmYwSnRlNVJ3dXJSczU0MjMxRFRsbyIsICJTXzQ5OGJicEt6QjZFYW5mdHNzMHhjN2NPYW9uZVJyM3BLcjdOZFJtc01vIiwgIldOQS1VTks3Rl96aHNBYjlzeVdPNklJUTF1SGxUbU9VOHI4Q3ZKMGNJTWsiLCAiV3hoX3NWM2lSSDliZ3JUQkppLWFZSE5DTHQtdmpoWDFzZC1pZ09mXzlsayIsICJfTy13SmlIM2VuU0I0Uk9IbnRUb1FUOEptTHR6LW1oTzJmMWM4OVhvZXJRIiwgImh2RFhod21HY0pRc0JDQTJPdGp1TEFjd0FNcERzYVUwbmtvdmNLT3FXTkUiXX19LCAiX3NkX2FsZyI6ICJzaGEtMjU2In0.BYUpdUIaUTNj5UlPBk6g0GhDp213yGD_HIMk8P45LwkSAfZgc_ayGnf9VC4gebeE-crDoonxf89Y7qsTA-4qdQ"); + + const VERIFIED_CLAIMS_TIME_DISCLOSURE: &Disclosure = + disclosure!("WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgInRpbWUiLCAiMjAxMi0wNC0yM1QxODoyNVoiXQ"); + const VERIFIED_CLAIMS_VERIFICATION_PROCESS_DISCLOSURE: &Disclosure = + disclosure!("WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgInZlcmlmaWNhdGlvbl9wcm9jZXNzIiwgImYyNGM2Zi02ZDNmLTRlYzUtOTczZS1iMGQ4NTA2ZjNiYzciXQ"); + const VERIFIED_CLAIMS_EVIDENCE_0_TYPE_DISCLOSURE: &Disclosure = + disclosure!("WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgInR5cGUiLCAiZG9jdW1lbnQiXQ"); + const VERIFIED_CLAIMS_EVIDENCE_0_METHOD_DISCLOSURE: &Disclosure = + disclosure!("WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgIm1ldGhvZCIsICJwaXBwIl0"); + const VERIFIED_CLAIMS_EVIDENCE_0_TIME_DISCLOSURE: &Disclosure = + disclosure!("WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgInRpbWUiLCAiMjAxMi0wNC0yMlQxMTozMFoiXQ"); + const VERIFIED_CLAIMS_EVIDENCE_0_DOCUMENT_DISCLOSURE: &Disclosure = + disclosure!("WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImRvY3VtZW50IiwgeyJ0eXBlIjogImlkY2FyZCIsICJpc3N1ZXIiOiB7Im5hbWUiOiAiU3RhZHQgQXVnc2J1cmciLCAiY291bnRyeSI6ICJERSJ9LCAibnVtYmVyIjogIjUzNTU0NTU0IiwgImRhdGVfb2ZfaXNzdWFuY2UiOiAiMjAxMC0wMy0yMyIsICJkYXRlX29mX2V4cGlyeSI6ICIyMDIwLTAzLTIyIn1d"); + const VERIFIED_CLAIMS_EVIDENCE_0_DISCLOSURE: &Disclosure = + disclosure!("WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgeyJfc2QiOiBbIjl3cGpWUFd1RDdQSzBuc1FETDhCMDZsbWRnVjNMVnliaEh5ZFFwVE55TEkiLCAiRzVFbmhPQU9vVTlYXzZRTU52ekZYanBFQV9SYy1BRXRtMWJHX3djYUtJayIsICJJaHdGcldVQjYzUmNacTl5dmdaMFhQYzdHb3doM08ya3FYZUJJc3dnMUI0IiwgIldweFE0SFNvRXRjVG1DQ0tPZURzbEJfZW11Y1lMejJvTzhvSE5yMWJFVlEiXX1d"); + const VERIFIED_CLAIMS_CLAIMS_GIVEN_NAME_DISCLOSURE: &Disclosure = + disclosure!("WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgImdpdmVuX25hbWUiLCAiTWF4Il0"); + const VERIFIED_CLAIMS_CLAIMS_FAMILY_NAME_DISCLOSURE: &Disclosure = + disclosure!("WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImZhbWlseV9uYW1lIiwgIk1cdTAwZmNsbGVyIl0"); + const VERIFIED_CLAIMS_CLAIMS_NATIONALITIES_DISCLOSURE: &Disclosure = + disclosure!("WyJuUHVvUW5rUkZxM0JJZUFtN0FuWEZBIiwgIm5hdGlvbmFsaXRpZXMiLCBbIkRFIl1d"); + const VERIFIED_CLAIMS_CLAIMS_BIRTHDATE_DISCLOSURE: &Disclosure = + disclosure!("WyI1YlBzMUlxdVpOYTBoa2FGenp6Wk53IiwgImJpcnRoZGF0ZSIsICIxOTU2LTAxLTI4Il0"); + const VERIFIED_CLAIMS_CLAIMS_PLACE_OF_BIRTH_DISCLOSURE: &Disclosure = + disclosure!("WyI1YTJXMF9OcmxFWnpmcW1rXzdQcS13IiwgInBsYWNlX29mX2JpcnRoIiwgeyJjb3VudHJ5IjogIklTIiwgImxvY2FsaXR5IjogIlx1MDBkZXlra3ZhYlx1MDBlNmphcmtsYXVzdHVyIn1d"); + const VERIFIED_CLAIMS_CLAIMS_ADDRESS_DISCLOSURE: &Disclosure = + disclosure!("WyJ5MXNWVTV3ZGZKYWhWZGd3UGdTN1JRIiwgImFkZHJlc3MiLCB7ImxvY2FsaXR5IjogIk1heHN0YWR0IiwgInBvc3RhbF9jb2RlIjogIjEyMzQ0IiwgImNvdW50cnkiOiAiREUiLCAic3RyZWV0X2FkZHJlc3MiOiAiV2VpZGVuc3RyYVx1MDBkZmUgMjIifV0"); + const BIRTH_MIDDLE_NAME_DISCLOSURE: &Disclosure = disclosure!( + "WyJIYlE0WDhzclZXM1FEeG5JSmRxeU9BIiwgImJpcnRoX21pZGRsZV9uYW1lIiwgIlRpbW90aGV1cyJd" + ); + const SALUTATION_DISCLOSURE: &Disclosure = + disclosure!("WyJDOUdTb3VqdmlKcXVFZ1lmb2pDYjFBIiwgInNhbHV0YXRpb24iLCAiRHIuIl0"); + const MSISDN_DISCLOSURE: &Disclosure = + disclosure!("WyJreDVrRjE3Vi14MEptd1V4OXZndnR3IiwgIm1zaXNkbiIsICI0OTEyMzQ1Njc4OSJd"); + + let params = VerificationParameters::from_resolver(&*JWK); + + let (revealed, verification) = PartsRef { + jwt: JWT, + disclosures: vec![ + VERIFIED_CLAIMS_TIME_DISCLOSURE, + VERIFIED_CLAIMS_VERIFICATION_PROCESS_DISCLOSURE, + VERIFIED_CLAIMS_EVIDENCE_0_TYPE_DISCLOSURE, + VERIFIED_CLAIMS_EVIDENCE_0_METHOD_DISCLOSURE, + VERIFIED_CLAIMS_EVIDENCE_0_TIME_DISCLOSURE, + VERIFIED_CLAIMS_EVIDENCE_0_DOCUMENT_DISCLOSURE, + VERIFIED_CLAIMS_EVIDENCE_0_DISCLOSURE, + VERIFIED_CLAIMS_CLAIMS_GIVEN_NAME_DISCLOSURE, + VERIFIED_CLAIMS_CLAIMS_FAMILY_NAME_DISCLOSURE, + VERIFIED_CLAIMS_CLAIMS_NATIONALITIES_DISCLOSURE, + VERIFIED_CLAIMS_CLAIMS_BIRTHDATE_DISCLOSURE, + VERIFIED_CLAIMS_CLAIMS_PLACE_OF_BIRTH_DISCLOSURE, + VERIFIED_CLAIMS_CLAIMS_ADDRESS_DISCLOSURE, + BIRTH_MIDDLE_NAME_DISCLOSURE, + SALUTATION_DISCLOSURE, + MSISDN_DISCLOSURE, + ], + } + .decode_reveal_verify(¶ms) + .await .unwrap(); + assert_eq!(verification, Ok(())); assert_eq!( - all_claims, - Example3Claims { - verified_claims: VerifiedClaims { - verification: Verification { - trust_framework: "de_aml".to_owned(), - time: Some("2012-04-23T18:25Z".to_owned()), - verification_process: Some("f24c6f-6d3f-4ec5-973e-b0d8506f3bc7".to_owned()), - evidence: vec![VerificationEvidence { - _type: Some("document".to_owned()), - method: Some("pipp".to_owned()), - time: Some("2012-04-22T11:30Z".to_owned()), - document: Some(VerificationEvidenceDocument { - _type: "idcard".to_owned(), - issuer: VerificationEvidenceDocumentIssuer { - name: "Stadt Augsburg".to_owned(), - country: "DE".to_owned(), - }, - number: "53554554".to_owned(), - date_of_issuance: "2010-03-23".to_owned(), - date_of_expiry: "2020-03-22".to_owned(), - }) - }], + *revealed.claims(), + JWTClaims::builder() + .iss("https://issuer.example.com") + .iat(1683000000) + .exp(1883000000) + .with_private_claims(Example3Claims { + verified_claims: VerifiedClaims { + verification: Verification { + trust_framework: "de_aml".to_owned(), + time: Some("2012-04-23T18:25Z".to_owned()), + verification_process: Some("f24c6f-6d3f-4ec5-973e-b0d8506f3bc7".to_owned()), + evidence: vec![VerificationEvidence { + _type: Some("document".to_owned()), + method: Some("pipp".to_owned()), + time: Some("2012-04-22T11:30Z".to_owned()), + document: Some(VerificationEvidenceDocument { + _type: "idcard".to_owned(), + issuer: VerificationEvidenceDocumentIssuer { + name: "Stadt Augsburg".to_owned(), + country: "DE".to_owned(), + }, + number: "53554554".to_owned(), + date_of_issuance: "2010-03-23".to_owned(), + date_of_expiry: "2020-03-22".to_owned(), + }) + }], + }, + claims: VerifiedClaimsClaims { + given_name: Some("Max".to_owned()), + family_name: Some("Müller".to_owned()), + nationalities: Some(vec!["DE".to_owned()]), + birthdate: Some("1956-01-28".to_owned()), + place_of_birth: Some(PlaceOfBirth { + country: "IS".to_owned(), + locality: "Þykkvabæjarklaustur".to_owned(), + }), + address: Some(Address { + locality: "Maxstadt".to_owned(), + postal_code: "12344".to_owned(), + country: "DE".to_owned(), + street_address: "Weidenstraße 22".to_owned(), + }), + }, }, - claims: VerifiedClaimsClaims { - given_name: Some("Max".to_owned()), - family_name: Some("Müller".to_owned()), - nationalities: Some(vec!["DE".to_owned()]), - birthdate: Some("1956-01-28".to_owned()), - place_of_birth: Some(PlaceOfBirth { - country: "IS".to_owned(), - locality: "Þykkvabæjarklaustur".to_owned(), - }), - address: Some(Address { - locality: "Maxstadt".to_owned(), - postal_code: "12344".to_owned(), - country: "DE".to_owned(), - street_address: "Weidenstraße 22".to_owned(), - }), - }, - }, - iss: "https://example.com/issuer".to_owned(), - iat: 1683000000, - exp: 1883000000, - birth_middle_name: Some("Timotheus".to_owned()), - salutation: Some("Dr.".to_owned()), - msisdn: Some("49123456789".to_owned()), - } + birth_middle_name: Some("Timotheus".to_owned()), + salutation: Some("Dr.".to_owned()), + msisdn: Some("49123456789".to_owned()), + }) + .unwrap() ) } diff --git a/crates/claims/crates/vc-jose-cose/Cargo.toml b/crates/claims/crates/vc-jose-cose/Cargo.toml index af0bb4924..676bd78ed 100644 --- a/crates/claims/crates/vc-jose-cose/Cargo.toml +++ b/crates/claims/crates/vc-jose-cose/Cargo.toml @@ -26,4 +26,4 @@ ssi-jws = { workspace = true, features = ["secp256r1"] } ssi-jwk.workspace = true ssi-cose = { workspace = true, features = ["secp256r1"] } async-std.workspace = true -hex.workspace = true \ No newline at end of file +hex.workspace = true diff --git a/crates/claims/crates/vc-jose-cose/src/jose/credential.rs b/crates/claims/crates/vc-jose-cose/src/jose/credential.rs index 84d476a30..bc53e268a 100644 --- a/crates/claims/crates/vc-jose-cose/src/jose/credential.rs +++ b/crates/claims/crates/vc-jose-cose/src/jose/credential.rs @@ -2,7 +2,7 @@ use super::JoseDecodeError; use serde::{de::DeserializeOwned, Serialize}; use ssi_claims_core::{ClaimsValidity, DateTimeProvider, SignatureError, ValidateClaims}; use ssi_json_ld::{iref::Uri, syntax::Context}; -use ssi_jws::{CompactJWS, DecodedJWS, JWSPayload, JWSSigner, ValidateJWSHeader}; +use ssi_jws::{DecodedJws, JwsPayload, JwsSigner, JwsSlice, ValidateJwsHeader}; use ssi_vc::{ enveloped::EnvelopedVerifiableCredential, v2::{Credential, CredentialTypes, JsonCredential}, @@ -19,9 +19,9 @@ impl JoseVc { /// Sign a JOSE VC into an enveloped verifiable credential. pub async fn sign_into_enveloped( &self, - signer: &impl JWSSigner, + signer: &impl JwsSigner, ) -> Result { - let jws = JWSPayload::sign(self, signer).await?; + let jws = JwsPayload::sign(self, signer).await?; Ok(EnvelopedVerifiableCredential { context: Context::iri_ref(ssi_vc::v2::CREDENTIALS_V2_CONTEXT_IRI.to_owned().into()), id: format!("data:application/vc-ld+jwt,{jws}").parse().unwrap(), @@ -31,8 +31,8 @@ impl JoseVc { impl JoseVc { /// Decode a JOSE VC. - pub fn decode(jws: &CompactJWS) -> Result, JoseDecodeError> { - jws.to_decoded()? + pub fn decode(jws: &JwsSlice) -> Result, JoseDecodeError> { + jws.decode()? .try_map(|payload| serde_json::from_slice(&payload).map(Self)) .map_err(Into::into) } @@ -40,12 +40,12 @@ impl JoseVc { impl JoseVc { /// Decode a JOSE VC with an arbitrary credential type. - pub fn decode_any(jws: &CompactJWS) -> Result, JoseDecodeError> { + pub fn decode_any(jws: &JwsSlice) -> Result, JoseDecodeError> { Self::decode(jws) } } -impl JWSPayload for JoseVc { +impl JwsPayload for JoseVc { fn typ(&self) -> Option<&str> { Some("vc-ld+jwt") } @@ -59,7 +59,7 @@ impl JWSPayload for JoseVc { } } -impl ValidateJWSHeader for JoseVc { +impl ValidateJwsHeader for JoseVc { fn validate_jws_header(&self, _env: &E, _header: &ssi_jws::Header) -> ClaimsValidity { // There are no formal obligations about `typ` and `cty`. // It SHOULD be `vc-ld+jwt` and `vc`, but it does not MUST. @@ -163,12 +163,12 @@ mod tests { use serde_json::json; use ssi_claims_core::VerificationParameters; use ssi_jwk::JWK; - use ssi_jws::{CompactJWS, CompactJWSBuf}; + use ssi_jws::{JwsSlice, JwsVec}; use ssi_vc::v2::JsonCredential; use crate::JoseVc; - async fn verify(input: &CompactJWS, key: &JWK) { + async fn verify(input: &JwsSlice, key: &JWK) { let vc = JoseVc::decode_any(input).unwrap(); let params = VerificationParameters::from_resolver(key); let result = vc.verify(params).await.unwrap(); @@ -205,7 +205,7 @@ mod tests { let key = JWK::generate_p256(); let enveloped = JoseVc(vc).sign_into_enveloped(&key).await.unwrap(); - let jws = CompactJWSBuf::new(enveloped.id.decoded_data().unwrap().into_owned()).unwrap(); + let jws = JwsVec::new(enveloped.id.decoded_data().unwrap().into_owned()).unwrap(); verify(&jws, &key).await } } diff --git a/crates/claims/crates/vc-jose-cose/src/jose/presentation.rs b/crates/claims/crates/vc-jose-cose/src/jose/presentation.rs index 76328a378..6faad621b 100644 --- a/crates/claims/crates/vc-jose-cose/src/jose/presentation.rs +++ b/crates/claims/crates/vc-jose-cose/src/jose/presentation.rs @@ -2,7 +2,7 @@ use super::JoseDecodeError; use serde::{de::DeserializeOwned, Serialize}; use ssi_claims_core::{ClaimsValidity, SignatureError, ValidateClaims}; use ssi_json_ld::{iref::Uri, syntax::Context}; -use ssi_jws::{CompactJWS, DecodedJWS, JWSPayload, JWSSigner, ValidateJWSHeader}; +use ssi_jws::{DecodedJws, JwsPayload, JwsSigner, JwsSlice, ValidateJwsHeader}; use ssi_vc::{ enveloped::{EnvelopedVerifiableCredential, EnvelopedVerifiablePresentation}, v2::{syntax::JsonPresentation, Presentation, PresentationTypes}, @@ -14,7 +14,7 @@ use std::borrow::Cow; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct JoseVp>(pub T); -impl JWSPayload for JoseVp { +impl JwsPayload for JoseVp { fn typ(&self) -> Option<&str> { Some("vp-ld+jwt") } @@ -28,7 +28,7 @@ impl JWSPayload for JoseVp { } } -impl ValidateJWSHeader for JoseVp { +impl ValidateJwsHeader for JoseVp { fn validate_jws_header(&self, _env: &E, _header: &ssi_jws::Header) -> ClaimsValidity { // There are no formal obligations about `typ` and `cty`. // It SHOULD be `vp-ld+jwt` and `vp`, but it does not MUST. @@ -40,9 +40,9 @@ impl JoseVp { /// Sign a JOSE VC into an enveloped verifiable presentation. pub async fn sign_into_enveloped( &self, - signer: &impl JWSSigner, + signer: &impl JwsSigner, ) -> Result { - let jws = JWSPayload::sign(self, signer).await?; + let jws = JwsPayload::sign(self, signer).await?; Ok(EnvelopedVerifiablePresentation { context: Context::iri_ref(ssi_vc::v2::CREDENTIALS_V2_CONTEXT_IRI.to_owned().into()), id: format!("data:application/vp-ld+jwt,{jws}").parse().unwrap(), @@ -52,8 +52,8 @@ impl JoseVp { impl JoseVp { /// Decode a JOSE VP. - pub fn decode(jws: &CompactJWS) -> Result, JoseDecodeError> { - jws.to_decoded()? + pub fn decode(jws: &JwsSlice) -> Result, JoseDecodeError> { + jws.decode()? .try_map(|payload| serde_json::from_slice(&payload).map(Self)) .map_err(Into::into) } @@ -61,7 +61,7 @@ impl JoseVp { impl JoseVp { /// Decode a JOSE VP with an arbitrary presentation type. - pub fn decode_any(jws: &CompactJWS) -> Result, JoseDecodeError> { + pub fn decode_any(jws: &JwsSlice) -> Result, JoseDecodeError> { Self::decode(jws) } } @@ -108,12 +108,12 @@ mod tests { use serde_json::json; use ssi_claims_core::VerificationParameters; use ssi_jwk::JWK; - use ssi_jws::{CompactJWS, CompactJWSBuf}; + use ssi_jws::{JwsSlice, JwsVec}; use ssi_vc::{enveloped::EnvelopedVerifiableCredential, v2::syntax::JsonPresentation}; use crate::JoseVp; - async fn verify(input: &CompactJWS, key: &JWK) { + async fn verify(input: &JwsSlice, key: &JWK) { let vp = JoseVp::decode_any(input).unwrap(); let params = VerificationParameters::from_resolver(key); let result = vp.verify(params).await.unwrap(); @@ -137,7 +137,7 @@ mod tests { let key = JWK::generate_p256(); let enveloped = JoseVp(vp).sign_into_enveloped(&key).await.unwrap(); - let jws = CompactJWSBuf::new(enveloped.id.decoded_data().unwrap().into_owned()).unwrap(); + let jws = JwsVec::new(enveloped.id.decoded_data().unwrap().into_owned()).unwrap(); verify(&jws, &key).await } } diff --git a/crates/claims/src/lib.rs b/crates/claims/src/lib.rs index 9fd4dc0d8..a38570c3d 100644 --- a/crates/claims/src/lib.rs +++ b/crates/claims/src/lib.rs @@ -12,7 +12,7 @@ pub use ssi_claims_core::*; /// See: pub use ssi_jws as jws; -pub use jws::{CompactJWS, CompactJWSBuf, CompactJWSStr, CompactJWSString, JWSPayload}; +pub use jws::{Jws, JwsBuf, JwsPayload, JwsSlice, JwsStr, JwsString, JwsVec}; /// JSON Web tokens (JWT). /// @@ -62,7 +62,7 @@ pub enum JsonCredentialOrJws { Credential(DataIntegrity), /// JSON Web Signature. - Jws(jws::CompactJWSString), + Jws(jws::JwsString), } /// JSON-like verifiable presentation or JWS (presumably JWT). @@ -81,7 +81,7 @@ pub enum JsonPresentationOrJws Presentation(DataIntegrity), /// JSON Web Signature. - Jws(jws::CompactJWSString), + Jws(jws::JwsString), } #[cfg(test)] diff --git a/crates/core/src/bytes_buf.rs b/crates/core/src/bytes_buf.rs new file mode 100644 index 000000000..af2f6feae --- /dev/null +++ b/crates/core/src/bytes_buf.rs @@ -0,0 +1,13 @@ +/// Byte buffer. +/// +/// Any type that implements `AsRef<[u8]>` and `Into>` such that both +/// implementation yields the same bytes. +/// +/// # Safety +/// +/// The `Into>` **must** return the same bytes as `AsRef<[u8]>`. +pub unsafe trait BytesBuf: AsRef<[u8]> + Into> {} + +unsafe impl BytesBuf for Vec {} + +unsafe impl BytesBuf for String {} diff --git a/crates/claims/crates/data-integrity/sd-primitives/src/json_pointer.rs b/crates/core/src/json_pointer.rs similarity index 51% rename from crates/claims/crates/data-integrity/sd-primitives/src/json_pointer.rs rename to crates/core/src/json_pointer.rs index c957d1416..6812022a8 100644 --- a/crates/claims/crates/data-integrity/sd-primitives/src/json_pointer.rs +++ b/crates/core/src/json_pointer.rs @@ -1,8 +1,24 @@ use core::fmt; -use std::{borrow::Cow, ops::Deref, str::FromStr}; +use std::{ + borrow::{Borrow, Cow}, + ops::Deref, + str::FromStr, +}; use serde::{Deserialize, Serialize}; +use crate::BytesBuf; + +#[macro_export] +macro_rules! json_pointer { + ($value:literal) => { + match $crate::JsonPointer::from_str_const($value) { + Ok(p) => p, + Err(_) => panic!("invalid JSON pointer"), + } + }; +} + #[derive(Debug, Clone, Copy, thiserror::Error)] #[error("invalid JSON pointer `{0}`")] pub struct InvalidJsonPointer(pub T); @@ -10,14 +26,35 @@ pub struct InvalidJsonPointer(pub T); /// JSON Pointer. /// /// See: -#[derive(Debug, Serialize)] -pub struct JsonPointer(str); +#[derive(Debug, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct JsonPointer([u8]); + +impl<'a> Default for &'a JsonPointer { + fn default() -> Self { + JsonPointer::ROOT + } +} impl JsonPointer { + pub const ROOT: &'static Self = unsafe { + // SAFETY: the empty string is a valid JSON pointer. + JsonPointer::new_unchecked(&[]) + }; + /// Converts the given string into a JSON pointer. - pub fn new(s: &str) -> Result<&Self, InvalidJsonPointer<&str>> { - if Self::validate(s) { - Ok(unsafe { Self::new_unchecked(s) }) + pub fn new>(s: &S) -> Result<&Self, InvalidJsonPointer<&S>> { + let bytes = s.as_ref(); + if Self::validate(bytes) { + Ok(unsafe { Self::new_unchecked(bytes) }) + } else { + Err(InvalidJsonPointer(s)) + } + } + + pub const fn from_str_const(s: &str) -> Result<&Self, InvalidJsonPointer<&str>> { + let bytes = s.as_bytes(); + if Self::validate(bytes) { + Ok(unsafe { Self::new_unchecked(bytes) }) } else { Err(InvalidJsonPointer(s)) } @@ -28,25 +65,42 @@ impl JsonPointer { /// # Safety /// /// The input string *must* be a valid JSON pointer. - pub unsafe fn new_unchecked(s: &str) -> &Self { + pub const unsafe fn new_unchecked(s: &[u8]) -> &Self { std::mem::transmute(s) } - pub fn validate(str: &str) -> bool { - let mut chars = str.chars(); - while let Some(c) = chars.next() { - if c == '~' && !matches!(chars.next(), Some('0' | '1')) { - return false; + pub const fn validate(bytes: &[u8]) -> bool { + if std::str::from_utf8(bytes).is_err() { + return false; + }; + + let mut i = 0; + while i < bytes.len() { + // Escape char. + if bytes[i] == b'~' { + i += 1; + if i >= bytes.len() || !matches!(bytes[i], b'0' | b'1') { + return false; + } } + + i += 1 } true } - pub fn as_str(&self) -> &str { + pub fn as_bytes(&self) -> &[u8] { &self.0 } + pub fn as_str(&self) -> &str { + unsafe { + // SAFETY: a JSON pointer is an UTF-8 encoded string by definition. + std::str::from_utf8_unchecked(&self.0) + } + } + pub fn is_empty(&self) -> bool { self.0.is_empty() } @@ -57,9 +111,8 @@ impl JsonPointer { } else { let mut i = 1; - let bytes = self.0.as_bytes(); - while i < bytes.len() { - if bytes[i] == b'/' { + while i < self.0.len() { + if self.0[i] == b'/' { break; } @@ -80,15 +133,29 @@ impl JsonPointer { } pub fn iter(&self) -> JsonPointerIter { - let mut tokens = self.0.split('/'); + let mut tokens = self.as_str().split('/'); tokens.next(); JsonPointerIter(tokens) } } +impl ToOwned for JsonPointer { + type Owned = JsonPointerBuf; + + fn to_owned(&self) -> Self::Owned { + JsonPointerBuf(self.0.to_owned()) + } +} + +impl AsRef for JsonPointer { + fn as_ref(&self) -> &JsonPointer { + self + } +} + impl fmt::Display for JsonPointer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) + self.as_str().fmt(f) } } @@ -114,30 +181,56 @@ impl<'a> Iterator for JsonPointerIter<'a> { /// JSON Pointer buffer. /// /// See: -#[derive(Debug, Clone, Serialize)] -pub struct JsonPointerBuf(String); +#[derive(Debug, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct JsonPointerBuf(Vec); + +impl Default for JsonPointerBuf { + fn default() -> Self { + JsonPointer::ROOT.to_owned() + } +} impl JsonPointerBuf { - /// Converts the given string into an owned JSON pointer. - pub fn new(value: String) -> Result { - if JsonPointer::validate(&value) { - Ok(Self(value)) + /// Converts the given byte string into an owned JSON pointer. + pub fn new(value: B) -> Result> { + if JsonPointer::validate(value.as_ref()) { + Ok(Self(value.into())) } else { Err(InvalidJsonPointer(value)) } } - /// Converts the given byte string into an owned JSON pointer. - pub fn from_bytes(value: Vec) -> Result>> { - match String::from_utf8(value) { - Ok(value) => { - if JsonPointer::validate(&value) { - Ok(Self(value)) - } else { - Err(InvalidJsonPointer(value.into_bytes())) + pub fn push(&mut self, token: &str) { + self.0.push(b'/'); + for c in token.chars() { + match c { + '~' => { + self.0.push(b'~'); + self.0.push(b'0'); + } + '/' => { + self.0.push(b'~'); + self.0.push(b'1'); + } + _ => { + let i = self.0.len(); + let len = c.len_utf8(); + self.0.resize(i + len, 0); + c.encode_utf8(&mut self.0[i..]); } } - Err(err) => Err(InvalidJsonPointer(err.into_bytes())), + } + } + + pub fn push_index(&mut self, i: usize) { + self.push(&i.to_string()) + } + + pub fn as_json_pointer(&self) -> &JsonPointer { + unsafe { + // SAFETY: the inner bytes are representing a JSON pointer by + // construction. + JsonPointer::new_unchecked(&self.0) } } } @@ -150,6 +243,18 @@ impl Deref for JsonPointerBuf { } } +impl Borrow for JsonPointerBuf { + fn borrow(&self) -> &JsonPointer { + self.as_json_pointer() + } +} + +impl AsRef for JsonPointerBuf { + fn as_ref(&self) -> &JsonPointer { + self.as_json_pointer() + } +} + impl FromStr for JsonPointerBuf { type Err = InvalidJsonPointer; @@ -168,7 +273,7 @@ impl TryFrom for JsonPointerBuf { impl fmt::Display for JsonPointerBuf { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) + self.as_str().fmt(f) } } @@ -185,7 +290,7 @@ impl<'de> Deserialize<'de> for JsonPointerBuf { #[derive(Debug)] #[repr(transparent)] -pub struct ReferenceToken(str); +pub struct ReferenceToken([u8]); impl ReferenceToken { /// Converts the given string into a JSON pointer reference token without @@ -194,25 +299,37 @@ impl ReferenceToken { /// # Safety /// /// The input string *must* be a valid JSON pointer reference token. - pub unsafe fn new_unchecked(s: &str) -> &Self { + pub const unsafe fn new_unchecked(s: &[u8]) -> &Self { std::mem::transmute(s) } pub fn is_escaped(&self) -> bool { - self.0.contains('~') + self.0.contains(&b'~') + } + + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + pub fn as_str(&self) -> &str { + unsafe { + // SAFETY: a reference token is an UTF-8 encoded string by + // definition. + std::str::from_utf8_unchecked(&self.0) + } } - pub fn to_str(&self) -> Cow { + pub fn to_decoded(&self) -> Cow { if self.is_escaped() { Cow::Owned(self.decode()) } else { - Cow::Borrowed(&self.0) + Cow::Borrowed(self.as_str()) } } pub fn decode(&self) -> String { let mut result = String::new(); - let mut chars = self.0.chars(); + let mut chars = self.as_str().chars(); while let Some(c) = chars.next() { let decoded_c = match c { '~' => match chars.next() { @@ -230,7 +347,7 @@ impl ReferenceToken { } pub fn as_array_index(&self) -> Option { - let mut chars = self.0.chars(); + let mut chars = self.as_str().chars(); let mut i = chars.next()?.to_digit(10)? as usize; if i == 0 { match chars.next() { @@ -250,6 +367,6 @@ impl ReferenceToken { impl fmt::Display for ReferenceToken { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) + self.as_str().fmt(f) } } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 13793b8c3..3b1b41df6 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -2,3 +2,9 @@ pub mod de; pub mod one_or_many; pub use one_or_many::OneOrMany; + +pub mod bytes_buf; +pub use bytes_buf::BytesBuf; + +pub mod json_pointer; +pub use json_pointer::{JsonPointer, JsonPointerBuf}; diff --git a/crates/dids/methods/ion/src/sidetree/operation/mod.rs b/crates/dids/methods/ion/src/sidetree/operation/mod.rs index 1a25f0d2f..49674391e 100644 --- a/crates/dids/methods/ion/src/sidetree/operation/mod.rs +++ b/crates/dids/methods/ion/src/sidetree/operation/mod.rs @@ -297,10 +297,10 @@ pub fn jws_decode_verify_inner( jwt: &str, get_key: impl FnOnce(&Claims) -> &PublicKeyJwk, ) -> Result<(ssi_jws::Header, Claims), JWSDecodeVerifyError> { - use ssi_jws::{decode_jws_parts, split_jws, verify_bytes, DecodedJWS}; + use ssi_jws::{decode_jws_parts, split_jws, verify_bytes, DecodedJws}; let (header_b64, payload_enc, signature_b64) = split_jws(jwt).map_err(JWSDecodeVerifyError::SplitJWS)?; - let DecodedJWS { + let DecodedJws { signing_bytes: DecodedSigningBytes { bytes: signing_bytes, diff --git a/crates/dids/methods/tz/tests/did.rs b/crates/dids/methods/tz/tests/did.rs index 676b5a220..904a79676 100644 --- a/crates/dids/methods/tz/tests/did.rs +++ b/crates/dids/methods/tz/tests/did.rs @@ -12,7 +12,7 @@ use ssi_claims::{ }; use ssi_dids_core::{did, resolution::Options, DIDResolver, VerificationMethodDIDResolver}; use ssi_jwk::JWK; -use ssi_jws::CompactJWSString; +use ssi_jws::JwsString; use ssi_verification_methods_core::{ProofPurpose, SingleSecretSigner}; use static_iref::{iri, uri}; @@ -177,7 +177,7 @@ async fn test_derivation_tz3() { // let header = ssi_jws::Header::new_unencoded(ssi_jwk::Algorithm::EdBlake2b, None); // let signing_bytes = header.encode_signing_bytes(&payload); // let signature = ssi_jws::sign_bytes(ssi_jwk::Algorithm::EdBlake2b, &signing_bytes, &key).unwrap(); -// let jws = ssi_jws::CompactJWSString::encode_detached(header, &signature); +// let jws = ssi_jws::JwsBuf::encode_detached(header, &signature); // eprintln!("JWS: {jws}"); // } @@ -235,7 +235,7 @@ async fn credential_prove_verify_did_tz1() { ssi_claims::data_integrity::suites::tezos::Options::new( r#"{"crv": "Ed25519","kty": "OKP","x": "CFdO_rVP08v1wQQVNybqBxHmTPOBPIt4Kn6LLhR1fMA"}"#.parse().unwrap() ), - ssi_claims::data_integrity::signing::JwsSignature::new( + ssi_claims::data_integrity::signing::DetachedJwsSignature::new( // FIXME: this is wrong! The VM expects an EdBlake2b signature, // instead this is EdDsa. "eyJhbGciOiJFZERTQSIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9..thpumbPTltH6b6P9QUydy8DcoK2Jj63-FIntxiq09XBk7guF_inA0iQWw7_B_GBwmmsmhYdGL4TdtiNieAdeAg".parse().unwrap() @@ -282,7 +282,7 @@ async fn credential_prove_verify_did_tz1() { ssi_claims::data_integrity::suites::tezos::Options::new( r#"{"crv": "Ed25519","kty": "OKP","x": "CFdO_rVP08v1wQQVNybqBxHmTPOBPIt4Kn6LLhR1fMA"}"#.parse().unwrap() ), - ssi_claims::data_integrity::signing::JwsSignature::new( + ssi_claims::data_integrity::signing::DetachedJwsSignature::new( // FIXME: this is wrong! The VM expects an EdBlake2b signature, // instead this is EdDsa. "eyJhbGciOiJFZERTQSIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9..7GLIUeNKvO3WsA3DmBZpbuPinhOcv7Mhgx9QP0svO55T_Zoy7wmJJtLXSoghtkI7DWOnVbiJO5X246Qr0CqGDw".parse().unwrap() @@ -297,7 +297,7 @@ async fn credential_prove_verify_did_tz1() { // mess with the VP proof to make verify fail let mut vp1 = vp.clone(); - vp1.proofs.first_mut().unwrap().signature.jws = CompactJWSString::from_string(format!( + vp1.proofs.first_mut().unwrap().signature.jws = JwsString::from_string(format!( "x{}", vp1.proofs.first_mut().unwrap().signature.jws )) diff --git a/crates/status/examples/status_list.rs b/crates/status/examples/status_list.rs index c613b1a95..097b2b6d1 100644 --- a/crates/status/examples/status_list.rs +++ b/crates/status/examples/status_list.rs @@ -100,7 +100,7 @@ impl Command { let signature = ssi_jws::sign_bytes_b64(header.algorithm, &signing_bytes, &jwk).unwrap(); - let jws = ssi_jws::CompactJWSString::from_signing_bytes_and_signature( + let jws = ssi_jws::JwsString::from_signing_bytes_and_signature( signing_bytes, signature.into_bytes(), ) diff --git a/crates/status/src/impl/bitstring_status_list/mod.rs b/crates/status/src/impl/bitstring_status_list/mod.rs index 5cca39a4b..c4f4e722a 100644 --- a/crates/status/src/impl/bitstring_status_list/mod.rs +++ b/crates/status/src/impl/bitstring_status_list/mod.rs @@ -532,9 +532,8 @@ impl<'a> Iterator for BitStringIter<'a> { type Item = u8; fn next(&mut self) -> Option { - self.bit_string.get(self.index).map(|status| { + self.bit_string.get(self.index).inspect(|_| { self.index += 1; - status }) } } diff --git a/crates/status/src/impl/bitstring_status_list/syntax/entry_set/credential.rs b/crates/status/src/impl/bitstring_status_list/syntax/entry_set/credential.rs index 25d105da9..d55ba56d8 100644 --- a/crates/status/src/impl/bitstring_status_list/syntax/entry_set/credential.rs +++ b/crates/status/src/impl/bitstring_status_list/syntax/entry_set/credential.rs @@ -15,7 +15,7 @@ use ssi_json_ld::{ Loader, }; use ssi_jwk::JWKResolver; -use ssi_jws::{CompactJWS, InvalidCompactJWS, ValidateJWSHeader}; +use ssi_jws::{InvalidJws, JwsSlice, ValidateJwsHeader}; use ssi_vc::v2::{syntax::JsonCredentialTypes, Context}; use ssi_verification_methods::{ssi_core::OneOrMany, AnyMethod, VerificationMethodResolver}; @@ -101,7 +101,7 @@ impl ValidateClaims for BitstringStatusListEntrySetCredential { } } -impl ValidateJWSHeader for BitstringStatusListEntrySetCredential { +impl ValidateJwsHeader for BitstringStatusListEntrySetCredential { fn validate_jws_header(&self, _env: &E, _header: &ssi_jws::Header) -> ClaimsValidity { Ok(()) } @@ -122,9 +122,9 @@ where ) -> Result { match media_type { "application/vc+ld+json+jwt" => { - let jws = CompactJWS::new(bytes) - .map_err(InvalidCompactJWS::into_owned)? - .to_decoded()? + let jws = JwsSlice::new(bytes) + .map_err(InvalidJws::into_owned)? + .decode()? .try_map::(|bytes| serde_json::from_slice(&bytes))?; jws.verify(params).await??; Ok(jws.signing_bytes.payload) diff --git a/crates/status/src/impl/bitstring_status_list/syntax/status_list/credential.rs b/crates/status/src/impl/bitstring_status_list/syntax/status_list/credential.rs index f0d729fcd..81c920274 100644 --- a/crates/status/src/impl/bitstring_status_list/syntax/status_list/credential.rs +++ b/crates/status/src/impl/bitstring_status_list/syntax/status_list/credential.rs @@ -16,7 +16,7 @@ use ssi_json_ld::{ Loader, }; use ssi_jwk::JWKResolver; -use ssi_jws::{CompactJWS, InvalidCompactJWS, ValidateJWSHeader}; +use ssi_jws::{InvalidJws, JwsSlice, ValidateJwsHeader}; use ssi_vc::{ syntax::RequiredType, v2::syntax::{Context, JsonCredentialTypes}, @@ -155,7 +155,7 @@ where } } -impl ValidateJWSHeader for BitstringStatusListCredential { +impl ValidateJwsHeader for BitstringStatusListCredential { fn validate_jws_header(&self, _env: &E, _header: &ssi_jws::Header) -> ClaimsValidity { Ok(()) } @@ -185,7 +185,7 @@ pub enum FromBytesError { UnexpectedMediaType(String), #[error(transparent)] - CompactJWS(#[from] InvalidCompactJWS>), + Jws(#[from] InvalidJws>), #[error("invalid JWS: {0}")] JWS(#[from] ssi_jws::DecodeError), @@ -221,9 +221,9 @@ where ) -> Result { match media_type { "application/vc+ld+json+jwt" => { - let jws = CompactJWS::new(bytes) - .map_err(InvalidCompactJWS::into_owned)? - .to_decoded()? + let jws = JwsSlice::new(bytes) + .map_err(InvalidJws::into_owned)? + .decode()? .try_map::(|bytes| serde_json::from_slice(&bytes))?; jws.verify(params).await??; Ok(jws.signing_bytes.payload) diff --git a/crates/status/src/impl/token_status_list/json.rs b/crates/status/src/impl/token_status_list/json.rs index 06c361f59..4ecb6c25d 100644 --- a/crates/status/src/impl/token_status_list/json.rs +++ b/crates/status/src/impl/token_status_list/json.rs @@ -6,8 +6,11 @@ use flate2::Compression; use iref::UriBuf; use serde::{Deserialize, Serialize}; use ssi_claims_core::ValidateClaims; -use ssi_jws::ValidateJWSHeader; -use ssi_jwt::{match_claim_type, AnyClaims, Claim, ClaimSet, IssuedAt, Issuer, JWTClaims, Subject}; +use ssi_jws::ValidateJwsHeader; +use ssi_jwt::{ + match_claim_type, AnyClaims, Claim, ClaimSet, InvalidClaimValue, IssuedAt, Issuer, JWTClaims, + Subject, +}; use crate::{ token_status_list::{BitString, StatusSize}, @@ -75,8 +78,6 @@ pub struct StatusListJwtPrivateClaims { } impl ClaimSet for StatusListJwtPrivateClaims { - type Error = serde_json::Error; - fn contains(&self) -> bool { match_claim_type! { match C { @@ -87,7 +88,7 @@ impl ClaimSet for StatusListJwtPrivateClaims { } } - fn try_get(&self) -> Result>, Self::Error> { + fn try_get(&self) -> Result>, InvalidClaimValue> { match_claim_type! { match C { TimeToLiveClaim => { @@ -103,7 +104,7 @@ impl ClaimSet for StatusListJwtPrivateClaims { } } - fn try_set(&mut self, claim: C) -> Result, Self::Error> { + fn try_set(&mut self, claim: C) -> Result, InvalidClaimValue> { match_claim_type! { match claim: C { TimeToLiveClaim => { @@ -121,7 +122,7 @@ impl ClaimSet for StatusListJwtPrivateClaims { } } - fn try_remove(&mut self) -> Result, Self::Error> { + fn try_remove(&mut self) -> Result, InvalidClaimValue> { match_claim_type! { match C { TimeToLiveClaim => { @@ -144,7 +145,7 @@ impl ValidateClaims for StatusListJwtPrivateClaims { } } -impl ValidateJWSHeader for StatusListJwtPrivateClaims { +impl ValidateJwsHeader for StatusListJwtPrivateClaims { fn validate_jws_header( &self, _env: &E, diff --git a/crates/status/src/impl/token_status_list/mod.rs b/crates/status/src/impl/token_status_list/mod.rs index 721b55864..b66367990 100644 --- a/crates/status/src/impl/token_status_list/mod.rs +++ b/crates/status/src/impl/token_status_list/mod.rs @@ -22,8 +22,8 @@ pub mod json; pub use json::StatusListJwt; use ssi_jwk::JWKResolver; -use ssi_jws::{CompactJWS, InvalidCompactJWS}; -use ssi_jwt::{ClaimSet, JWTClaims, ToDecodedJWT}; +use ssi_jws::{InvalidJws, JwsSlice}; +use ssi_jwt::{ClaimSet, InvalidClaimValue, JWTClaims, ToDecodedJwt}; use crate::{ EncodedStatusMap, FromBytes, FromBytesOptions, Overflow, StatusMap, StatusMapEntry, @@ -73,7 +73,7 @@ pub enum FromBytesError { UnexpectedMediaType(String), #[error(transparent)] - JWS(#[from] InvalidCompactJWS>), + JWS(#[from] InvalidJws>), #[error("invalid JWT: {0}")] JWT(#[from] ssi_jwt::DecodeError), @@ -109,8 +109,8 @@ where ) -> Result { match media_type { "statuslist+jwt" => { - let jwt = CompactJWS::new(bytes) - .map_err(InvalidCompactJWS::into_owned)? + let jwt = JwsSlice::new(bytes) + .map_err(InvalidJws::into_owned)? .to_decoded_custom_jwt::()?; match jwt.signing_bytes.header.type_.as_deref() { @@ -490,9 +490,8 @@ impl<'a> Iterator for BitStringIter<'a> { type Item = u8; fn next(&mut self) -> Option { - self.bit_string.get(self.index).map(|status| { + self.bit_string.get(self.index).inspect(|_| { self.index += 1; - status }) } } @@ -503,11 +502,14 @@ pub enum EntrySetFromBytesError { Json(#[from] serde_json::Error), #[error(transparent)] - JWS(#[from] InvalidCompactJWS>), + JWS(#[from] InvalidJws>), #[error(transparent)] JWT(#[from] ssi_jwt::DecodeError), + #[error(transparent)] + ClaimValue(#[from] InvalidClaimValue), + #[error("proof preparation failed: {0}")] ProofPreparation(#[from] ssi_claims_core::ProofPreparationError), @@ -549,8 +551,8 @@ where )) } "application/jwt" => { - let jwt = CompactJWS::new(bytes) - .map_err(InvalidCompactJWS::into_owned)? + let jwt = JwsSlice::new(bytes) + .map_err(InvalidJws::into_owned)? .to_decoded_jwt()?; jwt.verify(verifier).await??; diff --git a/crates/ucan/src/lib.rs b/crates/ucan/src/lib.rs index 32ecdbab9..b2ae5e32d 100644 --- a/crates/ucan/src/lib.rs +++ b/crates/ucan/src/lib.rs @@ -22,7 +22,7 @@ use ssi_dids_core::{ DIDBuf, DIDResolver, DIDURLBuf, Document, }; use ssi_jwk::{Algorithm, JWK}; -use ssi_jws::{decode_jws_parts, sign_bytes, split_jws, verify_bytes, Header, JWSSignature}; +use ssi_jws::{decode_jws_parts, sign_bytes, split_jws, verify_bytes, Header, JwsSignature}; use ssi_jwt::NumericDate; use ssi_verification_methods::{GenericVerificationMethod, InvalidVerificationMethod}; use std::{ @@ -36,7 +36,7 @@ use std::{ pub struct Ucan { pub header: Header, pub payload: Payload, - pub signature: JWSSignature, + pub signature: JwsSignature, // unfortunately this matters for sig verification // we have to keep track of how this ucan was created // alternatively we could have 2 different types? @@ -106,7 +106,7 @@ impl Ucan { self.header.algorithm, self.encode()? .rsplit_once('.') - .ok_or(ssi_jws::Error::InvalidJWS)? + .ok_or(ssi_jws::Error::InvalidJws)? .0 .as_bytes(), &key, diff --git a/crates/verification-methods/src/methods/w3c/ecdsa_secp_256k1_verification_key_2019.rs b/crates/verification-methods/src/methods/w3c/ecdsa_secp_256k1_verification_key_2019.rs index 51aa43dbd..edd280c46 100644 --- a/crates/verification-methods/src/methods/w3c/ecdsa_secp_256k1_verification_key_2019.rs +++ b/crates/verification-methods/src/methods/w3c/ecdsa_secp_256k1_verification_key_2019.rs @@ -90,7 +90,7 @@ impl EcdsaSecp256k1VerificationKey2019 { // let signing_bytes = header.encode_signing_bytes(data); ssi_jws::sign_bytes(algorithm, signing_bytes, secret_key) .map_err(|_| MessageSignatureError::InvalidSecretKey) - // Ok(CompactJWSString::from_signing_bytes_and_signature(signing_bytes, signature).unwrap()) + // Ok(JwsBuf::from_signing_bytes_and_signature(signing_bytes, signature).unwrap()) } pub fn verify_bytes( diff --git a/crates/verification-methods/src/methods/w3c/ed25519_verification_key_2018.rs b/crates/verification-methods/src/methods/w3c/ed25519_verification_key_2018.rs index 7b69fbc4e..828796e4e 100644 --- a/crates/verification-methods/src/methods/w3c/ed25519_verification_key_2018.rs +++ b/crates/verification-methods/src/methods/w3c/ed25519_verification_key_2018.rs @@ -8,7 +8,7 @@ use ssi_claims_core::{ InvalidProof, MessageSignatureError, ProofValidationError, ProofValidity, SignatureError, }; use ssi_jwk::JWK; -use ssi_jws::CompactJWSString; +use ssi_jws::JwsString; use ssi_verification_methods_core::{JwkVerificationMethod, VerificationMethodSet, VerifyBytes}; use static_iref::iri; @@ -65,12 +65,12 @@ impl Ed25519VerificationKey2018 { &self, data: &[u8], secret_key: &ed25519_dalek::SigningKey, - ) -> Result { + ) -> Result { let header = ssi_jws::Header::new_unencoded(ssi_jwk::Algorithm::EdDSA, None); let signing_bytes = header.encode_signing_bytes(data); let signature = secret_key.sign(&signing_bytes); - Ok(ssi_jws::CompactJWSString::from_signing_bytes_and_signature( + Ok(ssi_jws::JwsString::from_signing_bytes_and_signature( // TODO base64 encode signature? signing_bytes, signature.to_bytes(), diff --git a/examples/issue.rs b/examples/issue.rs index 4d53be8fb..93ee97001 100644 --- a/examples/issue.rs +++ b/examples/issue.rs @@ -5,7 +5,7 @@ use serde_json::json; use ssi_claims::{ data_integrity::{AnySuite, CryptographicSuite, ProofOptions}, - jws::JWSPayload, + jws::JwsPayload, vc::v1::ToJwtClaims, VerificationParameters, }; diff --git a/examples/present.rs b/examples/present.rs index 0160f2557..6076473c6 100644 --- a/examples/present.rs +++ b/examples/present.rs @@ -12,7 +12,7 @@ use ssi::{ claims::{ data_integrity::{AnySuite, CryptographicSuite, ProofOptions}, - jws::{CompactJWSString, JWSPayload}, + jws::{JwsPayload, JwsString}, }, verification_methods::{ProofPurpose, SingleSecretSigner}, }; @@ -31,7 +31,7 @@ async fn verify(proof_format_in: &str, proof_format_out: &str, input_vc: &str) { serde_json::from_str(input_vc).unwrap(); ssi::claims::JsonCredentialOrJws::Credential(vc_ldp) } - "jwt" => match CompactJWSString::from_string(input_vc.to_string()) { + "jwt" => match JwsString::from_string(input_vc.to_string()) { Ok(vc_jwt) => ssi::claims::JsonCredentialOrJws::Jws(vc_jwt), Err(_) => { panic!("Input must be a compact JWT"); diff --git a/src/lib.rs b/src/lib.rs index a552b05e9..460eb5f3a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,12 +36,12 @@ //! //! The simplest type of claim to load and verify is probably JSON Web //! Signatures (JWSs), often use to encode JSON Web Tokens (JWTs). To represent -//! such claims SSI provides the `CompactJWSString` type representing a JWS -//! in compact textual form. One can load a JWS using [`from_string`] and verify +//! such claims SSI provides the `JwsBuf` type representing a JWS +//! in compact textual form. One can load a JWS using [`new`] and verify //! it using [`verify`]. //! -//! [`from_string`]: crate::claims::CompactJWSString::from_string -//! [`verify`]: crate::claims::CompactJWS::verify +//! [`new`]: claims::JwsBuf::new +//! [`verify`]: claims::JwsSlice::verify //! //! ``` //! # use ssi_dids::example::ExampleDIDResolver; @@ -50,7 +50,7 @@ //! use ssi::prelude::*; //! //! // Load a JWT from the file system. -//! let jwt = CompactJWSString::from_string( +//! let jwt = JwsBuf::new( //! std::fs::read_to_string("examples/files/claims.jwt") //! .expect("unable to load JWT") //! ).expect("invalid JWS"); @@ -79,7 +79,7 @@ //! that will simply load a VC from a string, assuming it is signed using //! any Data-Integrity proof supported by SSI. //! -//! [`any_credential_from_json_str`]: crate::claims::vc::v1::data_integrity::any_credential_from_json_str +//! [`any_credential_from_json_str`]: claims::vc::v1::data_integrity::any_credential_from_json_str //! //! ``` //! # use ssi_dids::example::ExampleDIDResolver; @@ -164,7 +164,7 @@ //! implementation of the VC data-model 1.1 where you can set the credential type //! yourself. //! -//! [`SpecializedJsonCredential`]: crate::claims::vc::v1::SpecializedJsonCredential +//! [`SpecializedJsonCredential`]: claims::vc::v1::SpecializedJsonCredential //! //! ``` //! # #[async_std::main] @@ -229,7 +229,7 @@ //! [`SpecializedJsonCredential`]'s [`context`] field or leveraging its context type //! parameter. //! -//! [`context`]: crate::claims::vc::v1::SpecializedJsonCredential::context +//! [`context`]: claims::vc::v1::SpecializedJsonCredential::context //! //! # Data-Models //! @@ -237,8 +237,8 @@ //! - [`VC data-model 2.0`] //! - [`A wrapper type to accept both`] //! -//! [`VC data-model 2.0`]: crate::claims::vc::v2 -//! [`A wrapper type to accept both`]: crate::claims::vc::syntax::AnySpecializedJsonCredential +//! [`VC data-model 2.0`]: claims::vc::v2 +//! [`A wrapper type to accept both`]: claims::vc::syntax::AnySpecializedJsonCredential //! //! # Features #![doc = document_features::document_features!()] diff --git a/src/prelude.rs b/src/prelude.rs index c4e9975d6..4db90dbcc 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -5,7 +5,7 @@ pub use crate::{ ProofConfiguration, ProofOptions, }, vc::syntax::{AnyJsonCredential, AnyJsonPresentation}, - CompactJWS, CompactJWSBuf, CompactJWSStr, CompactJWSString, JWSPayload, JWTClaims, + JWTClaims, Jws, JwsBuf, JwsPayload, JwsSlice, JwsStr, JwsString, JwsVec, VerificationParameters, }, dids::{DIDResolver, DIDJWK},