diff --git a/bindings/web5_uniffi/src/lib.rs b/bindings/web5_uniffi/src/lib.rs index 4dde5fc0..c22f461f 100644 --- a/bindings/web5_uniffi/src/lib.rs +++ b/bindings/web5_uniffi/src/lib.rs @@ -1,7 +1,9 @@ use web5_uniffi_wrapper::{ credentials::{ presentation_definition::PresentationDefinition, - verifiable_credential_1_1::VerifiableCredential, + verifiable_credential_1_1::{ + data::VerifiableCredential as VerifiableCredentialData, VerifiableCredential, + }, }, crypto::{in_memory_key_manager::InMemoryKeyManager, key_manager::KeyManager}, dids::{ @@ -23,16 +25,10 @@ use web5_uniffi_wrapper::{ }; use web5::apid::{ - credentials::{ - presentation_definition::{ - Constraints as ConstraintsData, Field as FieldData, Filter as FilterData, - InputDescriptor as InputDescriptorData, Optionality, - PresentationDefinition as PresentationDefinitionData, - }, - verifiable_credential_1_1::{ - CredentialSubject as CredentialSubjectData, - VerifiableCredential as VerifiableCredentialData, - }, + credentials::presentation_definition::{ + Constraints as ConstraintsData, Field as FieldData, Filter as FilterData, + InputDescriptor as InputDescriptorData, Optionality, + PresentationDefinition as PresentationDefinitionData, }, crypto::jwk::Jwk as JwkData, dids::{ diff --git a/bindings/web5_uniffi/src/web5.udl b/bindings/web5_uniffi/src/web5.udl index b81b212d..e12f6188 100644 --- a/bindings/web5_uniffi/src/web5.udl +++ b/bindings/web5_uniffi/src/web5.udl @@ -208,29 +208,28 @@ interface BearerDid { Signer get_signer(string key_id); }; -dictionary CredentialSubjectData { - string id; - record? params; -}; - dictionary VerifiableCredentialData { sequence context; string id; sequence type; - string issuer; - string issuance_date; - string? expiration_date; - CredentialSubjectData credential_subject; + string json_serialized_issuer; + timestamp issuance_date; + timestamp? expiration_date; + string json_serialized_credential_subject; }; interface VerifiableCredential { + [Throws=RustCoreError] constructor(VerifiableCredentialData data); [Name=verify, Throws=RustCoreError] constructor([ByRef] string vcjwt); [Name=verify_with_verifier, Throws=RustCoreError] constructor([ByRef] string vcjwt, Verifier verifier); [Throws=RustCoreError] - string sign(Signer signer); + string sign(BearerDid bearer_did); + [Throws=RustCoreError] + string sign_with_signer([ByRef] string key_id, Signer signer); + [Throws=RustCoreError] VerifiableCredentialData get_data(); }; diff --git a/bindings/web5_uniffi_wrapper/Cargo.toml b/bindings/web5_uniffi_wrapper/Cargo.toml index ebb5daa9..b7f6f61c 100644 --- a/bindings/web5_uniffi_wrapper/Cargo.toml +++ b/bindings/web5_uniffi_wrapper/Cargo.toml @@ -7,5 +7,6 @@ repository.workspace = true license-file.workspace = true [dependencies] +serde_json = { workspace = true } thiserror = { workspace = true } web5 = { path = "../../crates/web5" } \ No newline at end of file diff --git a/bindings/web5_uniffi_wrapper/src/credentials/verifiable_credential_1_1.rs b/bindings/web5_uniffi_wrapper/src/credentials/verifiable_credential_1_1.rs index d4454642..5c374bd4 100644 --- a/bindings/web5_uniffi_wrapper/src/credentials/verifiable_credential_1_1.rs +++ b/bindings/web5_uniffi_wrapper/src/credentials/verifiable_credential_1_1.rs @@ -1,35 +1,111 @@ use crate::{ + dids::bearer_did::BearerDid, dsa::{Signer, Verifier}, - errors::Result, + errors::{Result, RustCoreError}, }; -use std::sync::Arc; +use std::sync::{Arc, RwLock}; use web5::apid::credentials::verifiable_credential_1_1::VerifiableCredential as InnerVerifiableCredential; -pub struct VerifiableCredential(pub InnerVerifiableCredential); +pub struct VerifiableCredential(pub Arc>); impl VerifiableCredential { - pub fn new(verifiable_credential: InnerVerifiableCredential) -> Self { - Self(verifiable_credential) + pub fn new(verifiable_credential: data::VerifiableCredential) -> Result { + let inner_verifiable_credential = verifiable_credential.to_inner()?; + + Ok(Self(Arc::new(RwLock::new(inner_verifiable_credential)))) } pub fn verify(vcjwt: &str) -> Result { - let vc = InnerVerifiableCredential::verify(vcjwt).map_err(|e| Arc::new(e.into()))?; - Ok(Self(vc)) + let inner_verifiable_credential = + InnerVerifiableCredential::verify(vcjwt).map_err(|e| Arc::new(e.into()))?; + + Ok(Self(Arc::new(RwLock::new(inner_verifiable_credential)))) } pub fn verify_with_verifier(vcjwt: &str, verifier: Arc) -> Result { - let vc = InnerVerifiableCredential::verify_with_verifier(vcjwt, verifier.to_inner()) - .map_err(|e| Arc::new(e.into()))?; - Ok(Self(vc)) + let inner_verifiable_credential = + InnerVerifiableCredential::verify_with_verifier(vcjwt, verifier.to_inner()) + .map_err(|e| Arc::new(e.into()))?; + + Ok(Self(Arc::new(RwLock::new(inner_verifiable_credential)))) + } + + pub fn sign(&self, bearer_did: Arc) -> Result { + let inner_verifiable_credential = self + .0 + .read() + .map_err(|e| RustCoreError::from_poison_error(e, "RwLockReadError"))?; + + inner_verifiable_credential + .sign(&bearer_did.0) + .map_err(|e| Arc::new(e.into())) } - pub fn sign(&self, signer: Arc) -> Result { - self.0 - .sign(signer.to_inner()) + pub fn sign_with_signer(&self, key_id: &str, signer: Arc) -> Result { + let inner_verifiable_credential = self + .0 + .read() + .map_err(|e| RustCoreError::from_poison_error(e, "RwLockReadError"))?; + + inner_verifiable_credential + .sign_with_signer(key_id, signer.to_inner()) .map_err(|e| Arc::new(e.into())) } - pub fn get_data(&self) -> InnerVerifiableCredential { - self.0.clone() + pub fn get_data(&self) -> Result { + let inner_verifiable_credential = self + .0 + .read() + .map_err(|e| RustCoreError::from_poison_error(e, "RwLockReadError"))?; + + data::VerifiableCredential::from_inner(inner_verifiable_credential.clone()) + } +} + +pub mod data { + use super::*; + use std::time::SystemTime; + + #[derive(Clone)] + pub struct VerifiableCredential { + pub context: Vec, + pub id: String, + pub r#type: Vec, + pub json_serialized_issuer: String, // JSON serialized + pub issuance_date: SystemTime, + pub expiration_date: Option, + pub json_serialized_credential_subject: String, // JSON serialized + } + + impl VerifiableCredential { + pub fn from_inner(inner_verifiable_credential: InnerVerifiableCredential) -> Result { + Ok(Self { + context: inner_verifiable_credential.context.clone(), + id: inner_verifiable_credential.id.clone(), + r#type: inner_verifiable_credential.r#type.clone(), + json_serialized_issuer: serde_json::to_string(&inner_verifiable_credential.issuer) + .map_err(|e| Arc::new(e.into()))?, + issuance_date: inner_verifiable_credential.issuance_date, + expiration_date: inner_verifiable_credential.expiration_date, + json_serialized_credential_subject: serde_json::to_string( + &inner_verifiable_credential.credential_subject, + ) + .map_err(|e| Arc::new(e.into()))?, + }) + } + + pub fn to_inner(&self) -> Result { + Ok(InnerVerifiableCredential { + context: self.context.clone(), + id: self.id.clone(), + r#type: self.r#type.clone(), + issuer: serde_json::from_str(&self.json_serialized_issuer) + .map_err(|e| Arc::new(e.into()))?, + issuance_date: self.issuance_date, + expiration_date: self.expiration_date, + credential_subject: serde_json::from_str(&self.json_serialized_credential_subject) + .map_err(|e| Arc::new(e.into()))?, + }) + } } } diff --git a/bindings/web5_uniffi_wrapper/src/errors.rs b/bindings/web5_uniffi_wrapper/src/errors.rs index 57534e83..286d6b4d 100644 --- a/bindings/web5_uniffi_wrapper/src/errors.rs +++ b/bindings/web5_uniffi_wrapper/src/errors.rs @@ -1,4 +1,5 @@ -use std::sync::Arc; +use serde_json::Error as SerdeJsonError; +use std::sync::{Arc, PoisonError}; use std::{any::type_name, fmt::Debug}; use thiserror::Error; use web5::apid::credentials::presentation_definition::PexError; @@ -21,6 +22,14 @@ pub enum RustCoreError { } impl RustCoreError { + pub fn from_poison_error(error: PoisonError, error_type: &str) -> Arc { + Arc::new(RustCoreError::Error { + r#type: error_type.to_string(), + variant: "PoisonError".to_string(), + message: error.to_string(), + }) + } + fn new(error: T) -> Self where T: std::error::Error + 'static, @@ -123,4 +132,10 @@ impl From for RustCoreError { } } +impl From for RustCoreError { + fn from(error: SerdeJsonError) -> Self { + RustCoreError::new(error) + } +} + pub type Result = std::result::Result>; diff --git a/crates/web5/Cargo.toml b/crates/web5/Cargo.toml index 0705d6a0..2222a58f 100644 --- a/crates/web5/Cargo.toml +++ b/crates/web5/Cargo.toml @@ -15,6 +15,7 @@ did-web = "0.2.2" ed25519-dalek = { version = "2.1.1", features = ["rand_core"] } getrandom = { version = "0.2.12", features = ["js"] } hex = "0.4" +josekit = "0.8.6" jsonpath-rust = "0.5.1" jsonschema = { version = "0.18.0", default-features = false } k256 = { version = "0.13.3", features = ["ecdsa", "jwk"] } diff --git a/crates/web5/src/apid/credentials/mod.rs b/crates/web5/src/apid/credentials/mod.rs index 84b0e442..eaa26cc6 100644 --- a/crates/web5/src/apid/credentials/mod.rs +++ b/crates/web5/src/apid/credentials/mod.rs @@ -1,3 +1,13 @@ +use std::time::SystemTimeError; + +use josekit::JoseError as JosekitError; +use serde_json::Error as SerdeJsonError; + +use super::dids::{ + bearer_did::BearerDidError, data_model::DataModelError, did::DidError, + resolution::resolution_metadata::ResolutionMetadataError, +}; + pub mod presentation_definition; pub mod verifiable_credential_1_1; @@ -15,6 +25,28 @@ pub enum CredentialError { VcDataModelValidationError(String), #[error("invalid timestamp: {0}")] InvalidTimestamp(String), + #[error("serde json error {0}")] + SerdeJsonError(String), + #[error(transparent)] + Jose(#[from] JosekitError), + #[error(transparent)] + BearerDid(#[from] BearerDidError), + #[error("missing kid jose header")] + MissingKid, + #[error(transparent)] + Resolution(#[from] ResolutionMetadataError), + #[error(transparent)] + DidDataModel(#[from] DataModelError), + #[error(transparent)] + Did(#[from] DidError), + #[error(transparent)] + SystemTime(#[from] SystemTimeError), +} + +impl From for CredentialError { + fn from(err: SerdeJsonError) -> Self { + CredentialError::SerdeJsonError(err.to_string()) + } } type Result = std::result::Result; diff --git a/crates/web5/src/apid/credentials/verifiable_credential_1_1.rs b/crates/web5/src/apid/credentials/verifiable_credential_1_1.rs index a313fb61..8c541ff0 100644 --- a/crates/web5/src/apid/credentials/verifiable_credential_1_1.rs +++ b/crates/web5/src/apid/credentials/verifiable_credential_1_1.rs @@ -1,11 +1,31 @@ -use super::Result; -use crate::apid::dsa::{Signer, Verifier}; +use super::{CredentialError, Result}; +use crate::apid::{ + dids::{ + bearer_did::BearerDid, + did::Did, + resolution::{ + resolution_metadata::ResolutionMetadataError, resolution_result::ResolutionResult, + }, + }, + dsa::{ed25519::Ed25519Verifier, DsaError, Signer, Verifier}, +}; +use chrono::{DateTime, Utc}; use core::fmt; -use serde::{Deserialize, Serialize}; +use josekit::{ + jws::{ + alg::eddsa::EddsaJwsAlgorithm as JosekitEddsaJwsAlgorithm, + JwsAlgorithm as JosekitJwsAlgorithm, JwsHeader, JwsSigner as JosekitJwsSigner, + JwsVerifier as JosekitJwsVerifier, + }, + jwt::JwtPayload, + JoseError as JosekitError, +}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::{ collections::HashMap, fmt::{Display, Formatter}, sync::Arc, + time::{SystemTime, UNIX_EPOCH}, }; const BASE_CONTEXT: &str = "https://www.w3.org/2018/credentials/v1"; @@ -42,21 +62,77 @@ impl Display for Issuer { } } -#[derive(Serialize, Deserialize, Debug, Clone, Default)] +fn serialize_system_time( + time: &SystemTime, + serializer: S, +) -> std::result::Result +where + S: Serializer, +{ + let datetime: chrono::DateTime = (*time).into(); + let s = datetime.to_rfc3339(); + serializer.serialize_str(&s) +} + +fn deserialize_system_time<'de, D>(deserializer: D) -> std::result::Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + let datetime = chrono::DateTime::parse_from_rfc3339(&s).map_err(serde::de::Error::custom)?; + Ok(datetime.with_timezone(&Utc).into()) +} + +fn serialize_option_system_time( + time: &Option, + serializer: S, +) -> std::result::Result +where + S: Serializer, +{ + match time { + Some(time) => serialize_system_time(time, serializer), + None => serializer.serialize_none(), + } +} + +fn deserialize_option_system_time<'de, D>( + deserializer: D, +) -> std::result::Result, D::Error> +where + D: Deserializer<'de>, +{ + let opt = Option::::deserialize(deserializer)?; + match opt { + Some(s) => { + let datetime = DateTime::parse_from_rfc3339(&s).map_err(serde::de::Error::custom)?; + Ok(Some(datetime.with_timezone(&Utc).into())) + } + None => Ok(None), + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct VerifiableCredential { #[serde(rename = "@context")] pub context: Vec, pub id: String, #[serde(rename = "type")] pub r#type: Vec, - - // 🚧 UDL support - // pub issuer: Issuer, - pub issuer: String, - #[serde(rename = "issuanceDate")] - pub issuance_date: String, - #[serde(rename = "expirationDate")] - pub expiration_date: Option, + pub issuer: Issuer, + #[serde( + rename = "issuanceDate", + serialize_with = "serialize_system_time", + deserialize_with = "deserialize_system_time" + )] + pub issuance_date: SystemTime, + #[serde( + rename = "expirationDate", + serialize_with = "serialize_option_system_time", + deserialize_with = "deserialize_option_system_time" + )] + pub expiration_date: Option, + #[serde(rename = "credentialSubject")] pub credential_subject: CredentialSubject, } @@ -67,16 +143,40 @@ pub struct CredentialSubject { pub params: Option>, } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct JwtPayloadVerifiableCredential { + #[serde(rename = "@context")] + context: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + id: Option, + #[serde(rename = "type")] + r#type: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + issuer: Option, + #[serde( + rename = "issuanceDate", + serialize_with = "serialize_option_system_time", + deserialize_with = "deserialize_option_system_time" + )] + issuance_date: Option, + #[serde( + rename = "expirationDate", + serialize_with = "serialize_option_system_time", + deserialize_with = "deserialize_option_system_time" + )] + expiration_date: Option, + #[serde(skip_serializing_if = "Option::is_none", rename = "credentialSubject")] + credential_subject: Option, +} + impl VerifiableCredential { pub fn new( id: String, context: Vec, r#type: Vec, - // 🚧 UDL - // issuer: Issuer, - issuer: String, - issuance_date: String, - expiration_date: Option, + issuer: Issuer, + issuance_date: SystemTime, + expiration_date: Option, credential_subject: CredentialSubject, ) -> Self { let context_with_base = std::iter::once(BASE_CONTEXT.to_string()) @@ -98,26 +198,353 @@ impl VerifiableCredential { } } - pub fn sign(&self, _signer: Arc) -> Result { - println!("VerifiableCredential.sign() called"); - Ok(String::default()) + pub fn sign(&self, bearer_did: &BearerDid) -> Result { + // default to first VM + let key_id = bearer_did.document.verification_method[0].id.clone(); + let signer = bearer_did.get_signer(key_id.clone())?; + + self.sign_with_signer(&key_id, signer) + } + + pub fn sign_with_signer(&self, key_id: &str, signer: Arc) -> Result { + let mut payload = JwtPayload::new(); + let vc_claim = JwtPayloadVerifiableCredential { + context: self.context.clone(), + id: Some(self.id.clone()), + r#type: self.r#type.clone(), + issuer: Some(self.issuer.clone()), + issuance_date: Some(self.issuance_date), + expiration_date: self.expiration_date, + credential_subject: Some(self.credential_subject.clone()), + }; + payload.set_claim("vc", Some(serde_json::to_value(vc_claim)?))?; + payload.set_issuer(&self.issuer.to_string()); + payload.set_jwt_id(&self.id); + payload.set_subject(&self.credential_subject.id); + payload.set_not_before(&self.issuance_date); + payload.set_issued_at(&SystemTime::now()); + if let Some(exp) = &self.expiration_date { + payload.set_expires_at(exp) + } + + let jose_signer = JoseSigner { + kid: key_id.to_string(), + signer, + }; + + let mut header = JwsHeader::new(); + header.set_token_type("JWT"); + let vc_jwt = josekit::jwt::encode_with_signer(&payload, &header, &jose_signer)?; + + Ok(vc_jwt) + } + + pub fn verify(vc_jwt: &str) -> Result { + // this function currently only supports Ed25519 + let header = josekit::jwt::decode_header(vc_jwt)?; + + let kid = header + .claim("kid") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| JosekitError::InvalidJwtFormat(CredentialError::MissingKid.into()))? + .to_string(); + + let did = Did::new(&kid)?; + + let resolution_result = ResolutionResult::new(&did.uri); + if let Some(err) = resolution_result.resolution_metadata.error.clone() { + return Err(CredentialError::Resolution(err)); + } + + let public_key_jwk = resolution_result + .document + .ok_or_else(|| { + JosekitError::InvalidJwtFormat(ResolutionMetadataError::InternalError.into()) + })? + .find_public_key_jwk(kid.to_string())?; + + let verifier = Ed25519Verifier::new(public_key_jwk); + + Self::verify_with_verifier(vc_jwt, Arc::new(verifier)) + } + + pub fn verify_with_verifier(vc_jwt: &str, verifier: Arc) -> Result { + let header = josekit::jwt::decode_header(vc_jwt)?; + + let kid = header + .claim("kid") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| JosekitError::InvalidJwtFormat(CredentialError::MissingKid.into()))? + .to_string(); + + let jose_verifier = &JoseVerifier { kid, verifier }; + + let (jwt_payload, _) = josekit::jwt::decode_with_verifier(vc_jwt, jose_verifier)?; + + let vc_claim = jwt_payload + .claim("vc") + .ok_or(CredentialError::MissingClaim("vc".to_string()))?; + let vc_payload = + serde_json::from_value::(vc_claim.clone())?; + + // registered claims checks + let jti = jwt_payload + .jwt_id() + .ok_or(CredentialError::MissingClaim("jti".to_string()))?; + let iss = jwt_payload + .issuer() + .ok_or(CredentialError::MissingClaim("issuer".to_string()))?; + let sub = jwt_payload + .subject() + .ok_or(CredentialError::MissingClaim("subject".to_string()))?; + let nbf = jwt_payload + .not_before() + .ok_or(CredentialError::MissingClaim("not_before".to_string()))?; + let exp = jwt_payload.expires_at(); + + if let Some(id) = &vc_payload.id { + if id != jti { + return Err(CredentialError::ClaimMismatch("id".to_string())); + } + } + + if let Some(issuer) = &vc_payload.issuer { + let vc_issuer = issuer.to_string(); + if iss != vc_issuer { + return Err(CredentialError::ClaimMismatch("issuer".to_string())); + } + } + + if let Some(credential_subject) = &vc_payload.credential_subject { + if sub != credential_subject.id { + return Err(CredentialError::ClaimMismatch("subject".to_string())); + } + } + + let now = SystemTime::now(); + match vc_payload.expiration_date { + Some(ref vc_payload_expiration_date) => match exp { + None => { + return Err(CredentialError::MisconfiguredExpirationDate( + "VC has expiration date but no exp in registered claims".to_string(), + )); + } + Some(exp) => { + if vc_payload_expiration_date + .duration_since(UNIX_EPOCH)? + .as_secs() + != exp.duration_since(UNIX_EPOCH)?.as_secs() + { + return Err(CredentialError::ClaimMismatch( + "expiration_date".to_string(), + )); + } + + if now > exp { + return Err(CredentialError::CredentialExpired); + } + } + }, + None => { + if let Some(exp) = exp { + if now > exp { + return Err(CredentialError::CredentialExpired); + } + } + } + } + + let vc_issuer = vc_payload.issuer.unwrap_or(Issuer::String(iss.to_string())); + + let vc_credential_subject = vc_payload.credential_subject.unwrap_or(CredentialSubject { + id: sub.to_string(), + params: None, + }); + + let vc = VerifiableCredential { + context: vc_payload.context, + id: jti.to_string(), + r#type: vc_payload.r#type, + issuer: vc_issuer, + issuance_date: nbf, + expiration_date: exp, + credential_subject: vc_credential_subject, + }; + + validate_vc_data_model(&vc)?; + + Ok(vc) + } +} + +fn validate_vc_data_model(vc: &VerifiableCredential) -> Result<()> { + // Required fields ["@context", "id", "type", "issuer", "issuanceDate", "credentialSubject"] + if vc.id.is_empty() { + return Err(CredentialError::VcDataModelValidationError( + "missing id".to_string(), + )); + } + + if vc.context.is_empty() || vc.context[0] != BASE_CONTEXT { + return Err(CredentialError::VcDataModelValidationError( + "missing context".to_string(), + )); + } + + if vc.r#type.is_empty() || vc.r#type[0] != BASE_TYPE { + return Err(CredentialError::VcDataModelValidationError( + "missing type".to_string(), + )); + } + + if vc.issuer.to_string().is_empty() { + return Err(CredentialError::VcDataModelValidationError( + "missing issuer".to_string(), + )); + } + + if vc.credential_subject.id.is_empty() { + return Err(CredentialError::VcDataModelValidationError( + "missing credential subject".to_string(), + )); + } + + let now = SystemTime::now(); + if vc.issuance_date > now { + return Err(CredentialError::VcDataModelValidationError( + "issuance date in future".to_string(), + )); + } + + // Validate expiration date if it exists + if let Some(expiration_date) = &vc.expiration_date { + if expiration_date < &now { + return Err(CredentialError::VcDataModelValidationError( + "credential expired".to_string(), + )); + } + } + + // TODO: Add validations to credential_status, credential_schema, and evidence once they are added to the VcDataModel + // https://github.com/TBD54566975/web5-rs/issues/112 + + Ok(()) +} + +#[derive(Clone)] +pub struct JoseSigner { + pub kid: String, + pub signer: Arc, +} + +impl JosekitJwsSigner for JoseSigner { + fn algorithm(&self) -> &dyn JosekitJwsAlgorithm { + &JosekitEddsaJwsAlgorithm::Eddsa + } + + fn key_id(&self) -> Option<&str> { + Some(&self.kid) + } + + fn signature_len(&self) -> usize { + 64 + } + + fn sign(&self, message: &[u8]) -> core::result::Result, JosekitError> { + self.signer + .sign(message) + // 🚧 improve error message semantics + .map_err(|err| JosekitError::InvalidSignature(err.into())) + } + + fn box_clone(&self) -> Box { + Box::new(self.clone()) + } +} + +impl core::fmt::Debug for JoseSigner { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Signer").field("kid", &self.kid).finish() + } +} + +#[derive(Clone)] +struct JoseVerifier { + pub kid: String, + pub verifier: Arc, +} + +impl JosekitJwsVerifier for JoseVerifier { + fn algorithm(&self) -> &dyn JosekitJwsAlgorithm { + &JosekitEddsaJwsAlgorithm::Eddsa + } + + fn key_id(&self) -> Option<&str> { + Some(self.kid.as_str()) + } + + fn verify(&self, message: &[u8], signature: &[u8]) -> core::result::Result<(), JosekitError> { + let result = self + .verifier + .verify(message, signature) + .map_err(|e| JosekitError::InvalidSignature(e.into()))?; + + match result { + true => Ok(()), + false => Err(JosekitError::InvalidSignature( + // 🚧 improve error message semantics + DsaError::VerificationFailure("ed25519 verification failed".to_string()).into(), + )), + } + } + + fn box_clone(&self) -> Box { + Box::new(self.clone()) } +} - pub fn verify(vcjwt: &str) -> Result { - // 🚧 call VerifiableCredential::verify_with_verifier with Ed25519Verifier - println!("VerifiableCredential::verify() called with {}", vcjwt); - Ok(Self { - ..Default::default() - }) +impl core::fmt::Debug for JoseVerifier { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Verifier").field("kid", &self.kid).finish() } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::apid::{ + crypto::key_managers::in_memory_key_manager::InMemoryKeyManager, + dids::methods::did_jwk::DidJwk, dsa::ed25519::Ed25519Generator, + }; + use std::time::Duration; + use uuid::Uuid; - pub fn verify_with_verifier(vcjwt: &str, _verifier: Arc) -> Result { - println!( - "VerifiableCredential::verify_with_verifier() called with {}", - vcjwt + #[test] + fn can_create_sign_and_verify() { + let key_manager = InMemoryKeyManager::new(); + let public_jwk = key_manager + .import_private_jwk(Ed25519Generator::generate()) + .unwrap(); + let did_jwk = DidJwk::from_public_jwk(public_jwk).unwrap(); + let bearer_did = BearerDid::new(&did_jwk.did.uri, Arc::new(key_manager)).unwrap(); + + let now = SystemTime::now(); + let vc = VerifiableCredential::new( + format!("urn:vc:uuid:{0}", Uuid::new_v4().to_string()), + vec![BASE_CONTEXT.to_string()], + vec![BASE_TYPE.to_string()], + Issuer::String(bearer_did.did.uri.clone()), + now, + Some(now + Duration::from_secs(20 * 365 * 24 * 60 * 60)), // now + 20 years + CredentialSubject { + id: bearer_did.did.uri.clone(), + ..Default::default() + }, ); - Ok(Self { - ..Default::default() - }) + + let vc_jwt = vc.sign(&bearer_did).unwrap(); + assert_ne!(String::default(), vc_jwt); + + VerifiableCredential::verify(&vc_jwt).unwrap(); } } diff --git a/crates/web5/src/apid/dids/data_model/document.rs b/crates/web5/src/apid/dids/data_model/document.rs index b3b75f64..024090ea 100644 --- a/crates/web5/src/apid/dids/data_model/document.rs +++ b/crates/web5/src/apid/dids/data_model/document.rs @@ -40,6 +40,6 @@ impl Document { return Ok(vm.public_key_jwk.clone()); } } - Err(super::DataModelError::PublicKeyJwk(key_id)) + Err(super::DataModelError::MissingPublicKeyJwk(key_id)) } } diff --git a/crates/web5/src/apid/dids/data_model/mod.rs b/crates/web5/src/apid/dids/data_model/mod.rs index 5a085146..73864229 100644 --- a/crates/web5/src/apid/dids/data_model/mod.rs +++ b/crates/web5/src/apid/dids/data_model/mod.rs @@ -5,7 +5,7 @@ pub mod verification_method; #[derive(thiserror::Error, Debug, Clone, PartialEq)] pub enum DataModelError { #[error("publicKeyJwk not found {0}")] - PublicKeyJwk(String), + MissingPublicKeyJwk(String), } type Result = std::result::Result;