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::{
384 syntax::NonEmptyVec,
385 v1::{JsonCredential, JsonPresentation},
386 },
387 VerificationParameters,
388 };
389 use ssi_dids_core::{did, DIDResolver};
390 use ssi_jwk::JWK;
391 use ssi_verification_methods_core::{ProofPurpose, ReferenceOrOwned, SingleSecretSigner};
392 use static_iref::uri;
393
394 #[test]
395 fn jwk_to_did_ethr() {
396 let jwk: JWK = serde_json::from_value(json!({
397 "alg": "ES256K-R",
398 "kty": "EC",
399 "crv": "secp256k1",
400 "x": "yclqMZ0MtyVkKm1eBh2AyaUtsqT0l5RJM3g4SzRT96A",
401 "y": "yQzUwKnftWCJPGs-faGaHiYi1sxA6fGJVw2Px_LCNe8",
402 }))
403 .unwrap();
404 let did = DIDEthr::generate(&jwk).unwrap();
405 assert_eq!(did, "did:ethr:0x2fbf1be19d90a29aea9363f4ef0b6bf1c4ff0758");
406 }
407
408 #[tokio::test]
409 async fn resolve_did_ethr_addr() {
410 let doc = DIDEthr
412 .resolve(did!("did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a"))
413 .await
414 .unwrap()
415 .document;
416 eprintln!("{}", serde_json::to_string_pretty(&doc).unwrap());
417 assert_eq!(
418 serde_json::to_value(doc).unwrap(),
419 json!({
420 "@context": [
421 "https://www.w3.org/ns/did/v1",
422 {
423 "blockchainAccountId": "https://w3id.org/security#blockchainAccountId",
424 "EcdsaSecp256k1RecoveryMethod2020": "https://identity.foundation/EcdsaSecp256k1RecoverySignature2020#EcdsaSecp256k1RecoveryMethod2020",
425 "Eip712Method2021": "https://w3id.org/security#Eip712Method2021"
426 }
427 ],
428 "id": "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a",
429 "verificationMethod": [{
430 "id": "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#controller",
431 "type": "EcdsaSecp256k1RecoveryMethod2020",
432 "controller": "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a",
433 "blockchainAccountId": "eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a"
434 }, {
435 "id": "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#Eip712Method2021",
436 "type": "Eip712Method2021",
437 "controller": "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a",
438 "blockchainAccountId": "eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a"
439 }],
440 "authentication": [
441 "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#controller",
442 "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#Eip712Method2021"
443 ],
444 "assertionMethod": [
445 "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#controller",
446 "did:ethr:0xb9c5714089478a327f09197987f16f9e5d936e8a#Eip712Method2021"
447 ]
448 })
449 );
450 }
451
452 #[tokio::test]
453 async fn resolve_did_ethr_pk() {
454 let doc = DIDEthr
455 .resolve(did!(
456 "did:ethr:0x03fdd57adec3d438ea237fe46b33ee1e016eda6b585c3e27ea66686c2ea5358479"
457 ))
458 .await
459 .unwrap()
460 .document;
461 eprintln!("{}", serde_json::to_string_pretty(&doc).unwrap());
462 let doc_expected: serde_json::Value =
463 serde_json::from_str(include_str!("../tests/did-pk.jsonld")).unwrap();
464 assert_eq!(
465 serde_json::to_value(doc).unwrap(),
466 serde_json::to_value(doc_expected).unwrap()
467 );
468 }
469
470 #[tokio::test]
471 async fn credential_prove_verify_did_ethr() {
472 eprintln!("with EcdsaSecp256k1RecoveryMethod2020...");
473 credential_prove_verify_did_ethr2(false).await;
474 eprintln!("with Eip712Method2021...");
475 credential_prove_verify_did_ethr2(true).await;
476 }
477
478 async fn credential_prove_verify_did_ethr2(eip712: bool) {
479 let didethr = DIDEthr.into_vm_resolver();
480 let verifier = VerificationParameters::from_resolver(&didethr);
481 let key: JWK = serde_json::from_value(json!({
482 "alg": "ES256K-R",
483 "kty": "EC",
484 "crv": "secp256k1",
485 "x": "yclqMZ0MtyVkKm1eBh2AyaUtsqT0l5RJM3g4SzRT96A",
486 "y": "yQzUwKnftWCJPGs-faGaHiYi1sxA6fGJVw2Px_LCNe8",
487 "d": "meTmccmR_6ZsOa2YuTTkKkJ4ZPYsKdAH1Wx_RRf2j_E"
488 }))
489 .unwrap();
490
491 let did = DIDEthr::generate(&key).unwrap();
492 eprintln!("did: {}", did);
493
494 let cred = JsonCredential::new(
495 None,
496 did.clone().into_uri().into(),
497 "2021-02-18T20:23:13Z".parse().unwrap(),
498 NonEmptyVec::new(json_syntax::json!({
499 "id": "did:example:foo"
500 })),
501 );
502
503 let verification_method = if eip712 {
504 ReferenceOrOwned::Reference(IriBuf::new(format!("{did}#Eip712Method2021")).unwrap())
505 } else {
506 ReferenceOrOwned::Reference(IriBuf::new(format!("{did}#controller")).unwrap())
507 };
508
509 let suite = AnySuite::pick(&key, Some(&verification_method)).unwrap();
510 let issue_options = ProofOptions::new(
511 "2021-02-18T20:23:13Z".parse().unwrap(),
512 verification_method,
513 ProofPurpose::Assertion,
514 AnyInputSuiteOptions::default(),
515 );
516
517 eprintln!("vm {:?}", issue_options.verification_method);
518 let signer = SingleSecretSigner::new(key).into_local();
519 let vc = suite
520 .sign(cred.clone(), &didethr, &signer, issue_options.clone())
521 .await
522 .unwrap();
523 println!(
524 "proof: {}",
525 serde_json::to_string_pretty(&vc.proofs).unwrap()
526 );
527 if eip712 {
528 assert_eq!(vc.proofs.first().unwrap().signature.as_ref(), "0xd3f4a049551fd25c7fb0789c7303be63265e8ade2630747de3807710382bbb7a25b0407e9f858a771782c35b4f487f4337341e9a4375a073730bda643895964e1b")
529 } else {
530 assert_eq!(vc.proofs.first().unwrap().signature.as_ref(), "eyJhbGciOiJFUzI1NkstUiIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9..nwNfIHhCQlI-j58zgqwJgX2irGJNP8hqLis-xS16hMwzs3OuvjqzZIHlwvdzDMPopUA_Oq7M7Iql2LNe0B22oQE");
531 }
532 assert!(vc.verify(&verifier).await.unwrap().is_ok());
533
534 let mut vc_bad_issuer = vc.clone();
536 vc_bad_issuer.issuer = uri!("did:pkh:example:bad").to_owned().into();
537
538 assert!(vc_bad_issuer.verify(&verifier).await.unwrap().is_err());
540
541 let wrong_key = JWK::generate_secp256k1();
543 let wrong_signer = SingleSecretSigner::new(wrong_key.clone()).into_local();
544 let vc_wrong_key = suite
545 .sign(
546 cred,
547 &didethr,
548 &wrong_signer,
549 ProofOptions {
550 options: AnyInputSuiteOptions::default()
551 .with_public_key(wrong_key.to_public())
552 .unwrap(),
553 ..issue_options
554 },
555 )
556 .await
557 .unwrap();
558 assert!(vc_wrong_key.verify(&verifier).await.unwrap().is_err());
559
560 let presentation = JsonPresentation::new(
562 Some(uri!("http://example.org/presentations/3731").to_owned()),
563 None,
564 vec![vc],
565 );
566
567 let vp_issue_options = ProofOptions::new(
568 "2021-02-18T20:23:13Z".parse().unwrap(),
569 IriBuf::new(format!("{did}#controller")).unwrap().into(),
570 ProofPurpose::Authentication,
571 AnyInputSuiteOptions::default(),
572 );
573
574 let vp = suite
575 .sign(presentation, &didethr, &signer, vp_issue_options)
576 .await
577 .unwrap();
578
579 println!("VP: {}", serde_json::to_string_pretty(&vp).unwrap());
580 assert!(vp.verify(&verifier).await.unwrap().is_ok());
581
582 let mut vp_fuzzed = vp.clone();
584 vp_fuzzed.proofs.first_mut().unwrap().signature.alter();
585 let vp_fuzzed_result = vp_fuzzed.verify(&verifier).await;
586 assert!(vp_fuzzed_result.is_err() || vp_fuzzed_result.is_ok_and(|v| v.is_err()));
587
588 let mut vp_bad_holder = vp;
590 vp_bad_holder.holder = Some(uri!("did:pkh:example:bad").to_owned());
591
592 assert!(vp_bad_holder.verify(&verifier).await.unwrap().is_err());
594 }
595
596 #[tokio::test]
597 async fn credential_verify_eip712vm() {
598 let didethr = DIDEthr.into_vm_resolver();
599 let vc = ssi_claims::vc::v1::data_integrity::any_credential_from_json_str(include_str!(
600 "../tests/vc.jsonld"
601 ))
602 .unwrap();
603 assert!(vc
605 .verify(VerificationParameters::from_resolver(didethr))
606 .await
607 .unwrap()
608 .is_ok())
609 }
610}