trustchain_api/
api.rs

1use crate::{TrustchainAPI, DATA_ATTRIBUTE, DATA_CREDENTIAL_TEMPLATE};
2use async_trait::async_trait;
3use did_ion::sidetree::DocumentState;
4use futures::{stream, StreamExt, TryStreamExt};
5use sha2::{Digest, Sha256};
6use ssi::{
7    did_resolve::DIDResolver,
8    jsonld::ContextLoader,
9    ldp::LinkedDataDocument,
10    vc::{Credential, CredentialOrJWT, LinkedDataProofOptions, Presentation, URI},
11};
12use std::error::Error;
13use trustchain_core::{
14    chain::DIDChain,
15    holder::Holder,
16    issuer::{Issuer, IssuerError},
17    resolver::{ResolverResult, TrustchainResolver},
18    vc::{CredentialError, DataCredentialError},
19    verifier::{Timestamp, Verifier, VerifierError},
20    vp::PresentationError,
21};
22use trustchain_ion::{
23    attest::attest_operation, attestor::IONAttestor, create::create_operation, trustchain_resolver,
24};
25
26/// API for Trustchain DID functionality.
27#[async_trait]
28pub trait TrustchainDIDAPI {
29    /// Creates a controlled DID from a passed document state, writing the associated create
30    /// operation to file in the operations path returning the file name including the created DID
31    /// suffix.
32    // TODO: consider replacing error variant with specific IONError/DIDError in future version.
33    fn create(
34        document_state: Option<DocumentState>,
35        verbose: bool,
36    ) -> Result<String, Box<dyn Error>> {
37        create_operation(document_state, verbose)
38    }
39    /// An uDID attests to a dDID, writing the associated update operation to file in the operations
40    /// path.
41    async fn attest(did: &str, controlled_did: &str, verbose: bool) -> Result<(), Box<dyn Error>> {
42        attest_operation(did, controlled_did, verbose).await
43    }
44    /// Resolves a given DID using given endpoint.
45    async fn resolve(did: &str, resolver: &dyn TrustchainResolver) -> ResolverResult {
46        // Result metadata, Document, Document metadata
47        resolver.resolve_as_result(did).await
48    }
49
50    /// Verifies a given DID using a resolver available at given endpoint, returning a result.
51    async fn verify<T, U>(
52        did: &str,
53        root_event_time: Timestamp,
54        verifier: &U,
55    ) -> Result<DIDChain, VerifierError>
56    where
57        T: DIDResolver + Send,
58        U: Verifier<T> + Send + Sync,
59    {
60        verifier.verify(did, root_event_time).await
61    }
62
63    // // TODO: the below have no CLI implementation currently but are planned
64    // /// Generates an update operation and writes to operations path.
65    // fn update(did: &str, controlled_did: &str, verbose: bool) -> Result<(), Box<dyn Error>> {
66    //     todo!()
67    // }
68    // /// Generates a recover operation and writes to operations path.
69    // fn recover(did: &str, verbose: bool) -> Result<(), Box<dyn Error>> {
70    //     todo!()
71    // }
72    // /// Generates a deactivate operation and writes to operations path.
73    // fn deactivate(did: &str, verbose: bool) -> Result<(), Box<dyn Error>> {
74    //     todo!()
75    // }
76    // /// Publishes operations within the operations path (queue).
77    // fn publish(did: &str, verbose: bool) -> Result<(), Box<dyn Error>> {
78    //     todo!()
79    // }
80}
81
82/// API for Trustchain VC functionality.
83#[async_trait]
84pub trait TrustchainVCAPI {
85    /// Signs a credential.
86    async fn sign(
87        mut credential: Credential,
88        did: &str,
89        linked_data_proof_options: Option<LinkedDataProofOptions>,
90        key_id: Option<&str>,
91        resolver: &dyn TrustchainResolver,
92        context_loader: &mut ContextLoader,
93    ) -> Result<Credential, IssuerError> {
94        credential.issuer = Some(ssi::vc::Issuer::URI(URI::String(did.to_string())));
95        let attestor = IONAttestor::new(did);
96        attestor
97            .sign(
98                &credential,
99                linked_data_proof_options,
100                key_id,
101                resolver,
102                context_loader,
103            )
104            .await
105    }
106
107    /// Verifies a credential
108    async fn verify_credential<T, U>(
109        credential: &Credential,
110        linked_data_proof_options: Option<LinkedDataProofOptions>,
111        root_event_time: Timestamp,
112        verifier: &U,
113        context_loader: &mut ContextLoader,
114    ) -> Result<DIDChain, CredentialError>
115    where
116        T: DIDResolver + Send,
117        U: Verifier<T> + Send + Sync,
118    {
119        // Verify signature
120        let result = credential
121            .verify(
122                linked_data_proof_options,
123                verifier.resolver().as_did_resolver(),
124                context_loader,
125            )
126            .await;
127        if !result.errors.is_empty() {
128            return Err(CredentialError::VerificationResultError(result));
129        }
130        // Verify issuer
131        let issuer = credential
132            .get_issuer()
133            .ok_or(CredentialError::NoIssuerPresent)?;
134        Ok(verifier.verify(issuer, root_event_time).await?)
135    }
136}
137
138/// API for Trustchain VP functionality.
139#[async_trait]
140pub trait TrustchainVPAPI {
141    /// Signs a presentation constructing a verifiable presentation.
142    async fn sign_presentation(
143        presentation: Presentation,
144        did: &str,
145        key_id: Option<&str>,
146        endpoint: &str,
147        linked_data_proof_options: Option<LinkedDataProofOptions>,
148        context_loader: &mut ContextLoader,
149    ) -> Result<Presentation, PresentationError> {
150        let resolver = trustchain_resolver(endpoint);
151        let attestor = IONAttestor::new(did);
152        Ok(attestor
153            .sign_presentation(
154                &presentation,
155                linked_data_proof_options,
156                key_id,
157                &resolver,
158                context_loader,
159            )
160            .await?)
161    }
162    /// Verifies a verifiable presentation.
163    async fn verify_presentation<T, U>(
164        presentation: &Presentation,
165        ldp_options: Option<LinkedDataProofOptions>,
166        root_event_time: Timestamp,
167        verifier: &U,
168        context_loader: &mut ContextLoader,
169    ) -> Result<(), PresentationError>
170    where
171        T: DIDResolver + Send,
172        U: Verifier<T> + Send + Sync,
173    {
174        // Check credentials are present in presentation
175        let credentials = presentation
176            .verifiable_credential
177            .as_ref()
178            .ok_or(PresentationError::NoCredentialsPresent)?;
179
180        // Verify signatures and issuers for each credential included in the presentation
181        // TODO: consider concurrency limit (as rate limiting for verifier requests)
182        let limit = Some(5);
183        let ldp_opts_and_context_loader: Vec<(Option<LinkedDataProofOptions>, ContextLoader)> = (0
184            ..credentials.len())
185            .map(|_| (ldp_options.clone(), context_loader.clone()))
186            .collect();
187        stream::iter(credentials.into_iter().zip(ldp_opts_and_context_loader))
188            .map(Ok)
189            .try_for_each_concurrent(
190                limit,
191                |(credential_or_jwt, (ldp_opts, mut context_loader))| async move {
192                    match credential_or_jwt {
193                        CredentialOrJWT::Credential(credential) => {
194                            TrustchainAPI::verify_credential(
195                                credential,
196                                ldp_opts,
197                                root_event_time,
198                                verifier,
199                                &mut context_loader,
200                            )
201                            .await
202                            .map(|_| ())
203                        }
204                        CredentialOrJWT::JWT(jwt) => {
205                            // decode and verify for credential jwts
206                            match Credential::decode_verify_jwt(
207                                jwt,
208                                ldp_opts.clone(),
209                                verifier.resolver().as_did_resolver(),
210                                &mut context_loader,
211                            )
212                            .await
213                            .0
214                            .ok_or(CredentialError::FailedToDecodeJWT)
215                            {
216                                Ok(credential) => TrustchainAPI::verify_credential(
217                                    &credential,
218                                    ldp_opts,
219                                    root_event_time,
220                                    verifier,
221                                    &mut context_loader,
222                                )
223                                .await
224                                .map(|_| ()),
225                                Err(e) => Err(e),
226                            }
227                        }
228                    }
229                },
230            )
231            .await?;
232
233        // Verify signature by holder to authenticate
234        let result = presentation
235            .verify(
236                ldp_options.clone(),
237                verifier.resolver().as_did_resolver(),
238                context_loader,
239            )
240            .await;
241        if !result.errors.is_empty() {
242            return Err(PresentationError::VerifiedHolderUnauthenticated(result));
243        }
244        Ok(())
245    }
246}
247
248/// API for Trustchain DATA functionality.
249#[async_trait]
250pub trait TrustchainDataAPI {
251    /// Signs data in the form of bytes.
252    async fn sign_data(
253        bytes: &[u8],
254        did: &str,
255        linked_data_proof_options: Option<LinkedDataProofOptions>,
256        key_id: Option<&str>,
257        resolver: &dyn TrustchainResolver,
258        context_loader: &mut ContextLoader,
259    ) -> Result<Credential, IssuerError> {
260        // Read the data credential template.
261        let mut credential = Credential::from_json_unsigned(DATA_CREDENTIAL_TEMPLATE).unwrap();
262        // Add the issuer & issuanceDate attributes.
263        credential.issuer = Some(ssi::vc::Issuer::URI(URI::String(did.to_string())));
264        credential.issuance_date = Some(chrono::offset::Local::now().into());
265
266        // Compute the SHA256 hash of the data.
267        let data_hash = Sha256::digest(bytes);
268
269        // Insert the data hash into the credential.
270        let data_element = credential
271            .credential_subject
272            .to_single_mut()
273            .expect("Template credential has a single credentialSubject.")
274            .property_set
275            .as_mut()
276            .expect("Template credential has a property set.")
277            .get_mut(DATA_ATTRIBUTE)
278            .expect("Template credential has a dataset property.");
279        *data_element = hex::encode(data_hash).to_string().into();
280
281        // Sign the credential
282        let attestor = IONAttestor::new(did);
283        Ok(attestor
284            .sign(
285                &credential,
286                linked_data_proof_options,
287                key_id,
288                resolver,
289                context_loader,
290            )
291            .await?)
292    }
293
294    /// Verifies a data credential by hashing the data bytes.
295    async fn verify_data<T, U>(
296        bytes: &[u8],
297        credential: &Credential,
298        linked_data_proof_options: Option<LinkedDataProofOptions>,
299        root_event_time: Timestamp,
300        verifier: &U,
301        context_loader: &mut ContextLoader,
302    ) -> Result<DIDChain, DataCredentialError>
303    where
304        T: DIDResolver + Send,
305        U: Verifier<T> + Send + Sync,
306    {
307        // Compute the SHA256 hash of the data.
308        let actual_hash = hex::encode(Sha256::digest(bytes));
309
310        // Check that the hash matches the dataset attribute value in the credential.
311        let expected_hash = credential
312            .credential_subject
313            .to_single()
314            .ok_or(DataCredentialError::ManyCredentialSubject(
315                credential.credential_subject.clone(),
316            ))?
317            .property_set
318            .as_ref()
319            .ok_or(DataCredentialError::MissingAttribute(
320                "property_set".to_string(),
321            ))?
322            .get(DATA_ATTRIBUTE)
323            .ok_or(DataCredentialError::MissingAttribute(
324                DATA_ATTRIBUTE.to_string(),
325            ))?
326            .as_str()
327            .expect("dataset attribute is a str");
328
329        if actual_hash != expected_hash {
330            return Err(DataCredentialError::MismatchedHashDigests(
331                expected_hash.to_string(),
332                actual_hash,
333            ));
334        };
335        // Verify the data credential.
336        TrustchainAPI::verify_credential(
337            credential,
338            linked_data_proof_options,
339            root_event_time,
340            verifier,
341            context_loader,
342        )
343        .await
344        .map_err(DataCredentialError::CredentialError)
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use crate::api::{
351        TrustchainDataAPI, TrustchainVCAPI, TrustchainVPAPI, DATA_CREDENTIAL_TEMPLATE,
352    };
353    use crate::TrustchainAPI;
354    use sha2::{Digest, Sha256};
355    use ssi::jsonld::ContextLoader;
356    use ssi::ldp::now_ns;
357    use ssi::one_or_many::OneOrMany;
358    use ssi::vc::{Credential, CredentialOrJWT, Presentation, VCDateTime, URI};
359    use trustchain_core::utils::init;
360    use trustchain_core::vc::{CredentialError, DataCredentialError};
361    use trustchain_core::vp::PresentationError;
362    use trustchain_core::{holder::Holder, issuer::Issuer};
363    use trustchain_ion::attestor::IONAttestor;
364    use trustchain_ion::trustchain_resolver;
365    use trustchain_ion::verifier::TrustchainVerifier;
366
367    // The root event time of DID documents in `trustchain-ion/src/data.rs` used for unit tests and the test below.
368    const ROOT_EVENT_TIME_1: u64 = 1666265405;
369
370    const TEST_UNSIGNED_VC: &str = r#"{
371        "@context": [
372          "https://www.w3.org/2018/credentials/v1",
373          "https://www.w3.org/2018/credentials/examples/v1",
374          "https://w3id.org/citizenship/v1"
375        ],
376        "type": ["VerifiableCredential"],
377        "issuer": "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q",
378        "credentialSubject": {
379          "givenName": "Jane",
380          "familyName": "Doe",
381          "degree": {
382            "type": "BachelorDegree",
383            "name": "Bachelor of Science and Arts",
384            "college": "College of Engineering"
385          }
386        }
387      }
388      "#;
389
390    #[ignore = "requires a running Sidetree node listening on http://localhost:3000"]
391    #[tokio::test]
392    async fn test_verify_credential() {
393        init();
394        let issuer_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; // root+1
395        let issuer = IONAttestor::new(issuer_did);
396        let mut vc_with_proof = signed_credential(issuer).await;
397        let resolver = trustchain_resolver("http://localhost:3000/");
398        let mut context_loader = ContextLoader::default();
399        let res = TrustchainAPI::verify_credential(
400            &vc_with_proof,
401            None,
402            ROOT_EVENT_TIME_1,
403            &TrustchainVerifier::new(resolver),
404            &mut context_loader,
405        )
406        .await;
407        assert!(res.is_ok());
408
409        // Change credential to make signature invalid
410        vc_with_proof.expiration_date = Some(VCDateTime::from(now_ns()));
411
412        // Verify: expect no warnings and a signature error as VC has changed
413        let resolver = trustchain_resolver("http://localhost:3000/");
414        let res = TrustchainAPI::verify_credential(
415            &vc_with_proof,
416            None,
417            ROOT_EVENT_TIME_1,
418            &TrustchainVerifier::new(resolver),
419            &mut context_loader,
420        )
421        .await;
422        if let CredentialError::VerificationResultError(ver_res) = res.err().unwrap() {
423            assert_eq!(ver_res.errors, vec!["signature error"]);
424        } else {
425            panic!("should error with VerificationResultError varient of CredentialError")
426        }
427    }
428
429    #[ignore = "requires a running Sidetree node listening on http://localhost:3000"]
430    #[tokio::test]
431    async fn test_verify_presentation() {
432        init();
433        let issuer_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; // root+1
434        let holder_did = "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"; // root+2
435
436        let issuer = IONAttestor::new(issuer_did);
437        let holder = IONAttestor::new(holder_did);
438
439        let vc_with_proof = signed_credential(issuer).await;
440        let resolver = trustchain_resolver("http://localhost:3000/");
441        let mut context_loader = ContextLoader::default();
442
443        // let vc: Credential = serde_json::from_str(TEST_UNSIGNED_VC).unwrap();
444        // let root_plus_1_signing_key: &str = r#"{"kty":"EC","crv":"secp256k1","x":"aApKobPO8H8wOv-oGT8K3Na-8l-B1AE3uBZrWGT6FJU","y":"dspEqltAtlTKJ7cVRP_gMMknyDPqUw-JHlpwS2mFuh0","d":"HbjLQf4tnwJR6861-91oGpERu8vmxDpW8ZroDCkmFvY"}"#;
445        // let jwk: JWK = serde_json::from_str(root_plus_1_signing_key).unwrap();
446        let mut presentation = Presentation {
447            verifiable_credential: Some(OneOrMany::Many(vec![
448                CredentialOrJWT::Credential(vc_with_proof.clone()),
449                CredentialOrJWT::Credential(vc_with_proof.clone()),
450                CredentialOrJWT::Credential(vc_with_proof.clone()),
451                CredentialOrJWT::Credential(vc_with_proof.clone()),
452                CredentialOrJWT::Credential(vc_with_proof.clone()),
453                CredentialOrJWT::Credential(vc_with_proof.clone()),
454                CredentialOrJWT::Credential(vc_with_proof.clone()),
455                CredentialOrJWT::Credential(vc_with_proof.clone()),
456                CredentialOrJWT::Credential(vc_with_proof.clone()),
457                CredentialOrJWT::Credential(vc_with_proof.clone()),
458                // Currently cannot generate a valid jwt that passes verification
459                // Open issue to implement jwt generation for Issuer
460                // https://github.com/alan-turing-institute/trustchain/issues/118
461                // CredentialOrJWT::JWT(
462                //     vc.generate_jwt(
463                //         Some(&jwk),
464                //         &LinkedDataProofOptions {
465                //             checks: None,
466                //             created: None,
467                //             ..Default::default() // created: None,
468                //                                  // challenge: None,
469                //                                  // domain: None,
470                //                                  // type_: None,
471                //                                  // eip712_domain: None,
472                //                                  // proof_purpose: None,
473                //                                  // verification_method: None,
474                //         },
475                //         &resolver,
476                //     )
477                //     .await
478                //     .unwrap(),
479                // ),
480            ])),
481            // NB. Holder must be specified in order to retrieve verification method to verify
482            // presentation. Otherwise must be specified in LinkedDataProofOptions.
483            // If the holder field is left unpopulated here, it is automatically populated during
484            // signing (with the did of the presentation signer) in `holder.sign_presentation()`
485            ..Default::default()
486        };
487
488        presentation = holder
489            .sign_presentation(&presentation, None, None, &resolver, &mut context_loader)
490            .await
491            .unwrap();
492        println!("{}", serde_json::to_string_pretty(&presentation).unwrap());
493        let res = TrustchainAPI::verify_presentation(
494            &presentation,
495            None,
496            ROOT_EVENT_TIME_1,
497            &TrustchainVerifier::new(resolver),
498            &mut context_loader,
499        )
500        .await;
501        println!("{:?}", res);
502        assert!(res.is_ok());
503    }
504
505    #[ignore = "requires a running Sidetree node listening on http://localhost:3000"]
506    #[tokio::test]
507    // No signature from holder in presentation (unauthenticated)
508    async fn test_verify_presentation_unauthenticated() {
509        init();
510        let issuer_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; // root+1
511        let issuer = IONAttestor::new(issuer_did);
512
513        let vc_with_proof = signed_credential(issuer).await;
514        let resolver = trustchain_resolver("http://localhost:3000/");
515        let presentation = Presentation {
516            verifiable_credential: Some(OneOrMany::Many(vec![CredentialOrJWT::Credential(
517                vc_with_proof,
518            )])),
519            ..Default::default()
520        };
521
522        println!("{}", serde_json::to_string_pretty(&presentation).unwrap());
523        assert!(matches!(
524            TrustchainAPI::verify_presentation(
525                &presentation,
526                None,
527                ROOT_EVENT_TIME_1,
528                &TrustchainVerifier::new(resolver),
529                &mut ContextLoader::default()
530            )
531            .await,
532            Err(PresentationError::VerifiedHolderUnauthenticated(..))
533        ));
534    }
535
536    // Helper function to create a signed credential given an attesor.
537    async fn signed_credential(attestor: IONAttestor) -> Credential {
538        let resolver = trustchain_resolver("http://localhost:3000/");
539        let vc: Credential = serde_json::from_str(TEST_UNSIGNED_VC).unwrap();
540        attestor
541            .sign(&vc, None, None, &resolver, &mut ContextLoader::default())
542            .await
543            .unwrap()
544    }
545
546    // Helper function to create a signed data credential given an attesor & data hash.
547    async fn signed_data_credential(issuer_did: &str, bytes: &[u8]) -> Credential {
548        let attestor = IONAttestor::new(issuer_did);
549        let resolver = trustchain_resolver("http://localhost:3000/");
550        let mut vc: Credential = serde_json::from_str(DATA_CREDENTIAL_TEMPLATE).unwrap();
551        vc.issuer = Some(ssi::vc::Issuer::URI(URI::String(issuer_did.to_string())));
552        // Insert the data hash into the credential.
553        let data_element = vc
554            .credential_subject
555            .to_single_mut()
556            .expect("Template credential has a single credentialSubject.")
557            .property_set
558            .as_mut()
559            .expect("Template credential has a property set.")
560            .get_mut(crate::DATA_ATTRIBUTE)
561            .expect("Template credential has a dataset property.");
562        *data_element = hex::encode(Sha256::digest(bytes)).to_string().into();
563        attestor
564            .sign(&vc, None, None, &resolver, &mut ContextLoader::default())
565            .await
566            .unwrap()
567    }
568
569    #[test]
570    fn test_data_credential_template() {
571        // Read the data credential template.
572        let credential = Credential::from_json_unsigned(DATA_CREDENTIAL_TEMPLATE).unwrap();
573        assert_eq!(credential.issuer.unwrap().get_id(), "did:ion:test:XYZ");
574    }
575
576    #[ignore = "requires a running Sidetree node listening on http://localhost:3000"]
577    #[tokio::test]
578    async fn test_verify_data() {
579        init();
580        let issuer_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; // root+1
581
582        let bytes = "test-data-content".as_bytes();
583        let expected_hash = hex::encode(Sha256::digest(bytes));
584
585        let vc_with_proof = signed_data_credential(issuer_did, bytes).await;
586
587        let resolver = trustchain_resolver("http://localhost:3000/");
588        let mut context_loader = ContextLoader::default();
589
590        let res = TrustchainAPI::verify_data(
591            bytes,
592            &vc_with_proof,
593            None,
594            ROOT_EVENT_TIME_1,
595            &TrustchainVerifier::new(resolver),
596            &mut context_loader,
597        )
598        .await;
599        assert!(res.is_ok());
600
601        // Change the data to make the hash digest invalid.
602        let bytes = "different-data-content".as_bytes();
603
604        // Verify: expect no warnings and a MismatchedHashDigests error as the data has changed.
605        let resolver = trustchain_resolver("http://localhost:3000/");
606        let res = TrustchainAPI::verify_data(
607            bytes,
608            &vc_with_proof,
609            None,
610            ROOT_EVENT_TIME_1,
611            &TrustchainVerifier::new(resolver),
612            &mut context_loader,
613        )
614        .await;
615        assert!(res.is_err());
616
617        if let DataCredentialError::MismatchedHashDigests(expected, actual) = res.err().unwrap() {
618            assert_eq!(expected, expected_hash);
619            assert_ne!(actual, expected_hash);
620        } else {
621            panic!("Unexpected CredentialError variant.")
622        }
623    }
624}