diff --git a/crates/claims/crates/jwt/src/claims/any.rs b/crates/claims/crates/jwt/src/claims/any.rs index 7c4b33045..d9e7ed485 100644 --- a/crates/claims/crates/jwt/src/claims/any.rs +++ b/crates/claims/crates/jwt/src/claims/any.rs @@ -49,6 +49,12 @@ impl IntoIterator for AnyClaims { } } +impl FromIterator<(String, serde_json::Value)> for AnyClaims { + fn from_iter>(iter: T) -> Self { + Self(BTreeMap::from_iter(iter)) + } +} + impl ClaimSet for AnyClaims { fn contains(&self) -> bool { self.contains(C::JWT_CLAIM_NAME) diff --git a/crates/claims/crates/sd-jwt/src/lib.rs b/crates/claims/crates/sd-jwt/src/lib.rs index dfb32979d..1e2d1e038 100644 --- a/crates/claims/crates/sd-jwt/src/lib.rs +++ b/crates/claims/crates/sd-jwt/src/lib.rs @@ -36,7 +36,7 @@ use ssi_claims_core::{ DateTimeProvider, ProofValidationError, ResolverProvider, SignatureError, ValidateClaims, Verification, }; -use ssi_core::{BytesBuf, JsonPointer, JsonPointerBuf}; +use ssi_core::BytesBuf; use ssi_jwk::JWKResolver; use ssi_jws::{DecodedJws, Jws, JwsPayload, JwsSignature, JwsSigner, ValidateJwsHeader}; use ssi_jwt::{AnyClaims, ClaimSet, DecodedJwt, JWTClaims}; @@ -48,6 +48,8 @@ use std::{ str::FromStr, }; +pub use ssi_core::{json_pointer, JsonPointer, JsonPointerBuf}; + pub(crate) mod utils; use utils::is_url_safe_base64_char; @@ -339,7 +341,7 @@ impl<'de> serde::Deserialize<'de> for &'de SdJwt { } /// Owned SD-JWT. -#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct SdJwtBuf(Vec); impl SdJwtBuf { diff --git a/crates/claims/crates/vc-jose-cose/Cargo.toml b/crates/claims/crates/vc-jose-cose/Cargo.toml index 676bd78ed..eb97fbc6c 100644 --- a/crates/claims/crates/vc-jose-cose/Cargo.toml +++ b/crates/claims/crates/vc-jose-cose/Cargo.toml @@ -11,6 +11,8 @@ documentation = "https://docs.rs/vc-jose-cose/" [dependencies] ssi-claims-core.workspace = true ssi-jws.workspace = true +ssi-jwt.workspace = true +ssi-sd-jwt.workspace = true ssi-cose.workspace = true ssi-vc.workspace = true ssi-json-ld.workspace = true diff --git a/crates/claims/crates/vc-jose-cose/src/lib.rs b/crates/claims/crates/vc-jose-cose/src/lib.rs index ace24fe0b..96551c36f 100644 --- a/crates/claims/crates/vc-jose-cose/src/lib.rs +++ b/crates/claims/crates/vc-jose-cose/src/lib.rs @@ -7,3 +7,6 @@ pub use jose::*; mod cose; pub use cose::*; + +mod sd_jwt; +pub use sd_jwt::*; diff --git a/crates/claims/crates/vc-jose-cose/src/sd_jwt/credential.rs b/crates/claims/crates/vc-jose-cose/src/sd_jwt/credential.rs new file mode 100644 index 000000000..c08f3b82e --- /dev/null +++ b/crates/claims/crates/vc-jose-cose/src/sd_jwt/credential.rs @@ -0,0 +1,286 @@ +use std::borrow::Borrow; + +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use ssi_claims_core::{ClaimsValidity, DateTimeProvider, SignatureError, ValidateClaims}; +use ssi_json_ld::{iref::Uri, syntax::Context}; +use ssi_jws::JwsSigner; +use ssi_jwt::{ClaimSet, InfallibleClaimSet, JWTClaims}; +use ssi_sd_jwt::{JsonPointer, RevealError, RevealedSdJwt, SdAlg, SdJwt, SdJwtBuf}; +use ssi_vc::{ + enveloped::EnvelopedVerifiableCredential, + v2::{Credential, CredentialTypes, JsonCredential}, + MaybeIdentified, +}; +use xsd_types::DateTimeStamp; + +/// SD-JWT Verifiable Credential. +/// +/// See: +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(transparent)] +pub struct SdJwtVc(pub T); + +impl SdJwtVc { + /// Returns this credential as JWT claims. + /// + /// These are the claims that will be encoded in the SD-JWT. + pub fn as_jwt_claims(&self) -> JWTClaims<&Self> { + JWTClaims { + registered: Default::default(), + private: self, + } + } + + /// Turns this credential into JWT claims. + /// + /// These claims can then be encoded in the SD-JWT. + pub fn into_jwt_claims(self) -> JWTClaims { + JWTClaims { + registered: Default::default(), + private: self, + } + } +} + +impl SdJwtVc { + /// Signs the credential into an SD-JWT without any concealed claims. + /// + /// The generated SD-JWT will not have any disclosures. + /// + /// Use [`Self::conceal_and_sign`] to select the claims to be concealed. + pub async fn sign(&self, signer: &impl JwsSigner) -> Result { + let pointers: [&JsonPointer; 0] = []; + self.conceal_and_sign(SdAlg::Sha256, &pointers, signer) + .await + } + + /// Signs the credential while concealing the claims selected by the given + /// JSON pointers. + /// + /// You can use [`Self::sign`] directly if you don't need to conceal + /// anything. + pub async fn conceal_and_sign( + &self, + sd_alg: SdAlg, + pointers: &[impl Borrow], + signer: &impl JwsSigner, + ) -> Result { + SdJwtBuf::conceal_and_sign(&self.as_jwt_claims(), sd_alg, pointers, signer).await + } + + /// Signs the credential into an enveloped verifiable credential (with an + /// SD-JWT identifier) without concealing any claim. + /// + /// The generated SD-JWT, encoded in the credential identifier, will not + /// have any disclosures. + /// + /// Use [`Self::conceal_and_sign_into_enveloped`] to select the claims to be + /// concealed. + pub async fn sign_into_enveloped( + &self, + signer: &impl JwsSigner, + ) -> Result { + let pointers: [&JsonPointer; 0] = []; + self.conceal_and_sign_into_enveloped(SdAlg::Sha256, &pointers, signer) + .await + } + + /// Signs the credential into an enveloped verifiable credential (with an + /// SD-JWT identifier) while concealing the claims selected by the given + /// JSON pointers. + /// + /// The generated SD-JWT, encoded in the credential identifier, will not + /// have any disclosures. + /// + /// Use [`Self::conceal_and_sign_into_enveloped`] to select the claims to be + /// concealed. + pub async fn conceal_and_sign_into_enveloped( + &self, + sd_alg: SdAlg, + pointers: &[impl Borrow], + signer: &impl JwsSigner, + ) -> Result { + let sd_jwt = self.conceal_and_sign(sd_alg, pointers, signer).await?; + Ok(EnvelopedVerifiableCredential { + context: Context::iri_ref(ssi_vc::v2::CREDENTIALS_V2_CONTEXT_IRI.to_owned().into()), + id: format!("data:application/vc-ld+sd-jwt,{sd_jwt}") + .parse() + .unwrap(), + }) + } +} + +impl SdJwtVc { + /// Decodes a SD-JWT VC, revealing its disclosed claims. + /// + /// This function requires the `T` parameter, representing the credential + /// type, to be known. If you don't know what `T` you should use, use the + /// [`Self::decode_reveal_any`]. + pub fn decode_reveal(sd_jwt: &SdJwt) -> Result, RevealError> { + sd_jwt.decode_reveal() + } +} + +impl SdJwtVc { + /// Decodes a SD-JWT VC, revealing its disclosed claims. + /// + /// This function uses [`JsonCredential`] as credential type. If you need + /// to use a custom credential type, use the [`Self::decode_reveal`] + /// function. + pub fn decode_reveal_any(sd_jwt: &SdJwt) -> Result, RevealError> { + sd_jwt.decode_reveal() + } +} + +impl MaybeIdentified for SdJwtVc { + fn id(&self) -> Option<&Uri> { + self.0.id() + } +} + +impl Credential for SdJwtVc { + type Description = T::Description; + type Subject = T::Subject; + type Issuer = T::Issuer; + type Status = T::Status; + type Schema = T::Schema; + type RelatedResource = T::RelatedResource; + type RefreshService = T::RefreshService; + type TermsOfUse = T::TermsOfUse; + type Evidence = T::Evidence; + + fn id(&self) -> Option<&Uri> { + Credential::id(&self.0) + } + + fn additional_types(&self) -> &[String] { + self.0.additional_types() + } + + fn types(&self) -> CredentialTypes { + self.0.types() + } + + fn name(&self) -> Option<&str> { + self.0.name() + } + + fn description(&self) -> Option<&Self::Description> { + self.0.description() + } + + fn credential_subjects(&self) -> &[Self::Subject] { + self.0.credential_subjects() + } + + fn issuer(&self) -> &Self::Issuer { + self.0.issuer() + } + + fn valid_from(&self) -> Option { + self.0.valid_from() + } + + fn valid_until(&self) -> Option { + self.0.valid_until() + } + + fn credential_status(&self) -> &[Self::Status] { + self.0.credential_status() + } + + fn credential_schemas(&self) -> &[Self::Schema] { + self.0.credential_schemas() + } + + fn related_resources(&self) -> &[Self::RelatedResource] { + self.0.related_resources() + } + + fn refresh_services(&self) -> &[Self::RefreshService] { + self.0.refresh_services() + } + + fn terms_of_use(&self) -> &[Self::TermsOfUse] { + self.0.terms_of_use() + } + + fn evidence(&self) -> &[Self::Evidence] { + self.0.evidence() + } + + fn validate_credential(&self, env: &E) -> ClaimsValidity + where + E: DateTimeProvider, + { + self.0.validate_credential(env) + } +} + +impl> ValidateClaims for SdJwtVc { + fn validate_claims(&self, environment: &E, proof: &P) -> ClaimsValidity { + self.0.validate_claims(environment, proof) + } +} + +impl ClaimSet for SdJwtVc {} +impl InfallibleClaimSet for SdJwtVc {} + +#[cfg(test)] +mod tests { + use serde_json::json; + use ssi_claims_core::VerificationParameters; + use ssi_jwk::JWK; + use ssi_sd_jwt::{json_pointer, SdAlg, SdJwt, SdJwtBuf}; + use ssi_vc::v2::JsonCredential; + + use crate::SdJwtVc; + + async fn verify(input: &SdJwt, key: &JWK) { + let vc = SdJwtVc::decode_reveal_any(input).unwrap(); + let params = VerificationParameters::from_resolver(key); + let result = vc.verify(params).await.unwrap(); + assert_eq!(result, Ok(())) + } + + #[async_std::test] + async fn sd_jwt_vc_roundtrip() { + let vc: JsonCredential = serde_json::from_value(json!({ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://www.w3.org/ns/credentials/examples/v2" + ], + "id": "http://university.example/credentials/1872", + "type": [ + "VerifiableCredential", + "ExampleAlumniCredential" + ], + "issuer": "https://university.example/issuers/565049", + "validFrom": "2010-01-01T19:23:24Z", + "credentialSchema": { + "id": "https://example.org/examples/degree.json", + "type": "JsonSchema" + }, + "credentialSubject": { + "id": "did:example:123", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts" + } + } + })) + .unwrap(); + + let key = JWK::generate_p256(); + let enveloped = SdJwtVc(vc) + .conceal_and_sign_into_enveloped( + SdAlg::Sha256, + &[json_pointer!("/credentialSubject/id")], + &key, + ) + .await + .unwrap(); + let jws = SdJwtBuf::new(enveloped.id.decoded_data().unwrap().into_owned()).unwrap(); + verify(&jws, &key).await + } +} diff --git a/crates/claims/crates/vc-jose-cose/src/sd_jwt/mod.rs b/crates/claims/crates/vc-jose-cose/src/sd_jwt/mod.rs new file mode 100644 index 000000000..7578da4b0 --- /dev/null +++ b/crates/claims/crates/vc-jose-cose/src/sd_jwt/mod.rs @@ -0,0 +1,5 @@ +mod credential; +pub use credential::*; + +mod presentation; +pub use presentation::*; diff --git a/crates/claims/crates/vc-jose-cose/src/sd_jwt/presentation.rs b/crates/claims/crates/vc-jose-cose/src/sd_jwt/presentation.rs new file mode 100644 index 000000000..6ffec2422 --- /dev/null +++ b/crates/claims/crates/vc-jose-cose/src/sd_jwt/presentation.rs @@ -0,0 +1,209 @@ +use std::borrow::Borrow; + +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use ssi_claims_core::{ClaimsValidity, SignatureError, ValidateClaims}; +use ssi_json_ld::{iref::Uri, syntax::Context}; +use ssi_jws::JwsSigner; +use ssi_jwt::{ClaimSet, InfallibleClaimSet, JWTClaims}; +use ssi_sd_jwt::{JsonPointer, RevealError, RevealedSdJwt, SdAlg, SdJwt, SdJwtBuf}; +use ssi_vc::{ + enveloped::EnvelopedVerifiableCredential, + v2::{syntax::JsonPresentation, Presentation, PresentationTypes}, + MaybeIdentified, +}; + +/// SD-JWT VP claims. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +#[serde(transparent)] +pub struct SdJwtVp>(pub T); + +impl SdJwtVp { + /// Returns this presentation as JWT claims. + /// + /// These are the claims that will be encoded in the SD-JWT. + pub fn as_jwt_claims(&self) -> JWTClaims<&Self> { + JWTClaims { + registered: Default::default(), + private: self, + } + } + + /// Turns this presentation into JWT claims. + /// + /// These claims can then be encoded in the SD-JWT. + pub fn into_jwt_claims(self) -> JWTClaims { + JWTClaims { + registered: Default::default(), + private: self, + } + } +} + +impl SdJwtVp { + /// Signs the presentation into an SD-JWT without any concealed claims. + /// + /// The generated SD-JWT will not have any disclosures. + /// + /// Use [`Self::conceal_and_sign`] to select the claims to be concealed. + pub async fn sign(&self, signer: &impl JwsSigner) -> Result { + let pointers: [&JsonPointer; 0] = []; + self.conceal_and_sign(SdAlg::Sha256, &pointers, signer) + .await + } + + /// Signs the presentation while concealing the claims selected by the given + /// JSON pointers. + /// + /// You can use [`Self::sign`] directly if you don't need to conceal + /// anything. + pub async fn conceal_and_sign( + &self, + sd_alg: SdAlg, + pointers: &[impl Borrow], + signer: &impl JwsSigner, + ) -> Result { + SdJwtBuf::conceal_and_sign(&self.as_jwt_claims(), sd_alg, pointers, signer).await + } + + /// Signs the presentation into an enveloped verifiable presentation (with + /// an SD-JWT identifier) without concealing any claim. + /// + /// The generated SD-JWT, encoded in the presentation identifier, will not + /// have any disclosures. + /// + /// Use [`Self::conceal_and_sign_into_enveloped`] to select the claims to be + /// concealed. + pub async fn sign_into_enveloped( + &self, + signer: &impl JwsSigner, + ) -> Result { + let pointers: [&JsonPointer; 0] = []; + self.conceal_and_sign_into_enveloped(SdAlg::Sha256, &pointers, signer) + .await + } + + /// Signs the presentation into an enveloped verifiable presentation (with + /// an SD-JWT identifier) while concealing the claims selected by the given + /// JSON pointers. + /// + /// The generated SD-JWT, encoded in the presentation identifier, will not + /// have any disclosures. + /// + /// Use [`Self::conceal_and_sign_into_enveloped`] to select the claims to be + /// concealed. + pub async fn conceal_and_sign_into_enveloped( + &self, + sd_alg: SdAlg, + pointers: &[impl Borrow], + signer: &impl JwsSigner, + ) -> Result { + let sd_jwt = self.conceal_and_sign(sd_alg, pointers, signer).await?; + Ok(EnvelopedVerifiableCredential { + context: Context::iri_ref(ssi_vc::v2::CREDENTIALS_V2_CONTEXT_IRI.to_owned().into()), + id: format!("data:application/vp-ld+sd-jwt,{sd_jwt}") + .parse() + .unwrap(), + }) + } +} + +impl SdJwtVp { + /// Decodes a SD-JWT VP, revealing its disclosed claims. + /// + /// This function requires the `T` parameter, representing the presentation + /// type, to be known. If you don't know what `T` you should use, use the + /// [`Self::decode_reveal_any`]. + pub fn decode_reveal(sd_jwt: &SdJwt) -> Result, RevealError> { + sd_jwt.decode_reveal() + } +} + +impl SdJwtVp { + /// Decodes a SD-JWT VP, revealing its disclosed claims. + /// + /// This function uses [`JsonPresentation`] + /// as presentation type. If you need to use a custom presentation type, use + /// the [`Self::decode_reveal`] function. + pub fn decode_reveal_any(sd_jwt: &SdJwt) -> Result, RevealError> { + sd_jwt.decode_reveal() + } +} + +impl MaybeIdentified for SdJwtVp { + fn id(&self) -> Option<&Uri> { + self.0.id() + } +} + +impl Presentation for SdJwtVp { + type Credential = T::Credential; + type Holder = T::Holder; + + fn id(&self) -> Option<&Uri> { + Presentation::id(&self.0) + } + + fn additional_types(&self) -> &[String] { + self.0.additional_types() + } + + fn types(&self) -> PresentationTypes { + self.0.types() + } + + fn verifiable_credentials(&self) -> &[Self::Credential] { + self.0.verifiable_credentials() + } + + fn holders(&self) -> &[Self::Holder] { + self.0.holders() + } +} + +impl> ValidateClaims for SdJwtVp { + fn validate_claims(&self, environment: &E, proof: &P) -> ClaimsValidity { + self.0.validate_claims(environment, proof) + } +} + +impl ClaimSet for SdJwtVp {} +impl InfallibleClaimSet for SdJwtVp {} + +#[cfg(test)] +mod tests { + use serde_json::json; + use ssi_claims_core::VerificationParameters; + use ssi_jwk::JWK; + use ssi_sd_jwt::{SdJwt, SdJwtBuf}; + use ssi_vc::{enveloped::EnvelopedVerifiableCredential, v2::syntax::JsonPresentation}; + + use crate::SdJwtVp; + + async fn verify(input: &SdJwt, key: &JWK) { + let vp = SdJwtVp::decode_reveal_any(input).unwrap(); + let params = VerificationParameters::from_resolver(key); + let result = vp.verify(params).await.unwrap(); + assert_eq!(result, Ok(())) + } + + #[async_std::test] + async fn sd_jwt_vp_roundtrip() { + let vp: JsonPresentation = serde_json::from_value(json!({ + "@context": [ + "https://www.w3.org/ns/credentials/v2", + "https://www.w3.org/ns/credentials/examples/v2" + ], + "type": "VerifiablePresentation", + "verifiableCredential": [{ + "@context": ["https://www.w3.org/ns/credentials/v2"], + "type": ["EnvelopedVerifiableCredential"], + "id": "data:application/vc-ld+jwt,eyJraWQiOiJFeEhrQk1XOWZtYmt2VjI2Nm1ScHVQMnNVWV9OX0VXSU4xbGFwVXpPOHJvIiwiYWxnIjoiRVMzODQifQ.eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvdjIiLCJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjIiXSwiaWQiOiJodHRwOi8vdW5pdmVyc2l0eS5leGFtcGxlL2NyZWRlbnRpYWxzLzE4NzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiRXhhbXBsZUFsdW1uaUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly91bml2ZXJzaXR5LmV4YW1wbGUvaXNzdWVycy81NjUwNDkiLCJ2YWxpZEZyb20iOiIyMDEwLTAxLTAxVDE5OjIzOjI0WiIsImNyZWRlbnRpYWxTY2hlbWEiOnsiaWQiOiJodHRwczovL2V4YW1wbGUub3JnL2V4YW1wbGVzL2RlZ3JlZS5qc29uIiwidHlwZSI6Ikpzb25TY2hlbWEifSwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZToxMjMiLCJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX19.d2k4O3FytQJf83kLh-HsXuPvh6yeOlhJELVo5TF71gu7elslQyOf2ZItAXrtbXF4Kz9WivNdztOayz4VUQ0Mwa8yCDZkP9B2pH-9S_tcAFxeoeJ6Z4XnFuL_DOfkR1fP" + }] + })).unwrap(); + + let key = JWK::generate_p256(); + let enveloped = SdJwtVp(vp).sign_into_enveloped(&key).await.unwrap(); + let jws = SdJwtBuf::new(enveloped.id.decoded_data().unwrap().into_owned()).unwrap(); + verify(&jws, &key).await + } +}