Files: 59cd5210437ca134f6d0863cc9ba87847790c76f / did-ethr / src / lib.rs
19851 bytesRaw
1 | use async_trait::async_trait; |
2 | use serde_json::Value; |
3 | use std::collections::BTreeMap; |
4 | |
5 | use ssi::caip10::BlockchainAccountId; |
6 | use ssi::caip2::ChainId; |
7 | use ssi::did::{ |
8 | Context, Contexts, DIDMethod, Document, Source, VerificationMethod, VerificationMethodMap, |
9 | DEFAULT_CONTEXT, DIDURL, |
10 | }; |
11 | use ssi::did_resolve::{ |
12 | DIDResolver, DocumentMetadata, ResolutionInputMetadata, ResolutionMetadata, ERROR_INVALID_DID, |
13 | TYPE_DID_LD_JSON, |
14 | }; |
15 | |
16 | /// did:ethr DID Method |
17 | /// |
18 | /// [Specification](https://github.com/decentralized-identity/ethr-did-resolver/) |
19 | pub struct DIDEthr; |
20 | |
21 | fn parse_did(did: &str) -> Option<(i64, String)> { |
22 | // https://github.com/decentralized-identity/ethr-did-resolver/blob/master/doc/did-method-spec.md#method-specific-identifier |
23 | let (network, addr_or_pk) = match did.split(':').collect::<Vec<&str>>().as_slice() { |
24 | ["did", "ethr", addr_or_pk] => ("mainnet".to_string(), addr_or_pk.to_string()), |
25 | ["did", "ethr", network, addr_or_pk] => (network.to_string(), addr_or_pk.to_string()), |
26 | _ => return None, |
27 | }; |
28 | let network_chain_id = match &network[..] { |
29 | "mainnet" => 1, |
30 | "morden" => 2, |
31 | "ropsten" => 3, |
32 | "rinkeby" => 4, |
33 | "goerli" => 5, |
34 | "kovan" => 42, |
35 | network_chain_id if network_chain_id.starts_with("0x") => { |
36 | match i64::from_str_radix(&network_chain_id[2..], 16) { |
37 | Ok(chain_id) => chain_id, |
38 | Err(_) => return None, |
39 | } |
40 | } |
41 | _ => { |
42 | return None; |
43 | } |
44 | }; |
45 | Some((network_chain_id, addr_or_pk)) |
46 | } |
47 | |
48 | /// Resolve an Ethr DID that uses a public key hex string instead of an account address |
49 | fn resolve_pk( |
50 | did: &str, |
51 | chain_id: i64, |
52 | public_key_hex: &str, |
53 | ) -> ( |
54 | ResolutionMetadata, |
55 | Option<Document>, |
56 | Option<DocumentMetadata>, |
57 | ) { |
58 | let mut context = BTreeMap::new(); |
59 | context.insert( |
60 | "blockchainAccountId".to_string(), |
61 | Value::String("https://w3id.org/security#blockchainAccountId".to_string()), |
62 | ); |
63 | context.insert( |
64 | "EcdsaSecp256k1RecoveryMethod2020".to_string(), |
65 | Value::String("https://identity.foundation/EcdsaSecp256k1RecoverySignature2020#EcdsaSecp256k1RecoveryMethod2020".to_string()), |
66 | ); |
67 | context.insert( |
68 | "EcdsaSecp256k1VerificationKey2019".to_string(), |
69 | Value::String("https://w3id.org/security#EcdsaSecp256k1VerificationKey2019".to_string()), |
70 | ); |
71 | context.insert( |
72 | "publicKeyJwk".to_string(), |
73 | serde_json::json!({ |
74 | "@id": "https://w3id.org/security#publicKeyJwk", |
75 | "@type": "@json" |
76 | }), |
77 | ); |
78 | if !public_key_hex.starts_with("0x") { |
79 | return ( |
80 | ResolutionMetadata::from_error(ERROR_INVALID_DID), |
81 | None, |
82 | None, |
83 | ); |
84 | }; |
85 | let pk_bytes = match hex::decode(&public_key_hex[2..]) { |
86 | Ok(pk_bytes) => pk_bytes, |
87 | Err(_) => { |
88 | return ( |
89 | ResolutionMetadata::from_error(ERROR_INVALID_DID), |
90 | None, |
91 | None, |
92 | ) |
93 | } |
94 | }; |
95 | |
96 | let pk_jwk = match ssi::jwk::secp256k1_parse(&pk_bytes) { |
97 | Ok(pk_bytes) => pk_bytes, |
98 | Err(e) => { |
99 | return ( |
100 | ResolutionMetadata::from_error(&format!("Unable to parse key: {}", e)), |
101 | None, |
102 | None, |
103 | ); |
104 | } |
105 | }; |
106 | let account_address = match ssi::keccak_hash::hash_public_key_eip55(&pk_jwk) { |
107 | Ok(hash) => hash, |
108 | Err(e) => { |
109 | return ( |
110 | ResolutionMetadata::from_error(&format!("Unable to hash account address: {}", e)), |
111 | None, |
112 | None, |
113 | ) |
114 | } |
115 | }; |
116 | let blockchain_account_id = BlockchainAccountId { |
117 | account_address, |
118 | chain_id: ChainId { |
119 | namespace: "eip155".to_string(), |
120 | reference: chain_id.to_string(), |
121 | }, |
122 | }; |
123 | let vm_didurl = DIDURL { |
124 | did: did.to_string(), |
125 | fragment: Some("controller".to_string()), |
126 | ..Default::default() |
127 | }; |
128 | let key_vm_didurl = DIDURL { |
129 | did: did.to_string(), |
130 | fragment: Some("controllerKey".to_string()), |
131 | ..Default::default() |
132 | }; |
133 | let vm = VerificationMethod::Map(VerificationMethodMap { |
134 | id: vm_didurl.to_string(), |
135 | type_: "EcdsaSecp256k1RecoveryMethod2020".to_string(), |
136 | controller: did.to_string(), |
137 | blockchain_account_id: Some(blockchain_account_id.to_string()), |
138 | ..Default::default() |
139 | }); |
140 | let key_vm = VerificationMethod::Map(VerificationMethodMap { |
141 | id: key_vm_didurl.to_string(), |
142 | type_: "EcdsaSecp256k1VerificationKey2019".to_string(), |
143 | controller: did.to_string(), |
144 | public_key_jwk: Some(pk_jwk), |
145 | ..Default::default() |
146 | }); |
147 | |
148 | let doc = Document { |
149 | context: Contexts::Many(vec![ |
150 | Context::URI(DEFAULT_CONTEXT.to_string()), |
151 | Context::Object(context), |
152 | ]), |
153 | id: did.to_string(), |
154 | authentication: Some(vec![ |
155 | VerificationMethod::DIDURL(vm_didurl.clone()), |
156 | VerificationMethod::DIDURL(key_vm_didurl.clone()), |
157 | ]), |
158 | assertion_method: Some(vec![ |
159 | VerificationMethod::DIDURL(vm_didurl), |
160 | VerificationMethod::DIDURL(key_vm_didurl), |
161 | ]), |
162 | verification_method: Some(vec![vm, key_vm]), |
163 | ..Default::default() |
164 | }; |
165 | |
166 | let res_meta = ResolutionMetadata { |
167 | content_type: Some(TYPE_DID_LD_JSON.to_string()), |
168 | ..Default::default() |
169 | }; |
170 | let doc_meta = DocumentMetadata { |
171 | ..Default::default() |
172 | }; |
173 | (res_meta, Some(doc), Some(doc_meta)) |
174 | } |
175 | |
176 | |
177 | |
178 | impl DIDResolver for DIDEthr { |
179 | async fn resolve( |
180 | &self, |
181 | did: &str, |
182 | _input_metadata: &ResolutionInputMetadata, |
183 | ) -> ( |
184 | ResolutionMetadata, |
185 | Option<Document>, |
186 | Option<DocumentMetadata>, |
187 | ) { |
188 | let (chain_id, addr_or_pk) = match parse_did(did) { |
189 | Some(parsed) => parsed, |
190 | None => { |
191 | return ( |
192 | ResolutionMetadata::from_error(ERROR_INVALID_DID), |
193 | None, |
194 | None, |
195 | ) |
196 | } |
197 | }; |
198 | let account_address = match addr_or_pk.len() { |
199 | 42 => addr_or_pk, |
200 | 68 => return resolve_pk(did, chain_id, &addr_or_pk), |
201 | _ => { |
202 | return ( |
203 | ResolutionMetadata::from_error(ERROR_INVALID_DID), |
204 | None, |
205 | None, |
206 | ) |
207 | } |
208 | }; |
209 | |
210 | let mut context = BTreeMap::new(); |
211 | context.insert( |
212 | "blockchainAccountId".to_string(), |
213 | Value::String("https://w3id.org/security#blockchainAccountId".to_string()), |
214 | ); |
215 | context.insert( |
216 | "EcdsaSecp256k1RecoveryMethod2020".to_string(), |
217 | Value::String("https://identity.foundation/EcdsaSecp256k1RecoverySignature2020#EcdsaSecp256k1RecoveryMethod2020".to_string()), |
218 | ); |
219 | context.insert( |
220 | "Eip712Method2021".to_string(), |
221 | Value::String("https://w3id.org/security#Eip712Method2021".to_string()), |
222 | ); |
223 | |
224 | let blockchain_account_id = BlockchainAccountId { |
225 | account_address, |
226 | chain_id: ChainId { |
227 | namespace: "eip155".to_string(), |
228 | reference: chain_id.to_string(), |
229 | }, |
230 | }; |
231 | let vm_didurl = DIDURL { |
232 | did: did.to_string(), |
233 | fragment: Some("controller".to_string()), |
234 | ..Default::default() |
235 | }; |
236 | let eip712vm_didurl = DIDURL { |
237 | did: did.to_string(), |
238 | fragment: Some("Eip712Method2021".to_string()), |
239 | ..Default::default() |
240 | }; |
241 | let vm = VerificationMethod::Map(VerificationMethodMap { |
242 | id: vm_didurl.to_string(), |
243 | type_: "EcdsaSecp256k1RecoveryMethod2020".to_string(), |
244 | controller: did.to_string(), |
245 | blockchain_account_id: Some(blockchain_account_id.to_string()), |
246 | ..Default::default() |
247 | }); |
248 | let eip712vm = VerificationMethod::Map(VerificationMethodMap { |
249 | id: eip712vm_didurl.to_string(), |
250 | type_: "Eip712Method2021".to_string(), |
251 | controller: did.to_string(), |
252 | blockchain_account_id: Some(blockchain_account_id.to_string()), |
253 | ..Default::default() |
254 | }); |
255 | |
256 | let doc = Document { |
257 | context: Contexts::Many(vec![ |
258 | Context::URI(DEFAULT_CONTEXT.to_string()), |
259 | Context::Object(context), |
260 | ]), |
261 | id: did.to_string(), |
262 | authentication: Some(vec![ |
263 | VerificationMethod::DIDURL(vm_didurl.clone()), |
264 | VerificationMethod::DIDURL(eip712vm_didurl.clone()), |
265 | ]), |
266 | assertion_method: Some(vec![ |
267 | VerificationMethod::DIDURL(vm_didurl), |
268 | VerificationMethod::DIDURL(eip712vm_didurl), |
269 | ]), |
270 | verification_method: Some(vec![vm, eip712vm]), |
271 | ..Default::default() |
272 | }; |
273 | |
274 | let res_meta = ResolutionMetadata { |
275 | content_type: Some(TYPE_DID_LD_JSON.to_string()), |
276 | ..Default::default() |
277 | }; |
278 | |
279 | let doc_meta = DocumentMetadata { |
280 | ..Default::default() |
281 | }; |
282 | |
283 | (res_meta, Some(doc), Some(doc_meta)) |
284 | } |
285 | |
286 | fn to_did_method(&self) -> Option<&dyn DIDMethod> { |
287 | Some(self) |
288 | } |
289 | } |
290 | |
291 | impl DIDMethod for DIDEthr { |
292 | fn name(&self) -> &'static str { |
293 | "ethr" |
294 | } |
295 | |
296 | fn generate(&self, source: &Source) -> Option<String> { |
297 | let jwk = match source { |
298 | Source::Key(jwk) => jwk, |
299 | Source::KeyAndPattern(jwk, pattern) => { |
300 | if !pattern.is_empty() { |
301 | // TODO: support pattern |
302 | return None; |
303 | } |
304 | jwk |
305 | } |
306 | _ => return None, |
307 | }; |
308 | let hash = match ssi::keccak_hash::hash_public_key(jwk) { |
309 | Ok(hash) => hash, |
310 | _ => return None, |
311 | }; |
312 | let did = format!("did:ethr:{}", hash); |
313 | Some(did) |
314 | } |
315 | |
316 | fn to_resolver(&self) -> &dyn DIDResolver { |
317 | self |
318 | } |
319 | } |
320 | |
321 | |
322 | mod tests { |
323 | use super::*; |
324 | use serde_json::json; |
325 | use ssi::did_resolve::ResolutionInputMetadata; |
326 | use ssi::jwk::JWK; |
327 | |
328 | |
329 | fn jwk_to_did_ethr() { |
330 | let jwk: JWK = serde_json::from_value(json!({ |
331 | "alg": "ES256K-R", |
332 | "kty": "EC", |
333 | "crv": "secp256k1", |
334 | "x": "yclqMZ0MtyVkKm1eBh2AyaUtsqT0l5RJM3g4SzRT96A", |
335 | "y": "yQzUwKnftWCJPGs-faGaHiYi1sxA6fGJVw2Px_LCNe8", |
336 | })) |
337 | .unwrap(); |
338 | let did = DIDEthr.generate(&Source::Key(&jwk)).unwrap(); |
339 | assert_eq!(did, "did:ethr:0x2fbf1be19d90a29aea9363f4ef0b6bf1c4ff0758"); |
340 | } |
341 | |
342 | |
343 | async fn resolve_did_ethr() { |
344 | // https://github.com/decentralized-identity/ethr-did-resolver/blob/master/doc/did-method-spec.md#create-register |
345 | let (res_meta, doc_opt, _meta_opt) = DIDEthr |
346 | .resolve( |
347 | "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a", |
348 | &ResolutionInputMetadata::default(), |
349 | ) |
350 | .await; |
351 | assert_eq!(res_meta.error, None); |
352 | let doc = doc_opt.unwrap(); |
353 | eprintln!("{}", serde_json::to_string_pretty(&doc).unwrap()); |
354 | assert_eq!( |
355 | serde_json::to_value(doc).unwrap(), |
356 | json!({ |
357 | "@context": [ |
358 | "https://www.w3.org/ns/did/v1", |
359 | { |
360 | "blockchainAccountId": "https://w3id.org/security#blockchainAccountId", |
361 | "EcdsaSecp256k1RecoveryMethod2020": "https://identity.foundation/EcdsaSecp256k1RecoverySignature2020#EcdsaSecp256k1RecoveryMethod2020", |
362 | "Eip712Method2021": "https://w3id.org/security#Eip712Method2021" |
363 | } |
364 | ], |
365 | "id": "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a", |
366 | "verificationMethod": [{ |
367 | "id": "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#controller", |
368 | "type": "EcdsaSecp256k1RecoveryMethod2020", |
369 | "controller": "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a", |
370 | "blockchainAccountId": "eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a" |
371 | }, { |
372 | "id": "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#Eip712Method2021", |
373 | "type": "Eip712Method2021", |
374 | "controller": "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a", |
375 | "blockchainAccountId": "eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a" |
376 | }], |
377 | "authentication": [ |
378 | "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#controller", |
379 | "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#Eip712Method2021" |
380 | ], |
381 | "assertionMethod": [ |
382 | "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#controller", |
383 | "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#Eip712Method2021" |
384 | ] |
385 | }) |
386 | ); |
387 | } |
388 | |
389 | |
390 | async fn resolve_did_ethr_pk() { |
391 | let (res_meta, doc_opt, _meta_opt) = DIDEthr |
392 | .resolve( |
393 | "did:ethr:0x03fdd57adec3d438ea237fe46b33ee1e016eda6b585c3e27ea66686c2ea5358479", |
394 | &ResolutionInputMetadata::default(), |
395 | ) |
396 | .await; |
397 | assert_eq!(res_meta.error, None); |
398 | let doc = doc_opt.unwrap(); |
399 | eprintln!("{}", serde_json::to_string_pretty(&doc).unwrap()); |
400 | let doc_expected: Value = |
401 | serde_json::from_str(include_str!("../tests/did-pk.jsonld")).unwrap(); |
402 | assert_eq!( |
403 | serde_json::to_value(doc).unwrap(), |
404 | serde_json::to_value(doc_expected).unwrap() |
405 | ); |
406 | } |
407 | |
408 | |
409 | async fn credential_prove_verify_did_ethr() { |
410 | eprintln!("with EcdsaSecp256k1RecoveryMethod2020..."); |
411 | credential_prove_verify_did_ethr2(false).await; |
412 | eprintln!("with Eip712Method2021..."); |
413 | credential_prove_verify_did_ethr2(true).await; |
414 | } |
415 | |
416 | async fn credential_prove_verify_did_ethr2(eip712: bool) { |
417 | use ssi::vc::{Credential, Issuer, LinkedDataProofOptions, URI}; |
418 | |
419 | let key: JWK = serde_json::from_value(json!({ |
420 | "alg": "ES256K-R", |
421 | "kty": "EC", |
422 | "crv": "secp256k1", |
423 | "x": "yclqMZ0MtyVkKm1eBh2AyaUtsqT0l5RJM3g4SzRT96A", |
424 | "y": "yQzUwKnftWCJPGs-faGaHiYi1sxA6fGJVw2Px_LCNe8", |
425 | "d": "meTmccmR_6ZsOa2YuTTkKkJ4ZPYsKdAH1Wx_RRf2j_E" |
426 | })) |
427 | .unwrap(); |
428 | let did = DIDEthr.generate(&Source::Key(&key)).unwrap(); |
429 | eprintln!("did: {}", did); |
430 | let mut vc: Credential = serde_json::from_value(json!({ |
431 | "@context": "https://www.w3.org/2018/credentials/v1", |
432 | "type": "VerifiableCredential", |
433 | "issuer": did.clone(), |
434 | "issuanceDate": "2021-02-18T20:23:13Z", |
435 | "credentialSubject": { |
436 | "id": "did:example:foo" |
437 | } |
438 | })) |
439 | .unwrap(); |
440 | vc.validate_unsigned().unwrap(); |
441 | let mut issue_options = LinkedDataProofOptions::default(); |
442 | if eip712 { |
443 | issue_options.verification_method = |
444 | Some(URI::String(did.to_string() + "#Eip712Method2021")); |
445 | } else { |
446 | issue_options.verification_method = Some(URI::String(did.to_string() + "#controller")); |
447 | } |
448 | eprintln!("vm {:?}", issue_options.verification_method); |
449 | let vc_no_proof = vc.clone(); |
450 | let proof = vc |
451 | .generate_proof(&key, &issue_options, &DIDEthr) |
452 | .await |
453 | .unwrap(); |
454 | println!("{}", serde_json::to_string_pretty(&proof).unwrap()); |
455 | vc.add_proof(proof); |
456 | vc.validate().unwrap(); |
457 | let verification_result = vc.verify(None, &DIDEthr).await; |
458 | println!("{:#?}", verification_result); |
459 | assert!(verification_result.errors.is_empty()); |
460 | |
461 | // test that issuer property is used for verification |
462 | let mut vc_bad_issuer = vc.clone(); |
463 | vc_bad_issuer.issuer = Some(Issuer::URI(URI::String("did:example:bad".to_string()))); |
464 | assert!(vc_bad_issuer.verify(None, &DIDEthr).await.errors.len() > 0); |
465 | |
466 | // Check that proof JWK must match proof verificationMethod |
467 | let mut vc_wrong_key = vc_no_proof.clone(); |
468 | let other_key = JWK::generate_ed25519().unwrap(); |
469 | use ssi::ldp::ProofSuite; |
470 | let proof_bad = ssi::ldp::Ed25519BLAKE2BDigestSize20Base58CheckEncodedSignature2021 |
471 | .sign(&vc_no_proof, &issue_options, &DIDEthr, &other_key, None) |
472 | .await |
473 | .unwrap(); |
474 | vc_wrong_key.add_proof(proof_bad); |
475 | vc_wrong_key.validate().unwrap(); |
476 | assert!(vc_wrong_key.verify(None, &DIDEthr).await.errors.len() > 0); |
477 | |
478 | // Make it into a VP |
479 | use ssi::one_or_many::OneOrMany; |
480 | use ssi::vc::{CredentialOrJWT, Presentation, ProofPurpose, DEFAULT_CONTEXT}; |
481 | let mut vp = Presentation { |
482 | context: ssi::vc::Contexts::Many(vec![ssi::vc::Context::URI(ssi::vc::URI::String( |
483 | DEFAULT_CONTEXT.to_string(), |
484 | ))]), |
485 | |
486 | id: Some(URI::String( |
487 | "http://example.org/presentations/3731".to_string(), |
488 | )), |
489 | type_: OneOrMany::One("VerifiablePresentation".to_string()), |
490 | verifiable_credential: Some(OneOrMany::One(CredentialOrJWT::Credential(vc))), |
491 | proof: None, |
492 | holder: None, |
493 | property_set: None, |
494 | }; |
495 | let mut vp_issue_options = LinkedDataProofOptions::default(); |
496 | vp.holder = Some(URI::String(did.to_string())); |
497 | vp_issue_options.verification_method = Some(URI::String(did.to_string() + "#controller")); |
498 | vp_issue_options.proof_purpose = Some(ProofPurpose::Authentication); |
499 | let vp_proof = vp |
500 | .generate_proof(&key, &vp_issue_options, &DIDEthr) |
501 | .await |
502 | .unwrap(); |
503 | vp.add_proof(vp_proof); |
504 | println!("VP: {}", serde_json::to_string_pretty(&vp).unwrap()); |
505 | vp.validate().unwrap(); |
506 | let vp_verification_result = vp.verify(Some(vp_issue_options.clone()), &DIDEthr).await; |
507 | println!("{:#?}", vp_verification_result); |
508 | assert!(vp_verification_result.errors.is_empty()); |
509 | |
510 | // mess with the VP proof to make verify fail |
511 | let mut vp1 = vp.clone(); |
512 | match vp1.proof { |
513 | Some(OneOrMany::One(ref mut proof)) => match proof.jws { |
514 | Some(ref mut jws) => { |
515 | jws.insert(0, 'x'); |
516 | } |
517 | _ => unreachable!(), |
518 | }, |
519 | _ => unreachable!(), |
520 | } |
521 | let vp_verification_result = vp1.verify(Some(vp_issue_options), &DIDEthr).await; |
522 | println!("{:#?}", vp_verification_result); |
523 | assert!(vp_verification_result.errors.len() >= 1); |
524 | |
525 | // test that holder is verified |
526 | let mut vp2 = vp.clone(); |
527 | vp2.holder = Some(URI::String("did:example:bad".to_string())); |
528 | assert!(vp2.verify(None, &DIDEthr).await.errors.len() > 0); |
529 | } |
530 | |
531 | |
532 | async fn credential_verify_eip712vm() { |
533 | use ssi::vc::Credential; |
534 | let vc: Credential = serde_json::from_str(include_str!("../tests/vc.jsonld")).unwrap(); |
535 | eprintln!("vc {:?}", vc); |
536 | let verification_result = vc.verify(None, &DIDEthr).await; |
537 | println!("{:#?}", verification_result); |
538 | assert!(verification_result.errors.is_empty()); |
539 | } |
540 | } |
541 |
Built with git-ssb-web