diff --git a/tinycloud-auth/src/identity.rs b/tinycloud-auth/src/identity.rs new file mode 100644 index 0000000..4c9d9dc --- /dev/null +++ b/tinycloud-auth/src/identity.rs @@ -0,0 +1,182 @@ +use crate::cacaos::siwe::encode_eip55; +use thiserror::Error; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PkhDid { + pub chain_id: u64, + pub address: String, +} + +#[derive(Error, Debug, Clone, PartialEq, Eq)] +pub enum IdentityError { + #[error("invalid EIP-155 address prefix")] + InvalidAddressPrefix, + #[error("invalid EIP-155 address length: expected 40, got {0}")] + InvalidAddressLength(usize), + #[error("invalid EIP-155 address character at {index}: {character}")] + InvalidAddressChar { index: usize, character: char }, + #[error("invalid EIP-155 chain ID")] + InvalidChainId, + #[error("invalid did:pkh:eip155 DID")] + InvalidPkhDid, + #[error("invalid DID")] + InvalidDid, +} + +fn hex_nibble(index: usize, c: char) -> Result { + match c { + '0'..='9' => Ok(c as u8 - b'0'), + 'a'..='f' => Ok(c as u8 - b'a' + 10), + 'A'..='F' => Ok(c as u8 - b'A' + 10), + _ => Err(IdentityError::InvalidAddressChar { + index, + character: c, + }), + } +} + +pub fn canonicalize_eip155_address(address: &str) -> Result { + let raw = address + .strip_prefix("0x") + .ok_or(IdentityError::InvalidAddressPrefix)?; + if raw.len() != 40 { + return Err(IdentityError::InvalidAddressLength(raw.len())); + } + + let mut bytes = [0u8; 20]; + let mut chars = raw.chars().enumerate(); + for byte in &mut bytes { + let (hi_index, hi) = chars.next().ok_or(IdentityError::InvalidPkhDid)?; + let (lo_index, lo) = chars.next().ok_or(IdentityError::InvalidPkhDid)?; + *byte = (hex_nibble(hi_index, hi)? << 4) | hex_nibble(lo_index, lo)?; + } + + Ok(format!("0x{}", encode_eip55(&bytes))) +} + +pub fn parse_pkh_did(did: &str) -> Result, IdentityError> { + let Some(rest) = did.strip_prefix("did:pkh:eip155:") else { + return Ok(None); + }; + let (chain_id, address) = rest.split_once(':').ok_or(IdentityError::InvalidPkhDid)?; + if address.contains(':') { + return Err(IdentityError::InvalidPkhDid); + } + let chain_id = chain_id + .parse::() + .ok() + .filter(|id| *id > 0) + .ok_or(IdentityError::InvalidChainId)?; + + Ok(Some(PkhDid { + chain_id, + address: canonicalize_eip155_address(address)?, + })) +} + +pub fn canonicalize_did(did: &str) -> Result { + if let Some(pkh) = parse_pkh_did(did)? { + return Ok(format!("did:pkh:eip155:{}:{}", pkh.chain_id, pkh.address)); + } + if did + .strip_prefix("did:") + .and_then(|rest| rest.split_once(':')) + .is_some() + { + return Ok(did.to_string()); + } + Err(IdentityError::InvalidDid) +} + +pub fn canonicalize_did_url(did_url: &str) -> Result { + match did_url.split_once('#') { + Some((did, fragment)) => Ok(format!("{}#{}", canonicalize_did(did)?, fragment)), + None => canonicalize_did(did_url), + } +} + +pub fn principal_did(did_url: &str) -> Result { + let did = did_url.split_once('#').map_or(did_url, |(did, _)| did); + canonicalize_did(did) +} + +pub fn canonicalize_principal(principal: &str) -> Result { + if principal.starts_with("did:") { + canonicalize_did(principal) + } else { + Ok(principal.to_string()) + } +} + +pub fn did_principal_matches(actual: &str, expected: &str) -> bool { + match (principal_did(actual), principal_did(expected)) { + (Ok(a), Ok(b)) => a == b, + _ => actual == expected, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const LOWER: &str = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"; + const CHECKSUM: &str = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"; + + #[test] + fn canonicalizes_eip155_address() { + assert_eq!(canonicalize_eip155_address(LOWER).unwrap(), CHECKSUM); + assert_eq!( + canonicalize_eip155_address("0xF39fd6e51aad88f6f4ce6ab8827279cfffb92266").unwrap(), + CHECKSUM + ); + } + + #[test] + fn canonicalizes_pkh_did() { + assert_eq!( + canonicalize_did(&format!("did:pkh:eip155:1:{LOWER}")).unwrap(), + format!("did:pkh:eip155:1:{CHECKSUM}") + ); + assert_eq!( + parse_pkh_did(&format!("did:pkh:eip155:1:{LOWER}")) + .unwrap() + .unwrap(), + PkhDid { + chain_id: 1, + address: CHECKSUM.to_string(), + } + ); + } + + #[test] + fn preserves_other_did_methods() { + assert_eq!( + canonicalize_did("did:key:z6MkExampleAbcd").unwrap(), + "did:key:z6MkExampleAbcd" + ); + assert!(!did_principal_matches( + "did:key:z6MkExampleAbcd", + "did:key:z6MkExampleabcd" + )); + } + + #[test] + fn canonicalizes_did_urls_and_principals() { + assert_eq!( + canonicalize_did_url(&format!("did:pkh:eip155:1:{LOWER}#controller")).unwrap(), + format!("did:pkh:eip155:1:{CHECKSUM}#controller") + ); + assert!(did_principal_matches( + &format!("did:pkh:eip155:1:{LOWER}#controller"), + &format!("did:pkh:eip155:1:{CHECKSUM}#other") + )); + } + + #[test] + fn rejects_invalid_supported_pkh_dids() { + assert_eq!( + canonicalize_did("did:pkh:eip155:1:0x1234").unwrap_err(), + IdentityError::InvalidAddressLength(4) + ); + } +} diff --git a/tinycloud-auth/src/lib.rs b/tinycloud-auth/src/lib.rs index 0d78298..c362a6e 100644 --- a/tinycloud-auth/src/lib.rs +++ b/tinycloud-auth/src/lib.rs @@ -1,4 +1,5 @@ pub mod authorization; +pub mod identity; pub mod protocol; pub mod resolver; pub mod resource; diff --git a/tinycloud-auth/src/resource.rs b/tinycloud-auth/src/resource.rs index ac10415..8cdc09c 100644 --- a/tinycloud-auth/src/resource.rs +++ b/tinycloud-auth/src/resource.rs @@ -6,6 +6,7 @@ use serde::Serialize; use serde_with::{DeserializeFromStr, SerializeDisplay}; use ssi::dids::{DIDBuf, DID}; +use crate::identity::canonicalize_did; use std::{convert::TryFrom, fmt, str::FromStr}; use thiserror::Error; @@ -294,6 +295,12 @@ pub enum KRIParseError { UriStringParse(#[from] iri_string::validate::Error), #[error("Invalid DID string: {0}")] DidParse(#[from] ssi::dids::InvalidDID), + #[error(transparent)] + Identity(#[from] crate::identity::IdentityError), +} + +fn did_from_suffix(suffix: &str) -> Result { + Ok(canonicalize_did(&format!("did:{suffix}"))?.try_into()?) } impl TryFrom<&UriStr> for SpaceId { @@ -315,10 +322,7 @@ impl TryFrom<&UriStr> for SpaceId { Some((suf, name)) } }) { - Ok(Self::new( - ["did:", suf].concat().try_into()?, - Name(name.to_string()), - )) + Ok(Self::new(did_from_suffix(suf)?, Name(name.to_string()))) } else { Err(KRIParseError::IncorrectForm) } @@ -357,13 +361,12 @@ impl TryFrom<&UriStr> for ResourceId { }) { Ok( - SpaceId::new(["did:", suf].concat().try_into()?, Name(name.to_string())) - .to_resource( - Service(service.to_string()), - path.map(|p| Path(p.to_string())), - uri.query().map(|q| q.into()), - uri.fragment().map(|q| q.into()), - ), + SpaceId::new(did_from_suffix(suf)?, Name(name.to_string())).to_resource( + Service(service.to_string()), + path.map(|p| Path(p.to_string())), + uri.query().map(|q| q.into()), + uri.fragment().map(|q| q.into()), + ), ) } else { Err(KRIParseError::IncorrectForm) @@ -446,6 +449,28 @@ mod tests { .unwrap(); } + #[test] + fn canonicalizes_pkh_eip155_addresses() { + let space: SpaceId = + "tinycloud:pkh:eip155:1:0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266:default" + .parse() + .unwrap(); + + assert_eq!( + space.to_string(), + "tinycloud:pkh:eip155:1:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266:default" + ); + + let resource: ResourceId = + "tinycloud:pkh:eip155:1:0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266:default/kv/path" + .parse() + .unwrap(); + assert_eq!( + resource.to_string(), + "tinycloud:pkh:eip155:1:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266:default/kv/path" + ); + } + #[test] fn roundtrip() { let resource_uri: String = "tinycloud:ens:example.eth:ns0/kv/prefix#list".into(); diff --git a/tinycloud-core/src/db.rs b/tinycloud-core/src/db.rs index 83d3c4d..a429641 100644 --- a/tinycloud-core/src/db.rs +++ b/tinycloud-core/src/db.rs @@ -24,6 +24,7 @@ use std::collections::{HashMap, HashSet}; use time::{format_description::well_known::Rfc3339, OffsetDateTime}; use tinycloud_auth::{ authorization::{EncodingError, TinyCloudDelegation}, + identity::{canonicalize_did, did_principal_matches}, resource::{Path, SpaceId}, }; @@ -1282,14 +1283,16 @@ async fn get_valid_delegations( /// /// Returns the original DID if it's already a PKH DID or if no delegation chain is found. async fn resolve_pkh_did(db: &C, did: &str) -> Result { + let canonical_did = canonicalize_did(did).unwrap_or_else(|_| did.to_string()); + // If already a PKH DID, return it directly - if did.starts_with("did:pkh:") { - return Ok(did.to_string()); + if canonical_did.starts_with("did:pkh:") { + return Ok(canonical_did); } // Look for a delegation where this DID is the delegatee // The delegator would be the next step up in the chain - let mut current_did = did.to_string(); + let mut current_did = canonical_did.clone(); let mut visited = std::collections::HashSet::new(); loop { @@ -1308,10 +1311,10 @@ async fn resolve_pkh_did(db: &C, did: &str) -> Result { // Found a parent - check if it's a PKH DID if del.delegator.starts_with("did:pkh:") { - return Ok(del.delegator); + return Ok(canonicalize_did(&del.delegator).unwrap_or(del.delegator)); } // Continue up the chain - current_did = del.delegator; + current_did = canonicalize_did(&del.delegator).unwrap_or(del.delegator); } None => { // No parent found - return what we have @@ -1321,7 +1324,7 @@ async fn resolve_pkh_did(db: &C, did: &str) -> Result return None, - Some("received") if del.delegatee != pkh_did => return None, + Some("created") if !did_principal_matches(&del.delegator, &pkh_did) => { + return None; + } + Some("received") if !did_principal_matches(&del.delegatee, &pkh_did) => { + return None; + } _ => {} } diff --git a/tinycloud-core/src/encryption_network/network_id.rs b/tinycloud-core/src/encryption_network/network_id.rs index 7ff5fe4..77e522a 100644 --- a/tinycloud-core/src/encryption_network/network_id.rs +++ b/tinycloud-core/src/encryption_network/network_id.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; use std::fmt; use std::str::FromStr; use thiserror::Error; +use tinycloud_auth::identity::canonicalize_principal; const NETWORK_ID_PREFIX: &str = "urn:tinycloud:encryption:"; @@ -22,6 +23,8 @@ pub enum NetworkIdError { MissingSeparator, #[error("network name may not contain ':' or '/'")] InvalidName, + #[error("invalid owner principal: {0}")] + InvalidPrincipal(String), } /// Owned, validated network id. Round-trips through [`Display`] and [`FromStr`]. @@ -48,6 +51,8 @@ impl NetworkId { if name.contains(':') || name.contains('/') { return Err(NetworkIdError::InvalidName); } + let owner_did = canonicalize_principal(&owner_did) + .map_err(|err| NetworkIdError::InvalidPrincipal(err.to_string()))?; Ok(Self { owner_did, name }) } @@ -146,6 +151,22 @@ mod tests { assert_eq!(err, NetworkIdError::InvalidName); } + #[test] + fn canonicalizes_pkh_principal() { + let id: NetworkId = + "urn:tinycloud:encryption:did:pkh:eip155:1:0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266:default" + .parse() + .unwrap(); + assert_eq!( + id.owner_did(), + "did:pkh:eip155:1:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + ); + assert_eq!( + id.to_string(), + "urn:tinycloud:encryption:did:pkh:eip155:1:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266:default" + ); + } + #[test] fn rejects_no_separator() { let err: Result = "urn:tinycloud:encryption:standalone".parse(); diff --git a/tinycloud-core/src/encryption_network/service.rs b/tinycloud-core/src/encryption_network/service.rs index 9996f26..c2d860c 100644 --- a/tinycloud-core/src/encryption_network/service.rs +++ b/tinycloud-core/src/encryption_network/service.rs @@ -17,6 +17,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use thiserror::Error; use time::{format_description::well_known::Rfc3339, OffsetDateTime}; +use tinycloud_auth::identity::did_principal_matches; use crate::hash::hash; use crate::keys::{public_key_to_did_key, Keypair, PublicKey}; @@ -40,6 +41,13 @@ use super::types::{ const DEFAULT_INVOCATION_TTL_SECONDS: i64 = 300; +fn network_id_matches(actual: &str, expected: &NetworkId) -> bool { + actual + .parse::() + .map(|actual| &actual == expected) + .unwrap_or_else(|_| actual == expected.to_string()) +} + #[derive(Debug, Error)] pub enum EncryptionServiceError { #[error("db error: {0}")] @@ -181,7 +189,7 @@ impl EncryptionService { let model = encryption_network::ActiveModel { network_id: Set(network_id.to_string()), - owner_did: Set(req.owner_did.clone()), + owner_did: Set(network_id.owner_did().to_string()), name: Set(req.name.clone()), alg: Set(generated.alg.clone()), key_version: Set(1), @@ -218,8 +226,8 @@ impl EncryptionService { ceremony_active.update(&self.db).await?; Ok(NetworkDescriptor { - network_id, - owner_did: req.owner_did, + network_id: network_id.clone(), + owner_did: network_id.owner_did().to_string(), name: req.name, members: vec![NetworkMemberDescriptor { node_id: self.node_did.clone(), @@ -349,10 +357,10 @@ impl EncryptionService { .iter() .find(|c| c.can == action) .ok_or(EncryptionServiceError::Unauthorized)?; - if cap.with != network_id.to_string() { + if !network_id_matches(&cap.with, network_id) { return Err(EncryptionServiceError::NetworkMismatch); } - if invocation.issuer != network_id.owner_did() { + if !did_principal_matches(&invocation.issuer, network_id.owner_did()) { return Err(EncryptionServiceError::OwnerMismatch); } let expected_body_hash = canonical_hash(body_value); @@ -399,7 +407,7 @@ impl EncryptionService { .iter() .find(|c| c.ability.to_string() == action) .ok_or(EncryptionServiceError::Unauthorized)?; - if cap.resource.to_string() != network_id.to_string() { + if !network_id_matches(&cap.resource.to_string(), network_id) { return Err(EncryptionServiceError::NetworkMismatch); } let expected_body_hash = canonical_hash(body_value); @@ -457,10 +465,10 @@ impl EncryptionService { .iter() .find(|c| c.can == DECRYPT_ACTION) .ok_or(EncryptionServiceError::Unauthorized)?; - if cap.with != network_id.to_string() { + if !network_id_matches(&cap.with, network_id) { return Err(EncryptionServiceError::NetworkMismatch); } - if invocation.issuer != network_id.owner_did() { + if !did_principal_matches(&invocation.issuer, network_id.owner_did()) { return Err(EncryptionServiceError::OwnerMismatch); } @@ -625,7 +633,7 @@ impl EncryptionService { .iter() .find(|c| c.ability.to_string() == DECRYPT_ACTION) .ok_or(EncryptionServiceError::Unauthorized)?; - if cap.resource.to_string() != network_id.to_string() { + if !network_id_matches(&cap.resource.to_string(), network_id) { return Err(EncryptionServiceError::NetworkMismatch); } diff --git a/tinycloud-core/src/models/delegation.rs b/tinycloud-core/src/models/delegation.rs index f83f49f..46f6cc8 100644 --- a/tinycloud-core/src/models/delegation.rs +++ b/tinycloud-core/src/models/delegation.rs @@ -5,7 +5,9 @@ use crate::types::{Ability, Facts, Resource}; use crate::{events::Delegation, models::*, relationships::*, util}; use sea_orm::{entity::prelude::*, sea_query::OnConflict, ConnectionTrait}; use time::OffsetDateTime; -use tinycloud_auth::{authorization::TinyCloudDelegation, ssi::dids::AnyDidMethod}; +use tinycloud_auth::{ + authorization::TinyCloudDelegation, identity::did_principal_matches, ssi::dids::AnyDidMethod, +}; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "delegation")] @@ -264,7 +266,7 @@ fn is_root_authority(cap: &util::Capability, delegator: &str) -> bool { if cap .resource .space() - .map(|o| o.did().as_str() == delegator) + .map(|o| did_principal_matches(o.did().as_str(), delegator)) .unwrap_or(false) { return true; @@ -274,7 +276,7 @@ fn is_root_authority(cap: &util::Capability, delegator: &str) -> bool { Resource::Other(uri) => uri .as_str() .parse::() - .map(|network_id| network_id.owner_did() == delegator) + .map(|network_id| did_principal_matches(network_id.owner_did(), delegator)) .unwrap_or(false), Resource::TinyCloud(_) => false, } diff --git a/tinycloud-core/src/models/invocation.rs b/tinycloud-core/src/models/invocation.rs index e49ac25..ce64c21 100644 --- a/tinycloud-core/src/models/invocation.rs +++ b/tinycloud-core/src/models/invocation.rs @@ -15,7 +15,10 @@ use sea_orm::{ use serde::Serialize; use std::collections::HashMap; use time::{format_description::well_known::Rfc3339, OffsetDateTime}; -use tinycloud_auth::{authorization::TinyCloudInvocation, resource::Path, ssi::dids::AnyDidMethod}; +use tinycloud_auth::{ + authorization::TinyCloudInvocation, identity::did_principal_matches, resource::Path, + ssi::dids::AnyDidMethod, +}; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "invocation")] @@ -144,9 +147,7 @@ async fn validate( // check parent identifies correct invoker for (p, _) in &parents { - if p.delegatee != invocation.invoker - && !invocation.invoker.starts_with(&p.delegatee) - { + if !did_principal_matches(&p.delegatee, &invocation.invoker) { return Err( InvocationError::UnauthorizedInvoker(invocation.invoker.clone()).into(), ); @@ -186,7 +187,7 @@ fn is_root_authority(cap: &util::Capability, invoker: &str) -> bool { if cap .resource .space() - .map(|o| o.did().as_str() == invoker) + .map(|o| did_principal_matches(o.did().as_str(), invoker)) .unwrap_or(false) { return true; @@ -196,7 +197,7 @@ fn is_root_authority(cap: &util::Capability, invoker: &str) -> bool { Resource::Other(uri) => uri .as_str() .parse::() - .map(|network_id| network_id.owner_did() == invoker) + .map(|network_id| did_principal_matches(network_id.owner_did(), invoker)) .unwrap_or(false), Resource::TinyCloud(_) => false, } diff --git a/tinycloud-core/src/models/revocation.rs b/tinycloud-core/src/models/revocation.rs index f3e512e..6260407 100644 --- a/tinycloud-core/src/models/revocation.rs +++ b/tinycloud-core/src/models/revocation.rs @@ -2,7 +2,7 @@ use super::super::{events::Revocation, models::*, relationships::*}; use crate::hash::{hash, Hash}; use sea_orm::{entity::prelude::*, sea_query::OnConflict, ConnectionTrait}; use time::OffsetDateTime; -use tinycloud_auth::authorization::TinyCloudRevocation; +use tinycloud_auth::{authorization::TinyCloudRevocation, identity::did_principal_matches}; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "revocation")] @@ -91,7 +91,7 @@ pub(crate) async fn process( .ok_or(RevocationError::MissingParents)?; // check the revoker is also the delegator - if delegation.delegator != r.revoker { + if !did_principal_matches(&delegation.delegator, &r.revoker) { return Err(RevocationError::UnauthorizedRevoker(r.revoker).into()); }; diff --git a/tinycloud-core/src/types/resource.rs b/tinycloud-core/src/types/resource.rs index 7627ab2..c02698d 100644 --- a/tinycloud-core/src/types/resource.rs +++ b/tinycloud-core/src/types/resource.rs @@ -10,6 +10,8 @@ use tinycloud_auth::resource::{ KRIParseError, ResourceId, SpaceId, }; +const ENCRYPTION_NETWORK_PREFIX: &str = "urn:tinycloud:encryption:"; + #[derive(Serialize, Deserialize, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] #[serde(untagged)] pub enum Resource { @@ -35,6 +37,12 @@ impl Resource { ) { (Ok(a), Ok(b)) => a == b, (Ok(_), Err(_)) | (Err(_), Ok(_)) => false, + (Err(_), Err(_)) + if a.as_str().starts_with(ENCRYPTION_NETWORK_PREFIX) + || b.as_str().starts_with(ENCRYPTION_NETWORK_PREFIX) => + { + false + } (Err(_), Err(_)) => a.as_str().starts_with(b.as_str()), } } @@ -186,19 +194,27 @@ mod tests { #[test] fn encryption_network_resources_extend_only_exact_network_ids() { - let default: Resource = "urn:tinycloud:encryption:did:pkh:eip155:1:0xabc:default" + let default: Resource = + "urn:tinycloud:encryption:did:pkh:eip155:1:0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266:default" .parse() .unwrap(); - let default_again: Resource = "urn:tinycloud:encryption:did:pkh:eip155:1:0xabc:default" + let default_again: Resource = + "urn:tinycloud:encryption:did:pkh:eip155:1:0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266:default" .parse() .unwrap(); - let default_evil: Resource = "urn:tinycloud:encryption:did:pkh:eip155:1:0xabc:default-evil" + let default_evil: Resource = + "urn:tinycloud:encryption:did:pkh:eip155:1:0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266:default-evil" + .parse() + .unwrap(); + let malformed_network: Resource = "urn:tinycloud:encryption:did:pkh:eip155:1:0xabc:default" .parse() .unwrap(); assert!(default.extends(&default_again)); assert!(!default_evil.extends(&default)); assert!(!default.extends(&default_evil)); + assert!(!malformed_network.extends(&default)); + assert!(!default.extends(&malformed_network)); } #[test] diff --git a/tinycloud-core/src/util.rs b/tinycloud-core/src/util.rs index 8e0c7ef..a28b007 100644 --- a/tinycloud-core/src/util.rs +++ b/tinycloud-core/src/util.rs @@ -1,6 +1,7 @@ use crate::types::{Ability, Resource}; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; +use tinycloud_auth::identity::principal_did; use tinycloud_auth::{ authorization::{TinyCloudDelegation, TinyCloudInvocation, TinyCloudRevocation}, cacaos::siwe::Message, @@ -17,7 +18,7 @@ use ucan_capabilities_object::Capabilities as UcanCapabilities; /// comparison purposes, the base DID is sufficient. This normalizes all /// DID strings to ensure consistent matching across delegation chains. fn strip_fragment(did: &str) -> String { - did.split('#').next().unwrap_or(did).to_string() + principal_did(did).unwrap_or_else(|_| did.split('#').next().unwrap_or(did).to_string()) } #[derive(Serialize, Deserialize, Clone, Debug, Hash, PartialEq, Eq)]