Skip to content

Commit

Permalink
Support verification methods with the same fragment (#623)
Browse files Browse the repository at this point in the history
* Update MethodQuery to disambiguate methods with the same fragment

* Example MethodQuery tests
  • Loading branch information
cycraig authored Jan 26, 2022
1 parent 49e6fcc commit cec57bc
Show file tree
Hide file tree
Showing 2 changed files with 269 additions and 15 deletions.
233 changes: 223 additions & 10 deletions identity-did/src/verification/method_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,52 @@ use crate::did::DID;
pub struct MethodQuery<'query>(Cow<'query, str>);

impl<'query> MethodQuery<'query> {
pub(crate) fn matches(&self, did: &CoreDIDUrl) -> bool {
match self.fragment().zip(did.fragment()) {
/// Returns whether this query matches the given DIDUrl.
pub(crate) fn matches(&self, did_url: &CoreDIDUrl) -> bool {
// Ensure the DID matches if included in the query.
if let Some(did_str) = self.did_str() {
if did_str != did_url.did().as_str() {
return false;
}
}

// Compare fragments.
match self.fragment().zip(did_url.fragment()) {
Some((a, b)) => a == b,
None => false,
}
}

/// Extract the DID portion of the query if it exists.
fn did_str(&self) -> Option<&str> {
let query: &str = self.0.as_ref();
if !query.starts_with(CoreDID::SCHEME) {
return None;
}

// Find end of DID section.
// did:example:123/path?service=agent&relativeRef=/credentials#degree
let mut end_pos: usize = query.len();
end_pos = end_pos.min(query.find('?').unwrap_or(end_pos));
end_pos = end_pos.min(query.find('/').unwrap_or(end_pos));
end_pos = end_pos.min(query.find('#').unwrap_or(end_pos));
query.get(0..end_pos)
}

/// Extract the query fragment if it exists.
fn fragment(&self) -> Option<&str> {
let query = self.0.as_ref();
if query.starts_with(CoreDID::SCHEME) && !query.ends_with('#') {
// Extract the fragment from a full DID-like string
query.rfind('#').map(|index| &query[index + 1..])
} else if let Some(stripped) = query.strip_prefix('#') {
// Remove the leading `#` if it was in the query
Some(stripped)
let query: &str = self.0.as_ref();
let fragment_maybe: Option<&str> = if query.starts_with(CoreDID::SCHEME) {
// Extract the fragment from a full DID-Url-like string.
query.rfind('#').and_then(|index| query.get(index + 1..))
} else if let Some(fragment_delimiter_index) = query.rfind('#') {
// Extract the fragment from a relative DID-Url.
query.get(fragment_delimiter_index + 1..)
} else {
// Assume the entire string is a fragment.
Some(query)
}
};
fragment_maybe.filter(|fragment| !fragment.is_empty())
}
}

Expand Down Expand Up @@ -82,3 +110,188 @@ impl<'query> From<&'query Signature> for MethodQuery<'query> {
Self(Cow::Borrowed(other.verification_method()))
}
}

