did_ethr/
lib.rs

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
20/// did:ethr DID Method
21///
22/// [Specification](https://github.com/decentralized-identity/ethr-did-resolver/)
23pub 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        // https://github.com/decentralized-identity/ethr-did-resolver/blob/master/doc/did-method-spec.md#method-specific-identifier
94        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
199/// Resolve an Ethr DID that uses a public key hex string instead of an account address
200fn 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        // https://github.com/decentralized-identity/ethr-did-resolver/blob/master/doc/did-method-spec.md#create-register
408        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        // test that issuer property is used for verification
532        let mut vc_bad_issuer = vc.clone();
533        vc_bad_issuer.issuer = uri!("did:pkh:example:bad").to_owned().into();
534
535        // It should fail.
536        assert!(vc_bad_issuer.verify(&verifier).await.unwrap().is_err());
537
538        // Check that proof JWK must match proof verificationMethod
539        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        // Make it into a VP
558        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        // Mess with proof signature to make verify fail.
580        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        // test that holder is verified
586        let mut vp_bad_holder = vp;
587        vp_bad_holder.holder = Some(uri!("did:pkh:example:bad").to_owned().into());
588
589        // It should fail.
590        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        // eprintln!("vc {:?}", vc);
601        assert!(vc
602            .verify(VerificationParameters::from_resolver(didethr))
603            .await
604            .unwrap()
605            .is_ok())
606    }
607}