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

Support %-encoded characters in DID URL #1496

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 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 bindings/wasm/src/common/imported_document_lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ impl From<&ArrayIToCoreDocument> for Vec<ImportedDocumentLock> {

pub(crate) struct ImportedDocumentReadGuard<'a>(tokio::sync::RwLockReadGuard<'a, CoreDocument>);

impl<'a> AsRef<CoreDocument> for ImportedDocumentReadGuard<'a> {
impl AsRef<CoreDocument> for ImportedDocumentReadGuard<'_> {
fn as_ref(&self) -> &CoreDocument {
self.0.as_ref()
}
Expand Down
2 changes: 1 addition & 1 deletion bindings/wasm/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ fn error_chain_fmt(e: &impl std::error::Error, f: &mut std::fmt::Formatter<'_>)

struct ErrorMessage<'a, E: std::error::Error>(&'a E);

impl<'a, E: std::error::Error> Display for ErrorMessage<'a, E> {
impl<E: std::error::Error> Display for ErrorMessage<'_, E> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
error_chain_fmt(self.0, f)
}
Expand Down
2 changes: 1 addition & 1 deletion identity_credential/src/credential/jwt_serialization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ where
}

#[cfg(feature = "validator")]
impl<'credential, T> CredentialJwtClaims<'credential, T>
impl<T> CredentialJwtClaims<'_, T>
where
T: ToOwned<Owned = T> + Serialize + DeserializeOwned,
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ use super::DomainLinkageValidationResult;
use crate::utils::url_only_includes_origin;

/// A validator for a Domain Linkage Configuration and Credentials.

pub struct JwtDomainLinkageValidator<V: JwsVerifier> {
validator: JwtCredentialValidator<V>,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ where
}

