From b4b7f54e0c811c342ed7dff1362ad6de913d6369 Mon Sep 17 00:00:00 2001 From: Tiago Nascimento Date: Wed, 28 Sep 2022 16:03:52 -0300 Subject: [PATCH] Add did-jwk support Signed-off-by: Tiago Nascimento --- Cargo.toml | 1 + did-jwk/Cargo.toml | 38 +++++++ did-jwk/README.md | 10 ++ did-jwk/src/lib.rs | 248 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 297 insertions(+) create mode 100644 did-jwk/Cargo.toml create mode 100644 did-jwk/README.md create mode 100644 did-jwk/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 3c9ab1648..098b617e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ ssi-caips = { path = "./ssi-caips", version = "0.1" } [workspace] members = [ "did-tezos", + "did-jwk", "did-key", "did-web", "did-ethr", diff --git a/did-jwk/Cargo.toml b/did-jwk/Cargo.toml new file mode 100644 index 000000000..d39f628b4 --- /dev/null +++ b/did-jwk/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "did-method-jwk" +version = "0.1.0" +authors = ["Spruce Systems, Inc."] +edition = "2021" +license = "Apache-2.0" +keywords = ["ssi", "did"] +categories = ["cryptography::cryptocurrencies"] +description = "did:jwk DID method, using the ssi crate" +repository = "https://github.com/spruceid/ssi/" +homepage = "https://github.com/spruceid/ssi/tree/main/did-jwk/" +documentation = "https://docs.rs/did-jwk/" + +[features] +default = ["secp256k1", "secp256r1"] +secp256k1 = ["ssi-dids/secp256k1"] +secp256r1 = ["ssi-dids/secp256r1"] +ssi_p384 = ["openssl"] + +ring = ["ssi-dids/ring"] +openssl = ["ssi-dids/openssl"] + +[dependencies] +ssi-dids = { path = "../ssi-dids", version = "0.1", default-features = false } +ssi-jwk = { path = "../ssi-jwk", version = "0.1", default-features = false, features = [ + "ripemd-160", +] } +async-trait = "0.1" +multibase = "0.8" +serde_json = "1.0" +serde_jcs = "0.1" + +[dev-dependencies] +ssi = { version = "0.4", path = "../", default-features = false } +serde = { version = "1.0", features = ["derive"] } +async-std = { version = "1.9", features = ["attributes"] } +serde_json = "1.0" +serde_jcs = "0.1" diff --git a/did-jwk/README.md b/did-jwk/README.md new file mode 100644 index 000000000..11334ebad --- /dev/null +++ b/did-jwk/README.md @@ -0,0 +1,10 @@ +# did-key + +Rust implementation of the [did:jwk][] DID Method, based on the [ssi][] library. + +## License + +[Apache License, Version 2.0](http://www.apache.org/licenses/) + +[did:jwk]: https://github.com/quartzjer/did-jwk/blob/main/spec.md +[ssi]: https://github.com/spruceid/ssi/ diff --git a/did-jwk/src/lib.rs b/did-jwk/src/lib.rs new file mode 100644 index 000000000..612e33cc0 --- /dev/null +++ b/did-jwk/src/lib.rs @@ -0,0 +1,248 @@ +use async_trait::async_trait; + +use ssi_dids::did_resolve::{ + DIDResolver, DocumentMetadata, ResolutionInputMetadata, ResolutionMetadata, ERROR_INVALID_DID, + ERROR_NOT_FOUND, +}; +use ssi_dids::{ + Context, Contexts, DIDMethod, Document, Source, VerificationMethod, VerificationMethodMap, + DEFAULT_CONTEXT, DIDURL, +}; + +pub struct DIDJWK; + +#[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, + ); + } + let method_specific_id = &did[8..]; + let data = match multibase::Base::decode(&multibase::Base::Base64Url, method_specific_id) { + Ok(data) => data, + Err(_err) => { + // TODO: pass through these errors somehow + return ( + ResolutionMetadata { + error: Some(ERROR_INVALID_DID.to_string()), + content_type: None, + property_set: None, + }, + None, + None, + ); + } + }; + + let 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, + }, + None, + None, + ); + }; + 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.to_string()), + Context::URI("https://w3id.org/security/suites/jws-2020/v1".to_string()), + ]), + 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()), + ) + } +} + +impl DIDMethod for DIDJWK { + fn name(&self) -> &'static 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 = serde_jcs::to_string(&jwk).unwrap(); + let did = + "did:jwk:".to_string() + &multibase::encode(multibase::Base::Base64Url, &jwk)[1..]; + Some(did) + } + + fn to_resolver(&self) -> &dyn DIDResolver { + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ssi_dids::did_resolve::{dereference, Content, DereferencingInputMetadata}; + use ssi_dids::Resource; + + #[async_std::test] + 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!(), + }; + + assert_eq!(vm.id, "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0" ); + assert_eq!(vm.type_, "JsonWebKey2020"); + assert_eq!(vm.controller, "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9"); + + assert!(vm.public_key_jwk.is_some()); + let jwk = serde_json::from_value(serde_json::json!({ + "kty": "EC", + "crv": "P-256", + "x": "acbIQiuMs3i8_uszEjJ2tpTtRM4EU3yz91PH6CdH2V0", + "y": "_KcyLj9vWMptnmKtm46GqDz8wf74I5LKgrl2GzH3nSE" + })) + .unwrap(); + assert_eq!(vm.public_key_jwk.unwrap(), jwk); + } + + #[async_std::test] + async fn to_p256() { + let jwk: ssi_jwk::JWK = serde_json::from_value(serde_json::json!({ + "crv": "P-256", + "kty": "EC", + "x": "acbIQiuMs3i8_uszEjJ2tpTtRM4EU3yz91PH6CdH2V0", + "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); + } + + #[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!(), + }; + + assert_eq!(vm.id, "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0" ); + assert_eq!(vm.type_, "JsonWebKey2020"); + assert_eq!(vm.controller, "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9"); + + assert!(vm.public_key_jwk.is_some()); + let jwk = serde_json::from_value(serde_json::json!({ + "kty": "OKP", + "crv": "X25519", + "use": "enc", + "x": "3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08" + })) + .unwrap(); + assert_eq!(vm.public_key_jwk.unwrap(), jwk); + } + + #[async_std::test] + async fn to_x25519() { + let json = serde_json::json!({ + "kty": "OKP", + "crv": "X25519", + "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); + } +}