trustchain_http/
requester.rs

1use std::{collections::HashMap, path::PathBuf};
2
3use josekit::{jwk::Jwk, jwt::JwtPayload};
4use serde_json::Value;
5use ssi::did::Service;
6use trustchain_core::utils::generate_key;
7use trustchain_ion::attestor::IONAttestor;
8
9use crate::{
10    attestation_encryption_utils::{
11        josekit_to_ssi_jwk, ssi_to_josekit_jwk, DecryptVerify, Entity, SignEncrypt,
12    },
13    attestation_utils::{
14        attestation_request_path, matching_endpoint, ContentCRChallenge, ContentCRInitiation,
15        ElementwiseSerializeDeserialize, IdentityCRChallenge, IdentityCRInitiation,
16        RequesterDetails,
17    },
18    attestation_utils::{CustomResponse, Nonce, TrustchainCRError},
19    ATTESTATION_FRAGMENT,
20};
21
22/// Initiates part 1 attestation request (identity challenge-response).
23///
24/// This function generates a temporary key to use as an identifier throughout the challenge-response process.
25/// It prompts the user to provide the organization name and operator name, which are included in the POST request
26/// to the endpoint specified in the attestor's DID document.
27pub async fn initiate_identity_challenge(
28    org_name: &str,
29    op_name: &str,
30    services: &[Service],
31) -> Result<(IdentityCRInitiation, PathBuf), TrustchainCRError> {
32    // generate temp key
33    let temp_s_key_ssi = generate_key();
34    let temp_p_key_ssi = temp_s_key_ssi.to_public();
35    let temp_s_key =
36        ssi_to_josekit_jwk(&temp_s_key_ssi).map_err(|_| TrustchainCRError::FailedToGenerateKey)?;
37    let temp_p_key =
38        ssi_to_josekit_jwk(&temp_p_key_ssi).map_err(|_| TrustchainCRError::FailedToGenerateKey)?;
39
40    // make identity_cr_initiation struct
41    let requester = RequesterDetails {
42        requester_org: org_name.to_owned(),
43        operator_name: op_name.to_owned(),
44    };
45    let mut identity_cr_initiation = IdentityCRInitiation {
46        temp_s_key: None,
47        temp_p_key: Some(temp_p_key.clone()),
48        requester_details: Some(requester.clone()),
49    };
50
51    // get endpoint and uri
52    let url_path = "/did/attestor/identity/initiate";
53    let endpoint = matching_endpoint(services, ATTESTATION_FRAGMENT)?;
54    let uri = format!("{}{}", endpoint, url_path);
55
56    // make POST request to endpoint
57    let client = reqwest::Client::new();
58    let result = client
59        .post(uri)
60        .json(&identity_cr_initiation)
61        .send()
62        .await
63        .map_err(TrustchainCRError::Reqwest)?;
64
65    if result.status() != 200 {
66        return Err(TrustchainCRError::FailedToInitiateCR);
67    }
68    // create new directory for attestation request
69    let path = attestation_request_path(&temp_s_key_ssi.to_public(), "requester")?;
70    std::fs::create_dir_all(&path).map_err(|_| TrustchainCRError::FailedAttestationRequest)?;
71
72    // Add secret key to struct
73    identity_cr_initiation.temp_s_key = Some(temp_s_key);
74
75    Ok((identity_cr_initiation, path))
76}
77
78/// Generates and posts response for part 1 of attesation process (identity challenge-response).
79///
80/// This function first decrypts and verifies the challenge received from attestor to extract
81/// challenge nonce. It then signs the nonce with the requester's temporary secret key and
82/// encrypts it with the attestor's public key, before posting the response to the attestor.
83/// If post request is successful, the updated ```CRIdentityChallenge``` is returned.
84pub async fn identity_response(
85    path: &PathBuf,
86    services: &[Service],
87    attestor_p_key: &Jwk,
88) -> Result<IdentityCRChallenge, TrustchainCRError> {
89    // deserialise challenge struct from file
90    let mut identity_challenge = IdentityCRChallenge::new()
91        .elementwise_deserialize(path)?
92        .ok_or(TrustchainCRError::FailedToDeserialize)?;
93    // get temp secret key from file
94    let identity_initiation = IdentityCRInitiation::new()
95        .elementwise_deserialize(path)?
96        .ok_or(TrustchainCRError::FailedToDeserialize)?;
97    let temp_s_key = identity_initiation.temp_s_key()?;
98    let temp_s_key_ssi = josekit_to_ssi_jwk(temp_s_key)?;
99
100    // decrypt and verify challenge
101    let requester = Entity {};
102    let decrypted_verified_payload = requester.decrypt_and_verify(
103        identity_challenge
104            .identity_challenge_signature
105            .clone()
106            .ok_or(TrustchainCRError::FieldNotFound)?,
107        temp_s_key,
108        attestor_p_key,
109    )?;
110    // sign and encrypt response
111    let signed_encrypted_response = requester.sign_and_encrypt_claim(
112        &decrypted_verified_payload,
113        temp_s_key,
114        attestor_p_key,
115    )?;
116    let key_id = temp_s_key_ssi.to_public().thumbprint()?;
117    // get uri for POST request response
118    let endpoint = matching_endpoint(services, ATTESTATION_FRAGMENT)?;
119    let url_path = "/did/attestor/identity/respond";
120    let uri = format!("{}{}/{}", endpoint, url_path, key_id);
121    // POST response
122    let client = reqwest::Client::new();
123    let result = client
124        .post(uri)
125        .json(&signed_encrypted_response)
126        .send()
127        .await
128        .map_err(TrustchainCRError::Reqwest)?;
129    if result.status() != 200 {
130        return Err(TrustchainCRError::FailedToRespond(result));
131    }
132    // extract nonce
133    let nonce_str = decrypted_verified_payload
134        .claim("identity_nonce")
135        .ok_or(TrustchainCRError::ClaimNotFound)?
136        .as_str()
137        .ok_or(TrustchainCRError::FailedToConvertToStr(
138            // Unwrap: not None since error would have propagated above if None
139            decrypted_verified_payload
140                .claim("identity_nonce")
141                .unwrap()
142                .clone(),
143        ))?;
144    let nonce = Nonce::from(String::from(nonce_str));
145    // update struct
146    identity_challenge.update_p_key = Some(attestor_p_key.clone());
147    identity_challenge.identity_nonce = Some(nonce);
148    identity_challenge.identity_response_signature = Some(signed_encrypted_response);
149
150    Ok(identity_challenge)
151}
152
153/// Initiates part 2 attestation request (content challenge-response).
154///
155/// This function posts the to be attested to candidate DID (dDID) to the attestor's endpoint.
156/// If the post request is successful, the response body contains the signed and encrypted
157/// challenge payload with a hashmap that contains an encrypted nonce per signing key.
158/// The response to the challenge is generated and posted to the attestor's endpoint.
159/// If the post request and the verification of the response are successful, the
160/// ```ContentCRInitiation``` and ```CRContentChallenge``` structs are returned.
161pub async fn initiate_content_challenge(
162    path: &PathBuf,
163    ddid: &str,
164    services: &[Service],
165    attestor_p_key: &Jwk,
166) -> Result<(ContentCRInitiation, ContentCRChallenge), TrustchainCRError> {
167    // deserialise identity_cr_initiation and get key id
168    let identity_cr_initiation = IdentityCRInitiation::new()
169        .elementwise_deserialize(path)?
170        .ok_or(TrustchainCRError::FailedToDeserialize)?;
171    let temp_s_key_ssi = josekit_to_ssi_jwk(&identity_cr_initiation.temp_s_key().cloned()?)?;
172    let key_id = temp_s_key_ssi.to_public().thumbprint()?;
173
174    let content_cr_initiation = ContentCRInitiation {
175        requester_did: Some(ddid.to_owned()),
176    };
177    // get uri for POST request response
178    let endpoint = matching_endpoint(services, ATTESTATION_FRAGMENT)?;
179    let url_path = "/did/attestor/content/initiate";
180    let uri = format!("{}{}/{}", endpoint, url_path, key_id);
181    // make POST request to endpoint
182    let client = reqwest::Client::new();
183    let result = client
184        .post(uri)
185        .json(&ddid)
186        .send()
187        .await
188        .map_err(TrustchainCRError::Reqwest)?;
189    if result.status() != 200 {
190        println!("Status code: {}", result.status());
191        return Err(TrustchainCRError::FailedToRespond(result));
192    }
193
194    let response_body: CustomResponse = result.json().await.map_err(TrustchainCRError::Reqwest)?;
195    let signed_encrypted_challenge = response_body
196        .data
197        .ok_or(TrustchainCRError::ResponseMustContainData)?;
198
199    // response
200    let (nonces, response) = content_response(
201        path,
202        &signed_encrypted_challenge.to_string(),
203        services,
204        attestor_p_key.clone(),
205        ddid,
206    )
207    .await?;
208    let content_challenge = ContentCRChallenge {
209        content_nonce: Some(nonces),
210        content_challenge_signature: Some(signed_encrypted_challenge.to_string()),
211        content_response_signature: Some(response),
212    };
213    Ok((content_cr_initiation, content_challenge))
214}
215
216/// Generates the response for the content challenge-response process and makes a POST request to
217/// the attestor endpoint.
218///
219/// This function first decrypts (temporary secret key) and verifies (attestor's public key) the
220/// challenge received from attestor to extract challenge nonces. It then decrypts each nonce with
221/// the corresponding signing key from the requestor's candidate DID (dDID) document, before
222/// posting the signed (temporary secret key) and encrypted (attestor's public key) response to
223/// the attestor's endpoint.
224/// If successful, the nonces and the (signed and encrypted) response are returned.
225pub async fn content_response(
226    path: &PathBuf,
227    challenge: &str,
228    services: &[Service],
229    attestor_p_key: Jwk,
230    ddid: &str,
231) -> Result<(HashMap<String, Nonce>, String), TrustchainCRError> {
232    // get keys
233    let identity_initiation = IdentityCRInitiation::new()
234        .elementwise_deserialize(path)?
235        .ok_or(TrustchainCRError::FailedToDeserialize)?;
236    let temp_s_key = identity_initiation.temp_s_key()?;
237    let temp_s_key_ssi = josekit_to_ssi_jwk(temp_s_key)?;
238    // get endpoint
239    let key_id = temp_s_key_ssi.to_public().thumbprint()?;
240    let endpoint = matching_endpoint(services, ATTESTATION_FRAGMENT)?;
241    let url_path = "/did/attestor/content/respond";
242    let uri = format!("{}{}/{}", endpoint, url_path, key_id);
243
244    // decrypt and verify payload
245    let requester = Entity {};
246    let decrypted_verified_payload =
247        requester.decrypt_and_verify(challenge.to_owned(), temp_s_key, &attestor_p_key)?;
248    // extract map with decrypted nonces from payload and decrypt each nonce
249    let challenges_map: HashMap<String, String> = serde_json::from_value(
250        decrypted_verified_payload
251            .claim("challenges")
252            .ok_or(TrustchainCRError::ClaimNotFound)?
253            .clone(),
254    )?;
255
256    // keymap with requester secret keys
257    let ion_attestor = IONAttestor::new(ddid);
258    let signing_keys = ion_attestor.signing_keys()?;
259    // iterate over all keys, convert to Jwk (josekit)
260    let mut signing_keys_map: HashMap<String, Jwk> = HashMap::new();
261    for key in signing_keys {
262        let key_id = key.thumbprint()?;
263        let jwk = ssi_to_josekit_jwk(&key)?;
264        signing_keys_map.insert(key_id, jwk);
265    }
266
267    // TODO: make functional version work with error propagation for HashMap fold
268    // let signing_keys_map = signing_keys
269    //     .into_iter()
270    //     .fold(HashMap::new(), |mut acc, key| {
271    //         let key_id = key.thumbprint().unwrap();
272    //         let jwk = ssi_to_josekit_jwk(&key);
273    //         acc.insert(key_id, jwk);
274    //         acc
275    //     });
276
277    let mut decrypted_nonces: HashMap<String, Nonce> = HashMap::new();
278    for (key_id, nonce) in challenges_map.iter() {
279        let payload = requester.decrypt(
280            &Value::from(nonce.clone()),
281            signing_keys_map
282                .get(key_id)
283                .ok_or(TrustchainCRError::KeyNotFound)?,
284        )?;
285        decrypted_nonces.insert(
286            String::from(key_id),
287            Nonce::from(
288                payload
289                    .claim("nonce")
290                    .ok_or(TrustchainCRError::ClaimNotFound)?
291                    .as_str()
292                    .ok_or(TrustchainCRError::FailedToConvertToStr(
293                        // Unwrap: not None since error would have propagated above if None
294                        payload.claim("nonce").unwrap().clone(),
295                    ))?
296                    .to_string(),
297            ),
298        );
299    }
300
301    // sign and encrypt response
302    let value: serde_json::Value = serde_json::to_value(&decrypted_nonces)?;
303    let mut payload = JwtPayload::new();
304    payload.set_claim("nonces", Some(value))?;
305    let signed_encrypted_response =
306        requester.sign_and_encrypt_claim(&payload, temp_s_key, &attestor_p_key)?;
307    // post response to endpoint
308    let client = reqwest::Client::new();
309    let result = client
310        .post(uri)
311        .json(&signed_encrypted_response)
312        .send()
313        .await
314        .map_err(TrustchainCRError::Reqwest)?;
315    if result.status() != 200 {
316        println!("Status code: {}", result.status());
317        return Err(TrustchainCRError::FailedToRespond(result));
318    }
319    Ok((decrypted_nonces, signed_encrypted_response))
320}