Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for relative references in core document #1341

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()),
}
}
}
Loading