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::{
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        // https://github.com/decentralized-identity/ethr-did-resolver/blob/master/doc/did-method-spec.md#create-register
411        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        // test that issuer property is used for verification
535        let mut vc_bad_issuer = vc.clone();
536        vc_bad_issuer.issuer = uri!("did:pkh:example:bad").to_owned().into();
537
538        // It should fail.
539        assert!(vc_bad_issuer.verify(&verifier).await.unwrap().is_err());
540
541        // Check that proof JWK must match proof verificationMethod
542        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        // Make it into a VP
561        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        // Mess with proof signature to make verify fail.
583        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        // test that holder is verified
589        let mut vp_bad_holder = vp;
590        vp_bad_holder.holder = Some(uri!("did:pkh:example:bad").to_owned());
591
592        // It should fail.
593        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        // eprintln!("vc {:?}", vc);
604        assert!(vc
605            .verify(VerificationParameters::from_resolver(didethr))
606            .await
607            .unwrap()
608            .is_ok())
609    }
610}