1use iref::Iri;
2use ssi_caips::caip10::BlockchainAccountId;
3use ssi_caips::caip2::ChainId;
4use ssi_dids_core::{
5 document::{
6 self,
7 representation::{self, MediaType},
8 DIDVerificationMethod,
9 },
10 resolution::{self, DIDMethodResolver, Error, Output},
11 DIDBuf, DIDMethod, DIDURLBuf, Document, DIDURL,
12};
13use static_iref::iri;
14use std::str::FromStr;
15
16mod json_ld_context;
17use json_ld_context::JsonLdContext;
18use ssi_jwk::JWK;
19
20pub struct DIDEthr;
24
25impl DIDEthr {
26 pub fn generate(jwk: &JWK) -> Result<DIDBuf, ssi_jwk::Error> {
27 let hash = ssi_jwk::eip155::hash_public_key(jwk)?;
28 Ok(DIDBuf::from_string(format!("did:ethr:{}", hash)).unwrap())
29 }
30}
31
32impl DIDMethod for DIDEthr {
33 const DID_METHOD_NAME: &'static str = "ethr";
34}
35
36impl DIDMethodResolver for DIDEthr {
37 async fn resolve_method_representation<'a>(
38 &'a self,
39 method_specific_id: &'a str,
40 options: resolution::Options,
41 ) -> Result<Output<Vec<u8>>, Error> {
42 let decoded_id = DecodedMethodSpecificId::from_str(method_specific_id)
43 .map_err(|_| Error::InvalidMethodSpecificId(method_specific_id.to_owned()))?;
44
45 let mut json_ld_context = JsonLdContext::default();
46
47 let doc = match decoded_id.address_or_public_key.len() {
48 42 => resolve_address(
49 &mut json_ld_context,
50 method_specific_id,
51 decoded_id.network_chain,
52 decoded_id.address_or_public_key,
53 ),
54 68 => resolve_public_key(
55 &mut json_ld_context,
56 method_specific_id,
57 decoded_id.network_chain,
58 &decoded_id.address_or_public_key,
59 ),
60 _ => Err(Error::InvalidMethodSpecificId(
61 method_specific_id.to_owned(),
62 )),
63 }?;
64
65 let content_type = options.accept.unwrap_or(MediaType::JsonLd);
66 let represented = doc.into_representation(representation::Options::from_media_type(
67 content_type,
68 move || representation::json_ld::Options {
69 context: representation::json_ld::Context::array(
70 representation::json_ld::DIDContext::V1,
71 json_ld_context.into_entries(),
72 ),
73 },
74 ));
75
76 Ok(resolution::Output::new(
77 represented.to_bytes(),
78 document::Metadata::default(),
79 resolution::Metadata::from_content_type(Some(content_type.to_string())),
80 ))
81 }
82}
83
84struct DecodedMethodSpecificId {
85 network_chain: NetworkChain,
86 address_or_public_key: String,
87}
88
89impl FromStr for DecodedMethodSpecificId {
90 type Err = InvalidNetwork;
91
92 fn from_str(method_specific_id: &str) -> Result<Self, Self::Err> {
93 let (network_name, address_or_public_key) = match method_specific_id.split_once(':') {
95 None => ("mainnet".to_string(), method_specific_id.to_string()),
96 Some((network, address_or_public_key)) => {
97 (network.to_string(), address_or_public_key.to_string())
98 }
99 };
100
101 Ok(DecodedMethodSpecificId {
102 network_chain: network_name.parse()?,
103 address_or_public_key,
104 })
105 }
106}
107
108#[derive(Debug, thiserror::Error)]
109#[error("invalid network `{0}`")]
110struct InvalidNetwork(String);
111
112enum NetworkChain {
113 Mainnet,
114 Morden,
115 Ropsten,
116 Rinkeby,
117 Georli,
118 Kovan,
119 Other(u64),
120}
121
122impl NetworkChain {
123 pub fn id(&self) -> u64 {
124 match self {
125 Self::Mainnet => 1,
126 Self::Morden => 2,
127 Self::Ropsten => 3,
128 Self::Rinkeby => 4,
129 Self::Georli => 5,
130 Self::Kovan => 42,
131 Self::Other(i) => *i,
132 }
133 }
134}
135
136impl FromStr for NetworkChain {
137 type Err = InvalidNetwork;
138
139 fn from_str(network_name: &str) -> Result<Self, Self::Err> {
140 match network_name {
141 "mainnet" => Ok(Self::Mainnet),
142 "morden" => Ok(Self::Morden),
143 "ropsten" => Ok(Self::Ropsten),
144 "rinkeby" => Ok(Self::Rinkeby),
145 "goerli" => Ok(Self::Georli),
146 "kovan" => Ok(Self::Kovan),
147 network_chain_id if network_chain_id.starts_with("0x") => {
148 match u64::from_str_radix(&network_chain_id[2..], 16) {
149 Ok(chain_id) => Ok(Self::Other(chain_id)),
150 Err(_) => Err(InvalidNetwork(network_name.to_owned())),
151 }
152 }
153 _ => Err(InvalidNetwork(network_name.to_owned())),
154 }
155 }
156}
157
158fn resolve_address(
159 json_ld_context: &mut JsonLdContext,
160 method_specific_id: &str,
161 network_chain: NetworkChain,
162 account_address: String,
163) -> Result<Document, Error> {
164 let blockchain_account_id = BlockchainAccountId {
165 account_address,
166 chain_id: ChainId {
167 namespace: "eip155".to_string(),
168 reference: network_chain.id().to_string(),
169 },
170 };
171
172 let did = DIDBuf::from_string(format!("did:ethr:{method_specific_id}")).unwrap();
173
174 let vm = VerificationMethod::EcdsaSecp256k1RecoveryMethod2020 {
175 id: DIDURLBuf::from_string(format!("{did}#controller")).unwrap(),
176 controller: did.to_owned(),
177 blockchain_account_id: blockchain_account_id.clone(),
178 };
179
180 let eip712_vm = VerificationMethod::Eip712Method2021 {
181 id: DIDURLBuf::from_string(format!("{did}#Eip712Method2021")).unwrap(),
182 controller: did.to_owned(),
183 blockchain_account_id,
184 };
185
186 json_ld_context.add_verification_method_type(vm.type_());
187 json_ld_context.add_verification_method_type(eip712_vm.type_());
188
189 let mut doc = Document::new(did);
190 doc.verification_relationships.assertion_method =
191 vec![vm.id().to_owned().into(), eip712_vm.id().to_owned().into()];
192 doc.verification_relationships.authentication =
193 vec![vm.id().to_owned().into(), eip712_vm.id().to_owned().into()];
194 doc.verification_method = vec![vm.into(), eip712_vm.into()];
195
196 Ok(doc)
197}
198
199fn resolve_public_key(
201 json_ld_context: &mut JsonLdContext,
202 method_specific_id: &str,
203 network_chain: NetworkChain,
204 public_key_hex: &str,
205) -> Result<Document, Error> {
206 if !public_key_hex.starts_with("0x") {
207 return Err(Error::InvalidMethodSpecificId(
208 method_specific_id.to_owned(),
209 ));
210 }
211
212 let pk_bytes = hex::decode(&public_key_hex[2..])
213 .map_err(|_| Error::InvalidMethodSpecificId(method_specific_id.to_owned()))?;
214
215 let pk_jwk = ssi_jwk::secp256k1_parse(&pk_bytes)
216 .map_err(|_| Error::InvalidMethodSpecificId(method_specific_id.to_owned()))?;
217
218 let account_address = ssi_jwk::eip155::hash_public_key_eip55(&pk_jwk)
219 .map_err(|_| Error::InvalidMethodSpecificId(method_specific_id.to_owned()))?;
220
221 let blockchain_account_id = BlockchainAccountId {
222 account_address,
223 chain_id: ChainId {
224 namespace: "eip155".to_string(),
225 reference: network_chain.id().to_string(),
226 },
227 };
228
229 let did = DIDBuf::from_string(format!("did:ethr:{method_specific_id}")).unwrap();
230
231 let vm = VerificationMethod::EcdsaSecp256k1RecoveryMethod2020 {
232 id: DIDURLBuf::from_string(format!("{did}#controller")).unwrap(),
233 controller: did.to_owned(),
234 blockchain_account_id,
235 };
236
237 let key_vm = VerificationMethod::EcdsaSecp256k1VerificationKey2019 {
238 id: DIDURLBuf::from_string(format!("{did}#controllerKey")).unwrap(),
239 controller: did.to_owned(),
240 public_key_jwk: pk_jwk,
241 };
242
243 json_ld_context.add_verification_method_type(vm.type_());
244 json_ld_context.add_verification_method_type(key_vm.type_());
245
246 let mut doc = Document::new(did);
247 doc.verification_relationships.assertion_method =
248 vec![vm.id().to_owned().into(), key_vm.id().to_owned().into()];
249 doc.verification_relationships.authentication =
250 vec![vm.id().to_owned().into(), key_vm.id().to_owned().into()];
251 doc.verification_method = vec![vm.into(), key_vm.into()];
252
253 Ok(doc)
254}
255
256#[allow(clippy::large_enum_variant)]
257pub enum VerificationMethod {
258 EcdsaSecp256k1VerificationKey2019 {
259 id: DIDURLBuf,
260 controller: DIDBuf,
261 public_key_jwk: JWK,
262 },
263 EcdsaSecp256k1RecoveryMethod2020 {
264 id: DIDURLBuf,
265 controller: DIDBuf,
266 blockchain_account_id: BlockchainAccountId,
267 },
268 Eip712Method2021 {
269 id: DIDURLBuf,
270 controller: DIDBuf,
271 blockchain_account_id: BlockchainAccountId,
272 },
273}
274
275impl VerificationMethod {
276 pub fn id(&self) -> &DIDURL {
277 match self {
278 Self::EcdsaSecp256k1VerificationKey2019 { id, .. } => id,
279 Self::EcdsaSecp256k1RecoveryMethod2020 { id, .. } => id,
280 Self::Eip712Method2021 { id, .. } => id,
281 }
282 }
283
284 pub fn type_(&self) -> VerificationMethodType {
285 match self {
286 Self::EcdsaSecp256k1VerificationKey2019 { .. } => {
287 VerificationMethodType::EcdsaSecp256k1VerificationKey2019
288 }
289 Self::EcdsaSecp256k1RecoveryMethod2020 { .. } => {
290 VerificationMethodType::EcdsaSecp256k1RecoveryMethod2020
291 }
292 Self::Eip712Method2021 { .. } => VerificationMethodType::Eip712Method2021,
293 }
294 }
295}
296
297pub enum VerificationMethodType {
298 EcdsaSecp256k1VerificationKey2019,
299 EcdsaSecp256k1RecoveryMethod2020,
300 Eip712Method2021,
301}
302
303impl VerificationMethodType {
304 pub fn name(&self) -> &'static str {
305 match self {
306 Self::EcdsaSecp256k1VerificationKey2019 => "EcdsaSecp256k1VerificationKey2019",
307 Self::EcdsaSecp256k1RecoveryMethod2020 => "EcdsaSecp256k1RecoveryMethod2020",
308 Self::Eip712Method2021 => "Eip712Method2021",
309 }
310 }
311
312 pub fn iri(&self) -> &'static Iri {
313 match self {
314 Self::EcdsaSecp256k1VerificationKey2019 => iri!("https://w3id.org/security#EcdsaSecp256k1VerificationKey2019"),
315 Self::EcdsaSecp256k1RecoveryMethod2020 => iri!("https://identity.foundation/EcdsaSecp256k1RecoverySignature2020#EcdsaSecp256k1RecoveryMethod2020"),
316 Self::Eip712Method2021 => iri!("https://w3id.org/security#Eip712Method2021")
317 }
318 }
319}
320
321impl From<VerificationMethod> for DIDVerificationMethod {
322 fn from(value: VerificationMethod) -> Self {
323 match value {
324 VerificationMethod::EcdsaSecp256k1VerificationKey2019 {
325 id,
326 controller,
327 public_key_jwk,
328 } => Self {
329 id,
330 type_: "EcdsaSecp256k1VerificationKey2019".to_owned(),
331 controller,
332 properties: [(
333 "publicKeyJwk".into(),
334 serde_json::to_value(&public_key_jwk).unwrap(),
335 )]
336 .into_iter()
337 .collect(),
338 },
339 VerificationMethod::EcdsaSecp256k1RecoveryMethod2020 {
340 id,
341 controller,
342 blockchain_account_id,
343 } => Self {
344 id,
345 type_: "EcdsaSecp256k1RecoveryMethod2020".to_owned(),
346 controller,
347 properties: [(
348 "blockchainAccountId".into(),
349 blockchain_account_id.to_string().into(),
350 )]
351 .into_iter()
352 .collect(),
353 },
354 VerificationMethod::Eip712Method2021 {
355 id,
356 controller,
357 blockchain_account_id,
358 } => Self {
359 id,
360 type_: "Eip712Method2021".to_owned(),
361 controller,
362 properties: [(
363 "blockchainAccountId".into(),
364 blockchain_account_id.to_string().into(),
365 )]
366 .into_iter()
367 .collect(),
368 },
369 }
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376 use iref::IriBuf;
377 use serde_json::json;
378 use ssi_claims::{
379 data_integrity::{
380 signing::AlterSignature, AnyInputSuiteOptions, AnySuite, CryptographicSuite,
381 ProofOptions,
382 },
383 vc::v1::{JsonCredential, JsonPresentation},
384 VerificationParameters,
385 };
386 use ssi_dids_core::{did, DIDResolver};
387 use ssi_jwk::JWK;
388 use ssi_verification_methods_core::{ProofPurpose, ReferenceOrOwned, SingleSecretSigner};
389 use static_iref::uri;
390
391 #[test]
392 fn jwk_to_did_ethr() {
393 let jwk: JWK = serde_json::from_value(json!({
394 "alg": "ES256K-R",
395 "kty": "EC",
396 "crv": "secp256k1",
397 "x": "yclqMZ0MtyVkKm1eBh2AyaUtsqT0l5RJM3g4SzRT96A",
398 "y": "yQzUwKnftWCJPGs-faGaHiYi1sxA6fGJVw2Px_LCNe8",
399 }))
400 .unwrap();
401 let did = DIDEthr::generate(&jwk).unwrap();
402 assert_eq!(did, "did:ethr:0x2fbf1be19d90a29aea9363f4ef0b6bf1c4ff0758");
403 }
404
405 #[tokio::test]
406 async fn resolve_did_ethr_addr() {
407 let doc = DIDEthr
409 .resolve(did!("did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a"))
410 .await
411 .unwrap()
412 .document;
413 eprintln!("{}", serde_json::to_string_pretty(&doc).unwrap());
414 assert_eq!(
415 serde_json::to_value(doc).unwrap(),
416 json!({
417 "@context": [
418 "https://www.w3.org/ns/did/v1",
419 {
420 "blockchainAccountId": "https://w3id.org/security#blockchainAccountId",
421 "EcdsaSecp256k1RecoveryMethod2020": "https://identity.foundation/EcdsaSecp256k1RecoverySignature2020#EcdsaSecp256k1RecoveryMethod2020",
422 "Eip712Method2021": "https://w3id.org/security#Eip712Method2021"
423 }
424 ],
425 "id": "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a",
426 "verificationMethod": [{
427 "id": "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#controller",
428 "type": "EcdsaSecp256k1RecoveryMethod2020",
429 "controller": "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a",
430 "blockchainAccountId": "eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a"
431 }, {
432 "id": "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#Eip712Method2021",
433 "type": "Eip712Method2021",
434 "controller": "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a",
435 "blockchainAccountId": "eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a"
436 }],
437 "authentication": [
438 "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#controller",
439 "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#Eip712Method2021"
440 ],
441 "assertionMethod": [
442 "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#controller",
443 "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#Eip712Method2021"
444 ]
445 })
446 );
447 }
448
449 #[tokio::test]
450 async fn resolve_did_ethr_pk() {
451 let doc = DIDEthr
452 .resolve(did!(
453 "did:ethr:0x03fdd57adec3d438ea237fe46b33ee1e016eda6b585c3e27ea66686c2ea5358479"
454 ))
455 .await
456 .unwrap()
457 .document;
458 eprintln!("{}", serde_json::to_string_pretty(&doc).unwrap());
459 let doc_expected: serde_json::Value =
460 serde_json::from_str(include_str!("../tests/did-pk.jsonld")).unwrap();
461 assert_eq!(
462 serde_json::to_value(doc).unwrap(),
463 serde_json::to_value(doc_expected).unwrap()
464 );
465 }
466
467 #[tokio::test]
468 async fn credential_prove_verify_did_ethr() {
469 eprintln!("with EcdsaSecp256k1RecoveryMethod2020...");
470 credential_prove_verify_did_ethr2(false).await;
471 eprintln!("with Eip712Method2021...");
472 credential_prove_verify_did_ethr2(true).await;
473 }
474
475 async fn credential_prove_verify_did_ethr2(eip712: bool) {
476 let didethr = DIDEthr.into_vm_resolver();
477 let verifier = VerificationParameters::from_resolver(&didethr);
478 let key: JWK = serde_json::from_value(json!({
479 "alg": "ES256K-R",
480 "kty": "EC",
481 "crv": "secp256k1",
482 "x": "yclqMZ0MtyVkKm1eBh2AyaUtsqT0l5RJM3g4SzRT96A",
483 "y": "yQzUwKnftWCJPGs-faGaHiYi1sxA6fGJVw2Px_LCNe8",
484 "d": "meTmccmR_6ZsOa2YuTTkKkJ4ZPYsKdAH1Wx_RRf2j_E"
485 }))
486 .unwrap();
487
488 let did = DIDEthr::generate(&key).unwrap();
489 eprintln!("did: {}", did);
490
491 let cred = JsonCredential::new(
492 None,
493 did.clone().into_uri().into(),
494 "2021-02-18T20:23:13Z".parse().unwrap(),
495 vec![json_syntax::json!({
496 "id": "did:example:foo"
497 })],
498 );
499
500 let verification_method = if eip712 {
501 ReferenceOrOwned::Reference(IriBuf::new(format!("{did}#Eip712Method2021")).unwrap())
502 } else {
503 ReferenceOrOwned::Reference(IriBuf::new(format!("{did}#controller")).unwrap())
504 };
505
506 let suite = AnySuite::pick(&key, Some(&verification_method)).unwrap();
507 let issue_options = ProofOptions::new(
508 "2021-02-18T20:23:13Z".parse().unwrap(),
509 verification_method,
510 ProofPurpose::Assertion,
511 AnyInputSuiteOptions::default(),
512 );
513
514 eprintln!("vm {:?}", issue_options.verification_method);
515 let signer = SingleSecretSigner::new(key).into_local();
516 let vc = suite
517 .sign(cred.clone(), &didethr, &signer, issue_options.clone())
518 .await
519 .unwrap();
520 println!(
521 "proof: {}",
522 serde_json::to_string_pretty(&vc.proofs).unwrap()
523 );
524 if eip712 {
525 assert_eq!(vc.proofs.first().unwrap().signature.as_ref(), "0xd3f4a049551fd25c7fb0789c7303be63265e8ade2630747de3807710382bbb7a25b0407e9f858a771782c35b4f487f4337341e9a4375a073730bda643895964e1b")
526 } else {
527 assert_eq!(vc.proofs.first().unwrap().signature.as_ref(), "eyJhbGciOiJFUzI1NkstUiIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9..nwNfIHhCQlI-j58zgqwJgX2irGJNP8hqLis-xS16hMwzs3OuvjqzZIHlwvdzDMPopUA_Oq7M7Iql2LNe0B22oQE");
528 }
529 assert!(vc.verify(&verifier).await.unwrap().is_ok());
530
531 let mut vc_bad_issuer = vc.clone();
533 vc_bad_issuer.issuer = uri!("did:pkh:example:bad").to_owned().into();
534
535 assert!(vc_bad_issuer.verify(&verifier).await.unwrap().is_err());
537
538 let wrong_key = JWK::generate_secp256k1();
540 let wrong_signer = SingleSecretSigner::new(wrong_key.clone()).into_local();
541 let vc_wrong_key = suite
542 .sign(
543 cred,
544 &didethr,
545 &wrong_signer,
546 ProofOptions {
547 options: AnyInputSuiteOptions::default()
548 .with_public_key(wrong_key.to_public())
549 .unwrap(),
550 ..issue_options
551 },
552 )
553 .await
554 .unwrap();
555 assert!(vc_wrong_key.verify(&verifier).await.unwrap().is_err());
556
557 let presentation = JsonPresentation::new(
559 Some(uri!("http://example.org/presentations/3731").to_owned()),
560 None,
561 vec![vc],
562 );
563
564 let vp_issue_options = ProofOptions::new(
565 "2021-02-18T20:23:13Z".parse().unwrap(),
566 IriBuf::new(format!("{did}#controller")).unwrap().into(),
567 ProofPurpose::Authentication,
568 AnyInputSuiteOptions::default(),
569 );
570
571 let vp = suite
572 .sign(presentation, &didethr, &signer, vp_issue_options)
573 .await
574 .unwrap();
575
576 println!("VP: {}", serde_json::to_string_pretty(&vp).unwrap());
577 assert!(vp.verify(&verifier).await.unwrap().is_ok());
578
579 let mut vp_fuzzed = vp.clone();
581 vp_fuzzed.proofs.first_mut().unwrap().signature.alter();
582 let vp_fuzzed_result = vp_fuzzed.verify(&verifier).await;
583 assert!(vp_fuzzed_result.is_err() || vp_fuzzed_result.is_ok_and(|v| v.is_err()));
584
585 let mut vp_bad_holder = vp;
587 vp_bad_holder.holder = Some(uri!("did:pkh:example:bad").to_owned().into());
588
589 assert!(vp_bad_holder.verify(&verifier).await.unwrap().is_err());
591 }
592
593 #[tokio::test]
594 async fn credential_verify_eip712vm() {
595 let didethr = DIDEthr.into_vm_resolver();
596 let vc = ssi_claims::vc::v1::data_integrity::any_credential_from_json_str(include_str!(
597 "../tests/vc.jsonld"
598 ))
599 .unwrap();
600 assert!(vc
602 .verify(VerificationParameters::from_resolver(didethr))
603 .await
604 .unwrap()
605 .is_ok())
606 }
607}