#[cfg(test)]
mod tests {
use std::ops::Not;

use super::*;

#[test]
fn test_did_str() {
assert!(MethodQuery::from("").did_str().is_none());
assert!(MethodQuery::from("fragment").did_str().is_none());
assert!(MethodQuery::from("#fragment").did_str().is_none());
assert!(MethodQuery::from("?query").did_str().is_none());
assert!(MethodQuery::from("/path").did_str().is_none());
assert!(MethodQuery::from("/path?query#fragment").did_str().is_none());
assert!(MethodQuery::from("method:missingscheme123").did_str().is_none());
assert!(MethodQuery::from("iota:example").did_str().is_none());
assert_eq!(
MethodQuery::from("did:iota:example").did_str(),
Some("did:iota:example")
);
assert_eq!(
MethodQuery::from("did:iota:example#fragment").did_str(),
Some("did:iota:example")
);
assert_eq!(
MethodQuery::from("did:iota:example?query").did_str(),
Some("did:iota:example")
);
assert_eq!(
MethodQuery::from("did:iota:example/path").did_str(),
Some("did:iota:example")
);
assert_eq!(
MethodQuery::from("did:iota:example/path?query#fragment").did_str(),
Some("did:iota:example")
);
assert_eq!(
MethodQuery::from("did:iota:example/path?query&relativeRef=/#fragment").did_str(),
Some("did:iota:example")
);
}

#[test]
fn test_fragment() {
assert!(MethodQuery::from("").fragment().is_none());
assert_eq!(MethodQuery::from("fragment").fragment(), Some("fragment"));
assert_eq!(MethodQuery::from("#fragment").fragment(), Some("fragment"));
assert_eq!(MethodQuery::from("/path?query#fragment").fragment(), Some("fragment"));
assert!(MethodQuery::from("did:iota:example").fragment().is_none());
assert_eq!(
MethodQuery::from("did:iota:example#fragment").fragment(),
Some("fragment")
);
assert!(MethodQuery::from("did:iota:example?query").fragment().is_none());
assert!(MethodQuery::from("did:iota:example/path").fragment().is_none());
assert_eq!(
MethodQuery::from("did:iota:example/path?query#fragment").fragment(),
Some("fragment")
);
assert_eq!(
MethodQuery::from("did:iota:example/path?query&relativeRef=/#fragment").fragment(),
Some("fragment")
);
}

#[test]
fn test_matches() {
let did_base: CoreDIDUrl = CoreDIDUrl::parse("did:iota:example").unwrap();
let did_path: CoreDIDUrl = CoreDIDUrl::parse("did:iota:example/path").unwrap();
let did_query: CoreDIDUrl = CoreDIDUrl::parse("did:iota:example?query").unwrap();
let did_fragment: CoreDIDUrl = CoreDIDUrl::parse("did:iota:example#fragment").unwrap();
let did_different_fragment: CoreDIDUrl = CoreDIDUrl::parse("did:iota:example#differentfragment").unwrap();
let did_url: CoreDIDUrl = CoreDIDUrl::parse("did:iota:example/path?query#fragment").unwrap();
let did_url_complex: CoreDIDUrl = CoreDIDUrl::parse("did:iota:example/path?query&relativeRef=/#fragment").unwrap();

// INVALID: empty query should not match anything.
{
let query_empty = MethodQuery::from("");
assert!(query_empty.matches(&did_base).not());
assert!(query_empty.matches(&did_path).not());
assert!(query_empty.matches(&did_query).not());
assert!(query_empty.matches(&did_fragment).not());
assert!(query_empty.matches(&did_different_fragment).not());
assert!(query_empty.matches(&did_url).not());
assert!(query_empty.matches(&did_url_complex).not());
}

// VALID: query with only a fragment should match the same fragment.
{
let query_fragment_only = MethodQuery::from("fragment");
assert!(query_fragment_only.matches(&did_base).not());
assert!(query_fragment_only.matches(&did_path).not());
assert!(query_fragment_only.matches(&did_query).not());
assert!(query_fragment_only.matches(&did_fragment));
assert!(query_fragment_only.matches(&did_different_fragment).not());
assert!(query_fragment_only.matches(&did_url));
assert!(query_fragment_only.matches(&did_url_complex));
}

// VALID: query with differentfragment should only match the same fragment.
{
let query_different_fragment = MethodQuery::from("differentfragment");
assert!(query_different_fragment.matches(&did_base).not());
assert!(query_different_fragment.matches(&did_path).not());
assert!(query_different_fragment.matches(&did_query).not());
assert!(query_different_fragment.matches(&did_fragment).not());
assert!(query_different_fragment.matches(&did_different_fragment));
assert!(query_different_fragment.matches(&did_url).not());
assert!(query_different_fragment.matches(&did_url_complex).not());
}

// VALID: query with a #fragment should match the same fragment.
{
let query_fragment_delimiter = MethodQuery::from("#fragment");
assert!(query_fragment_delimiter.matches(&did_base).not());
assert!(query_fragment_delimiter.matches(&did_path).not());
assert!(query_fragment_delimiter.matches(&did_query).not());
assert!(query_fragment_delimiter.matches(&did_fragment));
assert!(query_fragment_delimiter.matches(&did_different_fragment).not());
assert!(query_fragment_delimiter.matches(&did_url));
assert!(query_fragment_delimiter.matches(&did_url_complex));
}

// VALID: query with a relative DID Url should match the same fragment.
{
let query_relative_did_url = MethodQuery::from("/path?query#fragment");
assert!(query_relative_did_url.matches(&did_base).not());
assert!(query_relative_did_url.matches(&did_path).not());
assert!(query_relative_did_url.matches(&did_query).not());
assert!(query_relative_did_url.matches(&did_fragment));
assert!(query_relative_did_url.matches(&did_different_fragment).not());
assert!(query_relative_did_url.matches(&did_url));
assert!(query_relative_did_url.matches(&did_url_complex));
}

// INVALID: query with DID and no fragment should not match anything.
{
let query_did = MethodQuery::from("did:iota:example");
assert!(query_did.matches(&did_base).not());
assert!(query_did.matches(&did_path).not());
assert!(query_did.matches(&did_query).not());
assert!(query_did.matches(&did_fragment).not());
assert!(query_did.matches(&did_different_fragment).not());
assert!(query_did.matches(&did_url).not());
assert!(query_did.matches(&did_url_complex).not());
}

// VALID: query with a DID fragment should match the same fragment.
{
let query_did_fragment = MethodQuery::from("did:iota:example#fragment");
assert!(query_did_fragment.matches(&did_base).not());
assert!(query_did_fragment.matches(&did_path).not());
assert!(query_did_fragment.matches(&did_query).not());
assert!(query_did_fragment.matches(&did_fragment));
assert!(query_did_fragment.matches(&did_different_fragment).not());
assert!(query_did_fragment.matches(&did_url));
assert!(query_did_fragment.matches(&did_url_complex));
}

// VALID: query with a DID Url with a fragment should match the same fragment.
{
let query_did_fragment = MethodQuery::from("did:iota:example/path?query#fragment");
assert!(query_did_fragment.matches(&did_base).not());
assert!(query_did_fragment.matches(&did_path).not());
assert!(query_did_fragment.matches(&did_query).not());
assert!(query_did_fragment.matches(&did_fragment));
assert!(query_did_fragment.matches(&did_different_fragment).not());
assert!(query_did_fragment.matches(&did_url));
assert!(query_did_fragment.matches(&did_url_complex));
}

// VALID: query with a complex DID Url with a fragment should match the same fragment.
{
let query_did_fragment = MethodQuery::from("did:iota:example/path?query&relativeRef=/#fragment");
assert!(query_did_fragment.matches(&did_base).not());
assert!(query_did_fragment.matches(&did_path).not());
assert!(query_did_fragment.matches(&did_query).not());
assert!(query_did_fragment.matches(&did_fragment));
assert!(query_did_fragment.matches(&did_different_fragment).not());
assert!(query_did_fragment.matches(&did_url));
assert!(query_did_fragment.matches(&did_url_complex));
}
}
}
51 changes: 46 additions & 5 deletions identity-iota/src/document/iota_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1040,11 +1040,7 @@ mod tests {
// Ed25519 signature.
let expected = IotaVerificationMethod::try_from_core(
VerificationMethod::builder(Default::default())
.id(
"did:iota:HGE4tecHWL2YiZv5qAGtH7gaeQcaz2Z1CR15GWmMjY1M#sign-0"
.parse()
.unwrap(),
)
.id(DID_METHOD_ID.parse().unwrap())
.controller(valid_did())
.key_type(MethodType::Ed25519VerificationKey2018)
.key_data(MethodData::PublicKeyMultibase(
Expand Down Expand Up @@ -1115,6 +1111,51 @@ mod tests {
assert!(IotaDocument::verify_document(&document, &document).is_ok());
}

#[test]
fn test_sign_self_embedded_controller_method_with_same_fragment() {
let keypair: KeyPair = generate_testkey();
let mut document: IotaDocument = IotaDocument::new(&keypair).unwrap();
assert!(IotaDocument::verify_document(&document, &document).is_err());

// Add a new signing method from a controller DID Document with the SAME FRAGMENT
// as the default signing method.
let controller_keypair: KeyPair = KeyPair::new(KeyType::Ed25519).unwrap();
let controller_method: IotaVerificationMethod =
IotaVerificationMethod::from_keypair(&controller_keypair, IotaDocument::DEFAULT_METHOD_FRAGMENT).unwrap();
document
.insert_method(controller_method.clone(), MethodScope::capability_invocation())
.unwrap();

// VALID - resolving the fragment alone should return the first matching method in the list.
let default_signing_method: &IotaVerificationMethod = document.default_signing_method().unwrap();
assert_eq!(
document.resolve_method(IotaDocument::DEFAULT_METHOD_FRAGMENT).unwrap(),
default_signing_method
);
// VALID - resolving the entire id should return the exact method.
assert_eq!(
document.resolve_method(default_signing_method.id()).unwrap(),
default_signing_method
);
assert_eq!(
document.resolve_method(controller_method.id()).unwrap(),
&controller_method
);

// INVALID - sign with the controller's private key referencing only the fragment.
// Fails since both sign_self and verify_document resolve the wrong method.
document
.sign_self(controller_keypair.private(), IotaDocument::DEFAULT_METHOD_FRAGMENT)
.unwrap();
assert!(IotaDocument::verify_document(&document, &document).is_err());

// VALID - sign with the controller's private key referencing the full DID-Url of the method.
document
.sign_self(controller_keypair.private(), controller_method.id())
.unwrap();
assert!(IotaDocument::verify_document(&document, &document).is_ok());
}

#[test]
fn test_sign_self_fails() {
fn generate_document() -> (IotaDocument, KeyPair) {
Expand Down

0 comments on commit cec57bc

Please sign in to comment.