diff --git a/identity_credential/Cargo.toml b/identity_credential/Cargo.toml index 7372290a8..156230562 100644 --- a/identity_credential/Cargo.toml +++ b/identity_credential/Cargo.toml @@ -15,7 +15,7 @@ anyhow = { version = "1" } async-trait = { version = "0.1.64", default-features = false } bls12_381_plus = { workspace = true, optional = true } flate2 = { version = "1.0.28", default-features = false, features = ["rust_backend"], optional = true } -futures = { version = "0.3", default-features = false, optional = true } +futures = { version = "0.3", default-features = false, features = ["alloc"], optional = true } identity_core = { version = "=1.4.0", path = "../identity_core", default-features = false } identity_did = { version = "=1.4.0", path = "../identity_did", default-features = false } identity_document = { version = "=1.4.0", path = "../identity_document", default-features = false } diff --git a/identity_credential/src/credential/jwt_serialization.rs b/identity_credential/src/credential/jwt_serialization.rs index a92eb78ce..feb3de531 100644 --- a/identity_credential/src/credential/jwt_serialization.rs +++ b/identity_credential/src/credential/jwt_serialization.rs @@ -67,7 +67,7 @@ impl<'credential, T> CredentialJwtClaims<'credential, T> where T: ToOwned + Serialize + DeserializeOwned, { - pub(super) fn new(credential: &'credential Credential, custom: Option) -> Result { + pub(crate) fn new(credential: &'credential Credential, custom: Option) -> Result { let Credential { context, id, diff --git a/identity_credential/src/error.rs b/identity_credential/src/error.rs index 1c814c389..a5bbc1f86 100644 --- a/identity_credential/src/error.rs +++ b/identity_credential/src/error.rs @@ -3,6 +3,8 @@ //! Errors that may occur when working with Verifiable Credentials. +use crate::sd_jwt_vc; + /// Alias for a `Result` with the error type [`Error`]. pub type Result = ::core::result::Result; @@ -79,4 +81,9 @@ pub enum Error { /// Cause by an invalid attribute path #[error("Attribute Not found")] SelectiveDisclosureError, + + /// Failure of an SD-JWT VC operation. + #[cfg(feature = "sd-jwt-vc")] + #[error(transparent)] + SdJwtVc(#[from] sd_jwt_vc::Error), } diff --git a/identity_credential/src/sd_jwt_vc/builder.rs b/identity_credential/src/sd_jwt_vc/builder.rs index df7daafa3..8bef4059c 100644 --- a/identity_credential/src/sd_jwt_vc/builder.rs +++ b/identity_credential/src/sd_jwt_vc/builder.rs @@ -7,6 +7,7 @@ use std::sync::LazyLock; use identity_core::common::StringOrUrl; use identity_core::common::Timestamp; use identity_core::common::Url; +use identity_core::convert::ToJson; use sd_jwt_payload_rework::Hasher; use sd_jwt_payload_rework::JsonObject; use sd_jwt_payload_rework::JwsSigner; @@ -15,6 +16,10 @@ use sd_jwt_payload_rework::SdJwtBuilder; use sd_jwt_payload_rework::Sha256Hasher; use serde::Serialize; use serde_json::json; +use serde_json::Value; + +use crate::credential::Credential; +use crate::credential::CredentialJwtClaims; use super::Error; use super::Result; @@ -101,6 +106,25 @@ impl SdJwtVcBuilder { }) } + /// Creates a new [`SdJwtVcBuilder`] starting from a [`Credential`] that is converted to a JWT claim set. + pub fn new_from_credential(credential: Credential, hasher: H) -> std::result::Result { + let mut vc_jwt_claims = CredentialJwtClaims::new(&credential, None)? + .to_json_value() + .map_err(|e| crate::Error::JwtClaimsSetSerializationError(Box::new(e)))?; + // When converting a VC to its JWT claims representation, some VC specific claims are putted into a `vc` object + // property. Flatten out `vc`, keeping the other JWT claims intact. + { + let claims = vc_jwt_claims.as_object_mut().expect("serialized VC is a JSON object"); + let Value::Object(vc_properties) = claims.remove("vc").expect("serialized VC has `vc` property") else { + unreachable!("`vc` property's value is a JSON object"); + }; + for (key, value) in vc_properties { + claims.insert(key, value); + } + } + Ok(Self::new_with_hasher(vc_jwt_claims, hasher)?) + } + /// Substitutes a value with the digest of its disclosure. /// /// ## Notes @@ -244,7 +268,10 @@ impl SdJwtVcBuilder { #[cfg(test)] mod tests { + use super::*; + use crate::credential::CredentialBuilder; + use crate::credential::Subject; use crate::sd_jwt_vc::tests::TestSigner; #[tokio::test] @@ -327,4 +354,33 @@ mod tests { Ok(()) } + + #[tokio::test] + async fn building_sd_jwt_vc_from_credential_works() -> anyhow::Result<()> { + let credential = CredentialBuilder::default() + .id(Url::parse("https://example.com/credentials/42")?) + .issuance_date(Timestamp::now_utc()) + .issuer(Url::parse("https://example.com/issuers/42")?) + .subject(Subject::with_id(Url::parse("https://example.com/subjects/42")?)) + .build()?; + + let sd_jwt_vc = SdJwtVcBuilder::new_from_credential(credential.clone(), Sha256Hasher::default())? + .vct(Url::parse("https://example.com/types/0")?) + .finish(&TestSigner, "HS256") + .await?; + + assert_eq!(sd_jwt_vc.claims().nbf.as_ref().unwrap(), &credential.issuance_date); + assert_eq!(&sd_jwt_vc.claims().iss, credential.issuer.url()); + assert_eq!( + sd_jwt_vc.claims().sub.as_ref().unwrap().as_url(), + credential.credential_subject.first().unwrap().id.as_ref() + ); + assert_eq!( + sd_jwt_vc.claims().get("jti"), + Some(&json!(credential.id.as_ref().unwrap())) + ); + assert_eq!(sd_jwt_vc.claims().get("type"), Some(&json!("VerifiableCredential"))); + + Ok(()) + } } diff --git a/identity_credential/src/sd_jwt_vc/status.rs b/identity_credential/src/sd_jwt_vc/status.rs index 173844729..1c68db6d4 100644 --- a/identity_credential/src/sd_jwt_vc/status.rs +++ b/identity_credential/src/sd_jwt_vc/status.rs @@ -16,6 +16,9 @@ pub enum StatusMechanism { /// Reference to a status list containing this token's status. #[serde(rename = "status_list")] StatusList(StatusListRef), + /// A non-standard status mechanism. + #[serde(untagged)] + Custom(serde_json::Value), } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]