baseid_did/methods/
jwk.rs

1//! `did:jwk` method implementation.
2//!
3//! `did:jwk` encodes a JWK public key directly in the DID string as
4//! base64url-encoded JSON. No network resolution is needed.
5//!
6//! Reference: <https://github.com/quartzjer/did-jwk/blob/main/spec.md>
7
8use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
9use baseid_core::error::DidError;
10use baseid_crypto::{Jwk, PublicKey};
11
12use crate::document::{DidDocument, VerificationMethod, VerificationRelationship};
13use crate::resolution::{DidResolver, ResolutionMetadata, ResolutionResult};
14use crate::url::DidUrl;
15
16/// W3C DID context.
17const DID_CONTEXT: &str = "https://www.w3.org/ns/did/v1";
18/// JWK verification method context.
19const JWS_CONTEXT: &str = "https://w3id.org/security/suites/jws-2020/v1";
20
21/// Resolver for the `did:jwk` method.
22pub struct DidJwkResolver;
23
24impl DidResolver for DidJwkResolver {
25    fn method(&self) -> &str {
26        "jwk"
27    }
28
29    async fn resolve(&self, did: &str) -> baseid_core::Result<ResolutionResult> {
30        let url = DidUrl::parse(did)?;
31        if url.method != "jwk" {
32            return Err(DidError::UnsupportedMethod.into());
33        }
34
35        // Decode the base64url method-specific-id to get the JWK JSON.
36        let jwk_json = URL_SAFE_NO_PAD
37            .decode(&url.method_id)
38            .map_err(|_| DidError::ResolutionFailed)?;
39
40        let jwk: Jwk = serde_json::from_slice(&jwk_json).map_err(|_| DidError::ResolutionFailed)?;
41
42        let public_key = jwk
43            .to_public_key()
44            .map_err(|_| DidError::ResolutionFailed)?;
45
46        let document = Self::create(&public_key)?;
47
48        Ok(ResolutionResult {
49            document: Some(document),
50            metadata: ResolutionMetadata {
51                content_type: Some("application/did+ld+json".to_string()),
52                error: None,
53            },
54        })
55    }
56}
57
58impl DidJwkResolver {
59    /// Create a new `did:jwk` DID Document from a public key.
60    ///
61    /// Converts the public key to a JWK, serialises it to compact JSON
62    /// (no whitespace), base64url-encodes it, and builds a DID Document
63    /// with a single `JsonWebKey2020` verification method.
64    pub fn create(public_key: &PublicKey) -> baseid_core::Result<DidDocument> {
65        let jwk = Jwk::from_public_key(public_key)?;
66
67        // Compact JSON serialization (no whitespace).
68        let jwk_json = serde_json::to_string(&jwk).map_err(|_| DidError::ResolutionFailed)?;
69
70        let encoded = URL_SAFE_NO_PAD.encode(jwk_json.as_bytes());
71        let did = format!("did:jwk:{encoded}");
72
73        // Single verification method: JsonWebKey2020 with publicKeyJwk.
74        let vm_id = format!("{did}#0");
75        let jwk_value = serde_json::to_value(&jwk).map_err(|_| DidError::ResolutionFailed)?;
76        let vm = VerificationMethod {
77            id: vm_id.clone(),
78            r#type: "JsonWebKey2020".to_string(),
79            controller: did.clone(),
80            public_key_jwk: Some(jwk_value),
81            public_key_multibase: None,
82        };
83
84        Ok(DidDocument {
85            id: did,
86            context: vec![DID_CONTEXT.to_string(), JWS_CONTEXT.to_string()],
87            verification_method: vec![vm],
88            authentication: vec![VerificationRelationship::Reference(vm_id.clone())],
89            assertion_method: vec![VerificationRelationship::Reference(vm_id)],
90            key_agreement: vec![],
91            service: vec![],
92        })
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use baseid_core::types::KeyType;
100    use baseid_crypto::KeyPair;
101
102    #[test]
103    fn create_ed25519() {
104        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
105        let doc = DidJwkResolver::create(&kp.public).unwrap();
106
107        assert!(doc.id.starts_with("did:jwk:"));
108        assert_eq!(doc.context.len(), 2);
109        assert_eq!(doc.context[0], DID_CONTEXT);
110        assert_eq!(doc.context[1], JWS_CONTEXT);
111        assert_eq!(doc.verification_method.len(), 1);
112        assert_eq!(doc.verification_method[0].r#type, "JsonWebKey2020");
113        assert!(doc.verification_method[0].public_key_jwk.is_some());
114        assert_eq!(doc.authentication.len(), 1);
115        assert_eq!(doc.assertion_method.len(), 1);
116    }
117
118    #[test]
119    fn create_p256() {
120        let kp = KeyPair::generate(KeyType::P256).unwrap();
121        let doc = DidJwkResolver::create(&kp.public).unwrap();
122
123        assert!(doc.id.starts_with("did:jwk:"));
124        assert_eq!(doc.verification_method.len(), 1);
125
126        let jwk = doc.verification_method[0].public_key_jwk.as_ref().unwrap();
127        assert_eq!(jwk["crv"], "P-256");
128        assert_eq!(jwk["kty"], "EC");
129    }
130
131    #[tokio::test]
132    async fn resolve_ed25519() {
133        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
134        let created = DidJwkResolver::create(&kp.public).unwrap();
135
136        let resolver = DidJwkResolver;
137        let result = resolver.resolve(&created.id).await.unwrap();
138        let resolved = result.document.unwrap();
139
140        assert_eq!(resolved.id, created.id);
141        assert_eq!(resolved.verification_method.len(), 1);
142        assert_eq!(resolved.verification_method[0].r#type, "JsonWebKey2020");
143        assert_eq!(
144            result.metadata.content_type.as_deref(),
145            Some("application/did+ld+json")
146        );
147    }
148
149    #[tokio::test]
150    async fn resolve_p256() {
151        let kp = KeyPair::generate(KeyType::P256).unwrap();
152        let created = DidJwkResolver::create(&kp.public).unwrap();
153
154        let resolver = DidJwkResolver;
155        let result = resolver.resolve(&created.id).await.unwrap();
156        let resolved = result.document.unwrap();
157
158        assert_eq!(resolved.id, created.id);
159        let jwk = resolved.verification_method[0]
160            .public_key_jwk
161            .as_ref()
162            .unwrap();
163        assert_eq!(jwk["crv"], "P-256");
164        assert_eq!(jwk["kty"], "EC");
165    }
166
167    #[tokio::test]
168    async fn resolve_roundtrip() {
169        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
170        let created = DidJwkResolver::create(&kp.public).unwrap();
171        let did_string = created.id.clone();
172
173        let resolver = DidJwkResolver;
174        let result = resolver.resolve(&did_string).await.unwrap();
175        let resolved = result.document.unwrap();
176
177        // Extract JWK from the resolved document, convert to public key,
178        // and verify it matches the original.
179        let jwk_value = resolved.verification_method[0]
180            .public_key_jwk
181            .as_ref()
182            .unwrap();
183        let jwk: Jwk = serde_json::from_value(jwk_value.clone()).unwrap();
184        let recovered = jwk.to_public_key().unwrap();
185
186        assert_eq!(recovered.bytes, kp.public.bytes);
187        assert_eq!(recovered.key_type, kp.public.key_type);
188    }
189
190    #[tokio::test]
191    async fn resolve_wrong_method() {
192        let resolver = DidJwkResolver;
193        let result = resolver
194            .resolve("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK")
195            .await;
196        assert!(result.is_err());
197    }
198
199    #[tokio::test]
200    async fn resolve_invalid_base64() {
201        let resolver = DidJwkResolver;
202        let result = resolver.resolve("did:jwk:not-valid-base64!!!").await;
203        assert!(result.is_err());
204    }
205
206    #[tokio::test]
207    async fn create_and_resolve_all_key_types() {
208        let resolver = DidJwkResolver;
209
210        for key_type in [
211            KeyType::Ed25519,
212            KeyType::P256,
213            KeyType::P384,
214            KeyType::Secp256k1,
215        ] {
216            let kp = KeyPair::generate(key_type).unwrap();
217            let created = DidJwkResolver::create(&kp.public).unwrap();
218
219            assert!(
220                created.id.starts_with("did:jwk:"),
221                "DID for {key_type:?} should start with did:jwk:"
222            );
223
224            let result = resolver.resolve(&created.id).await.unwrap();
225            let resolved = result.document.unwrap();
226
227            assert_eq!(resolved.id, created.id);
228            assert_eq!(resolved.verification_method.len(), 1);
229            assert_eq!(resolved.verification_method[0].r#type, "JsonWebKey2020");
230
231            // Verify roundtrip: extract JWK, convert back to public key.
232            let jwk_value = resolved.verification_method[0]
233                .public_key_jwk
234                .as_ref()
235                .unwrap();
236            let jwk: Jwk = serde_json::from_value(jwk_value.clone()).unwrap();
237            let recovered = jwk.to_public_key().unwrap();
238
239            assert_eq!(
240                recovered.bytes, kp.public.bytes,
241                "key bytes mismatch for {key_type:?}"
242            );
243            assert_eq!(
244                recovered.key_type, key_type,
245                "key type mismatch for {key_type:?}"
246            );
247        }
248    }
249}