Skip to content

Commit

Permalink
add support for relative references in core document
Browse files Browse the repository at this point in the history
  • Loading branch information
wulfraem committed Mar 22, 2024
1 parent 4a144a3 commit ce8cc13
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 5 deletions.
2 changes: 1 addition & 1 deletion identity_document/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
155 changes: 152 additions & 3 deletions identity_document/src/document/core_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<E>(
id: &CoreDID,
value: &Option<serde_json::Value>,
) -> std::result::Result<OrderedSet<MethodRef>, 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::<std::result::Result<_, E>>()?,
// 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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
/// Helper struct, that resembles [`CoreDocumentData`] structure except that
/// `OrderedSet<MethodRef>` 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<OneOrSet<CoreDID>>,
#[serde(default = "Default::default")]
pub(crate) also_known_as: OrderedSet<Url>,
#[serde(default = "Default::default")]
pub(crate) verification_method: OrderedSet<VerificationMethod>,
pub(crate) authentication: Option<serde_json::Value>,
pub(crate) assertion_method: Option<serde_json::Value>,
pub(crate) key_agreement: Option<serde_json::Value>,
pub(crate) capability_delegation: Option<serde_json::Value>,
pub(crate) capability_invocation: Option<serde_json::Value>,
#[serde(default = "Default::default")]
pub(crate) service: OrderedSet<Service>,
#[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.
Expand All @@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
Expand All @@ -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),
}
};
}
Expand Down Expand Up @@ -653,6 +722,7 @@ impl CoreDocument {
match method {
MethodRef::Embed(method) => Some(method),
MethodRef::Refer(_) => None,
MethodRef::RelativeRefer(_) => None,
}
}

Expand Down Expand Up @@ -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 <did different from this document's>#<fragment>.
pub fn resolve_method_mut<'query, 'me, Q>(
&'me mut self,
Expand Down Expand Up @@ -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),
}
}

Expand Down Expand Up @@ -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),
}
}
Expand Down Expand Up @@ -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),
}
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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, Box<dyn std::error::Error>> =
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, Box<dyn std::error::Error>> =
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#"{
Expand Down
3 changes: 3 additions & 0 deletions identity_verification/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
46 changes: 45 additions & 1 deletion identity_verification/src/verification_method/method_ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -26,6 +32,7 @@ impl MethodRef {
match self {
Self::Embed(inner) => inner.id(),
Self::Refer(inner) => inner,
Self::RelativeRefer(inner) => inner,
}
}

Expand All @@ -36,6 +43,7 @@ impl MethodRef {
match self {
Self::Embed(inner) => Some(inner.controller()),
Self::Refer(_) => None,
Self::RelativeRefer(_) => None,
}
}

Expand All @@ -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)),
}
}

Expand All @@ -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)?),
})
}

Expand All @@ -86,6 +96,7 @@ impl MethodRef {
match self {
Self::Embed(inner) => Ok(inner),
Self::Refer(_) => Err(self.into()),
Self::RelativeRefer(_) => Err(self.into()),
}
}

Expand All @@ -100,15 +111,35 @@ 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<MethodRef, Error> {
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 {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
match self {
Self::Embed(inner) => Debug::fmt(inner, f),
Self::Refer(inner) => Debug::fmt(inner, f),
Self::RelativeRefer(inner) => Debug::fmt(inner, f),
}
}
}
Expand Down Expand Up @@ -142,3 +173,16 @@ impl KeyComparable for MethodRef {
self.id()
}
}

impl Serialize for MethodRef {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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()),
}
}
}

0 comments on commit ce8cc13

Please sign in to comment.