git ssb

0+

Spruce git-ssb bridge / ssi



Tree: 59cd5210437ca134f6d0863cc9ba87847790c76f

Files: 59cd5210437ca134f6d0863cc9ba87847790c76f / did-ethr / src / lib.rs

19851 bytesRaw
1use async_trait::async_trait;
2use serde_json::Value;
3use std::collections::BTreeMap;
4
5use ssi::caip10::BlockchainAccountId;
6use ssi::caip2::ChainId;
7use ssi::did::{
8 Context, Contexts, DIDMethod, Document, Source, VerificationMethod, VerificationMethodMap,
9 DEFAULT_CONTEXT, DIDURL,
10};
11use 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/)
19pub struct DIDEthr;
20
21fn 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
49fn 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#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
177#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
178impl 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
291impl 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#[cfg(test)]
322mod tests {
323 use super::*;
324 use serde_json::json;
325 use ssi::did_resolve::ResolutionInputMetadata;
326 use ssi::jwk::JWK;
327
328 #[test]
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 #[tokio::test]
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 #[tokio::test]
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 #[tokio::test]
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 #[tokio::test]
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