1#[derive(std::fmt::Debug, std::clone::Clone, serde::Serialize, serde::Deserialize)]
18pub struct DidDocument {
19 pub id: String,
21 #[serde(default, rename = "verificationMethod")]
24 pub verification_method: std::vec::Vec<VerificationMethod>,
25 #[serde(default, rename = "assertionMethod")]
27 pub assertion_method: std::vec::Vec<serde_json::Value>,
28 #[serde(default, rename = "keyAgreement")]
34 pub key_agreement: std::vec::Vec<VerificationMethod>,
35}
36
37const X25519_MULTICODEC: [u8; 2] = [0xec, 0x01];
40
41#[derive(std::fmt::Debug, std::clone::Clone, serde::Serialize, serde::Deserialize)]
43pub struct VerificationMethod {
44 pub id: String,
46 #[serde(rename = "type")]
48 pub r#type: String,
49 #[serde(default)]
51 pub controller: std::option::Option<String>,
52 #[serde(default, rename = "publicKeyMultibase")]
54 pub public_key_multibase: std::option::Option<String>,
55 #[serde(default, rename = "publicKeyJwk")]
58 pub public_key_jwk: std::option::Option<serde_json::Value>,
59}
60
61pub fn parse_did_document(
63 json: &str,
64) -> std::result::Result<DidDocument, crate::errors::AphError> {
65 match serde_json::from_str::<DidDocument>(json) {
66 std::result::Result::Ok(doc) => std::result::Result::Ok(doc),
67 std::result::Result::Err(_) => {
70 std::result::Result::Err(crate::errors::AphError::NotaryServiceUnreachable)
71 }
72 }
73}
74
75impl DidDocument {
76 pub fn key_agreement_x25519(
84 &self,
85 kid: &str,
86 ) -> std::result::Result<[u8; 32], crate::errors::AphError> {
87 let refuse = || crate::errors::AphError::seal_reader_key_unpublished(&self.id, kid);
88 let want = std::format!("{}#{}", self.id, kid);
89 let entry = self
90 .key_agreement
91 .iter()
92 .find(|entry| entry.id == want || entry.id == kid)
93 .ok_or_else(refuse)?;
94 let multibase = entry.public_key_multibase.as_deref().ok_or_else(refuse)?;
95 let bytes =
96 crate::crypto::multibase::base58btc_decode(multibase).map_err(|_| refuse())?;
97 if bytes.len() != 34 || bytes[..2] != X25519_MULTICODEC {
98 return std::result::Result::Err(refuse());
99 }
100 let mut key = [0u8; 32];
101 key.copy_from_slice(&bytes[2..]);
102 std::result::Result::Ok(key)
103 }
104
105 pub fn allows_assertion(&self, did_url: &str) -> bool {
111 self.assertion_method.iter().any(|entry| match entry {
112 serde_json::Value::String(s) => s == did_url,
113 serde_json::Value::Object(o) => {
114 o.get("id").and_then(|v| v.as_str()) == std::option::Option::Some(did_url)
115 }
116 _ => false,
117 })
118 }
119
120 pub fn resolve_key(
127 &self,
128 did_url: &str,
129 ) -> std::result::Result<super::NotaryPublicKey, crate::errors::AphError> {
130 let parsed = super::DidUrl::parse(did_url);
131 let entry = match parsed.fragment {
132 std::option::Option::Some(_) => self
133 .verification_method
134 .iter()
135 .find(|vm| vm.id == did_url)
136 .or_else(|| {
139 let want = std::format!("#{}", parsed.fragment.clone().unwrap_or_default());
140 self.verification_method.iter().find(|vm| vm.id == want)
141 }),
142 std::option::Option::None => {
143 if self.verification_method.len() == 1 {
144 self.verification_method.first()
145 } else {
146 std::option::Option::None
147 }
148 }
149 };
150
151 let entry = match entry {
152 std::option::Option::Some(e) => e,
153 std::option::Option::None => {
160 return std::result::Result::Err(crate::errors::AphError::notary_key_not_published(
161 std::format!("DID Document names no verificationMethod matching `{}`", did_url),
162 ));
163 }
164 };
165
166 let multibase = match entry.public_key_multibase.as_deref() {
167 std::option::Option::Some(m) => m,
168 std::option::Option::None => {
169 return std::result::Result::Err(crate::errors::AphError::unsupported_algorithm(
172 "verificationMethod without publicKeyMultibase (publicKeyJwk not supported)",
173 ));
174 }
175 };
176
177 match crate::crypto::did_key::decode_multibase_key(multibase)? {
178 crate::crypto::did_key::DecodedDidKey::Ed25519(key) => {
179 std::result::Result::Ok(super::NotaryPublicKey {
180 algorithm: super::KeyAlgorithm::Ed25519,
181 key_bytes: key.as_bytes().to_vec(),
182 kid: parsed.fragment,
183 })
184 }
185 crate::crypto::did_key::DecodedDidKey::P256(bytes) => {
186 std::result::Result::Ok(super::NotaryPublicKey {
187 algorithm: super::KeyAlgorithm::P256,
188 key_bytes: bytes,
189 kid: parsed.fragment,
190 })
191 }
192 }
193 }
194}
195
196#[cfg(test)]
197mod tests {
198 const SPEC_EXAMPLE: &str = r#"{
200 "@context": ["https://www.w3.org/ns/did/v1"],
201 "id": "did:web:notary.example.com",
202 "verificationMethod": [
203 {
204 "id": "did:web:notary.example.com#k1",
205 "type": "Multikey",
206 "controller": "did:web:notary.example.com",
207 "publicKeyMultibase": "z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV"
208 }
209 ],
210 "assertionMethod": ["did:web:notary.example.com#k1"]
211 }"#;
212
213 #[test]
214 fn parses_the_spec_example_document() {
215 let doc = super::parse_did_document(SPEC_EXAMPLE).unwrap();
218 std::assert_eq!(doc.id, "did:web:notary.example.com");
219 std::assert_eq!(doc.verification_method.len(), 1);
220 }
221
222 #[test]
223 fn resolves_the_key_named_by_the_fragment() {
224 let doc = super::parse_did_document(SPEC_EXAMPLE).unwrap();
226 let key = doc.resolve_key("did:web:notary.example.com#k1").unwrap();
227 std::assert_eq!(key.algorithm, crate::discovery::KeyAlgorithm::Ed25519);
228 std::assert_eq!(key.key_bytes.len(), 32);
229 std::assert_eq!(key.kid.as_deref(), Some("k1"));
230 }
231
232 #[test]
233 fn unknown_fragment_does_not_fall_back_to_another_key() {
234 let doc = super::parse_did_document(SPEC_EXAMPLE).unwrap();
237 std::assert!(doc.resolve_key("did:web:notary.example.com#k9").is_err());
238 }
239
240 #[test]
241 fn fragmentless_url_resolves_only_when_the_document_is_unambiguous() {
242 let doc = super::parse_did_document(SPEC_EXAMPLE).unwrap();
245 std::assert!(doc.resolve_key("did:web:notary.example.com").is_ok());
246
247 let multi = r#"{
248 "id": "did:web:n.example",
249 "verificationMethod": [
250 {"id":"did:web:n.example#a","type":"Multikey","publicKeyMultibase":"z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV"},
251 {"id":"did:web:n.example#b","type":"Multikey","publicKeyMultibase":"z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV"}
252 ]
253 }"#;
254 let doc = super::parse_did_document(multi).unwrap();
255 std::assert!(doc.resolve_key("did:web:n.example").is_err());
256 }
257
258 #[test]
259 fn unknown_document_fields_are_tolerated() {
260 let doc = r##"{
265 "@context": ["https://www.w3.org/ns/did/v1"],
266 "id": "did:web:n.example",
267 "service": [{"id":"#hub","type":"Hub","serviceEndpoint":"https://n.example/hub"}],
268 "authentication": ["did:web:n.example#k1"],
269 "verificationMethod": [
270 {"id":"did:web:n.example#k1","type":"Multikey","publicKeyMultibase":"z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV"}
271 ]
272 }"##;
273 std::assert!(super::parse_did_document(doc).unwrap().resolve_key("did:web:n.example#k1").is_ok());
274 }
275
276 #[test]
277 fn assertion_method_membership_is_reported() {
278 let doc = super::parse_did_document(SPEC_EXAMPLE).unwrap();
281 std::assert!(doc.allows_assertion("did:web:notary.example.com#k1"));
282 std::assert!(!doc.allows_assertion("did:web:notary.example.com#k2"));
283 }
284
285 #[test]
286 fn jwk_only_entry_reports_unsupported_rather_than_bad_signature() {
287 let doc = r#"{
290 "id": "did:web:n.example",
291 "verificationMethod": [
292 {"id":"did:web:n.example#k1","type":"JsonWebKey","publicKeyJwk":{"kty":"OKP"}}
293 ]
294 }"#;
295 let parsed = super::parse_did_document(doc).unwrap();
296 std::assert_eq!(
297 parsed.resolve_key("did:web:n.example#k1").unwrap_err().code(),
298 "APH_E010"
299 );
300 }
301
302 #[test]
303 fn malformed_json_reports_the_notary_as_unreachable() {
304 std::assert_eq!(
307 super::parse_did_document("{not json").unwrap_err().code(),
308 "APH_E008"
309 );
310 }
311
312 #[test]
313 fn key_agreement_lookup_resolves_x25519_and_refuses_with_e023_by_name() {
314 let x25519_raw = [7u8; 32];
320 let mut encoded_bytes = std::vec![0xecu8, 0x01];
321 encoded_bytes.extend_from_slice(&x25519_raw);
322 let multibase = crate::crypto::multibase::base58btc_encode(&encoded_bytes);
323 let doc: super::DidDocument = serde_json::from_value(serde_json::json!({
324 "id": "did:web:reader.example.com",
325 "verificationMethod": [],
326 "keyAgreement": [
327 {
328 "id": "did:web:reader.example.com#enc-1",
329 "type": "Multikey",
330 "controller": "did:web:reader.example.com",
331 "publicKeyMultibase": multibase
332 },
333 {
334 "id": "did:web:reader.example.com#sign-as-enc",
335 "type": "Multikey",
336 "controller": "did:web:reader.example.com",
337 "publicKeyMultibase": "z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV"
338 }
339 ]
340 }))
341 .expect("the fixture document parses");
342
343 std::assert_eq!(
344 doc.key_agreement_x25519("enc-1").expect("the published key resolves"),
345 x25519_raw
346 );
347
348 let missing = doc.key_agreement_x25519("enc-9").expect_err("an unpublished kid refuses");
349 std::assert_eq!(missing.code(), "APH_E023");
350 std::assert!(std::format!("{missing}").contains("enc-9"));
351
352 let wrong_codec = doc
353 .key_agreement_x25519("sign-as-enc")
354 .expect_err("an Ed25519 key in the keyAgreement slot is not an encryption key");
355 std::assert_eq!(wrong_codec.code(), "APH_E023");
356 }
357}