diff --git a/did-jwk/Cargo.toml b/did-jwk/Cargo.toml index 7869eab1c..22f2af309 100644 --- a/did-jwk/Cargo.toml +++ b/did-jwk/Cargo.toml @@ -18,14 +18,16 @@ secp256r1 = ["ssi-jwk/secp256r1"] ed25519 = ["ssi-jwk/ed25519"] [dependencies] -ssi-dids = { path = "../ssi-dids", version = "0.1" } -ssi-jwk = { path = "../ssi-jwk", version = "0.1", default-features = false } -async-trait = "0.1" +ssi-crypto.workspace = true +ssi-dids.workspace = true +ssi-jwk.workspace = true +async-trait.workspace = true multibase = "0.8" -serde_json = "1.0" +serde_json.workspace = true serde_jcs = "0.1" -iref = "2.2" -static-iref = "2.0" +iref.workspace = true +static-iref.workspace = true +thiserror.workspace = true [dev-dependencies] async-std = { version = "1.9", features = ["attributes"] } diff --git a/did-jwk/src/lib.rs b/did-jwk/src/lib.rs index 433eb25cd..2aee87e27 100644 --- a/did-jwk/src/lib.rs +++ b/did-jwk/src/lib.rs @@ -1,174 +1,252 @@ -use async_trait::async_trait; -use static_iref::iref; +use std::borrow::Cow; +use async_trait::async_trait; +use ssi_crypto::ProofPurposes; use ssi_dids::{ - did_resolve::{ - DIDResolver, DocumentMetadata, ResolutionInputMetadata, ResolutionMetadata, - ERROR_INVALID_DID, ERROR_NOT_FOUND, + document::{ + self, + representation::{self, MediaType}, + verification_method::VerificationMethod, + AnyVerificationMethod, VerificationRelationships, }, - Context, Contexts, DIDMethod, Document, Source, VerificationMethod, VerificationMethodMap, - DEFAULT_CONTEXT, DIDURL, + resolution::{DIDMethodResolver, Error, Metadata, Options, Output}, + DIDBuf, DIDURLBuf, Document, RelativeDIDURLBuf, DID, DIDURL, }; use ssi_jwk::JWK; -pub struct DIDJWK; +pub const JSON_WEB_KEY_2020_TYPE: &str = "JsonWebKey2020"; -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl DIDResolver for DIDJWK { - async fn resolve( - &self, - did: &str, - _input_metadata: &ResolutionInputMetadata, - ) -> ( - ResolutionMetadata, - Option, - Option, - ) { - if !did.starts_with("did:jwk:") { - return ( - ResolutionMetadata { - error: Some(ERROR_INVALID_DID.to_string()), - content_type: None, - property_set: None, - }, - None, - None, - ); +/// `JsonWebKey2020` verification method description. +pub struct JsonWebKey2020 { + /// Verification method identifier. + pub id: DIDURLBuf, + + // Key controller. + pub controller: DIDBuf, + + /// Public key (`publicKeyJwk`). + pub public_key: JWK, +} + +impl JsonWebKey2020 { + pub fn new(id: DIDURLBuf, controller: DIDBuf, public_key: JWK) -> Self { + Self { + id, + controller, + public_key, } - let method_specific_id = &did[8..]; - let data = match multibase::Base::decode(&multibase::Base::Base64Url, method_specific_id) { - Ok(data) => data, - Err(_err) => { - return ( - ResolutionMetadata { - error: Some(ERROR_INVALID_DID.to_string()), - content_type: None, - property_set: None, - }, - None, - None, - ); - } - }; + } +} - let jwk: JWK = if let Ok(jwk) = serde_json::from_slice(&data) { - jwk - } else { - return ( - ResolutionMetadata { - error: Some(ERROR_NOT_FOUND.to_string()), - content_type: None, - property_set: None, +impl VerificationMethod for JsonWebKey2020 { + fn id(&self) -> &DIDURL { + &self.id + } + + fn type_(&self) -> &str { + JSON_WEB_KEY_2020_TYPE + } + + fn controller(&self) -> &DID { + &self.controller + } +} + +/// Error raised when the conversion to a [`JsonWebKey2020`] failed. +#[derive(Debug, thiserror::Error)] +pub enum InvalidJsonWebKey2020 { + #[error("invalid type")] + InvalidType, + + #[error("missing public key")] + MissingPublicKey, + + #[error("invalid public key")] + InvalidPublicKey, + + #[error("invalid private key")] + PrivateKey, +} + +impl From for AnyVerificationMethod { + fn from(value: JsonWebKey2020) -> Self { + let public_key = serde_json::to_value(&value.public_key).unwrap(); + AnyVerificationMethod::new( + value.id, + JSON_WEB_KEY_2020_TYPE.to_string(), + value.controller, + [("publicKeyJwk".to_string(), public_key)] + .into_iter() + .collect(), + ) + } +} + +impl TryFrom for JsonWebKey2020 { + type Error = InvalidJsonWebKey2020; + + fn try_from(mut value: AnyVerificationMethod) -> Result { + if value.type_ == "JsonWebKey2020" { + match value.properties.remove("publicKeyJwk") { + Some(key_value) => match serde_json::from_value(key_value) { + Ok(public_key) => Ok(Self { + id: value.id, + controller: value.controller, + public_key, + }), + Err(_) => Err(InvalidJsonWebKey2020::InvalidPublicKey), }, - None, - None, - ); - }; + None => Err(InvalidJsonWebKey2020::MissingPublicKey), + } + } else { + Err(InvalidJsonWebKey2020::InvalidType) + } + } +} - let public_jwk = jwk.to_public(); +/// Reference to a `JsonWebKey2020` verification method description. +pub struct JsonWebKey2020Ref<'a> { + pub id: &'a DIDURL, + pub controller: &'a DID, + pub public_key: Cow<'a, JWK>, +} - if public_jwk != jwk { - return ( - ResolutionMetadata { - error: Some(ERROR_INVALID_DID.to_string()), - content_type: None, - property_set: None, +impl<'a> TryFrom<&'a AnyVerificationMethod> for JsonWebKey2020Ref<'a> { + type Error = InvalidJsonWebKey2020; + + fn try_from(value: &'a AnyVerificationMethod) -> Result { + if value.type_ == "JsonWebKey2020" { + match value.properties.get("publicKeyJwk") { + Some(key_value) => match serde_json::from_value(key_value.clone()) { + Ok(public_key) => Ok(Self { + id: &value.id, + controller: &value.controller, + public_key: Cow::Owned(public_key), + }), + Err(_) => Err(InvalidJsonWebKey2020::InvalidPublicKey), }, - None, - None, - ); + None => Err(InvalidJsonWebKey2020::MissingPublicKey), + } + } else { + Err(InvalidJsonWebKey2020::InvalidType) } + } +} - let vm_didurl = DIDURL { - did: did.to_string(), - fragment: Some("0".to_string()), - ..Default::default() - }; - let doc = Document { - context: Contexts::Many(vec![ - Context::URI(DEFAULT_CONTEXT.into()), - Context::URI(iref!("https://w3id.org/security/suites/jws-2020/v1").to_owned()), - ]), - id: did.to_string(), - verification_method: Some(vec![VerificationMethod::Map(VerificationMethodMap { - id: vm_didurl.to_string(), - type_: "JsonWebKey2020".to_string(), - controller: did.to_string(), - public_key_jwk: Some(jwk), - ..Default::default() - })]), - assertion_method: Some(vec![VerificationMethod::DIDURL(vm_didurl.clone())]), - authentication: Some(vec![VerificationMethod::DIDURL(vm_didurl.clone())]), - capability_invocation: Some(vec![VerificationMethod::DIDURL(vm_didurl.clone())]), - capability_delegation: Some(vec![VerificationMethod::DIDURL(vm_didurl.clone())]), - key_agreement: Some(vec![VerificationMethod::DIDURL(vm_didurl)]), - ..Default::default() - }; - ( - ResolutionMetadata::default(), - Some(doc), - Some(DocumentMetadata::default()), - ) +/// JSON Web Token (`jwt`) DID method. +pub struct JWKMethod; + +impl JWKMethod { + /// Generates a JWK DID from the given key. + /// + /// # Example + /// + /// ``` + /// use did_jwk::JWKMethod; + /// + /// let jwk: ssi_jwk::JWK = serde_json::from_value(serde_json::json!({ + /// "crv": "P-256", + /// "kty": "EC", + /// "x": "acbIQiuMs3i8_uszEjJ2tpTtRM4EU3yz91PH6CdH2V0", + /// "y": "_KcyLj9vWMptnmKtm46GqDz8wf74I5LKgrl2GzH3nSE" + /// })).unwrap(); + /// + /// let did = JWKMethod::generate(&jwk); + /// assert_eq!(did, "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9"); + /// ``` + pub fn generate(key: &JWK) -> DIDBuf { + let normalized = serde_jcs::to_string(key).unwrap(); + let method_id = multibase::Base::Base64Url.encode(normalized); + DIDBuf::new(format!("did:jwk:{method_id}").into_bytes()).unwrap() } } -impl DIDMethod for DIDJWK { - fn name(&self) -> &'static str { +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl DIDMethodResolver for JWKMethod { + fn method_name(&self) -> &str { "jwk" } - fn generate(&self, source: &Source) -> Option { - let jwk = match source { - Source::Key(jwk) => jwk, - Source::KeyAndPattern(jwk, pattern) => { - if !pattern.is_empty() { - // pattern not supported - return None; - } - jwk - } - _ => return None, - }; - let jwk = jwk.to_public(); - let jwk = if let Ok(jwk) = serde_jcs::to_string(&jwk) { - jwk - } else { - return None; + async fn resolve_method_representation( + &self, + method_specific_id: &str, + options: Options, + ) -> Result>, Error> { + let data = multibase::Base::decode(&multibase::Base::Base64Url, method_specific_id) + .map_err(|_| Error::InvalidMethodSpecificId(method_specific_id.to_string()))?; + + let jwk: JWK = serde_json::from_slice(&data) + .map_err(|_| Error::InvalidMethodSpecificId(method_specific_id.to_string()))?; + + let public_jwk = jwk.to_public(); + + if public_jwk != jwk { + return Err(Error::InvalidMethodSpecificId( + method_specific_id.to_string(), + )); + } + + let did = DIDBuf::new(format!("did:jwk:{method_specific_id}").into_bytes()).unwrap(); + + let document = Document { + verification_method: vec![JsonWebKey2020::new( + DIDURLBuf::new(format!("did:jwk:{method_specific_id}#0").into_bytes()).unwrap(), + did.clone(), + jwk, + ) + .into()], + verification_relationships: VerificationRelationships::from_reference( + RelativeDIDURLBuf::new(b"#0".to_vec()).unwrap().into(), + ProofPurposes::all(), + ), + ..Document::new(did) }; - let did = "did:jwk:".to_string() + &multibase::encode(multibase::Base::Base64Url, jwk)[1..]; - Some(did) - } + let represented = document.into_representation(representation::Options::from_media_type( + options.accept.unwrap_or(MediaType::JsonLd), + || representation::json_ld::Options { + context: representation::json_ld::Context::array( + representation::json_ld::DIDContext::V1, + vec![serde_json::Value::String( + "https://w3id.org/security/suites/jws-2020/v1".to_string(), + )], + ), + }, + )); - fn to_resolver(&self) -> &dyn DIDResolver { - self + Ok(Output::new( + represented.to_bytes(), + document::Metadata::default(), + Metadata::from_content_type(Some(represented.media_type().to_string())), + )) } } #[cfg(test)] mod tests { use super::*; - use ssi_dids::did_resolve::{dereference, Content, DereferencingInputMetadata}; - use ssi_dids::Resource; + use ssi_dids::{resolution, DIDResolver}; #[async_std::test] #[cfg(feature = "secp256r1")] async fn from_p256() { - let vm = "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0"; - let (res_meta, object, _meta) = - dereference(&DIDJWK, vm, &DereferencingInputMetadata::default()).await; - assert_eq!(res_meta.error, None); - let vm = match object { - Content::Object(Resource::VerificationMethod(vm)) => vm, - _ => unreachable!(), - }; + let did_url = DIDURL::new(b"did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0").unwrap(); + let resolved = JWKMethod + .dereference(did_url, &resolution::DerefOptions::default()) + .await + .unwrap(); - assert_eq!(vm.id, "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0" ); - assert_eq!(vm.type_, "JsonWebKey2020"); - assert_eq!(vm.controller, "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9"); + let vm: JsonWebKey2020Ref = resolved + .content + .as_verification_method() + .unwrap() + .try_into() + .unwrap(); + + assert_eq!(vm.id, did_url); + assert_eq!(vm.controller, did_url.did()); - assert!(vm.public_key_jwk.is_some()); let jwk = serde_json::from_value(serde_json::json!({ "kty": "EC", "crv": "P-256", @@ -176,7 +254,8 @@ mod tests { "y": "_KcyLj9vWMptnmKtm46GqDz8wf74I5LKgrl2GzH3nSE" })) .unwrap(); - assert_eq!(vm.public_key_jwk.unwrap(), jwk); + + assert_eq!(vm.public_key, jwk); } #[async_std::test] @@ -189,44 +268,45 @@ mod tests { "y": "_KcyLj9vWMptnmKtm46GqDz8wf74I5LKgrl2GzH3nSE" })) .unwrap(); + let expected = "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9"; - let did = DIDJWK.generate(&Source::Key(&jwk)).unwrap(); - assert_eq!(expected, did); - - let (res_meta, object, _meta) = - dereference(&DIDJWK, &did, &DereferencingInputMetadata::default()).await; - assert_eq!(res_meta.error, None); - - let public_key_jwk = match object { - Content::DIDDocument(document) => match document.verification_method.as_deref() { - Some( - [VerificationMethod::Map(VerificationMethodMap { - ref public_key_jwk, .. - })], - ) => public_key_jwk.to_owned().unwrap(), - _ => unreachable!(), - }, - _ => unreachable!(), - }; - assert_eq!(public_key_jwk, jwk); + let did = JWKMethod::generate(&jwk); + assert_eq!(did, expected); + + let resolved = JWKMethod + .resolve(&did, resolution::Options::default()) + .await + .unwrap(); + + let vm_method: JsonWebKey2020Ref = resolved + .document + .verification_method + .first() + .unwrap() + .try_into() + .unwrap(); + + assert_eq!(*vm_method.public_key, jwk); } #[async_std::test] async fn from_x25519() { - let vm = "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0"; - let (res_meta, object, _meta) = - dereference(&DIDJWK, vm, &DereferencingInputMetadata::default()).await; - assert_eq!(res_meta.error, None); - let vm = match object { - Content::Object(Resource::VerificationMethod(vm)) => vm, - _ => unreachable!(), - }; + let did_url = DIDURL::new(b"did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0").unwrap(); + let resolved = JWKMethod + .dereference(did_url, &resolution::DerefOptions::default()) + .await + .unwrap(); + + let vm: JsonWebKey2020Ref = resolved + .content + .as_verification_method() + .unwrap() + .try_into() + .unwrap(); - assert_eq!(vm.id, "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0" ); - assert_eq!(vm.type_, "JsonWebKey2020"); - assert_eq!(vm.controller, "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9"); + assert_eq!(vm.id, did_url); + assert_eq!(vm.controller, did_url.did()); - assert!(vm.public_key_jwk.is_some()); let jwk = serde_json::from_value(serde_json::json!({ "kty": "OKP", "crv": "X25519", @@ -234,7 +314,7 @@ mod tests { "x": "3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08" })) .unwrap(); - assert_eq!(vm.public_key_jwk.unwrap(), jwk); + assert_eq!(*vm.public_key, jwk); } #[async_std::test] @@ -245,27 +325,26 @@ mod tests { "use": "enc", "x": "3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08" }); + let jwk: ssi_jwk::JWK = serde_json::from_value(json).unwrap(); let expected = "did:jwk:eyJjcnYiOiJYMjU1MTkiLCJrdHkiOiJPS1AiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9"; - let did = DIDJWK.generate(&Source::Key(&jwk)).unwrap(); - assert_eq!(expected, did); - - let (res_meta, object, _meta) = - dereference(&DIDJWK, &did, &DereferencingInputMetadata::default()).await; - assert_eq!(res_meta.error, None); - - let public_key_jwk = match object { - Content::DIDDocument(document) => match document.verification_method.as_deref() { - Some( - [VerificationMethod::Map(VerificationMethodMap { - ref public_key_jwk, .. - })], - ) => public_key_jwk.to_owned().unwrap(), - _ => unreachable!(), - }, - _ => unreachable!(), - }; - assert_eq!(public_key_jwk, jwk); + let did = JWKMethod::generate(&jwk); + assert_eq!(did, expected); + + let resolved = JWKMethod + .resolve(&did, resolution::Options::default()) + .await + .unwrap(); + + let vm_method: JsonWebKey2020Ref = resolved + .document + .verification_method + .first() + .unwrap() + .try_into() + .unwrap(); + + assert_eq!(*vm_method.public_key, jwk); } #[async_std::test] @@ -273,11 +352,21 @@ mod tests { async fn deny_private_key() { let jwk = JWK::generate_ed25519().unwrap(); let json = serde_jcs::to_string(&jwk).unwrap(); - let did = - "did:jwk:".to_string() + &multibase::encode(multibase::Base::Base64Url, &json)[1..]; + let did = DIDBuf::new( + format!("did:jwk:{}", multibase::Base::Base64Url.encode(&json)).into_bytes(), + ) + .unwrap(); - let (res_meta, _object, _meta) = - dereference(&DIDJWK, &did, &DereferencingInputMetadata::default()).await; - assert!(res_meta.error.is_some()); + let resolved = JWKMethod + .resolve(&did, resolution::Options::default()) + .await + .unwrap(); + let result: Result = resolved + .document + .verification_method + .first() + .unwrap() + .try_into(); + assert!(matches!(result, Err(InvalidJsonWebKey2020::PrivateKey))) } }