Skip to main content

affinidi_tdk/
lib.rs

1/*!
2 * Affinidi Trust Development Kit
3 *
4 * Instantiate a TDK client with the `new` function
5 */
6
7#[cfg(feature = "data-integrity")]
8use affinidi_data_integrity::{
9    DataIntegrityError, DataIntegrityProof, VerifyOptions, verification_proof::VerificationProof,
10};
11use affinidi_did_resolver_cache_sdk::{DIDCacheClient, config::DIDCacheConfigBuilder};
12#[cfg(feature = "messaging")]
13use affinidi_messaging_sdk::ATM;
14#[cfg(feature = "messaging")]
15use affinidi_messaging_sdk::config::ATMConfigBuilder;
16use affinidi_secrets_resolver::{SecretsResolver, ThreadedSecretsResolver};
17use affinidi_tdk_common::{
18    TDKSharedState, create_http_client, environments::TDKEnvironments, errors::Result,
19    profiles::TDKProfile, tasks::authentication::AuthenticationCache,
20};
21use common::{config::TDKConfig, environments::TDKEnvironment};
22#[cfg(feature = "data-integrity")]
23use serde::Serialize;
24use std::sync::Arc;
25
26pub mod dids;
27pub mod secrets;
28
29// Re-export required crates for convenience to applications
30#[cfg(feature = "meeting-place")]
31pub use affinidi_meeting_place as meeting_place;
32
33pub use affinidi_messaging_didcomm as didcomm;
34#[cfg(feature = "messaging")]
35pub use affinidi_messaging_sdk as messaging;
36
37// did-peer functionality is now integrated into affinidi_did_common
38// Use affinidi_tdk::dids module for DID generation
39
40#[cfg(feature = "data-integrity")]
41pub use affinidi_data_integrity as data_integrity;
42
43// Always exported
44pub use affinidi_crypto;
45pub use affinidi_did_authentication as did_authentication;
46pub use affinidi_did_common as did_common;
47pub use affinidi_secrets_resolver as secrets_resolver;
48pub use affinidi_tdk_common as common;
49
50/// TDK instance that can be used to interact with Affinidi services
51#[derive(Clone)]
52pub struct TDK {
53    pub(crate) inner: Arc<TDKSharedState>,
54    #[cfg(feature = "messaging")]
55    pub atm: Option<ATM>,
56    #[cfg(feature = "meeting-place")]
57    pub meeting_place: Option<meeting_place::MeetingPlace>,
58}
59
60/// Affinidi Trusted Development Kit (TDK)
61///
62/// Use this to instantiate everything required to easily interact with Affinidi services
63/// If you are self hosting the services, you can use your own service URL's where required
64///
65/// Example:
66/// ```ignore
67/// use affinidi_tdk::TDK;
68/// use affinidi_tdk_common::config::TDKConfig;
69///
70/// let config = TDKConfig::new().build();
71/// let mut tdk = TDK::new(config).await?;
72///
73///
74/// ```
75/// NOTE: If feature-flag "messaging" is enabled, then there is an option to bring a
76/// pre-configgured ATM instance to the TDK. If none is specified then ATM is automatically setup
77/// for you.
78impl TDK {
79    pub async fn new(
80        config: TDKConfig,
81        #[cfg(feature = "messaging")] atm: Option<ATM>,
82    ) -> Result<Self> {
83        let client = create_http_client();
84
85        // Instantiate the DID resolver for TDK
86        let did_resolver = if let Some(did_resolver) = &config.did_resolver {
87            did_resolver.to_owned()
88        } else if let Some(did_resolver_config) = &config.did_resolver_config {
89            DIDCacheClient::new(did_resolver_config.to_owned()).await?
90        } else {
91            DIDCacheClient::new(DIDCacheConfigBuilder::default().build()).await?
92        };
93
94        // Instantiate the SecretsManager for TDK
95        let secrets_resolver = if let Some(secrets_resolver) = &config.secrets_resolver {
96            secrets_resolver.to_owned()
97        } else {
98            ThreadedSecretsResolver::new(None).await.0
99        };
100
101        // Instantiate the authentication cache
102        let (authentication, _) = AuthenticationCache::new(
103            config.authentication_cache_limit as u64,
104            &did_resolver,
105            secrets_resolver.clone(),
106            &client,
107            config.custom_auth_handlers.clone(),
108        );
109
110        authentication.start().await;
111
112        // Load Environment
113        // Adds secrets to the secrets resolver
114        // Removes secrets from the environment itself
115        let environment = if config.load_environment {
116            let mut environment = TDKEnvironments::fetch_from_file(
117                Some(&config.environment_path),
118                &config.environment_name,
119            )?;
120            for (_, profile) in environment.profiles.iter_mut() {
121                secrets_resolver
122                    .insert_vec(profile.secrets.as_slice())
123                    .await;
124
125                // Remove secrets from profile after adding them to the secrets resolver
126                profile.secrets.clear();
127            }
128            environment
129        } else {
130            TDKEnvironment::default()
131        };
132
133        // Create the shared state, then we can use this inside other Affinidi Crates
134        let shared_state = Arc::new(TDKSharedState {
135            config,
136            did_resolver,
137            secrets_resolver,
138            client,
139            environment,
140            authentication,
141        });
142
143        #[cfg(feature = "messaging")]
144        // Instantiate Affinidi Messaging
145        let atm = if shared_state.config.use_atm {
146            if let Some(atm) = atm {
147                Some(atm.to_owned())
148            } else {
149                // Use the same DID Resolver for ATM
150                Some(ATM::new(ATMConfigBuilder::default().build()?, shared_state.clone()).await?)
151            }
152        } else {
153            None
154        };
155
156        Ok(TDK {
157            inner: shared_state,
158            #[cfg(feature = "messaging")]
159            atm,
160            #[cfg(feature = "meeting-place")]
161            meeting_place: None,
162        })
163    }
164
165    /// Get the shared state of the TDK
166    pub fn get_shared_state(&self) -> Arc<TDKSharedState> {
167        self.inner.clone()
168    }
169
170    /// Adds a TDK Profile to the shared state
171    /// Which is really just adding the secrets to the secrets resolver
172    /// For the moment...
173    pub async fn add_profile(&self, profile: &TDKProfile) {
174        self.inner
175            .secrets_resolver
176            .insert_vec(&profile.secrets)
177            .await;
178    }
179
180    /// Access shared DID resolver
181    pub fn did_resolver(&self) -> &DIDCacheClient {
182        &self.inner.did_resolver
183    }
184
185    /// Verify a signed JSON Schema document which includes a DID lookup resolution step.
186    /// If you already have public key bytes, call
187    /// [`DataIntegrityProof::verify_with_public_key`] directly instead.
188    /// You must strip `proof` from the document as needed
189    /// Context is a copy of any context that needs to be passed in
190    #[cfg(feature = "data-integrity")]
191    pub async fn verify_data<S>(
192        &self,
193        signed_doc: &S,
194        context: Option<Vec<String>>,
195        proof: &DataIntegrityProof,
196    ) -> Result<VerificationProof>
197    where
198        S: Serialize,
199    {
200        use affinidi_did_common::document::DocumentExt;
201        use affinidi_tdk_common::errors::TDKError;
202
203        let did = if let Some((did, _)) = proof.verification_method.split_once("#") {
204            did
205        } else {
206            return Err(TDKError::DataIntegrity(DataIntegrityError::MalformedProof(
207                "Invalid proof:verificationMethod. Must be DID#key-id format".to_string(),
208            )));
209        };
210
211        let resolved = self.inner.did_resolver.resolve(did).await?;
212        let public_bytes = if let Some(vm) = resolved
213            .doc
214            .get_verification_method(&proof.verification_method)
215        {
216            vm.get_public_key_bytes()
217                .map_err(|e| DataIntegrityError::InvalidPublicKey {
218                    codec: None,
219                    len: 0,
220                    reason: format!("Failed to get public key bytes from verification method: {e}"),
221                })?
222        } else {
223            return Err(TDKError::DataIntegrity(DataIntegrityError::MalformedProof(
224                format!(
225                    "Couldn't find key-id ({}) in resolved DID Document",
226                    proof.verification_method
227                ),
228            )));
229        };
230
231        let mut options = VerifyOptions::new();
232        if let Some(ctx) = context {
233            options = options.with_expected_context(ctx);
234        }
235        proof
236            .verify_with_public_key(signed_doc, public_bytes.as_slice(), options)
237            .map_err(TDKError::DataIntegrity)?;
238
239        Ok(VerificationProof {
240            verified: true,
241            verified_document: None,
242        })
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use std::collections::HashMap;
249
250    use affinidi_data_integrity::{DataIntegrityError, crypto_suites::CryptoSuite};
251    use affinidi_tdk_common::{config::TDKConfig, errors::TDKError};
252
253    use crate::TDK;
254
255    #[tokio::test]
256    async fn invalid_verification_method() {
257        let proof = crate::DataIntegrityProof {
258                type_: "DataIntegrityProof".to_string(),
259                cryptosuite: CryptoSuite::EddsaJcs2022,
260                created: Some("2025-01-01T00:00:00Z".to_string()),
261                verification_method: "test".to_string(),
262                proof_purpose: "test".to_string(),
263                proof_value: Some("z2RPk8MWLoULfcbtpULoEsgfDsaAvyfD1PvQC2v3BjqqNtzGu8YJ4Nxq8CmJCZpPqA49uJhkxmxSztUQhBxqnVrYj".to_string()),
264                context: None,
265        };
266
267        let tdk = TDK::new(
268            TDKConfig::builder()
269                .with_load_environment(false)
270                .build()
271                .unwrap(),
272            None,
273        )
274        .await
275        .unwrap();
276        let result = tdk
277            .verify_data(&HashMap::<String, String>::new(), None, &proof)
278            .await;
279        assert!(result.is_err());
280        match result {
281            Err(TDKError::DataIntegrity(DataIntegrityError::MalformedProof(txt))) => {
282                assert_eq!(
283                    txt,
284                    "Invalid proof:verificationMethod. Must be DID#key-id format".to_string()
285                );
286            }
287            _ => panic!("Invalid return type"),
288        }
289    }
290
291    #[tokio::test]
292    async fn invalid_verification_method_2() {
293        let proof = crate::DataIntegrityProof {
294                type_: "DataIntegrityProof".to_string(),
295                cryptosuite: CryptoSuite::EddsaJcs2022,
296                created: Some("2025-01-01T00:00:00Z".to_string()),
297                verification_method: "did:key:not_a_key".to_string(),
298                proof_purpose: "test".to_string(),
299                proof_value: Some("z2RPk8MWLoULfcbtpULoEsgfDsaAvyfD1PvQC2v3BjqqNtzGu8YJ4Nxq8CmJCZpPqA49uJhkxmxSztUQhBxqnVrYj".to_string()),
300                context: None,
301        };
302
303        let tdk = TDK::new(
304            TDKConfig::builder()
305                .with_load_environment(false)
306                .build()
307                .unwrap(),
308            None,
309        )
310        .await
311        .unwrap();
312        let result = tdk
313            .verify_data(&HashMap::<String, String>::new(), None, &proof)
314            .await;
315        assert!(result.is_err());
316        match result {
317            Err(TDKError::DataIntegrity(DataIntegrityError::MalformedProof(txt))) => {
318                assert_eq!(
319                    txt,
320                    "Invalid proof:verificationMethod. Must be DID#key-id format".to_string()
321                );
322            }
323            _ => panic!("Invalid return type"),
324        }
325    }
326
327    #[tokio::test]
328    async fn invalid_verification_method_3() {
329        let proof = crate::DataIntegrityProof {
330                type_: "DataIntegrityProof".to_string(),
331                cryptosuite: CryptoSuite::EddsaJcs2022,
332                created: Some("2025-01-01T00:00:00Z".to_string()),
333                verification_method: "did:key:test#test".to_string(),
334                proof_purpose: "test".to_string(),
335                proof_value: Some("z2RPk8MWLoULfcbtpULoEsgfDsaAvyfD1PvQC2v3BjqqNtzGu8YJ4Nxq8CmJCZpPqA49uJhkxmxSztUQhBxqnVrYj".to_string()),
336                context: None,
337        };
338
339        let tdk = TDK::new(
340            TDKConfig::builder()
341                .with_load_environment(false)
342                .build()
343                .unwrap(),
344            None,
345        )
346        .await
347        .unwrap();
348        let result = tdk
349            .verify_data(&HashMap::<String, String>::new(), None, &proof)
350            .await;
351        assert!(result.is_err());
352        match result {
353            Err(TDKError::DIDResolver(txt)) => {
354                assert_eq!(
355                    txt,
356                    "DID error: Failed to parse DID: Invalid method-specific ID: invalid did:key encoding: Invalid multibase prefix: expected 'z' (base58btc), got 't'"
357                        .to_string()
358                );
359            }
360            _ => panic!("Invalid return type {result:#?}"),
361        }
362    }
363}