From a1c10346a1f5f6cf0b51fd447c284cdec390d256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Thu, 25 Jul 2024 15:28:30 +0200 Subject: [PATCH 1/3] Add JOSE VC/VP signature format. --- Cargo.toml | 3 +- crates/claims/Cargo.toml | 1 + crates/claims/crates/jws/src/signature.rs | 26 ++- crates/claims/crates/vc-jose-cose/Cargo.toml | 19 +++ .../vc-jose-cose/src/jose/credential.rs | 151 ++++++++++++++++++ .../crates/vc-jose-cose/src/jose/mod.rs | 17 ++ .../vc-jose-cose/src/jose/presentation.rs | 90 +++++++++++ crates/claims/crates/vc-jose-cose/src/lib.rs | 7 + crates/claims/crates/vc/Cargo.toml | 2 +- crates/claims/crates/vc/src/enveloped.rs | 29 ++++ crates/claims/crates/vc/src/lib.rs | 1 + crates/claims/src/lib.rs | 5 + 12 files changed, 332 insertions(+), 19 deletions(-) create mode 100644 crates/claims/crates/vc-jose-cose/Cargo.toml create mode 100644 crates/claims/crates/vc-jose-cose/src/jose/credential.rs create mode 100644 crates/claims/crates/vc-jose-cose/src/jose/mod.rs create mode 100644 crates/claims/crates/vc-jose-cose/src/jose/presentation.rs create mode 100644 crates/claims/crates/vc-jose-cose/src/lib.rs create mode 100644 crates/claims/crates/vc/src/enveloped.rs diff --git a/Cargo.toml b/Cargo.toml index a5b2e193c..43e05caf2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ ssi-jws = { path = "./crates/claims/crates/jws", version = "0.2", default-featur ssi-jwt = { path = "./crates/claims/crates/jwt", version = "0.2", default-features = false } ssi-sd-jwt = { path = "./crates/claims/crates/sd-jwt", version = "0.2", default-features = false } ssi-vc = { path = "./crates/claims/crates/vc", version = "0.3", default-features = false } +ssi-vc-jose-cose = { path = "./crates/claims/crates/vc-jose-cose", version = "0.1", default-features = false } ssi-data-integrity-core = { path = "./crates/claims/crates/data-integrity/core", version = "0.1", default-features = false } ssi-di-sd-primitives = { path = "./crates/claims/crates/data-integrity/sd-primitives", version = "0.1", default-features = false } ssi-data-integrity-suites = { path = "./crates/claims/crates/data-integrity/suites", version = "0.1", default-features = false } @@ -83,7 +84,7 @@ async-std = "1.9" async-trait = "0.1.68" thiserror = "1.0.40" chrono = "0.4.24" -iref = "3.1.2" +iref = "3.2.1" static-iref = "3.0" rdf-types = "0.22.3" xsd-types = "0.9.4" diff --git a/crates/claims/Cargo.toml b/crates/claims/Cargo.toml index 6e39c7660..cd4fccd53 100644 --- a/crates/claims/Cargo.toml +++ b/crates/claims/Cargo.toml @@ -69,6 +69,7 @@ ssi-jws.workspace = true ssi-jwt.workspace = true ssi-sd-jwt.workspace = true ssi-vc.workspace = true +ssi-vc-jose-cose.workspace = true ssi-data-integrity.workspace = true ssi-dids-core.workspace = true ssi-eip712 = { workspace = true, optional = true } diff --git a/crates/claims/crates/jws/src/signature.rs b/crates/claims/crates/jws/src/signature.rs index 9dad61a5a..5f7f5a83a 100644 --- a/crates/claims/crates/jws/src/signature.rs +++ b/crates/claims/crates/jws/src/signature.rs @@ -11,7 +11,14 @@ pub trait JWSPayload { /// JWS type. /// /// Value of the `typ` field in the JWS header. - fn typ(&self) -> Option<&'static str>; + fn typ(&self) -> Option<&str> { + None + } + + /// JWS cty header value. + fn cty(&self) -> Option<&str> { + None + } fn payload_bytes(&self) -> Cow<[u8]>; @@ -23,40 +30,24 @@ pub trait JWSPayload { } impl JWSPayload for [u8] { - fn typ(&self) -> Option<&'static str> { - None - } - fn payload_bytes(&self) -> Cow<[u8]> { Cow::Borrowed(self) } } impl JWSPayload for Vec { - fn typ(&self) -> Option<&'static str> { - None - } - fn payload_bytes(&self) -> Cow<[u8]> { Cow::Borrowed(self) } } impl JWSPayload for str { - fn typ(&self) -> Option<&'static str> { - None - } - fn payload_bytes(&self) -> Cow<[u8]> { Cow::Borrowed(self.as_bytes()) } } impl JWSPayload for String { - fn typ(&self) -> Option<&'static str> { - None - } - fn payload_bytes(&self) -> Cow<[u8]> { Cow::Borrowed(self.as_bytes()) } @@ -89,6 +80,7 @@ pub trait JWSSigner { let header = Header { algorithm: info.algorithm, key_id: info.key_id, + content_type: payload.cty().map(ToOwned::to_owned), type_: payload.typ().map(ToOwned::to_owned), ..Default::default() }; diff --git a/crates/claims/crates/vc-jose-cose/Cargo.toml b/crates/claims/crates/vc-jose-cose/Cargo.toml new file mode 100644 index 000000000..9719acc8e --- /dev/null +++ b/crates/claims/crates/vc-jose-cose/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ssi-vc-jose-cose" +version = "0.1.0" +edition = "2021" +authors = ["Spruce Systems, Inc."] +license = "Apache-2.0" +description = "Securing Verifiable Credentials using JOSE and COSE with the `ssi` library." +repository = "https://github.com/spruceid/ssi/" +documentation = "https://docs.rs/vc-jose-cose/" + +[dependencies] +ssi-claims-core.workspace = true +ssi-jws.workspace = true +ssi-vc.workspace = true +ssi-json-ld.workspace = true +xsd-types.workspace = true +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true \ No newline at end of file diff --git a/crates/claims/crates/vc-jose-cose/src/jose/credential.rs b/crates/claims/crates/vc-jose-cose/src/jose/credential.rs new file mode 100644 index 000000000..8b6588304 --- /dev/null +++ b/crates/claims/crates/vc-jose-cose/src/jose/credential.rs @@ -0,0 +1,151 @@ +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}; +use ssi_vc::{ + enveloped::EnvelopedVerifiableCredential, + v2::{Credential, CredentialTypes, JsonCredential}, + MaybeIdentified, +}; +use std::borrow::Cow; +use xsd_types::DateTimeStamp; + +/// Payload of a JWS-secured Verifiable Credential. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct JoseVc(pub T); + +impl JoseVc { + /// Sign a JOSE VC into an enveloped verifiable credential. + pub async fn sign_into_enveloped( + &self, + signer: &impl JWSSigner, + ) -> Result { + 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(), + }) + } +} + +impl JoseVc { + /// Decode a JOSE VC. + pub fn decode(jws: &CompactJWS) -> Result, JoseDecodeError> { + jws.to_decoded()? + .try_map(|payload| serde_json::from_slice(&payload).map(Self)) + .map_err(Into::into) + } +} + +impl JoseVc { + /// Decode a JOSE VC with an arbitrary credential type. + pub fn decode_any(jws: &CompactJWS) -> Result, JoseDecodeError> { + Self::decode(jws) + } +} + +impl JWSPayload for JoseVc { + fn typ(&self) -> Option<&str> { + Some("vc-ld+jwt") + } + + fn cty(&self) -> Option<&str> { + Some("vc") + } + + fn payload_bytes(&self) -> Cow<[u8]> { + Cow::Owned(serde_json::to_vec(&self.0).unwrap()) + } +} + +impl MaybeIdentified for JoseVc { + fn id(&self) -> Option<&Uri> { + self.0.id() + } +} + +impl Credential for JoseVc { + 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 JoseVc { + fn validate_claims(&self, environment: &E, proof: &P) -> ClaimsValidity { + self.0.validate_claims(environment, proof) + } +} diff --git a/crates/claims/crates/vc-jose-cose/src/jose/mod.rs b/crates/claims/crates/vc-jose-cose/src/jose/mod.rs new file mode 100644 index 000000000..1c61cf2d4 --- /dev/null +++ b/crates/claims/crates/vc-jose-cose/src/jose/mod.rs @@ -0,0 +1,17 @@ +mod credential; +pub use credential::*; + +mod presentation; +pub use presentation::*; + +/// Error that can occur when decoding a JOSE VC or VP. +#[derive(Debug, thiserror::Error)] +pub enum JoseDecodeError { + /// JWS error. + #[error(transparent)] + JWS(#[from] ssi_jws::DecodeError), + + /// JSON payload error. + #[error(transparent)] + JSON(#[from] serde_json::Error), +} diff --git a/crates/claims/crates/vc-jose-cose/src/jose/presentation.rs b/crates/claims/crates/vc-jose-cose/src/jose/presentation.rs new file mode 100644 index 000000000..9dbab0fc3 --- /dev/null +++ b/crates/claims/crates/vc-jose-cose/src/jose/presentation.rs @@ -0,0 +1,90 @@ +use super::JoseDecodeError; +use serde::{de::DeserializeOwned, Serialize}; +use ssi_claims_core::SignatureError; +use ssi_json_ld::{iref::Uri, syntax::Context}; +use ssi_jws::{CompactJWS, DecodedJWS, JWSPayload, JWSSigner}; +use ssi_vc::{ + enveloped::EnvelopedVerifiablePresentation, + v2::{syntax::JsonPresentation, Presentation, PresentationTypes}, + MaybeIdentified, +}; +use std::borrow::Cow; + +/// Payload of a JWS-secured Verifiable Presentation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct JoseVp(pub T); + +impl JWSPayload for JoseVp { + fn typ(&self) -> Option<&str> { + Some("vp-ld+jwt") + } + + fn cty(&self) -> Option<&str> { + Some("vp") + } + + fn payload_bytes(&self) -> Cow<[u8]> { + Cow::Owned(serde_json::to_vec(&self.0).unwrap()) + } +} + +impl JoseVp { + /// Sign a JOSE VC into an enveloped verifiable presentation. + pub async fn sign_into_enveloped( + &self, + signer: &impl JWSSigner, + ) -> Result { + 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(), + }) + } +} + +impl JoseVp { + /// Decode a JOSE VP. + pub fn decode(jws: &CompactJWS) -> Result, JoseDecodeError> { + jws.to_decoded()? + .try_map(|payload| serde_json::from_slice(&payload).map(Self)) + .map_err(Into::into) + } +} + +impl JoseVp { + /// Decode a JOSE VP with an arbitrary presentation type. + pub fn decode_any(jws: &CompactJWS) -> Result, JoseDecodeError> { + Self::decode(jws) + } +} + +impl MaybeIdentified for JoseVp { + fn id(&self) -> Option<&ssi_json_ld::iref::Uri> { + self.0.id() + } +} + +impl Presentation for JoseVp { + 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() + } +} diff --git a/crates/claims/crates/vc-jose-cose/src/lib.rs b/crates/claims/crates/vc-jose-cose/src/lib.rs new file mode 100644 index 000000000..14d7d86dd --- /dev/null +++ b/crates/claims/crates/vc-jose-cose/src/lib.rs @@ -0,0 +1,7 @@ +//! This library implements W3C's JOSE/COSE-based security formats for +//! Verifiable Credentials [1]. +//! +//! [1]: +mod jose; + +pub use jose::*; diff --git a/crates/claims/crates/vc/Cargo.toml b/crates/claims/crates/vc/Cargo.toml index 53d819cda..0b8327d9c 100644 --- a/crates/claims/crates/vc/Cargo.toml +++ b/crates/claims/crates/vc/Cargo.toml @@ -12,7 +12,7 @@ documentation = "https://docs.rs/ssi-claims/" eip712 = ["ssi-data-integrity/eip712"] [dependencies] -iref.workspace = true +iref = { workspace = true, features = ["data"] } rdf-types.workspace = true xsd-types.workspace = true static-iref.workspace = true diff --git a/crates/claims/crates/vc/src/enveloped.rs b/crates/claims/crates/vc/src/enveloped.rs new file mode 100644 index 000000000..57380b579 --- /dev/null +++ b/crates/claims/crates/vc/src/enveloped.rs @@ -0,0 +1,29 @@ +use iref::uri::data::DataUrlBuf; +use serde::{Deserialize, Serialize}; +use ssi_json_ld::syntax::Context; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub struct EnvelopedVerifiableCredential { + /// JSON-LD context object that defines at least `id` and `type`. + #[serde(rename = "@context")] + pub context: Context, + + /// Data URL that expresses a secured verifiable credential using an + /// enveloping security scheme such as [Securing Verifiable Credentials + /// using JOSE and COSE](https://www.w3.org/TR/vc-jose-cose/). + pub id: DataUrlBuf, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub struct EnvelopedVerifiablePresentation { + /// JSON-LD context object that defines at least `id` and `type`. + #[serde(rename = "@context")] + pub context: Context, + + /// Data URL that expresses a secured verifiable credential using an + /// enveloping security scheme such as [Securing Verifiable Credentials + /// using JOSE and COSE](https://www.w3.org/TR/vc-jose-cose/). + pub id: DataUrlBuf, +} diff --git a/crates/claims/crates/vc/src/lib.rs b/crates/claims/crates/vc/src/lib.rs index 4ac32f19f..e34332bbe 100644 --- a/crates/claims/crates/vc/src/lib.rs +++ b/crates/claims/crates/vc/src/lib.rs @@ -8,6 +8,7 @@ mod typed; pub use typed::*; pub mod datatype; +pub mod enveloped; pub mod syntax; pub mod v1; pub mod v2; diff --git a/crates/claims/src/lib.rs b/crates/claims/src/lib.rs index 8b0f6e326..23c206ccf 100644 --- a/crates/claims/src/lib.rs +++ b/crates/claims/src/lib.rs @@ -31,6 +31,11 @@ pub use ssi_sd_jwt as sd_jwt; /// See: pub use ssi_vc as vc; +/// Securing Verifiable Credentials using JOSE and COSE. +/// +/// See: +pub use ssi_vc_jose_cose as vc_jose_cose; + /// Data-Integrity Proofs. /// /// See: From 94ef9c28929b66f2ed6f2c017ba24c1dcb608a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Thu, 25 Jul 2024 17:04:51 +0200 Subject: [PATCH 2/3] Fix data url overflow issue in `iref`. --- Cargo.toml | 2 +- crates/claims/crates/jws/src/compact/bytes.rs | 10 ++- crates/claims/crates/vc-jose-cose/Cargo.toml | 7 +- .../vc-jose-cose/src/jose/credential.rs | 64 ++++++++++++++++++- .../vc-jose-cose/src/jose/presentation.rs | 63 ++++++++++++++++-- 5 files changed, 134 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 43e05caf2..d5d88880b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,7 +84,7 @@ async-std = "1.9" async-trait = "0.1.68" thiserror = "1.0.40" chrono = "0.4.24" -iref = "3.2.1" +iref = "3.2.2" static-iref = "3.0" rdf-types = "0.22.3" xsd-types = "0.9.4" diff --git a/crates/claims/crates/jws/src/compact/bytes.rs b/crates/claims/crates/jws/src/compact/bytes.rs index 73be9bfee..6ccb79884 100644 --- a/crates/claims/crates/jws/src/compact/bytes.rs +++ b/crates/claims/crates/jws/src/compact/bytes.rs @@ -9,9 +9,13 @@ use std::{borrow::Cow, ops::Deref}; pub struct CompactJWS([u8]); impl CompactJWS { - pub fn new(data: &[u8]) -> Result<&Self, InvalidCompactJWS<&[u8]>> { - if Self::check(data) { - Ok(unsafe { Self::new_unchecked(data) }) + pub fn new(data: &T) -> Result<&Self, InvalidCompactJWS<&T>> + where + T: ?Sized + AsRef<[u8]>, + { + let bytes = data.as_ref(); + if Self::check(bytes) { + Ok(unsafe { Self::new_unchecked(bytes) }) } else { Err(InvalidCompactJWS(data)) } diff --git a/crates/claims/crates/vc-jose-cose/Cargo.toml b/crates/claims/crates/vc-jose-cose/Cargo.toml index 9719acc8e..93bf9b29a 100644 --- a/crates/claims/crates/vc-jose-cose/Cargo.toml +++ b/crates/claims/crates/vc-jose-cose/Cargo.toml @@ -16,4 +16,9 @@ ssi-json-ld.workspace = true xsd-types.workspace = true serde.workspace = true serde_json.workspace = true -thiserror.workspace = true \ No newline at end of file +thiserror.workspace = true + +[dev-dependencies] +ssi-jws = { workspace = true, features = ["secp256r1"] } +ssi-jwk.workspace = true +async-std.workspace = true \ No newline at end of file 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 8b6588304..84d476a30 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}; +use ssi_jws::{CompactJWS, DecodedJWS, JWSPayload, JWSSigner, ValidateJWSHeader}; use ssi_vc::{ enveloped::EnvelopedVerifiableCredential, v2::{Credential, CredentialTypes, JsonCredential}, @@ -24,7 +24,7 @@ impl JoseVc { 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(), + id: format!("data:application/vc-ld+jwt,{jws}").parse().unwrap(), }) } } @@ -59,6 +59,14 @@ impl JWSPayload 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. + Ok(()) + } +} + impl MaybeIdentified for JoseVc { fn id(&self) -> Option<&Uri> { self.0.id() @@ -149,3 +157,55 @@ impl> ValidateClaims for JoseVc { self.0.validate_claims(environment, proof) } } + +#[cfg(test)] +mod tests { + use serde_json::json; + use ssi_claims_core::VerificationParameters; + use ssi_jwk::JWK; + use ssi_jws::{CompactJWS, CompactJWSBuf}; + use ssi_vc::v2::JsonCredential; + + use crate::JoseVc; + + async fn verify(input: &CompactJWS, key: &JWK) { + let vc = JoseVc::decode_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 jose_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 = JoseVc(vc).sign_into_enveloped(&key).await.unwrap(); + let jws = CompactJWSBuf::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 9dbab0fc3..76328a378 100644 --- a/crates/claims/crates/vc-jose-cose/src/jose/presentation.rs +++ b/crates/claims/crates/vc-jose-cose/src/jose/presentation.rs @@ -1,10 +1,10 @@ use super::JoseDecodeError; use serde::{de::DeserializeOwned, Serialize}; -use ssi_claims_core::SignatureError; +use ssi_claims_core::{ClaimsValidity, SignatureError, ValidateClaims}; use ssi_json_ld::{iref::Uri, syntax::Context}; -use ssi_jws::{CompactJWS, DecodedJWS, JWSPayload, JWSSigner}; +use ssi_jws::{CompactJWS, DecodedJWS, JWSPayload, JWSSigner, ValidateJWSHeader}; use ssi_vc::{ - enveloped::EnvelopedVerifiablePresentation, + enveloped::{EnvelopedVerifiableCredential, EnvelopedVerifiablePresentation}, v2::{syntax::JsonPresentation, Presentation, PresentationTypes}, MaybeIdentified, }; @@ -12,7 +12,7 @@ use std::borrow::Cow; /// Payload of a JWS-secured Verifiable Presentation. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct JoseVp(pub T); +pub struct JoseVp>(pub T); impl JWSPayload for JoseVp { fn typ(&self) -> Option<&str> { @@ -28,6 +28,14 @@ impl JWSPayload 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. + Ok(()) + } +} + impl JoseVp { /// Sign a JOSE VC into an enveloped verifiable presentation. pub async fn sign_into_enveloped( @@ -37,7 +45,7 @@ impl JoseVp { 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(), + id: format!("data:application/vp-ld+jwt,{jws}").parse().unwrap(), }) } } @@ -88,3 +96,48 @@ impl Presentation for JoseVp { self.0.holders() } } + +impl> ValidateClaims for JoseVp { + fn validate_claims(&self, environment: &E, proof: &P) -> ClaimsValidity { + self.0.validate_claims(environment, proof) + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + use ssi_claims_core::VerificationParameters; + use ssi_jwk::JWK; + use ssi_jws::{CompactJWS, CompactJWSBuf}; + use ssi_vc::{enveloped::EnvelopedVerifiableCredential, v2::syntax::JsonPresentation}; + + use crate::JoseVp; + + async fn verify(input: &CompactJWS, key: &JWK) { + let vp = JoseVp::decode_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 jose_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 = JoseVp(vp).sign_into_enveloped(&key).await.unwrap(); + let jws = CompactJWSBuf::new(enveloped.id.decoded_data().unwrap().into_owned()).unwrap(); + verify(&jws, &key).await + } +} From 9f083545027696c9f3fafb14f0b20f27f8f07ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Haudebourg?= Date: Thu, 25 Jul 2024 17:07:03 +0200 Subject: [PATCH 3/3] Trigger CI