1use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
30use serde::{Deserialize, Serialize};
31
32use crate::keys::{KeriDecodeError, KeriPublicKey};
33use crate::state::KeyState;
34use crate::types::CesrKey;
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(tag = "kty")]
41pub enum PublicKeyJwk {
42 #[serde(rename = "OKP")]
44 Okp {
45 kid: String,
47 crv: String,
49 x: String,
51 },
52 #[serde(rename = "EC")]
54 Ec {
55 kid: String,
57 crv: String,
59 x: String,
61 y: String,
63 },
64}
65
66impl PublicKeyJwk {
67 pub fn from_key(key: &KeriPublicKey, kid: &str) -> Result<Self, KeriDecodeError> {
75 match key {
76 KeriPublicKey::Ed25519 { key: raw, .. } => Ok(Self::Okp {
77 kid: kid.to_string(),
78 crv: "Ed25519".to_string(),
79 x: URL_SAFE_NO_PAD.encode(raw),
80 }),
81 KeriPublicKey::P256 {
82 key: compressed, ..
83 } => {
84 use p256::elliptic_curve::sec1::ToEncodedPoint;
85 let pk = p256::PublicKey::from_sec1_bytes(compressed).map_err(|e| {
88 KeriDecodeError::DecodeError(format!("P-256 point decode failed: {e}"))
89 })?;
90 let uncompressed = pk.to_encoded_point(false);
91 let x = uncompressed.x().ok_or_else(|| {
92 KeriDecodeError::DecodeError("P-256 point has no x-coordinate".to_string())
93 })?;
94 let y = uncompressed.y().ok_or_else(|| {
95 KeriDecodeError::DecodeError("P-256 point has no y-coordinate".to_string())
96 })?;
97 Ok(Self::Ec {
98 kid: kid.to_string(),
99 crv: "P-256".to_string(),
100 x: URL_SAFE_NO_PAD.encode(x),
101 y: URL_SAFE_NO_PAD.encode(y),
102 })
103 }
104 }
105 }
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114pub struct VerificationMethod {
115 pub id: String,
117 #[serde(rename = "type")]
119 pub type_: String,
120 pub controller: String,
122 #[serde(rename = "publicKeyJwk")]
124 pub public_key_jwk: PublicKeyJwk,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133pub struct DidWebsDocument {
134 pub id: String,
136 #[serde(rename = "verificationMethod")]
138 pub verification_method: Vec<VerificationMethod>,
139 pub service: Vec<serde_json::Value>,
142 #[serde(rename = "alsoKnownAs")]
145 pub also_known_as: Vec<String>,
146}
147
148impl DidWebsDocument {
149 pub fn from_key_state(state: &KeyState, domain: &str) -> Result<Self, KeriDecodeError> {
162 let aid = state.prefix.as_str();
163 let id = format!("did:webs:{domain}:{aid}");
164
165 let mut verification_method = Vec::with_capacity(state.current_keys.len());
166 for cesr_key in &state.current_keys {
167 verification_method.push(verification_method_for(cesr_key, &id)?);
168 }
169
170 Ok(Self {
171 id,
172 verification_method,
173 service: Vec::new(),
174 also_known_as: vec![format!("did:keri:{aid}")],
175 })
176 }
177}
178
179fn verification_method_for(
183 cesr_key: &CesrKey,
184 did: &str,
185) -> Result<VerificationMethod, KeriDecodeError> {
186 let kid = cesr_key.as_str();
187 let key = KeriPublicKey::parse(kid)?;
188 Ok(VerificationMethod {
189 id: format!("#{kid}"),
190 type_: "JsonWebKey".to_string(),
191 controller: did.to_string(),
192 public_key_jwk: PublicKeyJwk::from_key(&key, kid)?,
193 })
194}
195
196#[cfg(test)]
197#[allow(clippy::unwrap_used, clippy::expect_used)]
198mod tests {
199 use super::*;
200 use crate::types::{Prefix, Said, Threshold};
201
202 fn ed25519_state(aid: &str, key_cesr: &str) -> KeyState {
204 KeyState::from_inception(
205 Prefix::new_unchecked(aid.to_string()),
206 vec![CesrKey::new_unchecked(key_cesr.to_string())],
207 vec![Said::new_unchecked("ENext0".to_string())],
208 Threshold::Simple(1),
209 Threshold::Simple(1),
210 Said::new_unchecked(aid.to_string()),
211 vec![],
212 Threshold::Simple(0),
213 vec![],
214 )
215 }
216
217 fn ed25519_cesr(raw: &[u8; 32]) -> String {
219 KeriPublicKey::ed25519(raw).unwrap().to_qb64().unwrap()
220 }
221
222 fn p256_cesr() -> (String, [u8; 33]) {
224 use p256::elliptic_curve::sec1::ToEncodedPoint;
225 let sk = p256::SecretKey::from_slice(&[
227 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
228 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
229 0x1d, 0x1e, 0x1f, 0x20,
230 ])
231 .unwrap();
232 let pt = sk.public_key().to_encoded_point(true);
233 let mut compressed = [0u8; 33];
234 compressed.copy_from_slice(pt.as_bytes());
235 let cesr = KeriPublicKey::P256 {
236 key: compressed,
237 transferable: true,
238 }
239 .to_qb64()
240 .unwrap();
241 (cesr, compressed)
242 }
243
244 #[test]
245 fn document_has_canonical_field_order() {
246 let key = ed25519_cesr(&[3u8; 32]);
247 let state = ed25519_state("EAid000000000000000000000000000000000000000", &key);
248 let doc = DidWebsDocument::from_key_state(&state, "example.com").unwrap();
249
250 let json = serde_json::to_value(&doc).unwrap();
251 let keys: Vec<&str> = json
252 .as_object()
253 .unwrap()
254 .keys()
255 .map(String::as_str)
256 .collect();
257 assert_eq!(
259 keys,
260 vec!["id", "verificationMethod", "service", "alsoKnownAs"]
261 );
262 }
263
264 #[test]
265 fn ed25519_verification_method_matches_reference_shape() {
266 let key = ed25519_cesr(&[7u8; 32]);
267 let state = ed25519_state("EAid000000000000000000000000000000000000000", &key);
268 let doc = DidWebsDocument::from_key_state(&state, "example.com").unwrap();
269
270 assert_eq!(
271 doc.id,
272 "did:webs:example.com:EAid000000000000000000000000000000000000000"
273 );
274 assert_eq!(
275 doc.also_known_as,
276 vec!["did:keri:EAid000000000000000000000000000000000000000"]
277 );
278 assert!(doc.service.is_empty());
279
280 let vm = &doc.verification_method[0];
281 assert_eq!(vm.id, format!("#{key}"));
283 assert_eq!(vm.type_, "JsonWebKey");
284 assert_eq!(vm.controller, doc.id);
285 match &vm.public_key_jwk {
286 PublicKeyJwk::Okp { kid, crv, x } => {
287 assert_eq!(kid, &key);
288 assert_eq!(crv, "Ed25519");
289 assert_eq!(x, &URL_SAFE_NO_PAD.encode([7u8; 32]));
291 }
292 other => panic!("expected OKP JWK, got {other:?}"),
293 }
294 }
295
296 #[test]
297 fn ed25519_jwk_serializes_kty_first() {
298 let key = ed25519_cesr(&[1u8; 32]);
301 let state = ed25519_state("EAid000000000000000000000000000000000000000", &key);
302 let doc = DidWebsDocument::from_key_state(&state, "example.com").unwrap();
303 let jwk = serde_json::to_value(&doc.verification_method[0].public_key_jwk).unwrap();
304 let labels: Vec<&str> = jwk
305 .as_object()
306 .unwrap()
307 .keys()
308 .map(String::as_str)
309 .collect();
310 assert_eq!(labels, vec!["kty", "kid", "crv", "x"]);
311 assert_eq!(jwk["kty"], "OKP");
312 assert_eq!(jwk["crv"], "Ed25519");
313 }
314
315 #[test]
316 fn p256_verification_method_emits_ec_jwk_with_x_and_y() {
317 let (key, compressed) = p256_cesr();
318 let state = ed25519_state("EAidP256000000000000000000000000000000000000", &key);
319 let doc = DidWebsDocument::from_key_state(&state, "example.com").unwrap();
320
321 let vm = &doc.verification_method[0];
322 assert_eq!(vm.id, format!("#{key}"));
323 match &vm.public_key_jwk {
324 PublicKeyJwk::Ec { kid, crv, x, y } => {
325 assert_eq!(kid, &key);
326 assert_eq!(crv, "P-256");
327 assert_eq!(x, &URL_SAFE_NO_PAD.encode(&compressed[1..33]));
329 assert_eq!(URL_SAFE_NO_PAD.decode(y).unwrap().len(), 32);
331 }
332 other => panic!("expected EC JWK, got {other:?}"),
333 }
334 }
335
336 #[test]
337 fn multisig_emits_one_method_per_key() {
338 let k1 = ed25519_cesr(&[1u8; 32]);
339 let k2 = ed25519_cesr(&[2u8; 32]);
340 let mut state = ed25519_state("EAid000000000000000000000000000000000000000", &k1);
341 state.current_keys.push(CesrKey::new_unchecked(k2.clone()));
342 let doc = DidWebsDocument::from_key_state(&state, "example.com").unwrap();
343 assert_eq!(doc.verification_method.len(), 2);
344 assert_eq!(doc.verification_method[0].id, format!("#{k1}"));
345 assert_eq!(doc.verification_method[1].id, format!("#{k2}"));
346 }
347
348 #[test]
349 fn domain_with_port_and_path_is_preserved() {
350 let key = ed25519_cesr(&[5u8; 32]);
352 let state = ed25519_state("EAid000000000000000000000000000000000000000", &key);
353 let doc = DidWebsDocument::from_key_state(&state, "example.com%3A3901:dids").unwrap();
354 assert_eq!(
355 doc.id,
356 "did:webs:example.com%3A3901:dids:EAid000000000000000000000000000000000000000"
357 );
358 assert_eq!(doc.verification_method[0].controller, doc.id);
359 }
360
361 #[test]
362 fn undecodable_key_is_rejected_at_the_boundary() {
363 let mut state = ed25519_state("EAid000000000000000000000000000000000000000", "Dvalid");
364 state.current_keys = vec![CesrKey::new_unchecked("Xnot-a-verkey".to_string())];
365 assert!(DidWebsDocument::from_key_state(&state, "example.com").is_err());
366 }
367
368 #[test]
369 fn document_round_trips_through_json() {
370 let key = ed25519_cesr(&[9u8; 32]);
371 let state = ed25519_state("EAid000000000000000000000000000000000000000", &key);
372 let doc = DidWebsDocument::from_key_state(&state, "example.com").unwrap();
373 let wire = serde_json::to_string(&doc).unwrap();
374 let parsed: DidWebsDocument = serde_json::from_str(&wire).unwrap();
375 assert_eq!(parsed, doc);
376 }
377}