#[cfg(feature = "validator")]
impl<'presentation, CRED, T> PresentationJwtClaims<'presentation, CRED, T>
impl<CRED, T> PresentationJwtClaims<'_, CRED, T>
where
CRED: ToOwned<Owned = CRED> + Serialize + DeserializeOwned + Clone,
T: ToOwned<Owned = T> + Serialize + DeserializeOwned,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ where
D: serde::Deserializer<'de>,
{
struct ExactStrVisitor(&'static str);
impl<'a> Visitor<'a> for ExactStrVisitor {
impl Visitor<'_> for ExactStrVisitor {
type Value = &'static str;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "the exact string \"{}\"", self.0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ where
D: serde::Deserializer<'de>,
{
struct ExactStrVisitor(&'static str);
impl<'a> Visitor<'a> for ExactStrVisitor {
impl Visitor<'_> for ExactStrVisitor {
type Value = &'static str;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "the exact string \"{}\"", self.0)
Expand Down
2 changes: 1 addition & 1 deletion identity_did/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ repository.workspace = true
description = "Agnostic implementation of the Decentralized Identifiers (DID) standard."

[dependencies]
did_url_parser = { version = "0.2.0", features = ["std", "serde"] }
did_url_parser = { version = "0.3.0", features = ["std", "serde"] }
form_urlencoded = { version = "1.2.0", default-features = false, features = ["alloc"] }
identity_core = { version = "=1.4.0", path = "../identity_core", default-features = false }
identity_jose = { version = "=1.4.0", path = "../identity_jose" }
Expand Down
48 changes: 43 additions & 5 deletions identity_did/src/did_url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ impl RelativeDIDUrl {
self.path = value
.filter(|s| !s.is_empty())
.map(|s| {
if s.starts_with('/') && s.chars().all(is_char_path) {
if s.starts_with('/') && is_valid_url_segment(s, is_char_path) {
Ok(s.to_owned())
} else {
Err(Error::InvalidPath)
Expand Down Expand Up @@ -138,7 +138,7 @@ impl RelativeDIDUrl {
.map(|mut s| {
// Ignore leading '?' during validation.
s = s.strip_prefix('?').unwrap_or(s);
if s.is_empty() || !s.chars().all(is_char_query) {
if s.is_empty() || !is_valid_url_segment(s, is_char_query) {
return Err(Error::InvalidQuery);
}
Ok(format!("?{s}"))
Expand Down Expand Up @@ -188,7 +188,7 @@ impl RelativeDIDUrl {
.map(|mut s| {
// Ignore leading '#' during validation.
s = s.strip_prefix('#').unwrap_or(s);
if s.is_empty() || !s.chars().all(is_char_fragment) {
if s.is_empty() || !is_valid_url_segment(s, is_char_fragment) {
return Err(Error::InvalidFragment);
}
Ok(format!("#{s}"))
Expand Down Expand Up @@ -519,8 +519,7 @@ impl KeyComparable for DIDUrl {
#[inline(always)]
#[rustfmt::skip]
pub(crate) const fn is_char_path(ch: char) -> bool {
// Allow percent encoding or not?
is_char_method_id(ch) || matches!(ch, '~' | '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ';' | '=' | '@' | '/' /* | '%' */)
is_char_method_id(ch) || matches!(ch, '~' | '!' | '$' | '&' | '\'' | '(' | ')' | '*' | '+' | ',' | ';' | '=' | '@' | '/')
}

/// Checks whether a character satisfies DID Url query constraints.
Expand All @@ -535,6 +534,33 @@ pub(crate) const fn is_char_fragment(ch: char) -> bool {
is_char_path(ch) || ch == '?'
}

pub(crate) fn is_valid_percent_encoded_char(s: &str) -> bool {
let mut chars = s.chars();
let Some('%') = chars.next() else { return false };
s.len() == 3 && chars.take(2).all(|c| c.is_ascii_hexdigit())
UMR1352 marked this conversation as resolved.
Show resolved Hide resolved
}

pub(crate) fn is_valid_url_segment<F>(segment: &str, char_predicate: F) -> bool
where
F: Fn(char) -> bool,
{
let mut chars = segment.char_indices();
while let Some((i, c)) = chars.next() {
if c == '%' {
if !is_valid_percent_encoded_char(&segment[i..]) {
return false;
}
// skip the two HEX digits
chars.next();
chars.next();
} else if !char_predicate(c) {
return false;
}
}

true
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -639,6 +665,10 @@ mod tests {
assert!(relative_url.path().is_none());
assert!(relative_url.set_path(None).is_ok());
assert!(relative_url.path().is_none());

// Percent encoded path.
assert!(relative_url.set_path(Some("/p%AAth")).is_ok());
assert_eq!(relative_url.path().unwrap(), "/p%AAth");
}

#[rustfmt::skip]
Expand Down Expand Up @@ -697,6 +727,10 @@ mod tests {
assert_eq!(relative_url.query().unwrap(), "query");
assert!(relative_url.set_query(Some("name=value&name2=value2&3=true")).is_ok());
assert_eq!(relative_url.query().unwrap(), "name=value&name2=value2&3=true");

// With percent encoded char.
assert!(relative_url.set_query(Some("qu%EEry")).is_ok());
assert_eq!(relative_url.query().unwrap(), "qu%EEry");
}

#[rustfmt::skip]
Expand Down Expand Up @@ -745,6 +779,10 @@ mod tests {
assert!(relative_url.fragment().is_none());
assert!(relative_url.set_fragment(None).is_ok());
assert!(relative_url.fragment().is_none());

// Percent encoded fragment.
assert!(relative_url.set_fragment(Some("fr%AAgm%EEnt")).is_ok());
assert_eq!(relative_url.fragment().unwrap(), "fr%AAgm%EEnt");
}

#[rustfmt::skip]
Expand Down
4 changes: 2 additions & 2 deletions identity_document/src/document/core_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,7 @@ impl CoreDocument {
&'me self,
method_query: Q,
scope: Option<MethodScope>,
) -> Option<&VerificationMethod>
) -> Option<&'me VerificationMethod>
where
Q: Into<DIDUrlQuery<'query>>,
{
Expand Down Expand Up @@ -773,7 +773,7 @@ impl CoreDocument {
/// Returns the first [`Service`] with an `id` property matching the provided `service_query`, if present.
// NOTE: This method demonstrates unexpected behavior in the edge cases where the document contains
// services whose ids are of the form <did different from this document's>#<fragment>.
pub fn resolve_service<'query, 'me, Q>(&'me self, service_query: Q) -> Option<&Service>
pub fn resolve_service<'query, 'me, Q>(&'me self, service_query: Q) -> Option<&'me Service>
where
Q: Into<DIDUrlQuery<'query>>,
{
Expand Down
4 changes: 2 additions & 2 deletions identity_document/src/utils/did_url_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use identity_did::DID;
#[repr(transparent)]
pub struct DIDUrlQuery<'query>(Cow<'query, str>);

impl<'query> DIDUrlQuery<'query> {
impl DIDUrlQuery<'_> {
/// Returns whether this query matches the given DIDUrl.
pub(crate) fn matches(&self, did_url: &DIDUrl) -> bool {
// Ensure the DID matches if included in the query.
Expand Down Expand Up @@ -81,7 +81,7 @@ impl<'query> From<&'query DIDUrl> for DIDUrlQuery<'query> {
}
}

impl<'query> From<DIDUrl> for DIDUrlQuery<'query> {
impl From<DIDUrl> for DIDUrlQuery<'_> {
fn from(other: DIDUrl) -> Self {
Self(Cow::Owned(other.to_string()))
}
Expand Down
4 changes: 2 additions & 2 deletions identity_iota_core/src/document/iota_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ impl IotaDocument {
/// Returns the first [`Service`] with an `id` property matching the provided `service_query`, if present.
// NOTE: This method demonstrates unexpected behaviour in the edge cases where the document contains
// services whose ids are of the form <did different from this document's>#<fragment>.
pub fn resolve_service<'query, 'me, Q>(&'me self, service_query: Q) -> Option<&Service>
pub fn resolve_service<'query, 'me, Q>(&'me self, service_query: Q) -> Option<&'me Service>
where
Q: Into<DIDUrlQuery<'query>>,
{
Expand All @@ -347,7 +347,7 @@ impl IotaDocument {
&'me self,
method_query: Q,
scope: Option<MethodScope>,
) -> Option<&VerificationMethod>
) -> Option<&'me VerificationMethod>
where
Q: Into<DIDUrlQuery<'query>>,
{
Expand Down
2 changes: 1 addition & 1 deletion identity_jose/src/jws/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ pub struct JwsValidationIter<'decoder, 'payload, 'signatures> {
payload: &'payload [u8],
}

impl<'decoder, 'payload, 'signatures> Iterator for JwsValidationIter<'decoder, 'payload, 'signatures> {
impl<'payload> Iterator for JwsValidationIter<'_, 'payload, '_> {
type Item = Result<JwsValidationItem<'payload>>;

fn next(&mut self) -> Option<Self::Item> {
Expand Down
4 changes: 2 additions & 2 deletions identity_jose/src/jws/encoding/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ pub(super) struct Flatten<'payload, 'unprotected> {
pub(super) signature: JwsSignature<'unprotected>,
}

impl<'payload, 'unprotected> Flatten<'payload, 'unprotected> {
impl Flatten<'_, '_> {
pub(super) fn to_json(&self) -> Result<String> {
serde_json::to_string(&self).map_err(Error::InvalidJson)
}
Expand All @@ -99,7 +99,7 @@ pub(super) struct General<'payload, 'unprotected> {
pub(super) signatures: Vec<JwsSignature<'unprotected>>,
}

impl<'payload, 'unprotected> General<'payload, 'unprotected> {
impl General<'_, '_> {
pub(super) fn to_json(&self) -> Result<String> {
serde_json::to_string(&self).map_err(Error::InvalidJson)
}
Expand Down
2 changes: 1 addition & 1 deletion identity_jose/src/jws/recipient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub struct Recipient<'a> {
pub unprotected: Option<&'a JwsHeader>,
}

impl<'a> Default for Recipient<'a> {
impl Default for Recipient<'_> {
fn default() -> Self {
Self::new()
}
Expand Down
Loading