From ce8cc132ee5630b4b17d59764cc43ed3eb76305f Mon Sep 17 00:00:00 2001 From: Sebastian Wolfram Date: Fri, 22 Mar 2024 15:56:53 +0100 Subject: [PATCH] add support for relative references in core document --- identity_document/Cargo.toml | 2 +- .../src/document/core_document.rs | 155 +++++++++++++++++- identity_verification/src/error.rs | 3 + .../src/verification_method/method_ref.rs | 46 +++++- 4 files changed, 201 insertions(+), 5 deletions(-) diff --git a/identity_document/Cargo.toml b/identity_document/Cargo.toml index bebbd8f070..9632359edf 100644 --- a/identity_document/Cargo.toml +++ b/identity_document/Cargo.toml @@ -18,12 +18,12 @@ identity_did = { version = "=1.1.1", path = "../identity_did" } identity_verification = { version = "=1.1.1", path = "../identity_verification", default-features = false } indexmap = { version = "2.0", default-features = false, features = ["std", "serde"] } serde.workspace = true +serde_json.workspace = true strum.workspace = true thiserror.workspace = true [dev-dependencies] criterion = { version = "0.4.0", default-features = false, features = ["cargo_bench_support"] } -serde_json.workspace = true [[bench]] name = "deserialize_document" diff --git a/identity_document/src/document/core_document.rs b/identity_document/src/document/core_document.rs index 87fddd0fed..9fb9c6903e 100644 --- a/identity_document/src/document/core_document.rs +++ b/identity_document/src/document/core_document.rs @@ -34,7 +34,7 @@ use identity_verification::MethodRelationship; use identity_verification::MethodScope; use identity_verification::VerificationMethod; -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] #[rustfmt::skip] pub(crate) struct CoreDocumentData { @@ -88,6 +88,7 @@ impl CoreDocumentData { .map(|method_ref| match method_ref { MethodRef::Embed(_) => (method_ref.id(), true), MethodRef::Refer(_) => (method_ref.id(), false), + MethodRef::RelativeRefer(_) => (method_ref.id(), false), }) { if let Some(previous) = method_identifiers.insert(id, is_embedded) { @@ -221,6 +222,73 @@ impl CoreDocumentData { properties: current_data.properties, }) } + + /// Try parsing given [`serde_json::Value`] into set of [`MethodRef`](identity_verification::MethodRef). + fn try_parse_method_ref_set( + id: &CoreDID, + value: &Option, + ) -> std::result::Result, E> + where + E: serde::de::Error, + { + let set = match value { + // array with values (expected format if present) + Some(serde_json::Value::Array(array_value)) => array_value + .iter() + .map(|entry| MethodRef::try_from_value(entry, id).map_err(serde::de::Error::custom)) + .collect::>()?, + // rely on serde_json to generate error message for other formats + Some(non_array_value) => serde_json::from_value(non_array_value.clone()).map_err(serde::de::Error::custom)?, + // fallback if omitted + None => OrderedSet::default(), + }; + Ok(set) + } +} + +impl<'de> serde::Deserialize<'de> for CoreDocumentData { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + /// Helper struct, that resembles [`CoreDocumentData`] structure except that + /// `OrderedSet` fields are represented as [`serde_json::Value`] for parsing. + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub(crate) struct CoreDocumentDataInner { + pub(crate) id: CoreDID, + pub(crate) controller: Option>, + #[serde(default = "Default::default")] + pub(crate) also_known_as: OrderedSet, + #[serde(default = "Default::default")] + pub(crate) verification_method: OrderedSet, + pub(crate) authentication: Option, + pub(crate) assertion_method: Option, + pub(crate) key_agreement: Option, + pub(crate) capability_delegation: Option, + pub(crate) capability_invocation: Option, + #[serde(default = "Default::default")] + pub(crate) service: OrderedSet, + #[serde(flatten)] + pub(crate) properties: Object, + } + + let data = CoreDocumentDataInner::deserialize(deserializer)?; + + Ok(CoreDocumentData { + id: data.id.clone(), + controller: data.controller, + also_known_as: data.also_known_as, + verification_method: data.verification_method, + authentication: Self::try_parse_method_ref_set(&data.id, &data.authentication)?, + assertion_method: Self::try_parse_method_ref_set(&data.id, &data.assertion_method)?, + key_agreement: Self::try_parse_method_ref_set(&data.id, &data.key_agreement)?, + capability_delegation: Self::try_parse_method_ref_set(&data.id, &data.capability_delegation)?, + capability_invocation: Self::try_parse_method_ref_set(&data.id, &data.capability_invocation)?, + service: data.service, + properties: data.properties, + }) + } } /// A DID Document. @@ -234,7 +302,7 @@ pub struct CoreDocument pub(crate) data: CoreDocumentData, } -//Forward serialization to inner +// Forward serialization to inner impl Serialize for CoreDocument { fn serialize(&self, serializer: S) -> Result where @@ -250,6 +318,7 @@ macro_rules! method_ref_mut_helper { match $doc.data.$method.query_mut($query.into())? { MethodRef::Embed(method) => Some(method), MethodRef::Refer(ref did) => $doc.data.verification_method.query_mut(did), + MethodRef::RelativeRefer(ref did) => $doc.data.verification_method.query_mut(did), } }; } @@ -653,6 +722,7 @@ impl CoreDocument { match method { MethodRef::Embed(method) => Some(method), MethodRef::Refer(_) => None, + MethodRef::RelativeRefer(_) => None, } } @@ -736,7 +806,7 @@ impl CoreDocument { /// # Warning /// /// Incorrect use of this method can lead to distinct document resources being identified by the same DID URL. - // NOTE: This method demonstrates unexpected behaviour in the edge cases where the document contains methods + // NOTE: This method demonstrates unexpected behavior in the edge cases where the document contains methods // whose ids are of the form #. pub fn resolve_method_mut<'query, 'me, Q>( &'me mut self, @@ -784,6 +854,7 @@ impl CoreDocument { match method_ref { MethodRef::Embed(method) => Some(method), MethodRef::Refer(did) => self.data.verification_method.query(did), + MethodRef::RelativeRefer(did) => self.data.verification_method.query(did), } } @@ -813,6 +884,7 @@ impl CoreDocument { match method { Some(MethodRef::Embed(method)) => Some(method), Some(MethodRef::Refer(did)) => self.data.verification_method.query(&did.to_string()), + Some(MethodRef::RelativeRefer(did)) => self.data.verification_method.query(&did.to_string()), None => self.data.verification_method.query(query), } } @@ -843,6 +915,7 @@ impl CoreDocument { match method { Some(MethodRef::Embed(method)) => Some(method), Some(MethodRef::Refer(did)) => self.data.verification_method.query_mut(&did.to_string()), + Some(MethodRef::RelativeRefer(did)) => self.data.verification_method.query_mut(&did.to_string()), None => self.data.verification_method.query_mut(query), } } @@ -986,6 +1059,8 @@ impl CoreDocument { #[cfg(test)] mod tests { + use std::str::FromStr; + use identity_core::convert::FromJson; use identity_core::convert::ToJson; use identity_did::DID; @@ -1476,6 +1551,80 @@ mod tests { assert!(doc.is_ok()); } + #[test] + fn deserialize_relative_did_url() { + const ID: &str = "did:example:123"; + const AUTHENTICATION: &str = "#key-1"; + // The verification method types here are really Ed25519VerificationKey2020, changed to be compatible + // with the current version of this library. + let json_document = r###"{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "did:example:123", + "verificationMethod": [ + { + "id": "did:example:1234#key1", + "controller": "did:example:1234", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "3M5RCDjPTWPkKSN3sxUmmMqHbmRPegYP1tjcKyrDbt9J" + } + ], + "authentication": [ + "#key-1" + ] + }"###; + let doc_result: std::result::Result> = + CoreDocument::from_json(&json_document).map_err(Into::into); + + assert!(doc_result.is_ok()); + let doc = doc_result.unwrap(); + assert_eq!( + doc.authentication().first(), + Some(&MethodRef::RelativeRefer( + DIDUrl::from_str(&format!("{ID}{AUTHENTICATION}")).unwrap() + )), + ); + + let re_serialized = serde_json::to_string(&doc).unwrap(); + dbg!(&re_serialized); + } + + #[test] + fn serialize_relative_did_url() { + const AUTHENTICATION: &str = "#key-1"; + // The verification method types here are really Ed25519VerificationKey2020, changed to be compatible + // with the current version of this library. + let json_document = r###"{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "id": "did:example:123", + "verificationMethod": [ + { + "id": "did:example:1234#key1", + "controller": "did:example:1234", + "type": "Ed25519VerificationKey2018", + "publicKeyBase58": "3M5RCDjPTWPkKSN3sxUmmMqHbmRPegYP1tjcKyrDbt9J" + } + ], + "authentication": [ + "#key-1" + ] + }"###; + let doc_result: std::result::Result> = + CoreDocument::from_json(&json_document).map_err(Into::into); + assert!(doc_result.is_ok()); + let doc = doc_result.unwrap(); + + let re_serialized = serde_json::to_string(&doc).unwrap(); + let plain_json: serde_json::Value = serde_json::from_str(&re_serialized).unwrap(); + + assert_eq!(plain_json["authentication"][0].as_str(), Some(AUTHENTICATION),); + } + #[test] fn deserialize_duplicate_method_different_scopes() { const JSON_VERIFICATION_METHOD_KEY_AGREEMENT: &str = r#"{ diff --git a/identity_verification/src/error.rs b/identity_verification/src/error.rs index 97070de3bf..f8c7aeb4cb 100644 --- a/identity_verification/src/error.rs +++ b/identity_verification/src/error.rs @@ -39,4 +39,7 @@ pub enum Error { /// Caused by key material that is not a JSON Web Key. #[error("verification material format is not publicKeyJwk")] NotPublicKeyJwk, + /// Failed to deserialize [`MethodRef`](crate::MethodRef) + #[error("invalid method ref; {0}")] + InvalidMethodRef(#[from] serde_json::Error), } diff --git a/identity_verification/src/verification_method/method_ref.rs b/identity_verification/src/verification_method/method_ref.rs index 29ec574df7..568758aefe 100644 --- a/identity_verification/src/verification_method/method_ref.rs +++ b/identity_verification/src/verification_method/method_ref.rs @@ -5,19 +5,25 @@ use core::fmt::Debug; use core::fmt::Formatter; use identity_core::common::KeyComparable; +use identity_did::DID; +use serde::Serialize; +use serde::Serializer; use crate::verification_method::VerificationMethod; +use crate::Error; use identity_did::CoreDID; use identity_did::DIDUrl; /// A reference to a verification method, either a `DID` or embedded `Method`. -#[derive(Clone, PartialEq, Eq, Deserialize, Serialize)] +#[derive(Clone, PartialEq, Eq, Deserialize)] #[serde(untagged)] pub enum MethodRef { /// A [`VerificationMethod`] embedded in a verification relationship. Embed(VerificationMethod), /// A reference to a [`VerificationMethod`] in a verification relationship. Refer(DIDUrl), + /// A relative reference to a [`VerificationMethod`] in current document + RelativeRefer(DIDUrl), } impl MethodRef { @@ -26,6 +32,7 @@ impl MethodRef { match self { Self::Embed(inner) => inner.id(), Self::Refer(inner) => inner, + Self::RelativeRefer(inner) => inner, } } @@ -36,6 +43,7 @@ impl MethodRef { match self { Self::Embed(inner) => Some(inner.controller()), Self::Refer(_) => None, + Self::RelativeRefer(_) => None, } } @@ -61,6 +69,7 @@ impl MethodRef { match self { MethodRef::Embed(method) => MethodRef::Embed(method.map(f)), MethodRef::Refer(id) => MethodRef::Refer(id.map(f)), + MethodRef::RelativeRefer(id) => MethodRef::Refer(id.map(f)), } } @@ -72,6 +81,7 @@ impl MethodRef { Ok(match self { MethodRef::Embed(method) => MethodRef::Embed(method.try_map(f)?), MethodRef::Refer(id) => MethodRef::Refer(id.try_map(f)?), + MethodRef::RelativeRefer(id) => MethodRef::Refer(id.try_map(f)?), }) } @@ -86,6 +96,7 @@ impl MethodRef { match self { Self::Embed(inner) => Ok(inner), Self::Refer(_) => Err(self.into()), + Self::RelativeRefer(_) => Err(self.into()), } } @@ -100,8 +111,27 @@ impl MethodRef { match self { Self::Embed(_) => Err(self.into()), Self::Refer(inner) => Ok(inner), + Self::RelativeRefer(inner) => Ok(inner), } } + + /// Try to build instance from [`serde_json::Value`]. + pub fn try_from_value(value: &serde_json::Value, id: &CoreDID) -> Result { + let parsed = match value { + // relative references will be joined with document id + serde_json::Value::String(string_value) => { + if !string_value.starts_with("did:") { + MethodRef::RelativeRefer(id.clone().join(string_value).map_err(Error::DIDUrlConstructionError)?) + } else { + serde_json::from_value(value.clone())? + } + } + // otherwise parse as usual + _ => serde_json::from_value(value.clone())?, + }; + + Ok(parsed) + } } impl Debug for MethodRef { @@ -109,6 +139,7 @@ impl Debug for MethodRef { match self { Self::Embed(inner) => Debug::fmt(inner, f), Self::Refer(inner) => Debug::fmt(inner, f), + Self::RelativeRefer(inner) => Debug::fmt(inner, f), } } } @@ -142,3 +173,16 @@ impl KeyComparable for MethodRef { self.id() } } + +impl Serialize for MethodRef { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Self::Embed(value) => value.serialize(serializer), + Self::Refer(value) => value.serialize(serializer), + Self::RelativeRefer(value) => serializer.serialize_str(&value.url().to_string()), + } + } +}