dpp_vc/did_builder.rs
1//! `did:web` DID document builder.
2//!
3//! Constructs a W3C DID document from the node's `KeyStore`: primary key first,
4//! hygiene-archived keys as secondary verification methods, revoked keys excluded
5//! so their signatures stop verifying.
6
7use serde_json::{Value, json};
8
9use dpp_crypto::keystore::KeyStore;
10
11/// Build a `did:web` DID document for an issuer.
12///
13/// The DID is `did:web:{hostname}` (pathless; resolves to `/.well-known/did.json`).
14///
15/// The primary (current) key is listed first as `#key-1`.
16/// Any archived keys are appended as secondary verification methods so that
17/// signatures produced with rotated keys remain verifiable.
18pub fn build_did_document(store: &KeyStore, base_url: &str, key_id: &str) -> anyhow::Result<Value> {
19 if !store.has_key(key_id) {
20 store.generate_key(key_id)?;
21 }
22
23 // Only the public key is needed to build a DID document — read it
24 // directly from the store's plaintext `verifying_key_hex` rather than
25 // decrypting the private signing key just to derive it back.
26 let current = store
27 .public_key(key_id)
28 .ok_or_else(|| anyhow::anyhow!("no key found for {key_id}"))?;
29
30 let hostname = base_url
31 .trim_start_matches("https://")
32 .trim_start_matches("http://");
33
34 // Pathless form resolves to /.well-known/did.json per did:web spec.
35 // Port colon must be %-encoded (RFC 3986 §3.3 path segment rule).
36 let did = format!("did:web:{}", hostname.replace(':', "%3A"));
37
38 let primary_vm_id = format!("{did}#key-1");
39
40 let mut verification_methods = vec![json!({
41 "id": primary_vm_id,
42 "type": "JsonWebKey2020",
43 "controller": did,
44 // The JWK shape comes from the algorithm recorded on the key, not from
45 // an assumption here — `kty` and the parameter set differ per
46 // algorithm, and `dpp-crypto` is the crate that knows which is which.
47 "publicKeyJwk": current
48 .algorithm
49 .public_key_jwk(&hex::decode(¤t.verifying_key_hex)?)
50 })];
51
52 // Revoked keys are excluded entirely — neither a verification method nor an
53 // assertionMethod — so signatures they produced no longer verify (Gap 7).
54 let archived: Vec<_> = store
55 .archived_public_keys(key_id)
56 .into_iter()
57 .filter(|k| !k.revoked)
58 .collect();
59 for (idx, archived_key) in archived.iter().enumerate() {
60 let vm_id = format!("{did}#key-{}", idx + 2);
61 verification_methods.push(json!({
62 "id": vm_id,
63 "type": "JsonWebKey2020",
64 "controller": did,
65 // Per archived key, so a rotation across algorithms produces a
66 // document carrying both shapes rather than one mislabelled.
67 "publicKeyJwk": archived_key
68 .algorithm
69 .public_key_jwk(&hex::decode(&archived_key.verifying_key_hex)?)
70 }));
71 }
72
73 let assertion_methods: Vec<String> = verification_methods
74 .iter()
75 .filter_map(|vm| vm["id"].as_str().map(String::from))
76 .collect();
77
78 let doc = json!({
79 "@context": [
80 "https://www.w3.org/ns/did/v1",
81 "https://w3id.org/security/suites/jws-2020/v1"
82 ],
83 "id": did,
84 "verificationMethod": verification_methods,
85 "authentication": [primary_vm_id],
86 "assertionMethod": assertion_methods
87 });
88
89 Ok(doc)
90}