did_sol/
lib.rs

1use async_trait::async_trait;
2use serde_json::Value;
3use std::collections::BTreeMap;
4
5use ssi_caips::caip10::BlockchainAccountId;
6use ssi_caips::caip2::ChainId;
7use ssi_dids::did_resolve::{
8    DIDResolver, DocumentMetadata, ResolutionInputMetadata, ResolutionMetadata, ERROR_INVALID_DID,
9    TYPE_DID_LD_JSON,
10};
11use ssi_dids::{
12    Context, Contexts, DIDMethod, Document, Source, VerificationMethod, VerificationMethodMap,
13    DEFAULT_CONTEXT, DIDURL,
14};
15use ssi_jwk::{Base64urlUInt, OctetParams, Params, JWK};
16
17// https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-30.md
18const REFERENCE_SOLANA_MAINNET: &str = "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ";
19
20/// did:sol DID Method
21pub struct DIDSol;
22
23fn parse_did(did: &str) -> Option<(String, Vec<u8>)> {
24    let address = match did.split(':').collect::<Vec<&str>>().as_slice() {
25        ["did", "sol", address] => address.to_string(),
26        _ => return None,
27    };
28    let bytes = match bs58::decode(&address).into_vec() {
29        Ok(bytes) => bytes,
30        Err(_) => return None,
31    };
32    if bytes.len() != 32 {
33        return None;
34    }
35    Some((address, bytes))
36}
37
38#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
39#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
40impl DIDResolver for DIDSol {
41    async fn resolve(
42        &self,
43        did: &str,
44        _input_metadata: &ResolutionInputMetadata,
45    ) -> (
46        ResolutionMetadata,
47        Option<Document>,
48        Option<DocumentMetadata>,
49    ) {
50        let (address, public_key_bytes) = match parse_did(did) {
51            Some(parsed) => parsed,
52            None => {
53                return (
54                    ResolutionMetadata::from_error(ERROR_INVALID_DID),
55                    None,
56                    None,
57                )
58            }
59        };
60        let mut context = BTreeMap::new();
61        context.insert(
62            "blockchainAccountId".to_string(),
63            Value::String("https://w3id.org/security#blockchainAccountId".to_string()),
64        );
65        context.insert(
66            "publicKeyJwk".to_string(),
67            serde_json::json!({
68                "@id": "https://w3id.org/security#publicKeyJwk",
69                "@type": "@json"
70            }),
71        );
72        context.insert(
73            "Ed25519VerificationKey2018".to_string(),
74            Value::String("https://w3id.org/security#Ed25519VerificationKey2018".to_string()),
75        );
76        context.insert(
77            "SolanaMethod2021".to_string(),
78            Value::String("https://w3id.org/security#SolanaMethod2021".to_string()),
79        );
80        let blockchain_account_id = BlockchainAccountId {
81            account_address: address,
82            chain_id: ChainId {
83                namespace: "solana".to_string(),
84                reference: REFERENCE_SOLANA_MAINNET.to_string(),
85            },
86        };
87        let vm_didurl = DIDURL {
88            did: did.to_string(),
89            fragment: Some("controller".to_string()),
90            ..Default::default()
91        };
92        let solvm_didurl = DIDURL {
93            did: did.to_string(),
94            fragment: Some("SolanaMethod2021".to_string()),
95            ..Default::default()
96        };
97        let pk_jwk = JWK {
98            params: Params::OKP(OctetParams {
99                curve: "Ed25519".to_string(),
100                public_key: Base64urlUInt(public_key_bytes),
101                private_key: None,
102            }),
103            public_key_use: None,
104            key_operations: None,
105            algorithm: None,
106            key_id: None,
107            x509_url: None,
108            x509_certificate_chain: None,
109            x509_thumbprint_sha1: None,
110            x509_thumbprint_sha256: None,
111        };
112        let vm = VerificationMethod::Map(VerificationMethodMap {
113            id: vm_didurl.to_string(),
114            type_: "Ed25519VerificationKey2018".to_string(),
115            public_key_jwk: Some(pk_jwk.clone()),
116            controller: did.to_string(),
117            blockchain_account_id: Some(blockchain_account_id.to_string()),
118            ..Default::default()
119        });
120        let solvm = VerificationMethod::Map(VerificationMethodMap {
121            id: solvm_didurl.to_string(),
122            type_: "SolanaMethod2021".to_string(),
123            public_key_jwk: Some(pk_jwk),
124            controller: did.to_string(),
125            blockchain_account_id: Some(blockchain_account_id.to_string()),
126            ..Default::default()
127        });
128
129        let doc = Document {
130            context: Contexts::Many(vec![
131                Context::URI(DEFAULT_CONTEXT.into()),
132                Context::Object(context),
133            ]),
134            id: did.to_string(),
135            authentication: Some(vec![
136                VerificationMethod::DIDURL(vm_didurl.clone()),
137                VerificationMethod::DIDURL(solvm_didurl.clone()),
138            ]),
139            assertion_method: Some(vec![
140                VerificationMethod::DIDURL(vm_didurl),
141                VerificationMethod::DIDURL(solvm_didurl),
142            ]),
143            // TODO: authentication/assertion_method?
144            verification_method: Some(vec![vm, solvm]),
145            ..Default::default()
146        };
147
148        let res_meta = ResolutionMetadata {
149            content_type: Some(TYPE_DID_LD_JSON.to_string()),
150            ..Default::default()
151        };
152
153        let doc_meta = DocumentMetadata {
154            ..Default::default()
155        };
156
157        (res_meta, Some(doc), Some(doc_meta))
158    }
159
160    fn to_did_method(&self) -> Option<&dyn DIDMethod> {
161        Some(self)
162    }
163}
164
165impl DIDMethod for DIDSol {
166    fn name(&self) -> &'static str {
167        "sol"
168    }
169
170    fn generate(&self, source: &Source) -> Option<String> {
171        let jwk = match source {
172            Source::Key(jwk) => jwk,
173            Source::KeyAndPattern(jwk, pattern) => {
174                if !pattern.is_empty() {
175                    // pattern not supported
176                    return None;
177                }
178                jwk
179            }
180            _ => return None,
181        };
182        let did = match jwk.params {
183            Params::OKP(ref params) if params.curve == "Ed25519" => {
184                let addr = bs58::encode(&params.public_key.0).into_string();
185                format!("did:sol:{}", addr)
186            }
187            _ => {
188                dbg!(&jwk.params);
189                return None;
190            }
191        };
192        Some(did)
193    }
194
195    fn to_resolver(&self) -> &dyn DIDResolver {
196        self
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use serde_json::json;
204    use ssi_dids::did_resolve::ResolutionInputMetadata;
205    use ssi_jwk::JWK;
206    use ssi_ldp::{ProofSuite, ProofSuiteType};
207
208    #[test]
209    fn key_to_did_sol() {
210        let jwk: JWK = serde_json::from_value(json!({
211            "kty": "OKP",
212            "crv": "Ed25519",
213            "x": "qDkywhH-S6nNxQhA6SHKsoFW7A2gX-X0b3TtwVBMHm8"
214        }))
215        .unwrap();
216        let did = DIDSol.generate(&Source::Key(&jwk)).unwrap();
217        assert_eq!(did, "did:sol:CKg5d12Jhpej1JqtmxLJgaFqqeYjxgPqToJ4LBdvG9Ev");
218    }
219
220    #[tokio::test]
221    async fn resolve_did_sol() {
222        let (res_meta, doc_opt, _meta_opt) = DIDSol
223            .resolve(
224                "did:sol:CKg5d12Jhpej1JqtmxLJgaFqqeYjxgPqToJ4LBdvG9Ev",
225                &ResolutionInputMetadata::default(),
226            )
227            .await;
228        assert_eq!(res_meta.error, None);
229        let doc = doc_opt.unwrap();
230        eprintln!("{}", serde_json::to_string_pretty(&doc).unwrap());
231        let doc_expected: Document =
232            serde_json::from_str(include_str!("../tests/did.jsonld")).unwrap();
233        assert_eq!(
234            serde_json::to_value(doc).unwrap(),
235            serde_json::to_value(doc_expected).unwrap()
236        );
237    }
238
239    #[tokio::test]
240    async fn credential_prove_verify_did_sol() {
241        eprintln!("with Ed25519VerificationKey2018...");
242        credential_prove_verify_did_sol_1(false).await;
243        eprintln!("with SolanaMethod2021...");
244        credential_prove_verify_did_sol_1(true).await;
245    }
246
247    async fn credential_prove_verify_did_sol_1(solvm: bool) {
248        use ssi_vc::{Credential, Issuer, LinkedDataProofOptions, URI};
249
250        let key: JWK = serde_json::from_value(json!({
251            "kty": "OKP",
252            "crv": "Ed25519",
253            "x": "YwT3Ce5YqSjXK_bmI35kPEmzZT2WtUSE6XTllrLUGbQ",
254            "d": "ybizk5eZVSZiUtWURVdp-dx9JWtiP9uJaWfLupGU2ZU"
255        }))
256        .unwrap();
257        let did = DIDSol.generate(&Source::Key(&key)).unwrap();
258        eprintln!("did: {}", did);
259        let mut vc: Credential = serde_json::from_value(json!({
260            "@context": "https://www.w3.org/2018/credentials/v1",
261            "type": "VerifiableCredential",
262            "issuer": did.clone(),
263            "issuanceDate": "2021-02-18T20:23:13Z",
264            "credentialSubject": {
265                "id": "did:example:foo"
266            }
267        }))
268        .unwrap();
269        vc.validate_unsigned().unwrap();
270        let mut issue_options = LinkedDataProofOptions::default();
271        if solvm {
272            issue_options.verification_method =
273                Some(URI::String(did.to_string() + "#SolanaMethod2021"));
274        } else {
275            issue_options.verification_method = Some(URI::String(did.to_string() + "#controller"));
276        }
277        eprintln!("vm {:?}", issue_options.verification_method);
278        let mut context_loader = ssi_json_ld::ContextLoader::default();
279        let vc_no_proof = vc.clone();
280        let proof = vc
281            .generate_proof(&key, &issue_options, &DIDSol, &mut context_loader)
282            .await
283            .unwrap();
284        println!("{}", serde_json::to_string_pretty(&proof).unwrap());
285        vc.add_proof(proof);
286        vc.validate().unwrap();
287        let verification_result = vc.verify(None, &DIDSol, &mut context_loader).await;
288        println!("{:#?}", verification_result);
289        assert!(verification_result.errors.is_empty());
290
291        // test that issuer property is used for verification
292        let mut vc_bad_issuer = vc.clone();
293        vc_bad_issuer.issuer = Some(Issuer::URI(URI::String("did:example:bad".to_string())));
294        assert!(!vc_bad_issuer
295            .verify(None, &DIDSol, &mut context_loader)
296            .await
297            .errors
298            .is_empty());
299
300        // Check that proof JWK must match proof verificationMethod
301        let mut vc_wrong_key = vc_no_proof.clone();
302        let other_key = JWK::generate_ed25519().unwrap();
303        let proof_bad = ProofSuiteType::Ed25519BLAKE2BDigestSize20Base58CheckEncodedSignature2021
304            .sign(
305                &vc_no_proof,
306                &issue_options,
307                &DIDSol,
308                &mut context_loader,
309                &other_key,
310                None,
311            )
312            .await
313            .unwrap();
314        vc_wrong_key.add_proof(proof_bad);
315        vc_wrong_key.validate().unwrap();
316        assert!(!vc_wrong_key
317            .verify(None, &DIDSol, &mut context_loader)
318            .await
319            .errors
320            .is_empty());
321
322        // Make it into a VP
323        use ssi_core::one_or_many::OneOrMany;
324        use ssi_vc::{CredentialOrJWT, Presentation, ProofPurpose, DEFAULT_CONTEXT};
325        let mut vp = Presentation {
326            context: ssi_vc::Contexts::Many(vec![ssi_vc::Context::URI(ssi_vc::URI::String(
327                DEFAULT_CONTEXT.to_string(),
328            ))]),
329
330            id: Some("http://example.org/presentations/3731".try_into().unwrap()),
331            type_: OneOrMany::One("VerifiablePresentation".to_string()),
332            verifiable_credential: Some(OneOrMany::One(CredentialOrJWT::Credential(vc))),
333            proof: None,
334            holder: None,
335            property_set: None,
336            holder_binding: None,
337        };
338        let mut vp_issue_options = LinkedDataProofOptions::default();
339        vp.holder = Some(URI::String(did.to_string()));
340        vp_issue_options.verification_method = Some(URI::String(did.to_string() + "#controller"));
341        vp_issue_options.proof_purpose = Some(ProofPurpose::Authentication);
342        let mut context_loader = ssi_json_ld::ContextLoader::default();
343        let vp_proof = vp
344            .generate_proof(&key, &vp_issue_options, &DIDSol, &mut context_loader)
345            .await
346            .unwrap();
347        vp.add_proof(vp_proof);
348        println!("VP: {}", serde_json::to_string_pretty(&vp).unwrap());
349        vp.validate().unwrap();
350        let vp_verification_result = vp
351            .verify(Some(vp_issue_options.clone()), &DIDSol, &mut context_loader)
352            .await;
353        println!("{:#?}", vp_verification_result);
354        assert!(vp_verification_result.errors.is_empty());
355
356        // mess with the VP proof to make verify fail
357        let mut vp1 = vp.clone();
358        match vp1.proof {
359            Some(OneOrMany::One(ref mut proof)) => match proof.jws {
360                Some(ref mut jws) => {
361                    jws.insert(0, 'x');
362                }
363                _ => unreachable!(),
364            },
365            _ => unreachable!(),
366        }
367        let vp_verification_result = vp1
368            .verify(Some(vp_issue_options), &DIDSol, &mut context_loader)
369            .await;
370        println!("{:#?}", vp_verification_result);
371        assert!(!vp_verification_result.errors.is_empty());
372
373        // test that holder is verified
374        let mut vp2 = vp.clone();
375        vp2.holder = Some(URI::String("did:example:bad".to_string()));
376        assert!(!vp2
377            .verify(None, &DIDSol, &mut context_loader)
378            .await
379            .errors
380            .is_empty());
381    }
382
383    /*
384    #[tokio::test]
385    async fn credential_verify_eip712vm() {
386        use ssi_vc::Credential;
387        let vc: Credential = serde_json::from_str(include_str!("../tests/vc.jsonld")).unwrap();
388        eprintln!("vc {:?}", vc);
389        let verification_result = vc.verify(None, &DIDSol).await;
390        println!("{:#?}", verification_result);
391        assert!(verification_result.errors.is_empty());
392    }
393    */
394}