Skip to main content

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(&current.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}