Skip to main content

affinidi_data_integrity/
did_vm.rs

1//! DID verification-method helpers.
2//!
3//! A [`VerificationMethodResolver`] takes a verification-method URI
4//! (e.g. `did:key:z6Mk...#z6Mk...`) and returns the raw public key bytes
5//! plus their [`KeyType`]. The data-integrity verify pipeline uses this
6//! to pull keys out of DIDs without entangling this crate with full DID
7//! resolution.
8//!
9//! A [`DidKeyResolver`] is shipped by default — it handles the
10//! `did:key:` method purely from the URI, with no network I/O. For
11//! `did:web`, `did:webvh`, and friends, provide a custom impl that
12//! delegates to your preferred DID resolver (e.g.
13//! `affinidi-did-resolver-cache-sdk`) and maps back into this trait.
14
15#[cfg(feature = "slh-dsa")]
16use affinidi_secrets_resolver::multicodec::SLH_DSA_SHA2_128S_PUB;
17use affinidi_secrets_resolver::multicodec::{
18    ED25519_PUB, MultiEncoded, P256_PUB, P384_PUB, P521_PUB, SECP256K1_PUB,
19};
20#[cfg(feature = "ml-dsa")]
21use affinidi_secrets_resolver::multicodec::{ML_DSA_44_PUB, ML_DSA_65_PUB, ML_DSA_87_PUB};
22use affinidi_secrets_resolver::secrets::KeyType;
23use async_trait::async_trait;
24
25use crate::DataIntegrityError;
26
27/// Decoded public key material from a verification method.
28///
29/// Construct via [`ResolvedKey::new`] to stay forward-compatible with
30/// future fields. Direct struct-literal construction is blocked by
31/// `#[non_exhaustive]`.
32#[derive(Clone, Debug)]
33#[non_exhaustive]
34pub struct ResolvedKey {
35    pub key_type: KeyType,
36    pub public_key_bytes: Vec<u8>,
37}
38
39impl ResolvedKey {
40    /// Constructs a `ResolvedKey` for custom
41    /// [`VerificationMethodResolver`] implementations.
42    pub fn new(key_type: KeyType, public_key_bytes: Vec<u8>) -> Self {
43        Self {
44            key_type,
45            public_key_bytes,
46        }
47    }
48}
49
50/// Resolves a verification-method URI to its public key.
51///
52/// Implementors may do anything — purely local decoding (did:key),
53/// HTTP fetches (did:web), cached DID document lookups, HSM introspection.
54/// The method is async because the typical implementation involves I/O.
55///
56/// A blanket `&T: VerificationMethodResolver` impl means callers can
57/// pass a borrow of any resolver without wrapping it.
58#[async_trait]
59pub trait VerificationMethodResolver: Send + Sync {
60    /// Resolve a verification-method URI, returning its key type and raw
61    /// public-key bytes.
62    async fn resolve_vm(&self, vm: &str) -> Result<ResolvedKey, DataIntegrityError>;
63}
64
65/// Resolves `did:key:zXXX#zXXX` verification methods with no I/O.
66///
67/// Supports every public-key multicodec registered with this build:
68/// Ed25519, X25519, P-256, P-384, P-521, secp256k1, and (with features
69/// enabled) ML-DSA-{44,65,87} and SLH-DSA-SHA2-128s.
70pub struct DidKeyResolver;
71
72#[async_trait]
73impl VerificationMethodResolver for DidKeyResolver {
74    async fn resolve_vm(&self, vm: &str) -> Result<ResolvedKey, DataIntegrityError> {
75        resolve_did_key(vm)
76    }
77}
78
79/// Synchronous variant of [`DidKeyResolver::resolve_vm`].
80///
81/// `did:key:` is parseable locally, so callers with a key already in hand
82/// can skip the async machinery.
83pub fn resolve_did_key(vm: &str) -> Result<ResolvedKey, DataIntegrityError> {
84    // Strip the fragment to get the DID; fragment is expected to repeat
85    // the multibase key identifier, but we only need the DID body.
86    let did = vm.split('#').next().unwrap_or(vm);
87    let id = did.strip_prefix("did:key:").ok_or_else(|| {
88        DataIntegrityError::Resolver(format!(
89            "not a did:key URI (expected did:key:..., got {vm})"
90        ))
91    })?;
92
93    // id = multibase-encoded multicodec ||  public-key bytes.
94    let (_base, raw) = multibase::decode(id).map_err(|e| {
95        DataIntegrityError::Resolver(format!("multibase decode of did:key id failed: {e}"))
96    })?;
97    let mc = MultiEncoded::new(&raw).map_err(|e| {
98        DataIntegrityError::Resolver(format!("multicodec decode of did:key failed: {e}"))
99    })?;
100
101    let codec = mc.codec();
102    let data = mc.data();
103
104    let (key_type, expected_len): (KeyType, usize) = match codec {
105        ED25519_PUB => (KeyType::Ed25519, 32),
106        SECP256K1_PUB => (KeyType::Secp256k1, 33),
107        P256_PUB => (KeyType::P256, 33),
108        P384_PUB => (KeyType::P384, 49),
109        P521_PUB => (KeyType::P521, 67),
110        #[cfg(feature = "ml-dsa")]
111        ML_DSA_44_PUB => (KeyType::MlDsa44, 1312),
112        #[cfg(feature = "ml-dsa")]
113        ML_DSA_65_PUB => (KeyType::MlDsa65, 1952),
114        #[cfg(feature = "ml-dsa")]
115        ML_DSA_87_PUB => (KeyType::MlDsa87, 2592),
116        #[cfg(feature = "slh-dsa")]
117        SLH_DSA_SHA2_128S_PUB => (KeyType::SlhDsaSha2_128s, 32),
118        other => {
119            return Err(DataIntegrityError::InvalidPublicKey {
120                codec: Some(other),
121                len: data.len(),
122                reason: "unknown or unsupported multicodec for did:key".to_string(),
123            });
124        }
125    };
126
127    if data.len() != expected_len {
128        return Err(DataIntegrityError::InvalidPublicKey {
129            codec: Some(codec),
130            len: data.len(),
131            reason: format!(
132                "did:key public key length {} does not match expected {} for {key_type:?}",
133                data.len(),
134                expected_len
135            ),
136        });
137    }
138
139    Ok(ResolvedKey {
140        key_type,
141        public_key_bytes: data.to_vec(),
142    })
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use affinidi_secrets_resolver::secrets::Secret;
149
150    #[tokio::test]
151    async fn resolve_did_key_ed25519_roundtrip() {
152        let secret = Secret::generate_ed25519(None, Some(&[1u8; 32]));
153        let pk_mb = secret.get_public_keymultibase().unwrap();
154        let vm = format!("did:key:{pk_mb}#{pk_mb}");
155
156        let resolved = DidKeyResolver.resolve_vm(&vm).await.unwrap();
157        assert_eq!(resolved.key_type, KeyType::Ed25519);
158        assert_eq!(resolved.public_key_bytes, secret.get_public_bytes());
159    }
160
161    #[cfg(feature = "ml-dsa")]
162    #[tokio::test]
163    async fn resolve_did_key_ml_dsa_44_roundtrip() {
164        let secret = Secret::generate_ml_dsa_44(None, Some(&[2u8; 32]));
165        let pk_mb = secret.get_public_keymultibase().unwrap();
166        let vm = format!("did:key:{pk_mb}#{pk_mb}");
167
168        let resolved = DidKeyResolver.resolve_vm(&vm).await.unwrap();
169        assert_eq!(resolved.key_type, KeyType::MlDsa44);
170        assert_eq!(resolved.public_key_bytes, secret.get_public_bytes());
171        assert_eq!(resolved.public_key_bytes.len(), 1312);
172    }
173
174    #[cfg(feature = "slh-dsa")]
175    #[tokio::test]
176    async fn resolve_did_key_slh_dsa_roundtrip() {
177        let secret = Secret::generate_slh_dsa_sha2_128s(None);
178        let pk_mb = secret.get_public_keymultibase().unwrap();
179        let vm = format!("did:key:{pk_mb}#{pk_mb}");
180
181        let resolved = DidKeyResolver.resolve_vm(&vm).await.unwrap();
182        assert_eq!(resolved.key_type, KeyType::SlhDsaSha2_128s);
183        assert_eq!(resolved.public_key_bytes.len(), 32);
184    }
185
186    #[tokio::test]
187    async fn resolve_did_key_rejects_non_did_key() {
188        let err = DidKeyResolver
189            .resolve_vm("did:web:example.com#key-1")
190            .await
191            .unwrap_err();
192        assert!(matches!(err, DataIntegrityError::Resolver(_)));
193    }
194
195    #[tokio::test]
196    async fn resolve_did_key_rejects_unknown_codec() {
197        // multibase-encoded varint 0x9999 (not a registered pubkey codec)
198        // followed by 32 zero bytes.
199        let bogus = "did:key:z8NGuWZeMJTxQeofMjZPEdN2PC6eaDKhKCbF19UqjpDEwKzYwQnH3YzHK3#x";
200        let err = DidKeyResolver.resolve_vm(bogus).await.unwrap_err();
201        assert!(
202            matches!(
203                err,
204                DataIntegrityError::Resolver(_) | DataIntegrityError::InvalidPublicKey { .. }
205            ),
206            "got: {err:?}"
207        );
208    }
209}