holographic_memory/core/provenance/
did.rs1use anyhow::{anyhow, Result};
5use base64::engine::{general_purpose::GeneralPurpose, DecodePaddingMode, GeneralPurposeConfig};
6use base64::{alphabet, Engine};
7
8const ED25519_MULTICODEC: [u8; 2] = [0xed, 0x01];
9
10const B64URL: GeneralPurpose = GeneralPurpose::new(
12 &alphabet::URL_SAFE,
13 GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent),
14);
15
16pub fn did_key_from_ed25519(public_key: &[u8; 32]) -> String {
18 let mut prefixed = Vec::with_capacity(2 + 32);
19 prefixed.extend_from_slice(&ED25519_MULTICODEC);
20 prefixed.extend_from_slice(public_key);
21 let encoded = multibase::encode(multibase::Base::Base58Btc, &prefixed);
22 format!("did:key:{encoded}")
23}
24
25pub fn ed25519_from_did_key(did: &str) -> Result<[u8; 32]> {
27 let multibase_part = did
28 .strip_prefix("did:key:")
29 .ok_or_else(|| anyhow!("not a did:key URI"))?;
30 let (_, decoded) =
31 multibase::decode(multibase_part).map_err(|e| anyhow!("multibase decode failed: {e}"))?;
32 if decoded.len() != 34
33 || decoded[0] != ED25519_MULTICODEC[0]
34 || decoded[1] != ED25519_MULTICODEC[1]
35 {
36 return Err(anyhow!("not an Ed25519 did:key (wrong prefix or length)"));
37 }
38 let mut key = [0u8; 32];
39 key.copy_from_slice(&decoded[2..34]);
40 Ok(key)
41}
42
43pub fn did_jwk_from_ed25519(public_key: &[u8; 32]) -> String {
49 let b64url_nopad = GeneralPurpose::new(
51 &alphabet::URL_SAFE,
52 GeneralPurposeConfig::new().with_encode_padding(false),
53 );
54 let x = b64url_nopad.encode(public_key);
55 let jwk_json = format!("{{\"crv\":\"Ed25519\",\"kty\":\"OKP\",\"x\":\"{x}\"}}");
57 let b64url_pad = GeneralPurpose::new(&alphabet::URL_SAFE, GeneralPurposeConfig::new());
59 let enc = b64url_pad.encode(jwk_json.as_bytes());
60 format!("did:jwk:{enc}")
61}
62
63pub fn ed25519_from_did_jwk(did: &str) -> Result<[u8; 32]> {
66 let encoded = did
67 .strip_prefix("did:jwk:")
68 .ok_or_else(|| anyhow!("not a did:jwk URI"))?;
69 let json = B64URL
70 .decode(encoded)
71 .map_err(|e| anyhow!("did:jwk base64url decode failed: {e}"))?;
72 let jwk: serde_json::Value =
73 serde_json::from_slice(&json).map_err(|e| anyhow!("did:jwk JWK parse failed: {e}"))?;
74 if jwk.get("kty").and_then(|v| v.as_str()) != Some("OKP") {
75 return Err(anyhow!("did:jwk is not an OKP key"));
76 }
77 if jwk.get("crv").and_then(|v| v.as_str()) != Some("Ed25519") {
78 return Err(anyhow!("did:jwk is not an Ed25519 key"));
79 }
80 let x = jwk
81 .get("x")
82 .and_then(|v| v.as_str())
83 .ok_or_else(|| anyhow!("did:jwk JWK has no x parameter"))?;
84 let key_bytes = B64URL
85 .decode(x)
86 .map_err(|e| anyhow!("did:jwk x base64url decode failed: {e}"))?;
87 let key: [u8; 32] = key_bytes
88 .as_slice()
89 .try_into()
90 .map_err(|_| anyhow!("did:jwk x is not a 32-byte Ed25519 key"))?;
91 Ok(key)
92}
93
94pub fn did_web_from_domain(domain: &str, path: Option<&[&str]>) -> String {
98 let encoded_domain = domain.replace(':', "%3A");
99 match path {
100 Some(segments) if !segments.is_empty() => {
101 let path_str = segments.join(":");
102 format!("did:web:{encoded_domain}:{path_str}")
103 }
104 _ => format!("did:web:{encoded_domain}"),
105 }
106}
107
108pub fn did_web_to_url(did: &str) -> Result<String> {
113 let domain_path = did
114 .strip_prefix("did:web:")
115 .ok_or_else(|| anyhow!("not a did:web URI"))?;
116 let parts: Vec<&str> = domain_path.split(':').collect();
117 let domain = parts[0].replace("%3A", ":");
118 if parts.len() == 1 {
119 Ok(format!("https://{domain}/.well-known/did.json"))
120 } else {
121 let path = parts[1..].join("/");
122 Ok(format!("https://{domain}/{path}/did.json"))
123 }
124}
125
126pub fn create_did_document(did: &str, public_key: &[u8; 32]) -> serde_json::Value {
128 let key_multibase = {
129 let mut prefixed = Vec::with_capacity(34);
130 prefixed.extend_from_slice(&ED25519_MULTICODEC);
131 prefixed.extend_from_slice(public_key);
132 multibase::encode(multibase::Base::Base58Btc, &prefixed)
133 };
134 let key_id = format!("{did}#key-0");
135 serde_json::json!({
136 "@context": [
137 "https://www.w3.org/ns/did/v1",
138 "https://w3id.org/security/multikey/v1"
139 ],
140 "id": did,
141 "verificationMethod": [{
142 "id": &key_id,
143 "type": "Multikey",
144 "controller": did,
145 "publicKeyMultibase": key_multibase
146 }],
147 "authentication": [&key_id],
148 "assertionMethod": [&key_id]
149 })
150}
151
152pub fn ed25519_from_did_document(doc: &serde_json::Value) -> Result<[u8; 32]> {
156 let methods = doc
157 .get("verificationMethod")
158 .and_then(|v| v.as_array())
159 .ok_or_else(|| anyhow!("no verificationMethod in DID document"))?;
160 let method = methods
161 .first()
162 .ok_or_else(|| anyhow!("empty verificationMethod array"))?;
163 let multibase_key = method
164 .get("publicKeyMultibase")
165 .and_then(|v| v.as_str())
166 .ok_or_else(|| anyhow!("no publicKeyMultibase in verification method"))?;
167 let (_, decoded) =
168 multibase::decode(multibase_key).map_err(|e| anyhow!("multibase decode failed: {e}"))?;
169 if decoded.len() != 34
170 || decoded[0] != ED25519_MULTICODEC[0]
171 || decoded[1] != ED25519_MULTICODEC[1]
172 {
173 return Err(anyhow!("not an Ed25519 key (wrong multicodec prefix)"));
174 }
175 let mut key = [0u8; 32];
176 key.copy_from_slice(&decoded[2..34]);
177 Ok(key)
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn did_key_roundtrip() {
186 let pk = [0xABu8; 32];
187 let did = did_key_from_ed25519(&pk);
188 assert!(did.starts_with("did:key:z"));
189 let recovered = ed25519_from_did_key(&did).unwrap();
190 assert_eq!(recovered, pk);
191 }
192
193 #[test]
194 fn reject_bad_prefix() {
195 assert!(ed25519_from_did_key("did:web:example.com").is_err());
196 }
197
198 #[test]
199 fn did_jwk_from_ed25519_roundtrips() {
200 let pk = [0x42u8; 32];
201 let did = did_jwk_from_ed25519(&pk);
202 assert!(did.starts_with("did:jwk:"));
203 let recovered = ed25519_from_did_jwk(&did).unwrap();
204 assert_eq!(recovered, pk);
205 }
206
207 #[test]
208 fn did_jwk_resolves_vector_issuer() {
209 let issuer = "did:jwk:eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6IkE2RUh2X1BPRUw0ZGNOMFk1MHZBbVdmazFqQ2JwUTFmSGR5R1pCSlZNYmcifQ==";
210 let expected = "03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8";
211 let key = ed25519_from_did_jwk(issuer).unwrap();
212 let hex: String = key.iter().map(|b| format!("{b:02x}")).collect();
213 assert_eq!(hex, expected);
214 }
215
216 #[test]
217 fn did_web_simple() {
218 let did = did_web_from_domain("example.com", None);
219 assert_eq!(did, "did:web:example.com");
220 let url = did_web_to_url(&did).unwrap();
221 assert_eq!(url, "https://example.com/.well-known/did.json");
222 }
223
224 #[test]
225 fn did_web_with_path() {
226 let did = did_web_from_domain("example.com", Some(&["users", "alice"]));
227 assert_eq!(did, "did:web:example.com:users:alice");
228 let url = did_web_to_url(&did).unwrap();
229 assert_eq!(url, "https://example.com/users/alice/did.json");
230 }
231
232 #[test]
233 fn did_web_with_port() {
234 let did = did_web_from_domain("localhost%3A8080", None);
235 assert_eq!(did, "did:web:localhost%3A8080");
236 let url = did_web_to_url(&did).unwrap();
237 assert_eq!(url, "https://localhost:8080/.well-known/did.json");
238 }
239
240 #[test]
241 fn did_document_roundtrip() {
242 let pk = [0xCDu8; 32];
243 let did = did_web_from_domain("example.com", None);
244 let doc = create_did_document(&did, &pk);
245 let recovered = ed25519_from_did_document(&doc).unwrap();
246 assert_eq!(recovered, pk);
247 assert_eq!(doc["id"], "did:web:example.com");
248 }
249}