diff --git a/.github/ci/vdrproxy.dockerfile b/.github/ci/vdrproxy.dockerfile index 95c2b3da47..c5dc96ef1a 100644 --- a/.github/ci/vdrproxy.dockerfile +++ b/.github/ci/vdrproxy.dockerfile @@ -18,7 +18,7 @@ RUN apk update && apk upgrade && \ USER indy WORKDIR /home/indy -ARG RUST_VER="1.70.0" +ARG RUST_VER="1.79.0" RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $RUST_VER --default-host x86_64-unknown-linux-musl ENV PATH="/home/indy/.cargo/bin:$PATH" diff --git a/Cargo.lock b/Cargo.lock index 708de0a90e..fa3db8a79d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1736,6 +1736,22 @@ dependencies = [ "url", ] +[[package]] +name = "did_jwk" +version = "0.1.0" +dependencies = [ + "async-trait", + "base64 0.22.1", + "did_doc", + "did_parser_nom", + "did_resolver", + "public_key", + "serde", + "serde_json", + "thiserror", + "tokio", +] + [[package]] name = "did_key" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 8a22790358..1ff032934e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ members = [ "did_core/public_key", "misc/simple_message_relay", "misc/display_as_json", + "did_core/did_methods/did_jwk", ] [workspace.package] diff --git a/README.md b/README.md index 11a2560a82..364931b934 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ The repository contains Rust crates to build - [`did_sov`](did_core/did_methods/did_resolver_sov) - https://sovrin-foundation.github.io/sovrin/spec/did-method-spec-template.html - [`did_web`](did_core/did_methods/did_resolver_web) - https://w3c-ccg.github.io/did-method-web/ - [`did_key`](did_core/did_methods/did_key) - https://w3c-ccg.github.io/did-method-key/ + - [`did_jwk`](did_core/did_methods/did_jwk) - https://github.com/quartzjer/did-jwk/blob/main/spec.md # Contact Do you have a question ❓Are you considering using our components? πŸš€ We'll be excited to hear from you. πŸ‘‹ diff --git a/did_core/did_doc/src/schema/types/jsonwebkey.rs b/did_core/did_doc/src/schema/types/jsonwebkey.rs index cb3a7df339..8dcda2abb5 100644 --- a/did_core/did_doc/src/schema/types/jsonwebkey.rs +++ b/did_core/did_doc/src/schema/types/jsonwebkey.rs @@ -31,13 +31,13 @@ impl Display for JsonWebKeyError { // Unfortunately only supports curves from the original RFC // pub struct JsonWebKey(jsonwebkey::JsonWebKey); pub struct JsonWebKey { - kty: String, - crv: String, - x: String, + pub kty: String, + pub crv: String, + pub x: String, #[serde(flatten)] #[serde(skip_serializing_if = "HashMap::is_empty")] #[serde(default)] - extra: HashMap, + pub extra: HashMap, } impl JsonWebKey { diff --git a/did_core/did_methods/did_jwk/Cargo.toml b/did_core/did_methods/did_jwk/Cargo.toml new file mode 100644 index 0000000000..97bf2c2ab2 --- /dev/null +++ b/did_core/did_methods/did_jwk/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "did_jwk" +version = "0.1.0" +edition.workspace = true + +[dependencies] +did_parser_nom = { path = "../../did_parser_nom" } +did_doc = { path = "../../did_doc" } +did_resolver = { path = "../../did_resolver" } +public_key = { path = "../../public_key", features = ["jwk"] } +async-trait = "0.1.68" +serde = { version = "1.0.164", features = ["derive"] } +serde_json = "1.0.96" +base64 = "0.22.1" +thiserror = "1.0.44" + +[dev-dependencies] +tokio = { version = "1.38.0", default-features = false, features = ["macros", "rt"] } diff --git a/did_core/did_methods/did_jwk/src/error.rs b/did_core/did_methods/did_jwk/src/error.rs new file mode 100644 index 0000000000..66e979eb70 --- /dev/null +++ b/did_core/did_methods/did_jwk/src/error.rs @@ -0,0 +1,15 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum DidJwkError { + #[error("DID method not supported: {0}")] + MethodNotSupported(String), + #[error("Base64 encoding error: {0}")] + Base64Error(#[from] base64::DecodeError), + #[error("Serde JSON error: {0}")] + SerdeJsonError(#[from] serde_json::Error), + #[error("Public key error: {0}")] + PublicKeyError(#[from] public_key::PublicKeyError), + #[error("DID parser error: {0}")] + DidParserError(#[from] did_parser_nom::ParseError), +} diff --git a/did_core/did_methods/did_jwk/src/lib.rs b/did_core/did_methods/did_jwk/src/lib.rs new file mode 100644 index 0000000000..0f2803101c --- /dev/null +++ b/did_core/did_methods/did_jwk/src/lib.rs @@ -0,0 +1,270 @@ +use std::fmt::{self, Display}; + +use base64::{ + alphabet, + engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig}, + Engine, +}; +use did_doc::schema::types::jsonwebkey::JsonWebKey; +use did_parser_nom::Did; +use error::DidJwkError; +use public_key::{Key, KeyType}; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::json; + +pub mod error; +pub mod resolver; + +const USE: &str = "use"; +const USE_SIG: &str = "sig"; +const USE_ENC: &str = "enc"; + +/// A default [GeneralPurposeConfig] configuration with a [decode_padding_mode] of +/// [DecodePaddingMode::Indifferent] +const LENIENT_PAD: GeneralPurposeConfig = GeneralPurposeConfig::new() + .with_encode_padding(false) + .with_decode_padding_mode(DecodePaddingMode::Indifferent); + +/// A [GeneralPurpose] engine using the [alphabet::URL_SAFE] base64 alphabet and +/// [DecodePaddingMode::Indifferent] config to decode both padded and unpadded. +const URL_SAFE_LENIENT: GeneralPurpose = GeneralPurpose::new(&alphabet::URL_SAFE, LENIENT_PAD); + +/// Represents did:key where the DID ID is JWK public key itself +/// See the spec: https://github.com/quartzjer/did-jwk/blob/main/spec.md +#[derive(Clone, Debug, PartialEq)] +pub struct DidJwk { + jwk: JsonWebKey, + did: Did, +} + +impl DidJwk { + pub fn parse(did: T) -> Result + where + Did: TryFrom, + >::Error: Into, + { + let did: Did = did.try_into().map_err(Into::into)?; + Self::try_from(did) + } + + pub fn try_from_serialized_jwk(jwk: &str) -> Result { + let jwk: JsonWebKey = serde_json::from_str(jwk)?; + Self::try_from(jwk) + } + + pub fn jwk(&self) -> &JsonWebKey { + &self.jwk + } + + pub fn did(&self) -> &Did { + &self.did + } + + pub fn key(&self) -> Result { + Ok(Key::from_jwk(&serde_json::to_string(&self.jwk)?)?) + } +} + +impl TryFrom for DidJwk { + type Error = DidJwkError; + + fn try_from(did: Did) -> Result { + match did.method() { + Some("jwk") => {} + other => return Err(DidJwkError::MethodNotSupported(format!("{other:?}"))), + } + + let jwk = decode_jwk(did.id())?; + Ok(Self { jwk, did }) + } +} + +impl TryFrom for DidJwk { + type Error = DidJwkError; + + fn try_from(jwk: JsonWebKey) -> Result { + let encoded_jwk = encode_jwk(&jwk)?; + let did = Did::parse(format!("did:jwk:{encoded_jwk}",))?; + + Ok(Self { jwk, did }) + } +} + +impl TryFrom for DidJwk { + type Error = DidJwkError; + + fn try_from(key: Key) -> Result { + let jwk = key.to_jwk()?; + let mut jwk: JsonWebKey = serde_json::from_str(&jwk)?; + + match key.key_type() { + KeyType::Ed25519 + | KeyType::Bls12381g1g2 + | KeyType::Bls12381g1 + | KeyType::Bls12381g2 + | KeyType::P256 + | KeyType::P384 + | KeyType::P521 => { + jwk.extra.insert(String::from(USE), json!(USE_SIG)); + } + KeyType::X25519 => { + jwk.extra.insert(String::from(USE), json!(USE_ENC)); + } + } + + Self::try_from(jwk) + } +} + +impl Display for DidJwk { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.did) + } +} + +impl Serialize for DidJwk { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(self.did.did()) + } +} + +impl<'de> Deserialize<'de> for DidJwk { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + + DidJwk::parse(s).map_err(de::Error::custom) + } +} + +fn encode_jwk(jwk: &JsonWebKey) -> Result { + let jwk_bytes = serde_json::to_vec(jwk)?; + Ok(URL_SAFE_LENIENT.encode(jwk_bytes)) +} + +fn decode_jwk(encoded_jwk: &str) -> Result { + let jwk_bytes = URL_SAFE_LENIENT.decode(encoded_jwk)?; + Ok(serde_json::from_slice(&jwk_bytes)?) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn valid_key_base58_fingerprint() -> String { + "z6MkeWVt6dndY6EbFwEvb3VQU6ksQXKTeorkQ9sU29DY7yRX".to_string() + } + + fn valid_key() -> Key { + Key::from_fingerprint(&valid_key_base58_fingerprint()).unwrap() + } + + fn valid_serialized_jwk() -> String { + r#"{ + "kty": "OKP", + "crv": "Ed25519", + "x": "ANRjH_zxcKBxsjRPUtzRbp7FSVLKJXQ9APX9MP1j7k4", + "use": "sig" + }"# + .to_string() + } + + fn valid_jwk() -> JsonWebKey { + serde_json::from_str(&valid_serialized_jwk()).unwrap() + } + + fn valid_encoded_jwk() -> String { + URL_SAFE_LENIENT.encode(serde_json::to_vec(&valid_jwk()).unwrap()) + } + + fn valid_did_jwk_string() -> String { + format!("did:jwk:{}", valid_encoded_jwk()) + } + + fn invalid_did_jwk_string_wrong_method() -> String { + format!("did:sov:{}", valid_encoded_jwk()) + } + + fn invalid_did_jwk_string_invalid_id() -> String { + "did:jwk:somenonsense".to_string() + } + + fn valid_did_jwk() -> DidJwk { + DidJwk { + jwk: valid_jwk(), + did: Did::parse(valid_did_jwk_string()).unwrap(), + } + } + + #[test] + fn test_serialize() { + assert_eq!( + format!("\"{}\"", valid_did_jwk_string()), + serde_json::to_string(&valid_did_jwk()).unwrap(), + ); + } + + #[test] + fn test_deserialize() { + assert_eq!( + valid_did_jwk(), + serde_json::from_str::(&format!("\"{}\"", valid_did_jwk_string())).unwrap(), + ); + } + + #[test] + fn test_deserialize_error_wrong_method() { + assert!(serde_json::from_str::(&invalid_did_jwk_string_wrong_method()).is_err()); + } + + #[test] + fn test_deserialize_error_invalid_id() { + assert!(serde_json::from_str::(&invalid_did_jwk_string_invalid_id()).is_err()); + } + + #[test] + fn test_parse() { + assert_eq!( + valid_did_jwk(), + DidJwk::parse(valid_did_jwk_string()).unwrap(), + ); + } + + #[test] + fn test_parse_error_wrong_method() { + assert!(DidJwk::parse(invalid_did_jwk_string_wrong_method()).is_err()); + } + + #[test] + fn test_parse_error_invalid_id() { + assert!(DidJwk::parse(invalid_did_jwk_string_invalid_id()).is_err()); + } + + #[test] + fn test_to_key() { + assert_eq!(valid_did_jwk().key().unwrap(), valid_key()); + } + + #[test] + fn test_try_from_serialized_jwk() { + assert_eq!( + valid_did_jwk(), + DidJwk::try_from_serialized_jwk(&valid_serialized_jwk()).unwrap(), + ); + } + + #[test] + fn test_try_from_jwk() { + assert_eq!(valid_did_jwk(), DidJwk::try_from(valid_jwk()).unwrap(),); + } + + #[test] + fn test_try_from_key() { + assert_eq!(valid_did_jwk(), DidJwk::try_from(valid_key()).unwrap(),); + } +} diff --git a/did_core/did_methods/did_jwk/src/resolver.rs b/did_core/did_methods/did_jwk/src/resolver.rs new file mode 100644 index 0000000000..cf0da5b43b --- /dev/null +++ b/did_core/did_methods/did_jwk/src/resolver.rs @@ -0,0 +1,71 @@ +use async_trait::async_trait; +use did_doc::schema::{ + did_doc::DidDocument, + verification_method::{PublicKeyField, VerificationMethod, VerificationMethodType}, +}; +use did_parser_nom::{Did, DidUrl}; +use did_resolver::{ + error::GenericError, + traits::resolvable::{resolution_output::DidResolutionOutput, DidResolvable}, +}; +use serde_json::Value; + +use crate::DidJwk; + +#[derive(Default)] +pub struct DidJwkResolver; + +impl DidJwkResolver { + pub fn new() -> Self { + Self + } +} + +#[async_trait] +impl DidResolvable for DidJwkResolver { + type DidResolutionOptions = (); + + async fn resolve( + &self, + did: &Did, + _options: &Self::DidResolutionOptions, + ) -> Result { + let did_jwk = DidJwk::try_from(did.to_owned())?; + let jwk = did_jwk.jwk(); + + let jwk_use = jwk.extra.get("use").and_then(Value::as_str); + + let mut did_doc = DidDocument::new(did.to_owned()); + + let vm_id = DidUrl::parse(format!("{}#0", did))?; + + let vm = VerificationMethod::builder() + .id(vm_id.clone()) + .controller(did.clone()) + .verification_method_type(VerificationMethodType::JsonWebKey2020) + .public_key(PublicKeyField::Jwk { + public_key_jwk: jwk.clone(), + }) + .build(); + did_doc.add_verification_method(vm); + + match jwk_use { + Some("enc") => did_doc.add_key_agreement_ref(vm_id), + Some("sig") => { + did_doc.add_assertion_method_ref(vm_id.clone()); + did_doc.add_authentication_ref(vm_id.clone()); + did_doc.add_capability_invocation_ref(vm_id.clone()); + did_doc.add_capability_delegation_ref(vm_id.clone()); + } + _ => { + did_doc.add_assertion_method_ref(vm_id.clone()); + did_doc.add_authentication_ref(vm_id.clone()); + did_doc.add_capability_invocation_ref(vm_id.clone()); + did_doc.add_capability_delegation_ref(vm_id.clone()); + did_doc.add_key_agreement_ref(vm_id.clone()); + } + }; + + Ok(DidResolutionOutput::builder(did_doc).build()) + } +} diff --git a/did_core/did_methods/did_jwk/tests/resolution.rs b/did_core/did_methods/did_jwk/tests/resolution.rs new file mode 100644 index 0000000000..c5dc695ca2 --- /dev/null +++ b/did_core/did_methods/did_jwk/tests/resolution.rs @@ -0,0 +1,104 @@ +use did_doc::schema::did_doc::DidDocument; +use did_jwk::resolver::DidJwkResolver; +use did_parser_nom::Did; +use did_resolver::traits::resolvable::DidResolvable; +use serde_json::json; + +// https://github.com/quartzjer/did-jwk/blob/main/spec.md#p-256 +#[tokio::test] +async fn test_p256_spec_vector_resolution() { + let did = Did::parse("did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9".to_string()).unwrap(); + let spec_json = json!({ + "id": "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9", + "verificationMethod": [ + { + "id": "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0", + "type": "JsonWebKey2020", + "controller": "did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9", + "publicKeyJwk": { + "crv": "P-256", + "kty": "EC", + "x": "acbIQiuMs3i8_uszEjJ2tpTtRM4EU3yz91PH6CdH2V0", + "y": "_KcyLj9vWMptnmKtm46GqDz8wf74I5LKgrl2GzH3nSE" + } + } + ], + "assertionMethod": ["did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0"], + "authentication": ["did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0"], + "capabilityInvocation": ["did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0"], + "capabilityDelegation": ["did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0"], + "keyAgreement": ["did:jwk:eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImFjYklRaXVNczNpOF91c3pFakoydHBUdFJNNEVVM3l6OTFQSDZDZEgyVjAiLCJ5IjoiX0tjeUxqOXZXTXB0bm1LdG00NkdxRHo4d2Y3NEk1TEtncmwyR3pIM25TRSJ9#0"] + }); + let expected_did_doc: DidDocument = serde_json::from_value(spec_json).unwrap(); + + let resolver = DidJwkResolver::new(); + + let output = resolver.resolve(&did, &()).await.unwrap(); + let actual_did_doc = output.did_document; + assert_eq!(actual_did_doc, expected_did_doc); +} + +// https://github.com/quartzjer/did-jwk/blob/main/spec.md#x25519 +#[tokio::test] +async fn test_x25519_spec_vector_resolution() { + let did = Did::parse("did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9".to_string()).unwrap(); + let spec_json = json!({ + "id": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9", + "verificationMethod": [ + { + "id": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0", + "type": "JsonWebKey2020", + "controller": "did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9", + "publicKeyJwk": { + "kty":"OKP", + "crv":"X25519", + "use":"enc", + "x":"3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08" + } + } + ], + "keyAgreement": ["did:jwk:eyJrdHkiOiJPS1AiLCJjcnYiOiJYMjU1MTkiLCJ1c2UiOiJlbmMiLCJ4IjoiM3A3YmZYdDl3YlRUVzJIQzdPUTFOei1EUThoYmVHZE5yZngtRkctSUswOCJ9#0"] + }); + let expected_did_doc: DidDocument = serde_json::from_value(spec_json).unwrap(); + + let resolver = DidJwkResolver::new(); + + let output = resolver.resolve(&did, &()).await.unwrap(); + let actual_did_doc = output.did_document; + assert_eq!(actual_did_doc, expected_did_doc); +} + +#[tokio::test] +async fn test_ed25519_vector_resolution() { + // reference vectors from universal resolver + let did = Did::parse("did:jwk:eyJraWQiOiJ1cm46aWV0ZjpwYXJhbXM6b2F1dGg6andrLXRodW1icHJpbnQ6c2hhLTI1NjpGZk1iek9qTW1RNGVmVDZrdndUSUpqZWxUcWpsMHhqRUlXUTJxb2JzUk1NIiwia3R5IjoiT0tQIiwiY3J2IjoiRWQyNTUxOSIsImFsZyI6IkVkRFNBIiwieCI6IkFOUmpIX3p4Y0tCeHNqUlBVdHpSYnA3RlNWTEtKWFE5QVBYOU1QMWo3azQifQ".to_string()).unwrap(); + let json = json!({ + "id": "did:jwk:eyJraWQiOiJ1cm46aWV0ZjpwYXJhbXM6b2F1dGg6andrLXRodW1icHJpbnQ6c2hhLTI1NjpGZk1iek9qTW1RNGVmVDZrdndUSUpqZWxUcWpsMHhqRUlXUTJxb2JzUk1NIiwia3R5IjoiT0tQIiwiY3J2IjoiRWQyNTUxOSIsImFsZyI6IkVkRFNBIiwieCI6IkFOUmpIX3p4Y0tCeHNqUlBVdHpSYnA3RlNWTEtKWFE5QVBYOU1QMWo3azQifQ", + "verificationMethod": [ + { + "id": "did:jwk:eyJraWQiOiJ1cm46aWV0ZjpwYXJhbXM6b2F1dGg6andrLXRodW1icHJpbnQ6c2hhLTI1NjpGZk1iek9qTW1RNGVmVDZrdndUSUpqZWxUcWpsMHhqRUlXUTJxb2JzUk1NIiwia3R5IjoiT0tQIiwiY3J2IjoiRWQyNTUxOSIsImFsZyI6IkVkRFNBIiwieCI6IkFOUmpIX3p4Y0tCeHNqUlBVdHpSYnA3RlNWTEtKWFE5QVBYOU1QMWo3azQifQ#0", + "type": "JsonWebKey2020", + "controller": "did:jwk:eyJraWQiOiJ1cm46aWV0ZjpwYXJhbXM6b2F1dGg6andrLXRodW1icHJpbnQ6c2hhLTI1NjpGZk1iek9qTW1RNGVmVDZrdndUSUpqZWxUcWpsMHhqRUlXUTJxb2JzUk1NIiwia3R5IjoiT0tQIiwiY3J2IjoiRWQyNTUxOSIsImFsZyI6IkVkRFNBIiwieCI6IkFOUmpIX3p4Y0tCeHNqUlBVdHpSYnA3RlNWTEtKWFE5QVBYOU1QMWo3azQifQ", + "publicKeyJwk": { + "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:FfMbzOjMmQ4efT6kvwTIJjelTqjl0xjEIWQ2qobsRMM", + "kty": "OKP", + "crv": "Ed25519", + "alg": "EdDSA", + "x": "ANRjH_zxcKBxsjRPUtzRbp7FSVLKJXQ9APX9MP1j7k4" + } + } + ], + "authentication": ["did:jwk:eyJraWQiOiJ1cm46aWV0ZjpwYXJhbXM6b2F1dGg6andrLXRodW1icHJpbnQ6c2hhLTI1NjpGZk1iek9qTW1RNGVmVDZrdndUSUpqZWxUcWpsMHhqRUlXUTJxb2JzUk1NIiwia3R5IjoiT0tQIiwiY3J2IjoiRWQyNTUxOSIsImFsZyI6IkVkRFNBIiwieCI6IkFOUmpIX3p4Y0tCeHNqUlBVdHpSYnA3RlNWTEtKWFE5QVBYOU1QMWo3azQifQ#0"], + "assertionMethod": ["did:jwk:eyJraWQiOiJ1cm46aWV0ZjpwYXJhbXM6b2F1dGg6andrLXRodW1icHJpbnQ6c2hhLTI1NjpGZk1iek9qTW1RNGVmVDZrdndUSUpqZWxUcWpsMHhqRUlXUTJxb2JzUk1NIiwia3R5IjoiT0tQIiwiY3J2IjoiRWQyNTUxOSIsImFsZyI6IkVkRFNBIiwieCI6IkFOUmpIX3p4Y0tCeHNqUlBVdHpSYnA3RlNWTEtKWFE5QVBYOU1QMWo3azQifQ#0"], + "capabilityInvocation": ["did:jwk:eyJraWQiOiJ1cm46aWV0ZjpwYXJhbXM6b2F1dGg6andrLXRodW1icHJpbnQ6c2hhLTI1NjpGZk1iek9qTW1RNGVmVDZrdndUSUpqZWxUcWpsMHhqRUlXUTJxb2JzUk1NIiwia3R5IjoiT0tQIiwiY3J2IjoiRWQyNTUxOSIsImFsZyI6IkVkRFNBIiwieCI6IkFOUmpIX3p4Y0tCeHNqUlBVdHpSYnA3RlNWTEtKWFE5QVBYOU1QMWo3azQifQ#0"], + "capabilityDelegation": ["did:jwk:eyJraWQiOiJ1cm46aWV0ZjpwYXJhbXM6b2F1dGg6andrLXRodW1icHJpbnQ6c2hhLTI1NjpGZk1iek9qTW1RNGVmVDZrdndUSUpqZWxUcWpsMHhqRUlXUTJxb2JzUk1NIiwia3R5IjoiT0tQIiwiY3J2IjoiRWQyNTUxOSIsImFsZyI6IkVkRFNBIiwieCI6IkFOUmpIX3p4Y0tCeHNqUlBVdHpSYnA3RlNWTEtKWFE5QVBYOU1QMWo3azQifQ#0"], + "keyAgreement": ["did:jwk:eyJraWQiOiJ1cm46aWV0ZjpwYXJhbXM6b2F1dGg6andrLXRodW1icHJpbnQ6c2hhLTI1NjpGZk1iek9qTW1RNGVmVDZrdndUSUpqZWxUcWpsMHhqRUlXUTJxb2JzUk1NIiwia3R5IjoiT0tQIiwiY3J2IjoiRWQyNTUxOSIsImFsZyI6IkVkRFNBIiwieCI6IkFOUmpIX3p4Y0tCeHNqUlBVdHpSYnA3RlNWTEtKWFE5QVBYOU1QMWo3azQifQ#0"], + }); + let expected_did_doc: DidDocument = serde_json::from_value(json).unwrap(); + + let resolver = DidJwkResolver::new(); + + let output = resolver.resolve(&did, &()).await.unwrap(); + let actual_did_doc = output.did_document; + assert_eq!(actual_did_doc, expected_did_doc); +} diff --git a/did_core/public_key/src/jwk.rs b/did_core/public_key/src/jwk.rs index 05184a3a3c..0242dfc842 100644 --- a/did_core/public_key/src/jwk.rs +++ b/did_core/public_key/src/jwk.rs @@ -1,6 +1,6 @@ use askar_crypto::{ - alg::{AnyKey, BlsCurves, EcCurves}, - jwk::FromJwk, + alg::{AnyKey, AnyKeyCreate, BlsCurves, EcCurves, KeyAlg}, + jwk::{FromJwk, ToJwk}, repr::ToPublicBytes, }; @@ -30,71 +30,130 @@ impl Key { Key::new(pub_key_bytes, key_type) } + + pub fn to_jwk(&self) -> Result { + let askar_key = self.to_askar_local_key()?; + askar_key.to_jwk_public(None).map_err(|e| { + PublicKeyError::UnsupportedKeyType(format!("Could not process this key as JWK {e:?}")) + }) + } + + fn to_askar_local_key(&self) -> Result, PublicKeyError> { + let alg = public_key_type_to_askar_key_alg(self.key_type())?; + AnyKeyCreate::from_public_bytes(alg, self.key()).map_err(|e| { + PublicKeyError::UnsupportedKeyType(format!("Could not process key type {alg:?}: {e:?}")) + }) + } +} + +pub fn public_key_type_to_askar_key_alg(value: &KeyType) -> Result { + let alg = match value { + KeyType::Ed25519 => KeyAlg::Ed25519, + KeyType::X25519 => KeyAlg::X25519, + KeyType::Bls12381g1g2 => KeyAlg::Bls12_381(BlsCurves::G1G2), + KeyType::Bls12381g1 => KeyAlg::Bls12_381(BlsCurves::G1), + KeyType::Bls12381g2 => KeyAlg::Bls12_381(BlsCurves::G2), + KeyType::P256 => KeyAlg::EcCurve(EcCurves::Secp256r1), + KeyType::P384 => KeyAlg::EcCurve(EcCurves::Secp384r1), + other => { + return Err(PublicKeyError::UnsupportedKeyType(format!( + "Unsupported key type: {other:?}" + ))); + } + }; + Ok(alg) } #[cfg(test)] mod tests { + use serde_json::Value; + use super::*; + // vector from https://w3c-ccg.github.io/did-method-key/#ed25519-x25519 + const ED25519_JWK: &str = r#"{ + "kty": "OKP", + "crv": "Ed25519", + "x": "O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik" + }"#; + const ED25519_FINGERPRINT: &str = "z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp"; + + // vector from https://w3c-ccg.github.io/did-method-key/#ed25519-x25519 + const X25519_JWK: &str = r#"{ + "kty": "OKP", + "crv": "X25519", + "x": "W_Vcc7guviK-gPNDBmevVw-uJVamQV5rMNQGUwCqlH0" + }"#; + const X25519_FINGERPRINT: &str = "z6LShs9GGnqk85isEBzzshkuVWrVKsRp24GnDuHk8QWkARMW"; + + // vector from https://dev.uniresolver.io/ + const P256_JWK: &str = r#"{ + "kty": "EC", + "crv": "P-256", + "x": "fyNYMN0976ci7xqiSdag3buk-ZCwgXU4kz9XNkBlNUI", + "y": "hW2ojTNfH7Jbi8--CJUo3OCbH3y5n91g-IMA9MLMbTU" + }"#; + const P256_FINGERPRINT: &str = "zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169"; + + // vector from https://dev.uniresolver.io/ + const P384_JWK: &str = r#"{ + "kty": "EC", + "crv": "P-384", + "x": "bKq-gg3sJmfkJGrLl93bsumOTX1NubBySttAV19y5ClWK3DxEmqPy0at5lLqBiiv", + "y": "PJQtdHnInU9SY3e8Nn9aOPoP51OFbs-FWJUsU0TGjRtZ4bnhoZXtS92wdzuAotL9" + }"#; + const P384_FINGERPRINT: &str = + "z82Lkytz3HqpWiBmt2853ZgNgNG8qVoUJnyoMvGw6ZEBktGcwUVdKpUNJHct1wvp9pXjr7Y"; + #[test] fn test_from_ed25519_jwk() { - // vector from https://w3c-ccg.github.io/did-method-key/#ed25519-x25519 - let jwk = r#"{ - "kty": "OKP", - "crv": "Ed25519", - "x": "O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik" - }"#; - let key = Key::from_jwk(jwk).unwrap(); - assert_eq!( - key.fingerprint(), - "z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp" - ); + let key = Key::from_jwk(ED25519_JWK).unwrap(); + assert_eq!(key.fingerprint(), ED25519_FINGERPRINT); } #[test] fn test_from_x25519_jwk() { - // vector from https://w3c-ccg.github.io/did-method-key/#ed25519-x25519 - let jwk = r#"{ - "kty": "OKP", - "crv": "X25519", - "x": "W_Vcc7guviK-gPNDBmevVw-uJVamQV5rMNQGUwCqlH0" - }"#; - let key = Key::from_jwk(jwk).unwrap(); - assert_eq!( - key.fingerprint(), - "z6LShs9GGnqk85isEBzzshkuVWrVKsRp24GnDuHk8QWkARMW" - ); + let key = Key::from_jwk(X25519_JWK).unwrap(); + assert_eq!(key.fingerprint(), X25519_FINGERPRINT); } #[test] fn test_from_p256_jwk() { - // vector from https://dev.uniresolver.io/ - let jwk = r#"{ - "kty": "EC", - "crv": "P-256", - "x": "fyNYMN0976ci7xqiSdag3buk-ZCwgXU4kz9XNkBlNUI", - "y": "hW2ojTNfH7Jbi8--CJUo3OCbH3y5n91g-IMA9MLMbTU" - }"#; - let key = Key::from_jwk(jwk).unwrap(); - assert_eq!( - key.fingerprint(), - "zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169" - ); + let key = Key::from_jwk(P256_JWK).unwrap(); + assert_eq!(key.fingerprint(), P256_FINGERPRINT); } #[test] fn test_from_p384_jwk() { - // vector from https://dev.uniresolver.io/ - let jwk = r#"{ - "kty": "EC", - "crv": "P-384", - "x": "bKq-gg3sJmfkJGrLl93bsumOTX1NubBySttAV19y5ClWK3DxEmqPy0at5lLqBiiv", - "y": "PJQtdHnInU9SY3e8Nn9aOPoP51OFbs-FWJUsU0TGjRtZ4bnhoZXtS92wdzuAotL9" - }"#; - let key = Key::from_jwk(jwk).unwrap(); - assert_eq!( - key.fingerprint(), - "z82Lkytz3HqpWiBmt2853ZgNgNG8qVoUJnyoMvGw6ZEBktGcwUVdKpUNJHct1wvp9pXjr7Y" - ); + let key = Key::from_jwk(P384_JWK).unwrap(); + assert_eq!(key.fingerprint(), P384_FINGERPRINT); + } + + #[test] + fn test_ed25519_to_jwk() { + let key = Key::from_fingerprint(ED25519_FINGERPRINT).unwrap(); + let jwk: Value = serde_json::from_str(&key.to_jwk().unwrap()).unwrap(); + assert_eq!(jwk, serde_json::from_str::(ED25519_JWK).unwrap()); + } + + #[test] + fn test_x25519_to_jwk() { + let key = Key::from_fingerprint(X25519_FINGERPRINT).unwrap(); + let jwk: Value = serde_json::from_str(&key.to_jwk().unwrap()).unwrap(); + assert_eq!(jwk, serde_json::from_str::(X25519_JWK).unwrap()); + } + + #[test] + fn test_p256_to_jwk() { + let key = Key::from_fingerprint(P256_FINGERPRINT).unwrap(); + let jwk: Value = serde_json::from_str(&key.to_jwk().unwrap()).unwrap(); + assert_eq!(jwk, serde_json::from_str::(P256_JWK).unwrap()); + } + + #[test] + fn test_p384_to_jwk() { + let key = Key::from_fingerprint(P384_FINGERPRINT).unwrap(); + let jwk: Value = serde_json::from_str(&key.to_jwk().unwrap()).unwrap(); + assert_eq!(jwk, serde_json::from_str::(P384_JWK).unwrap()); } } diff --git a/justfile b/justfile index 832f8bd117..9cbc5c52df 100644 --- a/justfile +++ b/justfile @@ -29,4 +29,4 @@ test-integration-aries-vcx-vdrproxy test_name="": cargo test --manifest-path="aries/aries_vcx/Cargo.toml" -F vdr_proxy_ledger,anoncreds -- --ignored {{test_name}} test-integration-did-crate test_name="": - cargo test --examples -p did_doc -p did_parser_nom -p did_resolver -p did_resolver_registry -p did_resolver_sov -p did_resolver_web -p did_key -p did_peer -F did_doc/jwk --test "*" + cargo test --examples -p did_doc -p did_parser_nom -p did_resolver -p did_resolver_registry -p did_resolver_sov -p did_resolver_web -p did_key -p did_peer -p did_jwk -F did_doc/jwk --test "*"