From 26ff481264e9d76dfcfb61bf4f08e0b2392f5be2 Mon Sep 17 00:00:00 2001 From: pwochner Date: Fri, 26 May 2023 17:11:10 +0200 Subject: [PATCH 01/86] Add attestor module. Add route for initiating attestation. --- trustchain-http/src/attestor.rs | 101 ++++++++++++++++++++++++++++++++ trustchain-http/src/lib.rs | 2 +- trustchain-http/src/resolver.rs | 1 - trustchain-http/src/server.rs | 7 ++- 4 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 trustchain-http/src/attestor.rs diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs new file mode 100644 index 00000000..439ee8e3 --- /dev/null +++ b/trustchain-http/src/attestor.rs @@ -0,0 +1,101 @@ +use async_trait::async_trait; +use axum::{ + response::{Html, IntoResponse}, + Json, +}; +use hyper::StatusCode; +use log::{debug, info, log}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +use crate::state::AppState; +// use ssi::jwk::JWK; + +// Fields: +// - API access token +// - temporary public key +// - name of DE organisation ("name_downstream") +// - name of individual operator within DE responsible for the request + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AttestationInfo { + // api_access_token: JWT, + // temp_pub_key: JWK, + api_access_token: String, + temp_pub_key: String, + name_downstream: String, + name_operator: String, +} + +#[async_trait] +pub trait TrustchainAttestorHTTP {} + +/// Type for implementing the TrustchainIssuerHTTP trait that will contain additional handler methods. +pub struct TrustchainAttestorHTTPHandler; + +#[async_trait] +impl TrustchainAttestorHTTP for TrustchainAttestorHTTPHandler { + // async fn issue_credential( + // credential: &Credential, + // subject_id: Option<&str>, + // issuer_did: &str, + // resolver: &Resolver, + // ) -> Result { + // let mut credential = credential.to_owned(); + // credential.issuer = Some(ssi::vc::Issuer::URI(ssi::vc::URI::String( + // issuer_did.to_string(), + // ))); + // let now = chrono::offset::Utc::now(); + // credential.issuance_date = Some(VCDateTime::from(now)); + // if let Some(subject_id_str) = subject_id { + // if let OneOrMany::One(ref mut subject) = credential.credential_subject { + // subject.id = Some(ssi::vc::URI::String(subject_id_str.to_string())); + // } + // } + // let issuer = IONAttestor::new(issuer_did); + // Ok(issuer.sign(&credential, None, resolver).await?) + // } +} + +impl TrustchainAttestorHTTPHandler { + /// Receives subject DID in response to offer and returns signed credential. + pub async fn post_initiation( + Json(attestation_info): Json, + // app_state: Arc, + ) -> impl IntoResponse { + info!("Received attestation info: {:?}", attestation_info); + (StatusCode::OK, Html("Hello world!")) + } +} + +#[cfg(test)] +mod tests { + use axum_test_helper::TestClient; + + use crate::{config::HTTPConfig, server::TrustchainRouter}; + + use super::*; + + // Attestor integration tests + #[tokio::test] + #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] + async fn test_post_initiation() { + let app = TrustchainRouter::from(HTTPConfig::default()).into_router(); + let uri = "/did/attestor/initiate".to_string(); + let client = TestClient::new(app); + + let response = client + .post(&uri) + .json(&AttestationInfo { + api_access_token: "a".to_string(), + name_downstream: "b".to_string(), + name_operator: "c".to_string(), + temp_pub_key: "d".to_string(), + }) + .send() + .await; + assert_eq!(response.status(), 200); + assert_eq!(response.text().await, "Hello world!"); + } +} diff --git a/trustchain-http/src/lib.rs b/trustchain-http/src/lib.rs index d9f0eb50..c2b7dbf5 100644 --- a/trustchain-http/src/lib.rs +++ b/trustchain-http/src/lib.rs @@ -1,3 +1,4 @@ +pub mod attestor; pub mod config; pub mod errors; pub mod handlers; @@ -8,7 +9,6 @@ pub mod resolver; pub mod server; pub mod state; pub mod verifier; - /// Issuer DID // TODO: add issuer/verifier configuration as used for core/ion crates pub const ISSUER_DID: &str = "did:ion:test:EiBcLZcELCKKtmun_CUImSlb2wcxK5eM8YXSq3MrqNe5wA"; diff --git a/trustchain-http/src/resolver.rs b/trustchain-http/src/resolver.rs index b5bd6c88..685b7754 100644 --- a/trustchain-http/src/resolver.rs +++ b/trustchain-http/src/resolver.rs @@ -109,7 +109,6 @@ impl TrustchainHTTPHandler { State(app_state): State>, ) -> impl IntoResponse { debug!("Received DID to get trustchain: {}", did.as_str()); - // let mut verifier = .write().await; TrustchainHTTPHandler::resolve_chain( &did, &app_state.verifier, diff --git a/trustchain-http/src/server.rs b/trustchain-http/src/server.rs index 4076bef3..8c7f7eff 100644 --- a/trustchain-http/src/server.rs +++ b/trustchain-http/src/server.rs @@ -1,7 +1,8 @@ +use crate::attestor; // use axum::{routing::get, Router, middleware::{self, Next}, extract::{FromRequest, Request}}; use crate::middleware::validate_did; use crate::{config::HTTPConfig, handlers, issuer, resolver, state::AppState, verifier}; -use axum::routing::IntoMakeService; +use axum::routing::{post, IntoMakeService}; use axum::{middleware, routing::get, Router}; use hyper::server::conn::AddrIncoming; use std::sync::Arc; @@ -66,6 +67,10 @@ impl TrustchainRouter { "/did/bundle/:id", get(resolver::TrustchainHTTPHandler::get_verification_bundle), ) + .route( + "/did/attestor/initiate", + post(attestor::TrustchainAttestorHTTPHandler::post_initiation), + ) .with_state(shared_state), } } From 495ed125ff1bae7b1b158ba88e17c54425203dab Mon Sep 17 00:00:00 2001 From: pwochner Date: Wed, 21 Jun 2023 15:40:51 +0100 Subject: [PATCH 02/86] Add functionality to create directory for attestation request and write attestation info to file. --- trustchain-http/Cargo.toml | 2 + trustchain-http/src/attestor.rs | 116 +++++++++++++++++++++++++++++--- trustchain-http/src/errors.rs | 5 ++ 3 files changed, 115 insertions(+), 8 deletions(-) diff --git a/trustchain-http/Cargo.toml b/trustchain-http/Cargo.toml index 67c5729d..00db1d07 100644 --- a/trustchain-http/Cargo.toml +++ b/trustchain-http/Cargo.toml @@ -36,11 +36,13 @@ image = "0.23.14" hyper = "0.14.26" reqwest = "0.11.16" async-trait = "0.1" +sha2 = "0.10" thiserror="1.0" tower = "0.4" tower-http = { version = "0.4.0", features = ["map-request-body", "util"] } toml="0.7.2" lazy_static="1.4.0" +hex = "0.4.3" [dev-dependencies] axum-test-helper = "0.2.0" diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index 439ee8e3..7f4ba8ec 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -1,3 +1,4 @@ +use crate::{errors::TrustchainHTTPError, state::AppState}; use async_trait::async_trait; use axum::{ response::{Html, IntoResponse}, @@ -6,10 +7,12 @@ use axum::{ use hyper::StatusCode; use log::{debug, info, log}; use serde::{Deserialize, Serialize}; -use std::sync::Arc; - -use crate::state::AppState; -// use ssi::jwk::JWK; +use serde_json::to_string_pretty; +use sha2::{Digest, Sha256}; +use ssi::jwk::JWK; +use std::io::Write; +use std::{fs::OpenOptions, path::Path, path::PathBuf, sync::Arc}; +use trustchain_core::TRUSTCHAIN_DATA; // Fields: // - API access token @@ -17,13 +20,65 @@ use crate::state::AppState; // - name of DE organisation ("name_downstream") // - name of individual operator within DE responsible for the request -#[derive(Debug, Clone, Deserialize, Serialize)] +/// Writes received attestation request to unique path derived from the public key for the interaction. +fn write_attestation_info(attestation_info: &AttestationInfo) -> Result<(), TrustchainHTTPError> { + // Get environment for TRUSTCHAIN_DATA + + let directory = attestion_request_path(&attestation_info.temp_pub_key)?; + + // Make directory if non-existent + // Equivalent of os.makedirs(exist_ok=True) in python + std::fs::create_dir_all(&directory) + .map_err(|_| TrustchainHTTPError::FailedAttestationRequest)?; + + // Check if initial request exists ("attestation_info.json"), if yes, return InternalServerError + let full_path = directory.join("attestation_info.json"); + + if full_path.exists() { + return Err(TrustchainHTTPError::FailedAttestationRequest); + } + + // If not, write to file + // Open the new file + let mut file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(full_path) + .map_err(|_| TrustchainHTTPError::FailedAttestationRequest)?; + + // Write to file + writeln!(file, "{}", &to_string_pretty(attestation_info).unwrap()) + .map_err(|_| TrustchainHTTPError::FailedAttestationRequest)?; + + // Else do something? + + Ok(()) +} + +fn attestion_request_path(pub_key: &str) -> Result { + // Root path in TRUSTCHAIN_DATA + let path: String = std::env::var(TRUSTCHAIN_DATA) + .map_err(|_| TrustchainHTTPError::FailedAttestationRequest)?; + // Use hash of temp_pub_key + Ok(Path::new(path.as_str()) + .join("attestation_requests") + .join(attestation_request_id(pub_key))) +} + +pub fn attestation_request_id(pub_key: &str) -> String { + hex::encode(Sha256::digest(pub_key)) +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct AttestationInfo { // api_access_token: JWT, // temp_pub_key: JWK, api_access_token: String, temp_pub_key: String, + // TODO: change temp_pub_key + // temp_pub_key: JWK, name_downstream: String, name_operator: String, } @@ -65,18 +120,63 @@ impl TrustchainAttestorHTTPHandler { // app_state: Arc, ) -> impl IntoResponse { info!("Received attestation info: {:?}", attestation_info); - (StatusCode::OK, Html("Hello world!")) + + // Set-up path on server as hash of public key + + // Print received info to log and expect admin person to check? + (StatusCode::OK, Html("Received request. Please wait for operator to contact you through an alternative channel.")) } } #[cfg(test)] mod tests { - use axum_test_helper::TestClient; - use crate::{config::HTTPConfig, server::TrustchainRouter}; + use axum_test_helper::TestClient; + use lazy_static::lazy_static; + use trustchain_core::utils::init; use super::*; + // TODO: add this key when switched to JWK + const TEST_KEY: &str = + r#"{"kty":"OKP","crv":"Ed25519","x":"B2J8eJfFljEnKX9yt9_V4TCwcL8rd4qtD7T2Bz4TX0s"}"#; + const TEST_ATTESTATION_INFO: &str = r#"{ + "apiAccessToken": "abcd", + "tempPubKey": "some_string", + "nameDownstream": "myTrustworthyEntity", + "nameOperator": "trustworthyOperator" + }"#; + + #[test] + fn test_key() { + let key: JWK = serde_json::from_str(TEST_KEY).unwrap(); + } + + #[test] + fn test_write_attestation_info() { + init(); + let expected_attestation_info: AttestationInfo = + serde_json::from_str(TEST_ATTESTATION_INFO).unwrap(); + // Get expected path + let expected_path = + attestion_request_path(&expected_attestation_info.temp_pub_key).unwrap(); + println!("The test path is: {:?}", expected_path); + + // Write to file + assert!(write_attestation_info(&expected_attestation_info).is_ok()); + + // Check directory exists + assert!(expected_path.exists()); + + // Check file deserializes to ATTESTATION_INFO + let file_content = + std::fs::read_to_string(expected_path.join("attestation_info.json")).unwrap(); + println!("The file attestation_info.json contains: {}", file_content); + + let actual_attesation_info: AttestationInfo = serde_json::from_str(&file_content).unwrap(); + assert_eq!(expected_attestation_info.clone(), actual_attesation_info); + } + // Attestor integration tests #[tokio::test] #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] diff --git a/trustchain-http/src/errors.rs b/trustchain-http/src/errors.rs index 78589960..9ab2044c 100644 --- a/trustchain-http/src/errors.rs +++ b/trustchain-http/src/errors.rs @@ -24,6 +24,8 @@ pub enum TrustchainHTTPError { CredentialDoesNotExist, #[error("No issuer available.")] NoCredentialIssuer, + #[error("Attestation request failed.")] + FailedAttestationRequest, } impl From for TrustchainHTTPError { @@ -80,6 +82,9 @@ impl IntoResponse for TrustchainHTTPError { err @ TrustchainHTTPError::NoCredentialIssuer => { (StatusCode::BAD_REQUEST, err.to_string()) } + err @ TrustchainHTTPError::FailedAttestationRequest => { + (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) + } }; let body = Json(json!({ "error": err_message })); (status, body).into_response() From 955185fd966bbc119ebc24447b331a1b345844c6 Mon Sep 17 00:00:00 2001 From: pwochner Date: Mon, 3 Jul 2023 15:41:13 +0100 Subject: [PATCH 03/86] Add functionality to handle initial attestation request. --- trustchain-http/src/attestor.rs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index 7f4ba8ec..f5ddcf9b 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -1,7 +1,7 @@ use crate::{errors::TrustchainHTTPError, state::AppState}; use async_trait::async_trait; use axum::{ - response::{Html, IntoResponse}, + response::{Html, IntoResponse, Response}, Json, }; use hyper::StatusCode; @@ -56,6 +56,7 @@ fn write_attestation_info(attestation_info: &AttestationInfo) -> Result<(), Trus Ok(()) } +/// Returns unique path name for a specific attestation request derived from public key for the interaction. fn attestion_request_path(pub_key: &str) -> Result { // Root path in TRUSTCHAIN_DATA let path: String = std::env::var(TRUSTCHAIN_DATA) @@ -114,23 +115,32 @@ impl TrustchainAttestorHTTP for TrustchainAttestorHTTPHandler { } impl TrustchainAttestorHTTPHandler { - /// Receives subject DID in response to offer and returns signed credential. + /// Processes initial attestation request and provided data pub async fn post_initiation( Json(attestation_info): Json, // app_state: Arc, ) -> impl IntoResponse { info!("Received attestation info: {:?}", attestation_info); - // Set-up path on server as hash of public key - - // Print received info to log and expect admin person to check? - (StatusCode::OK, Html("Received request. Please wait for operator to contact you through an alternative channel.")) + match write_attestation_info(&attestation_info) { + Ok(()) => { + ( + StatusCode::OK, + Html("Received request. Please wait for operator to contact you through an alternative channel."), + ) + } + Err(_error) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Html("Attestation request failed."), + ), + } } } #[cfg(test)] mod tests { use crate::{config::HTTPConfig, server::TrustchainRouter}; + use axum::extract; use axum_test_helper::TestClient; use lazy_static::lazy_static; use trustchain_core::utils::init; @@ -178,6 +188,7 @@ mod tests { } // Attestor integration tests + // TODO: make test better #[tokio::test] #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] async fn test_post_initiation() { From d8b3706045aa899ebb491ed364b8d9dae737255f Mon Sep 17 00:00:00 2001 From: pwochner Date: Wed, 12 Jul 2023 15:47:36 +0100 Subject: [PATCH 04/86] Add script to play with josekit for nonce encryption. --- trustchain-http/Cargo.toml | 2 ++ trustchain-http/src/attestor.rs | 34 +++++++++++------- trustchain-http/src/encryption.rs | 59 +++++++++++++++++++++++++++++++ trustchain-http/src/lib.rs | 2 ++ 4 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 trustchain-http/src/encryption.rs diff --git a/trustchain-http/Cargo.toml b/trustchain-http/Cargo.toml index 00db1d07..81dc6f3e 100644 --- a/trustchain-http/Cargo.toml +++ b/trustchain-http/Cargo.toml @@ -43,6 +43,8 @@ tower-http = { version = "0.4.0", features = ["map-request-body", "util"] } toml="0.7.2" lazy_static="1.4.0" hex = "0.4.3" +rand = "0.8" +josekit = "0.8" [dev-dependencies] axum-test-helper = "0.2.0" diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index f5ddcf9b..cfb43647 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -6,6 +6,8 @@ use axum::{ }; use hyper::StatusCode; use log::{debug, info, log}; +use rand::Rng; +use rand::{distributions::Alphanumeric, thread_rng}; use serde::{Deserialize, Serialize}; use serde_json::to_string_pretty; use sha2::{Digest, Sha256}; @@ -70,6 +72,19 @@ fn attestion_request_path(pub_key: &str) -> Result pub fn attestation_request_id(pub_key: &str) -> String { hex::encode(Sha256::digest(pub_key)) } +// generate_nonce copied from (they rely on newer version of ssi: v0.6.0, +// WIP issue for TC: https://github.com/alan-turing-institute/trustchain/issues/85 +// https://github.com/spruceid/oidc4vci-rs/blob/main/src/nonce.rs +fn generate_nonce() -> String { + thread_rng() + .sample_iter(&Alphanumeric) + .take(32) + .map(char::from) + .collect() +} +// TODO: format correctly and convert to bytes?? + +// Encryption: https://github.com/hidekatsu-izuno/josekit-rs#signing-a-jwt-by-ecdsa #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] @@ -122,18 +137,7 @@ impl TrustchainAttestorHTTPHandler { ) -> impl IntoResponse { info!("Received attestation info: {:?}", attestation_info); - match write_attestation_info(&attestation_info) { - Ok(()) => { - ( - StatusCode::OK, - Html("Received request. Please wait for operator to contact you through an alternative channel."), - ) - } - Err(_error) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Html("Attestation request failed."), - ), - } + write_attestation_info(&attestation_info).map(|_| (StatusCode::OK, Html("Received request. Please wait for operator to contact you through an alternative channel."))) } } @@ -162,6 +166,12 @@ mod tests { let key: JWK = serde_json::from_str(TEST_KEY).unwrap(); } + #[test] + fn test_generate_nonce() { + let nonce = generate_nonce(); + assert_eq!(nonce.len(), 32); + } + #[test] fn test_write_attestation_info() { init(); diff --git a/trustchain-http/src/encryption.rs b/trustchain-http/src/encryption.rs new file mode 100644 index 00000000..54e977bd --- /dev/null +++ b/trustchain-http/src/encryption.rs @@ -0,0 +1,59 @@ +use josekit::{ + jwe::ECDH_ES, + jwe::{alg::ecdh_es::EcdhEsJweAlgorithm::EcdhEs, Dir, JweHeader}, + jwk::Jwk, + jwt::{self, JwtPayload}, + JoseError, +}; +use serde_json::Value; + +const temp_private_key: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg","d":"CfdUwQ-CcBQkWpIDPjhSJAq2SCg6hAGdcvLmCj0aA-c"}"#; +const temp_pub_key: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg"}"#; + +// const temp_private_key: &str = r#"{"crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg","d":"CfdUwQ-CcBQkWpIDPjhSJAq2SCg6hAGdcvLmCj0aA-c"}"#; +// const temp_pub_key: &str = r#"{"crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg"}"#; + +fn example() -> Result<(), JoseError> { + let mut header = JweHeader::new(); + header.set_token_type("JWT"); + // header.set_content_encryption("A128CBC-HS256"); + header.set_content_encryption("A256GCM"); + + let mut payload = JwtPayload::new(); + payload.set_subject("subject"); + payload.set_claim("Name of claim", Some(Value::String("my_claim".to_string())))?; + + // let key = b"0123456789ABCDEF0123456789ABCDEF"; + + // Encrypting JWT + let temp_pub_key_jwk: Jwk = serde_json::from_str(&temp_pub_key).unwrap(); + // let temp_pub_key_jwk = Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::Secp256k1).unwrap(); + let encrypter_from_jwk = ECDH_ES.encrypter_from_jwk(&temp_pub_key_jwk)?; + // let encrypter = Dir.encrypter_from_bytes(key)?; + let jwt = jwt::encode_with_encrypter(&payload, &header, &encrypter_from_jwk)?; + println!("JWT: {}", jwt); + + // Decrypting JWT + let temp_private_key_jwk: Jwk = serde_json::from_str(&temp_private_key).unwrap(); + let decrypter_from_jwk = ECDH_ES.decrypter_from_jwk(&temp_private_key_jwk)?; + // let decrypter = Dir.decrypter_from_bytes(key)?; + let (payload, header) = jwt::decode_with_decrypter(&jwt, &decrypter_from_jwk)?; + println!("Header: {}", header); + println!("Payload: {}", payload); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_example() { + example().unwrap(); + } + + #[test] + fn test_ec_key() { + let key = Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::Secp256k1).unwrap(); + println!("{}", serde_json::to_string_pretty(&key).unwrap()); + } +} diff --git a/trustchain-http/src/lib.rs b/trustchain-http/src/lib.rs index c2b7dbf5..a69ae163 100644 --- a/trustchain-http/src/lib.rs +++ b/trustchain-http/src/lib.rs @@ -9,6 +9,8 @@ pub mod resolver; pub mod server; pub mod state; pub mod verifier; +pub mod encryption; + /// Issuer DID // TODO: add issuer/verifier configuration as used for core/ion crates pub const ISSUER_DID: &str = "did:ion:test:EiBcLZcELCKKtmun_CUImSlb2wcxK5eM8YXSq3MrqNe5wA"; From 861dad8a25785004997e22c6cdfba85d4ab04a8a Mon Sep 17 00:00:00 2001 From: pwochner Date: Thu, 13 Jul 2023 15:53:17 +0100 Subject: [PATCH 05/86] Add functionality to sign encrypted nonce, verify signature and decrypt it. --- trustchain-http/src/encryption.rs | 71 +++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 14 deletions(-) diff --git a/trustchain-http/src/encryption.rs b/trustchain-http/src/encryption.rs index 54e977bd..ac35be4f 100644 --- a/trustchain-http/src/encryption.rs +++ b/trustchain-http/src/encryption.rs @@ -1,43 +1,86 @@ use josekit::{ + jwe::JweHeader, jwe::ECDH_ES, - jwe::{alg::ecdh_es::EcdhEsJweAlgorithm::EcdhEs, Dir, JweHeader}, jwk::Jwk, + jws::{JwsHeader, ES256K}, jwt::{self, JwtPayload}, JoseError, }; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; use serde_json::Value; -const temp_private_key: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg","d":"CfdUwQ-CcBQkWpIDPjhSJAq2SCg6hAGdcvLmCj0aA-c"}"#; -const temp_pub_key: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg"}"#; +const TEMP_PRIVATE_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg","d":"CfdUwQ-CcBQkWpIDPjhSJAq2SCg6hAGdcvLmCj0aA-c"}"#; +const TEMP_PUB_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg"}"#; +const UPSTREAM_PRIVATE_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JEV4WMgoJekTa5RQD5M92P1oLjdpMNYETQ3nbtKSnLQ","y":"dRfg_5i5wcMg1lxAffQORHpzgtm2yEIqgJoUk5ZklvI","d":"DZDZd9bxopCv2YJelMpQm_BJ0awvzpT6xWdWbaQlIJI"}"#; +const UPSTREAM_PUB_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JEV4WMgoJekTa5RQD5M92P1oLjdpMNYETQ3nbtKSnLQ","y":"dRfg_5i5wcMg1lxAffQORHpzgtm2yEIqgJoUk5ZklvI"}"#; + +pub struct IdentityChallenge { + // should the struct be public? + temp_pub_key: String, + upstream_priv_key: String, + nonce: String, + update_commitment: String, +} // const temp_private_key: &str = r#"{"crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg","d":"CfdUwQ-CcBQkWpIDPjhSJAq2SCg6hAGdcvLmCj0aA-c"}"#; // const temp_pub_key: &str = r#"{"crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg"}"#; +fn generate_nonce() -> String { + thread_rng() + .sample_iter(&Alphanumeric) + .take(32) + .map(char::from) + .collect() +} +/// Example of step 2 of CR Part I: Identity CR. fn example() -> Result<(), JoseError> { + let challenge = IdentityChallenge { + temp_pub_key: String::from(TEMP_PUB_KEY), + upstream_priv_key: String::from(UPSTREAM_PRIVATE_KEY), + nonce: generate_nonce(), + update_commitment: String::from("placeholderupdatecommitment"), + }; + let mut header = JweHeader::new(); header.set_token_type("JWT"); // header.set_content_encryption("A128CBC-HS256"); header.set_content_encryption("A256GCM"); let mut payload = JwtPayload::new(); - payload.set_subject("subject"); - payload.set_claim("Name of claim", Some(Value::String("my_claim".to_string())))?; - - // let key = b"0123456789ABCDEF0123456789ABCDEF"; + payload.set_claim("nonce", Some(Value::from(challenge.nonce)))?; + payload.set_claim( + "update_commitment", + Some(Value::from(challenge.update_commitment)), + )?; // Encrypting JWT - let temp_pub_key_jwk: Jwk = serde_json::from_str(&temp_pub_key).unwrap(); - // let temp_pub_key_jwk = Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::Secp256k1).unwrap(); + let temp_pub_key_jwk: Jwk = serde_json::from_str(&challenge.temp_pub_key).unwrap(); let encrypter_from_jwk = ECDH_ES.encrypter_from_jwk(&temp_pub_key_jwk)?; - // let encrypter = Dir.encrypter_from_bytes(key)?; let jwt = jwt::encode_with_encrypter(&payload, &header, &encrypter_from_jwk)?; println!("JWT: {}", jwt); - // Decrypting JWT - let temp_private_key_jwk: Jwk = serde_json::from_str(&temp_private_key).unwrap(); + // Signing JWT that contains encrypted nonce + // TODO: add update commitment + let mut header = JwsHeader::new(); + header.set_token_type("JWT"); + + let mut payload = JwtPayload::new(); + payload.set_claim("encrypted_nonce", Some(Value::from(jwt)))?; + let upstream_private_key_jwk: Jwk = serde_json::from_str(&challenge.upstream_priv_key).unwrap(); + let signer = ES256K.signer_from_jwk(&upstream_private_key_jwk)?; + let jwt = jwt::encode_with_signer(&payload, &header, &signer)?; + + // Verifying signature JWT + let upstream_public_key_jwk: Jwk = serde_json::from_str(&UPSTREAM_PUB_KEY).unwrap(); + let verifier = ES256K.verifier_from_jwk(&upstream_public_key_jwk)?; + // let verifier = ES256K.verifier_from_jwk(&temp_pub_key_jwk)?; // this should fail -> wrong key + let (payload, header) = jwt::decode_with_verifier(&jwt, &verifier)?; + + // // Decrypting JWT + let temp_private_key_jwk: Jwk = serde_json::from_str(&TEMP_PRIVATE_KEY).unwrap(); let decrypter_from_jwk = ECDH_ES.decrypter_from_jwk(&temp_private_key_jwk)?; - // let decrypter = Dir.decrypter_from_bytes(key)?; - let (payload, header) = jwt::decode_with_decrypter(&jwt, &decrypter_from_jwk)?; + let encrypted_nonce = payload.claim("encrypted_nonce").unwrap().as_str().unwrap(); + let (payload, header) = jwt::decode_with_decrypter(encrypted_nonce, &decrypter_from_jwk)?; println!("Header: {}", header); println!("Payload: {}", payload); Ok(()) From b2a537091df05b20141ef8c3bd3750655fb5657f Mon Sep 17 00:00:00 2001 From: pwochner Date: Tue, 25 Jul 2023 13:57:32 +0100 Subject: [PATCH 06/86] Conversion between ssi JWK and josekit Jwk type. --- trustchain-http/src/encryption.rs | 56 ++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/trustchain-http/src/encryption.rs b/trustchain-http/src/encryption.rs index ac35be4f..4713a1bf 100644 --- a/trustchain-http/src/encryption.rs +++ b/trustchain-http/src/encryption.rs @@ -8,12 +8,14 @@ use josekit::{ }; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use serde_json::Value; +use ssi::jwk::JWK; + +use crate::errors::TrustchainHTTPError; const TEMP_PRIVATE_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg","d":"CfdUwQ-CcBQkWpIDPjhSJAq2SCg6hAGdcvLmCj0aA-c"}"#; const TEMP_PUB_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg"}"#; const UPSTREAM_PRIVATE_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JEV4WMgoJekTa5RQD5M92P1oLjdpMNYETQ3nbtKSnLQ","y":"dRfg_5i5wcMg1lxAffQORHpzgtm2yEIqgJoUk5ZklvI","d":"DZDZd9bxopCv2YJelMpQm_BJ0awvzpT6xWdWbaQlIJI"}"#; const UPSTREAM_PUB_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JEV4WMgoJekTa5RQD5M92P1oLjdpMNYETQ3nbtKSnLQ","y":"dRfg_5i5wcMg1lxAffQORHpzgtm2yEIqgJoUk5ZklvI"}"#; - pub struct IdentityChallenge { // should the struct be public? temp_pub_key: String, @@ -86,8 +88,22 @@ fn example() -> Result<(), JoseError> { Ok(()) } +fn josekit_to_ssi_jwk(key: &Jwk) -> Result { + let key_as_str: &str = &serde_json::to_string(&key).unwrap(); + let ssi_key: JWK = serde_json::from_str(key_as_str).unwrap(); + Ok(ssi_key) +} + +fn ssi_to_josekit_jwk(key: &JWK) -> Result { + let key_as_str: &str = &serde_json::to_string(&key).unwrap(); + let ssi_key: Jwk = serde_json::from_str(key_as_str).unwrap(); + Ok(ssi_key) +} + #[cfg(test)] mod tests { + use sha2::digest::typenum::private::IsEqualPrivate; + use super::*; #[test] fn test_example() { @@ -95,8 +111,40 @@ mod tests { } #[test] - fn test_ec_key() { - let key = Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::Secp256k1).unwrap(); - println!("{}", serde_json::to_string_pretty(&key).unwrap()); + fn test_josekit_to_ssi_jwk() { + let expected_ssi_pub_key: JWK = serde_json::from_str(TEMP_PUB_KEY).unwrap(); + let expected_josekit_pub_key: Jwk = serde_json::from_str(TEMP_PUB_KEY).unwrap(); + + let ssi_pub_jwk = josekit_to_ssi_jwk(&expected_josekit_pub_key).unwrap(); + assert!(ssi_pub_jwk.equals_public(&expected_ssi_pub_key)); + + let expected_ssi_priv_key: JWK = serde_json::from_str(TEMP_PRIVATE_KEY).unwrap(); + let expected_josekit_priv_key: Jwk = serde_json::from_str(TEMP_PRIVATE_KEY).unwrap(); + + let ssi_priv_jwk = josekit_to_ssi_jwk(&expected_josekit_priv_key).unwrap(); + assert_eq!(ssi_priv_jwk, expected_ssi_priv_key); + + let wrong_expected_ssi_priv_key: JWK = serde_json::from_str(UPSTREAM_PRIVATE_KEY).unwrap(); + assert_ne!(ssi_priv_jwk, wrong_expected_ssi_priv_key); + } + + #[test] + fn test_ssi_to_josekit_jwk() { + let expected_ssi_pub_key: JWK = serde_json::from_str(TEMP_PUB_KEY).unwrap(); + let expected_josekit_pub_key: Jwk = serde_json::from_str(TEMP_PUB_KEY).unwrap(); + + let josekit_pub_jwk = ssi_to_josekit_jwk(&expected_ssi_pub_key).unwrap(); + assert_eq!(josekit_pub_jwk, expected_josekit_pub_key); + + let expected_ssi_priv_key: JWK = serde_json::from_str(TEMP_PRIVATE_KEY).unwrap(); + let expected_josekit_priv_key: Jwk = serde_json::from_str(TEMP_PRIVATE_KEY).unwrap(); + + let josekit_priv_jwk = ssi_to_josekit_jwk(&expected_ssi_priv_key).unwrap(); + assert_eq!(josekit_priv_jwk, expected_josekit_priv_key); } + // #[test] + // fn test_ec_key() { + // let key = Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::Secp256k1).unwrap(); + // println!("{}", serde_json::to_string_pretty(&key).unwrap()); + // } } From 02e16012554d3372e340a34f88e04b163242f801 Mon Sep 17 00:00:00 2001 From: pwochner Date: Fri, 28 Jul 2023 10:26:04 +0200 Subject: [PATCH 07/86] Refactor functionality for identity CR. --- trustchain-http/src/encryption.rs | 185 ++++++++++++++++++++---------- 1 file changed, 125 insertions(+), 60 deletions(-) diff --git a/trustchain-http/src/encryption.rs b/trustchain-http/src/encryption.rs index 4713a1bf..5d48a7cf 100644 --- a/trustchain-http/src/encryption.rs +++ b/trustchain-http/src/encryption.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use josekit::{ jwe::JweHeader, jwe::ECDH_ES, @@ -16,16 +18,58 @@ const TEMP_PRIVATE_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JokHTNHd1lI const TEMP_PUB_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg"}"#; const UPSTREAM_PRIVATE_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JEV4WMgoJekTa5RQD5M92P1oLjdpMNYETQ3nbtKSnLQ","y":"dRfg_5i5wcMg1lxAffQORHpzgtm2yEIqgJoUk5ZklvI","d":"DZDZd9bxopCv2YJelMpQm_BJ0awvzpT6xWdWbaQlIJI"}"#; const UPSTREAM_PUB_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JEV4WMgoJekTa5RQD5M92P1oLjdpMNYETQ3nbtKSnLQ","y":"dRfg_5i5wcMg1lxAffQORHpzgtm2yEIqgJoUk5ZklvI"}"#; + pub struct IdentityChallenge { - // should the struct be public? - temp_pub_key: String, - upstream_priv_key: String, nonce: String, update_commitment: String, } -// const temp_private_key: &str = r#"{"crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg","d":"CfdUwQ-CcBQkWpIDPjhSJAq2SCg6hAGdcvLmCj0aA-c"}"#; -// const temp_pub_key: &str = r#"{"crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg"}"#; +pub struct KeysCR { + private_key: Jwk, + public_key: Jwk, +} + +trait ChallengeResponse { + fn sign_and_encrypt(&self, payload: &JwtPayload) -> Result; + fn decrypt_and_verify(&self, input: String) -> Result; +} + +impl ChallengeResponse for KeysCR { + fn sign_and_encrypt(&self, payload: &JwtPayload) -> Result { + // Sign payload... + let mut header = JwsHeader::new(); + header.set_token_type("JWT"); + let signer = ES256K.signer_from_jwk(&self.private_key)?; + let signed_jwt = jwt::encode_with_signer(payload, &header, &signer)?; + + // ... then encrypt + let mut header = JweHeader::new(); + header.set_token_type("JWT"); + header.set_content_encryption("A128CBC-HS256"); + header.set_content_encryption("A256GCM"); + + let mut payload = JwtPayload::new(); // TODO: new name instead of reuse? + payload.set_claim("signed_jwt", Some(Value::from(signed_jwt.clone())))?; + + let encrypter = ECDH_ES.encrypter_from_jwk(&self.public_key)?; + let encrypted_jwt = jwt::encode_with_encrypter(&payload, &header, &encrypter)?; + Ok(encrypted_jwt) + } + fn decrypt_and_verify(&self, input: String) -> Result { + // Decrypt ... + let decrypter = ECDH_ES.decrypter_from_jwk(&self.private_key)?; + let (payload, header) = jwt::decode_with_decrypter(input, &decrypter)?; + + // ... then verify signature on decrypted content + let verifier = ES256K.verifier_from_jwk(&self.public_key)?; + let (payload, header) = jwt::decode_with_verifier( + &payload.claim("signed_jwt").unwrap().as_str().unwrap(), + &verifier, + )?; + Ok(payload) + } +} + fn generate_nonce() -> String { thread_rng() .sample_iter(&Alphanumeric) @@ -34,70 +78,62 @@ fn generate_nonce() -> String { .collect() } -/// Example of step 2 of CR Part I: Identity CR. -fn example() -> Result<(), JoseError> { - let challenge = IdentityChallenge { - temp_pub_key: String::from(TEMP_PUB_KEY), - upstream_priv_key: String::from(UPSTREAM_PRIVATE_KEY), - nonce: generate_nonce(), - update_commitment: String::from("placeholderupdatecommitment"), - }; +fn josekit_to_ssi_jwk(key: &Jwk) -> Result { + let key_as_str: &str = &serde_json::to_string(&key).unwrap(); + let ssi_key: JWK = serde_json::from_str(key_as_str).unwrap(); + Ok(ssi_key) +} - let mut header = JweHeader::new(); - header.set_token_type("JWT"); - // header.set_content_encryption("A128CBC-HS256"); - header.set_content_encryption("A256GCM"); +fn ssi_to_josekit_jwk(key: &JWK) -> Result { + let key_as_str: &str = &serde_json::to_string(&key).unwrap(); + let ssi_key: Jwk = serde_json::from_str(key_as_str).unwrap(); + Ok(ssi_key) +} +/// Generates the components required for identity challenge part of challenge response protocol +fn generate_challenge() { + // generate nonce + // get update commitment + todo!() +} + +fn present_challenge(challenge: &IdentityChallenge, keys: &KeysCR) -> Result { let mut payload = JwtPayload::new(); - payload.set_claim("nonce", Some(Value::from(challenge.nonce)))?; + payload.set_claim("nonce", Some(Value::from(challenge.nonce.clone())))?; // is this a good idea? payload.set_claim( "update_commitment", - Some(Value::from(challenge.update_commitment)), + Some(Value::from(challenge.update_commitment.clone())), )?; - // Encrypting JWT - let temp_pub_key_jwk: Jwk = serde_json::from_str(&challenge.temp_pub_key).unwrap(); - let encrypter_from_jwk = ECDH_ES.encrypter_from_jwk(&temp_pub_key_jwk)?; - let jwt = jwt::encode_with_encrypter(&payload, &header, &encrypter_from_jwk)?; - println!("JWT: {}", jwt); + let encrypted_challenge = keys.sign_and_encrypt(&payload).unwrap(); + println!("Please copy + paste this challenge and send it to the responsible operator via alternative channels."); + println!("Challenge:"); + println!("{}", encrypted_challenge); + Ok(encrypted_challenge) +} - // Signing JWT that contains encrypted nonce - // TODO: add update commitment - let mut header = JwsHeader::new(); - header.set_token_type("JWT"); +/// Extracts challenge nonce +fn present_response(challenge: String, keys: &KeysCR) -> Result { + let decrypted_challenge = keys.decrypt_and_verify(challenge).unwrap(); + let nonce = decrypted_challenge + .claim("nonce") + .unwrap() + .as_str() + .unwrap(); let mut payload = JwtPayload::new(); - payload.set_claim("encrypted_nonce", Some(Value::from(jwt)))?; - let upstream_private_key_jwk: Jwk = serde_json::from_str(&challenge.upstream_priv_key).unwrap(); - let signer = ES256K.signer_from_jwk(&upstream_private_key_jwk)?; - let jwt = jwt::encode_with_signer(&payload, &header, &signer)?; - - // Verifying signature JWT - let upstream_public_key_jwk: Jwk = serde_json::from_str(&UPSTREAM_PUB_KEY).unwrap(); - let verifier = ES256K.verifier_from_jwk(&upstream_public_key_jwk)?; - // let verifier = ES256K.verifier_from_jwk(&temp_pub_key_jwk)?; // this should fail -> wrong key - let (payload, header) = jwt::decode_with_verifier(&jwt, &verifier)?; - - // // Decrypting JWT - let temp_private_key_jwk: Jwk = serde_json::from_str(&TEMP_PRIVATE_KEY).unwrap(); - let decrypter_from_jwk = ECDH_ES.decrypter_from_jwk(&temp_private_key_jwk)?; - let encrypted_nonce = payload.claim("encrypted_nonce").unwrap().as_str().unwrap(); - let (payload, header) = jwt::decode_with_decrypter(encrypted_nonce, &decrypter_from_jwk)?; - println!("Header: {}", header); - println!("Payload: {}", payload); - Ok(()) -} + payload.set_claim("nonce", Some(Value::from(nonce)))?; + let response = keys.sign_and_encrypt(&payload).unwrap(); -fn josekit_to_ssi_jwk(key: &Jwk) -> Result { - let key_as_str: &str = &serde_json::to_string(&key).unwrap(); - let ssi_key: JWK = serde_json::from_str(key_as_str).unwrap(); - Ok(ssi_key) + Ok(response) } -fn ssi_to_josekit_jwk(key: &JWK) -> Result { - let key_as_str: &str = &serde_json::to_string(&key).unwrap(); - let ssi_key: Jwk = serde_json::from_str(key_as_str).unwrap(); - Ok(ssi_key) +/// Verifies if nonce is valid +fn verify_response(response: String, keys: &KeysCR) -> Result { + // TODO: only returns payload, we don't verify if nonce correct at this point + let payload = keys.decrypt_and_verify(response).unwrap(); + + Ok(payload) } #[cfg(test)] @@ -105,10 +141,6 @@ mod tests { use sha2::digest::typenum::private::IsEqualPrivate; use super::*; - #[test] - fn test_example() { - example().unwrap(); - } #[test] fn test_josekit_to_ssi_jwk() { @@ -142,6 +174,39 @@ mod tests { let josekit_priv_jwk = ssi_to_josekit_jwk(&expected_ssi_priv_key).unwrap(); assert_eq!(josekit_priv_jwk, expected_josekit_priv_key); } + + #[test] + fn test_present_challenge_and_response() { + // get challenge components and keys ready + let upstream_cr_keys = KeysCR { + private_key: serde_json::from_str(UPSTREAM_PRIVATE_KEY).unwrap(), + public_key: serde_json::from_str(TEMP_PUB_KEY).unwrap(), + }; + + let test_challenge = IdentityChallenge { + nonce: generate_nonce(), + update_commitment: String::from("somerandomstringfornow"), + }; + println!("======================"); + println!("The nonce is: {}", test_challenge.nonce); + println!("======================"); + let presented_challenge = present_challenge(&test_challenge, &upstream_cr_keys).unwrap(); + + // get keys for response ready + let downstream_cr_keys = KeysCR { + private_key: serde_json::from_str(TEMP_PRIVATE_KEY).unwrap(), + public_key: serde_json::from_str(UPSTREAM_PUB_KEY).unwrap(), + }; + let response = present_response(presented_challenge, &downstream_cr_keys).unwrap(); + + let verified_response = verify_response(response, &upstream_cr_keys).unwrap(); + let nonce_from_response = verified_response.claim("nonce").unwrap().as_str().unwrap(); + println!("======================"); + println!("Verified response: {}", nonce_from_response); + println!("======================"); + assert_eq!(test_challenge.nonce, nonce_from_response); + } + // #[test] // fn test_ec_key() { // let key = Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::Secp256k1).unwrap(); From 7c04f02227c60ee510bbeb0c26cafddf6b43919d Mon Sep 17 00:00:00 2001 From: pwochner Date: Fri, 4 Aug 2023 11:40:58 +0200 Subject: [PATCH 08/86] Test for content CR --- trustchain-http/src/encryption.rs | 251 ++++++++++++++++++++++++++++-- trustchain-http/src/errors.rs | 3 + 2 files changed, 240 insertions(+), 14 deletions(-) diff --git a/trustchain-http/src/encryption.rs b/trustchain-http/src/encryption.rs index 5d48a7cf..37a3bd25 100644 --- a/trustchain-http/src/encryption.rs +++ b/trustchain-http/src/encryption.rs @@ -9,19 +9,28 @@ use josekit::{ JoseError, }; use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use serde::{Deserialize, Serialize}; use serde_json::Value; +use sha2::{Digest, Sha256}; use ssi::jwk::JWK; -use crate::errors::TrustchainHTTPError; - const TEMP_PRIVATE_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg","d":"CfdUwQ-CcBQkWpIDPjhSJAq2SCg6hAGdcvLmCj0aA-c"}"#; const TEMP_PUB_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg"}"#; const UPSTREAM_PRIVATE_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JEV4WMgoJekTa5RQD5M92P1oLjdpMNYETQ3nbtKSnLQ","y":"dRfg_5i5wcMg1lxAffQORHpzgtm2yEIqgJoUk5ZklvI","d":"DZDZd9bxopCv2YJelMpQm_BJ0awvzpT6xWdWbaQlIJI"}"#; const UPSTREAM_PUB_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JEV4WMgoJekTa5RQD5M92P1oLjdpMNYETQ3nbtKSnLQ","y":"dRfg_5i5wcMg1lxAffQORHpzgtm2yEIqgJoUk5ZklvI"}"#; +const DOWNSTREAM_PRIV_KEY_1: &str = r#"{"kty":"EC","crv":"secp256k1","x":"Lt2ys7LE0ELccVtCETtVjMFavgjwYDjDBtuV_XCH7-g","y":"TdTT8oXUSXMvFbhnsYrqwOkL7-niHWFxW0vaBSnUMnI","d":"B7csdham680yGiIdxeyllmczap7-h6_LtKunRhRqfic"}"#; +const DOWNSTREAM_PUB_KEY_1: &str = r#"{"kty":"EC","crv":"secp256k1","x":"Lt2ys7LE0ELccVtCETtVjMFavgjwYDjDBtuV_XCH7-g","y":"TdTT8oXUSXMvFbhnsYrqwOkL7-niHWFxW0vaBSnUMnI"}"#; +const DOWNSTREAM_PRIV_KEY_2: &str = r#"{"kty":"EC","crv":"secp256k1","x":"AB1b_4-XSem0uiPGGuW_hf_AuPArukMuD2S95ypGDSE","y":"suvBnCbhicPdYZeqgxJfPFmiNHGYDjPiW8XkYHxwgBU","d":"V3zmieRjP9LYa1v8l8lYXh4LqU87bPspSAGqq34Up1Q"}"#; +const DOWNSTREAM_PUB_KEY_2: &str = r#"{"kty":"EC","crv":"secp256k1","x":"AB1b_4-XSem0uiPGGuW_hf_AuPArukMuD2S95ypGDSE","y":"suvBnCbhicPdYZeqgxJfPFmiNHGYDjPiW8XkYHxwgBU"}"#; pub struct IdentityChallenge { - nonce: String, - update_commitment: String, + nonce: String, // Maybe create a new Nonce type + update_commitment: String, // TODO: this should be a key, format??? +} +#[derive(Debug, Serialize, Deserialize)] +pub struct ContentChallengeItem { + encrypted_nonce: String, + hash_public_key: String, } pub struct KeysCR { @@ -29,6 +38,99 @@ pub struct KeysCR { public_key: Jwk, } +pub struct KeyPairs { + private_key: Jwk, + public_key: Jwk, +} + +// Orphan rule: need new trait in crate or new type. +// New trait: +trait ToJwk { + fn to_jwk(&self) -> Jwk { + todo!() + } +} + +// New type: +// Or we make our own key type (wrapper) +// Named field version +pub struct MyJWKNamedField { + key: Jwk, +} +// Tuple struct version +pub struct MyJWK(Jwk); + +impl From for Jwk { + fn from(value: MyJWK) -> Self { + value.0 + } +} + +impl From for JWK { + fn from(value: MyJWK) -> Self { + josekit_to_ssi_jwk(&value.0).unwrap() // copy code of function in here + } +} + +impl From for MyJWK { + fn from(value: JWK) -> Self { + todo!() + } +} + +impl From for MyJWK { + fn from(value: Jwk) -> Self { + todo!() + } +} + +// Ideas for structs: +/// A type for upstream entity? +struct UE { + keys_cr: KeysCR, +} + +/// A type for downstream entity? +struct DE; + +pub trait CRStateIO { + // read() returns any struct that implements the CRState trait (eg. Step2Claim) + // (the Box<> is needed because the different structs that could be returned will likely have + // different sizes) + fn read(&self) -> Box; + fn write(&self, payload: &str); +} + +// An empty trait implimented by all data types, eg. Step2Claim? +trait CRState { + fn status(&self) { + println!("Ok"); + } +} + +/// A type for a nonce +struct Nonce(String); + +// Data type to be read/written to file? +struct Step2Claim { + nonce: Nonce, + temp_pub_key: Jwk, +} +impl CRState for Step2Claim {} + +// Give the ability to DE to read and write CRState data files +impl CRStateIO for DE { + fn read(&self) -> Box { + todo!() + } + fn write(&self, payload: &str) { + todo!() + } +} + +// TODO: own type for nonce + +// trait Encryption { ?? trait ChallengeResponse { fn sign_and_encrypt(&self, payload: &JwtPayload) -> Result; fn decrypt_and_verify(&self, input: String) -> Result; @@ -90,14 +192,10 @@ fn ssi_to_josekit_jwk(key: &JWK) -> Result { Ok(ssi_key) } -/// Generates the components required for identity challenge part of challenge response protocol -fn generate_challenge() { - // generate nonce - // get update commitment - todo!() -} - -fn present_challenge(challenge: &IdentityChallenge, keys: &KeysCR) -> Result { +fn present_identity_challenge( + challenge: &IdentityChallenge, + keys: &KeysCR, +) -> Result { let mut payload = JwtPayload::new(); payload.set_claim("nonce", Some(Value::from(challenge.nonce.clone())))?; // is this a good idea? payload.set_claim( @@ -112,6 +210,15 @@ fn present_challenge(challenge: &IdentityChallenge, keys: &KeysCR) -> Result Result { + let nonce = generate_nonce(); + println!("Nonce: {}", nonce); + + let encrypted_challenge = encrypt(nonce, &key).unwrap(); + + Ok(encrypted_challenge) +} + /// Extracts challenge nonce fn present_response(challenge: String, keys: &KeysCR) -> Result { let decrypted_challenge = keys.decrypt_and_verify(challenge).unwrap(); @@ -136,6 +243,45 @@ fn verify_response(response: String, keys: &KeysCR) -> Result, +) -> Result { + // get number of keys + + // generate one nonce per key and encrypt it with key + + todo!() +} + +fn sign(payload: &JwtPayload, key: &Jwk) -> Result { + let mut header = JwsHeader::new(); + header.set_token_type("JWT"); + let signer = ES256K.signer_from_jwk(key)?; + let signed_jwt = jwt::encode_with_signer(payload, &header, &signer)?; + Ok(signed_jwt) +} + +fn encrypt(value: String, key: &Jwk) -> Result { + let mut header = JweHeader::new(); + header.set_token_type("JWT"); + header.set_content_encryption("A128CBC-HS256"); + header.set_content_encryption("A256GCM"); + + let mut payload = JwtPayload::new(); + payload.set_claim("nonce", Some(Value::from(value.clone())))?; + + let encrypter = ECDH_ES.encrypter_from_jwk(&key)?; + let encrypted_jwt = jwt::encode_with_encrypter(&payload, &header, &encrypter)?; + Ok(encrypted_jwt) +} + +fn decrypt(input: &Value, key: &Jwk) -> Result { + let decrypter = ECDH_ES.decrypter_from_jwk(&key)?; + let (payload, header) = jwt::decode_with_decrypter(input.as_str().unwrap(), &decrypter)?; + Ok(payload) +} + #[cfg(test)] mod tests { use sha2::digest::typenum::private::IsEqualPrivate; @@ -176,7 +322,7 @@ mod tests { } #[test] - fn test_present_challenge_and_response() { + fn test_identity_challenge_response() { // get challenge components and keys ready let upstream_cr_keys = KeysCR { private_key: serde_json::from_str(UPSTREAM_PRIVATE_KEY).unwrap(), @@ -190,7 +336,8 @@ mod tests { println!("======================"); println!("The nonce is: {}", test_challenge.nonce); println!("======================"); - let presented_challenge = present_challenge(&test_challenge, &upstream_cr_keys).unwrap(); + let presented_challenge = + present_identity_challenge(&test_challenge, &upstream_cr_keys).unwrap(); // get keys for response ready let downstream_cr_keys = KeysCR { @@ -207,6 +354,82 @@ mod tests { assert_eq!(test_challenge.nonce, nonce_from_response); } + #[test] + fn test_content_response() { + // keys we need + let upstream_cr_keys = KeysCR { + private_key: serde_json::from_str(UPSTREAM_PRIVATE_KEY).unwrap(), + public_key: serde_json::from_str(TEMP_PUB_KEY).unwrap(), + }; + + // TODO: extract public keys from did document -> Vec<&KeyPairs> + let mut downstream_keys = Vec::<&Jwk>::new(); + let downstream_pub_key_1: Jwk = serde_json::from_str(DOWNSTREAM_PUB_KEY_1).unwrap(); + let downstream_pub_key_2: Jwk = serde_json::from_str(DOWNSTREAM_PUB_KEY_2).unwrap(); + downstream_keys.push(&downstream_pub_key_1); + downstream_keys.push(&downstream_pub_key_2); + + // generate one nonce per public key -> sign individually (vec of signed nonces) + let challenge_vec: Vec = downstream_keys + .iter() + .map(|key| generate_challenge(key).unwrap()) + .collect(); + + let key_hash_vec: Vec = downstream_keys + .iter() + .map(|key| hex::encode(Sha256::digest(serde_json::to_string(&key).unwrap()))) + .collect(); + + println!("Vector with key hashes: {:?}", key_hash_vec); + + // sign (UE private key) and encrypt (DE temp public key) entire challenge + let mut payload = JwtPayload::new(); + payload + .set_claim("challenge", Some(Value::from(challenge_vec))) + .unwrap(); + payload + .set_claim("key_hash", Some(Value::from(key_hash_vec))) + .unwrap(); + let encrypted_challenge = upstream_cr_keys.sign_and_encrypt(&payload).unwrap(); + + // generate response + // verify and decrypt -> extract vectors with challenges and key hashes + let downstream_cr_keys = KeysCR { + private_key: serde_json::from_str(TEMP_PRIVATE_KEY).unwrap(), + public_key: serde_json::from_str(UPSTREAM_PUB_KEY).unwrap(), + }; + let decrypted_challenge = downstream_cr_keys + .decrypt_and_verify(encrypted_challenge) + .unwrap(); + + // extract vector with challenge nonce(s) + let challenge_vec = decrypted_challenge + .claim("challenge") + .unwrap() + .as_array() + .unwrap(); + + // private keys + let mut downstream_private_keys = Vec::<&Jwk>::new(); + let downstream_private_key_1: Jwk = serde_json::from_str(DOWNSTREAM_PRIV_KEY_1).unwrap(); + let downstream_private_key_2: Jwk = serde_json::from_str(DOWNSTREAM_PRIV_KEY_2).unwrap(); + downstream_private_keys.push(&downstream_private_key_1); + downstream_private_keys.push(&downstream_private_key_2); + + // decrypt each nonce + let response_vec: Vec = challenge_vec + .iter() + .zip(downstream_private_keys.iter()) + .map(|(nonce, key)| decrypt(&nonce, &key).unwrap()) + .collect(); + println!("Decrypted challenge vector: {:?}", response_vec); + // continue here!!!!!!!!!!!!! How do we find the right key for each nonce? + + // TODO: prepare response + + // TODO: verify response (nonces) + } + // #[test] // fn test_ec_key() { // let key = Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::Secp256k1).unwrap(); diff --git a/trustchain-http/src/errors.rs b/trustchain-http/src/errors.rs index 9ab2044c..2adb20fb 100644 --- a/trustchain-http/src/errors.rs +++ b/trustchain-http/src/errors.rs @@ -20,6 +20,9 @@ pub enum TrustchainHTTPError { ResolverError(ResolverError), #[error("Trustchain issuer error: {0}")] IssuerError(IssuerError), + // TODO: once needed in http propagate + // #[error("Jose error: {0}")] + // JoseError(JoseError), #[error("Credential does not exist.")] CredentialDoesNotExist, #[error("No issuer available.")] From 9f2592c34481d428c09cef34ceaad319640167de Mon Sep 17 00:00:00 2001 From: pwochner Date: Fri, 11 Aug 2023 09:55:31 +0100 Subject: [PATCH 09/86] Add generate/verify response to content CR. --- trustchain-http/Cargo.toml | 1 + trustchain-http/src/data.rs | 62 +++++++ trustchain-http/src/encryption.rs | 270 +++++++++++++++++++++++------- trustchain-http/src/lib.rs | 3 +- 4 files changed, 271 insertions(+), 65 deletions(-) create mode 100644 trustchain-http/src/data.rs diff --git a/trustchain-http/Cargo.toml b/trustchain-http/Cargo.toml index 81dc6f3e..e0b8a88f 100644 --- a/trustchain-http/Cargo.toml +++ b/trustchain-http/Cargo.toml @@ -15,6 +15,7 @@ path = "src/bin/main.rs" [dependencies] trustchain-core = { path = "../trustchain-core" } trustchain-ion = { path = "../trustchain-ion" } +anyhow = "*" did-ion="0.1.0" clap = { version = "^4", features=["derive", "env", "cargo"] } ssi = "0.4" diff --git a/trustchain-http/src/data.rs b/trustchain-http/src/data.rs new file mode 100644 index 00000000..88815a67 --- /dev/null +++ b/trustchain-http/src/data.rs @@ -0,0 +1,62 @@ +#![allow(dead_code)] + +pub const TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS: &str = r##" +{ + "@context" : [ + "https://www.w3.org/ns/did/v1", + { + "@base" : "did:ion:test:EiCBr7qGDecjkR2yUBhn3aNJPUR3TSEOlkpNcL0Q5Au9ZQ" + } + ], + "assertionMethod" : [ + "#V8jt_0c-aFlq40Uti2R_WiquxuzxyB8kn1cfWmXIU84" + ], + "authentication" : [ + "#V8jt_0c-aFlq40Uti2R_WiquxuzxyB8kn1cfWmXIU84" + ], + "capabilityDelegation" : [ + "#V8jt_0c-aFlq40Uti2R_WiquxuzxyB8kn1cfWmXIU84" + ], + "capabilityInvocation" : [ + "#V8jt_0c-aFlq40Uti2R_WiquxuzxyB8kn1cfWmXIU84" + ], + "id" : "did:ion:test:EiCBr7qGDecjkR2yUBhn3aNJPUR3TSEOlkpNcL0Q5Au9ZQ", + "keyAgreement" : [ + "#V8jt_0c-aFlq40Uti2R_WiquxuzxyB8kn1cfWmXIU84" + ], + "service" : [ + { + "id" : "#trustchain-controller-proof", + "type" : "TrustchainProofService", + "serviceEndpoint" : { + "proofValue" : "eyJhbGciOiJFUzI1NksifQ.IkVpQmNiTkRRcjZZNHNzZGc5QXo4eC1qNy1yS1FuNWk5T2Q2S3BjZ2c0RU1KOXci.Nii8p38DtzyurmPHO9sV2JLSH7-Pv-dCKQ0Y-H34rplwhhwca2nSra4ZofcUsHCG6u1oKJ0x4AmMUD2_3UIhRA", + "controller" : "did:ion:test:EiCBr7qGDecjkR2yUBhn3aNJPUR3TSEOlkpNcL0Q5Au9ZQ" + } + } + ], + "verificationMethod" : [ + { + "controller" : "did:ion:test:EiCBr7qGDecjkR2yUBhn3aNJPUR3TSEOlkpNcL0Q5Au9ZQ", + "id" : "#V8jt_0c-aFlq40Uti2R_WiquxuzxyB8kn1cfWmXIU84", + "publicKeyJwk" : { + "crv" : "secp256k1", + "kty" : "EC", + "x" : "RbIj1Y4jeqkn0cizEfxHZidD-GQouFmAtE6YCpxFjpg", + "y" : "ZcbgNp3hrfp3cujZFKqgFS0uFGOn2Rk16Y9nOv0h15s" + }, + "type" : "JsonWebSignature2020" + }, + { + "controller" : "did:ion:test:EiCBr7qGDecjkR2yUBhn3aNJPUR3TSEOlkpNcL0Q5Au9ZQ", + "id" : "#V9jt_0c-aFlq40Uti2R_WiquxuzxyB8kn1cfWmXIU85", + "publicKeyJwk" : { + "crv": "secp256k1", + "kty": "EC", + "x": "7ReQHHysGxbyuKEQmspQOjL7oQUqDTldTHuc9V3-yso", + "y": "kWvmS7ZOvDUhF8syO08PBzEpEk3BZMuukkvEJOKSjqE" + }, + "type" : "JsonWebSignature2020" + } + ] +} +"##; diff --git a/trustchain-http/src/encryption.rs b/trustchain-http/src/encryption.rs index 37a3bd25..e5c9d304 100644 --- a/trustchain-http/src/encryption.rs +++ b/trustchain-http/src/encryption.rs @@ -1,4 +1,4 @@ -use std::str::FromStr; +use std::{collections::HashMap, str::FromStr}; use josekit::{ jwe::JweHeader, @@ -10,9 +10,12 @@ use josekit::{ }; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use serde::{Deserialize, Serialize}; -use serde_json::Value; +use serde_json::{from_value, Value}; use sha2::{Digest, Sha256}; +use ssi::did::Document; +use ssi::did::VerificationMethod; use ssi::jwk::JWK; +use thiserror::Error; const TEMP_PRIVATE_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg","d":"CfdUwQ-CcBQkWpIDPjhSJAq2SCg6hAGdcvLmCj0aA-c"}"#; const TEMP_PUB_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg"}"#; @@ -23,6 +26,22 @@ const DOWNSTREAM_PUB_KEY_1: &str = r#"{"kty":"EC","crv":"secp256k1","x":"Lt2ys7L const DOWNSTREAM_PRIV_KEY_2: &str = r#"{"kty":"EC","crv":"secp256k1","x":"AB1b_4-XSem0uiPGGuW_hf_AuPArukMuD2S95ypGDSE","y":"suvBnCbhicPdYZeqgxJfPFmiNHGYDjPiW8XkYHxwgBU","d":"V3zmieRjP9LYa1v8l8lYXh4LqU87bPspSAGqq34Up1Q"}"#; const DOWNSTREAM_PUB_KEY_2: &str = r#"{"kty":"EC","crv":"secp256k1","x":"AB1b_4-XSem0uiPGGuW_hf_AuPArukMuD2S95ypGDSE","y":"suvBnCbhicPdYZeqgxJfPFmiNHGYDjPiW8XkYHxwgBU"}"#; +#[derive(Error, Debug)] +pub enum TrustchainCRError { + /// Serde JSON error. + #[error("Wrapped serialization error: {0}")] + Serde(serde_json::Error), + /// Wrapped jose error. + #[error("Wrapped jose error: {0}")] + Jose(JoseError), + /// Missing JWK from verification method + #[error("Missing JWK from verification method of a DID document.")] + MissingJWK, + /// Key not found in hashmap + #[error("Key id not found.")] + KeyNotFound, +} + pub struct IdentityChallenge { nonce: String, // Maybe create a new Nonce type update_commitment: String, // TODO: this should be a key, format??? @@ -130,7 +149,6 @@ impl CRStateIO for DE { // TODO: own type for nonce -// trait Encryption { ?? trait ChallengeResponse { fn sign_and_encrypt(&self, payload: &JwtPayload) -> Result; fn decrypt_and_verify(&self, input: String) -> Result; @@ -250,7 +268,13 @@ fn present_content_challenge( // get number of keys // generate one nonce per key and encrypt it with key - + // let challenges: HashMap = + // test_keys_map + // .iter() + // .fold(HashMap::new(), |mut acc, (key_id, key)| { + // acc.insert(String::from(key_id), generate_challenge(&key).unwrap()); + // acc + // }); todo!() } @@ -282,11 +306,114 @@ fn decrypt(input: &Value, key: &Jwk) -> Result { Ok(payload) } +/// Extract public keys from did document together with corresponding key ids +fn extract_key_ids_and_jwk(document: &Document) -> Result, TrustchainCRError> { + let mut my_map = HashMap::::new(); + if let Some(vms) = &document.verification_method { + // TODO: leave the commented code + // vms.iter().for_each(|vm| match vm { + // VerificationMethod::Map(vm_map) => { + // let id = vm_map.id; + // let key = vm_map.get_jwk().unwrap(); + // let key_jose = ssi_to_josekit_jwk(&key).unwrap(); + // my_map.insert(id, key_jose); + // } + // _ => (), + // }); + // TODO: consider rewriting functional with filter, partition, fold over returned error + // variants. + for vm in vms { + match vm { + VerificationMethod::Map(vm_map) => { + let id = vm_map.id.clone(); // TODo: use JWK::thumbprint() instead + let key = vm_map + .get_jwk() + .map_err(|_| TrustchainCRError::MissingJWK)?; + // let id = key + // .thumbprint() + // .map_err(|_| TrustchainCRError::MissingJWK)?; //TODO: different error variant? + let key_jose = + ssi_to_josekit_jwk(&key).map_err(|err| TrustchainCRError::Serde(err))?; + my_map.insert(id, key_jose); + } + _ => (), + } + } + } + Ok(my_map) +} + +fn generate_content_response( + challenges: HashMap, + did_keys_priv: HashMap, + cr_keys: &KeysCR, +) -> Result { + let decrypted_nonces: HashMap = + challenges + .iter() + .fold(HashMap::new(), |mut acc, (key_id, nonce)| { + acc.insert( + String::from(key_id), + decrypt( + &Some(Value::from(nonce.clone())).unwrap(), + did_keys_priv.get(key_id).unwrap(), + ) + .unwrap() + .claim("nonce") + .unwrap() + .as_str() + .unwrap() + .to_string(), + ); + + acc + }); + // Ok(decrypted_nonces) + // make payload + let value: serde_json::Value = serde_json::to_value(decrypted_nonces).unwrap(); + let mut payload = JwtPayload::new(); + payload.set_claim("nonces", Some(value)).unwrap(); + // sign (temp private key) and encrypt (UE public key) + let encrypted_response = cr_keys.sign_and_encrypt(&payload).unwrap(); + + Ok(encrypted_response) +} + +fn verify_content_response( + response: String, + cr_keys: &KeysCR, +) -> Result, TrustchainCRError> { + // verify signature and decrypt response + let decrypted_response = cr_keys.decrypt_and_verify(response).unwrap(); + + // extract response hashmap + let response_hashmap: HashMap = + serde_json::from_value(decrypted_response.claim("nonces").unwrap().clone()).unwrap(); + + Ok(response_hashmap) +} + #[cfg(test)] mod tests { - use sha2::digest::typenum::private::IsEqualPrivate; + + use serde_json::from_str; use super::*; + use crate::data::TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS; + + #[test] + fn test_extract_key_ids_and_jwk() { + let doc: Document = serde_json::from_str(TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS).unwrap(); + let test_keys_map = extract_key_ids_and_jwk(&doc).unwrap(); + println!("Hash map of DE public keys: {:?}", test_keys_map); + + let expected_key = "#V8jt_0c-aFlq40Uti2R_WiquxuzxyB8kn1cfWmXIU84"; + let first_key = test_keys_map.keys().next().expect("HashMap empty!"); + assert_eq!( + first_key, expected_key, + "The first key of the HashMap is not the expected key id." + ); + } #[test] fn test_josekit_to_ssi_jwk() { @@ -356,44 +483,56 @@ mod tests { #[test] fn test_content_response() { - // keys we need + // keys the UE needs let upstream_cr_keys = KeysCR { private_key: serde_json::from_str(UPSTREAM_PRIVATE_KEY).unwrap(), public_key: serde_json::from_str(TEMP_PUB_KEY).unwrap(), }; - // TODO: extract public keys from did document -> Vec<&KeyPairs> - let mut downstream_keys = Vec::<&Jwk>::new(); - let downstream_pub_key_1: Jwk = serde_json::from_str(DOWNSTREAM_PUB_KEY_1).unwrap(); - let downstream_pub_key_2: Jwk = serde_json::from_str(DOWNSTREAM_PUB_KEY_2).unwrap(); - downstream_keys.push(&downstream_pub_key_1); - downstream_keys.push(&downstream_pub_key_2); - - // generate one nonce per public key -> sign individually (vec of signed nonces) - let challenge_vec: Vec = downstream_keys - .iter() - .map(|key| generate_challenge(key).unwrap()) - .collect(); - - let key_hash_vec: Vec = downstream_keys + // extract DE public keys from did document -> Vec<&KeyPairs> + // let doc: Document = serde_json::from_str(TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS).unwrap(); + // let test_keys_map = extract_key_ids_and_jwk(&doc).unwrap(); + let mut test_keys_map: HashMap = HashMap::new(); + test_keys_map.insert( + String::from("key_1"), + serde_json::from_str(DOWNSTREAM_PUB_KEY_1).unwrap(), + ); + test_keys_map.insert( + String::from("key_2"), + serde_json::from_str(DOWNSTREAM_PUB_KEY_2).unwrap(), + ); + + // TODO: this should go in its own function + let nonces: HashMap = + test_keys_map + .iter() + .fold(HashMap::new(), |mut acc, (key_id, _)| { + acc.insert(String::from(key_id), generate_nonce()); + acc + }); + + for (key, val) in &nonces { + println!("{}", val); + } + + let challenges = nonces .iter() - .map(|key| hex::encode(Sha256::digest(serde_json::to_string(&key).unwrap()))) - .collect(); - - println!("Vector with key hashes: {:?}", key_hash_vec); + .fold(HashMap::new(), |mut acc, (key_id, nonce)| { + acc.insert( + String::from(key_id), + encrypt(nonce.clone(), &test_keys_map.get(key_id).unwrap()).unwrap(), + ); + acc + }); // sign (UE private key) and encrypt (DE temp public key) entire challenge + let value: serde_json::Value = serde_json::to_value(challenges).unwrap(); let mut payload = JwtPayload::new(); - payload - .set_claim("challenge", Some(Value::from(challenge_vec))) - .unwrap(); - payload - .set_claim("key_hash", Some(Value::from(key_hash_vec))) - .unwrap(); + payload.set_claim("challenges", Some(value)).unwrap(); + let encrypted_challenge = upstream_cr_keys.sign_and_encrypt(&payload).unwrap(); - // generate response - // verify and decrypt -> extract vectors with challenges and key hashes + // verify and decrypt let downstream_cr_keys = KeysCR { private_key: serde_json::from_str(TEMP_PRIVATE_KEY).unwrap(), public_key: serde_json::from_str(UPSTREAM_PUB_KEY).unwrap(), @@ -402,37 +541,40 @@ mod tests { .decrypt_and_verify(encrypted_challenge) .unwrap(); - // extract vector with challenge nonce(s) - let challenge_vec = decrypted_challenge - .claim("challenge") - .unwrap() - .as_array() - .unwrap(); - - // private keys - let mut downstream_private_keys = Vec::<&Jwk>::new(); - let downstream_private_key_1: Jwk = serde_json::from_str(DOWNSTREAM_PRIV_KEY_1).unwrap(); - let downstream_private_key_2: Jwk = serde_json::from_str(DOWNSTREAM_PRIV_KEY_2).unwrap(); - downstream_private_keys.push(&downstream_private_key_1); - downstream_private_keys.push(&downstream_private_key_2); - - // decrypt each nonce - let response_vec: Vec = challenge_vec - .iter() - .zip(downstream_private_keys.iter()) - .map(|(nonce, key)| decrypt(&nonce, &key).unwrap()) - .collect(); - println!("Decrypted challenge vector: {:?}", response_vec); - // continue here!!!!!!!!!!!!! How do we find the right key for each nonce? - - // TODO: prepare response - - // TODO: verify response (nonces) + // extract challenge hashmap + let challenges_hashmap: HashMap = + serde_json::from_value(decrypted_challenge.claim("challenges").unwrap().clone()) + .unwrap(); + + // Decrypt each challenge nonce + let mut test_priv_keys_map: HashMap = HashMap::new(); + test_priv_keys_map.insert( + String::from("key_1"), + serde_json::from_str(DOWNSTREAM_PRIV_KEY_1).unwrap(), + ); + test_priv_keys_map.insert( + String::from("key_2"), + serde_json::from_str(DOWNSTREAM_PRIV_KEY_2).unwrap(), + ); + + // ----------------------------------------- + let response = + generate_content_response(challenges_hashmap, test_priv_keys_map, &downstream_cr_keys) + .unwrap(); + + // UE: verify response + let verified_response = verify_content_response(response, &upstream_cr_keys).unwrap(); + + let nonce_1 = verified_response.get("key_1").unwrap(); + let expected_nonce_1 = nonces.get("key_1").unwrap(); + + assert_eq!( + verified_response.get("key_1").unwrap(), + nonces.get("key_1").unwrap() + ); + assert_eq!( + verified_response.get("key_2").unwrap(), + nonces.get("key_2").unwrap() + ); } - - // #[test] - // fn test_ec_key() { - // let key = Jwk::generate_ec_key(josekit::jwk::alg::ec::EcCurve::Secp256k1).unwrap(); - // println!("{}", serde_json::to_string_pretty(&key).unwrap()); - // } } diff --git a/trustchain-http/src/lib.rs b/trustchain-http/src/lib.rs index a69ae163..e0c5acc2 100644 --- a/trustchain-http/src/lib.rs +++ b/trustchain-http/src/lib.rs @@ -1,5 +1,7 @@ pub mod attestor; pub mod config; +pub mod data; +pub mod encryption; pub mod errors; pub mod handlers; pub mod issuer; @@ -9,7 +11,6 @@ pub mod resolver; pub mod server; pub mod state; pub mod verifier; -pub mod encryption; /// Issuer DID // TODO: add issuer/verifier configuration as used for core/ion crates From 204cd2a3e7b195b7d95c4f7f7cb5ec14c521185e Mon Sep 17 00:00:00 2001 From: pwochner Date: Fri, 11 Aug 2023 16:55:49 +0100 Subject: [PATCH 10/86] Types and traits for identity CR, signing and encrypting. --- trustchain-http/src/challenge_response.rs | 154 ++++++++++++++++++++++ trustchain-http/src/encryption.rs | 18 +++ trustchain-http/src/lib.rs | 1 + 3 files changed, 173 insertions(+) create mode 100644 trustchain-http/src/challenge_response.rs diff --git a/trustchain-http/src/challenge_response.rs b/trustchain-http/src/challenge_response.rs new file mode 100644 index 00000000..2bca0ad5 --- /dev/null +++ b/trustchain-http/src/challenge_response.rs @@ -0,0 +1,154 @@ +use josekit::jwe::{JweHeader, ECDH_ES}; +use josekit::jwk::Jwk; +use josekit::jws::{JwsHeader, ES256K}; +use josekit::jwt::{self, JwtPayload}; +use josekit::JoseError; +use rand::rngs::StdRng; +use rand::{distributions::Alphanumeric, Rng, SeedableRng}; +use serde_json::Value; +use std::collections::HashMap; +use thiserror::Error; +use trustchain_core::key_manager::{KeyManager, KeyType}; +use trustchain_core::subject::Subject; + +#[derive(Error, Debug)] +pub enum TrustchainCRError { + /// Serde JSON error. + #[error("Wrapped serialization error: {0}")] + Serde(serde_json::Error), + /// Wrapped jose error. + #[error("Wrapped jose error: {0}")] + Jose(JoseError), + /// Missing JWK from verification method + #[error("Missing JWK from verification method of a DID document.")] + MissingJWK, + /// Key not found in hashmap + #[error("Key id not found.")] + KeyNotFound, + /// Claim not found in JWTPayload + #[error("Claim not found in JWTPayload.")] + ClaimNotFound, +} + +impl From for TrustchainCRError { + fn from(err: JoseError) -> Self { + Self::Jose(err) + } +} + +struct UpstreamState {} // same as Downstream? + +// struct DownstreamState{} + +struct CRInitiation { + temp_p_key: Jwk, + requester_org: String, + operator_name: String, +} + +struct CRIdentityChallenge { + update_p_key: Jwk, + identity_nonce: String, // make own Nonce type + // field for the signed and encrypted challenge? +} + +impl TryFrom for JwtPayload { + type Error = TrustchainCRError; + fn try_from(value: CRIdentityChallenge) -> Result { + let mut payload = JwtPayload::new(); + payload.set_claim("identity_nonce", Some(Value::from(value.identity_nonce)))?; + // Todo: add update_p_key + Ok(payload) + } +} + +/// Interface for signing and then encrypting data. +trait SignEncrypt { + fn sign(&self, payload: &JwtPayload, secret_key: Jwk) -> Result { + let mut header = JwsHeader::new(); + header.set_token_type("JWT"); + let signer = ES256K.signer_from_jwk(&secret_key)?; + let signed_jwt = jwt::encode_with_signer(payload, &header, &signer)?; + Ok(signed_jwt) + } + /// `JWTPayload` is a wrapped [`Map`](https://docs.rs/serde_json/1.0.79/serde_json/struct.Map.html) + /// of claims. + fn encrypt(&self, payload: &JwtPayload, public_key: &Jwk) -> Result { + let mut header = JweHeader::new(); + header.set_token_type("JWT"); + header.set_content_encryption("A128CBC-HS256"); + header.set_content_encryption("A256GCM"); + + let encrypter = ECDH_ES.encrypter_from_jwk(&public_key)?; + let encrypted_jwt = jwt::encode_with_encrypter(payload, &header, &encrypter)?; + Ok(encrypted_jwt) + } + /// Combined sign and encryption + fn sign_and_encrypt_claim( + &self, + payload: &JwtPayload, + secret_key: Jwk, + public_key: Jwk, + ) -> Result { + let signed_encoded_payload = self.sign(payload, secret_key)?; + // make payload of claims to encrypt + let mut claims = JwtPayload::new(); + claims.set_claim("claim", Some(Value::from(signed_encoded_payload)))?; + self.encrypt(&claims, &public_key) + } +} +/// Interface for decrypting and then verify data. +trait DecryptVerify { + // fn decrypt + // fn verify + // fn decrypt_and_verify +} + +struct MyType { + foo: String, +} + +impl TryFrom for MyType { + type Error = TrustchainCRError; + fn try_from(value: JwtPayload) -> Result { + let x = value.claim("foo").ok_or(TrustchainCRError::ClaimNotFound)?; + Ok(Self { foo: x.to_string() }) + } +} + +// impl From for MyType { +// fn from(value: JwtPayload) -> Self { +// let x = value.claim("foo").unwrap(); +// Self { foo: x.to_string() } +// } +// } + +// impl From for JwtPayload { +// fn from(value: MyType) -> Self {} +// } + +/// Generates a random alphanumeric nonce of a specified length using a seeded random number generator. +fn generate_nonce(seed: u64) -> String { + let rng: StdRng = SeedableRng::seed_from_u64(seed); + rng.sample_iter(&Alphanumeric) + .take(32) + .map(char::from) + .collect() +} + +fn present_identity_challenge() { + todo!() +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_generate_nonce() { + let expected_nonce = String::from("IhPi3oZCnaWvL2oIeA07mg3ZtJzh0NoA"); + let nonce = generate_nonce(42); + assert_eq!(nonce, expected_nonce) + } +} diff --git a/trustchain-http/src/encryption.rs b/trustchain-http/src/encryption.rs index e5c9d304..ab6a1ae6 100644 --- a/trustchain-http/src/encryption.rs +++ b/trustchain-http/src/encryption.rs @@ -503,6 +503,7 @@ mod tests { ); // TODO: this should go in its own function + // map with unencrypted nonces so UE can store them for later verification let nonces: HashMap = test_keys_map .iter() @@ -577,4 +578,21 @@ mod tests { nonces.get("key_2").unwrap() ); } + #[test] + fn test_jwk_thumbprint() { + let doc: Document = serde_json::from_str(TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS).unwrap(); + let test_keys_map = extract_key_ids_and_jwk(&doc).unwrap(); + for (key_id, value) in test_keys_map { + println!("{}", key_id); + let ssi_key = josekit_to_ssi_jwk(&value).unwrap(); + let key_thumbprint = ssi_key.thumbprint().unwrap(); + println!("Thumbprint: {}", key_thumbprint); + } + } } + +// todo +// +// - what exactly does thumbprint() return and how does it relate to key id? +// - test did with multiple signing keys and corresponding private keys +// - add update commitment to identity CR diff --git a/trustchain-http/src/lib.rs b/trustchain-http/src/lib.rs index e0c5acc2..6f1d848f 100644 --- a/trustchain-http/src/lib.rs +++ b/trustchain-http/src/lib.rs @@ -1,4 +1,5 @@ pub mod attestor; +pub mod challenge_response; pub mod config; pub mod data; pub mod encryption; From 7283cf487bfce87bde651cfe31ca161831bb818a Mon Sep 17 00:00:00 2001 From: pwochner Date: Wed, 23 Aug 2023 06:49:11 +0100 Subject: [PATCH 11/86] Add test did document and keys for CR. --- trustchain-http/src/data.rs | 108 +++++++++++++++++++++++------------- 1 file changed, 69 insertions(+), 39 deletions(-) diff --git a/trustchain-http/src/data.rs b/trustchain-http/src/data.rs index 88815a67..d50a29da 100644 --- a/trustchain-http/src/data.rs +++ b/trustchain-http/src/data.rs @@ -2,61 +2,91 @@ pub const TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS: &str = r##" { - "@context" : [ - "https://www.w3.org/ns/did/v1", - { - "@base" : "did:ion:test:EiCBr7qGDecjkR2yUBhn3aNJPUR3TSEOlkpNcL0Q5Au9ZQ" - } - ], - "assertionMethod" : [ - "#V8jt_0c-aFlq40Uti2R_WiquxuzxyB8kn1cfWmXIU84" - ], "authentication" : [ - "#V8jt_0c-aFlq40Uti2R_WiquxuzxyB8kn1cfWmXIU84" - ], - "capabilityDelegation" : [ - "#V8jt_0c-aFlq40Uti2R_WiquxuzxyB8kn1cfWmXIU84" - ], - "capabilityInvocation" : [ - "#V8jt_0c-aFlq40Uti2R_WiquxuzxyB8kn1cfWmXIU84" - ], - "id" : "did:ion:test:EiCBr7qGDecjkR2yUBhn3aNJPUR3TSEOlkpNcL0Q5Au9ZQ", - "keyAgreement" : [ - "#V8jt_0c-aFlq40Uti2R_WiquxuzxyB8kn1cfWmXIU84" + "#bZdi2pQK5dk6YF8uVKz_P7SvRgZJ6DUT1KcsLM7L1QA", + "#a9vxpkAsksMUOXqjAdnZhQiVOKY-a0QDOdnrDL6lw40" ], "service" : [ { - "id" : "#trustchain-controller-proof", - "type" : "TrustchainProofService", - "serviceEndpoint" : { - "proofValue" : "eyJhbGciOiJFUzI1NksifQ.IkVpQmNiTkRRcjZZNHNzZGc5QXo4eC1qNy1yS1FuNWk5T2Q2S3BjZ2c0RU1KOXci.Nii8p38DtzyurmPHO9sV2JLSH7-Pv-dCKQ0Y-H34rplwhhwca2nSra4ZofcUsHCG6u1oKJ0x4AmMUD2_3UIhRA", - "controller" : "did:ion:test:EiCBr7qGDecjkR2yUBhn3aNJPUR3TSEOlkpNcL0Q5Au9ZQ" - } + "id" : "#TrustchainID", + "serviceEndpoint" : "https://identity.foundation/ion/trustchain-root-plus-2-not-downstream-yet", + "type" : "Identity" } ], "verificationMethod" : [ { - "controller" : "did:ion:test:EiCBr7qGDecjkR2yUBhn3aNJPUR3TSEOlkpNcL0Q5Au9ZQ", - "id" : "#V8jt_0c-aFlq40Uti2R_WiquxuzxyB8kn1cfWmXIU84", + "type" : "JsonWebSignature2020", + "controller" : "did:ion:test:EiC5GlkBZaC6SYiCexvcr2hgMPVdSoREIhK8KbekQRgphg", "publicKeyJwk" : { "crv" : "secp256k1", + "y" : "MSxXXbRIm3OWYgyhJBC3mpAg3uCniPsxkQs486i8XTw", "kty" : "EC", - "x" : "RbIj1Y4jeqkn0cizEfxHZidD-GQouFmAtE6YCpxFjpg", - "y" : "ZcbgNp3hrfp3cujZFKqgFS0uFGOn2Rk16Y9nOv0h15s" + "x" : "0vYBCPbQLlPCTW_iTdh9ubbrQqhZh9JWyP89tDKsbew" }, - "type" : "JsonWebSignature2020" + "id" : "#bZdi2pQK5dk6YF8uVKz_P7SvRgZJ6DUT1KcsLM7L1QA" }, { - "controller" : "did:ion:test:EiCBr7qGDecjkR2yUBhn3aNJPUR3TSEOlkpNcL0Q5Au9ZQ", - "id" : "#V9jt_0c-aFlq40Uti2R_WiquxuzxyB8kn1cfWmXIU85", + "type" : "JsonWebSignature2020", + "controller" : "did:ion:test:EiC5GlkBZaC6SYiCexvcr2hgMPVdSoREIhK8KbekQRgphg", "publicKeyJwk" : { - "crv": "secp256k1", - "kty": "EC", - "x": "7ReQHHysGxbyuKEQmspQOjL7oQUqDTldTHuc9V3-yso", - "y": "kWvmS7ZOvDUhF8syO08PBzEpEk3BZMuukkvEJOKSjqE" + "x" : "aeq7ALPoynBWX_QDFzJxyX8USRTHzL9lm52Orvzy-DM", + "crv" : "secp256k1", + "y" : "25MLCu-qxD_axvomnLZVgGHehJ_CO6pNE4IklQMaVzA", + "kty" : "EC" }, - "type" : "JsonWebSignature2020" + "id" : "#a9vxpkAsksMUOXqjAdnZhQiVOKY-a0QDOdnrDL6lw40" } - ] + ], + "assertionMethod" : [ + "#bZdi2pQK5dk6YF8uVKz_P7SvRgZJ6DUT1KcsLM7L1QA", + "#a9vxpkAsksMUOXqjAdnZhQiVOKY-a0QDOdnrDL6lw40" + ], + "@context" : [ + "https://www.w3.org/ns/did/v1", + { + "@base" : "did:ion:test:EiC5GlkBZaC6SYiCexvcr2hgMPVdSoREIhK8KbekQRgphg" + } + ], + "keyAgreement" : [ + "#bZdi2pQK5dk6YF8uVKz_P7SvRgZJ6DUT1KcsLM7L1QA", + "#a9vxpkAsksMUOXqjAdnZhQiVOKY-a0QDOdnrDL6lw40" + ], + "capabilityInvocation" : [ + "#bZdi2pQK5dk6YF8uVKz_P7SvRgZJ6DUT1KcsLM7L1QA", + "#a9vxpkAsksMUOXqjAdnZhQiVOKY-a0QDOdnrDL6lw40" + ], + "capabilityDelegation" : [ + "#bZdi2pQK5dk6YF8uVKz_P7SvRgZJ6DUT1KcsLM7L1QA", + "#a9vxpkAsksMUOXqjAdnZhQiVOKY-a0QDOdnrDL6lw40" + ], + "id" : "did:ion:test:EiC5GlkBZaC6SYiCexvcr2hgMPVdSoREIhK8KbekQRgphg" } "##; + +pub const TEST_KEY_ID_1: &str = r##"#bZdi2pQK5dk6YF8uVKz_P7SvRgZJ6DUT1KcsLM7L1QA"##; +// key_id: #bZdi2pQK5dk6YF8uVKz_P7SvRgZJ6DUT1KcsLM7L1QA +pub const TEST_SIGNING_KEY_1: &str = r##" +{ + "kty": "EC", + "crv": "secp256k1", + "x": "0vYBCPbQLlPCTW_iTdh9ubbrQqhZh9JWyP89tDKsbew", + "y": "MSxXXbRIm3OWYgyhJBC3mpAg3uCniPsxkQs486i8XTw", + "d": "JqWC8hlh9KX0XaUsl6xbiYtSX0TC1cEaqb338boJHDs" + } +"##; + +pub const TEST_KEY_ID_2: &str = r##"#a9vxpkAsksMUOXqjAdnZhQiVOKY-a0QDOdnrDL6lw40"##; +// key_id: #a9vxpkAsksMUOXqjAdnZhQiVOKY-a0QDOdnrDL6lw40 +pub const TEST_SIGNING_KEY_2: &str = r##" +{ + "kty": "EC", + "crv": "secp256k1", + "x": "aeq7ALPoynBWX_QDFzJxyX8USRTHzL9lm52Orvzy-DM", + "y": "25MLCu-qxD_axvomnLZVgGHehJ_CO6pNE4IklQMaVzA", + "d": "YoSojHkEat0RefQxbzeS-X2JIW3BCJTgc8-VM6ombWk" + } +"##; + +pub const TEST_UPSTREAM_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JEV4WMgoJekTa5RQD5M92P1oLjdpMNYETQ3nbtKSnLQ","y":"dRfg_5i5wcMg1lxAffQORHpzgtm2yEIqgJoUk5ZklvI","d":"DZDZd9bxopCv2YJelMpQm_BJ0awvzpT6xWdWbaQlIJI"}"#; +pub const TEST_TEMP_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg","d":"CfdUwQ-CcBQkWpIDPjhSJAq2SCg6hAGdcvLmCj0aA-c"}"#; +pub const TEST_UPDATE_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"AB1b_4-XSem0uiPGGuW_hf_AuPArukMuD2S95ypGDSE","y":"suvBnCbhicPdYZeqgxJfPFmiNHGYDjPiW8XkYHxwgBU"}"#; From ddc554e2d3888733af566d94c5600269a6b0189c Mon Sep 17 00:00:00 2001 From: pwochner Date: Wed, 23 Aug 2023 06:51:02 +0100 Subject: [PATCH 12/86] Test content CR on multiple signing keys. --- trustchain-http/src/encryption.rs | 58 +++++++++++++++---------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/trustchain-http/src/encryption.rs b/trustchain-http/src/encryption.rs index ab6a1ae6..f0dcc0a0 100644 --- a/trustchain-http/src/encryption.rs +++ b/trustchain-http/src/encryption.rs @@ -393,13 +393,21 @@ fn verify_content_response( Ok(response_hashmap) } +/// Reads key that corresponds to given key id from file +fn get_private_key(key_id: &str) -> Result { + todo!() +} + #[cfg(test)] mod tests { use serde_json::from_str; use super::*; - use crate::data::TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS; + use crate::data::{ + TEST_KEY_ID_1, TEST_KEY_ID_2, TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS, TEST_SIGNING_KEY_1, + TEST_SIGNING_KEY_2, + }; #[test] fn test_extract_key_ids_and_jwk() { @@ -490,19 +498,18 @@ mod tests { }; // extract DE public keys from did document -> Vec<&KeyPairs> - // let doc: Document = serde_json::from_str(TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS).unwrap(); - // let test_keys_map = extract_key_ids_and_jwk(&doc).unwrap(); - let mut test_keys_map: HashMap = HashMap::new(); - test_keys_map.insert( - String::from("key_1"), - serde_json::from_str(DOWNSTREAM_PUB_KEY_1).unwrap(), - ); - test_keys_map.insert( - String::from("key_2"), - serde_json::from_str(DOWNSTREAM_PUB_KEY_2).unwrap(), - ); + let doc: Document = serde_json::from_str(TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS).unwrap(); + let test_keys_map = extract_key_ids_and_jwk(&doc).unwrap(); + // let mut test_keys_map: HashMap = HashMap::new(); + // test_keys_map.insert( + // String::from("key_1"), + // serde_json::from_str(DOWNSTREAM_PUB_KEY_1).unwrap(), + // ); + // test_keys_map.insert( + // String::from("key_2"), + // serde_json::from_str(DOWNSTREAM_PUB_KEY_2).unwrap(), + // ); - // TODO: this should go in its own function // map with unencrypted nonces so UE can store them for later verification let nonces: HashMap = test_keys_map @@ -512,7 +519,7 @@ mod tests { acc }); - for (key, val) in &nonces { + for (_, val) in &nonces { println!("{}", val); } @@ -550,15 +557,14 @@ mod tests { // Decrypt each challenge nonce let mut test_priv_keys_map: HashMap = HashMap::new(); test_priv_keys_map.insert( - String::from("key_1"), - serde_json::from_str(DOWNSTREAM_PRIV_KEY_1).unwrap(), + String::from(TEST_KEY_ID_1), + serde_json::from_str(TEST_SIGNING_KEY_1).unwrap(), ); test_priv_keys_map.insert( - String::from("key_2"), - serde_json::from_str(DOWNSTREAM_PRIV_KEY_2).unwrap(), + String::from(TEST_KEY_ID_2), + serde_json::from_str(TEST_SIGNING_KEY_2).unwrap(), ); - // ----------------------------------------- let response = generate_content_response(challenges_hashmap, test_priv_keys_map, &downstream_cr_keys) .unwrap(); @@ -566,16 +572,13 @@ mod tests { // UE: verify response let verified_response = verify_content_response(response, &upstream_cr_keys).unwrap(); - let nonce_1 = verified_response.get("key_1").unwrap(); - let expected_nonce_1 = nonces.get("key_1").unwrap(); - assert_eq!( - verified_response.get("key_1").unwrap(), - nonces.get("key_1").unwrap() + verified_response.get(TEST_KEY_ID_1).unwrap(), + nonces.get(TEST_KEY_ID_1).unwrap() ); assert_eq!( - verified_response.get("key_2").unwrap(), - nonces.get("key_2").unwrap() + verified_response.get(TEST_KEY_ID_2).unwrap(), + nonces.get(TEST_KEY_ID_2).unwrap() ); } #[test] @@ -592,7 +595,4 @@ mod tests { } // todo -// -// - what exactly does thumbprint() return and how does it relate to key id? -// - test did with multiple signing keys and corresponding private keys // - add update commitment to identity CR From e6c9a811e1fc51c55badd40d97962ace2db9a486 Mon Sep 17 00:00:00 2001 From: pwochner Date: Wed, 23 Aug 2023 06:56:03 +0100 Subject: [PATCH 13/86] Types and traits for CR. Test for content CR. --- trustchain-http/src/challenge_response.rs | 511 +++++++++++++++++++--- 1 file changed, 456 insertions(+), 55 deletions(-) diff --git a/trustchain-http/src/challenge_response.rs b/trustchain-http/src/challenge_response.rs index 2bca0ad5..51193393 100644 --- a/trustchain-http/src/challenge_response.rs +++ b/trustchain-http/src/challenge_response.rs @@ -3,13 +3,14 @@ use josekit::jwk::Jwk; use josekit::jws::{JwsHeader, ES256K}; use josekit::jwt::{self, JwtPayload}; use josekit::JoseError; -use rand::rngs::StdRng; -use rand::{distributions::Alphanumeric, Rng, SeedableRng}; +use rand::thread_rng; +use rand::{distributions::Alphanumeric, Rng}; +use serde::{Deserialize, Serialize}; use serde_json::Value; +use ssi::did::{Document, VerificationMethod}; +use ssi::jwk::JWK; use std::collections::HashMap; use thiserror::Error; -use trustchain_core::key_manager::{KeyManager, KeyType}; -use trustchain_core::subject::Subject; #[derive(Error, Debug)] pub enum TrustchainCRError { @@ -19,15 +20,18 @@ pub enum TrustchainCRError { /// Wrapped jose error. #[error("Wrapped jose error: {0}")] Jose(JoseError), - /// Missing JWK from verification method + /// Missing JWK from verification method. #[error("Missing JWK from verification method of a DID document.")] MissingJWK, - /// Key not found in hashmap + /// Key not found in hashmap. #[error("Key id not found.")] KeyNotFound, - /// Claim not found in JWTPayload + /// Claim not found in JWTPayload. #[error("Claim not found in JWTPayload.")] ClaimNotFound, + /// Nonce type invalid. + #[error("Invalid nonce type.")] + InvalidNonceType, } impl From for TrustchainCRError { @@ -36,35 +40,175 @@ impl From for TrustchainCRError { } } -struct UpstreamState {} // same as Downstream? +// pub struct Nonce([u8; N]); +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct Nonce(String); -// struct DownstreamState{} +// impl Nonce { +impl Nonce { + pub fn new() -> Self { + Self( + thread_rng() + .sample_iter(&Alphanumeric) + .take(32) + .map(char::from) + .collect(), + ) + } +} -struct CRInitiation { - temp_p_key: Jwk, +impl AsRef for Nonce { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl ToString for Nonce { + fn to_string(&self) -> String { + self.0.clone() + } +} + +impl From for Nonce { + fn from(s: String) -> Self { + Self(s) + } +} + +#[derive(Debug)] +struct CRState { + initiation: Option, + identity_challenge_response: Option, +} + +struct Entity {} + +trait ElementwiseSerializeDeserialize { + fn elementwise_serialize(&self) -> Result<(), TrustchainCRError>; + // todo: default implementation, look if exists already + fn elementwise_deserialize(&self) -> Result<(), TrustchainCRError>; +} + +#[derive(Debug, Serialize, Deserialize)] +struct RequesterDetails { requester_org: String, operator_name: String, } +#[derive(Debug)] +struct CRInitiation { + temp_p_key: Jwk, + requester_details: RequesterDetails, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] struct CRIdentityChallenge { - update_p_key: Jwk, - identity_nonce: String, // make own Nonce type - // field for the signed and encrypted challenge? + update_p_key: Option, + identity_nonce: Option, // make own Nonce type + identity_challenge_signature: Option, + identity_response_signature: Option, } -impl TryFrom for JwtPayload { +struct CRContentChallenge { + content_nonce: Option>, + content_challenge_signature: Option, + content_response_signature: Option, +} + +impl TryFrom<&CRIdentityChallenge> for JwtPayload { + type Error = TrustchainCRError; + fn try_from(value: &CRIdentityChallenge) -> Result { + let mut payload = JwtPayload::new(); + payload.set_claim( + "identity_nonce", + Some(Value::from( + value.identity_nonce.as_ref().unwrap().to_string(), + )), + )?; + payload.set_claim( + "update_p_key", + Some(Value::from( + value.update_p_key.as_ref().unwrap().to_string(), + )), + )?; + Ok(payload) + } +} + +impl TryFrom<&JwtPayload> for CRIdentityChallenge { + type Error = TrustchainCRError; + fn try_from(value: &JwtPayload) -> Result { + let mut challenge = CRIdentityChallenge { + update_p_key: None, + identity_nonce: None, + identity_challenge_signature: None, + identity_response_signature: None, + }; + challenge.update_p_key = Some( + serde_json::from_str(value.claim("update_p_key").unwrap().as_str().unwrap()).unwrap(), + ); + challenge.identity_nonce = Some(Nonce::from( + value + .claim("identity_nonce") + .unwrap() + .as_str() + .unwrap() + .to_string(), + )); + Ok(challenge) + } +} + +impl TryFrom<&Nonce> for JwtPayload { type Error = TrustchainCRError; - fn try_from(value: CRIdentityChallenge) -> Result { + fn try_from(value: &Nonce) -> Result { let mut payload = JwtPayload::new(); - payload.set_claim("identity_nonce", Some(Value::from(value.identity_nonce)))?; - // Todo: add update_p_key + payload.set_claim("nonce", Some(Value::from(value.to_string())))?; Ok(payload) } } +// impl TryFrom<(&JwtPayload, Vec<&str>)> for CRIdentityChallenge { +// type Error = TrustchainCRError; +// fn try_from((value, claims): (&JwtPayload, Vec<&str>)) -> Result { +// let mut challenge = CRIdentityChallenge { +// update_p_key: None, +// identity_nonce: None, +// identity_challenge_signature: None, +// identity_response_signature: None, +// }; + +// for claim in claims { +// match claim { +// "update_p_key" => { +// challenge.update_p_key = Some( +// serde_json::from_str( +// value.claim("update_p_key").unwrap().as_str().unwrap(), +// ) +// .unwrap(), +// ); +// } +// "identity_nonce" => { +// challenge.identity_nonce = Some( +// value +// .claim("identity_nonce") +// .unwrap() +// .as_str() +// .unwrap() +// .to_string(), +// ); +// } +// _ => {} +// } +// } + +// Ok(challenge) +// } +// } + /// Interface for signing and then encrypting data. -trait SignEncrypt { - fn sign(&self, payload: &JwtPayload, secret_key: Jwk) -> Result { +pub trait SignEncrypt { + fn sign(&self, payload: &JwtPayload, secret_key: &Jwk) -> Result { let mut header = JwsHeader::new(); header.set_token_type("JWT"); let signer = ES256K.signer_from_jwk(&secret_key)?; @@ -87,68 +231,325 @@ trait SignEncrypt { fn sign_and_encrypt_claim( &self, payload: &JwtPayload, - secret_key: Jwk, - public_key: Jwk, + secret_key: &Jwk, + public_key: &Jwk, ) -> Result { let signed_encoded_payload = self.sign(payload, secret_key)?; - // make payload of claims to encrypt let mut claims = JwtPayload::new(); claims.set_claim("claim", Some(Value::from(signed_encoded_payload)))?; self.encrypt(&claims, &public_key) } } -/// Interface for decrypting and then verify data. +/// Interface for decrypting and then verifying data. trait DecryptVerify { - // fn decrypt - // fn verify - // fn decrypt_and_verify -} - -struct MyType { - foo: String, -} + fn decrypt(&self, value: &Value, secret_key: &Jwk) -> Result { + let decrypter = ECDH_ES.decrypter_from_jwk(&secret_key)?; + let (payload, _) = jwt::decode_with_decrypter(value.as_str().unwrap(), &decrypter)?; + Ok(payload) + } + fn decrypt_and_verify( + &self, + input: String, + secret_key: &Jwk, + public_key: &Jwk, + ) -> Result { + let decrypter = ECDH_ES.decrypter_from_jwk(secret_key)?; + let (payload, _) = jwt::decode_with_decrypter(input, &decrypter)?; -impl TryFrom for MyType { - type Error = TrustchainCRError; - fn try_from(value: JwtPayload) -> Result { - let x = value.claim("foo").ok_or(TrustchainCRError::ClaimNotFound)?; - Ok(Self { foo: x.to_string() }) + let verifier = ES256K.verifier_from_jwk(public_key)?; + let (payload, _) = jwt::decode_with_verifier( + &payload.claim("claim").unwrap().as_str().unwrap(), + &verifier, + )?; + Ok(payload) } } -// impl From for MyType { -// fn from(value: JwtPayload) -> Self { -// let x = value.claim("foo").unwrap(); -// Self { foo: x.to_string() } -// } -// } +impl SignEncrypt for Entity {} -// impl From for JwtPayload { -// fn from(value: MyType) -> Self {} -// } +impl DecryptVerify for Entity {} /// Generates a random alphanumeric nonce of a specified length using a seeded random number generator. -fn generate_nonce(seed: u64) -> String { - let rng: StdRng = SeedableRng::seed_from_u64(seed); - rng.sample_iter(&Alphanumeric) +fn generate_nonce() -> String { + // let rng: StdRng = SeedableRng::seed_from_u64(seed); + thread_rng() + .sample_iter(&Alphanumeric) .take(32) .map(char::from) .collect() } -fn present_identity_challenge() { - todo!() +// make a try_from instead +fn josekit_to_ssi_jwk(key: &Jwk) -> Result { + let key_as_str: &str = &serde_json::to_string(&key).unwrap(); + let ssi_key: JWK = serde_json::from_str(key_as_str).unwrap(); + Ok(ssi_key) +} + +fn ssi_to_josekit_jwk(key: &JWK) -> Result { + let key_as_str: &str = &serde_json::to_string(&key).unwrap(); + let ssi_key: Jwk = serde_json::from_str(key_as_str).unwrap(); + Ok(ssi_key) +} + +fn extract_key_ids_and_jwk(document: &Document) -> Result, TrustchainCRError> { + let mut my_map = HashMap::::new(); + if let Some(vms) = &document.verification_method { + // TODO: leave the commented code + // vms.iter().for_each(|vm| match vm { + // VerificationMethod::Map(vm_map) => { + // let id = vm_map.id; + // let key = vm_map.get_jwk().unwrap(); + // let key_jose = ssi_to_josekit_jwk(&key).unwrap(); + // my_map.insert(id, key_jose); + // } + // _ => (), + // }); + // TODO: consider rewriting functional with filter, partition, fold over returned error + // variants. + for vm in vms { + match vm { + VerificationMethod::Map(vm_map) => { + // let id = vm_map.id.clone(); // TODo: use JWK::thumbprint() instead + let key = vm_map + .get_jwk() + .map_err(|_| TrustchainCRError::MissingJWK)?; + let id = key + .thumbprint() + .map_err(|_| TrustchainCRError::MissingJWK)?; //TODO: different error variant? + let key_jose = + ssi_to_josekit_jwk(&key).map_err(|err| TrustchainCRError::Serde(err))?; + my_map.insert(id, key_jose); + } + _ => (), + } + } + } + Ok(my_map) } #[cfg(test)] mod tests { + use crate::data::{ + TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS, TEST_SIGNING_KEY_1, TEST_SIGNING_KEY_2, + TEST_TEMP_KEY, TEST_UPDATE_KEY, TEST_UPSTREAM_KEY, + }; + use super::*; #[test] - fn test_generate_nonce() { - let expected_nonce = String::from("IhPi3oZCnaWvL2oIeA07mg3ZtJzh0NoA"); - let nonce = generate_nonce(42); - assert_eq!(nonce, expected_nonce) + fn test_identity_challenge_response() { + // ==========| UE - generate challenge | ============== + let upstream_s_key: Jwk = serde_json::from_str(TEST_UPSTREAM_KEY).unwrap(); + let update_key: Jwk = serde_json::from_str(TEST_UPDATE_KEY).unwrap(); + let temp_s_key: Jwk = serde_json::from_str(TEST_TEMP_KEY).unwrap(); + let temp_p_key = temp_s_key.to_public_key().unwrap(); + + // generate challenge + let request_initiation = CRInitiation { + temp_p_key: temp_p_key.clone(), + requester_details: RequesterDetails { + requester_org: String::from("My Org"), + operator_name: String::from("John Doe"), + }, + }; + + let mut upstream_identity_challenge_response = CRIdentityChallenge { + update_p_key: Some(update_key.clone()), + identity_nonce: Some(Nonce::new()), + identity_challenge_signature: None, + identity_response_signature: None, + }; + + // sign and encrypt + let upstream_entity = Entity {}; + + let payload = JwtPayload::try_from(&upstream_identity_challenge_response).unwrap(); + let signed_encrypted_challenge = upstream_entity + .sign_and_encrypt_claim(&payload, &upstream_s_key, &request_initiation.temp_p_key) + .unwrap(); + + upstream_identity_challenge_response.identity_challenge_signature = + Some(signed_encrypted_challenge); + + // ==========| DE - generate response | ============== + + // decrypt and verify + let downstream_entity = Entity {}; + let upstream_p_key = upstream_s_key.to_public_key().unwrap(); + let signed_encrypted_challenge = upstream_identity_challenge_response + .identity_challenge_signature + .clone() + .unwrap(); + + let decrypted_verified_challenge = downstream_entity + .decrypt_and_verify(signed_encrypted_challenge, &temp_s_key, &upstream_p_key) + .unwrap(); + let downstream_identity_challenge = + CRIdentityChallenge::try_from(&decrypted_verified_challenge).unwrap(); + + // generate response + let mut payload = JwtPayload::new(); + payload + .set_claim( + "identity_nonce", + Some(Value::from( + downstream_identity_challenge + .identity_nonce + .as_ref() + .unwrap() + .to_string(), + )), + ) + .unwrap(); + let signed_encrypted_response = downstream_entity + .sign_and_encrypt_claim(&payload, &temp_s_key, &upstream_p_key) + .unwrap(); + + // ==========| UE - verify response | ============== + + // decrypt and verify signature + let decrypted_verified_response = upstream_entity + .decrypt_and_verify(signed_encrypted_response, &upstream_s_key, &temp_p_key) + .unwrap(); + + let nonce = decrypted_verified_response + .claim("identity_nonce") + .unwrap() + .as_str() + .unwrap(); + + let expected_nonce = upstream_identity_challenge_response + .identity_nonce + .unwrap() + .to_string(); + assert_eq!(nonce, expected_nonce); + } + + #[test] + fn test_content_challenge_response() { + // ==========| UE - generate challenge | ============== + let upstream_entity = Entity {}; + let upstream_s_key: Jwk = serde_json::from_str(TEST_UPSTREAM_KEY).unwrap(); + let temp_s_key: Jwk = serde_json::from_str(TEST_TEMP_KEY).unwrap(); + let temp_p_key = temp_s_key.to_public_key().unwrap(); + // get signing keys for DE from did document + let doc: Document = serde_json::from_str(TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS).unwrap(); + let test_keys_map = extract_key_ids_and_jwk(&doc).unwrap(); + + // generate map with unencrypted nonces so UE can store them for later verification + let nonces: HashMap = + test_keys_map + .iter() + .fold(HashMap::new(), |mut acc, (key_id, _)| { + acc.insert(String::from(key_id), Nonce::new()); + acc + }); + + for (_, val) in &nonces { + println!("{:?}", val); + } + + // turn nonces into challenges by encrypting them with the public keys of UE + let challenges = nonces + .iter() + .fold(HashMap::new(), |mut acc, (key_id, nonce)| { + acc.insert( + String::from(key_id), + upstream_entity + .encrypt( + &JwtPayload::try_from(nonce).unwrap(), + &test_keys_map.get(key_id).unwrap(), + ) + .unwrap(), + ); + acc + }); + + // sign (UE private key) and encrypt (DE temp public key) entire challenge + let value: serde_json::Value = serde_json::to_value(challenges).unwrap(); + let mut payload = JwtPayload::new(); + payload.set_claim("challenges", Some(value)).unwrap(); + let signed_encrypted_challenges = upstream_entity + .sign_and_encrypt_claim(&payload, &upstream_s_key, &temp_p_key) + .unwrap(); + + // ==========| DE - generate response | ============== + let downstream_entity = Entity {}; + let upstream_p_key = upstream_s_key.to_public_key().unwrap(); + + // decrypt and verify signature on challenges + let decrypted_verified_challenges = downstream_entity + .decrypt_and_verify(signed_encrypted_challenges, &temp_s_key, &upstream_p_key) + .unwrap(); + + // decrypt nonces from challenges + let challenges_map: HashMap = serde_json::from_value( + decrypted_verified_challenges + .claim("challenges") + .unwrap() + .clone(), + ) + .unwrap(); + + // todo: replace with function to read in private keys + let downstream_s_key_1: Jwk = serde_json::from_str(TEST_SIGNING_KEY_1).unwrap(); + let downstream_s_key_2: Jwk = serde_json::from_str(TEST_SIGNING_KEY_2).unwrap(); + let downstream_key_id_1 = josekit_to_ssi_jwk(&downstream_s_key_1) + .unwrap() + .thumbprint() + .unwrap(); + let downstream_key_id_2 = josekit_to_ssi_jwk(&downstream_s_key_2) + .unwrap() + .thumbprint() + .unwrap(); + + let mut downstream_s_keys_map: HashMap = HashMap::new(); + downstream_s_keys_map.insert(downstream_key_id_1, downstream_s_key_1); + downstream_s_keys_map.insert(downstream_key_id_2, downstream_s_key_2); + + let decrypted_nonces: HashMap = + challenges_map + .iter() + .fold(HashMap::new(), |mut acc, (key_id, nonce)| { + acc.insert( + String::from(key_id), + downstream_entity + .decrypt( + &Some(Value::from(nonce.clone())).unwrap(), + downstream_s_keys_map.get(key_id).unwrap(), + ) + .unwrap() + .claim("nonce") + .unwrap() + .as_str() + .unwrap() + .to_string(), + ); + + acc + }); + // sign and encrypt response + let value: serde_json::Value = serde_json::to_value(decrypted_nonces).unwrap(); + let mut payload = JwtPayload::new(); + payload.set_claim("nonces", Some(value)).unwrap(); + let signed_encrypted_response = downstream_entity + .sign_and_encrypt_claim(&payload, &temp_s_key, &upstream_p_key) + .unwrap(); + + // ==========| UE - verify response | ============== + let decrypted_verified_response = upstream_entity + .decrypt_and_verify(signed_encrypted_response, &upstream_s_key, &temp_p_key) + .unwrap(); + println!( + "Decrypted and verified response: {:?}", + decrypted_verified_response + ); + let verified_response_map: HashMap = + serde_json::from_value(decrypted_verified_response.claim("nonces").unwrap().clone()) + .unwrap(); + println!("Verified response map: {:?}", verified_response_map); + assert_eq!(verified_response_map, nonces); } } From d7bef94998aff2b36243dc5d8a87c9a860c7c128 Mon Sep 17 00:00:00 2001 From: pwochner Date: Mon, 4 Sep 2023 19:27:24 +0100 Subject: [PATCH 14/86] Implement writing identity CR struct to json file. --- trustchain-http/Cargo.toml | 1 + trustchain-http/src/challenge_response.rs | 189 ++++++++++++++++++++-- 2 files changed, 176 insertions(+), 14 deletions(-) diff --git a/trustchain-http/Cargo.toml b/trustchain-http/Cargo.toml index e0b8a88f..ee0c5d98 100644 --- a/trustchain-http/Cargo.toml +++ b/trustchain-http/Cargo.toml @@ -46,6 +46,7 @@ lazy_static="1.4.0" hex = "0.4.3" rand = "0.8" josekit = "0.8" +serde_with = "*" [dev-dependencies] axum-test-helper = "0.2.0" diff --git a/trustchain-http/src/challenge_response.rs b/trustchain-http/src/challenge_response.rs index 51193393..769aa3cb 100644 --- a/trustchain-http/src/challenge_response.rs +++ b/trustchain-http/src/challenge_response.rs @@ -6,10 +6,18 @@ use josekit::JoseError; use rand::thread_rng; use rand::{distributions::Alphanumeric, Rng}; use serde::{Deserialize, Serialize}; -use serde_json::Value; +use serde_json::{to_string_pretty as to_json, Value}; +use serde_with::skip_serializing_none; use ssi::did::{Document, VerificationMethod}; use ssi::jwk::JWK; +use std::any::type_name; use std::collections::HashMap; +use std::env; +use std::fs::{File, OpenOptions}; +use std::io::{BufWriter, Write}; + +use std::path::PathBuf; +use struct_iterable::Iterable; use thiserror::Error; #[derive(Error, Debug)] @@ -32,6 +40,12 @@ pub enum TrustchainCRError { /// Nonce type invalid. #[error("Invalid nonce type.")] InvalidNonceType, + /// Failed to open file. + #[error("Failed to open file.")] + FailedToOpen, + /// Failed to save to file. + #[error("Failed to save to file.")] + FailedToSave, } impl From for TrustchainCRError { @@ -75,7 +89,8 @@ impl From for Nonce { } } -#[derive(Debug)] +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize)] struct CRState { initiation: Option, identity_challenge_response: Option, @@ -84,30 +99,74 @@ struct CRState { struct Entity {} trait ElementwiseSerializeDeserialize { - fn elementwise_serialize(&self) -> Result<(), TrustchainCRError>; + fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError>; // todo: default implementation, look if exists already fn elementwise_deserialize(&self) -> Result<(), TrustchainCRError>; + + fn save_to_file(&self, path: &PathBuf, data: &str) -> Result<(), TrustchainCRError> { + // Open the new file + let file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path); + + // Write key to file + if let Ok(file) = file { + let mut writer = BufWriter::new(file); + match writer.write_all(data.as_bytes()) { + Ok(_) => Ok(()), + Err(_) => Err(TrustchainCRError::FailedToSave), + } + } else { + Err(TrustchainCRError::FailedToSave) + } + } } +#[skip_serializing_none] #[derive(Debug, Serialize, Deserialize)] struct RequesterDetails { requester_org: String, operator_name: String, } -#[derive(Debug)] +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize)] struct CRInitiation { temp_p_key: Jwk, requester_details: RequesterDetails, } -#[derive(Debug, Serialize, Deserialize, Clone)] +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Iterable)] struct CRIdentityChallenge { update_p_key: Option, identity_nonce: Option, // make own Nonce type identity_challenge_signature: Option, identity_response_signature: Option, } +// todo: add path to serialise/deserialise functions? +impl ElementwiseSerializeDeserialize for CRIdentityChallenge { + fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { + let file_path = path.join("update_p_key.json"); + let data: &str = &to_json(&self.update_p_key).unwrap(); + if !file_path.exists() { + save_to_file(&file_path, data); + } + let file_path = path.join("identity_nonce.json"); + let data: &str = &to_json(&self.identity_nonce).unwrap(); + if !file_path.exists() { + save_to_file(&file_path, data); + } + Ok(()) + // todo: handle case where file already exists + // todo: handle case where field is None + } + fn elementwise_deserialize(&self) -> Result<(), TrustchainCRError> { + Ok(()) + } +} struct CRContentChallenge { content_nonce: Option>, @@ -269,15 +328,15 @@ impl SignEncrypt for Entity {} impl DecryptVerify for Entity {} -/// Generates a random alphanumeric nonce of a specified length using a seeded random number generator. -fn generate_nonce() -> String { - // let rng: StdRng = SeedableRng::seed_from_u64(seed); - thread_rng() - .sample_iter(&Alphanumeric) - .take(32) - .map(char::from) - .collect() -} +// /// Generates a random alphanumeric nonce of a specified length using a seeded random number generator. +// fn generate_nonce() -> String { +// // let rng: StdRng = SeedableRng::seed_from_u64(seed); +// thread_rng() +// .sample_iter(&Alphanumeric) +// .take(32) +// .map(char::from) +// .collect() +// } // make a try_from instead fn josekit_to_ssi_jwk(key: &Jwk) -> Result { @@ -328,9 +387,72 @@ fn extract_key_ids_and_jwk(document: &Document) -> Result, Ok(my_map) } +// todo: trait? +fn save_to_file(path: &PathBuf, data: &str) -> Result<(), TrustchainCRError> { + // Open the new file + let file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path); + + // Write key to file + + if let Ok(mut file) = file { + let mut writer = BufWriter::new(file); + match writer.write_all(data.as_bytes()) { + Ok(_) => Ok(()), + Err(_) => Err(TrustchainCRError::FailedToSave), + } + } else { + Err(TrustchainCRError::FailedToSave) + } +} +// fn save_keys( +// &self, +// did_suffix: &str, +// key_type: KeyType, +// keys: &OneOrMany, +// overwrite: bool, +// ) -> Result<(), KeyManagerError> { +// // Get directory and path +// let directory = &self.get_path(did_suffix, &key_type, true)?; +// let path = &self.get_path(did_suffix, &key_type, false)?; + +// // Stop if keys already exist and overwrite is false. +// if self.keys_exist(did_suffix, &key_type) && !overwrite { +// return Err(KeyManagerError::FailedToSaveKey); +// } + +// // Make directory if non-existent +// match std::fs::create_dir_all(directory) { +// Ok(_) => (), +// Err(_) => return Err(KeyManagerError::FailedToCreateDir), +// }; + +// // Open the new file +// let file = OpenOptions::new() +// .create(true) +// .write(true) +// .truncate(true) +// .open(path); + +// // Write key to file +// if let Ok(mut file) = file { +// match writeln!(file, "{}", &to_json(keys).unwrap()) { +// Ok(_) => Ok(()), +// Err(_) => Err(KeyManagerError::FailedToSaveKey), +// } +// } else { +// Err(KeyManagerError::FailedToSaveKey) +// } +// } + #[cfg(test)] mod tests { + use std::{any::type_name, env}; + use crate::data::{ TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS, TEST_SIGNING_KEY_1, TEST_SIGNING_KEY_2, TEST_TEMP_KEY, TEST_UPDATE_KEY, TEST_UPSTREAM_KEY, @@ -552,4 +674,43 @@ mod tests { println!("Verified response map: {:?}", verified_response_map); assert_eq!(verified_response_map, nonces); } + #[test] + fn test_write_structs_to_file() { + let temp_s_key: Jwk = serde_json::from_str(TEST_TEMP_KEY).unwrap(); + let initiation = CRInitiation { + temp_p_key: temp_s_key.to_public_key().unwrap(), + requester_details: RequesterDetails { + requester_org: String::from("My Org"), + operator_name: String::from("John Doe"), + }, + }; + let initiation_json = to_json(&initiation).unwrap(); + + // write to file + let var_type: &str = type_name::<&CRInitiation>().split("::").last().unwrap(); + println!("Type name: {}", var_type); + let directory_path = env::current_dir().unwrap(); + let file_path = directory_path.join("test_initiation.json"); + save_to_file(&file_path, initiation_json.as_str()); + + // identity challenge + let identity_challenge = CRIdentityChallenge { + update_p_key: serde_json::from_str(TEST_UPDATE_KEY).unwrap(), + identity_nonce: Some(Nonce::new()), + identity_challenge_signature: Some(String::from("some challenge signature string")), + // identity_response_signature: Some(String::from("some response signature string")), + identity_response_signature: None, + }; + + identity_challenge.elementwise_serialize(&directory_path); + + // let identity_challenge_json = to_json(&identity_challenge).unwrap(); + // let file_path = directory_path.join("test_identity_challenge.json"); + // save_to_file(&file_path, identity_challenge_json.as_str()); + + // iterate over fields of struct + } } + +// Todo: +// - [ ] files should be read only From 683fb0fe22f5ccdb94e0f1ccceaf0f6b68a50bae Mon Sep 17 00:00:00 2001 From: pwochner Date: Tue, 5 Sep 2023 11:10:19 +0100 Subject: [PATCH 15/86] Make json read-only. Ignore None fields. --- trustchain-http/src/challenge_response.rs | 126 +++++++++------------- 1 file changed, 52 insertions(+), 74 deletions(-) diff --git a/trustchain-http/src/challenge_response.rs b/trustchain-http/src/challenge_response.rs index 769aa3cb..a72c1d46 100644 --- a/trustchain-http/src/challenge_response.rs +++ b/trustchain-http/src/challenge_response.rs @@ -10,14 +10,12 @@ use serde_json::{to_string_pretty as to_json, Value}; use serde_with::skip_serializing_none; use ssi::did::{Document, VerificationMethod}; use ssi::jwk::JWK; -use std::any::type_name; use std::collections::HashMap; -use std::env; -use std::fs::{File, OpenOptions}; +use std::fs; +use std::fs::OpenOptions; use std::io::{BufWriter, Write}; use std::path::PathBuf; -use struct_iterable::Iterable; use thiserror::Error; #[derive(Error, Debug)] @@ -46,6 +44,9 @@ pub enum TrustchainCRError { /// Failed to save to file. #[error("Failed to save to file.")] FailedToSave, + /// Failed to set permissions on file. + #[error("Failed to set permissions on file.")] + FailedToSetPermissions, } impl From for TrustchainCRError { @@ -105,21 +106,31 @@ trait ElementwiseSerializeDeserialize { fn save_to_file(&self, path: &PathBuf, data: &str) -> Result<(), TrustchainCRError> { // Open the new file - let file = OpenOptions::new() + let new_file = OpenOptions::new() .create(true) .write(true) .truncate(true) .open(path); // Write key to file - if let Ok(file) = file { - let mut writer = BufWriter::new(file); - match writer.write_all(data.as_bytes()) { - Ok(_) => Ok(()), - Err(_) => Err(TrustchainCRError::FailedToSave), + match new_file { + Ok(file) => { + let mut writer = BufWriter::new(file); + match writer.write_all(data.as_bytes()) { + Ok(_) => { + // Set file permissions to read-only (user, group, and others) + let mut permissions = fs::metadata(path) + .map_err(|_| TrustchainCRError::FailedToSetPermissions)? + .permissions(); + permissions.set_readonly(true); + fs::set_permissions(path, permissions) + .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; + Ok(()) + } + Err(_) => Err(TrustchainCRError::FailedToSave), + } } - } else { - Err(TrustchainCRError::FailedToSave) + Err(_) => Err(TrustchainCRError::FailedToSave), } } } @@ -139,7 +150,7 @@ struct CRInitiation { } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Iterable)] +#[derive(Debug, Serialize, Deserialize, Clone)] struct CRIdentityChallenge { update_p_key: Option, identity_nonce: Option, // make own Nonce type @@ -149,19 +160,35 @@ struct CRIdentityChallenge { // todo: add path to serialise/deserialise functions? impl ElementwiseSerializeDeserialize for CRIdentityChallenge { fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { - let file_path = path.join("update_p_key.json"); - let data: &str = &to_json(&self.update_p_key).unwrap(); - if !file_path.exists() { - save_to_file(&file_path, data); + if let Some(update_p_key) = &self.update_p_key { + let file_path = path.join("update_p_key.json"); + let data: &str = &to_json(update_p_key).unwrap(); + if !file_path.exists() { + self.save_to_file(&file_path, data); + } + } + if let Some(identity_nonce) = &self.identity_nonce { + let file_path = path.join("identity_nonce.json"); + let data: &str = &to_json(identity_nonce).unwrap(); + if !file_path.exists() { + self.save_to_file(&file_path, data); + } + } + if let Some(identity_challenge_signature) = &self.identity_challenge_signature { + let file_path = path.join("identity_challenge_signature.json"); + let data: &str = &to_json(identity_challenge_signature).unwrap(); + if !file_path.exists() { + self.save_to_file(&file_path, data); + } } - let file_path = path.join("identity_nonce.json"); - let data: &str = &to_json(&self.identity_nonce).unwrap(); - if !file_path.exists() { - save_to_file(&file_path, data); + if let Some(identity_response_signature) = &self.identity_response_signature { + let file_path = path.join("identity_response_signature.json"); + let data: &str = &to_json(identity_response_signature).unwrap(); + if !file_path.exists() { + self.save_to_file(&file_path, data); + } } Ok(()) - // todo: handle case where file already exists - // todo: handle case where field is None } fn elementwise_deserialize(&self) -> Result<(), TrustchainCRError> { Ok(()) @@ -328,16 +355,6 @@ impl SignEncrypt for Entity {} impl DecryptVerify for Entity {} -// /// Generates a random alphanumeric nonce of a specified length using a seeded random number generator. -// fn generate_nonce() -> String { -// // let rng: StdRng = SeedableRng::seed_from_u64(seed); -// thread_rng() -// .sample_iter(&Alphanumeric) -// .take(32) -// .map(char::from) -// .collect() -// } - // make a try_from instead fn josekit_to_ssi_jwk(key: &Jwk) -> Result { let key_as_str: &str = &serde_json::to_string(&key).unwrap(); @@ -408,45 +425,6 @@ fn save_to_file(path: &PathBuf, data: &str) -> Result<(), TrustchainCRError> { Err(TrustchainCRError::FailedToSave) } } -// fn save_keys( -// &self, -// did_suffix: &str, -// key_type: KeyType, -// keys: &OneOrMany, -// overwrite: bool, -// ) -> Result<(), KeyManagerError> { -// // Get directory and path -// let directory = &self.get_path(did_suffix, &key_type, true)?; -// let path = &self.get_path(did_suffix, &key_type, false)?; - -// // Stop if keys already exist and overwrite is false. -// if self.keys_exist(did_suffix, &key_type) && !overwrite { -// return Err(KeyManagerError::FailedToSaveKey); -// } - -// // Make directory if non-existent -// match std::fs::create_dir_all(directory) { -// Ok(_) => (), -// Err(_) => return Err(KeyManagerError::FailedToCreateDir), -// }; - -// // Open the new file -// let file = OpenOptions::new() -// .create(true) -// .write(true) -// .truncate(true) -// .open(path); - -// // Write key to file -// if let Ok(mut file) = file { -// match writeln!(file, "{}", &to_json(keys).unwrap()) { -// Ok(_) => Ok(()), -// Err(_) => Err(KeyManagerError::FailedToSaveKey), -// } -// } else { -// Err(KeyManagerError::FailedToSaveKey) -// } -// } #[cfg(test)] mod tests { @@ -712,5 +690,5 @@ mod tests { } } -// Todo: -// - [ ] files should be read only +// todo: +// - delete save_to_file function From 4654c2ef66a9e693b186e880a57903cdcd385123 Mon Sep 17 00:00:00 2001 From: pwochner Date: Thu, 7 Sep 2023 17:50:52 +0100 Subject: [PATCH 16/86] Implement elementwise deserialisation for CR. --- trustchain-http/src/challenge_response.rs | 642 +++++++++++++++------- 1 file changed, 435 insertions(+), 207 deletions(-) diff --git a/trustchain-http/src/challenge_response.rs b/trustchain-http/src/challenge_response.rs index a72c1d46..250602ce 100644 --- a/trustchain-http/src/challenge_response.rs +++ b/trustchain-http/src/challenge_response.rs @@ -11,8 +11,8 @@ use serde_with::skip_serializing_none; use ssi::did::{Document, VerificationMethod}; use ssi::jwk::JWK; use std::collections::HashMap; -use std::fs; use std::fs::OpenOptions; +use std::fs::{self, File}; use std::io::{BufWriter, Write}; use std::path::PathBuf; @@ -47,6 +47,9 @@ pub enum TrustchainCRError { /// Failed to set permissions on file. #[error("Failed to set permissions on file.")] FailedToSetPermissions, + /// Failed deserialize from file. + #[error("Failed to deserialize.")] + FailedToDeserialize, } impl From for TrustchainCRError { @@ -55,6 +58,110 @@ impl From for TrustchainCRError { } } +/// Interface for serializing and deserializing each field of structs to/from files. +trait ElementwiseSerializeDeserialize { + fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError>; + // todo: default implementation, look if exists already + fn elementwise_deserialize(self, path: &PathBuf) -> Result + where + Self: Sized; + + fn save_to_file(&self, path: &PathBuf, data: &str) -> Result<(), TrustchainCRError> { + // Open the new file + let new_file = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(path); + + // Write key to file + match new_file { + Ok(file) => { + let mut writer = BufWriter::new(file); + match writer.write_all(data.as_bytes()) { + Ok(_) => { + // Set file permissions to read-only (user, group, and others) + let mut permissions = fs::metadata(path) + .map_err(|_| TrustchainCRError::FailedToSetPermissions)? + .permissions(); + permissions.set_readonly(true); + fs::set_permissions(path, permissions) + .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; + Ok(()) + } + Err(_) => Err(TrustchainCRError::FailedToSave), + } + } + Err(_) => Err(TrustchainCRError::FailedToSave), + } + } +} + +/// Interface for signing and then encrypting data. +pub trait SignEncrypt { + fn sign(&self, payload: &JwtPayload, secret_key: &Jwk) -> Result { + let mut header = JwsHeader::new(); + header.set_token_type("JWT"); + let signer = ES256K.signer_from_jwk(&secret_key)?; + let signed_jwt = jwt::encode_with_signer(payload, &header, &signer)?; + Ok(signed_jwt) + } + /// `JWTPayload` is a wrapped [`Map`](https://docs.rs/serde_json/1.0.79/serde_json/struct.Map.html) + /// of claims. + fn encrypt(&self, payload: &JwtPayload, public_key: &Jwk) -> Result { + let mut header = JweHeader::new(); + header.set_token_type("JWT"); + header.set_content_encryption("A128CBC-HS256"); + header.set_content_encryption("A256GCM"); + + let encrypter = ECDH_ES.encrypter_from_jwk(&public_key)?; + let encrypted_jwt = jwt::encode_with_encrypter(payload, &header, &encrypter)?; + Ok(encrypted_jwt) + } + /// Combined sign and encryption + fn sign_and_encrypt_claim( + &self, + payload: &JwtPayload, + secret_key: &Jwk, + public_key: &Jwk, + ) -> Result { + let signed_encoded_payload = self.sign(payload, secret_key)?; + let mut claims = JwtPayload::new(); + claims.set_claim("claim", Some(Value::from(signed_encoded_payload)))?; + self.encrypt(&claims, &public_key) + } +} +/// Interface for decrypting and then verifying data. +trait DecryptVerify { + fn decrypt(&self, value: &Value, secret_key: &Jwk) -> Result { + let decrypter = ECDH_ES.decrypter_from_jwk(&secret_key)?; + let (payload, _) = jwt::decode_with_decrypter(value.as_str().unwrap(), &decrypter)?; + Ok(payload) + } + fn decrypt_and_verify( + &self, + input: String, + secret_key: &Jwk, + public_key: &Jwk, + ) -> Result { + let decrypter = ECDH_ES.decrypter_from_jwk(secret_key)?; + let (payload, _) = jwt::decode_with_decrypter(input, &decrypter)?; + + let verifier = ES256K.verifier_from_jwk(public_key)?; + let (payload, _) = jwt::decode_with_verifier( + &payload.claim("claim").unwrap().as_str().unwrap(), + &verifier, + )?; + Ok(payload) + } +} + +struct Entity {} + +impl SignEncrypt for Entity {} + +impl DecryptVerify for Entity {} + // pub struct Nonce([u8; N]); #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Nonce(String); @@ -95,43 +202,39 @@ impl From for Nonce { struct CRState { initiation: Option, identity_challenge_response: Option, + content_challenge_response: Option, } -struct Entity {} - -trait ElementwiseSerializeDeserialize { - fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError>; - // todo: default implementation, look if exists already - fn elementwise_deserialize(&self) -> Result<(), TrustchainCRError>; - - fn save_to_file(&self, path: &PathBuf, data: &str) -> Result<(), TrustchainCRError> { - // Open the new file - let new_file = OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(path); +impl CRState { + fn new() -> Self { + Self { + initiation: None, + identity_challenge_response: None, + content_challenge_response: None, + } + } +} - // Write key to file - match new_file { - Ok(file) => { - let mut writer = BufWriter::new(file); - match writer.write_all(data.as_bytes()) { - Ok(_) => { - // Set file permissions to read-only (user, group, and others) - let mut permissions = fs::metadata(path) - .map_err(|_| TrustchainCRError::FailedToSetPermissions)? - .permissions(); - permissions.set_readonly(true); - fs::set_permissions(path, permissions) - .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; - Ok(()) - } - Err(_) => Err(TrustchainCRError::FailedToSave), - } - } - Err(_) => Err(TrustchainCRError::FailedToSave), +impl ElementwiseSerializeDeserialize for CRState { + fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { + if let Some(initiation) = &self.initiation { + initiation.elementwise_serialize(path)?; + } + if let Some(identity_challenge_response) = &self.identity_challenge_response { + identity_challenge_response.elementwise_serialize(path)?; + } + if let Some(content_challenge_response) = &self.content_challenge_response { + content_challenge_response.elementwise_serialize(path)?; } + Ok(()) + } + fn elementwise_deserialize(mut self, path: &PathBuf) -> Result { + self.initiation = Some(CRInitiation::new().elementwise_deserialize(path)?); + self.identity_challenge_response = + Some(CRIdentityChallenge::new().elementwise_deserialize(path)?); + self.content_challenge_response = + Some(CRContentChallenge::new().elementwise_deserialize(path)?); + Ok(self) } } @@ -145,8 +248,65 @@ struct RequesterDetails { #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize)] struct CRInitiation { - temp_p_key: Jwk, - requester_details: RequesterDetails, + temp_p_key: Option, + requester_details: Option, +} + +impl CRInitiation { + fn new() -> Self { + Self { + temp_p_key: None, + requester_details: None, + } + } +} + +impl ElementwiseSerializeDeserialize for CRInitiation { + fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { + let file_path = path.join("temp_p_key.json"); + let data: &str = &to_json(&self.temp_p_key).unwrap(); + if !file_path.exists() { + self.save_to_file(&file_path, data); + } + + let file_path = path.join("requester_details.json"); + let data: &str = &to_json(&self.requester_details).unwrap(); + if !file_path.exists() { + self.save_to_file(&file_path, data); + } + + Ok(()) + } + fn elementwise_deserialize( + mut self, + path: &PathBuf, + ) -> Result { + // temporary public key + let full_path = path.join("temp_p_key.json"); + if !full_path.exists() { + return Err(TrustchainCRError::FailedToDeserialize); + } + let file = File::open(full_path).map_err(|_| TrustchainCRError::FailedToDeserialize)?; + let reader = std::io::BufReader::new(file); + self.temp_p_key = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; + // requester details + let full_path = path.join("requester_details.json"); + if !full_path.exists() { + return Err(TrustchainCRError::FailedToDeserialize); + } + let file = File::open(full_path).map_err(|_| TrustchainCRError::FailedToDeserialize)?; + let reader = std::io::BufReader::new(file); + self.requester_details = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; + + // let initiation = CRInitiation { + // temp_p_key: Some(temp_p_key), + // requester_details: Some(requester_details), + // }; + + Ok(self) + } } #[skip_serializing_none] @@ -157,6 +317,18 @@ struct CRIdentityChallenge { identity_challenge_signature: Option, identity_response_signature: Option, } + +impl CRIdentityChallenge { + fn new() -> Self { + Self { + update_p_key: None, + identity_nonce: None, + identity_challenge_signature: None, + identity_response_signature: None, + } + } +} + // todo: add path to serialise/deserialise functions? impl ElementwiseSerializeDeserialize for CRIdentityChallenge { fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { @@ -190,34 +362,49 @@ impl ElementwiseSerializeDeserialize for CRIdentityChallenge { } Ok(()) } - fn elementwise_deserialize(&self) -> Result<(), TrustchainCRError> { - Ok(()) - } -} - -struct CRContentChallenge { - content_nonce: Option>, - content_challenge_signature: Option, - content_response_signature: Option, -} + fn elementwise_deserialize( + mut self, + path: &PathBuf, + ) -> Result { + // update public key + let full_path = path.join("update_p_key.json"); + if !full_path.exists() { + return Err(TrustchainCRError::FailedToDeserialize); + } + let file = File::open(full_path).map_err(|_| TrustchainCRError::FailedToDeserialize)?; + let reader = std::io::BufReader::new(file); + self.update_p_key = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; + // identity nonce + let full_path = path.join("identity_nonce.json"); + if !full_path.exists() { + return Err(TrustchainCRError::FailedToDeserialize); + } + let file = File::open(full_path).map_err(|_| TrustchainCRError::FailedToDeserialize)?; + let reader = std::io::BufReader::new(file); + self.identity_nonce = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; + // identity challenge signature + let full_path = path.join("identity_challenge_signature.json"); + if !full_path.exists() { + return Err(TrustchainCRError::FailedToDeserialize); + } + let file = File::open(full_path).map_err(|_| TrustchainCRError::FailedToDeserialize)?; + let reader = std::io::BufReader::new(file); + self.identity_challenge_signature = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; + + // identity response signature + let full_path = path.join("identity_response_signature.json"); + if !full_path.exists() { + return Err(TrustchainCRError::FailedToDeserialize); + } + let file = File::open(full_path).map_err(|_| TrustchainCRError::FailedToDeserialize)?; + let reader = std::io::BufReader::new(file); + self.identity_response_signature = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; -impl TryFrom<&CRIdentityChallenge> for JwtPayload { - type Error = TrustchainCRError; - fn try_from(value: &CRIdentityChallenge) -> Result { - let mut payload = JwtPayload::new(); - payload.set_claim( - "identity_nonce", - Some(Value::from( - value.identity_nonce.as_ref().unwrap().to_string(), - )), - )?; - payload.set_claim( - "update_p_key", - Some(Value::from( - value.update_p_key.as_ref().unwrap().to_string(), - )), - )?; - Ok(payload) + Ok(self) } } @@ -245,115 +432,112 @@ impl TryFrom<&JwtPayload> for CRIdentityChallenge { } } -impl TryFrom<&Nonce> for JwtPayload { - type Error = TrustchainCRError; - fn try_from(value: &Nonce) -> Result { - let mut payload = JwtPayload::new(); - payload.set_claim("nonce", Some(Value::from(value.to_string())))?; - Ok(payload) - } +#[derive(Debug, Serialize, Deserialize)] +struct CRContentChallenge { + content_nonce: Option>, + content_challenge_signature: Option, + content_response_signature: Option, } -// impl TryFrom<(&JwtPayload, Vec<&str>)> for CRIdentityChallenge { -// type Error = TrustchainCRError; -// fn try_from((value, claims): (&JwtPayload, Vec<&str>)) -> Result { -// let mut challenge = CRIdentityChallenge { -// update_p_key: None, -// identity_nonce: None, -// identity_challenge_signature: None, -// identity_response_signature: None, -// }; - -// for claim in claims { -// match claim { -// "update_p_key" => { -// challenge.update_p_key = Some( -// serde_json::from_str( -// value.claim("update_p_key").unwrap().as_str().unwrap(), -// ) -// .unwrap(), -// ); -// } -// "identity_nonce" => { -// challenge.identity_nonce = Some( -// value -// .claim("identity_nonce") -// .unwrap() -// .as_str() -// .unwrap() -// .to_string(), -// ); -// } -// _ => {} -// } -// } - -// Ok(challenge) -// } -// } - -/// Interface for signing and then encrypting data. -pub trait SignEncrypt { - fn sign(&self, payload: &JwtPayload, secret_key: &Jwk) -> Result { - let mut header = JwsHeader::new(); - header.set_token_type("JWT"); - let signer = ES256K.signer_from_jwk(&secret_key)?; - let signed_jwt = jwt::encode_with_signer(payload, &header, &signer)?; - Ok(signed_jwt) +impl CRContentChallenge { + fn new() -> Self { + Self { + content_nonce: None, + content_challenge_signature: None, + content_response_signature: None, + } } - /// `JWTPayload` is a wrapped [`Map`](https://docs.rs/serde_json/1.0.79/serde_json/struct.Map.html) - /// of claims. - fn encrypt(&self, payload: &JwtPayload, public_key: &Jwk) -> Result { - let mut header = JweHeader::new(); - header.set_token_type("JWT"); - header.set_content_encryption("A128CBC-HS256"); - header.set_content_encryption("A256GCM"); +} - let encrypter = ECDH_ES.encrypter_from_jwk(&public_key)?; - let encrypted_jwt = jwt::encode_with_encrypter(payload, &header, &encrypter)?; - Ok(encrypted_jwt) +impl ElementwiseSerializeDeserialize for CRContentChallenge { + fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { + if let Some(content_nonce) = &self.content_nonce { + let file_path = path.join("content_nonce.json"); + let data: &str = &to_json(content_nonce).unwrap(); + if !file_path.exists() { + self.save_to_file(&file_path, data); + } + } + if let Some(content_challenge_signature) = &self.content_challenge_signature { + let file_path = path.join("content_challenge_signature.json"); + let data: &str = &to_json(content_challenge_signature).unwrap(); + if !file_path.exists() { + self.save_to_file(&file_path, data); + } + } + if let Some(content_response_signature) = &self.content_response_signature { + let file_path = path.join("content_response_signature.json"); + let data: &str = &to_json(content_response_signature).unwrap(); + if !file_path.exists() { + self.save_to_file(&file_path, data); + } + } + Ok(()) } - /// Combined sign and encryption - fn sign_and_encrypt_claim( - &self, - payload: &JwtPayload, - secret_key: &Jwk, - public_key: &Jwk, - ) -> Result { - let signed_encoded_payload = self.sign(payload, secret_key)?; - let mut claims = JwtPayload::new(); - claims.set_claim("claim", Some(Value::from(signed_encoded_payload)))?; - self.encrypt(&claims, &public_key) + fn elementwise_deserialize( + mut self, + path: &PathBuf, + ) -> Result { + // content nonce(s) + let full_path = path.join("content_nonce.json"); + if !full_path.exists() { + return Err(TrustchainCRError::FailedToDeserialize); + } + let file = File::open(full_path).map_err(|_| TrustchainCRError::FailedToDeserialize)?; + let reader = std::io::BufReader::new(file); + self.content_nonce = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; + // content challenge signature + let full_path = path.join("content_challenge_signature.json"); + if !full_path.exists() { + return Err(TrustchainCRError::FailedToDeserialize); + } + let file = File::open(full_path).map_err(|_| TrustchainCRError::FailedToDeserialize)?; + let reader = std::io::BufReader::new(file); + self.content_challenge_signature = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; + // content response signature + let full_path = path.join("content_response_signature.json"); + if !full_path.exists() { + return Err(TrustchainCRError::FailedToDeserialize); + } + let file = File::open(full_path).map_err(|_| TrustchainCRError::FailedToDeserialize)?; + let reader = std::io::BufReader::new(file); + self.content_response_signature = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; + + Ok(self) } } -/// Interface for decrypting and then verifying data. -trait DecryptVerify { - fn decrypt(&self, value: &Value, secret_key: &Jwk) -> Result { - let decrypter = ECDH_ES.decrypter_from_jwk(&secret_key)?; - let (payload, _) = jwt::decode_with_decrypter(value.as_str().unwrap(), &decrypter)?; - Ok(payload) - } - fn decrypt_and_verify( - &self, - input: String, - secret_key: &Jwk, - public_key: &Jwk, - ) -> Result { - let decrypter = ECDH_ES.decrypter_from_jwk(secret_key)?; - let (payload, _) = jwt::decode_with_decrypter(input, &decrypter)?; - let verifier = ES256K.verifier_from_jwk(public_key)?; - let (payload, _) = jwt::decode_with_verifier( - &payload.claim("claim").unwrap().as_str().unwrap(), - &verifier, +impl TryFrom<&CRIdentityChallenge> for JwtPayload { + type Error = TrustchainCRError; + fn try_from(value: &CRIdentityChallenge) -> Result { + let mut payload = JwtPayload::new(); + payload.set_claim( + "identity_nonce", + Some(Value::from( + value.identity_nonce.as_ref().unwrap().to_string(), + )), + )?; + payload.set_claim( + "update_p_key", + Some(Value::from( + value.update_p_key.as_ref().unwrap().to_string(), + )), )?; Ok(payload) } } -impl SignEncrypt for Entity {} - -impl DecryptVerify for Entity {} +impl TryFrom<&Nonce> for JwtPayload { + type Error = TrustchainCRError; + fn try_from(value: &Nonce) -> Result { + let mut payload = JwtPayload::new(); + payload.set_claim("nonce", Some(Value::from(value.to_string())))?; + Ok(payload) + } +} // make a try_from instead fn josekit_to_ssi_jwk(key: &Jwk) -> Result { @@ -404,32 +588,12 @@ fn extract_key_ids_and_jwk(document: &Document) -> Result, Ok(my_map) } -// todo: trait? -fn save_to_file(path: &PathBuf, data: &str) -> Result<(), TrustchainCRError> { - // Open the new file - let file = OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(path); - - // Write key to file - - if let Ok(mut file) = file { - let mut writer = BufWriter::new(file); - match writer.write_all(data.as_bytes()) { - Ok(_) => Ok(()), - Err(_) => Err(TrustchainCRError::FailedToSave), - } - } else { - Err(TrustchainCRError::FailedToSave) - } -} - #[cfg(test)] mod tests { - use std::{any::type_name, env}; + use std::env; + + use josekit::jwe::alg::direct::DirectJweAlgorithm; use crate::data::{ TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS, TEST_SIGNING_KEY_1, TEST_SIGNING_KEY_2, @@ -448,11 +612,11 @@ mod tests { // generate challenge let request_initiation = CRInitiation { - temp_p_key: temp_p_key.clone(), - requester_details: RequesterDetails { + temp_p_key: Some(temp_p_key.clone()), + requester_details: Some(RequesterDetails { requester_org: String::from("My Org"), operator_name: String::from("John Doe"), - }, + }), }; let mut upstream_identity_challenge_response = CRIdentityChallenge { @@ -467,7 +631,11 @@ mod tests { let payload = JwtPayload::try_from(&upstream_identity_challenge_response).unwrap(); let signed_encrypted_challenge = upstream_entity - .sign_and_encrypt_claim(&payload, &upstream_s_key, &request_initiation.temp_p_key) + .sign_and_encrypt_claim( + &payload, + &upstream_s_key, + &request_initiation.temp_p_key.unwrap(), + ) .unwrap(); upstream_identity_challenge_response.identity_challenge_signature = @@ -654,41 +822,101 @@ mod tests { } #[test] fn test_write_structs_to_file() { + // ==========| Identity CR | ============== let temp_s_key: Jwk = serde_json::from_str(TEST_TEMP_KEY).unwrap(); let initiation = CRInitiation { - temp_p_key: temp_s_key.to_public_key().unwrap(), - requester_details: RequesterDetails { + temp_p_key: Some(temp_s_key.to_public_key().unwrap()), + requester_details: Some(RequesterDetails { requester_org: String::from("My Org"), operator_name: String::from("John Doe"), - }, + }), }; - let initiation_json = to_json(&initiation).unwrap(); - - // write to file - let var_type: &str = type_name::<&CRInitiation>().split("::").last().unwrap(); - println!("Type name: {}", var_type); - let directory_path = env::current_dir().unwrap(); - let file_path = directory_path.join("test_initiation.json"); - save_to_file(&file_path, initiation_json.as_str()); // identity challenge let identity_challenge = CRIdentityChallenge { update_p_key: serde_json::from_str(TEST_UPDATE_KEY).unwrap(), identity_nonce: Some(Nonce::new()), identity_challenge_signature: Some(String::from("some challenge signature string")), - // identity_response_signature: Some(String::from("some response signature string")), - identity_response_signature: None, + identity_response_signature: Some(String::from("some response signature string")), + // identity_response_signature: None, }; - identity_challenge.elementwise_serialize(&directory_path); + // ==========| Content CR | ============== + // get signing keys for DE from did document + let doc: Document = serde_json::from_str(TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS).unwrap(); + let test_keys_map = extract_key_ids_and_jwk(&doc).unwrap(); - // let identity_challenge_json = to_json(&identity_challenge).unwrap(); - // let file_path = directory_path.join("test_identity_challenge.json"); - // save_to_file(&file_path, identity_challenge_json.as_str()); + // generate map with unencrypted nonces so UE can store them for later verification + let nonces: HashMap = + test_keys_map + .iter() + .fold(HashMap::new(), |mut acc, (key_id, _)| { + acc.insert(String::from(key_id), Nonce::new()); + acc + }); + let content_challenge_response = CRContentChallenge { + content_nonce: Some(nonces), + content_challenge_signature: Some(String::from( + "some content challenge signature string", + )), + content_response_signature: Some(String::from( + "some content response signature string", + )), + }; - // iterate over fields of struct + // ==========| CR state | ============== + let cr_state = CRState { + initiation: Some(initiation), + identity_challenge_response: Some(identity_challenge), + content_challenge_response: Some(content_challenge_response), + }; + // write to file + let directory_path = env::current_dir().unwrap(); + cr_state.elementwise_serialize(&directory_path).unwrap(); + } + + #[test] + fn test_deserialize_initiation() { + let directory_path = env::current_dir().unwrap(); + let initiation = CRInitiation::new() + .elementwise_deserialize(&directory_path) + .unwrap(); + println!("Initiation deserialized from files: {:?}", initiation); + } + + #[test] + fn test_deserialize_identity_challenge() { + let directory_path = env::current_dir().unwrap(); + let identity_challenge = CRIdentityChallenge::new() + .elementwise_deserialize(&directory_path) + .unwrap(); + println!( + "Identity challenge deserialized from files: {:?}", + identity_challenge + ); + } + + #[test] + fn test_deserialize_content_challenge() { + let directory_path = env::current_dir().unwrap(); + let content_challenge = CRContentChallenge::new() + .elementwise_deserialize(&directory_path) + .unwrap(); + println!( + "Content challenge deserialized from files: {:?}", + content_challenge + ); } -} -// todo: -// - delete save_to_file function + #[test] + fn test_deserialize_challenge_state() { + let directory_path = env::current_dir().unwrap(); + let challenge_state = CRState::new() + .elementwise_deserialize(&directory_path) + .unwrap(); + println!( + "Challenge state deserialized from files: {:?}", + challenge_state + ); + } +} From 6af4e4ac1884467fbecb3b97b5e51564a3cad35f Mon Sep 17 00:00:00 2001 From: pwochner Date: Mon, 18 Sep 2023 13:41:25 +0100 Subject: [PATCH 17/86] Improve elementwise serialise. --- trustchain-http/src/challenge_response.rs | 209 ++++++++++++++-------- 1 file changed, 136 insertions(+), 73 deletions(-) diff --git a/trustchain-http/src/challenge_response.rs b/trustchain-http/src/challenge_response.rs index 250602ce..841943db 100644 --- a/trustchain-http/src/challenge_response.rs +++ b/trustchain-http/src/challenge_response.rs @@ -14,6 +14,7 @@ use std::collections::HashMap; use std::fs::OpenOptions; use std::fs::{self, File}; use std::io::{BufWriter, Write}; +use tracing::field; use std::path::PathBuf; use thiserror::Error; @@ -50,6 +51,9 @@ pub enum TrustchainCRError { /// Failed deserialize from file. #[error("Failed to deserialize.")] FailedToDeserialize, + /// Failed to check CR status. + #[error("Failed to determine CR status.")] + FailedStatusCheck, } impl From for TrustchainCRError { @@ -59,9 +63,26 @@ impl From for TrustchainCRError { } /// Interface for serializing and deserializing each field of structs to/from files. -trait ElementwiseSerializeDeserialize { - fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError>; - // todo: default implementation, look if exists already +trait ElementwiseSerializeDeserialize +where + Self: Serialize, +{ + fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { + let serialized = + serde_json::to_value(&self).map_err(|_| TrustchainCRError::FailedToSave)?; + if let Value::Object(fields) = serialized { + for (field_name, field_value) in fields { + if !field_value.is_null() { + let json_filename = format!("{}.json", field_name); + let file_path = path.join(json_filename); + + self.save_to_file(&file_path, &to_json(&field_value).unwrap())?; + } + } + } + Ok(()) + } + fn elementwise_deserialize(self, path: &PathBuf) -> Result where Self: Sized; @@ -97,6 +118,10 @@ trait ElementwiseSerializeDeserialize { } } +pub trait IsComplete { + fn is_complete(&self) -> bool; +} + /// Interface for signing and then encrypting data. pub trait SignEncrypt { fn sign(&self, payload: &JwtPayload, secret_key: &Jwk) -> Result { @@ -262,21 +287,35 @@ impl CRInitiation { } impl ElementwiseSerializeDeserialize for CRInitiation { - fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { - let file_path = path.join("temp_p_key.json"); - let data: &str = &to_json(&self.temp_p_key).unwrap(); - if !file_path.exists() { - self.save_to_file(&file_path, data); - } - - let file_path = path.join("requester_details.json"); - let data: &str = &to_json(&self.requester_details).unwrap(); - if !file_path.exists() { - self.save_to_file(&file_path, data); - } - - Ok(()) - } + // fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { + // // let file_path = path.join("temp_p_key.json"); + // // let data: &str = &to_json(&self.temp_p_key).unwrap(); + // // if !file_path.exists() { + // // self.save_to_file(&file_path, data); + // // } + + // // let file_path = path.join("requester_details.json"); + // // let data: &str = &to_json(&self.requester_details).unwrap(); + // // if !file_path.exists() { + // // self.save_to_file(&file_path, data); + // // } + + // // =======| new version |=========== + // let serialized = serde_json::to_value(&self).expect("Serialization failed"); + + // if let Value::Object(fields) = serialized { + // for (field_name, field_value) in fields { + // if !field_value.is_null() { + // let json_filename = format!("{}.json", field_name); + // let file_path = path.join(json_filename); + + // self.save_to_file(&file_path, &to_json(&field_value).unwrap()); + // } + // } + // } + + // Ok(()) + // } fn elementwise_deserialize( mut self, path: &PathBuf, @@ -331,37 +370,37 @@ impl CRIdentityChallenge { // todo: add path to serialise/deserialise functions? impl ElementwiseSerializeDeserialize for CRIdentityChallenge { - fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { - if let Some(update_p_key) = &self.update_p_key { - let file_path = path.join("update_p_key.json"); - let data: &str = &to_json(update_p_key).unwrap(); - if !file_path.exists() { - self.save_to_file(&file_path, data); - } - } - if let Some(identity_nonce) = &self.identity_nonce { - let file_path = path.join("identity_nonce.json"); - let data: &str = &to_json(identity_nonce).unwrap(); - if !file_path.exists() { - self.save_to_file(&file_path, data); - } - } - if let Some(identity_challenge_signature) = &self.identity_challenge_signature { - let file_path = path.join("identity_challenge_signature.json"); - let data: &str = &to_json(identity_challenge_signature).unwrap(); - if !file_path.exists() { - self.save_to_file(&file_path, data); - } - } - if let Some(identity_response_signature) = &self.identity_response_signature { - let file_path = path.join("identity_response_signature.json"); - let data: &str = &to_json(identity_response_signature).unwrap(); - if !file_path.exists() { - self.save_to_file(&file_path, data); - } - } - Ok(()) - } + // fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { + // if let Some(update_p_key) = &self.update_p_key { + // let file_path = path.join("update_p_key.json"); + // let data: &str = &to_json(update_p_key).unwrap(); + // if !file_path.exists() { + // self.save_to_file(&file_path, data); + // } + // } + // if let Some(identity_nonce) = &self.identity_nonce { + // let file_path = path.join("identity_nonce.json"); + // let data: &str = &to_json(identity_nonce).unwrap(); + // if !file_path.exists() { + // self.save_to_file(&file_path, data); + // } + // } + // if let Some(identity_challenge_signature) = &self.identity_challenge_signature { + // let file_path = path.join("identity_challenge_signature.json"); + // let data: &str = &to_json(identity_challenge_signature).unwrap(); + // if !file_path.exists() { + // self.save_to_file(&file_path, data); + // } + // } + // if let Some(identity_response_signature) = &self.identity_response_signature { + // let file_path = path.join("identity_response_signature.json"); + // let data: &str = &to_json(identity_response_signature).unwrap(); + // if !file_path.exists() { + // self.save_to_file(&file_path, data); + // } + // } + // Ok(()) + // } fn elementwise_deserialize( mut self, path: &PathBuf, @@ -450,30 +489,30 @@ impl CRContentChallenge { } impl ElementwiseSerializeDeserialize for CRContentChallenge { - fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { - if let Some(content_nonce) = &self.content_nonce { - let file_path = path.join("content_nonce.json"); - let data: &str = &to_json(content_nonce).unwrap(); - if !file_path.exists() { - self.save_to_file(&file_path, data); - } - } - if let Some(content_challenge_signature) = &self.content_challenge_signature { - let file_path = path.join("content_challenge_signature.json"); - let data: &str = &to_json(content_challenge_signature).unwrap(); - if !file_path.exists() { - self.save_to_file(&file_path, data); - } - } - if let Some(content_response_signature) = &self.content_response_signature { - let file_path = path.join("content_response_signature.json"); - let data: &str = &to_json(content_response_signature).unwrap(); - if !file_path.exists() { - self.save_to_file(&file_path, data); - } - } - Ok(()) - } + // fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { + // if let Some(content_nonce) = &self.content_nonce { + // let file_path = path.join("content_nonce.json"); + // let data: &str = &to_json(content_nonce).unwrap(); + // if !file_path.exists() { + // self.save_to_file(&file_path, data); + // } + // } + // if let Some(content_challenge_signature) = &self.content_challenge_signature { + // let file_path = path.join("content_challenge_signature.json"); + // let data: &str = &to_json(content_challenge_signature).unwrap(); + // if !file_path.exists() { + // self.save_to_file(&file_path, data); + // } + // } + // if let Some(content_response_signature) = &self.content_response_signature { + // let file_path = path.join("content_response_signature.json"); + // let data: &str = &to_json(content_response_signature).unwrap(); + // if !file_path.exists() { + // self.save_to_file(&file_path, data); + // } + // } + // Ok(()) + // } fn elementwise_deserialize( mut self, path: &PathBuf, @@ -588,6 +627,10 @@ fn extract_key_ids_and_jwk(document: &Document) -> Result, Ok(my_map) } +fn check_cr_state(state: CRState) -> Result<(), TrustchainCRError> { + todo!() +} + #[cfg(test)] mod tests { @@ -873,6 +916,22 @@ mod tests { // write to file let directory_path = env::current_dir().unwrap(); cr_state.elementwise_serialize(&directory_path).unwrap(); + + // test serialise CR state + // let serialized = serde_json::to_value(&cr_state.initiation).expect("Serialization failed"); + + // if let Value::Object(fields) = serialized { + // for (field_name, field_value) in fields { + // match field_value { + // Value::Null => println!("Field {} is empty.", field_name), + // // _ => println!("Field {} has a value: {:?}", field_name, field_value), + // _ => { + // let json_filename = format!("{}.json", field_name); + // write_to_json_file(&json_filename, &field_value); + // } + // } + // } + // } } #[test] @@ -920,3 +979,7 @@ mod tests { ); } } + +// notes: +// - new version of elementwise serialise: is there much benefit? +// - deserialise still still needs to be hardcoded From 73940174fc6d2bb82a3e8b583bcd8f54600334eb Mon Sep 17 00:00:00 2001 From: pwochner Date: Wed, 4 Oct 2023 11:00:22 +0200 Subject: [PATCH 18/86] Error handling of elementwise deserialisation for initiation: throw error if deserialize fails, but not if file doesn't exist. Improve tests. --- trustchain-http/Cargo.toml | 1 + trustchain-http/src/challenge_response.rs | 141 +++++++++++++--------- 2 files changed, 88 insertions(+), 54 deletions(-) diff --git a/trustchain-http/Cargo.toml b/trustchain-http/Cargo.toml index ee0c5d98..8f6229c1 100644 --- a/trustchain-http/Cargo.toml +++ b/trustchain-http/Cargo.toml @@ -47,6 +47,7 @@ hex = "0.4.3" rand = "0.8" josekit = "0.8" serde_with = "*" +tempfile = "*" [dev-dependencies] axum-test-helper = "0.2.0" diff --git a/trustchain-http/src/challenge_response.rs b/trustchain-http/src/challenge_response.rs index 841943db..708a60ed 100644 --- a/trustchain-http/src/challenge_response.rs +++ b/trustchain-http/src/challenge_response.rs @@ -14,7 +14,6 @@ use std::collections::HashMap; use std::fs::OpenOptions; use std::fs::{self, File}; use std::io::{BufWriter, Write}; -use tracing::field; use std::path::PathBuf; use thiserror::Error; @@ -320,30 +319,41 @@ impl ElementwiseSerializeDeserialize for CRInitiation { mut self, path: &PathBuf, ) -> Result { - // temporary public key - let full_path = path.join("temp_p_key.json"); - if !full_path.exists() { - return Err(TrustchainCRError::FailedToDeserialize); - } - let file = File::open(full_path).map_err(|_| TrustchainCRError::FailedToDeserialize)?; - let reader = std::io::BufReader::new(file); - self.temp_p_key = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; - // requester details - let full_path = path.join("requester_details.json"); - if !full_path.exists() { - return Err(TrustchainCRError::FailedToDeserialize); - } - let file = File::open(full_path).map_err(|_| TrustchainCRError::FailedToDeserialize)?; - let reader = std::io::BufReader::new(file); - self.requester_details = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; + let temp_p_key_path = path.join("temp_p_key.json"); + self.temp_p_key = match File::open(&temp_p_key_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + // self.temp_p_key = serde_json::from_reader(reader) + // .map_err(|_| TrustchainCRError::FailedToDeserialize) + // .ok() + } + Err(_) => None, + }; - // let initiation = CRInitiation { - // temp_p_key: Some(temp_p_key), - // requester_details: Some(requester_details), + // self.temp_p_key = match File::open(&temp_p_key_path) { + // Ok(file) => { + // let reader = std::io::BufReader::new(file); + // serde_json::from_reader(reader) + // .map_err(|_| TrustchainCRError::FailedToDeserialize) + // .ok() + // } + // Err(_) => None, // }; + let requester_details_path = path.join("requester_details.json"); + self.requester_details = match File::open(&requester_details_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; + Ok(self) } } @@ -635,8 +645,7 @@ fn check_cr_state(state: CRState) -> Result<(), TrustchainCRError> { mod tests { use std::env; - - use josekit::jwe::alg::direct::DirectJweAlgorithm; + use tempfile::tempdir; use crate::data::{ TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS, TEST_SIGNING_KEY_1, TEST_SIGNING_KEY_2, @@ -915,32 +924,9 @@ mod tests { }; // write to file let directory_path = env::current_dir().unwrap(); - cr_state.elementwise_serialize(&directory_path).unwrap(); - - // test serialise CR state - // let serialized = serde_json::to_value(&cr_state.initiation).expect("Serialization failed"); - - // if let Value::Object(fields) = serialized { - // for (field_name, field_value) in fields { - // match field_value { - // Value::Null => println!("Field {} is empty.", field_name), - // // _ => println!("Field {} has a value: {:?}", field_name, field_value), - // _ => { - // let json_filename = format!("{}.json", field_name); - // write_to_json_file(&json_filename, &field_value); - // } - // } - // } - // } - } - - #[test] - fn test_deserialize_initiation() { - let directory_path = env::current_dir().unwrap(); - let initiation = CRInitiation::new() - .elementwise_deserialize(&directory_path) - .unwrap(); - println!("Initiation deserialized from files: {:?}", initiation); + println!("directory path: {:?}", directory_path); + let result = cr_state.elementwise_serialize(&directory_path); + assert_eq!(result.is_ok(), true); } #[test] @@ -978,8 +964,55 @@ mod tests { challenge_state ); } -} -// notes: -// - new version of elementwise serialise: is there much benefit? -// - deserialise still still needs to be hardcoded + #[test] + fn test_elementwise_deserialize_initiation() { + let cr_initiation = CRInitiation::new(); + let temp_path = tempdir().unwrap().into_path(); + + // Test case 1: None of the json files exist + let result = cr_initiation.elementwise_deserialize(&temp_path); + assert!(result.is_ok()); + let initiation = result.unwrap(); + assert!(initiation.temp_p_key.is_none()); + assert!(initiation.requester_details.is_none()); + + // Test case 2: Only one json file exists and can be deserialized + let cr_initiation = CRInitiation::new(); + let temp_p_key_path = temp_path.join("temp_p_key.json"); + let temp_p_key_file = File::create(&temp_p_key_path).unwrap(); + let temp_p_key: Jwk = serde_json::from_str(TEST_TEMP_KEY).unwrap(); + serde_json::to_writer(temp_p_key_file, &temp_p_key).unwrap(); + + let result = cr_initiation.elementwise_deserialize(&temp_path); + assert!(result.is_ok()); + let initiation = result.unwrap(); + assert!(initiation.temp_p_key.is_some()); + assert!(initiation.requester_details.is_none()); + + // Test case 3: Both json files exist and can be deserialized + let cr_initiation = CRInitiation::new(); + let requester_details_path = temp_path.join("requester_details.json"); + let requester_details_file = File::create(&requester_details_path).unwrap(); + let requester_details = RequesterDetails { + requester_org: String::from("My Org"), + operator_name: String::from("John Doe"), + }; + serde_json::to_writer(requester_details_file, &requester_details).unwrap(); + let result = cr_initiation.elementwise_deserialize(&temp_path); + assert!(result.is_ok()); + let initiation = result.unwrap(); + assert!(initiation.temp_p_key.is_some()); + assert!(initiation.requester_details.is_some()); + + // Test case 4: Both json files exist but one is invalid json and cannot be + // deserialized + let cr_initiation = CRInitiation::new(); + // override temp key with invalid key + let temp_p_key_file = File::create(&temp_p_key_path).unwrap(); + serde_json::to_writer(temp_p_key_file, "this is not valid json").unwrap(); + let result = cr_initiation.elementwise_deserialize(&temp_path); + assert!(result.is_err()); + println!("Error: {:?}", result.unwrap_err()); + } +} From 1759fc0d1ceea85146c67fffbdde931e7ad6f22d Mon Sep 17 00:00:00 2001 From: pwochner Date: Wed, 4 Oct 2023 17:54:59 +0200 Subject: [PATCH 19/86] Error handling of elementwise deserialisation for identity and content CR. --- trustchain-http/src/challenge_response.rs | 230 +++++++++++++++------- 1 file changed, 154 insertions(+), 76 deletions(-) diff --git a/trustchain-http/src/challenge_response.rs b/trustchain-http/src/challenge_response.rs index 708a60ed..577e1bce 100644 --- a/trustchain-http/src/challenge_response.rs +++ b/trustchain-http/src/challenge_response.rs @@ -326,23 +326,10 @@ impl ElementwiseSerializeDeserialize for CRInitiation { let deserialized = serde_json::from_reader(reader) .map_err(|_| TrustchainCRError::FailedToDeserialize)?; Some(deserialized) - // self.temp_p_key = serde_json::from_reader(reader) - // .map_err(|_| TrustchainCRError::FailedToDeserialize) - // .ok() } Err(_) => None, }; - // self.temp_p_key = match File::open(&temp_p_key_path) { - // Ok(file) => { - // let reader = std::io::BufReader::new(file); - // serde_json::from_reader(reader) - // .map_err(|_| TrustchainCRError::FailedToDeserialize) - // .ok() - // } - // Err(_) => None, - // }; - let requester_details_path = path.join("requester_details.json"); self.requester_details = match File::open(&requester_details_path) { Ok(file) => { @@ -416,42 +403,59 @@ impl ElementwiseSerializeDeserialize for CRIdentityChallenge { path: &PathBuf, ) -> Result { // update public key - let full_path = path.join("update_p_key.json"); - if !full_path.exists() { - return Err(TrustchainCRError::FailedToDeserialize); - } - let file = File::open(full_path).map_err(|_| TrustchainCRError::FailedToDeserialize)?; - let reader = std::io::BufReader::new(file); - self.update_p_key = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; + let mut full_path = path.join("update_p_key.json"); + self.update_p_key = match File::open(&full_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; + + // if !full_path.exists() { + // return Err(TrustchainCRError::FailedToDeserialize); + // } + // let file = File::open(full_path).map_err(|_| TrustchainCRError::FailedToDeserialize)?; + // let reader = std::io::BufReader::new(file); + // self.update_p_key = serde_json::from_reader(reader) + // .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; // identity nonce - let full_path = path.join("identity_nonce.json"); - if !full_path.exists() { - return Err(TrustchainCRError::FailedToDeserialize); - } - let file = File::open(full_path).map_err(|_| TrustchainCRError::FailedToDeserialize)?; - let reader = std::io::BufReader::new(file); - self.identity_nonce = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; + full_path = path.join("identity_nonce.json"); + self.identity_nonce = match File::open(&full_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; + // identity challenge signature - let full_path = path.join("identity_challenge_signature.json"); - if !full_path.exists() { - return Err(TrustchainCRError::FailedToDeserialize); - } - let file = File::open(full_path).map_err(|_| TrustchainCRError::FailedToDeserialize)?; - let reader = std::io::BufReader::new(file); - self.identity_challenge_signature = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; + full_path = path.join("identity_challenge_signature.json"); + self.identity_challenge_signature = match File::open(&full_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; // identity response signature - let full_path = path.join("identity_response_signature.json"); - if !full_path.exists() { - return Err(TrustchainCRError::FailedToDeserialize); - } - let file = File::open(full_path).map_err(|_| TrustchainCRError::FailedToDeserialize)?; - let reader = std::io::BufReader::new(file); - self.identity_response_signature = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; + full_path = path.join("identity_response_signature.json"); + self.identity_response_signature = match File::open(&full_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; Ok(self) } @@ -528,32 +532,39 @@ impl ElementwiseSerializeDeserialize for CRContentChallenge { path: &PathBuf, ) -> Result { // content nonce(s) - let full_path = path.join("content_nonce.json"); - if !full_path.exists() { - return Err(TrustchainCRError::FailedToDeserialize); - } - let file = File::open(full_path).map_err(|_| TrustchainCRError::FailedToDeserialize)?; - let reader = std::io::BufReader::new(file); - self.content_nonce = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; + let mut full_path = path.join("content_nonce.json"); + self.content_nonce = match File::open(&full_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; + // content challenge signature - let full_path = path.join("content_challenge_signature.json"); - if !full_path.exists() { - return Err(TrustchainCRError::FailedToDeserialize); - } - let file = File::open(full_path).map_err(|_| TrustchainCRError::FailedToDeserialize)?; - let reader = std::io::BufReader::new(file); - self.content_challenge_signature = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; + full_path = path.join("content_challenge_signature.json"); + self.content_challenge_signature = match File::open(&full_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; // content response signature - let full_path = path.join("content_response_signature.json"); - if !full_path.exists() { - return Err(TrustchainCRError::FailedToDeserialize); - } - let file = File::open(full_path).map_err(|_| TrustchainCRError::FailedToDeserialize)?; - let reader = std::io::BufReader::new(file); - self.content_response_signature = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; + full_path = path.join("content_response_signature.json"); + self.content_response_signature = match File::open(&full_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; Ok(self) } @@ -932,13 +943,8 @@ mod tests { #[test] fn test_deserialize_identity_challenge() { let directory_path = env::current_dir().unwrap(); - let identity_challenge = CRIdentityChallenge::new() - .elementwise_deserialize(&directory_path) - .unwrap(); - println!( - "Identity challenge deserialized from files: {:?}", - identity_challenge - ); + let result = CRIdentityChallenge::new().elementwise_deserialize(&directory_path); + println!("Result identity challenge deserialization: {:?}", result); } #[test] @@ -1015,4 +1021,76 @@ mod tests { assert!(result.is_err()); println!("Error: {:?}", result.unwrap_err()); } + + #[test] + fn test_elementwise_deserialize_identity_challenge() { + let identity_challenge = CRIdentityChallenge::new(); + let temp_path = tempdir().unwrap().into_path(); + + // Test case 1: None of the json files exist + let result = identity_challenge.elementwise_deserialize(&temp_path); + assert!(result.is_ok()); + let identity_challenge = result.unwrap(); + assert!(identity_challenge.update_p_key.is_none()); + assert!(identity_challenge.identity_nonce.is_none()); + assert!(identity_challenge.identity_challenge_signature.is_none()); + assert!(identity_challenge.identity_response_signature.is_none()); + + // Test case 2: Only one json file exists and can be deserialized + let update_p_key_path = temp_path.join("update_p_key.json"); + let update_p_key_file = File::create(&update_p_key_path).unwrap(); + let update_p_key: Jwk = serde_json::from_str(TEST_UPDATE_KEY).unwrap(); + serde_json::to_writer(update_p_key_file, &update_p_key).unwrap(); + let identity_challenge = CRIdentityChallenge::new(); + let result = identity_challenge.elementwise_deserialize(&temp_path); + assert!(result.is_ok()); + let identity_challenge = result.unwrap(); + assert_eq!(identity_challenge.update_p_key, Some(update_p_key)); + assert!(identity_challenge.identity_nonce.is_none()); + assert!(identity_challenge.identity_challenge_signature.is_none()); + assert!(identity_challenge.identity_response_signature.is_none()); + + // Test case 3: One file exists but cannot be deserialized + let identity_nonce_path = temp_path.join("identity_nonce.json"); + let identity_nonce_file = File::create(&identity_nonce_path).unwrap(); + serde_json::to_writer(identity_nonce_file, &42).unwrap(); + let identity_challenge = CRIdentityChallenge::new(); + let result = identity_challenge.elementwise_deserialize(&temp_path); + assert!(result.is_err()); + println!("Error: {:?}", result.unwrap_err()); + } + + #[test] + fn test_elementwise_deserialize_content_challenge() { + let content_challenge = CRContentChallenge::new(); + let temp_path = tempdir().unwrap().into_path(); + + // Test case 1: None of the json files exist + let result = content_challenge.elementwise_deserialize(&temp_path); + assert!(result.is_ok()); + let content_challenge = result.unwrap(); + assert!(content_challenge.content_nonce.is_none()); + assert!(content_challenge.content_challenge_signature.is_none()); + assert!(content_challenge.content_response_signature.is_none()); + + // Test case 2: Only one json file exists and can be deserialized + let content_nonce_path = temp_path.join("content_nonce.json"); + let content_nonce_file = File::create(&content_nonce_path).unwrap(); + let mut nonces_map: HashMap<&str, Nonce> = HashMap::new(); + nonces_map.insert("test_id", Nonce::new()); + serde_json::to_writer(content_nonce_file, &nonces_map).unwrap(); + let result = content_challenge.elementwise_deserialize(&temp_path); + assert!(result.is_ok()); + let content_challenge = result.unwrap(); + assert!(content_challenge.content_nonce.is_some()); + assert!(content_challenge.content_challenge_signature.is_none()); + assert!(content_challenge.content_response_signature.is_none()); + + // Test case 3: One file exists but cannot be deserialized + let content_nonce_file = File::create(&content_nonce_path).unwrap(); + serde_json::to_writer(content_nonce_file, "thisisinvalid").unwrap(); + let result = content_challenge.elementwise_deserialize(&temp_path); + print!("Result: {:?}", result); + assert!(result.is_err()); + } } From 2e0ebf8115a08833376f19931adb430c50e7fd10 Mon Sep 17 00:00:00 2001 From: pwochner Date: Thu, 5 Oct 2023 14:37:42 +0200 Subject: [PATCH 20/86] Return none for elementwise_deserialize() if none of the files exist. --- trustchain-http/src/challenge_response.rs | 133 ++++++++++++---------- 1 file changed, 74 insertions(+), 59 deletions(-) diff --git a/trustchain-http/src/challenge_response.rs b/trustchain-http/src/challenge_response.rs index 577e1bce..0bac46b7 100644 --- a/trustchain-http/src/challenge_response.rs +++ b/trustchain-http/src/challenge_response.rs @@ -82,7 +82,7 @@ where Ok(()) } - fn elementwise_deserialize(self, path: &PathBuf) -> Result + fn elementwise_deserialize(self, path: &PathBuf) -> Result, TrustchainCRError> where Self: Sized; @@ -252,13 +252,16 @@ impl ElementwiseSerializeDeserialize for CRState { } Ok(()) } - fn elementwise_deserialize(mut self, path: &PathBuf) -> Result { - self.initiation = Some(CRInitiation::new().elementwise_deserialize(path)?); + fn elementwise_deserialize( + mut self, + path: &PathBuf, + ) -> Result, TrustchainCRError> { + self.initiation = CRInitiation::new().elementwise_deserialize(path)?; self.identity_challenge_response = - Some(CRIdentityChallenge::new().elementwise_deserialize(path)?); + CRIdentityChallenge::new().elementwise_deserialize(path)?; self.content_challenge_response = - Some(CRContentChallenge::new().elementwise_deserialize(path)?); - Ok(self) + CRContentChallenge::new().elementwise_deserialize(path)?; + Ok(Some(self)) } } @@ -318,7 +321,7 @@ impl ElementwiseSerializeDeserialize for CRInitiation { fn elementwise_deserialize( mut self, path: &PathBuf, - ) -> Result { + ) -> Result, TrustchainCRError> { let temp_p_key_path = path.join("temp_p_key.json"); self.temp_p_key = match File::open(&temp_p_key_path) { Ok(file) => { @@ -341,7 +344,11 @@ impl ElementwiseSerializeDeserialize for CRInitiation { Err(_) => None, }; - Ok(self) + if self.temp_p_key.is_none() && self.requester_details.is_none() { + return Ok(None); + } + + Ok(Some(self)) } } @@ -401,7 +408,7 @@ impl ElementwiseSerializeDeserialize for CRIdentityChallenge { fn elementwise_deserialize( mut self, path: &PathBuf, - ) -> Result { + ) -> Result, TrustchainCRError> { // update public key let mut full_path = path.join("update_p_key.json"); self.update_p_key = match File::open(&full_path) { @@ -413,14 +420,6 @@ impl ElementwiseSerializeDeserialize for CRIdentityChallenge { } Err(_) => None, }; - - // if !full_path.exists() { - // return Err(TrustchainCRError::FailedToDeserialize); - // } - // let file = File::open(full_path).map_err(|_| TrustchainCRError::FailedToDeserialize)?; - // let reader = std::io::BufReader::new(file); - // self.update_p_key = serde_json::from_reader(reader) - // .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; // identity nonce full_path = path.join("identity_nonce.json"); self.identity_nonce = match File::open(&full_path) { @@ -432,7 +431,6 @@ impl ElementwiseSerializeDeserialize for CRIdentityChallenge { } Err(_) => None, }; - // identity challenge signature full_path = path.join("identity_challenge_signature.json"); self.identity_challenge_signature = match File::open(&full_path) { @@ -444,7 +442,6 @@ impl ElementwiseSerializeDeserialize for CRIdentityChallenge { } Err(_) => None, }; - // identity response signature full_path = path.join("identity_response_signature.json"); self.identity_response_signature = match File::open(&full_path) { @@ -457,7 +454,15 @@ impl ElementwiseSerializeDeserialize for CRIdentityChallenge { Err(_) => None, }; - Ok(self) + if self.update_p_key.is_none() + && self.identity_nonce.is_none() + && self.identity_challenge_signature.is_none() + && self.identity_response_signature.is_none() + { + return Ok(None); + } + + Ok(Some(self)) } } @@ -530,7 +535,7 @@ impl ElementwiseSerializeDeserialize for CRContentChallenge { fn elementwise_deserialize( mut self, path: &PathBuf, - ) -> Result { + ) -> Result, TrustchainCRError> { // content nonce(s) let mut full_path = path.join("content_nonce.json"); self.content_nonce = match File::open(&full_path) { @@ -566,7 +571,14 @@ impl ElementwiseSerializeDeserialize for CRContentChallenge { Err(_) => None, }; - Ok(self) + if self.content_nonce.is_none() + && self.content_challenge_signature.is_none() + && self.content_response_signature.is_none() + { + return Ok(None); + } + + Ok(Some(self)) } } @@ -941,34 +953,44 @@ mod tests { } #[test] - fn test_deserialize_identity_challenge() { - let directory_path = env::current_dir().unwrap(); - let result = CRIdentityChallenge::new().elementwise_deserialize(&directory_path); - println!("Result identity challenge deserialization: {:?}", result); - } + fn test_deserialize_challenge_state() { + let path = tempdir().unwrap().into_path(); + let challenge_state = CRState::new(); - #[test] - fn test_deserialize_content_challenge() { - let directory_path = env::current_dir().unwrap(); - let content_challenge = CRContentChallenge::new() - .elementwise_deserialize(&directory_path) - .unwrap(); - println!( - "Content challenge deserialized from files: {:?}", - content_challenge - ); - } + // Test case 1: some files exist and can be deserialised + let initiatiation = CRInitiation { + temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), + requester_details: Some(RequesterDetails { + requester_org: String::from("My Org"), + operator_name: String::from("John Doe"), + }), + }; + let _ = initiatiation.elementwise_serialize(&path); + let identity_challenge = CRIdentityChallenge { + update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), + identity_nonce: Some(Nonce::new()), + identity_challenge_signature: Some(String::from("some challenge signature string")), + identity_response_signature: Some(String::from("some response signature string")), + }; + let _ = identity_challenge.elementwise_serialize(&path); - #[test] - fn test_deserialize_challenge_state() { - let directory_path = env::current_dir().unwrap(); - let challenge_state = CRState::new() - .elementwise_deserialize(&directory_path) - .unwrap(); + let result = challenge_state.elementwise_deserialize(&path); + assert!(result.is_ok()); + let challenge_state = result.unwrap().unwrap(); println!( "Challenge state deserialized from files: {:?}", challenge_state ); + assert!(challenge_state.initiation.is_some()); + assert!(challenge_state.identity_challenge_response.is_some()); + assert!(challenge_state.content_challenge_response.is_none()); + + // Test case 2: one file cannot be deserialized + let identity_nonce_path = path.join("content_nonce.json"); + let identity_nonce_file = File::create(&identity_nonce_path).unwrap(); + serde_json::to_writer(identity_nonce_file, &42).unwrap(); + let challenge_state = CRState::new().elementwise_deserialize(&path); + assert!(challenge_state.is_err()); } #[test] @@ -980,8 +1002,7 @@ mod tests { let result = cr_initiation.elementwise_deserialize(&temp_path); assert!(result.is_ok()); let initiation = result.unwrap(); - assert!(initiation.temp_p_key.is_none()); - assert!(initiation.requester_details.is_none()); + assert!(initiation.is_none()); // Test case 2: Only one json file exists and can be deserialized let cr_initiation = CRInitiation::new(); @@ -992,7 +1013,7 @@ mod tests { let result = cr_initiation.elementwise_deserialize(&temp_path); assert!(result.is_ok()); - let initiation = result.unwrap(); + let initiation = result.unwrap().unwrap(); assert!(initiation.temp_p_key.is_some()); assert!(initiation.requester_details.is_none()); @@ -1007,7 +1028,7 @@ mod tests { serde_json::to_writer(requester_details_file, &requester_details).unwrap(); let result = cr_initiation.elementwise_deserialize(&temp_path); assert!(result.is_ok()); - let initiation = result.unwrap(); + let initiation = result.unwrap().unwrap(); assert!(initiation.temp_p_key.is_some()); assert!(initiation.requester_details.is_some()); @@ -1019,7 +1040,6 @@ mod tests { serde_json::to_writer(temp_p_key_file, "this is not valid json").unwrap(); let result = cr_initiation.elementwise_deserialize(&temp_path); assert!(result.is_err()); - println!("Error: {:?}", result.unwrap_err()); } #[test] @@ -1031,10 +1051,7 @@ mod tests { let result = identity_challenge.elementwise_deserialize(&temp_path); assert!(result.is_ok()); let identity_challenge = result.unwrap(); - assert!(identity_challenge.update_p_key.is_none()); - assert!(identity_challenge.identity_nonce.is_none()); - assert!(identity_challenge.identity_challenge_signature.is_none()); - assert!(identity_challenge.identity_response_signature.is_none()); + assert!(identity_challenge.is_none()); // Test case 2: Only one json file exists and can be deserialized let update_p_key_path = temp_path.join("update_p_key.json"); @@ -1044,7 +1061,7 @@ mod tests { let identity_challenge = CRIdentityChallenge::new(); let result = identity_challenge.elementwise_deserialize(&temp_path); assert!(result.is_ok()); - let identity_challenge = result.unwrap(); + let identity_challenge = result.unwrap().unwrap(); assert_eq!(identity_challenge.update_p_key, Some(update_p_key)); assert!(identity_challenge.identity_nonce.is_none()); assert!(identity_challenge.identity_challenge_signature.is_none()); @@ -1068,12 +1085,10 @@ mod tests { // Test case 1: None of the json files exist let result = content_challenge.elementwise_deserialize(&temp_path); assert!(result.is_ok()); - let content_challenge = result.unwrap(); - assert!(content_challenge.content_nonce.is_none()); - assert!(content_challenge.content_challenge_signature.is_none()); - assert!(content_challenge.content_response_signature.is_none()); + assert!(result.unwrap().is_none()); // Test case 2: Only one json file exists and can be deserialized + let content_challenge = CRContentChallenge::new(); let content_nonce_path = temp_path.join("content_nonce.json"); let content_nonce_file = File::create(&content_nonce_path).unwrap(); let mut nonces_map: HashMap<&str, Nonce> = HashMap::new(); @@ -1081,7 +1096,7 @@ mod tests { serde_json::to_writer(content_nonce_file, &nonces_map).unwrap(); let result = content_challenge.elementwise_deserialize(&temp_path); assert!(result.is_ok()); - let content_challenge = result.unwrap(); + let content_challenge = result.unwrap().unwrap(); assert!(content_challenge.content_nonce.is_some()); assert!(content_challenge.content_challenge_signature.is_none()); assert!(content_challenge.content_response_signature.is_none()); From 25a2f073cc2def79e3985dc209af7dc5315995a3 Mon Sep 17 00:00:00 2001 From: pwochner Date: Thu, 5 Oct 2023 16:26:35 +0200 Subject: [PATCH 21/86] Don't overwrite file if already exists for elementwise serialize. --- trustchain-http/src/challenge_response.rs | 24 +++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/trustchain-http/src/challenge_response.rs b/trustchain-http/src/challenge_response.rs index 0bac46b7..85075a7e 100644 --- a/trustchain-http/src/challenge_response.rs +++ b/trustchain-http/src/challenge_response.rs @@ -87,12 +87,13 @@ where Self: Sized; fn save_to_file(&self, path: &PathBuf, data: &str) -> Result<(), TrustchainCRError> { - // Open the new file - let new_file = OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(path); + if path.exists() { + println!("File already exists: {:?}", path); + return Ok(()); + } + + // Open the new file if it doesn't exist yet + let new_file = OpenOptions::new().create(true).write(true).open(path); // Write key to file match new_file { @@ -112,6 +113,7 @@ where Err(_) => Err(TrustchainCRError::FailedToSave), } } + Err(_) => Err(TrustchainCRError::FailedToSave), } } @@ -896,7 +898,7 @@ mod tests { assert_eq!(verified_response_map, nonces); } #[test] - fn test_write_structs_to_file() { + fn test_elementwise_serialize() { // ==========| Identity CR | ============== let temp_s_key: Jwk = serde_json::from_str(TEST_TEMP_KEY).unwrap(); let initiation = CRInitiation { @@ -946,10 +948,12 @@ mod tests { content_challenge_response: Some(content_challenge_response), }; // write to file - let directory_path = env::current_dir().unwrap(); - println!("directory path: {:?}", directory_path); - let result = cr_state.elementwise_serialize(&directory_path); + let path = tempdir().unwrap().into_path(); + let result = cr_state.elementwise_serialize(&path); assert_eq!(result.is_ok(), true); + + // try to write to file again + let result = cr_state.elementwise_serialize(&path); } #[test] From 8332ceafb26623a6100685947f6e9b22ee3bdffa Mon Sep 17 00:00:00 2001 From: pwochner Date: Mon, 9 Oct 2023 16:14:06 +0100 Subject: [PATCH 22/86] Adds status check for challenge response. --- trustchain-http/Cargo.toml | 1 + trustchain-http/src/challenge_response.rs | 182 +++++++++++++++++++++- 2 files changed, 175 insertions(+), 8 deletions(-) diff --git a/trustchain-http/Cargo.toml b/trustchain-http/Cargo.toml index 8f6229c1..2b4158fd 100644 --- a/trustchain-http/Cargo.toml +++ b/trustchain-http/Cargo.toml @@ -48,6 +48,7 @@ rand = "0.8" josekit = "0.8" serde_with = "*" tempfile = "*" +is_empty = "*" [dev-dependencies] axum-test-helper = "0.2.0" diff --git a/trustchain-http/src/challenge_response.rs b/trustchain-http/src/challenge_response.rs index 85075a7e..01c3e398 100644 --- a/trustchain-http/src/challenge_response.rs +++ b/trustchain-http/src/challenge_response.rs @@ -15,6 +15,7 @@ use std::fs::OpenOptions; use std::fs::{self, File}; use std::io::{BufWriter, Write}; +use is_empty::IsEmpty; use std::path::PathBuf; use thiserror::Error; @@ -53,6 +54,9 @@ pub enum TrustchainCRError { /// Failed to check CR status. #[error("Failed to determine CR status.")] FailedStatusCheck, + /// Path for CR does not exist. + #[error("Path does not exist. No challenge-response record for this temporary key id.")] + CRPathNotFound, } impl From for TrustchainCRError { @@ -224,7 +228,7 @@ impl From for Nonce { } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, IsEmpty)] struct CRState { initiation: Option, identity_challenge_response: Option, @@ -239,6 +243,73 @@ impl CRState { content_challenge_response: None, } } + + fn check_cr_status(&self) -> Result<(), TrustchainCRError> { + if self.is_empty() { + println!("No records found for this challenge-response identifier. The challenge-response process has not been initiated yet. "); + return Ok(()); + } + + // CR complete + if self.is_complete() { + println!("Challenge-response complete."); + return Ok(()); + } + + // CR initation + let initiation = self.initiation.as_ref().unwrap(); + if !initiation.is_complete() { + println!("Initiation incomplete."); + return Ok(()); + } + println!("Initiation complete."); + + // Identity challenge-response + let identity_challenge_response = self.identity_challenge_response.as_ref().unwrap(); + if !identity_challenge_response.is_complete() { + if identity_challenge_response.update_p_key.is_some() + && identity_challenge_response.identity_nonce.is_some() + && identity_challenge_response + .identity_challenge_signature + .is_some() + { + println!("Identity challenge has been presented. Await response."); + return Ok(()); + } + println!("Identity challenge incomplete."); + return Ok(()); + } + println!("Identity challenge-response complete."); + + // Content challenge-response + let content_challenge_response = self.content_challenge_response.as_ref().unwrap(); + if !content_challenge_response.is_complete() { + if content_challenge_response.content_nonce.is_some() + && content_challenge_response + .content_challenge_signature + .is_some() + { + println!("Content challenge has been presented. Await response."); + return Ok(()); + } + println!("Content challenge incomplete."); + return Ok(()); + } + println!("Content challenge-response complete."); + Ok(()) + } +} + +impl IsComplete for CRState { + fn is_complete(&self) -> bool { + if self.initiation.is_some() + && self.identity_challenge_response.is_some() + && self.content_challenge_response.is_some() + { + return true; + } + return false; + } } impl ElementwiseSerializeDeserialize for CRState { @@ -275,7 +346,7 @@ struct RequesterDetails { } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, IsEmpty)] struct CRInitiation { temp_p_key: Option, requester_details: Option, @@ -290,6 +361,15 @@ impl CRInitiation { } } +impl IsComplete for CRInitiation { + fn is_complete(&self) -> bool { + if self.temp_p_key.is_some() && self.requester_details.is_some() { + return true; + } + return false; + } +} + impl ElementwiseSerializeDeserialize for CRInitiation { // fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { // // let file_path = path.join("temp_p_key.json"); @@ -355,7 +435,7 @@ impl ElementwiseSerializeDeserialize for CRInitiation { } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, IsEmpty)] struct CRIdentityChallenge { update_p_key: Option, identity_nonce: Option, // make own Nonce type @@ -374,6 +454,19 @@ impl CRIdentityChallenge { } } +impl IsComplete for CRIdentityChallenge { + fn is_complete(&self) -> bool { + if self.update_p_key.is_some() + && self.identity_nonce.is_some() + && self.identity_challenge_signature.is_some() + && self.identity_response_signature.is_some() + { + return true; + } + return false; + } +} + // todo: add path to serialise/deserialise functions? impl ElementwiseSerializeDeserialize for CRIdentityChallenge { // fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { @@ -492,7 +585,7 @@ impl TryFrom<&JwtPayload> for CRIdentityChallenge { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, IsEmpty)] struct CRContentChallenge { content_nonce: Option>, content_challenge_signature: Option, @@ -509,6 +602,18 @@ impl CRContentChallenge { } } +impl IsComplete for CRContentChallenge { + fn is_complete(&self) -> bool { + if self.content_nonce.is_some() + && self.content_challenge_signature.is_some() + && self.content_response_signature.is_some() + { + return true; + } + return false; + } +} + impl ElementwiseSerializeDeserialize for CRContentChallenge { // fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { // if let Some(content_nonce) = &self.content_nonce { @@ -662,14 +767,60 @@ fn extract_key_ids_and_jwk(document: &Document) -> Result, Ok(my_map) } -fn check_cr_state(state: CRState) -> Result<(), TrustchainCRError> { - todo!() -} +// fn check_cr_state(path: &PathBuf, key_id: &str, state: CRState) -> Result<(), TrustchainCRError> { +// if !path.exists() { +// return Err(TrustchainCRError::CRPathNotFound); +// } +// let cr_state = CRState::new().elementwise_deserialize(path).unwrap().unwrap(); + +// // The directory exists, but there are no json files +// if cr_state.is_empty() { +// println!("No records found for this challenge-response identifier. The challenge-response process has not been initiated yet. ") +// return Ok(()); +// } + +// // CR complete +// if cr_state.is_complete(){ +// println!("Challenge-response complete."); +// return Ok(()); +// } + +// // CR initation +// if !cr_state.initiation.unwrap().is_complete() { +// println!("Initiation incomplete."); +// return Ok(()); +// } +// println!("Initiation complete."); + +// // Identity challenge-response +// if !cr_state.identity_challenge_response.unwrap().is_complete() { +// if cr_state.identity_challenge_response.unwrap().update_p_key.is_some() && cr_state.identity_challenge_response.unwrap().identity_nonce.is_some() && cr_state.identity_challenge_response.unwrap().identity_challenge_signature.is_some() { +// println!("Identity challenge has been presented. Await response."); +// return Ok(()) +// } +// println!("Identity challenge incomplete."); +// return Ok(()) +// } +// println!("Identity challenge-response complete."); + +// // Content challenge-response +// if !cr_state.content_challenge_response.unwrap().is_complete() { +// if cr_state.content_challenge_response.unwrap().content_nonce.is_some() && cr_state.content_challenge_response.unwrap().content_challenge_signature.is_some() { +// println!("Content challenge has been presented. Await response."); +// return Ok(()) +// } +// println!("Content challenge incomplete."); +// return Ok(()) +// } +// println!("Content challenge-response complete."); +// Ok(()) +// } #[cfg(test)] mod tests { - use std::env; + use std::fs::create_dir; + use tempfile::tempdir; use crate::data::{ @@ -1112,4 +1263,19 @@ mod tests { print!("Result: {:?}", result); assert!(result.is_err()); } + + #[test] + fn test_cr_status_check() { + let temp_path = tempdir().unwrap().into_path(); + let key_id = "test_key_id"; + let cr_path = temp_path.join(key_id); + let _ = create_dir(&cr_path); + + // Test case 1: no files exist + let cr_state = CRState::new() + .elementwise_deserialize(&cr_path) + .unwrap() + .unwrap(); + let result = cr_state.check_cr_status(); + } } From 1bb4e6cca1db77f5035f081dbdc585e9abf305e6 Mon Sep 17 00:00:00 2001 From: pwochner Date: Thu, 12 Oct 2023 15:38:12 +0100 Subject: [PATCH 23/86] Use enum for checking CR status. Add struct for Content CR Initiation. More tests. --- trustchain-http/src/challenge_response.rs | 490 +++++++++++++--------- 1 file changed, 292 insertions(+), 198 deletions(-) diff --git a/trustchain-http/src/challenge_response.rs b/trustchain-http/src/challenge_response.rs index 01c3e398..cc581807 100644 --- a/trustchain-http/src/challenge_response.rs +++ b/trustchain-http/src/challenge_response.rs @@ -59,6 +59,43 @@ pub enum TrustchainCRError { CRPathNotFound, } +#[derive(Debug, PartialEq)] +enum CurrentCRState { + NotStarted, + IdentityCRInitiated, + IdentityChallengeComplete, + IdentityResponseComplete, + ContentCRInitiated, + ContentChallengeComplete, + ContentResponseComplete, +} + +fn get_status_message(current_state: &CurrentCRState) -> String { + match current_state { + CurrentCRState::NotStarted => { + return String::from("No records found for this challenge-response identifier. The challenge-response process has not been initiated yet."); + } + CurrentCRState::IdentityCRInitiated => { + return String::from("Identity challenge-response initiated. Await response."); + } + CurrentCRState::IdentityChallengeComplete => { + return String::from("Identity challenge has been presented. Await response."); + } + CurrentCRState::IdentityResponseComplete => { + return String::from("Identity challenge-response complete."); + } + CurrentCRState::ContentCRInitiated => { + return String::from("Content challenge-response initiated. Await response."); + } + CurrentCRState::ContentChallengeComplete => { + return String::from("Content challenge has been presented. Await response."); + } + CurrentCRState::ContentResponseComplete => { + return String::from("Challenge-response complete."); + } + } +} + impl From for TrustchainCRError { fn from(err: JoseError) -> Self { Self::Jose(err) @@ -123,10 +160,6 @@ where } } -pub trait IsComplete { - fn is_complete(&self) -> bool; -} - /// Interface for signing and then encrypting data. pub trait SignEncrypt { fn sign(&self, payload: &JwtPayload, secret_key: &Jwk) -> Result { @@ -230,96 +263,126 @@ impl From for Nonce { #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, IsEmpty)] struct CRState { - initiation: Option, + identity_cr_initiation: Option, identity_challenge_response: Option, + content_cr_initiation: Option, content_challenge_response: Option, } impl CRState { fn new() -> Self { Self { - initiation: None, + identity_cr_initiation: None, identity_challenge_response: None, + content_cr_initiation: None, content_challenge_response: None, } } - fn check_cr_status(&self) -> Result<(), TrustchainCRError> { + fn is_complete(&self) -> bool { + if self.identity_cr_initiation.is_some() + && self.identity_challenge_response.is_some() + && self.content_cr_initiation.is_some() + && self.content_challenge_response.is_some() + { + return true; + } + return false; + } + + fn check_cr_status(&self) -> Result { + let mut current_state = CurrentCRState::NotStarted; if self.is_empty() { - println!("No records found for this challenge-response identifier. The challenge-response process has not been initiated yet. "); - return Ok(()); + println!("{}", get_status_message(¤t_state)); + return Ok(current_state); } // CR complete if self.is_complete() { - println!("Challenge-response complete."); - return Ok(()); + current_state = CurrentCRState::ContentResponseComplete; + println!("{}", get_status_message(¤t_state)); + return Ok(current_state); } - // CR initation - let initiation = self.initiation.as_ref().unwrap(); - if !initiation.is_complete() { - println!("Initiation incomplete."); - return Ok(()); + // Identity CR initation + if self.identity_cr_initiation.is_none() + || !self.identity_cr_initiation.as_ref().unwrap().is_complete() + { + println!("{}", get_status_message(¤t_state)); + return Ok(current_state); } - println!("Initiation complete."); - - // Identity challenge-response - let identity_challenge_response = self.identity_challenge_response.as_ref().unwrap(); - if !identity_challenge_response.is_complete() { - if identity_challenge_response.update_p_key.is_some() - && identity_challenge_response.identity_nonce.is_some() - && identity_challenge_response - .identity_challenge_signature - .is_some() - { - println!("Identity challenge has been presented. Await response."); - return Ok(()); - } - println!("Identity challenge incomplete."); - return Ok(()); + current_state = CurrentCRState::IdentityCRInitiated; + println!("{}", get_status_message(¤t_state)); + + // Identity challenge + if self.identity_challenge_response.is_none() + || !self + .identity_challenge_response + .as_ref() + .unwrap() + .challenge_complete() + { + return Ok(current_state); } - println!("Identity challenge-response complete."); - - // Content challenge-response - let content_challenge_response = self.content_challenge_response.as_ref().unwrap(); - if !content_challenge_response.is_complete() { - if content_challenge_response.content_nonce.is_some() - && content_challenge_response - .content_challenge_signature - .is_some() - { - println!("Content challenge has been presented. Await response."); - return Ok(()); - } - println!("Content challenge incomplete."); - return Ok(()); + current_state = CurrentCRState::IdentityChallengeComplete; + println!("{}", get_status_message(¤t_state)); + + // Identity response + if !self + .identity_challenge_response + .as_ref() + .unwrap() + .response_complete() + { + return Ok(current_state); } - println!("Content challenge-response complete."); - Ok(()) - } -} + current_state = CurrentCRState::IdentityResponseComplete; -impl IsComplete for CRState { - fn is_complete(&self) -> bool { - if self.initiation.is_some() - && self.identity_challenge_response.is_some() - && self.content_challenge_response.is_some() + // Content CR initation + if self.content_cr_initiation.is_none() + || !self.content_cr_initiation.as_ref().unwrap().is_complete() { - return true; + return Ok(current_state); } - return false; + current_state = CurrentCRState::ContentCRInitiated; + + // Content challenge + if self.content_challenge_response.is_none() + || !self + .content_challenge_response + .as_ref() + .unwrap() + .challenge_complete() + { + return Ok(current_state); + } + current_state = CurrentCRState::ContentChallengeComplete; + + // Content response + if !self + .content_challenge_response + .as_ref() + .unwrap() + .response_complete() + { + return Ok(current_state); + } + + return Ok(current_state); } } impl ElementwiseSerializeDeserialize for CRState { fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { - if let Some(initiation) = &self.initiation { - initiation.elementwise_serialize(path)?; + if let Some(identity_initiation) = &self.identity_cr_initiation { + identity_initiation.elementwise_serialize(path)?; } if let Some(identity_challenge_response) = &self.identity_challenge_response { identity_challenge_response.elementwise_serialize(path)?; } + if let Some(content_cr_initiation) = &self.content_cr_initiation { + content_cr_initiation.elementwise_serialize(path)?; + } if let Some(content_challenge_response) = &self.content_challenge_response { content_challenge_response.elementwise_serialize(path)?; } @@ -329,9 +392,10 @@ impl ElementwiseSerializeDeserialize for CRState { mut self, path: &PathBuf, ) -> Result, TrustchainCRError> { - self.initiation = CRInitiation::new().elementwise_deserialize(path)?; + self.identity_cr_initiation = IdentityCRInitiation::new().elementwise_deserialize(path)?; self.identity_challenge_response = CRIdentityChallenge::new().elementwise_deserialize(path)?; + self.content_cr_initiation = ContentCRInitiation::new().elementwise_deserialize(path)?; self.content_challenge_response = CRContentChallenge::new().elementwise_deserialize(path)?; Ok(Some(self)) @@ -347,30 +411,25 @@ struct RequesterDetails { #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, IsEmpty)] -struct CRInitiation { +struct IdentityCRInitiation { temp_p_key: Option, requester_details: Option, } -impl CRInitiation { +impl IdentityCRInitiation { fn new() -> Self { Self { temp_p_key: None, requester_details: None, } } -} -impl IsComplete for CRInitiation { fn is_complete(&self) -> bool { - if self.temp_p_key.is_some() && self.requester_details.is_some() { - return true; - } - return false; + return self.temp_p_key.is_some() && self.requester_details.is_some(); } } -impl ElementwiseSerializeDeserialize for CRInitiation { +impl ElementwiseSerializeDeserialize for IdentityCRInitiation { // fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { // // let file_path = path.join("temp_p_key.json"); // // let data: &str = &to_json(&self.temp_p_key).unwrap(); @@ -403,7 +462,7 @@ impl ElementwiseSerializeDeserialize for CRInitiation { fn elementwise_deserialize( mut self, path: &PathBuf, - ) -> Result, TrustchainCRError> { + ) -> Result, TrustchainCRError> { let temp_p_key_path = path.join("temp_p_key.json"); self.temp_p_key = match File::open(&temp_p_key_path) { Ok(file) => { @@ -434,6 +493,60 @@ impl ElementwiseSerializeDeserialize for CRInitiation { } } +#[derive(Debug, Serialize, Deserialize, Clone, IsEmpty)] +struct ContentCRInitiation { + temp_p_key: Option, + requester_did: Option, +} + +impl ContentCRInitiation { + fn new() -> Self { + Self { + temp_p_key: None, + requester_did: None, + } + } + + fn is_complete(&self) -> bool { + return self.temp_p_key.is_some() && self.requester_did.is_some(); + } +} + +impl ElementwiseSerializeDeserialize for ContentCRInitiation { + fn elementwise_deserialize( + mut self, + path: &PathBuf, + ) -> Result, TrustchainCRError> { + let temp_p_key_path = path.join("temp_p_key.json"); + self.temp_p_key = match File::open(&temp_p_key_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; + + let requester_details_path = path.join("requester_did.json"); + self.requester_did = match File::open(&requester_details_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; + + if self.temp_p_key.is_none() && self.requester_did.is_none() { + return Ok(None); + } + + Ok(Some(self)) + } +} + #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, IsEmpty)] struct CRIdentityChallenge { @@ -452,54 +565,19 @@ impl CRIdentityChallenge { identity_response_signature: None, } } -} -impl IsComplete for CRIdentityChallenge { - fn is_complete(&self) -> bool { - if self.update_p_key.is_some() + fn challenge_complete(&self) -> bool { + return self.update_p_key.is_some() && self.identity_nonce.is_some() - && self.identity_challenge_signature.is_some() - && self.identity_response_signature.is_some() - { - return true; - } - return false; + && self.identity_challenge_signature.is_some(); + } + + fn response_complete(&self) -> bool { + return self.challenge_complete() && self.identity_response_signature.is_some(); } } -// todo: add path to serialise/deserialise functions? impl ElementwiseSerializeDeserialize for CRIdentityChallenge { - // fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { - // if let Some(update_p_key) = &self.update_p_key { - // let file_path = path.join("update_p_key.json"); - // let data: &str = &to_json(update_p_key).unwrap(); - // if !file_path.exists() { - // self.save_to_file(&file_path, data); - // } - // } - // if let Some(identity_nonce) = &self.identity_nonce { - // let file_path = path.join("identity_nonce.json"); - // let data: &str = &to_json(identity_nonce).unwrap(); - // if !file_path.exists() { - // self.save_to_file(&file_path, data); - // } - // } - // if let Some(identity_challenge_signature) = &self.identity_challenge_signature { - // let file_path = path.join("identity_challenge_signature.json"); - // let data: &str = &to_json(identity_challenge_signature).unwrap(); - // if !file_path.exists() { - // self.save_to_file(&file_path, data); - // } - // } - // if let Some(identity_response_signature) = &self.identity_response_signature { - // let file_path = path.join("identity_response_signature.json"); - // let data: &str = &to_json(identity_response_signature).unwrap(); - // if !file_path.exists() { - // self.save_to_file(&file_path, data); - // } - // } - // Ok(()) - // } fn elementwise_deserialize( mut self, path: &PathBuf, @@ -600,17 +678,13 @@ impl CRContentChallenge { content_response_signature: None, } } -} -impl IsComplete for CRContentChallenge { - fn is_complete(&self) -> bool { - if self.content_nonce.is_some() - && self.content_challenge_signature.is_some() - && self.content_response_signature.is_some() - { - return true; - } - return false; + fn challenge_complete(&self) -> bool { + return self.content_nonce.is_some() && self.content_challenge_signature.is_some(); + } + + fn response_complete(&self) -> bool { + return self.challenge_complete() && self.content_response_signature.is_some(); } } @@ -749,13 +823,12 @@ fn extract_key_ids_and_jwk(document: &Document) -> Result, for vm in vms { match vm { VerificationMethod::Map(vm_map) => { - // let id = vm_map.id.clone(); // TODo: use JWK::thumbprint() instead let key = vm_map .get_jwk() .map_err(|_| TrustchainCRError::MissingJWK)?; let id = key .thumbprint() - .map_err(|_| TrustchainCRError::MissingJWK)?; //TODO: different error variant? + .map_err(|_| TrustchainCRError::MissingJWK)?; let key_jose = ssi_to_josekit_jwk(&key).map_err(|err| TrustchainCRError::Serde(err))?; my_map.insert(id, key_jose); @@ -767,62 +840,13 @@ fn extract_key_ids_and_jwk(document: &Document) -> Result, Ok(my_map) } -// fn check_cr_state(path: &PathBuf, key_id: &str, state: CRState) -> Result<(), TrustchainCRError> { -// if !path.exists() { -// return Err(TrustchainCRError::CRPathNotFound); -// } -// let cr_state = CRState::new().elementwise_deserialize(path).unwrap().unwrap(); - -// // The directory exists, but there are no json files -// if cr_state.is_empty() { -// println!("No records found for this challenge-response identifier. The challenge-response process has not been initiated yet. ") -// return Ok(()); -// } - -// // CR complete -// if cr_state.is_complete(){ -// println!("Challenge-response complete."); -// return Ok(()); -// } - -// // CR initation -// if !cr_state.initiation.unwrap().is_complete() { -// println!("Initiation incomplete."); -// return Ok(()); -// } -// println!("Initiation complete."); - -// // Identity challenge-response -// if !cr_state.identity_challenge_response.unwrap().is_complete() { -// if cr_state.identity_challenge_response.unwrap().update_p_key.is_some() && cr_state.identity_challenge_response.unwrap().identity_nonce.is_some() && cr_state.identity_challenge_response.unwrap().identity_challenge_signature.is_some() { -// println!("Identity challenge has been presented. Await response."); -// return Ok(()) -// } -// println!("Identity challenge incomplete."); -// return Ok(()) -// } -// println!("Identity challenge-response complete."); - -// // Content challenge-response -// if !cr_state.content_challenge_response.unwrap().is_complete() { -// if cr_state.content_challenge_response.unwrap().content_nonce.is_some() && cr_state.content_challenge_response.unwrap().content_challenge_signature.is_some() { -// println!("Content challenge has been presented. Await response."); -// return Ok(()) -// } -// println!("Content challenge incomplete."); -// return Ok(()) -// } -// println!("Content challenge-response complete."); -// Ok(()) -// } - #[cfg(test)] mod tests { - use std::fs::create_dir; - use tempfile::tempdir; + use std::str; + use crate::data::{ TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS, TEST_SIGNING_KEY_1, TEST_SIGNING_KEY_2, TEST_TEMP_KEY, TEST_UPDATE_KEY, TEST_UPSTREAM_KEY, @@ -839,7 +863,7 @@ mod tests { let temp_p_key = temp_s_key.to_public_key().unwrap(); // generate challenge - let request_initiation = CRInitiation { + let request_initiation = IdentityCRInitiation { temp_p_key: Some(temp_p_key.clone()), requester_details: Some(RequesterDetails { requester_org: String::from("My Org"), @@ -989,7 +1013,6 @@ mod tests { ) .unwrap(); - // todo: replace with function to read in private keys let downstream_s_key_1: Jwk = serde_json::from_str(TEST_SIGNING_KEY_1).unwrap(); let downstream_s_key_2: Jwk = serde_json::from_str(TEST_SIGNING_KEY_2).unwrap(); let downstream_key_id_1 = josekit_to_ssi_jwk(&downstream_s_key_1) @@ -1052,7 +1075,7 @@ mod tests { fn test_elementwise_serialize() { // ==========| Identity CR | ============== let temp_s_key: Jwk = serde_json::from_str(TEST_TEMP_KEY).unwrap(); - let initiation = CRInitiation { + let initiation = IdentityCRInitiation { temp_p_key: Some(temp_s_key.to_public_key().unwrap()), requester_details: Some(RequesterDetails { requester_org: String::from("My Org"), @@ -1066,10 +1089,13 @@ mod tests { identity_nonce: Some(Nonce::new()), identity_challenge_signature: Some(String::from("some challenge signature string")), identity_response_signature: Some(String::from("some response signature string")), - // identity_response_signature: None, }; // ==========| Content CR | ============== + let content_initiation = ContentCRInitiation { + temp_p_key: Some(temp_s_key.to_public_key().unwrap()), + requester_did: Some("did:example:123456789abcdefghi".to_string()), + }; // get signing keys for DE from did document let doc: Document = serde_json::from_str(TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS).unwrap(); let test_keys_map = extract_key_ids_and_jwk(&doc).unwrap(); @@ -1094,8 +1120,9 @@ mod tests { // ==========| CR state | ============== let cr_state = CRState { - initiation: Some(initiation), + identity_cr_initiation: Some(initiation), identity_challenge_response: Some(identity_challenge), + content_cr_initiation: Some(content_initiation), content_challenge_response: Some(content_challenge_response), }; // write to file @@ -1105,6 +1132,7 @@ mod tests { // try to write to file again let result = cr_state.elementwise_serialize(&path); + assert_eq!(result.is_ok(), true); } #[test] @@ -1113,14 +1141,14 @@ mod tests { let challenge_state = CRState::new(); // Test case 1: some files exist and can be deserialised - let initiatiation = CRInitiation { + let identity_initiatiation = IdentityCRInitiation { temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), requester_details: Some(RequesterDetails { requester_org: String::from("My Org"), operator_name: String::from("John Doe"), }), }; - let _ = initiatiation.elementwise_serialize(&path); + let _ = identity_initiatiation.elementwise_serialize(&path); let identity_challenge = CRIdentityChallenge { update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), identity_nonce: Some(Nonce::new()), @@ -1129,6 +1157,12 @@ mod tests { }; let _ = identity_challenge.elementwise_serialize(&path); + let content_cr_initiation = ContentCRInitiation { + temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), + requester_did: Some("did:example:123456789abcdefghi".to_string()), + }; + let _ = content_cr_initiation.elementwise_serialize(&path); + let result = challenge_state.elementwise_deserialize(&path); assert!(result.is_ok()); let challenge_state = result.unwrap().unwrap(); @@ -1136,8 +1170,9 @@ mod tests { "Challenge state deserialized from files: {:?}", challenge_state ); - assert!(challenge_state.initiation.is_some()); + assert!(challenge_state.identity_cr_initiation.is_some()); assert!(challenge_state.identity_challenge_response.is_some()); + assert!(challenge_state.content_cr_initiation.is_some()); assert!(challenge_state.content_challenge_response.is_none()); // Test case 2: one file cannot be deserialized @@ -1150,7 +1185,7 @@ mod tests { #[test] fn test_elementwise_deserialize_initiation() { - let cr_initiation = CRInitiation::new(); + let cr_initiation = IdentityCRInitiation::new(); let temp_path = tempdir().unwrap().into_path(); // Test case 1: None of the json files exist @@ -1160,7 +1195,7 @@ mod tests { assert!(initiation.is_none()); // Test case 2: Only one json file exists and can be deserialized - let cr_initiation = CRInitiation::new(); + let cr_initiation = IdentityCRInitiation::new(); let temp_p_key_path = temp_path.join("temp_p_key.json"); let temp_p_key_file = File::create(&temp_p_key_path).unwrap(); let temp_p_key: Jwk = serde_json::from_str(TEST_TEMP_KEY).unwrap(); @@ -1173,7 +1208,7 @@ mod tests { assert!(initiation.requester_details.is_none()); // Test case 3: Both json files exist and can be deserialized - let cr_initiation = CRInitiation::new(); + let cr_initiation = IdentityCRInitiation::new(); let requester_details_path = temp_path.join("requester_details.json"); let requester_details_file = File::create(&requester_details_path).unwrap(); let requester_details = RequesterDetails { @@ -1189,7 +1224,7 @@ mod tests { // Test case 4: Both json files exist but one is invalid json and cannot be // deserialized - let cr_initiation = CRInitiation::new(); + let cr_initiation = IdentityCRInitiation::new(); // override temp key with invalid key let temp_p_key_file = File::create(&temp_p_key_path).unwrap(); serde_json::to_writer(temp_p_key_file, "this is not valid json").unwrap(); @@ -1265,17 +1300,76 @@ mod tests { } #[test] - fn test_cr_status_check() { - let temp_path = tempdir().unwrap().into_path(); - let key_id = "test_key_id"; - let cr_path = temp_path.join(key_id); - let _ = create_dir(&cr_path); + fn test_check_cr_status() { + let mut cr_state = CRState::new(); + // Test case 1: CR State is empty + let result = cr_state.check_cr_status().unwrap(); + assert_eq!(result, CurrentCRState::NotStarted); + + // Test case 2: some, but not all, initation information exists + cr_state.identity_cr_initiation = Some(IdentityCRInitiation { + temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), + requester_details: None, + }); + let result = cr_state.check_cr_status(); + assert_eq!(result.unwrap(), CurrentCRState::NotStarted); - // Test case 1: no files exist - let cr_state = CRState::new() - .elementwise_deserialize(&cr_path) - .unwrap() - .unwrap(); + // Test case 3: identity initiation completed, identity challenge presented + cr_state.identity_cr_initiation = Some(IdentityCRInitiation { + temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), + requester_details: Some(RequesterDetails { + requester_org: String::from("My Org"), + operator_name: String::from("John Doe"), + }), + }); + cr_state.identity_challenge_response = Some(CRIdentityChallenge { + update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), + identity_nonce: Some(Nonce::new()), + identity_challenge_signature: Some(String::from("some challenge signature string")), + identity_response_signature: None, + }); + let result = cr_state.check_cr_status(); + assert_eq!(result.unwrap(), CurrentCRState::IdentityChallengeComplete); + + // Test case 4: Identity challenge response complete, content challenge initiated + cr_state.identity_challenge_response = Some(CRIdentityChallenge { + update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), + identity_nonce: Some(Nonce::new()), + identity_challenge_signature: Some(String::from("some challenge signature string")), + identity_response_signature: Some(String::from("some response signature string")), + }); + cr_state.content_cr_initiation = { + Some(ContentCRInitiation { + temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), + requester_did: Some("did:example:123456789abcdefghi".to_string()), + }) + }; + let result = cr_state.check_cr_status(); + assert_eq!(result.unwrap(), CurrentCRState::ContentCRInitiated); + + // Test case 5: Content challenge-response complete + cr_state.content_challenge_response = Some(CRContentChallenge { + content_nonce: Some(HashMap::new()), + content_challenge_signature: Some(String::from( + "some content challenge signature string", + )), + content_response_signature: Some(String::from( + "some content response signature string", + )), + }); + let result = cr_state.check_cr_status(); + assert_eq!(result.unwrap(), CurrentCRState::ContentResponseComplete); + } + #[test] + fn test_check_cr_status_inconsistent_order() { + let mut cr_state = CRState::new(); + cr_state.identity_challenge_response = Some(CRIdentityChallenge { + update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), + identity_nonce: Some(Nonce::new()), + identity_challenge_signature: Some(String::from("some challenge signature string")), + identity_response_signature: Some(String::from("some response signature string")), + }); let result = cr_state.check_cr_status(); + assert_eq!(result.unwrap(), CurrentCRState::NotStarted); } } From f1f6789b9a24bff48682d67fd9b496fd16cd9fde Mon Sep 17 00:00:00 2001 From: pwochner Date: Mon, 16 Oct 2023 12:28:39 +0100 Subject: [PATCH 24/86] Remove unused code. Improve messages for CR status check. --- trustchain-http/src/challenge_response.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/trustchain-http/src/challenge_response.rs b/trustchain-http/src/challenge_response.rs index cc581807..659ab0d3 100644 --- a/trustchain-http/src/challenge_response.rs +++ b/trustchain-http/src/challenge_response.rs @@ -73,7 +73,7 @@ enum CurrentCRState { fn get_status_message(current_state: &CurrentCRState) -> String { match current_state { CurrentCRState::NotStarted => { - return String::from("No records found for this challenge-response identifier. The challenge-response process has not been initiated yet."); + return String::from("No records found for this challenge-response identifier. \nThe challenge-response process has not been initiated yet."); } CurrentCRState::IdentityCRInitiated => { return String::from("Identity challenge-response initiated. Await response."); @@ -229,7 +229,6 @@ impl DecryptVerify for Entity {} #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Nonce(String); -// impl Nonce { impl Nonce { pub fn new() -> Self { Self( @@ -291,6 +290,8 @@ impl CRState { } fn check_cr_status(&self) -> Result { + println!("Checking current challenge-response status..."); + println!(" "); let mut current_state = CurrentCRState::NotStarted; if self.is_empty() { println!("{}", get_status_message(¤t_state)); From 45ebe843d639448d40417cc9db21d5bf161fdcb6 Mon Sep 17 00:00:00 2001 From: pwochner Date: Tue, 17 Oct 2023 09:37:52 +0100 Subject: [PATCH 25/86] Add docstrings. --- trustchain-http/src/challenge_response.rs | 40 +++++++++++++++-------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/trustchain-http/src/challenge_response.rs b/trustchain-http/src/challenge_response.rs index 659ab0d3..94c2d386 100644 --- a/trustchain-http/src/challenge_response.rs +++ b/trustchain-http/src/challenge_response.rs @@ -70,6 +70,7 @@ enum CurrentCRState { ContentResponseComplete, } +/// Returns message that corresponds to the current state of the challenge-response process. fn get_status_message(current_state: &CurrentCRState) -> String { match current_state { CurrentCRState::NotStarted => { @@ -107,6 +108,7 @@ trait ElementwiseSerializeDeserialize where Self: Serialize, { + /// Serialize each field of the struct to a file. fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { let serialized = serde_json::to_value(&self).map_err(|_| TrustchainCRError::FailedToSave)?; @@ -122,11 +124,11 @@ where } Ok(()) } - + /// Deserializes each field of the struct from a file. fn elementwise_deserialize(self, path: &PathBuf) -> Result, TrustchainCRError> where Self: Sized; - + /// Save data to file. If file already exists, do nothing. fn save_to_file(&self, path: &PathBuf, data: &str) -> Result<(), TrustchainCRError> { if path.exists() { println!("File already exists: {:?}", path); @@ -162,6 +164,7 @@ where /// Interface for signing and then encrypting data. pub trait SignEncrypt { + /// Cryptographically signs a payload with a secret key. fn sign(&self, payload: &JwtPayload, secret_key: &Jwk) -> Result { let mut header = JwsHeader::new(); header.set_token_type("JWT"); @@ -171,6 +174,7 @@ pub trait SignEncrypt { } /// `JWTPayload` is a wrapped [`Map`](https://docs.rs/serde_json/1.0.79/serde_json/struct.Map.html) /// of claims. + /// Cryptographically encrypts a payload with a public key. fn encrypt(&self, payload: &JwtPayload, public_key: &Jwk) -> Result { let mut header = JweHeader::new(); header.set_token_type("JWT"); @@ -181,26 +185,28 @@ pub trait SignEncrypt { let encrypted_jwt = jwt::encode_with_encrypter(payload, &header, &encrypter)?; Ok(encrypted_jwt) } - /// Combined sign and encryption + /// Wrapper function for signing and encrypting a payload. fn sign_and_encrypt_claim( &self, payload: &JwtPayload, secret_key: &Jwk, public_key: &Jwk, ) -> Result { - let signed_encoded_payload = self.sign(payload, secret_key)?; + let signed_payload = self.sign(payload, secret_key)?; let mut claims = JwtPayload::new(); - claims.set_claim("claim", Some(Value::from(signed_encoded_payload)))?; + claims.set_claim("claim", Some(Value::from(signed_payload)))?; self.encrypt(&claims, &public_key) } } /// Interface for decrypting and then verifying data. trait DecryptVerify { + /// Decrypts a payload with a secret key. fn decrypt(&self, value: &Value, secret_key: &Jwk) -> Result { let decrypter = ECDH_ES.decrypter_from_jwk(&secret_key)?; let (payload, _) = jwt::decode_with_decrypter(value.as_str().unwrap(), &decrypter)?; Ok(payload) } + /// Wrapper function that combines decrypting a payload with a secret key and then verifying it with a public key. fn decrypt_and_verify( &self, input: String, @@ -277,7 +283,7 @@ impl CRState { content_challenge_response: None, } } - + /// Returns true if all fields have a non-null value. fn is_complete(&self) -> bool { if self.identity_cr_initiation.is_some() && self.identity_challenge_response.is_some() @@ -288,7 +294,7 @@ impl CRState { } return false; } - + /// Determines current status of the challenge response process and accordingly prints messages to the console. fn check_cr_status(&self) -> Result { println!("Checking current challenge-response status..."); println!(" "); @@ -374,6 +380,7 @@ impl CRState { } impl ElementwiseSerializeDeserialize for CRState { + /// Serialize each field of the struct to a file. Fields with null values are ignored. fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { if let Some(identity_initiation) = &self.identity_cr_initiation { identity_initiation.elementwise_serialize(path)?; @@ -389,6 +396,7 @@ impl ElementwiseSerializeDeserialize for CRState { } Ok(()) } + /// Deserialize each field of the struct from a file. All fields are optional. fn elementwise_deserialize( mut self, path: &PathBuf, @@ -460,6 +468,7 @@ impl ElementwiseSerializeDeserialize for IdentityCRInitiation { // Ok(()) // } + /// Deserialize each field of the struct from a file. Fields are optional. If no files are found, return None. fn elementwise_deserialize( mut self, path: &PathBuf, @@ -514,6 +523,7 @@ impl ContentCRInitiation { } impl ElementwiseSerializeDeserialize for ContentCRInitiation { + /// Deserialize each field of the struct from a file. Fields are optional. If no files are found, return None. fn elementwise_deserialize( mut self, path: &PathBuf, @@ -566,19 +576,20 @@ impl CRIdentityChallenge { identity_response_signature: None, } } - + /// Returns true if all fields required for the challenge have a non-null value. fn challenge_complete(&self) -> bool { return self.update_p_key.is_some() && self.identity_nonce.is_some() && self.identity_challenge_signature.is_some(); } - + /// Returns true if all fields of the challenge-response have a non-null value. fn response_complete(&self) -> bool { return self.challenge_complete() && self.identity_response_signature.is_some(); } } impl ElementwiseSerializeDeserialize for CRIdentityChallenge { + /// Deserialize each field of the struct from a file. Fields are optional. If no files are found, return None. fn elementwise_deserialize( mut self, path: &PathBuf, @@ -679,11 +690,11 @@ impl CRContentChallenge { content_response_signature: None, } } - + /// Returns true if all fields required for the challenge have a non-null value. fn challenge_complete(&self) -> bool { return self.content_nonce.is_some() && self.content_challenge_signature.is_some(); } - + /// Returns true if all fields required for the challenge-response have a non-null value. fn response_complete(&self) -> bool { return self.challenge_complete() && self.content_response_signature.is_some(); } @@ -714,6 +725,8 @@ impl ElementwiseSerializeDeserialize for CRContentChallenge { // } // Ok(()) // } + + /// Deserialize each field of the struct from a file. Fields are optional. If no files are found, return None. fn elementwise_deserialize( mut self, path: &PathBuf, @@ -793,19 +806,20 @@ impl TryFrom<&Nonce> for JwtPayload { } } -// make a try_from instead +/// Converts key from josekit Jwk into ssi JWK fn josekit_to_ssi_jwk(key: &Jwk) -> Result { let key_as_str: &str = &serde_json::to_string(&key).unwrap(); let ssi_key: JWK = serde_json::from_str(key_as_str).unwrap(); Ok(ssi_key) } - +/// Converts key from ssi JWK into josekit Jwk fn ssi_to_josekit_jwk(key: &JWK) -> Result { let key_as_str: &str = &serde_json::to_string(&key).unwrap(); let ssi_key: Jwk = serde_json::from_str(key_as_str).unwrap(); Ok(ssi_key) } +/// Extracts public keys contained in DID document fn extract_key_ids_and_jwk(document: &Document) -> Result, TrustchainCRError> { let mut my_map = HashMap::::new(); if let Some(vms) = &document.verification_method { From e7d19e296e882c7d4e496239eda83ad9228be804 Mon Sep 17 00:00:00 2001 From: pwochner Date: Thu, 2 Nov 2023 16:07:46 +0000 Subject: [PATCH 26/86] Add initiate identity CR to Trustchain cli. --- trustchain-cli/Cargo.toml | 2 + trustchain-cli/src/bin/main.rs | 61 ++++++++- trustchain-http/src/attestation_utils.rs | 19 +++ trustchain-http/src/challenge_response.rs | 143 +++++++++++++++++++++- trustchain-http/src/encryption.rs | 56 ++++----- trustchain-http/src/lib.rs | 1 + 6 files changed, 252 insertions(+), 30 deletions(-) create mode 100644 trustchain-http/src/attestation_utils.rs diff --git a/trustchain-cli/Cargo.toml b/trustchain-cli/Cargo.toml index 9f352bd7..e9746d5f 100644 --- a/trustchain-cli/Cargo.toml +++ b/trustchain-cli/Cargo.toml @@ -12,6 +12,8 @@ path = "src/bin/main.rs" trustchain-core = { path = "../trustchain-core" } trustchain-ion = { path = "../trustchain-ion" } trustchain-api = { path = "../trustchain-api" } +trustchain-http = { path = "../trustchain-http" } + clap = { version = "4.0.32", features=["derive", "cargo"] } did-ion = {git="https://github.com/alan-turing-institute/ssi.git", branch="modify-encode-sign-jwt"} diff --git a/trustchain-cli/src/bin/main.rs b/trustchain-cli/src/bin/main.rs index 1f8c7e4f..58d33c10 100644 --- a/trustchain-cli/src/bin/main.rs +++ b/trustchain-cli/src/bin/main.rs @@ -1,10 +1,11 @@ //! Trustchain CLI binary use clap::{arg, ArgAction, Command}; +use core::panic; use serde_json::to_string_pretty; use ssi::{jsonld::ContextLoader, ldp::LinkedDataDocument, vc::Credential}; use std::{ fs::File, - io::{stdin, BufReader}, + io::{self, stdin, BufReader}, }; use trustchain_api::{ api::{TrustchainDIDAPI, TrustchainVCAPI}, @@ -12,6 +13,7 @@ use trustchain_api::{ }; use trustchain_cli::config::cli_config; use trustchain_core::{vc::CredentialError, verifier::Verifier}; +use trustchain_http::challenge_response::initiate_identity_challenge; use trustchain_ion::{ attest::attest_operation, create::create_operation, get_ion_resolver, verifier::IONVerifier, }; @@ -79,6 +81,26 @@ fn cli() -> Command { .arg(arg!(-t --root_event_time ).required(false)), ), ) + .subcommand( // Pam: change this + Command::new("cr") + // .about("Challenge-response functionality: initiate, present, respond.") + .about("Challenge-response functionality for identity challenge-response and content challenge-response.") + .subcommand_required(true) + .arg_required_else_help(true) + .allow_external_subcommands(true) + .subcommand( + Command::new("identity") + .about("Identity challenge-response functionality: initiate, present, respond.") + .arg(arg!(-v - -verbose).action(ArgAction::SetTrue)) + .arg(arg!(-f --file_path ).required(false)) + .subcommand( + Command::new("initiate") + .about("Initiates a new identity challenge-response process.") + .arg(arg!(-v - -verbose).action(ArgAction::Count)) + .arg(arg!(-d --did ).required(true)) + ) + ) + ) } #[tokio::main] @@ -267,6 +289,43 @@ async fn main() -> Result<(), Box> { _ => panic!("Unrecognised VC subcommand."), } } + Some(("cr", sub_matches)) => match sub_matches.subcommand() { + Some(("identity", sub_matches)) => match sub_matches.subcommand() { + Some(("initiate", sub_matches)) => { + // resolve DID and extract endpoint + let did = sub_matches.get_one::("did").unwrap(); + let (_, doc, _) = TrustchainAPI::resolve(did, resolver).await?; + // let endpoints = doc.unwrap().get_endpoints().unwrap(); // TODO: this is a vec => which endpoint? + let services = doc.unwrap().service; + + // user promt for org name and operator name + println!("Please enter your organisation name: "); + let mut org_name = String::new(); + io::stdin() + .read_line(&mut org_name) + .expect("Failed to read line"); + + let mut op_name = String::new(); + println!("Please enter your operator name: "); + io::stdin() + .read_line(&mut op_name) + .expect("Failed to read line"); + + println!("Organisation name: {}", org_name); + println!("Operator name: {}", op_name); + // initiate identity challenge + initiate_identity_challenge( + org_name.trim().to_string(), + op_name.trim().to_string(), + &services.unwrap(), + ) + .await?; + } + _ => panic!("Unrecognised CR subcommand."), + }, + _ => panic!("Unrecognised CR subcommand."), + }, + _ => panic!("Unrecognised subcommand."), } Ok(()) diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs new file mode 100644 index 00000000..c3fad03f --- /dev/null +++ b/trustchain-http/src/attestation_utils.rs @@ -0,0 +1,19 @@ +use std::path::{Path, PathBuf}; + +use ssi::jwk::JWK; +use trustchain_core::TRUSTCHAIN_DATA; + +use crate::challenge_response::TrustchainCRError; + +/// Returns unique path name for a specific attestation request derived from public key for the interaction. +pub fn attestation_request_path(key: &JWK) -> Result { + // Root path in TRUSTCHAIN_DATA + let path: String = + std::env::var(TRUSTCHAIN_DATA).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; + let key_id = key + .thumbprint() + .map_err(|_| TrustchainCRError::MissingJWK)?; // Use hash of temp_pub_key + Ok(Path::new(path.as_str()) + .join("attestation_requests") + .join(key_id)) +} diff --git a/trustchain-http/src/challenge_response.rs b/trustchain-http/src/challenge_response.rs index 94c2d386..731c2163 100644 --- a/trustchain-http/src/challenge_response.rs +++ b/trustchain-http/src/challenge_response.rs @@ -1,3 +1,4 @@ +use crate::attestation_utils::attestation_request_path; use josekit::jwe::{JweHeader, ECDH_ES}; use josekit::jwk::Jwk; use josekit::jws::{JwsHeader, ES256K}; @@ -8,12 +9,14 @@ use rand::{distributions::Alphanumeric, Rng}; use serde::{Deserialize, Serialize}; use serde_json::{to_string_pretty as to_json, Value}; use serde_with::skip_serializing_none; -use ssi::did::{Document, VerificationMethod}; +use ssi::did::{Document, Service, ServiceEndpoint, VerificationMethod}; use ssi::jwk::JWK; +use ssi::one_or_many::OneOrMany; use std::collections::HashMap; use std::fs::OpenOptions; use std::fs::{self, File}; use std::io::{BufWriter, Write}; +use trustchain_core::utils::generate_key; use is_empty::IsEmpty; use std::path::PathBuf; @@ -57,6 +60,21 @@ pub enum TrustchainCRError { /// Path for CR does not exist. #[error("Path does not exist. No challenge-response record for this temporary key id.")] CRPathNotFound, + /// Failed to generate key. + #[error("Failed to generate key.")] + FailedToGenerateKey, + /// Reqwest error. + #[error("Network request failed.")] + Reqwest(reqwest::Error), + /// Invalid service endpoint. + #[error("Invalid service endpoint.")] + InvalidServiceEndpoint, + /// CR initiation failed + #[error("Failed to initiate challenge-response.")] + FailedToInitiateCR, + /// Failed attestation request + #[error("Failed attestation request.")] + FailedAttestationRequest, } #[derive(Debug, PartialEq)] @@ -855,9 +873,84 @@ fn extract_key_ids_and_jwk(document: &Document) -> Result, Ok(my_map) } +/// Initiates the identity challenge-response process by sending a POST request to the upstream endpoint. +/// +/// This function generates a temporary key to use as an identifier throughout the challenge-response process. +/// It prompts the user to provide the organization name and operator name, which are included in the POST request +/// to the endpoint specified in the upstream's DID document. +pub async fn initiate_identity_challenge( + org_name: String, + op_name: String, + services: &Vec, +) -> Result<(), TrustchainCRError> { + // generate temp key + let temp_s_key_ssi = generate_key(); + let temp_s_key = + ssi_to_josekit_jwk(&temp_s_key_ssi).map_err(|_| TrustchainCRError::FailedToGenerateKey)?; + + // make identity_cr_initiation struct + let requester = RequesterDetails { + requester_org: org_name, + operator_name: op_name, + }; + let identity_cr_initiation = IdentityCRInitiation { + temp_p_key: temp_s_key.to_public_key().ok(), + requester_details: Some(requester), + }; + // extract URI from service endpoint + println!("Services: {:?}", services); + let uri = matching_endpoint(services, "Trustchain").unwrap(); // this is just to make current example work + // let uri = matching_endpoint(services, "identity-cr").unwrap(); // TODO: use this one once we have example published + + // make POST request to endpoint + let client = reqwest::Client::new(); + let result = client + .post(uri) + .json(&identity_cr_initiation) + .send() + .await + .map_err(|err| TrustchainCRError::Reqwest(err))?; + + if result.status() != 200 { + println!("Status code: {}", result.status()); + return Err(TrustchainCRError::FailedToInitiateCR); + } + // create new directory + let directory = attestation_request_path(&temp_s_key_ssi.to_public())?; + std::fs::create_dir_all(&directory).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; + + // serialise identity_cr_initiation + identity_cr_initiation.elementwise_serialize(&directory)?; + println!("Successfully initiated attestation request."); + println!("You will receive more information on the challenge-response process via alternative communication channel."); + Ok(()) +} + +/// Returns endpoint that contains the given fragment from the given list of service endpoints. +/// Throws error if no or more than one matching endpoint is found. +fn matching_endpoint(services: &Vec, fragment: &str) -> Result { + let mut endpoints = Vec::new(); + for service in services { + if service.id.contains(fragment) { + match &service.service_endpoint { + Some(OneOrMany::One(ServiceEndpoint::URI(uri))) => { + endpoints.push(uri.to_string()); + } + + _ => return Err(TrustchainCRError::InvalidServiceEndpoint), + } + } + } + if endpoints.len() != 1 { + return Err(TrustchainCRError::InvalidServiceEndpoint); + } + return Ok(endpoints[0].clone()); +} + #[cfg(test)] mod tests { + // use ssi::vc::URI; use tempfile::tempdir; use std::str; @@ -1387,4 +1480,52 @@ mod tests { let result = cr_state.check_cr_status(); assert_eq!(result.unwrap(), CurrentCRState::NotStarted); } + + #[test] + fn test_matching_endpoint() { + let services = vec![ + Service { + id: String::from("did:example:123456789abcdefghi#service-1"), + service_endpoint: Some(OneOrMany::One(ServiceEndpoint::URI(String::from( + "https://example.com/endpoint-1", + )))), + type_: ssi::one_or_many::OneOrMany::One("Service1".to_string()), + property_set: None, + }, + Service { + id: String::from("did:example:123456789abcdefghi#service-2"), + service_endpoint: Some(OneOrMany::One(ServiceEndpoint::URI(String::from( + "https://example.com/endpoint-2", + )))), + type_: ssi::one_or_many::OneOrMany::One("Service2".to_string()), + property_set: None, + }, + ]; + let result = matching_endpoint(&services, "service-1"); + assert_eq!(result.unwrap(), "https://example.com/endpoint-1"); + } + + #[test] + fn test_matching_endpoint_multiple_endpoints_found() { + let services = vec![ + Service { + id: String::from("did:example:123456789abcdefghi#service-1"), + service_endpoint: Some(OneOrMany::One(ServiceEndpoint::URI(String::from( + "https://example.com/endpoint-1", + )))), + type_: ssi::one_or_many::OneOrMany::One("Service1".to_string()), + property_set: None, + }, + Service { + id: String::from("did:example:123456789abcdefghi#service-1"), + service_endpoint: Some(OneOrMany::One(ServiceEndpoint::URI(String::from( + "https://example.com/endpoint-2", + )))), + type_: ssi::one_or_many::OneOrMany::One("Service1".to_string()), + property_set: None, + }, + ]; + let result = matching_endpoint(&services, "service-1"); + assert!(result.is_err()); + } } diff --git a/trustchain-http/src/encryption.rs b/trustchain-http/src/encryption.rs index f0dcc0a0..73570adc 100644 --- a/trustchain-http/src/encryption.rs +++ b/trustchain-http/src/encryption.rs @@ -112,40 +112,40 @@ struct UE { /// A type for downstream entity? struct DE; -pub trait CRStateIO { - // read() returns any struct that implements the CRState trait (eg. Step2Claim) - // (the Box<> is needed because the different structs that could be returned will likely have - // different sizes) - fn read(&self) -> Box; - fn write(&self, payload: &str); -} +// pub trait CRStateIO { +// // read() returns any struct that implements the CRState trait (eg. Step2Claim) +// // (the Box<> is needed because the different structs that could be returned will likely have +// // different sizes) +// fn read(&self) -> Box; +// fn write(&self, payload: &str); +// } // An empty trait implimented by all data types, eg. Step2Claim? -trait CRState { - fn status(&self) { - println!("Ok"); - } -} +// trait CRState { +// fn status(&self) { +// println!("Ok"); +// } +// } /// A type for a nonce struct Nonce(String); -// Data type to be read/written to file? -struct Step2Claim { - nonce: Nonce, - temp_pub_key: Jwk, -} -impl CRState for Step2Claim {} - -// Give the ability to DE to read and write CRState data files -impl CRStateIO for DE { - fn read(&self) -> Box { - todo!() - } - fn write(&self, payload: &str) { - todo!() - } -} +// // Data type to be read/written to file? +// struct Step2Claim { +// nonce: Nonce, +// temp_pub_key: Jwk, +// } +// impl CRState for Step2Claim {} + +// // Give the ability to DE to read and write CRState data files +// impl CRStateIO for DE { +// fn read(&self) -> Box { +// todo!() +// } +// fn write(&self, payload: &str) { +// todo!() +// } +// } // TODO: own type for nonce diff --git a/trustchain-http/src/lib.rs b/trustchain-http/src/lib.rs index a7a07298..34a09b3c 100644 --- a/trustchain-http/src/lib.rs +++ b/trustchain-http/src/lib.rs @@ -1,3 +1,4 @@ +pub mod attestation_utils; pub mod attestor; pub mod challenge_response; pub mod config; From 0b01d2de6e2f7157344967dec080d9f192b6bc95 Mon Sep 17 00:00:00 2001 From: pwochner Date: Mon, 6 Nov 2023 14:15:02 +0000 Subject: [PATCH 27/86] Refactor challenge response functionality. --- trustchain-cli/src/bin/main.rs | 2 +- .../src/attestation_encryption_utils.rs | 176 +++ trustchain-http/src/attestation_utils.rs | 1086 +++++++++++++- trustchain-http/src/challenge_response.rs | 1298 ----------------- trustchain-http/src/lib.rs | 3 +- trustchain-http/src/requester.rs | 64 + 6 files changed, 1326 insertions(+), 1303 deletions(-) create mode 100644 trustchain-http/src/attestation_encryption_utils.rs create mode 100644 trustchain-http/src/requester.rs diff --git a/trustchain-cli/src/bin/main.rs b/trustchain-cli/src/bin/main.rs index 58d33c10..308dcbf4 100644 --- a/trustchain-cli/src/bin/main.rs +++ b/trustchain-cli/src/bin/main.rs @@ -13,7 +13,7 @@ use trustchain_api::{ }; use trustchain_cli::config::cli_config; use trustchain_core::{vc::CredentialError, verifier::Verifier}; -use trustchain_http::challenge_response::initiate_identity_challenge; +use trustchain_http::requester::initiate_identity_challenge; use trustchain_ion::{ attest::attest_operation, create::create_operation, get_ion_resolver, verifier::IONVerifier, }; diff --git a/trustchain-http/src/attestation_encryption_utils.rs b/trustchain-http/src/attestation_encryption_utils.rs new file mode 100644 index 00000000..49db692e --- /dev/null +++ b/trustchain-http/src/attestation_encryption_utils.rs @@ -0,0 +1,176 @@ +use std::collections::HashMap; + +use josekit::jwe::ECDH_ES; +use josekit::jwk::Jwk; +use josekit::jws::{JwsHeader, ES256K}; +use josekit::jwt::{self, JwtPayload}; +use serde_json::Value; +use ssi::did::{Document, VerificationMethod}; +use ssi::jwk::JWK; + +use crate::attestation_utils::TrustchainCRError; + +struct Entity {} + +impl SignEncrypt for Entity {} + +impl DecryptVerify for Entity {} + +/// Interface for signing and then encrypting data. +pub trait SignEncrypt { + /// Cryptographically signs a payload with a secret key. + fn sign(&self, payload: &JwtPayload, secret_key: &Jwk) -> Result { + let mut header = JwsHeader::new(); + header.set_token_type("JWT"); + let signer = ES256K.signer_from_jwk(&secret_key)?; + let signed_jwt = jwt::encode_with_signer(payload, &header, &signer)?; + Ok(signed_jwt) + } + /// `JWTPayload` is a wrapped [`Map`](https://docs.rs/serde_json/1.0.79/serde_json/struct.Map.html) + /// of claims. + /// Cryptographically encrypts a payload with a public key. + fn encrypt(&self, payload: &JwtPayload, public_key: &Jwk) -> Result { + let mut header = josekit::jwe::JweHeader::new(); + header.set_token_type("JWT"); + header.set_content_encryption("A128CBC-HS256"); + header.set_content_encryption("A256GCM"); + + let encrypter = ECDH_ES.encrypter_from_jwk(&public_key)?; + let encrypted_jwt = jwt::encode_with_encrypter(payload, &header, &encrypter)?; + Ok(encrypted_jwt) + } + /// Wrapper function for signing and encrypting a payload. + fn sign_and_encrypt_claim( + &self, + payload: &JwtPayload, + secret_key: &Jwk, + public_key: &Jwk, + ) -> Result { + let signed_payload = self.sign(payload, secret_key)?; + let mut claims = JwtPayload::new(); + claims.set_claim("claim", Some(Value::from(signed_payload)))?; + self.encrypt(&claims, &public_key) + } +} +/// Interface for decrypting and then verifying data. +trait DecryptVerify { + /// Decrypts a payload with a secret key. + fn decrypt(&self, value: &Value, secret_key: &Jwk) -> Result { + let decrypter = ECDH_ES.decrypter_from_jwk(&secret_key)?; + let (payload, _) = jwt::decode_with_decrypter(value.as_str().unwrap(), &decrypter)?; + Ok(payload) + } + /// Wrapper function that combines decrypting a payload with a secret key and then verifying it with a public key. + fn decrypt_and_verify( + &self, + input: String, + secret_key: &Jwk, + public_key: &Jwk, + ) -> Result { + let decrypter = ECDH_ES.decrypter_from_jwk(secret_key)?; + let (payload, _) = jwt::decode_with_decrypter(input, &decrypter)?; + + let verifier = ES256K.verifier_from_jwk(public_key)?; + let (payload, _) = jwt::decode_with_verifier( + &payload.claim("claim").unwrap().as_str().unwrap(), + &verifier, + )?; + Ok(payload) + } +} + +/// Converts key from josekit Jwk into ssi JWK +pub fn josekit_to_ssi_jwk(key: &Jwk) -> Result { + let key_as_str: &str = &serde_json::to_string(&key).unwrap(); + let ssi_key: JWK = serde_json::from_str(key_as_str).unwrap(); + Ok(ssi_key) +} +/// Converts key from ssi JWK into josekit Jwk +pub fn ssi_to_josekit_jwk(key: &JWK) -> Result { + let key_as_str: &str = &serde_json::to_string(&key).unwrap(); + let ssi_key: Jwk = serde_json::from_str(key_as_str).unwrap(); + Ok(ssi_key) +} + +/// Extracts public keys contained in DID document +pub fn extract_key_ids_and_jwk( + document: &Document, +) -> Result, TrustchainCRError> { + let mut my_map = HashMap::::new(); + if let Some(vms) = &document.verification_method { + // TODO: leave the commented code + // vms.iter().for_each(|vm| match vm { + // VerificationMethod::Map(vm_map) => { + // let id = vm_map.id; + // let key = vm_map.get_jwk().unwrap(); + // let key_jose = ssi_to_josekit_jwk(&key).unwrap(); + // my_map.insert(id, key_jose); + // } + // _ => (), + // }); + // TODO: consider rewriting functional with filter, partition, fold over returned error + // variants. + for vm in vms { + match vm { + VerificationMethod::Map(vm_map) => { + let key = vm_map + .get_jwk() + .map_err(|_| TrustchainCRError::MissingJWK)?; + let id = key + .thumbprint() + .map_err(|_| TrustchainCRError::MissingJWK)?; + let key_jose = + ssi_to_josekit_jwk(&key).map_err(|err| TrustchainCRError::Serde(err))?; + my_map.insert(id, key_jose); + } + _ => (), + } + } + } + Ok(my_map) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::data::{ + TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS, TEST_SIGNING_KEY_1, TEST_SIGNING_KEY_2, + TEST_TEMP_KEY, TEST_UPDATE_KEY, TEST_UPSTREAM_KEY, + }; + #[test] + fn test_sign_encrypt_and_decrypt_verify() { + let entity = Entity {}; + let mut payload = JwtPayload::new(); + payload + .set_claim("test", Some(Value::from("This is a test claim."))) + .unwrap(); + // encrypt and sign payload + let secret_key_1: Jwk = serde_json::from_str(TEST_SIGNING_KEY_1).unwrap(); + let secret_key_2: Jwk = serde_json::from_str(TEST_SIGNING_KEY_2).unwrap(); + let public_key_1 = secret_key_1.to_public_key().unwrap(); + let public_key_2 = secret_key_2.to_public_key().unwrap(); + let signed_encrypted_payload = entity + .sign_and_encrypt_claim(&payload, &secret_key_1, &public_key_2) + .unwrap(); + // decrypt and verify payload + let decrypted_verified_payload = entity + .decrypt_and_verify(signed_encrypted_payload, &secret_key_2, &public_key_1) + .unwrap(); + assert_eq!( + decrypted_verified_payload + .claim("test") + .unwrap() + .as_str() + .unwrap(), + "This is a test claim." + ); + } + + #[test] + fn test_extract_key_ids_and_jwk() { + let document: Document = + serde_json::from_str(TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS).unwrap(); + let key_ids_and_jwk = extract_key_ids_and_jwk(&document).unwrap(); + assert_eq!(key_ids_and_jwk.len(), 2); + } +} diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index c3fad03f..90c6c7a3 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -1,9 +1,725 @@ -use std::path::{Path, PathBuf}; +use std::{ + collections::HashMap, + fs::{self, File}, + io::{BufWriter, Write}, + path::{Path, PathBuf}, +}; -use ssi::jwk::JWK; +use is_empty::IsEmpty; +use josekit::JoseError; +use josekit::{jwk::Jwk, jwt::JwtPayload}; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use serde::{Deserialize, Serialize}; +use serde_json::{to_string_pretty as to_json, Value}; +use serde_with::skip_serializing_none; +use ssi::{did::Service, jwk::JWK}; +use ssi::{did::ServiceEndpoint, one_or_many::OneOrMany}; +use std::fs::OpenOptions; +use thiserror::Error; use trustchain_core::TRUSTCHAIN_DATA; -use crate::challenge_response::TrustchainCRError; +#[derive(Error, Debug)] +pub enum TrustchainCRError { + /// Serde JSON error. + #[error("Wrapped serialization error: {0}")] + Serde(serde_json::Error), + /// Wrapped jose error. + #[error("Wrapped jose error: {0}")] + Jose(JoseError), + /// Missing JWK from verification method. + #[error("Missing JWK from verification method of a DID document.")] + MissingJWK, + /// Key not found in hashmap. + #[error("Key id not found.")] + KeyNotFound, + /// Claim not found in JWTPayload. + #[error("Claim not found in JWTPayload.")] + ClaimNotFound, + /// Nonce type invalid. + #[error("Invalid nonce type.")] + InvalidNonceType, + /// Failed to open file. + #[error("Failed to open file.")] + FailedToOpen, + /// Failed to save to file. + #[error("Failed to save to file.")] + FailedToSave, + /// Failed to set permissions on file. + #[error("Failed to set permissions on file.")] + FailedToSetPermissions, + /// Failed deserialize from file. + #[error("Failed to deserialize.")] + FailedToDeserialize, + /// Failed to check CR status. + #[error("Failed to determine CR status.")] + FailedStatusCheck, + /// Path for CR does not exist. + #[error("Path does not exist. No challenge-response record for this temporary key id.")] + CRPathNotFound, + /// Failed to generate key. + #[error("Failed to generate key.")] + FailedToGenerateKey, + /// Reqwest error. + #[error("Network request failed.")] + Reqwest(reqwest::Error), + /// Invalid service endpoint. + #[error("Invalid service endpoint.")] + InvalidServiceEndpoint, + /// CR initiation failed + #[error("Failed to initiate challenge-response.")] + FailedToInitiateCR, + /// Failed attestation request + #[error("Failed attestation request.")] + FailedAttestationRequest, +} + +impl From for TrustchainCRError { + fn from(err: JoseError) -> Self { + Self::Jose(err) + } +} + +#[derive(Debug, PartialEq)] +enum CurrentCRState { + NotStarted, + IdentityCRInitiated, + IdentityChallengeComplete, + IdentityResponseComplete, + ContentCRInitiated, + ContentChallengeComplete, + ContentResponseComplete, +} + +// pub struct Nonce([u8; N]); +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct Nonce(String); + +impl Nonce { + pub fn new() -> Self { + Self( + thread_rng() + .sample_iter(&Alphanumeric) + .take(32) + .map(char::from) + .collect(), + ) + } +} + +impl AsRef for Nonce { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl ToString for Nonce { + fn to_string(&self) -> String { + self.0.clone() + } +} + +impl From for Nonce { + fn from(s: String) -> Self { + Self(s) + } +} + +impl TryFrom<&Nonce> for JwtPayload { + type Error = TrustchainCRError; + fn try_from(value: &Nonce) -> Result { + let mut payload = JwtPayload::new(); + payload.set_claim("nonce", Some(Value::from(value.to_string())))?; + Ok(payload) + } +} + +/// Interface for serializing and deserializing each field of structs to/from files. +pub trait ElementwiseSerializeDeserialize +where + Self: Serialize, +{ + /// Serialize each field of the struct to a file. + fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { + let serialized = + serde_json::to_value(&self).map_err(|_| TrustchainCRError::FailedToSave)?; + if let Value::Object(fields) = serialized { + for (field_name, field_value) in fields { + if !field_value.is_null() { + let json_filename = format!("{}.json", field_name); + let file_path = path.join(json_filename); + + self.save_to_file(&file_path, &to_json(&field_value).unwrap())?; + } + } + } + Ok(()) + } + /// Deserializes each field of the struct from a file. + fn elementwise_deserialize(self, path: &PathBuf) -> Result, TrustchainCRError> + where + Self: Sized; + /// Save data to file. If file already exists, do nothing. + fn save_to_file(&self, path: &PathBuf, data: &str) -> Result<(), TrustchainCRError> { + if path.exists() { + println!("File already exists: {:?}", path); + return Ok(()); + } + + // Open the new file if it doesn't exist yet + let new_file = OpenOptions::new().create(true).write(true).open(path); + + // Write key to file + match new_file { + Ok(file) => { + let mut writer = BufWriter::new(file); + match writer.write_all(data.as_bytes()) { + Ok(_) => { + // Set file permissions to read-only (user, group, and others) + let mut permissions = fs::metadata(path) + .map_err(|_| TrustchainCRError::FailedToSetPermissions)? + .permissions(); + permissions.set_readonly(true); + fs::set_permissions(path, permissions) + .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; + Ok(()) + } + Err(_) => Err(TrustchainCRError::FailedToSave), + } + } + + Err(_) => Err(TrustchainCRError::FailedToSave), + } + } +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize)] +pub struct RequesterDetails { + pub requester_org: String, + pub operator_name: String, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, IsEmpty)] +pub struct IdentityCRInitiation { + pub temp_p_key: Option, + pub requester_details: Option, +} + +impl IdentityCRInitiation { + fn new() -> Self { + Self { + temp_p_key: None, + requester_details: None, + } + } + + fn is_complete(&self) -> bool { + return self.temp_p_key.is_some() && self.requester_details.is_some(); + } +} + +impl ElementwiseSerializeDeserialize for IdentityCRInitiation { + /// Deserialize each field of the struct from a file. Fields are optional. If no files are found, return None. + fn elementwise_deserialize( + mut self, + path: &PathBuf, + ) -> Result, TrustchainCRError> { + let temp_p_key_path = path.join("temp_p_key.json"); + self.temp_p_key = match File::open(&temp_p_key_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; + + let requester_details_path = path.join("requester_details.json"); + self.requester_details = match File::open(&requester_details_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; + + if self.temp_p_key.is_none() && self.requester_details.is_none() { + return Ok(None); + } + + Ok(Some(self)) + } +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, IsEmpty)] +struct CRIdentityChallenge { + update_p_key: Option, + identity_nonce: Option, // make own Nonce type + identity_challenge_signature: Option, + identity_response_signature: Option, +} + +impl CRIdentityChallenge { + fn new() -> Self { + Self { + update_p_key: None, + identity_nonce: None, + identity_challenge_signature: None, + identity_response_signature: None, + } + } + /// Returns true if all fields required for the challenge have a non-null value. + fn challenge_complete(&self) -> bool { + return self.update_p_key.is_some() + && self.identity_nonce.is_some() + && self.identity_challenge_signature.is_some(); + } + /// Returns true if all fields of the challenge-response have a non-null value. + fn response_complete(&self) -> bool { + return self.challenge_complete() && self.identity_response_signature.is_some(); + } +} + +impl ElementwiseSerializeDeserialize for CRIdentityChallenge { + /// Deserialize each field of the struct from a file. Fields are optional. If no files are found, return None. + fn elementwise_deserialize( + mut self, + path: &PathBuf, + ) -> Result, TrustchainCRError> { + // update public key + let mut full_path = path.join("update_p_key.json"); + self.update_p_key = match File::open(&full_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; + // identity nonce + full_path = path.join("identity_nonce.json"); + self.identity_nonce = match File::open(&full_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; + // identity challenge signature + full_path = path.join("identity_challenge_signature.json"); + self.identity_challenge_signature = match File::open(&full_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; + // identity response signature + full_path = path.join("identity_response_signature.json"); + self.identity_response_signature = match File::open(&full_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; + + if self.update_p_key.is_none() + && self.identity_nonce.is_none() + && self.identity_challenge_signature.is_none() + && self.identity_response_signature.is_none() + { + return Ok(None); + } + + Ok(Some(self)) + } +} + +impl TryFrom<&CRIdentityChallenge> for JwtPayload { + type Error = TrustchainCRError; + fn try_from(value: &CRIdentityChallenge) -> Result { + let mut payload = JwtPayload::new(); + payload.set_claim( + "identity_nonce", + Some(Value::from( + value.identity_nonce.as_ref().unwrap().to_string(), + )), + )?; + payload.set_claim( + "update_p_key", + Some(Value::from( + value.update_p_key.as_ref().unwrap().to_string(), + )), + )?; + Ok(payload) + } +} + +impl TryFrom<&JwtPayload> for CRIdentityChallenge { + type Error = TrustchainCRError; + fn try_from(value: &JwtPayload) -> Result { + let mut challenge = CRIdentityChallenge { + update_p_key: None, + identity_nonce: None, + identity_challenge_signature: None, + identity_response_signature: None, + }; + challenge.update_p_key = Some( + serde_json::from_str(value.claim("update_p_key").unwrap().as_str().unwrap()).unwrap(), + ); + challenge.identity_nonce = Some(Nonce::from( + value + .claim("identity_nonce") + .unwrap() + .as_str() + .unwrap() + .to_string(), + )); + Ok(challenge) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, IsEmpty)] +struct ContentCRInitiation { + temp_p_key: Option, + requester_did: Option, +} + +impl ContentCRInitiation { + fn new() -> Self { + Self { + temp_p_key: None, + requester_did: None, + } + } + + fn is_complete(&self) -> bool { + return self.temp_p_key.is_some() && self.requester_did.is_some(); + } +} + +impl ElementwiseSerializeDeserialize for ContentCRInitiation { + /// Deserialize each field of the struct from a file. Fields are optional. If no files are found, return None. + fn elementwise_deserialize( + mut self, + path: &PathBuf, + ) -> Result, TrustchainCRError> { + let temp_p_key_path = path.join("temp_p_key.json"); + self.temp_p_key = match File::open(&temp_p_key_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; + + let requester_details_path = path.join("requester_did.json"); + self.requester_did = match File::open(&requester_details_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; + + if self.temp_p_key.is_none() && self.requester_did.is_none() { + return Ok(None); + } + + Ok(Some(self)) + } +} + +#[derive(Debug, Serialize, Deserialize, IsEmpty)] +struct CRContentChallenge { + content_nonce: Option>, + content_challenge_signature: Option, + content_response_signature: Option, +} + +impl CRContentChallenge { + fn new() -> Self { + Self { + content_nonce: None, + content_challenge_signature: None, + content_response_signature: None, + } + } + /// Returns true if all fields required for the challenge have a non-null value. + fn challenge_complete(&self) -> bool { + return self.content_nonce.is_some() && self.content_challenge_signature.is_some(); + } + /// Returns true if all fields required for the challenge-response have a non-null value. + fn response_complete(&self) -> bool { + return self.challenge_complete() && self.content_response_signature.is_some(); + } +} + +impl ElementwiseSerializeDeserialize for CRContentChallenge { + /// Deserialize each field of the struct from a file. Fields are optional. If no files are found, return None. + fn elementwise_deserialize( + mut self, + path: &PathBuf, + ) -> Result, TrustchainCRError> { + // content nonce(s) + let mut full_path = path.join("content_nonce.json"); + self.content_nonce = match File::open(&full_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; + + // content challenge signature + full_path = path.join("content_challenge_signature.json"); + self.content_challenge_signature = match File::open(&full_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; + // content response signature + full_path = path.join("content_response_signature.json"); + self.content_response_signature = match File::open(&full_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; + + if self.content_nonce.is_none() + && self.content_challenge_signature.is_none() + && self.content_response_signature.is_none() + { + return Ok(None); + } + + Ok(Some(self)) + } +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, IsEmpty)] +struct CRState { + identity_cr_initiation: Option, + identity_challenge_response: Option, + content_cr_initiation: Option, + content_challenge_response: Option, +} + +impl CRState { + fn new() -> Self { + Self { + identity_cr_initiation: None, + identity_challenge_response: None, + content_cr_initiation: None, + content_challenge_response: None, + } + } + /// Returns true if all fields have a non-null value. + fn is_complete(&self) -> bool { + if self.identity_cr_initiation.is_some() + && self.identity_challenge_response.is_some() + && self.content_cr_initiation.is_some() + && self.content_challenge_response.is_some() + { + return true; + } + return false; + } + /// Determines current status of the challenge response process and accordingly prints messages to the console. + fn check_cr_status(&self) -> Result { + println!("Checking current challenge-response status..."); + println!(" "); + let mut current_state = CurrentCRState::NotStarted; + if self.is_empty() { + println!("{}", get_status_message(¤t_state)); + return Ok(current_state); + } + + // CR complete + if self.is_complete() { + current_state = CurrentCRState::ContentResponseComplete; + println!("{}", get_status_message(¤t_state)); + return Ok(current_state); + } + + // Identity CR initation + if self.identity_cr_initiation.is_none() + || !self.identity_cr_initiation.as_ref().unwrap().is_complete() + { + println!("{}", get_status_message(¤t_state)); + return Ok(current_state); + } + current_state = CurrentCRState::IdentityCRInitiated; + println!("{}", get_status_message(¤t_state)); + + // Identity challenge + if self.identity_challenge_response.is_none() + || !self + .identity_challenge_response + .as_ref() + .unwrap() + .challenge_complete() + { + return Ok(current_state); + } + current_state = CurrentCRState::IdentityChallengeComplete; + println!("{}", get_status_message(¤t_state)); + + // Identity response + if !self + .identity_challenge_response + .as_ref() + .unwrap() + .response_complete() + { + return Ok(current_state); + } + current_state = CurrentCRState::IdentityResponseComplete; + + // Content CR initation + if self.content_cr_initiation.is_none() + || !self.content_cr_initiation.as_ref().unwrap().is_complete() + { + return Ok(current_state); + } + current_state = CurrentCRState::ContentCRInitiated; + + // Content challenge + if self.content_challenge_response.is_none() + || !self + .content_challenge_response + .as_ref() + .unwrap() + .challenge_complete() + { + return Ok(current_state); + } + current_state = CurrentCRState::ContentChallengeComplete; + + // Content response + if !self + .content_challenge_response + .as_ref() + .unwrap() + .response_complete() + { + return Ok(current_state); + } + + return Ok(current_state); + } +} + +impl ElementwiseSerializeDeserialize for CRState { + /// Serialize each field of the struct to a file. Fields with null values are ignored. + fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { + if let Some(identity_initiation) = &self.identity_cr_initiation { + identity_initiation.elementwise_serialize(path)?; + } + if let Some(identity_challenge_response) = &self.identity_challenge_response { + identity_challenge_response.elementwise_serialize(path)?; + } + if let Some(content_cr_initiation) = &self.content_cr_initiation { + content_cr_initiation.elementwise_serialize(path)?; + } + if let Some(content_challenge_response) = &self.content_challenge_response { + content_challenge_response.elementwise_serialize(path)?; + } + Ok(()) + } + /// Deserialize each field of the struct from a file. All fields are optional. + fn elementwise_deserialize( + mut self, + path: &PathBuf, + ) -> Result, TrustchainCRError> { + self.identity_cr_initiation = IdentityCRInitiation::new().elementwise_deserialize(path)?; + self.identity_challenge_response = + CRIdentityChallenge::new().elementwise_deserialize(path)?; + self.content_cr_initiation = ContentCRInitiation::new().elementwise_deserialize(path)?; + self.content_challenge_response = + CRContentChallenge::new().elementwise_deserialize(path)?; + Ok(Some(self)) + } +} + +/// Returns message that corresponds to the current state of the challenge-response process. +fn get_status_message(current_state: &CurrentCRState) -> String { + match current_state { + CurrentCRState::NotStarted => { + return String::from("No records found for this challenge-response identifier. \nThe challenge-response process has not been initiated yet."); + } + CurrentCRState::IdentityCRInitiated => { + return String::from("Identity challenge-response initiated. Await response."); + } + CurrentCRState::IdentityChallengeComplete => { + return String::from("Identity challenge has been presented. Await response."); + } + CurrentCRState::IdentityResponseComplete => { + return String::from("Identity challenge-response complete."); + } + CurrentCRState::ContentCRInitiated => { + return String::from("Content challenge-response initiated. Await response."); + } + CurrentCRState::ContentChallengeComplete => { + return String::from("Content challenge has been presented. Await response."); + } + CurrentCRState::ContentResponseComplete => { + return String::from("Challenge-response complete."); + } + } +} + +/// Returns endpoint that contains the given fragment from the given list of service endpoints. +/// Throws error if no or more than one matching endpoint is found. +pub fn matching_endpoint( + services: &Vec, + fragment: &str, +) -> Result { + let mut endpoints = Vec::new(); + for service in services { + if service.id.contains(fragment) { + match &service.service_endpoint { + Some(OneOrMany::One(ServiceEndpoint::URI(uri))) => { + endpoints.push(uri.to_string()); + } + + _ => return Err(TrustchainCRError::InvalidServiceEndpoint), + } + } + } + if endpoints.len() != 1 { + return Err(TrustchainCRError::InvalidServiceEndpoint); + } + return Ok(endpoints[0].clone()); +} /// Returns unique path name for a specific attestation request derived from public key for the interaction. pub fn attestation_request_path(key: &JWK) -> Result { @@ -17,3 +733,367 @@ pub fn attestation_request_path(key: &JWK) -> Result .join("attestation_requests") .join(key_id)) } + +#[cfg(test)] +mod tests { + use crate::attestation_encryption_utils::extract_key_ids_and_jwk; + use crate::data::{ + TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS, TEST_SIGNING_KEY_1, TEST_SIGNING_KEY_2, + TEST_TEMP_KEY, TEST_UPDATE_KEY, TEST_UPSTREAM_KEY, + }; + use ssi::did::Document; + use tempfile::tempdir; + + use super::*; + + #[test] + fn test_elementwise_serialize() { + //TODO: more of an integration test + // ==========| Identity CR | ============== + let temp_s_key: Jwk = serde_json::from_str(TEST_TEMP_KEY).unwrap(); + let initiation = IdentityCRInitiation { + temp_p_key: Some(temp_s_key.to_public_key().unwrap()), + requester_details: Some(RequesterDetails { + requester_org: String::from("My Org"), + operator_name: String::from("John Doe"), + }), + }; + + // identity challenge + let identity_challenge = CRIdentityChallenge { + update_p_key: serde_json::from_str(TEST_UPDATE_KEY).unwrap(), + identity_nonce: Some(Nonce::new()), + identity_challenge_signature: Some(String::from("some challenge signature string")), + identity_response_signature: Some(String::from("some response signature string")), + }; + + // ==========| Content CR | ============== + let content_initiation = ContentCRInitiation { + temp_p_key: Some(temp_s_key.to_public_key().unwrap()), + requester_did: Some("did:example:123456789abcdefghi".to_string()), + }; + // get signing keys for DE from did document + let doc: Document = serde_json::from_str(TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS).unwrap(); + let test_keys_map = extract_key_ids_and_jwk(&doc).unwrap(); + + // generate map with unencrypted nonces so UE can store them for later verification + let nonces: HashMap = + test_keys_map + .iter() + .fold(HashMap::new(), |mut acc, (key_id, _)| { + acc.insert(String::from(key_id), Nonce::new()); + acc + }); + let content_challenge_response = CRContentChallenge { + content_nonce: Some(nonces), + content_challenge_signature: Some(String::from( + "some content challenge signature string", + )), + content_response_signature: Some(String::from( + "some content response signature string", + )), + }; + + // ==========| CR state | ============== + let cr_state = CRState { + identity_cr_initiation: Some(initiation), + identity_challenge_response: Some(identity_challenge), + content_cr_initiation: Some(content_initiation), + content_challenge_response: Some(content_challenge_response), + }; + // write to file + let path = tempdir().unwrap().into_path(); + let result = cr_state.elementwise_serialize(&path); + assert_eq!(result.is_ok(), true); + + // try to write to file again + let result = cr_state.elementwise_serialize(&path); + assert_eq!(result.is_ok(), true); + } + + #[test] + fn test_elementwise_deserialize_initiation() { + let cr_initiation = IdentityCRInitiation::new(); + let temp_path = tempdir().unwrap().into_path(); + + // Test case 1: None of the json files exist + let result = cr_initiation.elementwise_deserialize(&temp_path); + assert!(result.is_ok()); + let initiation = result.unwrap(); + assert!(initiation.is_none()); + + // Test case 2: Only one json file exists and can be deserialized + let cr_initiation = IdentityCRInitiation::new(); + let temp_p_key_path = temp_path.join("temp_p_key.json"); + let temp_p_key_file = File::create(&temp_p_key_path).unwrap(); + let temp_p_key: Jwk = serde_json::from_str(TEST_TEMP_KEY).unwrap(); + serde_json::to_writer(temp_p_key_file, &temp_p_key).unwrap(); + + let result = cr_initiation.elementwise_deserialize(&temp_path); + assert!(result.is_ok()); + let initiation = result.unwrap().unwrap(); + assert!(initiation.temp_p_key.is_some()); + assert!(initiation.requester_details.is_none()); + + // Test case 3: Both json files exist and can be deserialized + let cr_initiation = IdentityCRInitiation::new(); + let requester_details_path = temp_path.join("requester_details.json"); + let requester_details_file = File::create(&requester_details_path).unwrap(); + let requester_details = RequesterDetails { + requester_org: String::from("My Org"), + operator_name: String::from("John Doe"), + }; + serde_json::to_writer(requester_details_file, &requester_details).unwrap(); + let result = cr_initiation.elementwise_deserialize(&temp_path); + assert!(result.is_ok()); + let initiation = result.unwrap().unwrap(); + assert!(initiation.temp_p_key.is_some()); + assert!(initiation.requester_details.is_some()); + + // Test case 4: Both json files exist but one is invalid json and cannot be + // deserialized + let cr_initiation = IdentityCRInitiation::new(); + // override temp key with invalid key + let temp_p_key_file = File::create(&temp_p_key_path).unwrap(); + serde_json::to_writer(temp_p_key_file, "this is not valid json").unwrap(); + let result = cr_initiation.elementwise_deserialize(&temp_path); + assert!(result.is_err()); + } + + #[test] + fn test_elementwise_deserialize_identity_challenge() { + let identity_challenge = CRIdentityChallenge::new(); + let temp_path = tempdir().unwrap().into_path(); + + // Test case 1: None of the json files exist + let result = identity_challenge.elementwise_deserialize(&temp_path); + assert!(result.is_ok()); + let identity_challenge = result.unwrap(); + assert!(identity_challenge.is_none()); + + // Test case 2: Only one json file exists and can be deserialized + let update_p_key_path = temp_path.join("update_p_key.json"); + let update_p_key_file = File::create(&update_p_key_path).unwrap(); + let update_p_key: Jwk = serde_json::from_str(TEST_UPDATE_KEY).unwrap(); + serde_json::to_writer(update_p_key_file, &update_p_key).unwrap(); + let identity_challenge = CRIdentityChallenge::new(); + let result = identity_challenge.elementwise_deserialize(&temp_path); + assert!(result.is_ok()); + let identity_challenge = result.unwrap().unwrap(); + assert_eq!(identity_challenge.update_p_key, Some(update_p_key)); + assert!(identity_challenge.identity_nonce.is_none()); + assert!(identity_challenge.identity_challenge_signature.is_none()); + assert!(identity_challenge.identity_response_signature.is_none()); + + // Test case 3: One file exists but cannot be deserialized + let identity_nonce_path = temp_path.join("identity_nonce.json"); + let identity_nonce_file = File::create(&identity_nonce_path).unwrap(); + serde_json::to_writer(identity_nonce_file, &42).unwrap(); + let identity_challenge = CRIdentityChallenge::new(); + let result = identity_challenge.elementwise_deserialize(&temp_path); + assert!(result.is_err()); + println!("Error: {:?}", result.unwrap_err()); + } + + #[test] + fn test_elementwise_deserialize_content_challenge() { + let content_challenge = CRContentChallenge::new(); + let temp_path = tempdir().unwrap().into_path(); + + // Test case 1: None of the json files exist + let result = content_challenge.elementwise_deserialize(&temp_path); + assert!(result.is_ok()); + assert!(result.unwrap().is_none()); + + // Test case 2: Only one json file exists and can be deserialized + let content_challenge = CRContentChallenge::new(); + let content_nonce_path = temp_path.join("content_nonce.json"); + let content_nonce_file = File::create(&content_nonce_path).unwrap(); + let mut nonces_map: HashMap<&str, Nonce> = HashMap::new(); + nonces_map.insert("test_id", Nonce::new()); + serde_json::to_writer(content_nonce_file, &nonces_map).unwrap(); + let result = content_challenge.elementwise_deserialize(&temp_path); + assert!(result.is_ok()); + let content_challenge = result.unwrap().unwrap(); + assert!(content_challenge.content_nonce.is_some()); + assert!(content_challenge.content_challenge_signature.is_none()); + assert!(content_challenge.content_response_signature.is_none()); + + // Test case 3: One file exists but cannot be deserialized + let content_nonce_file = File::create(&content_nonce_path).unwrap(); + serde_json::to_writer(content_nonce_file, "thisisinvalid").unwrap(); + let result = content_challenge.elementwise_deserialize(&temp_path); + print!("Result: {:?}", result); + assert!(result.is_err()); + } + + #[test] + fn test_deserialize_challenge_state() { + let path = tempdir().unwrap().into_path(); + let challenge_state = CRState::new(); + + // Test case 1: some files exist and can be deserialised + let identity_initiatiation = IdentityCRInitiation { + temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), + requester_details: Some(RequesterDetails { + requester_org: String::from("My Org"), + operator_name: String::from("John Doe"), + }), + }; + let _ = identity_initiatiation.elementwise_serialize(&path); + let identity_challenge = CRIdentityChallenge { + update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), + identity_nonce: Some(Nonce::new()), + identity_challenge_signature: Some(String::from("some challenge signature string")), + identity_response_signature: Some(String::from("some response signature string")), + }; + let _ = identity_challenge.elementwise_serialize(&path); + + let content_cr_initiation = ContentCRInitiation { + temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), + requester_did: Some("did:example:123456789abcdefghi".to_string()), + }; + let _ = content_cr_initiation.elementwise_serialize(&path); + + let result = challenge_state.elementwise_deserialize(&path); + assert!(result.is_ok()); + let challenge_state = result.unwrap().unwrap(); + println!( + "Challenge state deserialized from files: {:?}", + challenge_state + ); + assert!(challenge_state.identity_cr_initiation.is_some()); + assert!(challenge_state.identity_challenge_response.is_some()); + assert!(challenge_state.content_cr_initiation.is_some()); + assert!(challenge_state.content_challenge_response.is_none()); + + // Test case 2: one file cannot be deserialized + let identity_nonce_path = path.join("content_nonce.json"); + let identity_nonce_file = File::create(&identity_nonce_path).unwrap(); + serde_json::to_writer(identity_nonce_file, &42).unwrap(); + let challenge_state = CRState::new().elementwise_deserialize(&path); + assert!(challenge_state.is_err()); + } + + #[test] + fn test_matching_endpoint() { + let services = vec![ + Service { + id: String::from("did:example:123456789abcdefghi#service-1"), + service_endpoint: Some(OneOrMany::One(ServiceEndpoint::URI(String::from( + "https://example.com/endpoint-1", + )))), + type_: ssi::one_or_many::OneOrMany::One("Service1".to_string()), + property_set: None, + }, + Service { + id: String::from("did:example:123456789abcdefghi#service-2"), + service_endpoint: Some(OneOrMany::One(ServiceEndpoint::URI(String::from( + "https://example.com/endpoint-2", + )))), + type_: ssi::one_or_many::OneOrMany::One("Service2".to_string()), + property_set: None, + }, + ]; + let result = matching_endpoint(&services, "service-1"); + assert_eq!(result.unwrap(), "https://example.com/endpoint-1"); + } + + #[test] + fn test_matching_endpoint_multiple_endpoints_found() { + let services = vec![ + Service { + id: String::from("did:example:123456789abcdefghi#service-1"), + service_endpoint: Some(OneOrMany::One(ServiceEndpoint::URI(String::from( + "https://example.com/endpoint-1", + )))), + type_: ssi::one_or_many::OneOrMany::One("Service1".to_string()), + property_set: None, + }, + Service { + id: String::from("did:example:123456789abcdefghi#service-1"), + service_endpoint: Some(OneOrMany::One(ServiceEndpoint::URI(String::from( + "https://example.com/endpoint-2", + )))), + type_: ssi::one_or_many::OneOrMany::One("Service1".to_string()), + property_set: None, + }, + ]; + let result = matching_endpoint(&services, "service-1"); + assert!(result.is_err()); + } + + #[test] + fn test_check_cr_status() { + let mut cr_state = CRState::new(); + // Test case 1: CR State is empty + let result = cr_state.check_cr_status().unwrap(); + assert_eq!(result, CurrentCRState::NotStarted); + + // Test case 2: some, but not all, initation information exists + cr_state.identity_cr_initiation = Some(IdentityCRInitiation { + temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), + requester_details: None, + }); + let result = cr_state.check_cr_status(); + assert_eq!(result.unwrap(), CurrentCRState::NotStarted); + + // Test case 3: identity initiation completed, identity challenge presented + cr_state.identity_cr_initiation = Some(IdentityCRInitiation { + temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), + requester_details: Some(RequesterDetails { + requester_org: String::from("My Org"), + operator_name: String::from("John Doe"), + }), + }); + cr_state.identity_challenge_response = Some(CRIdentityChallenge { + update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), + identity_nonce: Some(Nonce::new()), + identity_challenge_signature: Some(String::from("some challenge signature string")), + identity_response_signature: None, + }); + let result = cr_state.check_cr_status(); + assert_eq!(result.unwrap(), CurrentCRState::IdentityChallengeComplete); + + // Test case 4: Identity challenge response complete, content challenge initiated + cr_state.identity_challenge_response = Some(CRIdentityChallenge { + update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), + identity_nonce: Some(Nonce::new()), + identity_challenge_signature: Some(String::from("some challenge signature string")), + identity_response_signature: Some(String::from("some response signature string")), + }); + cr_state.content_cr_initiation = { + Some(ContentCRInitiation { + temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), + requester_did: Some("did:example:123456789abcdefghi".to_string()), + }) + }; + let result = cr_state.check_cr_status(); + assert_eq!(result.unwrap(), CurrentCRState::ContentCRInitiated); + + // Test case 5: Content challenge-response complete + cr_state.content_challenge_response = Some(CRContentChallenge { + content_nonce: Some(HashMap::new()), + content_challenge_signature: Some(String::from( + "some content challenge signature string", + )), + content_response_signature: Some(String::from( + "some content response signature string", + )), + }); + let result = cr_state.check_cr_status(); + assert_eq!(result.unwrap(), CurrentCRState::ContentResponseComplete); + } + #[test] + fn test_check_cr_status_inconsistent_order() { + let mut cr_state = CRState::new(); + cr_state.identity_challenge_response = Some(CRIdentityChallenge { + update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), + identity_nonce: Some(Nonce::new()), + identity_challenge_signature: Some(String::from("some challenge signature string")), + identity_response_signature: Some(String::from("some response signature string")), + }); + let result = cr_state.check_cr_status(); + assert_eq!(result.unwrap(), CurrentCRState::NotStarted); + } +} diff --git a/trustchain-http/src/challenge_response.rs b/trustchain-http/src/challenge_response.rs index 731c2163..d0fbc6ef 100644 --- a/trustchain-http/src/challenge_response.rs +++ b/trustchain-http/src/challenge_response.rs @@ -1,952 +1,3 @@ -use crate::attestation_utils::attestation_request_path; -use josekit::jwe::{JweHeader, ECDH_ES}; -use josekit::jwk::Jwk; -use josekit::jws::{JwsHeader, ES256K}; -use josekit::jwt::{self, JwtPayload}; -use josekit::JoseError; -use rand::thread_rng; -use rand::{distributions::Alphanumeric, Rng}; -use serde::{Deserialize, Serialize}; -use serde_json::{to_string_pretty as to_json, Value}; -use serde_with::skip_serializing_none; -use ssi::did::{Document, Service, ServiceEndpoint, VerificationMethod}; -use ssi::jwk::JWK; -use ssi::one_or_many::OneOrMany; -use std::collections::HashMap; -use std::fs::OpenOptions; -use std::fs::{self, File}; -use std::io::{BufWriter, Write}; -use trustchain_core::utils::generate_key; - -use is_empty::IsEmpty; -use std::path::PathBuf; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum TrustchainCRError { - /// Serde JSON error. - #[error("Wrapped serialization error: {0}")] - Serde(serde_json::Error), - /// Wrapped jose error. - #[error("Wrapped jose error: {0}")] - Jose(JoseError), - /// Missing JWK from verification method. - #[error("Missing JWK from verification method of a DID document.")] - MissingJWK, - /// Key not found in hashmap. - #[error("Key id not found.")] - KeyNotFound, - /// Claim not found in JWTPayload. - #[error("Claim not found in JWTPayload.")] - ClaimNotFound, - /// Nonce type invalid. - #[error("Invalid nonce type.")] - InvalidNonceType, - /// Failed to open file. - #[error("Failed to open file.")] - FailedToOpen, - /// Failed to save to file. - #[error("Failed to save to file.")] - FailedToSave, - /// Failed to set permissions on file. - #[error("Failed to set permissions on file.")] - FailedToSetPermissions, - /// Failed deserialize from file. - #[error("Failed to deserialize.")] - FailedToDeserialize, - /// Failed to check CR status. - #[error("Failed to determine CR status.")] - FailedStatusCheck, - /// Path for CR does not exist. - #[error("Path does not exist. No challenge-response record for this temporary key id.")] - CRPathNotFound, - /// Failed to generate key. - #[error("Failed to generate key.")] - FailedToGenerateKey, - /// Reqwest error. - #[error("Network request failed.")] - Reqwest(reqwest::Error), - /// Invalid service endpoint. - #[error("Invalid service endpoint.")] - InvalidServiceEndpoint, - /// CR initiation failed - #[error("Failed to initiate challenge-response.")] - FailedToInitiateCR, - /// Failed attestation request - #[error("Failed attestation request.")] - FailedAttestationRequest, -} - -#[derive(Debug, PartialEq)] -enum CurrentCRState { - NotStarted, - IdentityCRInitiated, - IdentityChallengeComplete, - IdentityResponseComplete, - ContentCRInitiated, - ContentChallengeComplete, - ContentResponseComplete, -} - -/// Returns message that corresponds to the current state of the challenge-response process. -fn get_status_message(current_state: &CurrentCRState) -> String { - match current_state { - CurrentCRState::NotStarted => { - return String::from("No records found for this challenge-response identifier. \nThe challenge-response process has not been initiated yet."); - } - CurrentCRState::IdentityCRInitiated => { - return String::from("Identity challenge-response initiated. Await response."); - } - CurrentCRState::IdentityChallengeComplete => { - return String::from("Identity challenge has been presented. Await response."); - } - CurrentCRState::IdentityResponseComplete => { - return String::from("Identity challenge-response complete."); - } - CurrentCRState::ContentCRInitiated => { - return String::from("Content challenge-response initiated. Await response."); - } - CurrentCRState::ContentChallengeComplete => { - return String::from("Content challenge has been presented. Await response."); - } - CurrentCRState::ContentResponseComplete => { - return String::from("Challenge-response complete."); - } - } -} - -impl From for TrustchainCRError { - fn from(err: JoseError) -> Self { - Self::Jose(err) - } -} - -/// Interface for serializing and deserializing each field of structs to/from files. -trait ElementwiseSerializeDeserialize -where - Self: Serialize, -{ - /// Serialize each field of the struct to a file. - fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { - let serialized = - serde_json::to_value(&self).map_err(|_| TrustchainCRError::FailedToSave)?; - if let Value::Object(fields) = serialized { - for (field_name, field_value) in fields { - if !field_value.is_null() { - let json_filename = format!("{}.json", field_name); - let file_path = path.join(json_filename); - - self.save_to_file(&file_path, &to_json(&field_value).unwrap())?; - } - } - } - Ok(()) - } - /// Deserializes each field of the struct from a file. - fn elementwise_deserialize(self, path: &PathBuf) -> Result, TrustchainCRError> - where - Self: Sized; - /// Save data to file. If file already exists, do nothing. - fn save_to_file(&self, path: &PathBuf, data: &str) -> Result<(), TrustchainCRError> { - if path.exists() { - println!("File already exists: {:?}", path); - return Ok(()); - } - - // Open the new file if it doesn't exist yet - let new_file = OpenOptions::new().create(true).write(true).open(path); - - // Write key to file - match new_file { - Ok(file) => { - let mut writer = BufWriter::new(file); - match writer.write_all(data.as_bytes()) { - Ok(_) => { - // Set file permissions to read-only (user, group, and others) - let mut permissions = fs::metadata(path) - .map_err(|_| TrustchainCRError::FailedToSetPermissions)? - .permissions(); - permissions.set_readonly(true); - fs::set_permissions(path, permissions) - .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; - Ok(()) - } - Err(_) => Err(TrustchainCRError::FailedToSave), - } - } - - Err(_) => Err(TrustchainCRError::FailedToSave), - } - } -} - -/// Interface for signing and then encrypting data. -pub trait SignEncrypt { - /// Cryptographically signs a payload with a secret key. - fn sign(&self, payload: &JwtPayload, secret_key: &Jwk) -> Result { - let mut header = JwsHeader::new(); - header.set_token_type("JWT"); - let signer = ES256K.signer_from_jwk(&secret_key)?; - let signed_jwt = jwt::encode_with_signer(payload, &header, &signer)?; - Ok(signed_jwt) - } - /// `JWTPayload` is a wrapped [`Map`](https://docs.rs/serde_json/1.0.79/serde_json/struct.Map.html) - /// of claims. - /// Cryptographically encrypts a payload with a public key. - fn encrypt(&self, payload: &JwtPayload, public_key: &Jwk) -> Result { - let mut header = JweHeader::new(); - header.set_token_type("JWT"); - header.set_content_encryption("A128CBC-HS256"); - header.set_content_encryption("A256GCM"); - - let encrypter = ECDH_ES.encrypter_from_jwk(&public_key)?; - let encrypted_jwt = jwt::encode_with_encrypter(payload, &header, &encrypter)?; - Ok(encrypted_jwt) - } - /// Wrapper function for signing and encrypting a payload. - fn sign_and_encrypt_claim( - &self, - payload: &JwtPayload, - secret_key: &Jwk, - public_key: &Jwk, - ) -> Result { - let signed_payload = self.sign(payload, secret_key)?; - let mut claims = JwtPayload::new(); - claims.set_claim("claim", Some(Value::from(signed_payload)))?; - self.encrypt(&claims, &public_key) - } -} -/// Interface for decrypting and then verifying data. -trait DecryptVerify { - /// Decrypts a payload with a secret key. - fn decrypt(&self, value: &Value, secret_key: &Jwk) -> Result { - let decrypter = ECDH_ES.decrypter_from_jwk(&secret_key)?; - let (payload, _) = jwt::decode_with_decrypter(value.as_str().unwrap(), &decrypter)?; - Ok(payload) - } - /// Wrapper function that combines decrypting a payload with a secret key and then verifying it with a public key. - fn decrypt_and_verify( - &self, - input: String, - secret_key: &Jwk, - public_key: &Jwk, - ) -> Result { - let decrypter = ECDH_ES.decrypter_from_jwk(secret_key)?; - let (payload, _) = jwt::decode_with_decrypter(input, &decrypter)?; - - let verifier = ES256K.verifier_from_jwk(public_key)?; - let (payload, _) = jwt::decode_with_verifier( - &payload.claim("claim").unwrap().as_str().unwrap(), - &verifier, - )?; - Ok(payload) - } -} - -struct Entity {} - -impl SignEncrypt for Entity {} - -impl DecryptVerify for Entity {} - -// pub struct Nonce([u8; N]); -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -pub struct Nonce(String); - -impl Nonce { - pub fn new() -> Self { - Self( - thread_rng() - .sample_iter(&Alphanumeric) - .take(32) - .map(char::from) - .collect(), - ) - } -} - -impl AsRef for Nonce { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl ToString for Nonce { - fn to_string(&self) -> String { - self.0.clone() - } -} - -impl From for Nonce { - fn from(s: String) -> Self { - Self(s) - } -} - -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, IsEmpty)] -struct CRState { - identity_cr_initiation: Option, - identity_challenge_response: Option, - content_cr_initiation: Option, - content_challenge_response: Option, -} - -impl CRState { - fn new() -> Self { - Self { - identity_cr_initiation: None, - identity_challenge_response: None, - content_cr_initiation: None, - content_challenge_response: None, - } - } - /// Returns true if all fields have a non-null value. - fn is_complete(&self) -> bool { - if self.identity_cr_initiation.is_some() - && self.identity_challenge_response.is_some() - && self.content_cr_initiation.is_some() - && self.content_challenge_response.is_some() - { - return true; - } - return false; - } - /// Determines current status of the challenge response process and accordingly prints messages to the console. - fn check_cr_status(&self) -> Result { - println!("Checking current challenge-response status..."); - println!(" "); - let mut current_state = CurrentCRState::NotStarted; - if self.is_empty() { - println!("{}", get_status_message(¤t_state)); - return Ok(current_state); - } - - // CR complete - if self.is_complete() { - current_state = CurrentCRState::ContentResponseComplete; - println!("{}", get_status_message(¤t_state)); - return Ok(current_state); - } - - // Identity CR initation - if self.identity_cr_initiation.is_none() - || !self.identity_cr_initiation.as_ref().unwrap().is_complete() - { - println!("{}", get_status_message(¤t_state)); - return Ok(current_state); - } - current_state = CurrentCRState::IdentityCRInitiated; - println!("{}", get_status_message(¤t_state)); - - // Identity challenge - if self.identity_challenge_response.is_none() - || !self - .identity_challenge_response - .as_ref() - .unwrap() - .challenge_complete() - { - return Ok(current_state); - } - current_state = CurrentCRState::IdentityChallengeComplete; - println!("{}", get_status_message(¤t_state)); - - // Identity response - if !self - .identity_challenge_response - .as_ref() - .unwrap() - .response_complete() - { - return Ok(current_state); - } - current_state = CurrentCRState::IdentityResponseComplete; - - // Content CR initation - if self.content_cr_initiation.is_none() - || !self.content_cr_initiation.as_ref().unwrap().is_complete() - { - return Ok(current_state); - } - current_state = CurrentCRState::ContentCRInitiated; - - // Content challenge - if self.content_challenge_response.is_none() - || !self - .content_challenge_response - .as_ref() - .unwrap() - .challenge_complete() - { - return Ok(current_state); - } - current_state = CurrentCRState::ContentChallengeComplete; - - // Content response - if !self - .content_challenge_response - .as_ref() - .unwrap() - .response_complete() - { - return Ok(current_state); - } - - return Ok(current_state); - } -} - -impl ElementwiseSerializeDeserialize for CRState { - /// Serialize each field of the struct to a file. Fields with null values are ignored. - fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { - if let Some(identity_initiation) = &self.identity_cr_initiation { - identity_initiation.elementwise_serialize(path)?; - } - if let Some(identity_challenge_response) = &self.identity_challenge_response { - identity_challenge_response.elementwise_serialize(path)?; - } - if let Some(content_cr_initiation) = &self.content_cr_initiation { - content_cr_initiation.elementwise_serialize(path)?; - } - if let Some(content_challenge_response) = &self.content_challenge_response { - content_challenge_response.elementwise_serialize(path)?; - } - Ok(()) - } - /// Deserialize each field of the struct from a file. All fields are optional. - fn elementwise_deserialize( - mut self, - path: &PathBuf, - ) -> Result, TrustchainCRError> { - self.identity_cr_initiation = IdentityCRInitiation::new().elementwise_deserialize(path)?; - self.identity_challenge_response = - CRIdentityChallenge::new().elementwise_deserialize(path)?; - self.content_cr_initiation = ContentCRInitiation::new().elementwise_deserialize(path)?; - self.content_challenge_response = - CRContentChallenge::new().elementwise_deserialize(path)?; - Ok(Some(self)) - } -} - -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize)] -struct RequesterDetails { - requester_org: String, - operator_name: String, -} - -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, IsEmpty)] -struct IdentityCRInitiation { - temp_p_key: Option, - requester_details: Option, -} - -impl IdentityCRInitiation { - fn new() -> Self { - Self { - temp_p_key: None, - requester_details: None, - } - } - - fn is_complete(&self) -> bool { - return self.temp_p_key.is_some() && self.requester_details.is_some(); - } -} - -impl ElementwiseSerializeDeserialize for IdentityCRInitiation { - // fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { - // // let file_path = path.join("temp_p_key.json"); - // // let data: &str = &to_json(&self.temp_p_key).unwrap(); - // // if !file_path.exists() { - // // self.save_to_file(&file_path, data); - // // } - - // // let file_path = path.join("requester_details.json"); - // // let data: &str = &to_json(&self.requester_details).unwrap(); - // // if !file_path.exists() { - // // self.save_to_file(&file_path, data); - // // } - - // // =======| new version |=========== - // let serialized = serde_json::to_value(&self).expect("Serialization failed"); - - // if let Value::Object(fields) = serialized { - // for (field_name, field_value) in fields { - // if !field_value.is_null() { - // let json_filename = format!("{}.json", field_name); - // let file_path = path.join(json_filename); - - // self.save_to_file(&file_path, &to_json(&field_value).unwrap()); - // } - // } - // } - - // Ok(()) - // } - /// Deserialize each field of the struct from a file. Fields are optional. If no files are found, return None. - fn elementwise_deserialize( - mut self, - path: &PathBuf, - ) -> Result, TrustchainCRError> { - let temp_p_key_path = path.join("temp_p_key.json"); - self.temp_p_key = match File::open(&temp_p_key_path) { - Ok(file) => { - let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; - Some(deserialized) - } - Err(_) => None, - }; - - let requester_details_path = path.join("requester_details.json"); - self.requester_details = match File::open(&requester_details_path) { - Ok(file) => { - let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; - Some(deserialized) - } - Err(_) => None, - }; - - if self.temp_p_key.is_none() && self.requester_details.is_none() { - return Ok(None); - } - - Ok(Some(self)) - } -} - -#[derive(Debug, Serialize, Deserialize, Clone, IsEmpty)] -struct ContentCRInitiation { - temp_p_key: Option, - requester_did: Option, -} - -impl ContentCRInitiation { - fn new() -> Self { - Self { - temp_p_key: None, - requester_did: None, - } - } - - fn is_complete(&self) -> bool { - return self.temp_p_key.is_some() && self.requester_did.is_some(); - } -} - -impl ElementwiseSerializeDeserialize for ContentCRInitiation { - /// Deserialize each field of the struct from a file. Fields are optional. If no files are found, return None. - fn elementwise_deserialize( - mut self, - path: &PathBuf, - ) -> Result, TrustchainCRError> { - let temp_p_key_path = path.join("temp_p_key.json"); - self.temp_p_key = match File::open(&temp_p_key_path) { - Ok(file) => { - let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; - Some(deserialized) - } - Err(_) => None, - }; - - let requester_details_path = path.join("requester_did.json"); - self.requester_did = match File::open(&requester_details_path) { - Ok(file) => { - let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; - Some(deserialized) - } - Err(_) => None, - }; - - if self.temp_p_key.is_none() && self.requester_did.is_none() { - return Ok(None); - } - - Ok(Some(self)) - } -} - -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, IsEmpty)] -struct CRIdentityChallenge { - update_p_key: Option, - identity_nonce: Option, // make own Nonce type - identity_challenge_signature: Option, - identity_response_signature: Option, -} - -impl CRIdentityChallenge { - fn new() -> Self { - Self { - update_p_key: None, - identity_nonce: None, - identity_challenge_signature: None, - identity_response_signature: None, - } - } - /// Returns true if all fields required for the challenge have a non-null value. - fn challenge_complete(&self) -> bool { - return self.update_p_key.is_some() - && self.identity_nonce.is_some() - && self.identity_challenge_signature.is_some(); - } - /// Returns true if all fields of the challenge-response have a non-null value. - fn response_complete(&self) -> bool { - return self.challenge_complete() && self.identity_response_signature.is_some(); - } -} - -impl ElementwiseSerializeDeserialize for CRIdentityChallenge { - /// Deserialize each field of the struct from a file. Fields are optional. If no files are found, return None. - fn elementwise_deserialize( - mut self, - path: &PathBuf, - ) -> Result, TrustchainCRError> { - // update public key - let mut full_path = path.join("update_p_key.json"); - self.update_p_key = match File::open(&full_path) { - Ok(file) => { - let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; - Some(deserialized) - } - Err(_) => None, - }; - // identity nonce - full_path = path.join("identity_nonce.json"); - self.identity_nonce = match File::open(&full_path) { - Ok(file) => { - let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; - Some(deserialized) - } - Err(_) => None, - }; - // identity challenge signature - full_path = path.join("identity_challenge_signature.json"); - self.identity_challenge_signature = match File::open(&full_path) { - Ok(file) => { - let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; - Some(deserialized) - } - Err(_) => None, - }; - // identity response signature - full_path = path.join("identity_response_signature.json"); - self.identity_response_signature = match File::open(&full_path) { - Ok(file) => { - let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; - Some(deserialized) - } - Err(_) => None, - }; - - if self.update_p_key.is_none() - && self.identity_nonce.is_none() - && self.identity_challenge_signature.is_none() - && self.identity_response_signature.is_none() - { - return Ok(None); - } - - Ok(Some(self)) - } -} - -impl TryFrom<&JwtPayload> for CRIdentityChallenge { - type Error = TrustchainCRError; - fn try_from(value: &JwtPayload) -> Result { - let mut challenge = CRIdentityChallenge { - update_p_key: None, - identity_nonce: None, - identity_challenge_signature: None, - identity_response_signature: None, - }; - challenge.update_p_key = Some( - serde_json::from_str(value.claim("update_p_key").unwrap().as_str().unwrap()).unwrap(), - ); - challenge.identity_nonce = Some(Nonce::from( - value - .claim("identity_nonce") - .unwrap() - .as_str() - .unwrap() - .to_string(), - )); - Ok(challenge) - } -} - -#[derive(Debug, Serialize, Deserialize, IsEmpty)] -struct CRContentChallenge { - content_nonce: Option>, - content_challenge_signature: Option, - content_response_signature: Option, -} - -impl CRContentChallenge { - fn new() -> Self { - Self { - content_nonce: None, - content_challenge_signature: None, - content_response_signature: None, - } - } - /// Returns true if all fields required for the challenge have a non-null value. - fn challenge_complete(&self) -> bool { - return self.content_nonce.is_some() && self.content_challenge_signature.is_some(); - } - /// Returns true if all fields required for the challenge-response have a non-null value. - fn response_complete(&self) -> bool { - return self.challenge_complete() && self.content_response_signature.is_some(); - } -} - -impl ElementwiseSerializeDeserialize for CRContentChallenge { - // fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { - // if let Some(content_nonce) = &self.content_nonce { - // let file_path = path.join("content_nonce.json"); - // let data: &str = &to_json(content_nonce).unwrap(); - // if !file_path.exists() { - // self.save_to_file(&file_path, data); - // } - // } - // if let Some(content_challenge_signature) = &self.content_challenge_signature { - // let file_path = path.join("content_challenge_signature.json"); - // let data: &str = &to_json(content_challenge_signature).unwrap(); - // if !file_path.exists() { - // self.save_to_file(&file_path, data); - // } - // } - // if let Some(content_response_signature) = &self.content_response_signature { - // let file_path = path.join("content_response_signature.json"); - // let data: &str = &to_json(content_response_signature).unwrap(); - // if !file_path.exists() { - // self.save_to_file(&file_path, data); - // } - // } - // Ok(()) - // } - - /// Deserialize each field of the struct from a file. Fields are optional. If no files are found, return None. - fn elementwise_deserialize( - mut self, - path: &PathBuf, - ) -> Result, TrustchainCRError> { - // content nonce(s) - let mut full_path = path.join("content_nonce.json"); - self.content_nonce = match File::open(&full_path) { - Ok(file) => { - let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; - Some(deserialized) - } - Err(_) => None, - }; - - // content challenge signature - full_path = path.join("content_challenge_signature.json"); - self.content_challenge_signature = match File::open(&full_path) { - Ok(file) => { - let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; - Some(deserialized) - } - Err(_) => None, - }; - // content response signature - full_path = path.join("content_response_signature.json"); - self.content_response_signature = match File::open(&full_path) { - Ok(file) => { - let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; - Some(deserialized) - } - Err(_) => None, - }; - - if self.content_nonce.is_none() - && self.content_challenge_signature.is_none() - && self.content_response_signature.is_none() - { - return Ok(None); - } - - Ok(Some(self)) - } -} - -impl TryFrom<&CRIdentityChallenge> for JwtPayload { - type Error = TrustchainCRError; - fn try_from(value: &CRIdentityChallenge) -> Result { - let mut payload = JwtPayload::new(); - payload.set_claim( - "identity_nonce", - Some(Value::from( - value.identity_nonce.as_ref().unwrap().to_string(), - )), - )?; - payload.set_claim( - "update_p_key", - Some(Value::from( - value.update_p_key.as_ref().unwrap().to_string(), - )), - )?; - Ok(payload) - } -} - -impl TryFrom<&Nonce> for JwtPayload { - type Error = TrustchainCRError; - fn try_from(value: &Nonce) -> Result { - let mut payload = JwtPayload::new(); - payload.set_claim("nonce", Some(Value::from(value.to_string())))?; - Ok(payload) - } -} - -/// Converts key from josekit Jwk into ssi JWK -fn josekit_to_ssi_jwk(key: &Jwk) -> Result { - let key_as_str: &str = &serde_json::to_string(&key).unwrap(); - let ssi_key: JWK = serde_json::from_str(key_as_str).unwrap(); - Ok(ssi_key) -} -/// Converts key from ssi JWK into josekit Jwk -fn ssi_to_josekit_jwk(key: &JWK) -> Result { - let key_as_str: &str = &serde_json::to_string(&key).unwrap(); - let ssi_key: Jwk = serde_json::from_str(key_as_str).unwrap(); - Ok(ssi_key) -} - -/// Extracts public keys contained in DID document -fn extract_key_ids_and_jwk(document: &Document) -> Result, TrustchainCRError> { - let mut my_map = HashMap::::new(); - if let Some(vms) = &document.verification_method { - // TODO: leave the commented code - // vms.iter().for_each(|vm| match vm { - // VerificationMethod::Map(vm_map) => { - // let id = vm_map.id; - // let key = vm_map.get_jwk().unwrap(); - // let key_jose = ssi_to_josekit_jwk(&key).unwrap(); - // my_map.insert(id, key_jose); - // } - // _ => (), - // }); - // TODO: consider rewriting functional with filter, partition, fold over returned error - // variants. - for vm in vms { - match vm { - VerificationMethod::Map(vm_map) => { - let key = vm_map - .get_jwk() - .map_err(|_| TrustchainCRError::MissingJWK)?; - let id = key - .thumbprint() - .map_err(|_| TrustchainCRError::MissingJWK)?; - let key_jose = - ssi_to_josekit_jwk(&key).map_err(|err| TrustchainCRError::Serde(err))?; - my_map.insert(id, key_jose); - } - _ => (), - } - } - } - Ok(my_map) -} - -/// Initiates the identity challenge-response process by sending a POST request to the upstream endpoint. -/// -/// This function generates a temporary key to use as an identifier throughout the challenge-response process. -/// It prompts the user to provide the organization name and operator name, which are included in the POST request -/// to the endpoint specified in the upstream's DID document. -pub async fn initiate_identity_challenge( - org_name: String, - op_name: String, - services: &Vec, -) -> Result<(), TrustchainCRError> { - // generate temp key - let temp_s_key_ssi = generate_key(); - let temp_s_key = - ssi_to_josekit_jwk(&temp_s_key_ssi).map_err(|_| TrustchainCRError::FailedToGenerateKey)?; - - // make identity_cr_initiation struct - let requester = RequesterDetails { - requester_org: org_name, - operator_name: op_name, - }; - let identity_cr_initiation = IdentityCRInitiation { - temp_p_key: temp_s_key.to_public_key().ok(), - requester_details: Some(requester), - }; - // extract URI from service endpoint - println!("Services: {:?}", services); - let uri = matching_endpoint(services, "Trustchain").unwrap(); // this is just to make current example work - // let uri = matching_endpoint(services, "identity-cr").unwrap(); // TODO: use this one once we have example published - - // make POST request to endpoint - let client = reqwest::Client::new(); - let result = client - .post(uri) - .json(&identity_cr_initiation) - .send() - .await - .map_err(|err| TrustchainCRError::Reqwest(err))?; - - if result.status() != 200 { - println!("Status code: {}", result.status()); - return Err(TrustchainCRError::FailedToInitiateCR); - } - // create new directory - let directory = attestation_request_path(&temp_s_key_ssi.to_public())?; - std::fs::create_dir_all(&directory).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; - - // serialise identity_cr_initiation - identity_cr_initiation.elementwise_serialize(&directory)?; - println!("Successfully initiated attestation request."); - println!("You will receive more information on the challenge-response process via alternative communication channel."); - Ok(()) -} - -/// Returns endpoint that contains the given fragment from the given list of service endpoints. -/// Throws error if no or more than one matching endpoint is found. -fn matching_endpoint(services: &Vec, fragment: &str) -> Result { - let mut endpoints = Vec::new(); - for service in services { - if service.id.contains(fragment) { - match &service.service_endpoint { - Some(OneOrMany::One(ServiceEndpoint::URI(uri))) => { - endpoints.push(uri.to_string()); - } - - _ => return Err(TrustchainCRError::InvalidServiceEndpoint), - } - } - } - if endpoints.len() != 1 { - return Err(TrustchainCRError::InvalidServiceEndpoint); - } - return Ok(endpoints[0].clone()); -} - #[cfg(test)] mod tests { @@ -1179,353 +230,4 @@ mod tests { println!("Verified response map: {:?}", verified_response_map); assert_eq!(verified_response_map, nonces); } - #[test] - fn test_elementwise_serialize() { - // ==========| Identity CR | ============== - let temp_s_key: Jwk = serde_json::from_str(TEST_TEMP_KEY).unwrap(); - let initiation = IdentityCRInitiation { - temp_p_key: Some(temp_s_key.to_public_key().unwrap()), - requester_details: Some(RequesterDetails { - requester_org: String::from("My Org"), - operator_name: String::from("John Doe"), - }), - }; - - // identity challenge - let identity_challenge = CRIdentityChallenge { - update_p_key: serde_json::from_str(TEST_UPDATE_KEY).unwrap(), - identity_nonce: Some(Nonce::new()), - identity_challenge_signature: Some(String::from("some challenge signature string")), - identity_response_signature: Some(String::from("some response signature string")), - }; - - // ==========| Content CR | ============== - let content_initiation = ContentCRInitiation { - temp_p_key: Some(temp_s_key.to_public_key().unwrap()), - requester_did: Some("did:example:123456789abcdefghi".to_string()), - }; - // get signing keys for DE from did document - let doc: Document = serde_json::from_str(TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS).unwrap(); - let test_keys_map = extract_key_ids_and_jwk(&doc).unwrap(); - - // generate map with unencrypted nonces so UE can store them for later verification - let nonces: HashMap = - test_keys_map - .iter() - .fold(HashMap::new(), |mut acc, (key_id, _)| { - acc.insert(String::from(key_id), Nonce::new()); - acc - }); - let content_challenge_response = CRContentChallenge { - content_nonce: Some(nonces), - content_challenge_signature: Some(String::from( - "some content challenge signature string", - )), - content_response_signature: Some(String::from( - "some content response signature string", - )), - }; - - // ==========| CR state | ============== - let cr_state = CRState { - identity_cr_initiation: Some(initiation), - identity_challenge_response: Some(identity_challenge), - content_cr_initiation: Some(content_initiation), - content_challenge_response: Some(content_challenge_response), - }; - // write to file - let path = tempdir().unwrap().into_path(); - let result = cr_state.elementwise_serialize(&path); - assert_eq!(result.is_ok(), true); - - // try to write to file again - let result = cr_state.elementwise_serialize(&path); - assert_eq!(result.is_ok(), true); - } - - #[test] - fn test_deserialize_challenge_state() { - let path = tempdir().unwrap().into_path(); - let challenge_state = CRState::new(); - - // Test case 1: some files exist and can be deserialised - let identity_initiatiation = IdentityCRInitiation { - temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), - requester_details: Some(RequesterDetails { - requester_org: String::from("My Org"), - operator_name: String::from("John Doe"), - }), - }; - let _ = identity_initiatiation.elementwise_serialize(&path); - let identity_challenge = CRIdentityChallenge { - update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), - identity_nonce: Some(Nonce::new()), - identity_challenge_signature: Some(String::from("some challenge signature string")), - identity_response_signature: Some(String::from("some response signature string")), - }; - let _ = identity_challenge.elementwise_serialize(&path); - - let content_cr_initiation = ContentCRInitiation { - temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), - requester_did: Some("did:example:123456789abcdefghi".to_string()), - }; - let _ = content_cr_initiation.elementwise_serialize(&path); - - let result = challenge_state.elementwise_deserialize(&path); - assert!(result.is_ok()); - let challenge_state = result.unwrap().unwrap(); - println!( - "Challenge state deserialized from files: {:?}", - challenge_state - ); - assert!(challenge_state.identity_cr_initiation.is_some()); - assert!(challenge_state.identity_challenge_response.is_some()); - assert!(challenge_state.content_cr_initiation.is_some()); - assert!(challenge_state.content_challenge_response.is_none()); - - // Test case 2: one file cannot be deserialized - let identity_nonce_path = path.join("content_nonce.json"); - let identity_nonce_file = File::create(&identity_nonce_path).unwrap(); - serde_json::to_writer(identity_nonce_file, &42).unwrap(); - let challenge_state = CRState::new().elementwise_deserialize(&path); - assert!(challenge_state.is_err()); - } - - #[test] - fn test_elementwise_deserialize_initiation() { - let cr_initiation = IdentityCRInitiation::new(); - let temp_path = tempdir().unwrap().into_path(); - - // Test case 1: None of the json files exist - let result = cr_initiation.elementwise_deserialize(&temp_path); - assert!(result.is_ok()); - let initiation = result.unwrap(); - assert!(initiation.is_none()); - - // Test case 2: Only one json file exists and can be deserialized - let cr_initiation = IdentityCRInitiation::new(); - let temp_p_key_path = temp_path.join("temp_p_key.json"); - let temp_p_key_file = File::create(&temp_p_key_path).unwrap(); - let temp_p_key: Jwk = serde_json::from_str(TEST_TEMP_KEY).unwrap(); - serde_json::to_writer(temp_p_key_file, &temp_p_key).unwrap(); - - let result = cr_initiation.elementwise_deserialize(&temp_path); - assert!(result.is_ok()); - let initiation = result.unwrap().unwrap(); - assert!(initiation.temp_p_key.is_some()); - assert!(initiation.requester_details.is_none()); - - // Test case 3: Both json files exist and can be deserialized - let cr_initiation = IdentityCRInitiation::new(); - let requester_details_path = temp_path.join("requester_details.json"); - let requester_details_file = File::create(&requester_details_path).unwrap(); - let requester_details = RequesterDetails { - requester_org: String::from("My Org"), - operator_name: String::from("John Doe"), - }; - serde_json::to_writer(requester_details_file, &requester_details).unwrap(); - let result = cr_initiation.elementwise_deserialize(&temp_path); - assert!(result.is_ok()); - let initiation = result.unwrap().unwrap(); - assert!(initiation.temp_p_key.is_some()); - assert!(initiation.requester_details.is_some()); - - // Test case 4: Both json files exist but one is invalid json and cannot be - // deserialized - let cr_initiation = IdentityCRInitiation::new(); - // override temp key with invalid key - let temp_p_key_file = File::create(&temp_p_key_path).unwrap(); - serde_json::to_writer(temp_p_key_file, "this is not valid json").unwrap(); - let result = cr_initiation.elementwise_deserialize(&temp_path); - assert!(result.is_err()); - } - - #[test] - fn test_elementwise_deserialize_identity_challenge() { - let identity_challenge = CRIdentityChallenge::new(); - let temp_path = tempdir().unwrap().into_path(); - - // Test case 1: None of the json files exist - let result = identity_challenge.elementwise_deserialize(&temp_path); - assert!(result.is_ok()); - let identity_challenge = result.unwrap(); - assert!(identity_challenge.is_none()); - - // Test case 2: Only one json file exists and can be deserialized - let update_p_key_path = temp_path.join("update_p_key.json"); - let update_p_key_file = File::create(&update_p_key_path).unwrap(); - let update_p_key: Jwk = serde_json::from_str(TEST_UPDATE_KEY).unwrap(); - serde_json::to_writer(update_p_key_file, &update_p_key).unwrap(); - let identity_challenge = CRIdentityChallenge::new(); - let result = identity_challenge.elementwise_deserialize(&temp_path); - assert!(result.is_ok()); - let identity_challenge = result.unwrap().unwrap(); - assert_eq!(identity_challenge.update_p_key, Some(update_p_key)); - assert!(identity_challenge.identity_nonce.is_none()); - assert!(identity_challenge.identity_challenge_signature.is_none()); - assert!(identity_challenge.identity_response_signature.is_none()); - - // Test case 3: One file exists but cannot be deserialized - let identity_nonce_path = temp_path.join("identity_nonce.json"); - let identity_nonce_file = File::create(&identity_nonce_path).unwrap(); - serde_json::to_writer(identity_nonce_file, &42).unwrap(); - let identity_challenge = CRIdentityChallenge::new(); - let result = identity_challenge.elementwise_deserialize(&temp_path); - assert!(result.is_err()); - println!("Error: {:?}", result.unwrap_err()); - } - - #[test] - fn test_elementwise_deserialize_content_challenge() { - let content_challenge = CRContentChallenge::new(); - let temp_path = tempdir().unwrap().into_path(); - - // Test case 1: None of the json files exist - let result = content_challenge.elementwise_deserialize(&temp_path); - assert!(result.is_ok()); - assert!(result.unwrap().is_none()); - - // Test case 2: Only one json file exists and can be deserialized - let content_challenge = CRContentChallenge::new(); - let content_nonce_path = temp_path.join("content_nonce.json"); - let content_nonce_file = File::create(&content_nonce_path).unwrap(); - let mut nonces_map: HashMap<&str, Nonce> = HashMap::new(); - nonces_map.insert("test_id", Nonce::new()); - serde_json::to_writer(content_nonce_file, &nonces_map).unwrap(); - let result = content_challenge.elementwise_deserialize(&temp_path); - assert!(result.is_ok()); - let content_challenge = result.unwrap().unwrap(); - assert!(content_challenge.content_nonce.is_some()); - assert!(content_challenge.content_challenge_signature.is_none()); - assert!(content_challenge.content_response_signature.is_none()); - - // Test case 3: One file exists but cannot be deserialized - let content_nonce_file = File::create(&content_nonce_path).unwrap(); - serde_json::to_writer(content_nonce_file, "thisisinvalid").unwrap(); - let result = content_challenge.elementwise_deserialize(&temp_path); - print!("Result: {:?}", result); - assert!(result.is_err()); - } - - #[test] - fn test_check_cr_status() { - let mut cr_state = CRState::new(); - // Test case 1: CR State is empty - let result = cr_state.check_cr_status().unwrap(); - assert_eq!(result, CurrentCRState::NotStarted); - - // Test case 2: some, but not all, initation information exists - cr_state.identity_cr_initiation = Some(IdentityCRInitiation { - temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), - requester_details: None, - }); - let result = cr_state.check_cr_status(); - assert_eq!(result.unwrap(), CurrentCRState::NotStarted); - - // Test case 3: identity initiation completed, identity challenge presented - cr_state.identity_cr_initiation = Some(IdentityCRInitiation { - temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), - requester_details: Some(RequesterDetails { - requester_org: String::from("My Org"), - operator_name: String::from("John Doe"), - }), - }); - cr_state.identity_challenge_response = Some(CRIdentityChallenge { - update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), - identity_nonce: Some(Nonce::new()), - identity_challenge_signature: Some(String::from("some challenge signature string")), - identity_response_signature: None, - }); - let result = cr_state.check_cr_status(); - assert_eq!(result.unwrap(), CurrentCRState::IdentityChallengeComplete); - - // Test case 4: Identity challenge response complete, content challenge initiated - cr_state.identity_challenge_response = Some(CRIdentityChallenge { - update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), - identity_nonce: Some(Nonce::new()), - identity_challenge_signature: Some(String::from("some challenge signature string")), - identity_response_signature: Some(String::from("some response signature string")), - }); - cr_state.content_cr_initiation = { - Some(ContentCRInitiation { - temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), - requester_did: Some("did:example:123456789abcdefghi".to_string()), - }) - }; - let result = cr_state.check_cr_status(); - assert_eq!(result.unwrap(), CurrentCRState::ContentCRInitiated); - - // Test case 5: Content challenge-response complete - cr_state.content_challenge_response = Some(CRContentChallenge { - content_nonce: Some(HashMap::new()), - content_challenge_signature: Some(String::from( - "some content challenge signature string", - )), - content_response_signature: Some(String::from( - "some content response signature string", - )), - }); - let result = cr_state.check_cr_status(); - assert_eq!(result.unwrap(), CurrentCRState::ContentResponseComplete); - } - #[test] - fn test_check_cr_status_inconsistent_order() { - let mut cr_state = CRState::new(); - cr_state.identity_challenge_response = Some(CRIdentityChallenge { - update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), - identity_nonce: Some(Nonce::new()), - identity_challenge_signature: Some(String::from("some challenge signature string")), - identity_response_signature: Some(String::from("some response signature string")), - }); - let result = cr_state.check_cr_status(); - assert_eq!(result.unwrap(), CurrentCRState::NotStarted); - } - - #[test] - fn test_matching_endpoint() { - let services = vec![ - Service { - id: String::from("did:example:123456789abcdefghi#service-1"), - service_endpoint: Some(OneOrMany::One(ServiceEndpoint::URI(String::from( - "https://example.com/endpoint-1", - )))), - type_: ssi::one_or_many::OneOrMany::One("Service1".to_string()), - property_set: None, - }, - Service { - id: String::from("did:example:123456789abcdefghi#service-2"), - service_endpoint: Some(OneOrMany::One(ServiceEndpoint::URI(String::from( - "https://example.com/endpoint-2", - )))), - type_: ssi::one_or_many::OneOrMany::One("Service2".to_string()), - property_set: None, - }, - ]; - let result = matching_endpoint(&services, "service-1"); - assert_eq!(result.unwrap(), "https://example.com/endpoint-1"); - } - - #[test] - fn test_matching_endpoint_multiple_endpoints_found() { - let services = vec![ - Service { - id: String::from("did:example:123456789abcdefghi#service-1"), - service_endpoint: Some(OneOrMany::One(ServiceEndpoint::URI(String::from( - "https://example.com/endpoint-1", - )))), - type_: ssi::one_or_many::OneOrMany::One("Service1".to_string()), - property_set: None, - }, - Service { - id: String::from("did:example:123456789abcdefghi#service-1"), - service_endpoint: Some(OneOrMany::One(ServiceEndpoint::URI(String::from( - "https://example.com/endpoint-2", - )))), - type_: ssi::one_or_many::OneOrMany::One("Service1".to_string()), - property_set: None, - }, - ]; - let result = matching_endpoint(&services, "service-1"); - assert!(result.is_err()); - } } diff --git a/trustchain-http/src/lib.rs b/trustchain-http/src/lib.rs index 34a09b3c..106199c2 100644 --- a/trustchain-http/src/lib.rs +++ b/trustchain-http/src/lib.rs @@ -1,6 +1,6 @@ +pub mod attestation_encryption_utils; pub mod attestation_utils; pub mod attestor; -pub mod challenge_response; pub mod config; pub mod data; pub mod encryption; @@ -8,6 +8,7 @@ pub mod errors; pub mod issuer; pub mod middleware; pub mod qrcode; +pub mod requester; pub mod resolver; pub mod root; pub mod server; diff --git a/trustchain-http/src/requester.rs b/trustchain-http/src/requester.rs new file mode 100644 index 00000000..de193ab6 --- /dev/null +++ b/trustchain-http/src/requester.rs @@ -0,0 +1,64 @@ +use ssi::did::Service; +use trustchain_core::utils::generate_key; + +use crate::{ + attestation_encryption_utils::ssi_to_josekit_jwk, + attestation_utils::TrustchainCRError, + attestation_utils::{ + attestation_request_path, matching_endpoint, ElementwiseSerializeDeserialize, + IdentityCRInitiation, RequesterDetails, + }, +}; + +/// Initiates the identity challenge-response process by sending a POST request to the upstream endpoint. +/// +/// This function generates a temporary key to use as an identifier throughout the challenge-response process. +/// It prompts the user to provide the organization name and operator name, which are included in the POST request +/// to the endpoint specified in the upstream's DID document. +pub async fn initiate_identity_challenge( + org_name: String, + op_name: String, + services: &Vec, +) -> Result<(), TrustchainCRError> { + // generate temp key + let temp_s_key_ssi = generate_key(); + let temp_s_key = + ssi_to_josekit_jwk(&temp_s_key_ssi).map_err(|_| TrustchainCRError::FailedToGenerateKey)?; + + // make identity_cr_initiation struct + let requester = RequesterDetails { + requester_org: org_name, + operator_name: op_name, + }; + let identity_cr_initiation = IdentityCRInitiation { + temp_p_key: temp_s_key.to_public_key().ok(), + requester_details: Some(requester), + }; + // extract URI from service endpoint + println!("Services: {:?}", services); + let uri = matching_endpoint(services, "Trustchain").unwrap(); // this is just to make current example work + // let uri = matching_endpoint(services, "identity-cr").unwrap(); // TODO: use this one once we have example published + + // make POST request to endpoint + let client = reqwest::Client::new(); + let result = client + .post(uri) + .json(&identity_cr_initiation) + .send() + .await + .map_err(|err| TrustchainCRError::Reqwest(err))?; + + if result.status() != 200 { + println!("Status code: {}", result.status()); + return Err(TrustchainCRError::FailedToInitiateCR); + } + // create new directory + let directory = attestation_request_path(&temp_s_key_ssi.to_public())?; + std::fs::create_dir_all(&directory).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; + + // serialise identity_cr_initiation + identity_cr_initiation.elementwise_serialize(&directory)?; + println!("Successfully initiated attestation request."); + println!("You will receive more information on the challenge-response process via alternative communication channel."); + Ok(()) +} From 33fe5d0ca0f4b9475e8a899299854f7703704af6 Mon Sep 17 00:00:00 2001 From: pwochner Date: Thu, 9 Nov 2023 15:36:58 +0100 Subject: [PATCH 28/86] Attestor: remove unused functionality. Modify functionality to post initiation request. --- trustchain-http/src/attestation_utils.rs | 2 +- trustchain-http/src/attestor.rs | 175 ++++++++--------------- 2 files changed, 59 insertions(+), 118 deletions(-) diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index 90c6c7a3..e42dde9f 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -207,7 +207,7 @@ pub struct IdentityCRInitiation { } impl IdentityCRInitiation { - fn new() -> Self { + pub fn new() -> Self { Self { temp_p_key: None, requester_details: None, diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index cfb43647..5ace246b 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -1,5 +1,10 @@ +use crate::attestation_encryption_utils::{josekit_to_ssi_jwk, ssi_to_josekit_jwk}; +use crate::attestation_utils::{ + attestation_request_path, ElementwiseSerializeDeserialize, IdentityCRInitiation, +}; use crate::{errors::TrustchainHTTPError, state::AppState}; use async_trait::async_trait; +use axum::extract::path; use axum::{ response::{Html, IntoResponse, Response}, Json, @@ -11,7 +16,6 @@ use rand::{distributions::Alphanumeric, thread_rng}; use serde::{Deserialize, Serialize}; use serde_json::to_string_pretty; use sha2::{Digest, Sha256}; -use ssi::jwk::JWK; use std::io::Write; use std::{fs::OpenOptions, path::Path, path::PathBuf, sync::Arc}; use trustchain_core::TRUSTCHAIN_DATA; @@ -22,67 +26,43 @@ use trustchain_core::TRUSTCHAIN_DATA; // - name of DE organisation ("name_downstream") // - name of individual operator within DE responsible for the request -/// Writes received attestation request to unique path derived from the public key for the interaction. -fn write_attestation_info(attestation_info: &AttestationInfo) -> Result<(), TrustchainHTTPError> { - // Get environment for TRUSTCHAIN_DATA - - let directory = attestion_request_path(&attestation_info.temp_pub_key)?; +// /// Writes received attestation request to unique path derived from the public key for the interaction. +// fn write_attestation_info( +// attestation_info: &IdentityCRInitiation, +// ) -> Result<(), TrustchainHTTPError> { +// // Get environment for TRUSTCHAIN_DATA - // Make directory if non-existent - // Equivalent of os.makedirs(exist_ok=True) in python - std::fs::create_dir_all(&directory) - .map_err(|_| TrustchainHTTPError::FailedAttestationRequest)?; +// let directory = attestion_request_path(&attestation_info.temp_p_key.unwrap().to_string())?; - // Check if initial request exists ("attestation_info.json"), if yes, return InternalServerError - let full_path = directory.join("attestation_info.json"); +// // Make directory if non-existent +// // Equivalent of os.makedirs(exist_ok=True) in python +// std::fs::create_dir_all(&directory) +// .map_err(|_| TrustchainHTTPError::FailedAttestationRequest)?; - if full_path.exists() { - return Err(TrustchainHTTPError::FailedAttestationRequest); - } +// // Check if initial request exists ("attestation_info.json"), if yes, return InternalServerError +// let full_path = directory.join("attestation_info.json"); - // If not, write to file - // Open the new file - let mut file = OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(full_path) - .map_err(|_| TrustchainHTTPError::FailedAttestationRequest)?; +// if full_path.exists() { +// return Err(TrustchainHTTPError::FailedAttestationRequest); +// } - // Write to file - writeln!(file, "{}", &to_string_pretty(attestation_info).unwrap()) - .map_err(|_| TrustchainHTTPError::FailedAttestationRequest)?; +// // If not, write to file +// // Open the new file +// let mut file = OpenOptions::new() +// .create(true) +// .write(true) +// .truncate(true) +// .open(full_path) +// .map_err(|_| TrustchainHTTPError::FailedAttestationRequest)?; - // Else do something? +// // Write to file +// writeln!(file, "{}", &to_string_pretty(attestation_info).unwrap()) +// .map_err(|_| TrustchainHTTPError::FailedAttestationRequest)?; - Ok(()) -} +// // Else do something? -/// Returns unique path name for a specific attestation request derived from public key for the interaction. -fn attestion_request_path(pub_key: &str) -> Result { - // Root path in TRUSTCHAIN_DATA - let path: String = std::env::var(TRUSTCHAIN_DATA) - .map_err(|_| TrustchainHTTPError::FailedAttestationRequest)?; - // Use hash of temp_pub_key - Ok(Path::new(path.as_str()) - .join("attestation_requests") - .join(attestation_request_id(pub_key))) -} - -pub fn attestation_request_id(pub_key: &str) -> String { - hex::encode(Sha256::digest(pub_key)) -} -// generate_nonce copied from (they rely on newer version of ssi: v0.6.0, -// WIP issue for TC: https://github.com/alan-turing-institute/trustchain/issues/85 -// https://github.com/spruceid/oidc4vci-rs/blob/main/src/nonce.rs -fn generate_nonce() -> String { - thread_rng() - .sample_iter(&Alphanumeric) - .take(32) - .map(char::from) - .collect() -} -// TODO: format correctly and convert to bytes?? +// Ok(()) +// } // Encryption: https://github.com/hidekatsu-izuno/josekit-rs#signing-a-jwt-by-ecdsa @@ -132,91 +112,52 @@ impl TrustchainAttestorHTTP for TrustchainAttestorHTTPHandler { impl TrustchainAttestorHTTPHandler { /// Processes initial attestation request and provided data pub async fn post_initiation( - Json(attestation_info): Json, + Json(attestation_initiation): Json, // app_state: Arc, ) -> impl IntoResponse { - info!("Received attestation info: {:?}", attestation_info); - - write_attestation_info(&attestation_info).map(|_| (StatusCode::OK, Html("Received request. Please wait for operator to contact you through an alternative channel."))) + info!("Received attestation info: {:?}", attestation_initiation); + let temp_p_key_ssi = + josekit_to_ssi_jwk(attestation_initiation.temp_p_key.as_ref().unwrap()); + let path = attestation_request_path(&temp_p_key_ssi.unwrap()).unwrap(); + // create directory and save attestation initation to file + let _ = std::fs::create_dir_all(&path); + let _ = attestation_initiation.elementwise_serialize(&path).map(|_| (StatusCode::OK, Html("Received request. Please wait for operator to contact you through an alternative channel."))); } } #[cfg(test)] mod tests { - use crate::{config::HTTPConfig, server::TrustchainRouter}; - use axum::extract; + use crate::{ + attestation_utils::RequesterDetails, config::HTTPConfig, server::TrustchainRouter, + }; use axum_test_helper::TestClient; - use lazy_static::lazy_static; - use trustchain_core::utils::init; use super::*; // TODO: add this key when switched to JWK - const TEST_KEY: &str = - r#"{"kty":"OKP","crv":"Ed25519","x":"B2J8eJfFljEnKX9yt9_V4TCwcL8rd4qtD7T2Bz4TX0s"}"#; - const TEST_ATTESTATION_INFO: &str = r#"{ - "apiAccessToken": "abcd", - "tempPubKey": "some_string", - "nameDownstream": "myTrustworthyEntity", - "nameOperator": "trustworthyOperator" - }"#; - - #[test] - fn test_key() { - let key: JWK = serde_json::from_str(TEST_KEY).unwrap(); - } - - #[test] - fn test_generate_nonce() { - let nonce = generate_nonce(); - assert_eq!(nonce.len(), 32); - } - - #[test] - fn test_write_attestation_info() { - init(); - let expected_attestation_info: AttestationInfo = - serde_json::from_str(TEST_ATTESTATION_INFO).unwrap(); - // Get expected path - let expected_path = - attestion_request_path(&expected_attestation_info.temp_pub_key).unwrap(); - println!("The test path is: {:?}", expected_path); - - // Write to file - assert!(write_attestation_info(&expected_attestation_info).is_ok()); - - // Check directory exists - assert!(expected_path.exists()); - - // Check file deserializes to ATTESTATION_INFO - let file_content = - std::fs::read_to_string(expected_path.join("attestation_info.json")).unwrap(); - println!("The file attestation_info.json contains: {}", file_content); - - let actual_attesation_info: AttestationInfo = serde_json::from_str(&file_content).unwrap(); - assert_eq!(expected_attestation_info.clone(), actual_attesation_info); - } + use crate::data::TEST_TEMP_KEY; // Attestor integration tests // TODO: make test better #[tokio::test] #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] async fn test_post_initiation() { + let attestation_initiation: IdentityCRInitiation = IdentityCRInitiation { + temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), + requester_details: Some(RequesterDetails { + requester_org: "myTrustworthyEntity".to_string(), + operator_name: "trustworthyOperator".to_string(), + }), + }; + let initiation_json = serde_json::to_string_pretty(&attestation_initiation).unwrap(); + println!("Attestation initiation: {:?}", initiation_json); let app = TrustchainRouter::from(HTTPConfig::default()).into_router(); let uri = "/did/attestor/initiate".to_string(); let client = TestClient::new(app); - let response = client - .post(&uri) - .json(&AttestationInfo { - api_access_token: "a".to_string(), - name_downstream: "b".to_string(), - name_operator: "c".to_string(), - temp_pub_key: "d".to_string(), - }) - .send() - .await; + let response = client.post(&uri).json(&attestation_initiation).send().await; assert_eq!(response.status(), 200); - assert_eq!(response.text().await, "Hello world!"); + println!("Response text: {:?}", response.text().await); + // assert_eq!(response.text().await, "Received request. Please wait for operator to contact you through an alternative channel."); } } From 7f060ef87faf8f841732dd79232858a6ead725c6 Mon Sep 17 00:00:00 2001 From: pwochner Date: Thu, 9 Nov 2023 15:37:36 +0100 Subject: [PATCH 29/86] Cli: Add command to present identity challenge. --- trustchain-cli/src/bin/main.rs | 54 ++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/trustchain-cli/src/bin/main.rs b/trustchain-cli/src/bin/main.rs index 308dcbf4..c8d3fb4d 100644 --- a/trustchain-cli/src/bin/main.rs +++ b/trustchain-cli/src/bin/main.rs @@ -2,18 +2,24 @@ use clap::{arg, ArgAction, Command}; use core::panic; use serde_json::to_string_pretty; -use ssi::{jsonld::ContextLoader, ldp::LinkedDataDocument, vc::Credential}; +use ssi::{jsonld::ContextLoader, jwk::JWK, ldp::LinkedDataDocument, vc::Credential}; use std::{ fs::File, io::{self, stdin, BufReader}, + path::{Path, PathBuf}, }; use trustchain_api::{ api::{TrustchainDIDAPI, TrustchainVCAPI}, TrustchainAPI, }; use trustchain_cli::config::cli_config; -use trustchain_core::{vc::CredentialError, verifier::Verifier}; -use trustchain_http::requester::initiate_identity_challenge; +use trustchain_core::{vc::CredentialError, verifier::Verifier, TRUSTCHAIN_DATA}; +use trustchain_http::{ + attestation_utils::{ + Nonce, ElementwiseSerializeDeserialize, IdentityCRInitiation, TrustchainCRError + }, + requester::initiate_identity_challenge, +}; use trustchain_ion::{ attest::attest_operation, create::create_operation, get_ion_resolver, verifier::IONVerifier, }; @@ -99,7 +105,14 @@ fn cli() -> Command { .arg(arg!(-v - -verbose).action(ArgAction::Count)) .arg(arg!(-d --did ).required(true)) ) + .subcommand( + Command::new("present") + .about("Produce challenges to be presented to requestor.") + .arg(arg!(-v - -verbose).action(ArgAction::Count)) + .arg(arg!(-p --path ).required(true)) + ) ) + ) } @@ -321,6 +334,41 @@ async fn main() -> Result<(), Box> { ) .await?; } + Some(("present", sub_matches)) => { + // get attestation request path from provided input + let trustchain_dir: String = std::env::var(TRUSTCHAIN_DATA).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; + let path_to_check = sub_matches.get_one::("path").unwrap(); + let path = PathBuf::new().join(trustchain_dir).join("attestation_requests").join(path_to_check); + if !path.exists() { + panic!("Provided attestation request not found. Path does not exist."); + } + let identity_initiation = IdentityCRInitiation::new() + .elementwise_deserialize(&path) + .unwrap(); + // Show requester information to user and ask for confirmation to proceed + println!("---------------------------------"); + println!("Requester information: {:?}", identity_initiation.unwrap().requester_details.unwrap()); + println!("---------------------------------"); + println!("Recognise this attestation request and want to proceed? (y/n)"); + let mut prompt = String::new(); + io::stdin() + .read_line(&mut prompt) + .expect("Failed to read line"); + let prompt = prompt.trim(); + if prompt != "y" && prompt != "yes" { + println!("Aborting attestation request."); + return Ok(()); + + } + let nonce = Nonce::new(); + println!("---------------------------------"); + println!("Identity challenge-response nonce: {:?}", nonce.to_string()); + // TODO: update commitment + // TODO: endpoint to send response to + // TODO: Print to terminal and instruct to send via alternative channels + println!("Please send the above nonce, update commitment and endpoint to the requester via alternative channels."); + + } _ => panic!("Unrecognised CR subcommand."), }, _ => panic!("Unrecognised CR subcommand."), From d93b064e22bacdd9b92b8ea065fcfda63839a804 Mon Sep 17 00:00:00 2001 From: pwochner Date: Fri, 10 Nov 2023 17:45:53 +0100 Subject: [PATCH 30/86] Make methods to get signing keys public. --- trustchain-ion/src/attestor.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/trustchain-ion/src/attestor.rs b/trustchain-ion/src/attestor.rs index 958a2eab..fe77bee4 100644 --- a/trustchain-ion/src/attestor.rs +++ b/trustchain-ion/src/attestor.rs @@ -34,12 +34,14 @@ impl IONAttestor { } } /// Gets the signing keys of the attestor. - fn signing_keys(&self) -> Result, KeyManagerError> { + /// TODO: made public to use in challenge-response. Consider refactoring key manager. + pub fn signing_keys(&self) -> Result, KeyManagerError> { self.read_signing_keys(self.did_suffix()) } /// Gets the signing key with ID `key_id` of the attestor. - fn signing_key(&self, key_id: Option<&str>) -> Result { + /// TODO: made public to use in challenge-response. Consider refactoring key manager. + pub fn signing_key(&self, key_id: Option<&str>) -> Result { let keys = self.signing_keys()?; // If no key_id is given, return the first available key. if let Some(key_id) = key_id { From e09b598719969c5ff8a14ca7c72e364e8b9c30c4 Mon Sep 17 00:00:00 2001 From: pwochner Date: Fri, 10 Nov 2023 17:50:56 +0100 Subject: [PATCH 31/86] Http: Refactor CR structs to have optional fields for public and secret keys. Cli: improve command to present identity challenge. --- trustchain-cli/Cargo.toml | 1 + trustchain-cli/src/bin/main.rs | 35 ++-- .../src/attestation_encryption_utils.rs | 2 +- trustchain-http/src/attestation_utils.rs | 191 ++++++++++-------- trustchain-http/src/attestor.rs | 60 +++++- trustchain-http/src/requester.rs | 44 +++- 6 files changed, 225 insertions(+), 108 deletions(-) diff --git a/trustchain-cli/Cargo.toml b/trustchain-cli/Cargo.toml index e9746d5f..abf83986 100644 --- a/trustchain-cli/Cargo.toml +++ b/trustchain-cli/Cargo.toml @@ -23,3 +23,4 @@ serde_json = "1.0" ssi = {git="https://github.com/alan-turing-institute/ssi.git", branch="modify-encode-sign-jwt", features = ["http-did", "secp256k1"]} tokio = {version = "1.20.1", features = ["full"]} toml="0.7.2" +josekit = "0.8" diff --git a/trustchain-cli/src/bin/main.rs b/trustchain-cli/src/bin/main.rs index c8d3fb4d..ed1f9583 100644 --- a/trustchain-cli/src/bin/main.rs +++ b/trustchain-cli/src/bin/main.rs @@ -2,7 +2,7 @@ use clap::{arg, ArgAction, Command}; use core::panic; use serde_json::to_string_pretty; -use ssi::{jsonld::ContextLoader, jwk::JWK, ldp::LinkedDataDocument, vc::Credential}; +use ssi::{jsonld::ContextLoader, jwk::JWK, ldp::LinkedDataDocument, vc::Credential, ucan::Payload}; use std::{ fs::File, io::{self, stdin, BufReader}, @@ -13,12 +13,12 @@ use trustchain_api::{ TrustchainAPI, }; use trustchain_cli::config::cli_config; -use trustchain_core::{vc::CredentialError, verifier::Verifier, TRUSTCHAIN_DATA}; +use trustchain_core::{vc::CredentialError, verifier::Verifier, TRUSTCHAIN_DATA, utils::generate_key}; use trustchain_http::{ attestation_utils::{ - Nonce, ElementwiseSerializeDeserialize, IdentityCRInitiation, TrustchainCRError + ElementwiseSerializeDeserialize, IdentityCRInitiation, TrustchainCRError, CRIdentityChallenge }, - requester::initiate_identity_challenge, + requester::initiate_identity_challenge, attestation_encryption_utils::ssi_to_josekit_jwk, attestor::present_identity_challenge, }; use trustchain_ion::{ attest::attest_operation, create::create_operation, get_ion_resolver, verifier::IONVerifier, @@ -338,6 +338,7 @@ async fn main() -> Result<(), Box> { // get attestation request path from provided input let trustchain_dir: String = std::env::var(TRUSTCHAIN_DATA).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; let path_to_check = sub_matches.get_one::("path").unwrap(); + let did = sub_matches.get_one::("did").unwrap(); let path = PathBuf::new().join(trustchain_dir).join("attestation_requests").join(path_to_check); if !path.exists() { panic!("Provided attestation request not found. Path does not exist."); @@ -347,7 +348,7 @@ async fn main() -> Result<(), Box> { .unwrap(); // Show requester information to user and ask for confirmation to proceed println!("---------------------------------"); - println!("Requester information: {:?}", identity_initiation.unwrap().requester_details.unwrap()); + println!("Requester information: {:?}", identity_initiation.as_ref().and_then(|i| i.requester_details.as_ref())); println!("---------------------------------"); println!("Recognise this attestation request and want to proceed? (y/n)"); let mut prompt = String::new(); @@ -360,14 +361,22 @@ async fn main() -> Result<(), Box> { return Ok(()); } - let nonce = Nonce::new(); - println!("---------------------------------"); - println!("Identity challenge-response nonce: {:?}", nonce.to_string()); - // TODO: update commitment - // TODO: endpoint to send response to - // TODO: Print to terminal and instruct to send via alternative channels - println!("Please send the above nonce, update commitment and endpoint to the requester via alternative channels."); - + let temp_p_key = identity_initiation.unwrap().temp_p_key.unwrap(); + + // call function to present challenge + let identity_challenge = present_identity_challenge(&did, &temp_p_key)?; + + // print signed and encrypted payload to terminal + let payload = identity_challenge.identity_challenge_signature.as_ref().unwrap(); + println!("---------------------------------"); + println!("Payload: {:?}", payload); + println!("---------------------------------"); + println!("Please send the above payload and subdirectory to the requester via alternative channels."); + + // TODO: print subdirectory for response (will be appended to endpoint in did) + + // serialise struct + identity_challenge.elementwise_serialize(&path)?; } _ => panic!("Unrecognised CR subcommand."), }, diff --git a/trustchain-http/src/attestation_encryption_utils.rs b/trustchain-http/src/attestation_encryption_utils.rs index 49db692e..5aa14b54 100644 --- a/trustchain-http/src/attestation_encryption_utils.rs +++ b/trustchain-http/src/attestation_encryption_utils.rs @@ -10,7 +10,7 @@ use ssi::jwk::JWK; use crate::attestation_utils::TrustchainCRError; -struct Entity {} +pub struct Entity {} impl SignEncrypt for Entity {} diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index e42dde9f..5a462a4f 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -193,16 +193,17 @@ where } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Clone)] pub struct RequesterDetails { pub requester_org: String, pub operator_name: String, } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, IsEmpty)] +#[derive(Debug, Serialize, Deserialize, IsEmpty, Clone)] pub struct IdentityCRInitiation { pub temp_p_key: Option, + pub temp_s_key: Option, pub requester_details: Option, } @@ -210,6 +211,7 @@ impl IdentityCRInitiation { pub fn new() -> Self { Self { temp_p_key: None, + temp_s_key: None, requester_details: None, } } @@ -236,6 +238,17 @@ impl ElementwiseSerializeDeserialize for IdentityCRInitiation { Err(_) => None, }; + let temp_s_key_path = path.join("temp_s_key.json"); + self.temp_s_key = match File::open(&temp_s_key_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; + let requester_details_path = path.join("requester_details.json"); self.requester_details = match File::open(&requester_details_path) { Ok(file) => { @@ -247,7 +260,10 @@ impl ElementwiseSerializeDeserialize for IdentityCRInitiation { Err(_) => None, }; - if self.temp_p_key.is_none() && self.requester_details.is_none() { + if self.temp_p_key.is_none() + && self.temp_s_key.is_none() + && self.requester_details.is_none() + { return Ok(None); } @@ -257,17 +273,19 @@ impl ElementwiseSerializeDeserialize for IdentityCRInitiation { #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, IsEmpty)] -struct CRIdentityChallenge { - update_p_key: Option, - identity_nonce: Option, // make own Nonce type - identity_challenge_signature: Option, - identity_response_signature: Option, +pub struct CRIdentityChallenge { + pub update_p_key: Option, + pub update_s_key: Option, + pub identity_nonce: Option, // make own Nonce type + pub identity_challenge_signature: Option, + pub identity_response_signature: Option, } impl CRIdentityChallenge { fn new() -> Self { Self { update_p_key: None, + update_s_key: None, identity_nonce: None, identity_challenge_signature: None, identity_response_signature: None, @@ -373,6 +391,7 @@ impl TryFrom<&JwtPayload> for CRIdentityChallenge { fn try_from(value: &JwtPayload) -> Result { let mut challenge = CRIdentityChallenge { update_p_key: None, + update_s_key: None, identity_nonce: None, identity_challenge_signature: None, identity_response_signature: None, @@ -748,11 +767,11 @@ mod tests { #[test] fn test_elementwise_serialize() { - //TODO: more of an integration test // ==========| Identity CR | ============== let temp_s_key: Jwk = serde_json::from_str(TEST_TEMP_KEY).unwrap(); let initiation = IdentityCRInitiation { - temp_p_key: Some(temp_s_key.to_public_key().unwrap()), + temp_p_key: None, + temp_s_key: Some(temp_s_key.to_public_key().unwrap()), requester_details: Some(RequesterDetails { requester_org: String::from("My Org"), operator_name: String::from("John Doe"), @@ -762,6 +781,7 @@ mod tests { // identity challenge let identity_challenge = CRIdentityChallenge { update_p_key: serde_json::from_str(TEST_UPDATE_KEY).unwrap(), + update_s_key: None, identity_nonce: Some(Nonce::new()), identity_challenge_signature: Some(String::from("some challenge signature string")), identity_response_signature: Some(String::from("some response signature string")), @@ -832,6 +852,7 @@ mod tests { let result = cr_initiation.elementwise_deserialize(&temp_path); assert!(result.is_ok()); let initiation = result.unwrap().unwrap(); + assert!(initiation.temp_s_key.is_none()); assert!(initiation.temp_p_key.is_some()); assert!(initiation.requester_details.is_none()); @@ -934,7 +955,8 @@ mod tests { // Test case 1: some files exist and can be deserialised let identity_initiatiation = IdentityCRInitiation { - temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), + temp_s_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), + temp_p_key: None, requester_details: Some(RequesterDetails { requester_org: String::from("My Org"), operator_name: String::from("John Doe"), @@ -943,6 +965,7 @@ mod tests { let _ = identity_initiatiation.elementwise_serialize(&path); let identity_challenge = CRIdentityChallenge { update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), + update_s_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), identity_nonce: Some(Nonce::new()), identity_challenge_signature: Some(String::from("some challenge signature string")), identity_response_signature: Some(String::from("some response signature string")), @@ -1023,77 +1046,77 @@ mod tests { assert!(result.is_err()); } - #[test] - fn test_check_cr_status() { - let mut cr_state = CRState::new(); - // Test case 1: CR State is empty - let result = cr_state.check_cr_status().unwrap(); - assert_eq!(result, CurrentCRState::NotStarted); - - // Test case 2: some, but not all, initation information exists - cr_state.identity_cr_initiation = Some(IdentityCRInitiation { - temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), - requester_details: None, - }); - let result = cr_state.check_cr_status(); - assert_eq!(result.unwrap(), CurrentCRState::NotStarted); - - // Test case 3: identity initiation completed, identity challenge presented - cr_state.identity_cr_initiation = Some(IdentityCRInitiation { - temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), - requester_details: Some(RequesterDetails { - requester_org: String::from("My Org"), - operator_name: String::from("John Doe"), - }), - }); - cr_state.identity_challenge_response = Some(CRIdentityChallenge { - update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), - identity_nonce: Some(Nonce::new()), - identity_challenge_signature: Some(String::from("some challenge signature string")), - identity_response_signature: None, - }); - let result = cr_state.check_cr_status(); - assert_eq!(result.unwrap(), CurrentCRState::IdentityChallengeComplete); - - // Test case 4: Identity challenge response complete, content challenge initiated - cr_state.identity_challenge_response = Some(CRIdentityChallenge { - update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), - identity_nonce: Some(Nonce::new()), - identity_challenge_signature: Some(String::from("some challenge signature string")), - identity_response_signature: Some(String::from("some response signature string")), - }); - cr_state.content_cr_initiation = { - Some(ContentCRInitiation { - temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), - requester_did: Some("did:example:123456789abcdefghi".to_string()), - }) - }; - let result = cr_state.check_cr_status(); - assert_eq!(result.unwrap(), CurrentCRState::ContentCRInitiated); - - // Test case 5: Content challenge-response complete - cr_state.content_challenge_response = Some(CRContentChallenge { - content_nonce: Some(HashMap::new()), - content_challenge_signature: Some(String::from( - "some content challenge signature string", - )), - content_response_signature: Some(String::from( - "some content response signature string", - )), - }); - let result = cr_state.check_cr_status(); - assert_eq!(result.unwrap(), CurrentCRState::ContentResponseComplete); - } - #[test] - fn test_check_cr_status_inconsistent_order() { - let mut cr_state = CRState::new(); - cr_state.identity_challenge_response = Some(CRIdentityChallenge { - update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), - identity_nonce: Some(Nonce::new()), - identity_challenge_signature: Some(String::from("some challenge signature string")), - identity_response_signature: Some(String::from("some response signature string")), - }); - let result = cr_state.check_cr_status(); - assert_eq!(result.unwrap(), CurrentCRState::NotStarted); - } + // #[test] + // fn test_check_cr_status() { + // let mut cr_state = CRState::new(); + // // Test case 1: CR State is empty + // let result = cr_state.check_cr_status().unwrap(); + // assert_eq!(result, CurrentCRState::NotStarted); + + // // Test case 2: some, but not all, initation information exists + // cr_state.identity_cr_initiation = Some(IdentityCRInitiation { + // temp_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), + // requester_details: None, + // }); + // let result = cr_state.check_cr_status(); + // assert_eq!(result.unwrap(), CurrentCRState::NotStarted); + + // // Test case 3: identity initiation completed, identity challenge presented + // cr_state.identity_cr_initiation = Some(IdentityCRInitiation { + // temp_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), + // requester_details: Some(RequesterDetails { + // requester_org: String::from("My Org"), + // operator_name: String::from("John Doe"), + // }), + // }); + // cr_state.identity_challenge_response = Some(CRIdentityChallenge { + // update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), + // identity_nonce: Some(Nonce::new()), + // identity_challenge_signature: Some(String::from("some challenge signature string")), + // identity_response_signature: None, + // }); + // let result = cr_state.check_cr_status(); + // assert_eq!(result.unwrap(), CurrentCRState::IdentityChallengeComplete); + + // // Test case 4: Identity challenge response complete, content challenge initiated + // cr_state.identity_challenge_response = Some(CRIdentityChallenge { + // update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), + // identity_nonce: Some(Nonce::new()), + // identity_challenge_signature: Some(String::from("some challenge signature string")), + // identity_response_signature: Some(String::from("some response signature string")), + // }); + // cr_state.content_cr_initiation = { + // Some(ContentCRInitiation { + // temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), + // requester_did: Some("did:example:123456789abcdefghi".to_string()), + // }) + // }; + // let result = cr_state.check_cr_status(); + // assert_eq!(result.unwrap(), CurrentCRState::ContentCRInitiated); + + // // Test case 5: Content challenge-response complete + // cr_state.content_challenge_response = Some(CRContentChallenge { + // content_nonce: Some(HashMap::new()), + // content_challenge_signature: Some(String::from( + // "some content challenge signature string", + // )), + // content_response_signature: Some(String::from( + // "some content response signature string", + // )), + // }); + // let result = cr_state.check_cr_status(); + // assert_eq!(result.unwrap(), CurrentCRState::ContentResponseComplete); + // } + // #[test] + // fn test_check_cr_status_inconsistent_order() { + // let mut cr_state = CRState::new(); + // cr_state.identity_challenge_response = Some(CRIdentityChallenge { + // update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), + // identity_nonce: Some(Nonce::new()), + // identity_challenge_signature: Some(String::from("some challenge signature string")), + // identity_response_signature: Some(String::from("some response signature string")), + // }); + // let result = cr_state.check_cr_status(); + // assert_eq!(result.unwrap(), CurrentCRState::NotStarted); + // } } diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index 5ace246b..d1334417 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -1,6 +1,9 @@ -use crate::attestation_encryption_utils::{josekit_to_ssi_jwk, ssi_to_josekit_jwk}; +use crate::attestation_encryption_utils::{ + josekit_to_ssi_jwk, ssi_to_josekit_jwk, Entity, SignEncrypt, +}; use crate::attestation_utils::{ - attestation_request_path, ElementwiseSerializeDeserialize, IdentityCRInitiation, + attestation_request_path, CRIdentityChallenge, ElementwiseSerializeDeserialize, + IdentityCRInitiation, Nonce, TrustchainCRError, }; use crate::{errors::TrustchainHTTPError, state::AppState}; use async_trait::async_trait; @@ -10,6 +13,8 @@ use axum::{ Json, }; use hyper::StatusCode; +use josekit::jwk::Jwk; +use josekit::jwt::JwtPayload; use log::{debug, info, log}; use rand::Rng; use rand::{distributions::Alphanumeric, thread_rng}; @@ -18,7 +23,11 @@ use serde_json::to_string_pretty; use sha2::{Digest, Sha256}; use std::io::Write; use std::{fs::OpenOptions, path::Path, path::PathBuf, sync::Arc}; +use trustchain_core::key_manager::AttestorKeyManager; +use trustchain_core::subject::Subject; +use trustchain_core::utils::generate_key; use trustchain_core::TRUSTCHAIN_DATA; +use trustchain_ion::attestor::IONAttestor; // Fields: // - API access token @@ -125,12 +134,54 @@ impl TrustchainAttestorHTTPHandler { } } +pub fn present_identity_challenge( + did: &str, + temp_p_key: &Jwk, +) -> Result { + // generate nonce and update key + let nonce = Nonce::new(); + let update_s_key_ssi = generate_key(); + let update_p_key_ssi = update_s_key_ssi.to_public(); + let update_s_key = ssi_to_josekit_jwk(&update_s_key_ssi) + .map_err(|_| TrustchainCRError::FailedToGenerateKey)?; + let update_p_key = ssi_to_josekit_jwk(&update_p_key_ssi) + .map_err(|_| TrustchainCRError::FailedToGenerateKey)?; + // let update_p_key_string = serde_json::to_string_pretty(&update_p_key)?; + + let mut identity_challenge = CRIdentityChallenge { + update_p_key: Some(update_p_key), + update_s_key: Some(update_s_key), + identity_nonce: Some(nonce), + identity_challenge_signature: None, + identity_response_signature: None, + }; + + // make payload + let payload = JwtPayload::try_from(&identity_challenge).unwrap(); + + // get signing key from ION attestor + let ion_attestor = IONAttestor::new(did); + let signing_keys = ion_attestor.signing_keys().unwrap(); + let signing_key_ssi = signing_keys.first().unwrap(); + let signing_key = + ssi_to_josekit_jwk(&signing_key_ssi).map_err(|_| TrustchainCRError::FailedToGenerateKey)?; + + // sign (with pub key) and encrypt (with temp_p_key) payload + let attestor = Entity {}; + let signed_encrypted_challenge = + attestor.sign_and_encrypt_claim(&payload, &signing_key, &temp_p_key); + identity_challenge.identity_challenge_signature = Some(signed_encrypted_challenge?); + + Ok(identity_challenge) +} + #[cfg(test)] mod tests { use crate::{ attestation_utils::RequesterDetails, config::HTTPConfig, server::TrustchainRouter, }; use axum_test_helper::TestClient; + use ssi::jwk::JWK; use super::*; @@ -142,8 +193,11 @@ mod tests { #[tokio::test] #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] async fn test_post_initiation() { + let temp_s_key_ssi: JWK = serde_json::from_str(TEST_TEMP_KEY).unwrap(); + let temp_p_key_ssi = temp_s_key_ssi.to_public(); let attestation_initiation: IdentityCRInitiation = IdentityCRInitiation { - temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), + temp_s_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), + temp_p_key: Some(ssi_to_josekit_jwk(&temp_p_key_ssi).unwrap()), requester_details: Some(RequesterDetails { requester_org: "myTrustworthyEntity".to_string(), operator_name: "trustworthyOperator".to_string(), diff --git a/trustchain-http/src/requester.rs b/trustchain-http/src/requester.rs index de193ab6..87cde2cc 100644 --- a/trustchain-http/src/requester.rs +++ b/trustchain-http/src/requester.rs @@ -1,12 +1,13 @@ +use josekit::jwt::JwtPayload; use ssi::did::Service; use trustchain_core::utils::generate_key; use crate::{ - attestation_encryption_utils::ssi_to_josekit_jwk, + attestation_encryption_utils::{ssi_to_josekit_jwk, Entity}, attestation_utils::TrustchainCRError, attestation_utils::{ - attestation_request_path, matching_endpoint, ElementwiseSerializeDeserialize, - IdentityCRInitiation, RequesterDetails, + attestation_request_path, matching_endpoint, CRIdentityChallenge, + ElementwiseSerializeDeserialize, IdentityCRInitiation, RequesterDetails, }, }; @@ -22,18 +23,29 @@ pub async fn initiate_identity_challenge( ) -> Result<(), TrustchainCRError> { // generate temp key let temp_s_key_ssi = generate_key(); + let temp_p_key_ssi = temp_s_key_ssi.to_public(); let temp_s_key = ssi_to_josekit_jwk(&temp_s_key_ssi).map_err(|_| TrustchainCRError::FailedToGenerateKey)?; + let temp_p_key = + ssi_to_josekit_jwk(&temp_p_key_ssi).map_err(|_| TrustchainCRError::FailedToGenerateKey)?; // make identity_cr_initiation struct let requester = RequesterDetails { requester_org: org_name, operator_name: op_name, }; - let identity_cr_initiation = IdentityCRInitiation { - temp_p_key: temp_s_key.to_public_key().ok(), - requester_details: Some(requester), + let mut identity_cr_initiation = IdentityCRInitiation { + temp_s_key: None, + temp_p_key: Some(temp_p_key.clone()), + requester_details: Some(requester.clone()), }; + + // let identity_cr_initiation_attestor = IdentityCRInitiation { + // temp_p_key: Some(temp_p_key), + // temp_s_key: None, + // requester_details: Some(requester), + // }; + // extract URI from service endpoint println!("Services: {:?}", services); let uri = matching_endpoint(services, "Trustchain").unwrap(); // this is just to make current example work @@ -56,9 +68,27 @@ pub async fn initiate_identity_challenge( let directory = attestation_request_path(&temp_s_key_ssi.to_public())?; std::fs::create_dir_all(&directory).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; - // serialise identity_cr_initiation + // serialise identity_cr_initiation struct to file + identity_cr_initiation.temp_s_key = Some(temp_s_key); identity_cr_initiation.elementwise_serialize(&directory)?; println!("Successfully initiated attestation request."); println!("You will receive more information on the challenge-response process via alternative communication channel."); Ok(()) } + +pub fn identity_response( + challenge_payload: JwtPayload, + identity_initiation: IdentityCRInitiation, + endpoint: String, + upstream_p_key: String, +) -> Result<(), TrustchainCRError> { + // TODO: get all required keys: temp_s_key and public key upstream + // TODO: decrypt and verify challenge + let requester = Entity {}; + // let decrypted_verified_challenge = requester + // .decrypt_and_verify(challenge_payload, &temp_s_key, &upstream_p_key) + // .unwrap(); + // TODO: sign and encrypt response + // TODO: send response to endpoint + todo!("Implement identity response") +} From 73f8c732524aacb57a876ee4602648a731c2fef4 Mon Sep 17 00:00:00 2001 From: pwochner Date: Thu, 16 Nov 2023 15:47:46 +0100 Subject: [PATCH 32/86] Remove unused imports. Command for identity cr present and respond. --- trustchain-cli/src/bin/main.rs | 60 ++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/trustchain-cli/src/bin/main.rs b/trustchain-cli/src/bin/main.rs index ed1f9583..135fc518 100644 --- a/trustchain-cli/src/bin/main.rs +++ b/trustchain-cli/src/bin/main.rs @@ -2,23 +2,23 @@ use clap::{arg, ArgAction, Command}; use core::panic; use serde_json::to_string_pretty; -use ssi::{jsonld::ContextLoader, jwk::JWK, ldp::LinkedDataDocument, vc::Credential, ucan::Payload}; +use ssi::{jsonld::ContextLoader, ldp::LinkedDataDocument, vc::Credential}; use std::{ fs::File, io::{self, stdin, BufReader}, - path::{Path, PathBuf}, + path::PathBuf, }; use trustchain_api::{ api::{TrustchainDIDAPI, TrustchainVCAPI}, TrustchainAPI, }; use trustchain_cli::config::cli_config; -use trustchain_core::{vc::CredentialError, verifier::Verifier, TRUSTCHAIN_DATA, utils::generate_key}; +use trustchain_core::{vc::CredentialError, verifier::Verifier, TRUSTCHAIN_DATA, utils:: extract_keys}; use trustchain_http::{ attestation_utils::{ - ElementwiseSerializeDeserialize, IdentityCRInitiation, TrustchainCRError, CRIdentityChallenge + ElementwiseSerializeDeserialize, IdentityCRInitiation, TrustchainCRError }, - requester::initiate_identity_challenge, attestation_encryption_utils::ssi_to_josekit_jwk, attestor::present_identity_challenge, + requester::{initiate_identity_challenge, identity_response}, attestation_encryption_utils::ssi_to_josekit_jwk, attestor::present_identity_challenge, }; use trustchain_ion::{ attest::attest_operation, create::create_operation, get_ion_resolver, verifier::IONVerifier, @@ -103,13 +103,21 @@ fn cli() -> Command { Command::new("initiate") .about("Initiates a new identity challenge-response process.") .arg(arg!(-v - -verbose).action(ArgAction::Count)) - .arg(arg!(-d --did ).required(true)) + .arg(arg!(-d --did ).required(true)) ) .subcommand( Command::new("present") - .about("Produce challenges to be presented to requestor.") + .about("Produce challenge for identity CR to be presented to requestor.") .arg(arg!(-v - -verbose).action(ArgAction::Count)) .arg(arg!(-p --path ).required(true)) + .arg(arg!(-d --did ).required(true)) + ) + .subcommand( + Command::new("respond") + .about("Produce response for identity challenge to be posted to attestor.") + .arg(arg!(-v - -verbose).action(ArgAction::Count)) + .arg(arg!(-p --path ).required(true)) + .arg(arg!(-d --did ).required(true)) ) ) @@ -308,9 +316,8 @@ async fn main() -> Result<(), Box> { // resolve DID and extract endpoint let did = sub_matches.get_one::("did").unwrap(); let (_, doc, _) = TrustchainAPI::resolve(did, resolver).await?; - // let endpoints = doc.unwrap().get_endpoints().unwrap(); // TODO: this is a vec => which endpoint? let services = doc.unwrap().service; - + // user promt for org name and operator name println!("Please enter your organisation name: "); let mut org_name = String::new(); @@ -348,7 +355,8 @@ async fn main() -> Result<(), Box> { .unwrap(); // Show requester information to user and ask for confirmation to proceed println!("---------------------------------"); - println!("Requester information: {:?}", identity_initiation.as_ref().and_then(|i| i.requester_details.as_ref())); + println!("Requester information: "); + println!("{:?}", identity_initiation.as_ref().unwrap().requester_details.as_ref().unwrap()); println!("---------------------------------"); println!("Recognise this attestation request and want to proceed? (y/n)"); let mut prompt = String::new(); @@ -361,6 +369,7 @@ async fn main() -> Result<(), Box> { return Ok(()); } + let temp_p_key = identity_initiation.unwrap().temp_p_key.unwrap(); // call function to present challenge @@ -369,15 +378,38 @@ async fn main() -> Result<(), Box> { // print signed and encrypted payload to terminal let payload = identity_challenge.identity_challenge_signature.as_ref().unwrap(); println!("---------------------------------"); - println!("Payload: {:?}", payload); + println!("Payload:"); + println!("{:?}", payload); + println!("Path: /did/attestor/identity/respond/"); println!("---------------------------------"); - println!("Please send the above payload and subdirectory to the requester via alternative channels."); - - // TODO: print subdirectory for response (will be appended to endpoint in did) + println!("Please send the above payload and path to the requester via alternative channels."); + println!("To respond, the requester posts it to the provided path, which has to be appended to the attestor endpoint."); + println!(" has to be replaced by the key_id of the temporary public key provided in the initial request."); // serialise struct identity_challenge.elementwise_serialize(&path)?; } + Some(("respond", sub_matches)) => { + // get attestation request path from provided input + let trustchain_dir: String = std::env::var(TRUSTCHAIN_DATA).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; + let path_to_check = sub_matches.get_one::("path").unwrap(); + let path = PathBuf::new().join(trustchain_dir).join("attestation_requests").join(path_to_check); + if !path.exists() { + panic!("Provided attestation request not found. Path does not exist."); + } + let did = sub_matches.get_one::("did").unwrap(); + let (_, doc, _) = TrustchainAPI::resolve(did, resolver).await?; + let doc = doc.unwrap(); + // extract attestor public key from did document + let public_keys = extract_keys(&doc); + let attestor_public_key_ssi = public_keys.first().unwrap(); + let public_key = ssi_to_josekit_jwk(attestor_public_key_ssi).unwrap(); + // url path and service endpoint + let url_path = String::from("/did/attestor/identity/respond/"); + let services = doc.service.unwrap(); + println!("Path: {:?}", path); + identity_response(path, services, url_path, public_key).await?; + } _ => panic!("Unrecognised CR subcommand."), }, _ => panic!("Unrecognised CR subcommand."), From f4c0115653e4762278876f1e2265526fad5a0b93 Mon Sep 17 00:00:00 2001 From: pwochner Date: Thu, 16 Nov 2023 15:48:41 +0100 Subject: [PATCH 33/86] Make DecryptVerify trait public. --- trustchain-http/src/attestation_encryption_utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trustchain-http/src/attestation_encryption_utils.rs b/trustchain-http/src/attestation_encryption_utils.rs index 5aa14b54..bac8eaa1 100644 --- a/trustchain-http/src/attestation_encryption_utils.rs +++ b/trustchain-http/src/attestation_encryption_utils.rs @@ -53,7 +53,7 @@ pub trait SignEncrypt { } } /// Interface for decrypting and then verifying data. -trait DecryptVerify { +pub trait DecryptVerify { /// Decrypts a payload with a secret key. fn decrypt(&self, value: &Value, secret_key: &Jwk) -> Result { let decrypter = ECDH_ES.decrypter_from_jwk(&secret_key)?; From 94ad443f8e1cfd29cf967dcec23a2be85570eea8 Mon Sep 17 00:00:00 2001 From: pwochner Date: Thu, 16 Nov 2023 15:49:36 +0100 Subject: [PATCH 34/86] make CRIdentityChallenge struct public. More error variants. --- trustchain-http/src/attestation_utils.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index 5a462a4f..7b330c87 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -71,6 +71,15 @@ pub enum TrustchainCRError { /// Failed attestation request #[error("Failed attestation request.")] FailedAttestationRequest, + /// Field of struct not found + #[error("Field not found.")] + FieldNotFound, + /// Field to respond + #[error("Response to challenge failed.")] + FailedToRespond, + // Failed to verify nonce + #[error("Failed to verify nonce.")] + FailedToVerifyNonce, } impl From for TrustchainCRError { @@ -282,7 +291,7 @@ pub struct CRIdentityChallenge { } impl CRIdentityChallenge { - fn new() -> Self { + pub fn new() -> Self { Self { update_p_key: None, update_s_key: None, From c1f2b11cd2e5fdf5cedbe51e3a97761e27402c1c Mon Sep 17 00:00:00 2001 From: pwochner Date: Thu, 16 Nov 2023 15:51:57 +0100 Subject: [PATCH 35/86] Add functionality for route handler identity CR response. Remove unused imports. --- trustchain-http/src/attestor.rs | 138 ++++++++++++++++++++++++++------ 1 file changed, 114 insertions(+), 24 deletions(-) diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index d1334417..7932fe52 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -1,30 +1,29 @@ use crate::attestation_encryption_utils::{ - josekit_to_ssi_jwk, ssi_to_josekit_jwk, Entity, SignEncrypt, + josekit_to_ssi_jwk, ssi_to_josekit_jwk, DecryptVerify, Entity, SignEncrypt, }; use crate::attestation_utils::{ attestation_request_path, CRIdentityChallenge, ElementwiseSerializeDeserialize, IdentityCRInitiation, Nonce, TrustchainCRError, }; -use crate::{errors::TrustchainHTTPError, state::AppState}; + use async_trait::async_trait; -use axum::extract::path; +use axum::extract::Path; use axum::{ - response::{Html, IntoResponse, Response}, + response::{Html, IntoResponse}, Json, }; use hyper::StatusCode; use josekit::jwk::Jwk; use josekit::jwt::JwtPayload; -use log::{debug, info, log}; -use rand::Rng; -use rand::{distributions::Alphanumeric, thread_rng}; +use log::info; + use serde::{Deserialize, Serialize}; -use serde_json::to_string_pretty; -use sha2::{Digest, Sha256}; -use std::io::Write; -use std::{fs::OpenOptions, path::Path, path::PathBuf, sync::Arc}; -use trustchain_core::key_manager::AttestorKeyManager; -use trustchain_core::subject::Subject; + +use std::fs::File; +use std::io::BufReader; +use std::path::PathBuf; +use std::result; + use trustchain_core::utils::generate_key; use trustchain_core::TRUSTCHAIN_DATA; use trustchain_ion::attestor::IONAttestor; @@ -75,17 +74,10 @@ use trustchain_ion::attestor::IONAttestor; // Encryption: https://github.com/hidekatsu-izuno/josekit-rs#signing-a-jwt-by-ecdsa -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct AttestationInfo { - // api_access_token: JWT, - // temp_pub_key: JWK, - api_access_token: String, - temp_pub_key: String, - // TODO: change temp_pub_key - // temp_pub_key: JWK, - name_downstream: String, - name_operator: String, +#[derive(Serialize)] +struct CustomResponse { + message: String, + path: Option, } #[async_trait] @@ -132,6 +124,66 @@ impl TrustchainAttestorHTTPHandler { let _ = std::fs::create_dir_all(&path); let _ = attestation_initiation.elementwise_serialize(&path).map(|_| (StatusCode::OK, Html("Received request. Please wait for operator to contact you through an alternative channel."))); } + + pub async fn post_response( + Path((did, key_id)): Path<(String, String)>, + Json(response): Json, + ) -> impl IntoResponse { + // get keys (attestor secret key, temp public key) + let trustchain_dir: String = std::env::var(TRUSTCHAIN_DATA).unwrap(); + let path = PathBuf::new() + .join(trustchain_dir) + .join("attestation_requests") + .join(&key_id); + if !path.exists() { + panic!("Provided attestation request not found. Path does not exist."); + } + // deserialise + let mut identity_challenge = CRIdentityChallenge::new() + .elementwise_deserialize(&path) + .unwrap() + .unwrap(); + // get signing key from ION attestor + let ion_attestor = IONAttestor::new(&did); + let signing_keys = ion_attestor.signing_keys().unwrap(); + let signing_key_ssi = signing_keys.first().unwrap(); + let signing_key = ssi_to_josekit_jwk(&signing_key_ssi); + // get temp public key + info!("Path: {:?}", path); + let temp_key_path = path.join("temp_p_key.json"); + let file = File::open(&temp_key_path).unwrap(); + let reader = BufReader::new(file); + let temp_p_key_ssi = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize) + .unwrap(); + let temp_p_key = ssi_to_josekit_jwk(&temp_p_key_ssi).unwrap(); + + // decrypt and verify + let attestor = Entity {}; + let payload = attestor + .decrypt_and_verify(response.clone(), &signing_key.unwrap(), &temp_p_key) + .unwrap(); + + let result = verify_nonce(payload, &path); + match result { + Ok(_) => { + identity_challenge.identity_response_signature = Some(response.clone()); + identity_challenge.elementwise_serialize(&path).unwrap(); + let respone = CustomResponse { + message: "Verification successful. Please use the provided path to initiate the second part of the attestation process.".to_string(), + path:Some(format!("/did/attestor/content/initiate/{}", &key_id)), + }; + (StatusCode::OK, Json(respone)); + } + Err(_) => { + let respone = CustomResponse { + message: "Verification failed. Please try again.".to_string(), + path: None, + }; + (StatusCode::BAD_REQUEST, Json(respone)); + } + } + } } pub fn present_identity_challenge( @@ -175,13 +227,31 @@ pub fn present_identity_challenge( Ok(identity_challenge) } +fn verify_nonce(payload: JwtPayload, path: &PathBuf) -> Result<(), TrustchainCRError> { + // get nonce from payload + let nonce = payload.claim("identity_nonce").unwrap().as_str().unwrap(); + // deserialise expected nonce + let nonce_path = path.join("identity_nonce.json"); + let file = File::open(&nonce_path).unwrap(); + let reader = BufReader::new(file); + let expected_nonce: String = + serde_json::from_reader(reader).map_err(|_| TrustchainCRError::FailedToDeserialize)?; + + if nonce != expected_nonce { + return Err(TrustchainCRError::FailedToVerifyNonce); + } + Ok(()) +} + #[cfg(test)] mod tests { + use crate::data::TEST_UPDATE_KEY; use crate::{ attestation_utils::RequesterDetails, config::HTTPConfig, server::TrustchainRouter, }; use axum_test_helper::TestClient; use ssi::jwk::JWK; + use tempfile::tempdir; use super::*; @@ -214,4 +284,24 @@ mod tests { println!("Response text: {:?}", response.text().await); // assert_eq!(response.text().await, "Received request. Please wait for operator to contact you through an alternative channel."); } + + #[test] + fn test_verify_nonce() { + let temp_path = tempdir().unwrap().into_path(); + let expected_nonce = Nonce::from(String::from("test_nonce")); + let identity_challenge = CRIdentityChallenge { + update_p_key: serde_json::from_str(TEST_UPDATE_KEY).unwrap(), + update_s_key: None, + identity_nonce: Some(expected_nonce.clone()), + identity_challenge_signature: None, + identity_response_signature: None, + }; + identity_challenge + .elementwise_serialize(&temp_path) + .unwrap(); + // make payload + let payload = JwtPayload::try_from(&identity_challenge).unwrap(); + let result = verify_nonce(payload, &temp_path); + assert!(result.is_ok()); + } } From a78d82a8681e369eca5c1b14ce848b1be4b6d03b Mon Sep 17 00:00:00 2001 From: pwochner Date: Thu, 16 Nov 2023 15:53:02 +0100 Subject: [PATCH 36/86] Add functionality to post response to identity challenge. --- trustchain-http/src/requester.rs | 105 +++++++++++++++++++++++-------- 1 file changed, 79 insertions(+), 26 deletions(-) diff --git a/trustchain-http/src/requester.rs b/trustchain-http/src/requester.rs index 87cde2cc..fcce1cad 100644 --- a/trustchain-http/src/requester.rs +++ b/trustchain-http/src/requester.rs @@ -1,14 +1,21 @@ -use josekit::jwt::JwtPayload; -use ssi::did::Service; +use std::{fs::File, io::BufReader, path::PathBuf}; + +use josekit::jwk::Jwk; +use ssi::{ + did::{Service, ServiceEndpoint}, + vc::OneOrMany, +}; use trustchain_core::utils::generate_key; use crate::{ - attestation_encryption_utils::{ssi_to_josekit_jwk, Entity}, - attestation_utils::TrustchainCRError, + attestation_encryption_utils::{ + josekit_to_ssi_jwk, ssi_to_josekit_jwk, DecryptVerify, Entity, SignEncrypt, + }, attestation_utils::{ attestation_request_path, matching_endpoint, CRIdentityChallenge, ElementwiseSerializeDeserialize, IdentityCRInitiation, RequesterDetails, }, + attestation_utils::{Nonce, TrustchainCRError}, }; /// Initiates the identity challenge-response process by sending a POST request to the upstream endpoint. @@ -40,16 +47,9 @@ pub async fn initiate_identity_challenge( requester_details: Some(requester.clone()), }; - // let identity_cr_initiation_attestor = IdentityCRInitiation { - // temp_p_key: Some(temp_p_key), - // temp_s_key: None, - // requester_details: Some(requester), - // }; - // extract URI from service endpoint - println!("Services: {:?}", services); - let uri = matching_endpoint(services, "Trustchain").unwrap(); // this is just to make current example work - // let uri = matching_endpoint(services, "identity-cr").unwrap(); // TODO: use this one once we have example published + // TODO: this is just to make current example work + let uri = matching_endpoint(services, "Trustchain").unwrap(); // make POST request to endpoint let client = reqwest::Client::new(); @@ -76,19 +76,72 @@ pub async fn initiate_identity_challenge( Ok(()) } -pub fn identity_response( - challenge_payload: JwtPayload, - identity_initiation: IdentityCRInitiation, - endpoint: String, - upstream_p_key: String, +pub async fn identity_response( + path: PathBuf, + services: Vec, + url_path: String, + attestor_p_key: Jwk, ) -> Result<(), TrustchainCRError> { - // TODO: get all required keys: temp_s_key and public key upstream - // TODO: decrypt and verify challenge + // deserialise challenge struct from file + let result = CRIdentityChallenge::new().elementwise_deserialize(&path); + let mut identity_challenge = result.unwrap().unwrap(); + let identity_initiation = IdentityCRInitiation::new().elementwise_deserialize(&path); + let temp_s_key = identity_initiation.unwrap().unwrap().temp_s_key.unwrap(); + let temp_s_key_ssi = josekit_to_ssi_jwk(&temp_s_key).unwrap(); + + // decrypt and verify challenge let requester = Entity {}; - // let decrypted_verified_challenge = requester - // .decrypt_and_verify(challenge_payload, &temp_s_key, &upstream_p_key) - // .unwrap(); - // TODO: sign and encrypt response - // TODO: send response to endpoint - todo!("Implement identity response") + let decrypted_verified_payload = requester + .decrypt_and_verify( + identity_challenge + .identity_challenge_signature + .clone() + .unwrap(), + &temp_s_key, + &attestor_p_key, + ) + .unwrap(); + // sign and encrypt response + let signed_encrypted_response = requester + .sign_and_encrypt_claim(&decrypted_verified_payload, &temp_s_key, &attestor_p_key) + .unwrap(); + println!( + "Signed and encrypted response: {:?}", + signed_encrypted_response + ); + let key_id = temp_s_key_ssi.to_public().thumbprint().unwrap(); + // get uri for POST request response + let endpoint = &services.first().unwrap().service_endpoint; + let endpoint = match endpoint { + Some(OneOrMany::One(ServiceEndpoint::URI(uri))) => uri, + + _ => Err(TrustchainCRError::InvalidServiceEndpoint)?, + }; + let uri = format!("{}{}{}", endpoint, url_path, key_id); + // POST response + let client = reqwest::Client::new(); + let result = client + .post(uri) + .json(&signed_encrypted_response) + .send() + .await + .map_err(|err| TrustchainCRError::Reqwest(err))?; + if result.status() != 200 { + println!("Status code: {}", result.status()); + return Err(TrustchainCRError::FailedToRespond); + } + // extract nonce + let nonce_str = decrypted_verified_payload + .claim("identity_nonce") + .unwrap() + .as_str() + .unwrap(); + let nonce = Nonce::from(String::from(nonce_str)); + // update struct + identity_challenge.update_p_key = Some(attestor_p_key); + identity_challenge.identity_nonce = Some(nonce); + identity_challenge.identity_response_signature = Some(signed_encrypted_response); + // serialise + identity_challenge.elementwise_serialize(&path)?; + Ok(()) } From 51cdc8b2fa4ce7f9bcda8510def58b90df499afb Mon Sep 17 00:00:00 2001 From: pwochner Date: Thu, 16 Nov 2023 15:53:59 +0100 Subject: [PATCH 37/86] Add routes for identity CR present challenge and post response. --- trustchain-http/src/server.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/trustchain-http/src/server.rs b/trustchain-http/src/server.rs index 6c888a71..f6f589f6 100644 --- a/trustchain-http/src/server.rs +++ b/trustchain-http/src/server.rs @@ -97,9 +97,17 @@ impl TrustchainRouter { get(root::TrustchainRootHTTPHandler::get_block_timestamp), ) .route( - "/did/attestor/initiate", + "/did/attestor/identity/initiate", post(attestor::TrustchainAttestorHTTPHandler::post_initiation), ) + .route( + "/did/attestor/identity/respond/:did/:key_id", + post(attestor::TrustchainAttestorHTTPHandler::post_response), + ) + // .route( + // "/did/attestor/content/:key_id", + // post(attestor::TrustchainAttestorHTTPHandler::post_initiation), + // ) .with_state(shared_state), } } From 508fda4b4d6620330dcfd4d08e9ed4d1197a832b Mon Sep 17 00:00:00 2001 From: pwochner Date: Fri, 24 Nov 2023 11:23:20 +0000 Subject: [PATCH 38/86] Remove temp public key from ContentCRInitiation struct. Make structs and functions public. Fix tests for checking CR status. --- trustchain-http/src/attestation_utils.rs | 216 +++++++++++------------ 1 file changed, 105 insertions(+), 111 deletions(-) diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index 7b330c87..46b48d98 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -89,7 +89,7 @@ impl From for TrustchainCRError { } #[derive(Debug, PartialEq)] -enum CurrentCRState { +pub enum CurrentCRState { NotStarted, IdentityCRInitiated, IdentityChallengeComplete, @@ -421,21 +421,19 @@ impl TryFrom<&JwtPayload> for CRIdentityChallenge { } #[derive(Debug, Serialize, Deserialize, Clone, IsEmpty)] -struct ContentCRInitiation { - temp_p_key: Option, - requester_did: Option, +pub struct ContentCRInitiation { + pub requester_did: Option, } impl ContentCRInitiation { - fn new() -> Self { + pub fn new() -> Self { Self { - temp_p_key: None, requester_did: None, } } fn is_complete(&self) -> bool { - return self.temp_p_key.is_some() && self.requester_did.is_some(); + return self.requester_did.is_some(); } } @@ -445,17 +443,6 @@ impl ElementwiseSerializeDeserialize for ContentCRInitiation { mut self, path: &PathBuf, ) -> Result, TrustchainCRError> { - let temp_p_key_path = path.join("temp_p_key.json"); - self.temp_p_key = match File::open(&temp_p_key_path) { - Ok(file) => { - let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; - Some(deserialized) - } - Err(_) => None, - }; - let requester_details_path = path.join("requester_did.json"); self.requester_did = match File::open(&requester_details_path) { Ok(file) => { @@ -467,7 +454,10 @@ impl ElementwiseSerializeDeserialize for ContentCRInitiation { Err(_) => None, }; - if self.temp_p_key.is_none() && self.requester_did.is_none() { + // if self.temp_p_key.is_none() && self.requester_did.is_none() { + // return Ok(None); + // } + if self.requester_did.is_none() { return Ok(None); } @@ -476,14 +466,14 @@ impl ElementwiseSerializeDeserialize for ContentCRInitiation { } #[derive(Debug, Serialize, Deserialize, IsEmpty)] -struct CRContentChallenge { - content_nonce: Option>, - content_challenge_signature: Option, - content_response_signature: Option, +pub struct CRContentChallenge { + pub content_nonce: Option>, + pub content_challenge_signature: Option, + pub content_response_signature: Option, } impl CRContentChallenge { - fn new() -> Self { + pub fn new() -> Self { Self { content_nonce: None, content_challenge_signature: None, @@ -554,15 +544,15 @@ impl ElementwiseSerializeDeserialize for CRContentChallenge { #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, IsEmpty)] -struct CRState { - identity_cr_initiation: Option, - identity_challenge_response: Option, - content_cr_initiation: Option, - content_challenge_response: Option, +pub struct CRState { + pub identity_cr_initiation: Option, + pub identity_challenge_response: Option, + pub content_cr_initiation: Option, + pub content_challenge_response: Option, } impl CRState { - fn new() -> Self { + pub fn new() -> Self { Self { identity_cr_initiation: None, identity_challenge_response: None, @@ -571,7 +561,7 @@ impl CRState { } } /// Returns true if all fields have a non-null value. - fn is_complete(&self) -> bool { + pub fn is_complete(&self) -> bool { if self.identity_cr_initiation.is_some() && self.identity_challenge_response.is_some() && self.content_cr_initiation.is_some() @@ -582,7 +572,7 @@ impl CRState { return false; } /// Determines current status of the challenge response process and accordingly prints messages to the console. - fn check_cr_status(&self) -> Result { + pub fn check_cr_status(&self) -> Result { println!("Checking current challenge-response status..."); println!(" "); let mut current_state = CurrentCRState::NotStarted; @@ -765,10 +755,7 @@ pub fn attestation_request_path(key: &JWK) -> Result #[cfg(test)] mod tests { use crate::attestation_encryption_utils::extract_key_ids_and_jwk; - use crate::data::{ - TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS, TEST_SIGNING_KEY_1, TEST_SIGNING_KEY_2, - TEST_TEMP_KEY, TEST_UPDATE_KEY, TEST_UPSTREAM_KEY, - }; + use crate::data::{TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS, TEST_TEMP_KEY, TEST_UPDATE_KEY}; use ssi::did::Document; use tempfile::tempdir; @@ -798,7 +785,7 @@ mod tests { // ==========| Content CR | ============== let content_initiation = ContentCRInitiation { - temp_p_key: Some(temp_s_key.to_public_key().unwrap()), + // temp_p_key: Some(temp_s_key.to_public_key().unwrap()), requester_did: Some("did:example:123456789abcdefghi".to_string()), }; // get signing keys for DE from did document @@ -982,7 +969,7 @@ mod tests { let _ = identity_challenge.elementwise_serialize(&path); let content_cr_initiation = ContentCRInitiation { - temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), + // temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), requester_did: Some("did:example:123456789abcdefghi".to_string()), }; let _ = content_cr_initiation.elementwise_serialize(&path); @@ -1055,77 +1042,84 @@ mod tests { assert!(result.is_err()); } - // #[test] - // fn test_check_cr_status() { - // let mut cr_state = CRState::new(); - // // Test case 1: CR State is empty - // let result = cr_state.check_cr_status().unwrap(); - // assert_eq!(result, CurrentCRState::NotStarted); - - // // Test case 2: some, but not all, initation information exists - // cr_state.identity_cr_initiation = Some(IdentityCRInitiation { - // temp_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), - // requester_details: None, - // }); - // let result = cr_state.check_cr_status(); - // assert_eq!(result.unwrap(), CurrentCRState::NotStarted); - - // // Test case 3: identity initiation completed, identity challenge presented - // cr_state.identity_cr_initiation = Some(IdentityCRInitiation { - // temp_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), - // requester_details: Some(RequesterDetails { - // requester_org: String::from("My Org"), - // operator_name: String::from("John Doe"), - // }), - // }); - // cr_state.identity_challenge_response = Some(CRIdentityChallenge { - // update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), - // identity_nonce: Some(Nonce::new()), - // identity_challenge_signature: Some(String::from("some challenge signature string")), - // identity_response_signature: None, - // }); - // let result = cr_state.check_cr_status(); - // assert_eq!(result.unwrap(), CurrentCRState::IdentityChallengeComplete); - - // // Test case 4: Identity challenge response complete, content challenge initiated - // cr_state.identity_challenge_response = Some(CRIdentityChallenge { - // update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), - // identity_nonce: Some(Nonce::new()), - // identity_challenge_signature: Some(String::from("some challenge signature string")), - // identity_response_signature: Some(String::from("some response signature string")), - // }); - // cr_state.content_cr_initiation = { - // Some(ContentCRInitiation { - // temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), - // requester_did: Some("did:example:123456789abcdefghi".to_string()), - // }) - // }; - // let result = cr_state.check_cr_status(); - // assert_eq!(result.unwrap(), CurrentCRState::ContentCRInitiated); - - // // Test case 5: Content challenge-response complete - // cr_state.content_challenge_response = Some(CRContentChallenge { - // content_nonce: Some(HashMap::new()), - // content_challenge_signature: Some(String::from( - // "some content challenge signature string", - // )), - // content_response_signature: Some(String::from( - // "some content response signature string", - // )), - // }); - // let result = cr_state.check_cr_status(); - // assert_eq!(result.unwrap(), CurrentCRState::ContentResponseComplete); - // } - // #[test] - // fn test_check_cr_status_inconsistent_order() { - // let mut cr_state = CRState::new(); - // cr_state.identity_challenge_response = Some(CRIdentityChallenge { - // update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), - // identity_nonce: Some(Nonce::new()), - // identity_challenge_signature: Some(String::from("some challenge signature string")), - // identity_response_signature: Some(String::from("some response signature string")), - // }); - // let result = cr_state.check_cr_status(); - // assert_eq!(result.unwrap(), CurrentCRState::NotStarted); - // } + #[test] + fn test_check_cr_status() { + let mut cr_state = CRState::new(); + // Test case 1: CR State is empty + let result = cr_state.check_cr_status().unwrap(); + assert_eq!(result, CurrentCRState::NotStarted); + + // Test case 2: some, but not all, initation information exists + cr_state.identity_cr_initiation = Some(IdentityCRInitiation { + // Same key used here for testing purposes + temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), + temp_s_key: None, + requester_details: None, + }); + let result = cr_state.check_cr_status(); + assert_eq!(result.unwrap(), CurrentCRState::NotStarted); + + // Test case 3: identity initiation completed, identity challenge presented + cr_state.identity_cr_initiation = Some(IdentityCRInitiation { + // Same key used here for testing purposes + temp_p_key: Some(serde_json::from_str(TEST_TEMP_KEY).unwrap()), + temp_s_key: None, + requester_details: Some(RequesterDetails { + requester_org: String::from("My Org"), + operator_name: String::from("John Doe"), + }), + }); + cr_state.identity_challenge_response = Some(CRIdentityChallenge { + update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), + update_s_key: None, + identity_nonce: Some(Nonce::new()), + identity_challenge_signature: Some(String::from("some challenge signature string")), + identity_response_signature: None, + }); + let result = cr_state.check_cr_status(); + assert_eq!(result.unwrap(), CurrentCRState::IdentityChallengeComplete); + + // Test case 4: Identity challenge response complete, content challenge initiated + cr_state.identity_challenge_response = Some(CRIdentityChallenge { + // Same key used here for testing purposes + update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), + update_s_key: None, + identity_nonce: Some(Nonce::new()), + identity_challenge_signature: Some(String::from("some challenge signature string")), + identity_response_signature: Some(String::from("some response signature string")), + }); + cr_state.content_cr_initiation = { + Some(ContentCRInitiation { + requester_did: Some("did:example:123456789abcdefghi".to_string()), + }) + }; + let result = cr_state.check_cr_status(); + assert_eq!(result.unwrap(), CurrentCRState::ContentCRInitiated); + + // Test case 5: Content challenge-response complete + cr_state.content_challenge_response = Some(CRContentChallenge { + content_nonce: Some(HashMap::new()), + content_challenge_signature: Some(String::from( + "some content challenge signature string", + )), + content_response_signature: Some(String::from( + "some content response signature string", + )), + }); + let result = cr_state.check_cr_status(); + assert_eq!(result.unwrap(), CurrentCRState::ContentResponseComplete); + } + #[test] + fn test_check_cr_status_inconsistent_order() { + let mut cr_state = CRState::new(); + cr_state.identity_challenge_response = Some(CRIdentityChallenge { + update_s_key: None, + update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), + identity_nonce: Some(Nonce::new()), + identity_challenge_signature: Some(String::from("some challenge signature string")), + identity_response_signature: Some(String::from("some response signature string")), + }); + let result = cr_state.check_cr_status(); + assert_eq!(result.unwrap(), CurrentCRState::NotStarted); + } } From 3d9d8227c93c05a2dee17048852a3beffb725e50 Mon Sep 17 00:00:00 2001 From: pwochner Date: Fri, 24 Nov 2023 11:28:13 +0000 Subject: [PATCH 39/86] Routes and route handlers for content CR. Remove unused code. Add doc strings. --- trustchain-http/Cargo.toml | 1 + trustchain-http/src/attestor.rs | 247 ++++++++++++++++++++++--------- trustchain-http/src/lib.rs | 1 - trustchain-http/src/requester.rs | 189 ++++++++++++++++++++++- trustchain-http/src/server.rs | 44 +++++- 5 files changed, 398 insertions(+), 84 deletions(-) diff --git a/trustchain-http/Cargo.toml b/trustchain-http/Cargo.toml index 06f18ac8..c2a9a510 100644 --- a/trustchain-http/Cargo.toml +++ b/trustchain-http/Cargo.toml @@ -14,6 +14,7 @@ path = "src/bin/main.rs" trustchain-core = { path = "../trustchain-core" } trustchain-ion = { path = "../trustchain-ion" } trustchain-api = { path = "../trustchain-api" } + async-trait = "0.1" axum = "0.6" axum-server = {version="0.5.1", features = ["tls-rustls"] } diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index 7932fe52..cfcf1e36 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -1,11 +1,12 @@ use crate::attestation_encryption_utils::{ - josekit_to_ssi_jwk, ssi_to_josekit_jwk, DecryptVerify, Entity, SignEncrypt, + extract_key_ids_and_jwk, josekit_to_ssi_jwk, ssi_to_josekit_jwk, DecryptVerify, Entity, + SignEncrypt, }; use crate::attestation_utils::{ - attestation_request_path, CRIdentityChallenge, ElementwiseSerializeDeserialize, - IdentityCRInitiation, Nonce, TrustchainCRError, + attestation_request_path, CRContentChallenge, CRIdentityChallenge, + ElementwiseSerializeDeserialize, IdentityCRInitiation, Nonce, TrustchainCRError, }; - +use crate::state::AppState; use async_trait::async_trait; use axum::extract::Path; use axum::{ @@ -17,67 +18,28 @@ use josekit::jwk::Jwk; use josekit::jwt::JwtPayload; use log::info; -use serde::{Deserialize, Serialize}; +use serde::Serialize; +use trustchain_api::api::TrustchainDIDAPI; +use trustchain_api::TrustchainAPI; +use trustchain_core::verifier::Verifier; +use std::collections::HashMap; use std::fs::File; use std::io::BufReader; use std::path::PathBuf; -use std::result; +use std::sync::Arc; use trustchain_core::utils::generate_key; use trustchain_core::TRUSTCHAIN_DATA; use trustchain_ion::attestor::IONAttestor; -// Fields: -// - API access token -// - temporary public key -// - name of DE organisation ("name_downstream") -// - name of individual operator within DE responsible for the request - -// /// Writes received attestation request to unique path derived from the public key for the interaction. -// fn write_attestation_info( -// attestation_info: &IdentityCRInitiation, -// ) -> Result<(), TrustchainHTTPError> { -// // Get environment for TRUSTCHAIN_DATA - -// let directory = attestion_request_path(&attestation_info.temp_p_key.unwrap().to_string())?; - -// // Make directory if non-existent -// // Equivalent of os.makedirs(exist_ok=True) in python -// std::fs::create_dir_all(&directory) -// .map_err(|_| TrustchainHTTPError::FailedAttestationRequest)?; - -// // Check if initial request exists ("attestation_info.json"), if yes, return InternalServerError -// let full_path = directory.join("attestation_info.json"); - -// if full_path.exists() { -// return Err(TrustchainHTTPError::FailedAttestationRequest); -// } - -// // If not, write to file -// // Open the new file -// let mut file = OpenOptions::new() -// .create(true) -// .write(true) -// .truncate(true) -// .open(full_path) -// .map_err(|_| TrustchainHTTPError::FailedAttestationRequest)?; - -// // Write to file -// writeln!(file, "{}", &to_string_pretty(attestation_info).unwrap()) -// .map_err(|_| TrustchainHTTPError::FailedAttestationRequest)?; - -// // Else do something? - -// Ok(()) -// } - // Encryption: https://github.com/hidekatsu-izuno/josekit-rs#signing-a-jwt-by-ecdsa #[derive(Serialize)] struct CustomResponse { message: String, path: Option, + data: Option, } #[async_trait] @@ -111,10 +73,9 @@ impl TrustchainAttestorHTTP for TrustchainAttestorHTTPHandler { } impl TrustchainAttestorHTTPHandler { - /// Processes initial attestation request and provided data - pub async fn post_initiation( + /// Processes initial attestation request and provided data. + pub async fn post_identity_initiation( Json(attestation_initiation): Json, - // app_state: Arc, ) -> impl IntoResponse { info!("Received attestation info: {:?}", attestation_initiation); let temp_p_key_ssi = @@ -125,11 +86,11 @@ impl TrustchainAttestorHTTPHandler { let _ = attestation_initiation.elementwise_serialize(&path).map(|_| (StatusCode::OK, Html("Received request. Please wait for operator to contact you through an alternative channel."))); } - pub async fn post_response( - Path((did, key_id)): Path<(String, String)>, - Json(response): Json, + /// Processes response to identity challenge. + pub async fn post_identity_response( + (Path(key_id), Json(response)): (Path, Json), + app_state: Arc, ) -> impl IntoResponse { - // get keys (attestor secret key, temp public key) let trustchain_dir: String = std::env::var(TRUSTCHAIN_DATA).unwrap(); let path = PathBuf::new() .join(trustchain_dir) @@ -138,18 +99,17 @@ impl TrustchainAttestorHTTPHandler { if !path.exists() { panic!("Provided attestation request not found. Path does not exist."); } - // deserialise let mut identity_challenge = CRIdentityChallenge::new() .elementwise_deserialize(&path) .unwrap() .unwrap(); // get signing key from ION attestor + let did = app_state.config.server_did.as_ref().unwrap().to_owned(); let ion_attestor = IONAttestor::new(&did); let signing_keys = ion_attestor.signing_keys().unwrap(); let signing_key_ssi = signing_keys.first().unwrap(); let signing_key = ssi_to_josekit_jwk(&signing_key_ssi); // get temp public key - info!("Path: {:?}", path); let temp_key_path = path.join("temp_p_key.json"); let file = File::open(&temp_key_path).unwrap(); let reader = BufReader::new(file); @@ -158,12 +118,11 @@ impl TrustchainAttestorHTTPHandler { .unwrap(); let temp_p_key = ssi_to_josekit_jwk(&temp_p_key_ssi).unwrap(); - // decrypt and verify + // verify response let attestor = Entity {}; let payload = attestor .decrypt_and_verify(response.clone(), &signing_key.unwrap(), &temp_p_key) .unwrap(); - let result = verify_nonce(payload, &path); match result { Ok(_) => { @@ -171,21 +130,176 @@ impl TrustchainAttestorHTTPHandler { identity_challenge.elementwise_serialize(&path).unwrap(); let respone = CustomResponse { message: "Verification successful. Please use the provided path to initiate the second part of the attestation process.".to_string(), - path:Some(format!("/did/attestor/content/initiate/{}", &key_id)), + path: Some(format!("/did/attestor/content/initiate/{}", &key_id)), + data: None }; - (StatusCode::OK, Json(respone)); + (StatusCode::OK, respone); } Err(_) => { - let respone = CustomResponse { + let response = CustomResponse { message: "Verification failed. Please try again.".to_string(), path: None, + data: None, + }; + (StatusCode::BAD_REQUEST, response); + } + } + } + + /// Processes initiation of second part of attestation request (content challenge-response). + pub async fn post_content_initiation( + (Path(key_id), Json(ddid)): (Path, Json), + app_state: Arc, + ) -> impl IntoResponse { + // TODO: Do this properly (get endpoint from config). + let did = app_state.config.server_did.as_ref().unwrap().to_owned(); + let result = TrustchainAPI::resolve(&ddid, app_state.verifier.resolver()).await; + let candidate_doc = match result { + Ok((_, doc, _)) => doc.unwrap(), + Err(_) => { + let respone = CustomResponse { + message: "Resolution of candidate DID failed.".to_string(), + path: None, + data: None, + }; + return (StatusCode::BAD_REQUEST, Json(respone)); + } + }; + + // extract map of keys from candidate document and generate a nonce per key + let requester_keys = extract_key_ids_and_jwk(&candidate_doc).unwrap(); + let attestor = Entity {}; + let nonces: HashMap = + requester_keys + .iter() + .fold(HashMap::new(), |mut acc, (key_id, _)| { + acc.insert(String::from(key_id), Nonce::new()); + acc + }); + + // sign and encrypt nonces to generate challenges + let challenges = nonces + .iter() + .fold(HashMap::new(), |mut acc, (key_id, nonce)| { + acc.insert( + String::from(key_id), + attestor + .encrypt( + &JwtPayload::try_from(nonce).unwrap(), + &requester_keys.get(key_id).unwrap(), + ) + .unwrap(), + ); + acc + }); + // get public and secret keys + let path = PathBuf::new() + .join(std::env::var(TRUSTCHAIN_DATA).unwrap()) + .join("attestation_requests") + .join(&key_id); + let identity_cr_initiation = IdentityCRInitiation::new() + .elementwise_deserialize(&path) + .unwrap() + .unwrap(); + let ion_attestor = IONAttestor::new(&did); + let signing_keys = ion_attestor.signing_keys().unwrap(); + let signing_key_ssi = signing_keys.first().unwrap(); + let signing_key = ssi_to_josekit_jwk(&signing_key_ssi).unwrap(); + + // sign and encrypt challenges + let value: serde_json::Value = serde_json::to_value(challenges).unwrap(); + let mut payload = JwtPayload::new(); + payload.set_claim("challenges", Some(value)).unwrap(); + let signed_encrypted_challenges = attestor.sign_and_encrypt_claim( + &payload, + &signing_key, + &identity_cr_initiation.temp_p_key.unwrap(), + ); + + match signed_encrypted_challenges { + Ok(signed_encrypted_challenges) => { + let content_challenge = CRContentChallenge { + content_nonce: Some(nonces), + content_challenge_signature: Some(signed_encrypted_challenges.clone()), + content_response_signature: None, + }; + content_challenge.elementwise_serialize(&path).unwrap(); + let response = CustomResponse { + message: "Challenges generated successfully.".to_string(), + path: None, + data: Some(signed_encrypted_challenges), + }; + (StatusCode::OK, Json(response)) + } + Err(_) => { + let response = CustomResponse { + message: "Failed to generate challenges.".to_string(), + path: None, + data: None, }; - (StatusCode::BAD_REQUEST, Json(respone)); + (StatusCode::BAD_REQUEST, Json(response)) } } } + /// Processes response to second part of attestation request (content challenge-response). + pub async fn post_content_response( + (Path(key_id), Json(response)): (Path, Json), + app_state: Arc, + ) -> impl IntoResponse { + // deserialise expected nonce map + let path = PathBuf::new() + .join(std::env::var(TRUSTCHAIN_DATA).unwrap()) + .join("attestation_requests") + .join(&key_id); + let identity_cr_initiation = IdentityCRInitiation::new() + .elementwise_deserialize(&path) + .unwrap() + .unwrap(); + let mut content_challenge = CRContentChallenge::new() + .elementwise_deserialize(&path) + .unwrap() + .unwrap(); + let expected_nonce = content_challenge.content_nonce.clone().unwrap(); + // get signing key from ION attestor + let did = app_state.config.server_did.as_ref().unwrap().to_owned(); + let ion_attestor = IONAttestor::new(&did); + let signing_keys = ion_attestor.signing_keys().unwrap(); + let signing_key_ssi = signing_keys.first().unwrap(); + let signing_key = ssi_to_josekit_jwk(&signing_key_ssi).unwrap(); + + // decrypt and verify response => nonces map + let attestor = Entity {}; + let payload = attestor + .decrypt_and_verify( + response.clone(), + &signing_key, + &identity_cr_initiation.temp_p_key.unwrap(), + ) + .unwrap(); + let nonces_map: HashMap = + serde_json::from_value(payload.claim("nonces").unwrap().clone()).unwrap(); + // verify nonces + if nonces_map.eq(&expected_nonce) { + content_challenge.content_response_signature = Some(response.clone()); + content_challenge.elementwise_serialize(&path).unwrap(); + let response = CustomResponse { + message: "Attestation request successful.".to_string(), + path: None, + data: None, + }; + return (StatusCode::OK, Json(response)); + } + + let response = CustomResponse { + message: "Verification failed. Attestation request unsuccessful.".to_string(), + path: None, + data: None, + }; + (StatusCode::BAD_REQUEST, Json(response)) + } } +/// Generates challenge for first part of attestation request (identity challenge-response). pub fn present_identity_challenge( did: &str, temp_p_key: &Jwk, @@ -227,6 +341,7 @@ pub fn present_identity_challenge( Ok(identity_challenge) } +/// Verifies nonce for challenge-response. fn verify_nonce(payload: JwtPayload, path: &PathBuf) -> Result<(), TrustchainCRError> { // get nonce from payload let nonce = payload.claim("identity_nonce").unwrap().as_str().unwrap(); @@ -255,7 +370,6 @@ mod tests { use super::*; - // TODO: add this key when switched to JWK use crate::data::TEST_TEMP_KEY; // Attestor integration tests @@ -282,7 +396,6 @@ mod tests { let response = client.post(&uri).json(&attestation_initiation).send().await; assert_eq!(response.status(), 200); println!("Response text: {:?}", response.text().await); - // assert_eq!(response.text().await, "Received request. Please wait for operator to contact you through an alternative channel."); } #[test] diff --git a/trustchain-http/src/lib.rs b/trustchain-http/src/lib.rs index 106199c2..adc20b90 100644 --- a/trustchain-http/src/lib.rs +++ b/trustchain-http/src/lib.rs @@ -3,7 +3,6 @@ pub mod attestation_utils; pub mod attestor; pub mod config; pub mod data; -pub mod encryption; pub mod errors; pub mod issuer; pub mod middleware; diff --git a/trustchain-http/src/requester.rs b/trustchain-http/src/requester.rs index fcce1cad..f978677e 100644 --- a/trustchain-http/src/requester.rs +++ b/trustchain-http/src/requester.rs @@ -1,32 +1,36 @@ -use std::{fs::File, io::BufReader, path::PathBuf}; +use std::{collections::HashMap, path::PathBuf}; -use josekit::jwk::Jwk; +use josekit::{jwk::Jwk, jwt::JwtPayload}; +use serde_json::Value; use ssi::{ did::{Service, ServiceEndpoint}, vc::OneOrMany, }; use trustchain_core::utils::generate_key; +use trustchain_ion::attestor::IONAttestor; use crate::{ attestation_encryption_utils::{ josekit_to_ssi_jwk, ssi_to_josekit_jwk, DecryptVerify, Entity, SignEncrypt, }, attestation_utils::{ - attestation_request_path, matching_endpoint, CRIdentityChallenge, - ElementwiseSerializeDeserialize, IdentityCRInitiation, RequesterDetails, + attestation_request_path, matching_endpoint, CRContentChallenge, CRIdentityChallenge, + ContentCRInitiation, ElementwiseSerializeDeserialize, IdentityCRInitiation, + RequesterDetails, }, attestation_utils::{Nonce, TrustchainCRError}, }; -/// Initiates the identity challenge-response process by sending a POST request to the upstream endpoint. +/// Initiates the identity challenge-response process by sending a POST request to the attestor endpoint. /// /// This function generates a temporary key to use as an identifier throughout the challenge-response process. /// It prompts the user to provide the organization name and operator name, which are included in the POST request -/// to the endpoint specified in the upstream's DID document. +/// to the endpoint specified in the attestor's DID document. pub async fn initiate_identity_challenge( org_name: String, op_name: String, services: &Vec, + url_path: &String, ) -> Result<(), TrustchainCRError> { // generate temp key let temp_s_key_ssi = generate_key(); @@ -47,9 +51,16 @@ pub async fn initiate_identity_challenge( requester_details: Some(requester.clone()), }; - // extract URI from service endpoint + // get endpoint and uir // TODO: this is just to make current example work - let uri = matching_endpoint(services, "Trustchain").unwrap(); + // let uri = matching_endpoint(services, "Trustchain").unwrap(); + let endpoint = &services.first().unwrap().service_endpoint; + let endpoint = match endpoint { + Some(OneOrMany::One(ServiceEndpoint::URI(uri))) => uri, + + _ => Err(TrustchainCRError::InvalidServiceEndpoint)?, + }; + let uri = format!("{}{}", endpoint, url_path); // make POST request to endpoint let client = reqwest::Client::new(); @@ -76,6 +87,11 @@ pub async fn initiate_identity_challenge( Ok(()) } +/// Generates the response for the identity challenge-response process and makes a POST request to the attestor endpoint. +/// +/// This function first decrypts and verifies the challenge received from attestor to extract challenge nonce. +/// It then signs the nonce with the requester's temporary secret key and encrypts it with the attestor's public key, +/// before posting the response to the attestor's endpoint, using the provided url path. pub async fn identity_response( path: PathBuf, services: Vec, @@ -145,3 +161,160 @@ pub async fn identity_response( identity_challenge.elementwise_serialize(&path)?; Ok(()) } + +/// Initiates the content challenge-response process by sending a POST request to the attestor endpoint. +/// +/// This function makes a POST request with the candidate DID (dDID) to the attestor endpoint, using the url path received during +/// the identity challenge-response. +pub async fn initiate_content_challenge( + path: PathBuf, + ddid: &String, + services: &Vec, + url_path: &String, +) -> Result<(), TrustchainCRError> { + // deserialise identity_cr_initiation and get key id + let identity_cr_initiation = IdentityCRInitiation::new() + .elementwise_deserialize(&path) + .unwrap() + .unwrap(); + let temp_s_key_ssi = josekit_to_ssi_jwk(&identity_cr_initiation.temp_s_key.unwrap()).unwrap(); + let key_id = temp_s_key_ssi.to_public().thumbprint().unwrap(); + + let content_cr_initiation = ContentCRInitiation { + requester_did: Some(ddid.clone()), + }; + + // get uri for POST request response + let endpoint = &services.first().unwrap().service_endpoint; + let endpoint = match endpoint { + Some(OneOrMany::One(ServiceEndpoint::URI(uri))) => uri, + + _ => Err(TrustchainCRError::InvalidServiceEndpoint)?, + }; + let uri = format!("{}{}/{}", endpoint, url_path, key_id); + println!("URI: {}", uri); + // make POST request to endpoint + let client = reqwest::Client::new(); + let result = client + .post(uri) + .json(&ddid) + .send() + .await + .map_err(|err| TrustchainCRError::Reqwest(err))?; + + if result.status() != 200 { + println!("Status code: {}", result.status()); + return Err(TrustchainCRError::FailedToInitiateCR); + } + + // serialise struct to file + content_cr_initiation.elementwise_serialize(&path)?; + Ok(()) +} + +/// Generates the response for the content challenge-response process and makes a POST request to +/// the attestor endpoint. +/// +/// This function first decrypts (temporary secret key) and verifies (attestor's public key) the +/// challenge received from attestor to extract challenge nonces. It then decrypts each nonce with the corresponding +/// signing key from the requestor's candidate DID (dDID) document, before posting the signed (temporary secret key) +/// and encrypted (attestor's public key) response to the attestor's endpoint, using the provided url path. +pub async fn content_response( + path: PathBuf, + services: Vec, + url_path: &String, + attestor_p_key: Jwk, + ddid: &String, +) -> Result<(), TrustchainCRError> { + // deserialise challenge struct from file + let result = CRContentChallenge::new().elementwise_deserialize(&path); + let mut content_challenge = result.unwrap().unwrap(); + let challenge = content_challenge + .content_challenge_signature + .clone() + .unwrap(); + + // get keys + let identity_initiation = IdentityCRInitiation::new().elementwise_deserialize(&path); + let temp_s_key = identity_initiation.unwrap().unwrap().temp_s_key.unwrap(); + let temp_s_key_ssi = josekit_to_ssi_jwk(&temp_s_key).unwrap(); + // get endpoint + let key_id = temp_s_key_ssi.to_public().thumbprint().unwrap(); + let endpoint = &services.first().unwrap().service_endpoint; + let endpoint = match endpoint { + Some(OneOrMany::One(ServiceEndpoint::URI(uri))) => uri, + + _ => Err(TrustchainCRError::InvalidServiceEndpoint)?, + }; + let uri = format!("{}{}{}", endpoint, url_path, key_id); + + // decrypt and verify payload + let requester = Entity {}; + let decrypted_verified_payload = requester + .decrypt_and_verify(challenge, &temp_s_key, &attestor_p_key) + .unwrap(); + // extract map with decrypted nonces from payload and decrypt each nonce + let challenges_map: HashMap = serde_json::from_value( + decrypted_verified_payload + .claim("challenges") + .unwrap() + .clone(), + ) + .unwrap(); + + // keymap with requester secret keys + let ion_attestor = IONAttestor::new(&ddid); + let signing_keys = ion_attestor.signing_keys().unwrap(); + // iterate over all keys, convert to Jwk (josekit) -> TODO: functional + let mut signing_keys_map: HashMap = HashMap::new(); + for key in signing_keys { + let key_id = key.thumbprint().unwrap(); + let jwk = ssi_to_josekit_jwk(&key).unwrap(); + signing_keys_map.insert(key_id, jwk); + } + + let decrypted_nonces: HashMap = + challenges_map + .iter() + .fold(HashMap::new(), |mut acc, (key_id, nonce)| { + acc.insert( + String::from(key_id), + requester + .decrypt( + &Some(Value::from(nonce.clone())).unwrap(), + signing_keys_map.get(key_id).unwrap(), + ) + .unwrap() + .claim("nonce") + .unwrap() + .as_str() + .unwrap() + .to_string(), + ); + + acc + }); + // sign and encrypt response + let value: serde_json::Value = serde_json::to_value(decrypted_nonces).unwrap(); + let mut payload = JwtPayload::new(); + payload.set_claim("nonces", Some(value)).unwrap(); + let signed_encrypted_response = requester + .sign_and_encrypt_claim(&payload, &temp_s_key, &attestor_p_key) + .unwrap(); + // post respone to endpoint + let client = reqwest::Client::new(); + let result = client + .post(uri) + .json(&signed_encrypted_response) + .send() + .await + .map_err(|err| TrustchainCRError::Reqwest(err))?; + if result.status() != 200 { + println!("Status code: {}", result.status()); + return Err(TrustchainCRError::FailedToRespond); + } + // serialise + content_challenge.content_response_signature = Some(signed_encrypted_response); + content_challenge.elementwise_serialize(&path)?; + Ok(()) +} diff --git a/trustchain-http/src/server.rs b/trustchain-http/src/server.rs index f6f589f6..706ac275 100644 --- a/trustchain-http/src/server.rs +++ b/trustchain-http/src/server.rs @@ -97,17 +97,45 @@ impl TrustchainRouter { get(root::TrustchainRootHTTPHandler::get_block_timestamp), ) .route( - "/did/attestor/identity/initiate", - post(attestor::TrustchainAttestorHTTPHandler::post_initiation), + "/did/attestor/identity/initiate/", + post(attestor::TrustchainAttestorHTTPHandler::post_identity_initiation), ) .route( - "/did/attestor/identity/respond/:did/:key_id", - post(attestor::TrustchainAttestorHTTPHandler::post_response), + "/did/attestor/identity/respond/:key_id", + // post(attestor::TrustchainAttestorHTTPHandler::post_response), + post({ + let state = shared_state.clone(); + move |key_id| { + attestor::TrustchainAttestorHTTPHandler::post_identity_response( + key_id, state, + ) + } + }), + ) + .route( + "/did/attestor/content/initiate/:key_id", + // post(attestor::TrustchainAttestorHTTPHandler::post_content_initiation), + post({ + let state = shared_state.clone(); + move |(key_id, ddid)| { + attestor::TrustchainAttestorHTTPHandler::post_content_initiation( + (key_id, ddid), + state, + ) + } + }), + ) + .route( + "/did/attestor/content/respond/:key_id", + post({ + let state = shared_state.clone(); + move |key_id| { + attestor::TrustchainAttestorHTTPHandler::post_content_response( + key_id, state, + ) + } + }), ) - // .route( - // "/did/attestor/content/:key_id", - // post(attestor::TrustchainAttestorHTTPHandler::post_initiation), - // ) .with_state(shared_state), } } From db9575ceae5dbb257934b7ad8db236105dd451e2 Mon Sep 17 00:00:00 2001 From: pwochner Date: Fri, 24 Nov 2023 11:31:07 +0000 Subject: [PATCH 40/86] Cli commands for: content CR initiate, content CR respond, CR complete. Improve help for cli. --- trustchain-cli/src/bin/main.rs | 105 +++++++++++++++++++++++++++++---- 1 file changed, 95 insertions(+), 10 deletions(-) diff --git a/trustchain-cli/src/bin/main.rs b/trustchain-cli/src/bin/main.rs index 135fc518..5ea3e8e0 100644 --- a/trustchain-cli/src/bin/main.rs +++ b/trustchain-cli/src/bin/main.rs @@ -16,9 +16,9 @@ use trustchain_cli::config::cli_config; use trustchain_core::{vc::CredentialError, verifier::Verifier, TRUSTCHAIN_DATA, utils:: extract_keys}; use trustchain_http::{ attestation_utils::{ - ElementwiseSerializeDeserialize, IdentityCRInitiation, TrustchainCRError + ElementwiseSerializeDeserialize, IdentityCRInitiation, TrustchainCRError, CRState }, - requester::{initiate_identity_challenge, identity_response}, attestation_encryption_utils::ssi_to_josekit_jwk, attestor::present_identity_challenge, + requester::{initiate_identity_challenge, identity_response, initiate_content_challenge, content_response}, attestation_encryption_utils::ssi_to_josekit_jwk, attestor::present_identity_challenge, }; use trustchain_ion::{ attest::attest_operation, create::create_operation, get_ion_resolver, verifier::IONVerifier, @@ -84,13 +84,12 @@ fn cli() -> Command { .about("Verifies a credential.") .arg(arg!(-v - -verbose).action(ArgAction::Count)) .arg(arg!(-f --credential_file ).required(false)) - .arg(arg!(-t --root_event_time ).required(false)), + .arg(arg!(-t --root_event_time ).required(false)) ), ) - .subcommand( // Pam: change this + .subcommand( Command::new("cr") - // .about("Challenge-response functionality: initiate, present, respond.") - .about("Challenge-response functionality for identity challenge-response and content challenge-response.") + .about("Challenge-response functionality for attestation challenge response process (identity and content challenge-response).") .subcommand_required(true) .arg_required_else_help(true) .allow_external_subcommands(true) @@ -104,6 +103,7 @@ fn cli() -> Command { .about("Initiates a new identity challenge-response process.") .arg(arg!(-v - -verbose).action(ArgAction::Count)) .arg(arg!(-d --did ).required(true)) + .arg(arg!(-p --urlpath ).required(true)) ) .subcommand( Command::new("present") @@ -120,6 +120,35 @@ fn cli() -> Command { .arg(arg!(-d --did ).required(true)) ) ) + .subcommand( + Command::new("content") + .about("Content challenge-response functionality: initiate, respond.") + .arg(arg!(-v - -verbose).action(ArgAction::SetTrue)) + .arg(arg!(-f --file_path ).required(false)) + .subcommand( + Command::new("initiate") + .about("Initiates the content challenge-response process.") + .arg(arg!(-v - -verbose).action(ArgAction::Count)) + .arg(arg!(-d --did ).required(true)) + .arg(arg!(-d --ddid ).required(true)) + .arg(arg!(-p --path ).required(true)) + .arg(arg!(-p --urlpath ).required(true)) + ) + .subcommand( + Command::new("respond") + .about("Respond to content challenge.") + .arg(arg!(-v - -verbose).action(ArgAction::Count)) + .arg(arg!(-d --did ).required(true)) + .arg(arg!(-d --ddid ).required(true)) + .arg(arg!(-p --path ).required(true)) + .arg(arg!(-p --urlpath ).required(true)) + )) + .subcommand( + Command::new("complete") + .about("Check if challenge-response for attestation request has been completed.") + .arg(arg!(-v - -verbose).action(ArgAction::SetTrue)) + .arg(arg!(-p --path ).required(true)) + ) ) } @@ -314,9 +343,11 @@ async fn main() -> Result<(), Box> { Some(("identity", sub_matches)) => match sub_matches.subcommand() { Some(("initiate", sub_matches)) => { // resolve DID and extract endpoint + let url_path = sub_matches.get_one::("url_path").unwrap(); let did = sub_matches.get_one::("did").unwrap(); let (_, doc, _) = TrustchainAPI::resolve(did, resolver).await?; let services = doc.unwrap().service; + println!("Services: {:?}", services); // user promt for org name and operator name println!("Please enter your organisation name: "); @@ -334,12 +365,14 @@ async fn main() -> Result<(), Box> { println!("Organisation name: {}", org_name); println!("Operator name: {}", op_name); // initiate identity challenge - initiate_identity_challenge( + let result = initiate_identity_challenge( org_name.trim().to_string(), op_name.trim().to_string(), &services.unwrap(), + url_path ) .await?; + println!("Result: {:?}", result); } Some(("present", sub_matches)) => { // get attestation request path from provided input @@ -410,11 +443,63 @@ async fn main() -> Result<(), Box> { println!("Path: {:?}", path); identity_response(path, services, url_path, public_key).await?; } - _ => panic!("Unrecognised CR subcommand."), + _ => panic!("Unrecognised CR identity subcommand."), + }, + Some(("content", sub_matches)) => match sub_matches.subcommand() { + Some(("initiate", sub_matches)) => { + let did = sub_matches.get_one::("did").unwrap(); + let ddid = sub_matches.get_one::("ddid").unwrap(); + let path_to_check = sub_matches.get_one::("path").unwrap(); + let url_path = sub_matches.get_one::("urlpath").unwrap(); + + // check attestation request path + let trustchain_dir: String = std::env::var(TRUSTCHAIN_DATA).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; + let path = PathBuf::new().join(trustchain_dir).join("attestation_requests").join(path_to_check); + if !path.exists() { + panic!("Provided attestation request not found. Path does not exist."); + } + + // resolve DID and generate challenge + let (_, doc, _) = TrustchainAPI::resolve(did, resolver).await?; + let doc = doc.unwrap(); + let services = doc.service.unwrap(); + let result = initiate_content_challenge(path, ddid, &services, url_path).await?; + println!("Result: {:?}", result); + } + Some(("respond", sub_matches)) => { + // get provided input arguments + let trustchain_dir: String = std::env::var(TRUSTCHAIN_DATA).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; + let path_to_check = sub_matches.get_one::("path").unwrap(); + let path = PathBuf::new().join(trustchain_dir).join("attestation_requests").join(path_to_check); + if !path.exists() { + panic!("Provided attestation request not found. Path does not exist."); + } + let url_path = sub_matches.get_one::("url_path").unwrap(); + let did = sub_matches.get_one::("did").unwrap(); + let ddid = sub_matches.get_one::("did").unwrap(); + let (_, doc, _) = TrustchainAPI::resolve(did, resolver).await?; + let doc = doc.unwrap(); + // extract attestor public key from did document + let public_keys = extract_keys(&doc); + let attestor_public_key_ssi = public_keys.first().unwrap(); + let attestor_public_key = ssi_to_josekit_jwk(attestor_public_key_ssi).unwrap(); + // service endpoint + let services = doc.service.unwrap(); + let result = content_response(path, services, url_path, attestor_public_key, ddid).await?; + println!("Result: {:?}", result); + } + _ => panic!("Unrecognised CR content subcommand."),}, + Some(("complete", sub_matches)) => { + let path_to_check = sub_matches.get_one::("path").unwrap(); + let trustchain_dir: String = std::env::var(TRUSTCHAIN_DATA).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; + let path = PathBuf::new().join(trustchain_dir).join("attestation_requests").join(path_to_check); + let cr_state = CRState::new().elementwise_deserialize(&path).unwrap().unwrap(); + let current_state = cr_state.check_cr_status().unwrap(); + + println!("State of attestation challenge-response process: {:?}", current_state); }, _ => panic!("Unrecognised CR subcommand."), - }, - + } _ => panic!("Unrecognised subcommand."), } Ok(()) From 349091671b0433a4763733b765b9eb2f926f5210 Mon Sep 17 00:00:00 2001 From: pwochner Date: Fri, 24 Nov 2023 13:34:36 +0000 Subject: [PATCH 41/86] Hard code url paths for challenge-response. Remove as input arguments in cli. --- trustchain-cli/src/bin/main.rs | 11 ++++------- trustchain-http/src/attestor.rs | 26 +++++++++++++++++--------- trustchain-http/src/requester.rs | 16 ++++++++-------- trustchain-http/src/temp_s_key.json | 9 +++++++++ trustchain-http/src/test.json | 13 +++++++++++++ trustchain-http/src/test_p_key.json | 12 ++++++++++++ 6 files changed, 63 insertions(+), 24 deletions(-) create mode 100644 trustchain-http/src/temp_s_key.json create mode 100644 trustchain-http/src/test.json create mode 100644 trustchain-http/src/test_p_key.json diff --git a/trustchain-cli/src/bin/main.rs b/trustchain-cli/src/bin/main.rs index 5ea3e8e0..9423b852 100644 --- a/trustchain-cli/src/bin/main.rs +++ b/trustchain-cli/src/bin/main.rs @@ -103,7 +103,6 @@ fn cli() -> Command { .about("Initiates a new identity challenge-response process.") .arg(arg!(-v - -verbose).action(ArgAction::Count)) .arg(arg!(-d --did ).required(true)) - .arg(arg!(-p --urlpath ).required(true)) ) .subcommand( Command::new("present") @@ -132,7 +131,6 @@ fn cli() -> Command { .arg(arg!(-d --did ).required(true)) .arg(arg!(-d --ddid ).required(true)) .arg(arg!(-p --path ).required(true)) - .arg(arg!(-p --urlpath ).required(true)) ) .subcommand( Command::new("respond") @@ -141,7 +139,6 @@ fn cli() -> Command { .arg(arg!(-d --did ).required(true)) .arg(arg!(-d --ddid ).required(true)) .arg(arg!(-p --path ).required(true)) - .arg(arg!(-p --urlpath ).required(true)) )) .subcommand( Command::new("complete") @@ -343,7 +340,7 @@ async fn main() -> Result<(), Box> { Some(("identity", sub_matches)) => match sub_matches.subcommand() { Some(("initiate", sub_matches)) => { // resolve DID and extract endpoint - let url_path = sub_matches.get_one::("url_path").unwrap(); + let url_path = "/did/attestor/identity/initiate"; let did = sub_matches.get_one::("did").unwrap(); let (_, doc, _) = TrustchainAPI::resolve(did, resolver).await?; let services = doc.unwrap().service; @@ -438,7 +435,7 @@ async fn main() -> Result<(), Box> { let attestor_public_key_ssi = public_keys.first().unwrap(); let public_key = ssi_to_josekit_jwk(attestor_public_key_ssi).unwrap(); // url path and service endpoint - let url_path = String::from("/did/attestor/identity/respond/"); + let url_path = "/did/attestor/identity/respond"; let services = doc.service.unwrap(); println!("Path: {:?}", path); identity_response(path, services, url_path, public_key).await?; @@ -450,7 +447,7 @@ async fn main() -> Result<(), Box> { let did = sub_matches.get_one::("did").unwrap(); let ddid = sub_matches.get_one::("ddid").unwrap(); let path_to_check = sub_matches.get_one::("path").unwrap(); - let url_path = sub_matches.get_one::("urlpath").unwrap(); + let url_path = "/did/attestor/content/initiate"; // check attestation request path let trustchain_dir: String = std::env::var(TRUSTCHAIN_DATA).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; @@ -474,7 +471,7 @@ async fn main() -> Result<(), Box> { if !path.exists() { panic!("Provided attestation request not found. Path does not exist."); } - let url_path = sub_matches.get_one::("url_path").unwrap(); + let url_path = "/did/attestor/content/respond"; let did = sub_matches.get_one::("did").unwrap(); let ddid = sub_matches.get_one::("did").unwrap(); let (_, doc, _) = TrustchainAPI::resolve(did, resolver).await?; diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index cfcf1e36..6ef6a118 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -38,7 +38,6 @@ use trustchain_ion::attestor::IONAttestor; #[derive(Serialize)] struct CustomResponse { message: String, - path: Option, data: Option, } @@ -83,7 +82,23 @@ impl TrustchainAttestorHTTPHandler { let path = attestation_request_path(&temp_p_key_ssi.unwrap()).unwrap(); // create directory and save attestation initation to file let _ = std::fs::create_dir_all(&path); - let _ = attestation_initiation.elementwise_serialize(&path).map(|_| (StatusCode::OK, Html("Received request. Please wait for operator to contact you through an alternative channel."))); + let result = attestation_initiation.elementwise_serialize(&path); + match result { + Ok(_) => { + let response = CustomResponse { + message: "Received attestation request. Please wait for operator to contact you through an alternative channel.".to_string(), + data: None, + }; + (StatusCode::OK, Json(response)) + } + Err(_) => { + let response = CustomResponse { + message: "Attestation request failed.".to_string(), + data: None, + }; + (StatusCode::BAD_REQUEST, Json(response)) + } + } } /// Processes response to identity challenge. @@ -130,7 +145,6 @@ impl TrustchainAttestorHTTPHandler { identity_challenge.elementwise_serialize(&path).unwrap(); let respone = CustomResponse { message: "Verification successful. Please use the provided path to initiate the second part of the attestation process.".to_string(), - path: Some(format!("/did/attestor/content/initiate/{}", &key_id)), data: None }; (StatusCode::OK, respone); @@ -138,7 +152,6 @@ impl TrustchainAttestorHTTPHandler { Err(_) => { let response = CustomResponse { message: "Verification failed. Please try again.".to_string(), - path: None, data: None, }; (StatusCode::BAD_REQUEST, response); @@ -159,7 +172,6 @@ impl TrustchainAttestorHTTPHandler { Err(_) => { let respone = CustomResponse { message: "Resolution of candidate DID failed.".to_string(), - path: None, data: None, }; return (StatusCode::BAD_REQUEST, Json(respone)); @@ -226,7 +238,6 @@ impl TrustchainAttestorHTTPHandler { content_challenge.elementwise_serialize(&path).unwrap(); let response = CustomResponse { message: "Challenges generated successfully.".to_string(), - path: None, data: Some(signed_encrypted_challenges), }; (StatusCode::OK, Json(response)) @@ -234,7 +245,6 @@ impl TrustchainAttestorHTTPHandler { Err(_) => { let response = CustomResponse { message: "Failed to generate challenges.".to_string(), - path: None, data: None, }; (StatusCode::BAD_REQUEST, Json(response)) @@ -284,7 +294,6 @@ impl TrustchainAttestorHTTPHandler { content_challenge.elementwise_serialize(&path).unwrap(); let response = CustomResponse { message: "Attestation request successful.".to_string(), - path: None, data: None, }; return (StatusCode::OK, Json(response)); @@ -292,7 +301,6 @@ impl TrustchainAttestorHTTPHandler { let response = CustomResponse { message: "Verification failed. Attestation request unsuccessful.".to_string(), - path: None, data: None, }; (StatusCode::BAD_REQUEST, Json(response)) diff --git a/trustchain-http/src/requester.rs b/trustchain-http/src/requester.rs index f978677e..8328523c 100644 --- a/trustchain-http/src/requester.rs +++ b/trustchain-http/src/requester.rs @@ -30,7 +30,7 @@ pub async fn initiate_identity_challenge( org_name: String, op_name: String, services: &Vec, - url_path: &String, + url_path: &str, ) -> Result<(), TrustchainCRError> { // generate temp key let temp_s_key_ssi = generate_key(); @@ -71,8 +71,8 @@ pub async fn initiate_identity_challenge( .await .map_err(|err| TrustchainCRError::Reqwest(err))?; + println!("Status code: {}", result.status()); if result.status() != 200 { - println!("Status code: {}", result.status()); return Err(TrustchainCRError::FailedToInitiateCR); } // create new directory @@ -95,7 +95,7 @@ pub async fn initiate_identity_challenge( pub async fn identity_response( path: PathBuf, services: Vec, - url_path: String, + url_path: &str, attestor_p_key: Jwk, ) -> Result<(), TrustchainCRError> { // deserialise challenge struct from file @@ -133,7 +133,7 @@ pub async fn identity_response( _ => Err(TrustchainCRError::InvalidServiceEndpoint)?, }; - let uri = format!("{}{}{}", endpoint, url_path, key_id); + let uri = format!("{}{}/{}", endpoint, url_path, key_id); // POST response let client = reqwest::Client::new(); let result = client @@ -142,8 +142,8 @@ pub async fn identity_response( .send() .await .map_err(|err| TrustchainCRError::Reqwest(err))?; + println!("Status code: {}", result.status()); if result.status() != 200 { - println!("Status code: {}", result.status()); return Err(TrustchainCRError::FailedToRespond); } // extract nonce @@ -170,7 +170,7 @@ pub async fn initiate_content_challenge( path: PathBuf, ddid: &String, services: &Vec, - url_path: &String, + url_path: &str, ) -> Result<(), TrustchainCRError> { // deserialise identity_cr_initiation and get key id let identity_cr_initiation = IdentityCRInitiation::new() @@ -222,7 +222,7 @@ pub async fn initiate_content_challenge( pub async fn content_response( path: PathBuf, services: Vec, - url_path: &String, + url_path: &str, attestor_p_key: Jwk, ddid: &String, ) -> Result<(), TrustchainCRError> { @@ -246,7 +246,7 @@ pub async fn content_response( _ => Err(TrustchainCRError::InvalidServiceEndpoint)?, }; - let uri = format!("{}{}{}", endpoint, url_path, key_id); + let uri = format!("{}{}/{}", endpoint, url_path, key_id); // decrypt and verify payload let requester = Entity {}; diff --git a/trustchain-http/src/temp_s_key.json b/trustchain-http/src/temp_s_key.json new file mode 100644 index 00000000..79927b1a --- /dev/null +++ b/trustchain-http/src/temp_s_key.json @@ -0,0 +1,9 @@ +{ + "temp_p_key": { + "kty": "EC", + "crv": "secp256k1", + "x": "JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U", + "y": "z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg", + "d": "CfdUwQ-CcBQkWpIDPjhSJAq2SCg6hAGdcvLmCj0aA-c" + } +} \ No newline at end of file diff --git a/trustchain-http/src/test.json b/trustchain-http/src/test.json new file mode 100644 index 00000000..0aa0af02 --- /dev/null +++ b/trustchain-http/src/test.json @@ -0,0 +1,13 @@ +{ + "temp_p_key": { + "kty": "EC", + "crv": "secp256k1", + "x": "JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U", + "y": "z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg", + "d": "CfdUwQ-CcBQkWpIDPjhSJAq2SCg6hAGdcvLmCj0aA-c" + }, + "requester_details": { + "requester_org": "myTrustworthyEntity", + "operator_name": "trustworthyOperator" + } +} \ No newline at end of file diff --git a/trustchain-http/src/test_p_key.json b/trustchain-http/src/test_p_key.json new file mode 100644 index 00000000..97bb895b --- /dev/null +++ b/trustchain-http/src/test_p_key.json @@ -0,0 +1,12 @@ +{ + "temp_p_key": { + "kty": "EC", + "crv": "secp256k1", + "x": "JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U", + "y": "z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg" + }, + "requester_details": { + "requester_org": "myTrustworthyEntity", + "operator_name": "trustworthyOperator" + } +} \ No newline at end of file From 951b2f101e9a5c4561561dea4fe24928d42032c1 Mon Sep 17 00:00:00 2001 From: pwochner Date: Fri, 24 Nov 2023 13:43:34 +0000 Subject: [PATCH 42/86] Remove url_path from input arguments to CR functions to function body. --- trustchain-cli/src/bin/main.rs | 13 ++++--------- trustchain-http/src/requester.rs | 19 ++++++------------- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/trustchain-cli/src/bin/main.rs b/trustchain-cli/src/bin/main.rs index 9423b852..f54de552 100644 --- a/trustchain-cli/src/bin/main.rs +++ b/trustchain-cli/src/bin/main.rs @@ -340,7 +340,6 @@ async fn main() -> Result<(), Box> { Some(("identity", sub_matches)) => match sub_matches.subcommand() { Some(("initiate", sub_matches)) => { // resolve DID and extract endpoint - let url_path = "/did/attestor/identity/initiate"; let did = sub_matches.get_one::("did").unwrap(); let (_, doc, _) = TrustchainAPI::resolve(did, resolver).await?; let services = doc.unwrap().service; @@ -366,7 +365,6 @@ async fn main() -> Result<(), Box> { org_name.trim().to_string(), op_name.trim().to_string(), &services.unwrap(), - url_path ) .await?; println!("Result: {:?}", result); @@ -434,11 +432,10 @@ async fn main() -> Result<(), Box> { let public_keys = extract_keys(&doc); let attestor_public_key_ssi = public_keys.first().unwrap(); let public_key = ssi_to_josekit_jwk(attestor_public_key_ssi).unwrap(); - // url path and service endpoint - let url_path = "/did/attestor/identity/respond"; + // service endpoint let services = doc.service.unwrap(); println!("Path: {:?}", path); - identity_response(path, services, url_path, public_key).await?; + identity_response(path, services, public_key).await?; } _ => panic!("Unrecognised CR identity subcommand."), }, @@ -447,7 +444,6 @@ async fn main() -> Result<(), Box> { let did = sub_matches.get_one::("did").unwrap(); let ddid = sub_matches.get_one::("ddid").unwrap(); let path_to_check = sub_matches.get_one::("path").unwrap(); - let url_path = "/did/attestor/content/initiate"; // check attestation request path let trustchain_dir: String = std::env::var(TRUSTCHAIN_DATA).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; @@ -460,7 +456,7 @@ async fn main() -> Result<(), Box> { let (_, doc, _) = TrustchainAPI::resolve(did, resolver).await?; let doc = doc.unwrap(); let services = doc.service.unwrap(); - let result = initiate_content_challenge(path, ddid, &services, url_path).await?; + let result = initiate_content_challenge(path, ddid, &services).await?; println!("Result: {:?}", result); } Some(("respond", sub_matches)) => { @@ -471,7 +467,6 @@ async fn main() -> Result<(), Box> { if !path.exists() { panic!("Provided attestation request not found. Path does not exist."); } - let url_path = "/did/attestor/content/respond"; let did = sub_matches.get_one::("did").unwrap(); let ddid = sub_matches.get_one::("did").unwrap(); let (_, doc, _) = TrustchainAPI::resolve(did, resolver).await?; @@ -482,7 +477,7 @@ async fn main() -> Result<(), Box> { let attestor_public_key = ssi_to_josekit_jwk(attestor_public_key_ssi).unwrap(); // service endpoint let services = doc.service.unwrap(); - let result = content_response(path, services, url_path, attestor_public_key, ddid).await?; + let result = content_response(path, services, attestor_public_key, ddid).await?; println!("Result: {:?}", result); } _ => panic!("Unrecognised CR content subcommand."),}, diff --git a/trustchain-http/src/requester.rs b/trustchain-http/src/requester.rs index 8328523c..5a18036d 100644 --- a/trustchain-http/src/requester.rs +++ b/trustchain-http/src/requester.rs @@ -30,7 +30,6 @@ pub async fn initiate_identity_challenge( org_name: String, op_name: String, services: &Vec, - url_path: &str, ) -> Result<(), TrustchainCRError> { // generate temp key let temp_s_key_ssi = generate_key(); @@ -51,15 +50,9 @@ pub async fn initiate_identity_challenge( requester_details: Some(requester.clone()), }; - // get endpoint and uir - // TODO: this is just to make current example work - // let uri = matching_endpoint(services, "Trustchain").unwrap(); - let endpoint = &services.first().unwrap().service_endpoint; - let endpoint = match endpoint { - Some(OneOrMany::One(ServiceEndpoint::URI(uri))) => uri, - - _ => Err(TrustchainCRError::InvalidServiceEndpoint)?, - }; + // get endpoint and uri + let url_path = "/did/attestor/identity/initiate"; + let endpoint = matching_endpoint(services, "AttestationEndpoint").unwrap(); let uri = format!("{}{}", endpoint, url_path); // make POST request to endpoint @@ -95,7 +88,6 @@ pub async fn initiate_identity_challenge( pub async fn identity_response( path: PathBuf, services: Vec, - url_path: &str, attestor_p_key: Jwk, ) -> Result<(), TrustchainCRError> { // deserialise challenge struct from file @@ -133,6 +125,7 @@ pub async fn identity_response( _ => Err(TrustchainCRError::InvalidServiceEndpoint)?, }; + let url_path = "/did/attestor/identity/respond"; let uri = format!("{}{}/{}", endpoint, url_path, key_id); // POST response let client = reqwest::Client::new(); @@ -170,7 +163,6 @@ pub async fn initiate_content_challenge( path: PathBuf, ddid: &String, services: &Vec, - url_path: &str, ) -> Result<(), TrustchainCRError> { // deserialise identity_cr_initiation and get key id let identity_cr_initiation = IdentityCRInitiation::new() @@ -191,6 +183,7 @@ pub async fn initiate_content_challenge( _ => Err(TrustchainCRError::InvalidServiceEndpoint)?, }; + let url_path = "/did/attestor/content/initiate"; let uri = format!("{}{}/{}", endpoint, url_path, key_id); println!("URI: {}", uri); // make POST request to endpoint @@ -222,7 +215,6 @@ pub async fn initiate_content_challenge( pub async fn content_response( path: PathBuf, services: Vec, - url_path: &str, attestor_p_key: Jwk, ddid: &String, ) -> Result<(), TrustchainCRError> { @@ -246,6 +238,7 @@ pub async fn content_response( _ => Err(TrustchainCRError::InvalidServiceEndpoint)?, }; + let url_path = "/did/attestor/content/respond"; let uri = format!("{}{}/{}", endpoint, url_path, key_id); // decrypt and verify payload From e0974058e7d6e4da46b505bf147725fccc3e0cdf Mon Sep 17 00:00:00 2001 From: pwochner Date: Mon, 27 Nov 2023 14:09:42 +0000 Subject: [PATCH 43/86] Part 1.1 integration test for CR. --- trustchain-cli/src/bin/main.rs | 5 +- trustchain-http/Cargo.toml | 1 + trustchain-http/src/attestation_utils.rs | 1 + trustchain-http/src/requester.rs | 3 +- trustchain-http/src/server.rs | 2 +- trustchain-http/src/state.rs | 6 +- trustchain-http/tests/attestation.rs | 123 +++++++++++++++++++++++ 7 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 trustchain-http/tests/attestation.rs diff --git a/trustchain-cli/src/bin/main.rs b/trustchain-cli/src/bin/main.rs index f54de552..b77d6a17 100644 --- a/trustchain-cli/src/bin/main.rs +++ b/trustchain-cli/src/bin/main.rs @@ -154,6 +154,7 @@ fn cli() -> Command { async fn main() -> Result<(), Box> { let matches = cli().get_matches(); let endpoint = cli_config().ion_endpoint.to_address(); + let root_event_time: u32 = cli_config().root_event_time; let verifier = IONVerifier::new(get_ion_resolver(&endpoint)); let resolver = verifier.resolver(); let mut context_loader = ContextLoader::default(); @@ -339,8 +340,9 @@ async fn main() -> Result<(), Box> { Some(("cr", sub_matches)) => match sub_matches.subcommand() { Some(("identity", sub_matches)) => match sub_matches.subcommand() { Some(("initiate", sub_matches)) => { - // resolve DID and extract endpoint + // verify DID before resolving and extracting endpoint let did = sub_matches.get_one::("did").unwrap(); + let _result = verifier.verify(did, root_event_time.into()).await?; let (_, doc, _) = TrustchainAPI::resolve(did, resolver).await?; let services = doc.unwrap().service; println!("Services: {:?}", services); @@ -451,6 +453,7 @@ async fn main() -> Result<(), Box> { if !path.exists() { panic!("Provided attestation request not found. Path does not exist."); } + // resolve DID and generate challenge let (_, doc, _) = TrustchainAPI::resolve(did, resolver).await?; diff --git a/trustchain-http/Cargo.toml b/trustchain-http/Cargo.toml index c2a9a510..0cbf5584 100644 --- a/trustchain-http/Cargo.toml +++ b/trustchain-http/Cargo.toml @@ -53,3 +53,4 @@ is_empty = "*" [dev-dependencies] axum-test-helper = "0.2.0" +mockall = "0.11.4" diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index 46b48d98..a4090d32 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -723,6 +723,7 @@ pub fn matching_endpoint( ) -> Result { let mut endpoints = Vec::new(); for service in services { + println!("service id: {}", service.id); if service.id.contains(fragment) { match &service.service_endpoint { Some(OneOrMany::One(ServiceEndpoint::URI(uri))) => { diff --git a/trustchain-http/src/requester.rs b/trustchain-http/src/requester.rs index 5a18036d..2a09a668 100644 --- a/trustchain-http/src/requester.rs +++ b/trustchain-http/src/requester.rs @@ -52,8 +52,9 @@ pub async fn initiate_identity_challenge( // get endpoint and uri let url_path = "/did/attestor/identity/initiate"; - let endpoint = matching_endpoint(services, "AttestationEndpoint").unwrap(); + let endpoint = matching_endpoint(services, "TrustchainAttestation").unwrap(); let uri = format!("{}{}", endpoint, url_path); + println!("URI: {}", uri); // make POST request to endpoint let client = reqwest::Client::new(); diff --git a/trustchain-http/src/server.rs b/trustchain-http/src/server.rs index 706ac275..efa70a8d 100644 --- a/trustchain-http/src/server.rs +++ b/trustchain-http/src/server.rs @@ -97,7 +97,7 @@ impl TrustchainRouter { get(root::TrustchainRootHTTPHandler::get_block_timestamp), ) .route( - "/did/attestor/identity/initiate/", + "/did/attestor/identity/initiate", post(attestor::TrustchainAttestorHTTPHandler::post_identity_initiation), ) .route( diff --git a/trustchain-http/src/state.rs b/trustchain-http/src/state.rs index 10248254..1d4866a3 100644 --- a/trustchain-http/src/state.rs +++ b/trustchain-http/src/state.rs @@ -28,7 +28,8 @@ impl AppState { .unwrap_or_default() .as_slice(), ) - .expect("Credential cache could not be deserialized."); + // .expect("Credential cache could not be deserialized."); + .unwrap_or_default(); let root_candidates = RwLock::new(HashMap::new()); let presentation_requests: HashMap = serde_json::from_reader( std::fs::read(std::path::Path::new(&path).join("presentations/requests/cache.json")) @@ -36,7 +37,8 @@ impl AppState { .unwrap_or_default() .as_slice(), ) - .expect("Presentation cache could not be deserialized."); + // .expect("Presentation cache could not be deserialized."); + .unwrap_or_default(); Self { config, verifier, diff --git a/trustchain-http/tests/attestation.rs b/trustchain-http/tests/attestation.rs new file mode 100644 index 00000000..5d660ea2 --- /dev/null +++ b/trustchain-http/tests/attestation.rs @@ -0,0 +1,123 @@ +use axum::http::request; +use rand::rngs::mock; +use trustchain_core::verifier::Verifier; +use trustchain_core::TRUSTCHAIN_DATA; +use trustchain_http::attestation_utils::{ElementwiseSerializeDeserialize, IdentityCRInitiation}; +use trustchain_http::requester::initiate_identity_challenge; +/// Integration test for attestation challenge-response process. +use trustchain_ion::{get_ion_resolver, verifier::IONVerifier}; + +// The root event time of DID documents used in integration test below. +const ROOT_EVENT_TIME_1: u64 = 1666265405; + +use hyper::Server; +use mockall::automock; +use std::fs; +use std::sync::Once; +use std::{net::TcpListener, path::PathBuf}; +use tower::make::Shared; +use trustchain_core::utils::init; +use trustchain_http::{config::HTTPConfig, server::TrustchainRouter}; + +#[automock] +pub trait AttestationUtils { + fn attestation_request_path(&self) -> String; +} +// TODO: fix so can be used for all HTTP tests +/// Init for HTTP crate +// static INIT_HTTP: Once = Once::new(); +// lazy_static! { +// static ref HANDLE = +// } +// pub fn init_http() { +// INIT_HTTP.call_once(|| { +// init(); +// let listener = +// TcpListener::bind("127.0.0.1:8082").expect("Could not bind ephemeral socket"); +// let addr = listener.local_addr().unwrap(); +// let port = addr.port(); +// let http_config = HTTPConfig { +// port, +// server_did: Some( +// "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A".to_string(), +// ), +// root_event_time: Some(ROOT_EVENT_TIME_1), +// ..Default::default() +// }; +// // Run server +// tokio::spawn(async move { +// let server = Server::from_tcp(listener).unwrap().serve(Shared::new( +// TrustchainRouter::from(http_config).into_router(), +// )); +// server.await.expect("server error"); +// }); +// }); +// } + +#[tokio::test] +#[ignore] +async fn attestation_challenge_response() { + // init_http(); + init(); + // |------------| requester |------------| + // Use ROOT_PLUS_1 as attestor. Run server on localhost:8081. + let attestor_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; + // let attestor_did = "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"; + let resolver = get_ion_resolver("http://localhost:8081/"); + let verifier = IONVerifier::new(resolver); + let resolver = verifier.resolver(); + // Verify the attestor did to make sure we can trust the endpoint. + let result = verifier.verify(attestor_did, ROOT_EVENT_TIME_1).await; + assert!(result.is_ok()); + // Resolve did document. + let result = resolver.resolve_as_result(attestor_did).await; + assert!(result.is_ok()); + // Get services from did document. + let (_, doc, _) = result.unwrap(); + let doc = doc.unwrap(); + let services = doc.service.unwrap(); + println!("services: {:?}", services); + + // Part 1.1: Initiate attestation request (identity initiation). + let expected_org_name = String::from("My Org"); + let expected_operator_name = String::from("Some Operator"); + + let result = initiate_identity_challenge( + expected_org_name.clone(), + expected_operator_name.clone(), + &services, + ) + .await; + assert!(result.is_ok()); + + // |------------| attestor |------------| + // Part 1.2: check the serialized data matches that received in 1.1 (this step is done manually) + // by the upstream in deployment using `trustchain-cli` + let path = std::env::var(TRUSTCHAIN_DATA).unwrap(); + let attestation_requests_path = PathBuf::from(path).join("attestation_requests"); + // For the test, there should be only one attestation request (subdirectory). + let paths = fs::read_dir(attestation_requests_path).unwrap(); + let request_path: PathBuf = paths.map(|path| path.unwrap().path()).collect(); + + // TODO: Deserialized received information and check that it is correct. + let identity_initiation = IdentityCRInitiation::new() + .elementwise_deserialize(&request_path) + .unwrap() + .unwrap(); + println!("identity_initiation: {:?}", identity_initiation); + let org_name = identity_initiation + .requester_details + .clone() + .unwrap() + .requester_org; + let operator_name = identity_initiation + .requester_details + .clone() + .unwrap() + .operator_name; + // assert_eq!(expected_org_name, org_name); + // TODO: present identity challenge. + + // |------------| requester |------------| + // TODO: identity response. +} From bd4a362ad11d2d65ebabe11260199ccfac8761c9 Mon Sep 17 00:00:00 2001 From: pwochner Date: Mon, 27 Nov 2023 14:42:29 +0000 Subject: [PATCH 44/86] Started Part 1.2 of intergration test CR. --- trustchain-cli/src/bin/main.rs | 4 ++-- trustchain-http/src/attestation_utils.rs | 1 + trustchain-http/src/requester.rs | 8 +++---- trustchain-http/tests/attestation.rs | 27 +++++++++++++++++------- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/trustchain-cli/src/bin/main.rs b/trustchain-cli/src/bin/main.rs index b77d6a17..709db439 100644 --- a/trustchain-cli/src/bin/main.rs +++ b/trustchain-cli/src/bin/main.rs @@ -364,8 +364,8 @@ async fn main() -> Result<(), Box> { println!("Operator name: {}", op_name); // initiate identity challenge let result = initiate_identity_challenge( - org_name.trim().to_string(), - op_name.trim().to_string(), + org_name.trim(), + op_name.trim(), &services.unwrap(), ) .await?; diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index a4090d32..6614aba7 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -286,6 +286,7 @@ pub struct CRIdentityChallenge { pub update_p_key: Option, pub update_s_key: Option, pub identity_nonce: Option, // make own Nonce type + /// Encrypted identity challenge, signed by the attestor. pub identity_challenge_signature: Option, pub identity_response_signature: Option, } diff --git a/trustchain-http/src/requester.rs b/trustchain-http/src/requester.rs index 2a09a668..39e80e09 100644 --- a/trustchain-http/src/requester.rs +++ b/trustchain-http/src/requester.rs @@ -27,8 +27,8 @@ use crate::{ /// It prompts the user to provide the organization name and operator name, which are included in the POST request /// to the endpoint specified in the attestor's DID document. pub async fn initiate_identity_challenge( - org_name: String, - op_name: String, + org_name: &str, + op_name: &str, services: &Vec, ) -> Result<(), TrustchainCRError> { // generate temp key @@ -41,8 +41,8 @@ pub async fn initiate_identity_challenge( // make identity_cr_initiation struct let requester = RequesterDetails { - requester_org: org_name, - operator_name: op_name, + requester_org: org_name.to_owned(), + operator_name: op_name.to_owned(), }; let mut identity_cr_initiation = IdentityCRInitiation { temp_s_key: None, diff --git a/trustchain-http/tests/attestation.rs b/trustchain-http/tests/attestation.rs index 5d660ea2..0bd5ee4b 100644 --- a/trustchain-http/tests/attestation.rs +++ b/trustchain-http/tests/attestation.rs @@ -3,6 +3,7 @@ use rand::rngs::mock; use trustchain_core::verifier::Verifier; use trustchain_core::TRUSTCHAIN_DATA; use trustchain_http::attestation_utils::{ElementwiseSerializeDeserialize, IdentityCRInitiation}; +use trustchain_http::attestor::present_identity_challenge; use trustchain_http::requester::initiate_identity_challenge; /// Integration test for attestation challenge-response process. use trustchain_ion::{get_ion_resolver, verifier::IONVerifier}; @@ -57,8 +58,11 @@ pub trait AttestationUtils { #[tokio::test] #[ignore] async fn attestation_challenge_response() { + // Set-up + // init_http(); init(); + // |------------| requester |------------| // Use ROOT_PLUS_1 as attestor. Run server on localhost:8081. let attestor_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; @@ -82,12 +86,8 @@ async fn attestation_challenge_response() { let expected_org_name = String::from("My Org"); let expected_operator_name = String::from("Some Operator"); - let result = initiate_identity_challenge( - expected_org_name.clone(), - expected_operator_name.clone(), - &services, - ) - .await; + let result = + initiate_identity_challenge(&expected_org_name, &expected_operator_name, &services).await; assert!(result.is_ok()); // |------------| attestor |------------| @@ -115,9 +115,20 @@ async fn attestation_challenge_response() { .clone() .unwrap() .operator_name; - // assert_eq!(expected_org_name, org_name); - // TODO: present identity challenge. + assert_eq!(expected_org_name, org_name); + assert_eq!(expected_operator_name, operator_name); + // Present identity challenge payload. + let temp_p_key = identity_initiation.clone().temp_p_key.unwrap(); + let identity_challenge = present_identity_challenge(&attestor_did, &temp_p_key).unwrap(); + let payload = identity_challenge + .identity_challenge_signature + .as_ref() + .unwrap(); + // Check update key as expected + // identity_challenge + // Check none nonce as expected + // assert!(identity_challenge_nonce.is_some()); // |------------| requester |------------| // TODO: identity response. } From c277aaee8fe20faa2c2b0c608c51ef7aa95b7e52 Mon Sep 17 00:00:00 2001 From: pwochner Date: Mon, 27 Nov 2023 17:01:02 +0000 Subject: [PATCH 45/86] Part 1.3 of intergration test CR. Spawn server from test. --- trustchain-cli/src/bin/main.rs | 4 +- trustchain-http/src/attestation_utils.rs | 8 +- trustchain-http/src/lib.rs | 3 + trustchain-http/src/requester.rs | 29 +++----- trustchain-http/tests/attestation.rs | 95 ++++++++++++++---------- 5 files changed, 80 insertions(+), 59 deletions(-) diff --git a/trustchain-cli/src/bin/main.rs b/trustchain-cli/src/bin/main.rs index 709db439..ba58f4cd 100644 --- a/trustchain-cli/src/bin/main.rs +++ b/trustchain-cli/src/bin/main.rs @@ -437,7 +437,9 @@ async fn main() -> Result<(), Box> { // service endpoint let services = doc.service.unwrap(); println!("Path: {:?}", path); - identity_response(path, services, public_key).await?; + let identity_challenge_response = identity_response(&path, &services, public_key).await?; + // serialise struct + identity_challenge_response.elementwise_serialize(&path)?; } _ => panic!("Unrecognised CR identity subcommand."), }, diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index 6614aba7..c93dc111 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -5,10 +5,12 @@ use std::{ path::{Path, PathBuf}, }; +// use axum::response::Response; use is_empty::IsEmpty; use josekit::JoseError; use josekit::{jwk::Jwk, jwt::JwtPayload}; use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use reqwest::Response; use serde::{Deserialize, Serialize}; use serde_json::{to_string_pretty as to_json, Value}; use serde_with::skip_serializing_none; @@ -76,7 +78,7 @@ pub enum TrustchainCRError { FieldNotFound, /// Field to respond #[error("Response to challenge failed.")] - FailedToRespond, + FailedToRespond(reqwest::Response), // Failed to verify nonce #[error("Failed to verify nonce.")] FailedToVerifyNonce, @@ -719,13 +721,13 @@ fn get_status_message(current_state: &CurrentCRState) -> String { /// Returns endpoint that contains the given fragment from the given list of service endpoints. /// Throws error if no or more than one matching endpoint is found. pub fn matching_endpoint( - services: &Vec, + services: &[Service], fragment: &str, ) -> Result { let mut endpoints = Vec::new(); for service in services { println!("service id: {}", service.id); - if service.id.contains(fragment) { + if service.id.eq(fragment) { match &service.service_endpoint { Some(OneOrMany::One(ServiceEndpoint::URI(uri))) => { endpoints.push(uri.to_string()); diff --git a/trustchain-http/src/lib.rs b/trustchain-http/src/lib.rs index adc20b90..c54d9942 100644 --- a/trustchain-http/src/lib.rs +++ b/trustchain-http/src/lib.rs @@ -14,3 +14,6 @@ pub mod server; pub mod state; pub mod static_handlers; pub mod verifier; + +/// Fragment for service ID of Trustchain attestion +pub(crate) const ATTESTATION_FRAGMENT: &str = "#TrustchainAttestation"; diff --git a/trustchain-http/src/requester.rs b/trustchain-http/src/requester.rs index 39e80e09..596d5baa 100644 --- a/trustchain-http/src/requester.rs +++ b/trustchain-http/src/requester.rs @@ -19,6 +19,7 @@ use crate::{ RequesterDetails, }, attestation_utils::{Nonce, TrustchainCRError}, + ATTESTATION_FRAGMENT, }; /// Initiates the identity challenge-response process by sending a POST request to the attestor endpoint. @@ -52,7 +53,7 @@ pub async fn initiate_identity_challenge( // get endpoint and uri let url_path = "/did/attestor/identity/initiate"; - let endpoint = matching_endpoint(services, "TrustchainAttestation").unwrap(); + let endpoint = matching_endpoint(services, ATTESTATION_FRAGMENT).unwrap(); let uri = format!("{}{}", endpoint, url_path); println!("URI: {}", uri); @@ -87,14 +88,14 @@ pub async fn initiate_identity_challenge( /// It then signs the nonce with the requester's temporary secret key and encrypts it with the attestor's public key, /// before posting the response to the attestor's endpoint, using the provided url path. pub async fn identity_response( - path: PathBuf, - services: Vec, + path: &PathBuf, + services: &[Service], attestor_p_key: Jwk, -) -> Result<(), TrustchainCRError> { +) -> Result { // deserialise challenge struct from file - let result = CRIdentityChallenge::new().elementwise_deserialize(&path); + let result = CRIdentityChallenge::new().elementwise_deserialize(path); let mut identity_challenge = result.unwrap().unwrap(); - let identity_initiation = IdentityCRInitiation::new().elementwise_deserialize(&path); + let identity_initiation = IdentityCRInitiation::new().elementwise_deserialize(path); let temp_s_key = identity_initiation.unwrap().unwrap().temp_s_key.unwrap(); let temp_s_key_ssi = josekit_to_ssi_jwk(&temp_s_key).unwrap(); @@ -120,12 +121,7 @@ pub async fn identity_response( ); let key_id = temp_s_key_ssi.to_public().thumbprint().unwrap(); // get uri for POST request response - let endpoint = &services.first().unwrap().service_endpoint; - let endpoint = match endpoint { - Some(OneOrMany::One(ServiceEndpoint::URI(uri))) => uri, - - _ => Err(TrustchainCRError::InvalidServiceEndpoint)?, - }; + let endpoint = matching_endpoint(services, ATTESTATION_FRAGMENT).unwrap(); let url_path = "/did/attestor/identity/respond"; let uri = format!("{}{}/{}", endpoint, url_path, key_id); // POST response @@ -138,7 +134,7 @@ pub async fn identity_response( .map_err(|err| TrustchainCRError::Reqwest(err))?; println!("Status code: {}", result.status()); if result.status() != 200 { - return Err(TrustchainCRError::FailedToRespond); + return Err(TrustchainCRError::FailedToRespond(result)); } // extract nonce let nonce_str = decrypted_verified_payload @@ -151,9 +147,8 @@ pub async fn identity_response( identity_challenge.update_p_key = Some(attestor_p_key); identity_challenge.identity_nonce = Some(nonce); identity_challenge.identity_response_signature = Some(signed_encrypted_response); - // serialise - identity_challenge.elementwise_serialize(&path)?; - Ok(()) + + Ok(identity_challenge) } /// Initiates the content challenge-response process by sending a POST request to the attestor endpoint. @@ -305,7 +300,7 @@ pub async fn content_response( .map_err(|err| TrustchainCRError::Reqwest(err))?; if result.status() != 200 { println!("Status code: {}", result.status()); - return Err(TrustchainCRError::FailedToRespond); + return Err(TrustchainCRError::FailedToRespond(result)); } // serialise content_challenge.content_response_signature = Some(signed_encrypted_response); diff --git a/trustchain-http/tests/attestation.rs b/trustchain-http/tests/attestation.rs index 0bd5ee4b..5f753d37 100644 --- a/trustchain-http/tests/attestation.rs +++ b/trustchain-http/tests/attestation.rs @@ -2,9 +2,10 @@ use axum::http::request; use rand::rngs::mock; use trustchain_core::verifier::Verifier; use trustchain_core::TRUSTCHAIN_DATA; +use trustchain_http::attestation_encryption_utils::ssi_to_josekit_jwk; use trustchain_http::attestation_utils::{ElementwiseSerializeDeserialize, IdentityCRInitiation}; use trustchain_http::attestor::present_identity_challenge; -use trustchain_http::requester::initiate_identity_challenge; +use trustchain_http::requester::{identity_response, initiate_identity_challenge}; /// Integration test for attestation challenge-response process. use trustchain_ion::{get_ion_resolver, verifier::IONVerifier}; @@ -17,7 +18,7 @@ use std::fs; use std::sync::Once; use std::{net::TcpListener, path::PathBuf}; use tower::make::Shared; -use trustchain_core::utils::init; +use trustchain_core::utils::{extract_keys, init}; use trustchain_http::{config::HTTPConfig, server::TrustchainRouter}; #[automock] @@ -30,39 +31,34 @@ pub trait AttestationUtils { // lazy_static! { // static ref HANDLE = // } -// pub fn init_http() { -// INIT_HTTP.call_once(|| { -// init(); -// let listener = -// TcpListener::bind("127.0.0.1:8082").expect("Could not bind ephemeral socket"); -// let addr = listener.local_addr().unwrap(); -// let port = addr.port(); -// let http_config = HTTPConfig { -// port, -// server_did: Some( -// "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A".to_string(), -// ), -// root_event_time: Some(ROOT_EVENT_TIME_1), -// ..Default::default() -// }; -// // Run server -// tokio::spawn(async move { -// let server = Server::from_tcp(listener).unwrap().serve(Shared::new( -// TrustchainRouter::from(http_config).into_router(), -// )); -// server.await.expect("server error"); -// }); -// }); -// } + +async fn start_server() { + let listener = TcpListener::bind("127.0.0.1:8081").expect("Could not bind ephemeral socket"); + let addr = listener.local_addr().unwrap(); + let port = addr.port(); + let http_config = HTTPConfig { + port, + server_did: Some("did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A".to_string()), + root_event_time: Some(ROOT_EVENT_TIME_1), + ..Default::default() + }; + // Run server + tokio::spawn(async move { + let server = Server::from_tcp(listener).unwrap().serve(Shared::new( + TrustchainRouter::from(http_config).into_router(), + )); + server.await.expect("server error"); + }); +} #[tokio::test] #[ignore] async fn attestation_challenge_response() { - // Set-up + // Set-up: init test paths, get upstream info // init_http(); init(); - + start_server().await; // |------------| requester |------------| // Use ROOT_PLUS_1 as attestor. Run server on localhost:8081. let attestor_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; @@ -77,9 +73,9 @@ async fn attestation_challenge_response() { let result = resolver.resolve_as_result(attestor_did).await; assert!(result.is_ok()); // Get services from did document. - let (_, doc, _) = result.unwrap(); - let doc = doc.unwrap(); - let services = doc.service.unwrap(); + let (_, attestor_doc, _) = result.unwrap(); + let attestor_doc = attestor_doc.as_ref().unwrap(); + let services = attestor_doc.service.as_ref().unwrap(); println!("services: {:?}", services); // Part 1.1: Initiate attestation request (identity initiation). @@ -95,6 +91,7 @@ async fn attestation_challenge_response() { // by the upstream in deployment using `trustchain-cli` let path = std::env::var(TRUSTCHAIN_DATA).unwrap(); let attestation_requests_path = PathBuf::from(path).join("attestation_requests"); + // For the test, there should be only one attestation request (subdirectory). let paths = fs::read_dir(attestation_requests_path).unwrap(); let request_path: PathBuf = paths.map(|path| path.unwrap().path()).collect(); @@ -119,16 +116,38 @@ async fn attestation_challenge_response() { assert_eq!(expected_operator_name, operator_name); // Present identity challenge payload. let temp_p_key = identity_initiation.clone().temp_p_key.unwrap(); - let identity_challenge = present_identity_challenge(&attestor_did, &temp_p_key).unwrap(); - let payload = identity_challenge + let identity_challenge_attestor = + present_identity_challenge(&attestor_did, &temp_p_key).unwrap(); + let payload = identity_challenge_attestor .identity_challenge_signature .as_ref() .unwrap(); - // Check update key as expected - // identity_challenge - // Check none nonce as expected - // assert!(identity_challenge_nonce.is_some()); + // // Write payload + // std::fs::write( + // request_path.join("identity_challenge_signature.json"), + // payload, + // ) + // .unwrap(); + + // TODO: remove as only need payload + // Write payload as downstream (this step would done manually or by GUI) for use in subsequent + // response. However, as secret key for decrypting response in part 1.3 is required, serialise + // full struct instead. + identity_challenge_attestor + .elementwise_serialize(&request_path) + .unwrap(); + + // println!("result: {:?}", result); + + // Part 1.3: Downstream responds to challenge // |------------| requester |------------| - // TODO: identity response. + let public_keys = extract_keys(&attestor_doc); + let attestor_public_key_ssi = public_keys.first().unwrap(); + let attestor_public_key = ssi_to_josekit_jwk(attestor_public_key_ssi).unwrap(); + + // Check nonce component is captured with the response being Ok + let result = identity_response(&request_path, services, attestor_public_key).await; + println!("result: {:?}", result); + // Pat } From 49510f5b6a15185b5f26bc902eeb67552dc4b85b Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Tue, 28 Nov 2023 09:37:24 +0000 Subject: [PATCH 46/86] Add init_http() for test http server --- trustchain-ffi/Cargo.toml | 1 + trustchain-ffi/src/mobile.rs | 5 +++++ trustchain-http/src/lib.rs | 1 + trustchain-http/src/resolver.rs | 2 +- trustchain-http/src/state.rs | 4 ++-- trustchain-http/src/utils.rs | 28 ++++++++++++++++++++++++++++ 6 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 trustchain-http/src/utils.rs diff --git a/trustchain-ffi/Cargo.toml b/trustchain-ffi/Cargo.toml index 278ed386..a27c81bb 100644 --- a/trustchain-ffi/Cargo.toml +++ b/trustchain-ffi/Cargo.toml @@ -13,6 +13,7 @@ crate-type = ["cdylib", "staticlib"] trustchain-core = { path = "../trustchain-core" } trustchain-ion = { path = "../trustchain-ion" } trustchain-api = { path = "../trustchain-api" } +trustchain-http = { path = "../trustchain-http" } anyhow = "1.0" chrono = "0.4.26" diff --git a/trustchain-ffi/src/mobile.rs b/trustchain-ffi/src/mobile.rs index a86b178b..aae0008a 100644 --- a/trustchain-ffi/src/mobile.rs +++ b/trustchain-ffi/src/mobile.rs @@ -249,6 +249,7 @@ pub fn create_operation_mnemonic(mnemonic: String) -> Result { mod tests { use ssi::vc::CredentialOrJWT; use trustchain_core::utils::canonicalize_str; + use trustchain_http::utils::init_http; use crate::config::parse_toml; @@ -378,6 +379,7 @@ mod tests { #[test] #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] fn test_did_resolve() { + init_http(); let did = "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q".to_string(); let ffi_opts = serde_json::to_string(&parse_toml(TEST_FFI_CONFIG)).unwrap(); did_resolve(did, ffi_opts).unwrap(); @@ -386,6 +388,7 @@ mod tests { #[test] #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] fn test_did_verify() { + init_http(); let did = "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q".to_string(); let ffi_opts = serde_json::to_string(&parse_toml(TEST_FFI_CONFIG)).unwrap(); did_verify(did, ffi_opts).unwrap(); @@ -394,6 +397,7 @@ mod tests { #[test] #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] fn test_vc_verify_credential() { + init_http(); let ffi_opts = serde_json::to_string(&parse_toml(TEST_FFI_CONFIG)).unwrap(); let credential: Credential = serde_json::from_str(TEST_CREDENTIAL).unwrap(); vc_verify_credential(serde_json::to_string(&credential).unwrap(), ffi_opts).unwrap(); @@ -442,6 +446,7 @@ mod tests { #[test] #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] fn test_vp_verify_presentation() { + init_http(); let ffi_opts = serde_json::to_string(&parse_toml(TEST_FFI_CONFIG)).unwrap(); vp_verify_presentation(TEST_PRESENTATION.to_string(), ffi_opts).unwrap(); } diff --git a/trustchain-http/src/lib.rs b/trustchain-http/src/lib.rs index e22ee409..a42b0971 100644 --- a/trustchain-http/src/lib.rs +++ b/trustchain-http/src/lib.rs @@ -9,4 +9,5 @@ pub mod root; pub mod server; pub mod state; pub mod static_handlers; +pub mod utils; pub mod verifier; diff --git a/trustchain-http/src/resolver.rs b/trustchain-http/src/resolver.rs index 65d8d9dc..eac794be 100644 --- a/trustchain-http/src/resolver.rs +++ b/trustchain-http/src/resolver.rs @@ -177,7 +177,7 @@ mod tests { use trustchain_core::utils::canonicalize_str; use trustchain_ion::trustchain_resolver_light_client; const TEST_ROOT_PLUS_2_RESOLVED: &str = r##"{"@context":"https://w3id.org/did-resolution/v1","didDocument":{"@context":["https://www.w3.org/ns/did/v1",{"@base":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"}],"id":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","controller":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A","verificationMethod":[{"id":"#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","type":"JsonWebSignature2020","controller":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","publicKeyJwk":{"kty":"EC","crv":"secp256k1","x":"0nnR-pz2EZGfb7E1qfuHhnDR824HhBioxz4E-EBMnM4","y":"rWqDVJ3h16RT1N-Us7H7xRxvbC0UlMMQQgxmXOXd4bY"}},{"id":"#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA","type":"JsonWebSignature2020","controller":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","publicKeyJwk":{"kty":"OKP","crv":"RSSKey2023","x":"EyGvw3AkcUf2TZToBh6pddeaaocmvTuLCSLun_yYJpL7x0W3gVEzeKlj06J5Sej9Duk0W_yGhbOKCahOx16LszwTHVgnH9FjRk0nwOer4yKaKnjTZ2FlZsYI0OI__jhCGP9cbcOEd-1rfvUFu-ghsj6oHfSXDBm0Ekplkgs1IktoicuMsF-bD7I6tZRpP9tqFGqARUqvR2daQN-scwYUNsv5ap3XakBCDvOCBc_rPAwzapY_nuC3L6x60UGBAPtUBANdaMhAU0gxd-3JMjcSjFgwzAhw5Eorr7bIp1_od6OfBRYu3sIkij5Es6RDBLghUAx2Z3dznniJRh5Xlx_8zn4SYw_xhV1X04vY5U4O7-7veKMqKxzzoGOR7O137gSTtBk66ISXfE0k6LLsZK0Qkzi0B6YQ0Xo86d-COFNhRWQ_Lq3SCSiOaJ4lFP5_RVlHzgUXm6XY1X0jrkVPWdT42VxGjFvy_KX9f50dOkdPJTax8bGv1nEpDm-55UN8nrIzsRODaxMBooRL1y4OxyW1tpHaEdsoHvsZrLzM5g7FB2ah-62TCGkPcG3Yx84MPp50eRPIlj2omMFxMpnAZKBSRMGtk35A6xAZUI6KTYGfNI-IuWKdk0UOn6xL8W3EwMTxRgx1v7iklbgxKuCBoOeAK7FhoOVzL5YnUCHb1NUwAxDs9I5pNmrvaXsDDLKLIoz50hRAdnK92whifFoWoJOOJbQTb9sx43zmB1J7G_T28MG6UetI4dZljoNfWpXePl3vNwW979nNg7GU3N_V8ZE_slRmUv-rAw9jD0w9KXVCuZuwGIKoJ2Co8qjZxnhZUtmi3wFJin73V5BC684ebh40fnA9z-H1Kwa3ItX_mQSVYeMV-_1fydNULsdhlEnpwI5XNQ25LGqMNb4v-YRBXLSmN5CituV9rPXg5ZzQvy8VVE9qxWnicCxz2TzFrxFOOIhNTxf-YQT5Re5HJAvdy7Y9szo-i_PgskFdVm4UxMgH9ddrFUhDPNmVtVY8PoXlMzuU6gKR-1np9J6FBttHOIPu7LFFdO0Vd_Y3-Dl5mdBXFcP1Do1GN7ojcuRUB4rmB__upRAQQsqCApGurtGP1zgtMQm6ozF0gt_JpoXgvZEFK5kkm92vpedrSfDPBBn5NPIgmQgKSYfvmWRmADyr2J9bc6EjJr1-YD7QR1r2g_eGRBE1S6dexWceWTq-RktXQYOSJBnKLSkbqJniuoA70BMkjU4Jsj1EJB7oxE41RRMchA4BRlClSi31ga0T_bk31rNTLQNLGSrBrh0x2nlG8IZUZLB4fIKKweFD9pL1qhLMM-SQl3YR4-v2wxjlMXTrEDjz2xdwJsQhhzM5trtqhVdxfgBwB_ZBtU9KJqYvkB_3BhY3kYQSGDLhyCHbjyIVYl7saQGkTz_owGfj8tD3gU9oJlZHDyjf4p9AObfF4YXKjVBpPrPgwgNd-G4LAgUOn4DAVwGmGBjQaNWiLet4g4lRsLS3LkM1az1w_KyYCX_k9bptp4qLgwV6HqbLx1V5WkmubxLMpHlbV0tZFLzwThEaKpqNyz7M5qIyDvaSbTFtQ9feXhRHU7VN1MgH2AQmQzHiygXHs5qafdGSsKoMm6c_6R2-NXl3asM1TSUmD82yKonGYhSHHy60KvB4M2rVTKRENxR93u7gaYr_4cqFY9LlcqGUMzxmm6TadfSHz3rSj53C8c3Z3U9x9ftbKGOZeybdWhYbRGyES_HzmlXV5MFY5qHiE6INi_ao7Xxm8VRi5rdaHlVDWfBb8gJENbUHDDcsKQfae-4j_vXmvq4s_9L5It5kVLCT9f5NEf7jsxSP3mg9hqgwdY96ob73GsHO3HRoQARhPUt-2o7i1JzScqRH38AeDr9XnxC2Qu4LT6ffOmMKzA3qngyxKmkvyKmIl3_eEhDxpdTSf2ba6EGOD2GuzvGv2a_P9QHw52mvtEoCLNJAslzsxwxbLSnLIOkbJca1Ew26womAjSgnNwUvPCkz4lmSNTbyF63wvmNJJeD0UgkBTb2MxDw_39ukWvH0mOSJegpmENWzMhvKvxxMgB5Y1VY6Hq06V9mcg4iD0AdI-dM646yU8iLfMAAkB-EvwUUMXRE3KGU9Kx6dqhsSCrow4QDpzk0B4FCATLwawfGc1_rxQyumhF9nagl8jP1ITcLi-hlUyrOsKfSK_s3WKTw4j9iBoBWCzHrX1YC_2UTnq5XIdbY9tT4NajRzqwKLV3aYWRnqXLg_-l5k0H2GmwmRnm4ZqU-9YuAy8MQR5CM93H1gxE7oL_IWIyH_tCXrVH4hRhjd7GrWcA90s1AFpCHhBZs72ORxG_Rh8VcJpB5cTpbQfk1ESme0-UTXoSnuLPfNIQb6I6fwFkIvBx9YL7gxaVmjHMgk9BLR89iwuo3VsEsAs4ktbFfZ70l821y6q_xmOBPF-BxJzlVuHMq9hfyYVA-1ka8tBBeEy8NJ1PlYBMiVjHoKWMfqDKo0ONNv1Il_ThirUq-MM4pc0ENOqwCYkomNBFfFHdbS8L1Y5yIruufFxRbRPt6xC1TnDtq3K7JCpRjsTqv_1_u81WA4UIlW49NaruM-2lPlL6P7rWtBqG4axy6U9WYqom7aXBW0cbg31hY39xZb49G_SfSYewGr_pelurFdTag1R3ZL5VuDTggqErrppxKIBYHQP7M_reJ8fQf4JcXOmMkUOap1K7QJvvENxlQ_RQRj10d-t9spgDv5gki7uMDSA3fp4q4gf3HxZhYwPaImQ9J44zCCLUdo5dyhHsyd9neEeBniNZk5LDZRfX66ERlj49CO2dHmHLe-YQACZnMQDDug7LF0il3QHinPD-nedAAxpjfUus9Ay9vRx6nB3fHr-_9C76qx_NjCehMZHlsAOgZGU-yjdwY2uu8lvnb8dvmCbkIBYn4S_aWJ0qIOEjfWuADwWO9BXI5uzQZ0EhKuhALABMhOIi4pmnHqCE0Durvn9RaPiFz6ZKFhW2d85ZAkks_-ARI0phaKzggmB4E6k5EV3cLqkI63Oiiq21QY0VCvc0LuNoAVYzG8s4bx3udSSORrRJm2fOdURg3wtPlFq21m_7y8D09xKpHkXgEbuDJV3hWk52u0Rxv1MTY2V2_LkHIDF6my-MZLQQh0dQYnUjDfvQ3bTqj6UE4MZ07R6UZzl3Vjw53lM2x4gI17Trma17Ag6Yg6XiQA7QqgXKWy3jG6AuBLjuYRPeYo18lJm00D1D_Z_C--D6zMJKr5ohYrTi4ea_dh3CI82xBNwjeTAd95r6X0wzC3xodd7FSWJMCgt0MF6pz-MEL_jNi6sK9mIn05U4icLZLjBwl2lObaoiYxpyWEpnuMGy8J7dM1Z_aRpYt3J-Zw7i3Yf4JI2JV9u1Mo-ywQyXgRcRBhK3emrFT2fxH8SqkKwJCWn7frvbukOzSQiKD8RFuXA-SWK60mJ3erCRnka-xkGg3AiBxxeE8Prk8EGzLcB1UDRGQ_x1PXmMNtdBK65dtv1b0jGTM_uSHFndWXOrFALwi66JGyIca2WnCfQRQDR5EPyD2d2Naecbj_jMwFUsbYCxGTc76n46c1pI_QH1rxDBQ7j1Tj_rcQz6Bk7DMTNnlTFhJn2h7yVnoRPenlNCWZWZPRpr4vnvS6Ii30os5W2QaGHI_TqhhaXRFU8Z7K4PUUUVEv6u3KIZpvcuVxAbcx-ppLVkj-r2vM061Nx9aXEBFd2whV1Tw2rjf-6fm10N7U3ssLGC6sfHRpSVcsENk-ZjuYH7sY-zmN7Hf8zOYHIAZDUr1rjCgG2yCujbdOPFtPs4QKC_cFSzbpOjRmJ-urzi7duH_vH3_TBhMzM4jowgM70l1LoB9sjQ68wzlaAs74T04IroWMULoZOdaeIS54ugR79EhgqvukrIDLEoCekAY7jAs-iNW14YRPrtdul8zVUjLd4I_X3efx-IX7HvR4RUp-6lqMSN46IfvlScl0qBY_SBgCpdEw66SRo1OAIAuTy7VWX_mbvLtgZPPMkaVheFwYwBZnBLKQKyJHrNrKRQ5GdrSnJP89jdh-o6VEqG_whEec3cB1LwXipXb6v1vi-7jxU4kpU_BTMtEChb21tRhmfKGiQxHbOTRJbHVoQJ4NFlS14bTYAEuJm6yXnIW-GOVCLvlHShp5jeWc_9vvvBZnk4C7bDxY80GxadNmsKy_-AcEFN_QI9pt6lckDeTOQxgVz6Anz58RIkvJ1oPL8A5FZOl4iYuQGDAqTP6Yo-SdHbuVOuV3aM9K3L6RMgj5Z9z517O3oqsmthQdy5xtxhalD2bjV4fNsQrsXIGuNa4nAnFtfsi0uN4ahR1_YYVuQgfEQLOGSzJnw-bQ7m8tOxlDOP4MsXg6BFSBvo0LPwieTdNbZR_N4FueA59bt73HfANTd-xz6ycnZNRNO9DbxBRwXJnQogguwZQdLLLuZjqoglKwi3gmMHvCR-3QngZYQw46vAkTUuYfdG0OgaYuAAqtsEvJRaBVSud7q6pgMqM5UbG9eWv20h-bMQeBEpIuVG08HOEc9TeUzDOoE87PzBkfBqVu_s1tyItQQ-DqSvfCQBobT1pYeVsuyJSGXuaF5MXooxYfRpsAuysjWDKDNxAarmMCpioPCo5ebD0elYa6S1KV52RN15vaAZLPqNRiFkek3oy_M8C9Fi2nLzXG1Bjn_JlKzni0I3pofwFNE2ZJnoLSVpLwVLQUzzCB5GoS5P5C1DcPDxpjAr7e8pWb0QAyyIuz1EvSssczBargovo8iNxthV_MgoN4UGY3RtkDRyw2DPcFdji7AYXw_q3xlxXsWEZMfjTlkG0FfwSTHbhrL-BIXXw1u88y-w5SvjBBwk2wW0SjPVgm-qq8yonWXhnVfu4xRLMY7qNRltkzyB5pQ44rJ0iFr6tXtKus3rUTx2PbQOPNCYJynCWQnA8anAlOiTmIJV8G-MYkP3hH3g-VZSnWE8gQhbvXy9OY4YtyqX96TXRGuHNuZBDEHiPmNAvKkfgVdGE1xrxPnfZ5eN2RQWXAf5a8xgISY1bXxlt1prbFSiHTMLnikDpYNy95JBQnPEqdIYRhgzh29L_RQpIM2ItE6rPrJCl-NL0Mo3YZNdFepgL-5uOjFilpmO_EfAc06pm5sP-g6S3vOx8I9j4JrOnhygXvZx4Mr2D8-R_7s2F5QOYKCpcYmhKSqaPbdAX-q6oNQQ3fesRtmDJIVbBmioMmu5k3C8hh_L2RNAe6ItXT7XVCo-QFQ8fiUIOMWASrYHiy8qsbX4kKQJ98v070GnqCMpKVtB9522SHxJWv4h6Kpsmadh9WjAmzItl4tRV763mNcLeidWzlJFUcfZIVm9OrWbHinBUjKFnoeexpecTm2ncrzpUkMmJghWKv9hUzk6wGkQhsps-94GvQJT2ou4T5xLpeATQ3oenwez9tEwxQ07tB7FHEiIBpA4PFExNwdv8sxaEe2Zaoakh1iEjIbd4uBcEAd_E8eE3VSEPvB2_zT8nek2I9pcHEIHA52Q2_j979f-vAyJci99RN1Va8nvk3TyMz_g6OCknUZcqkhXK3lqigvhkUBl-IxjWqagdTwPfwGPtwV3JT71CZDfBWujVMLPGB_gT_dhsWlIN-sC_yiWL_thQrkgKFPqXPwQKCyz8r_iv4f8NnJIh3W6_hUURFsnu0NpVAlhi7iOU-B0cqk1NHN9BgNbT_zU2aVBEFBrlQetG5pyxxgyDSvrz-igEzZ9oqa7-EIgNv8P-0T0IUrlCIQSfPsiAUsbExwg5JwdgdQ_gD9HUt4U2Npk03XtaAySY1IXJCXeJLp0OIcc8hFeaiPMMv7Caif9RsIxjwnikwLFGtpNy70Ed6CkTMtxBR4uShDzbSz7Hk90gu5-jV5WGysOA9AbW24iqgfgCKjrjgfrod_MNG939PdD9KOV0x3MqbZJmBLB7jKCINC2ilgH3Ez4crHFZJEkuJ_Qq-KDXW7l7hjHUG_debtAu6qI1edYP09UkgmQtnZgLcGAWUhDxWhdf4XYOHfqXxfhiVu8tF-ly7iqWkmRCqhRGV5NmzUWuwvQ8-Jlh4kRa7nhpwb7ivyXiDubq85_tKuha0qKFzzz8gFuiefICHX_Uy3xM8m6Gy3KfYirumMAkuB5-IY7Dgr6IZK8YXGLZb3QEXmOjuwp8Rmm-bMnCXehgCJZplNtcWi7eQxsP4y0IoEUsmmC5Y1as1sAs8-R9XlxBfP3hdGWbOupZfS6FmMRiGD9HoWesUSVtRs_tgOUPPVav2HRIK2CLYBRwgI1NaeRcpnO8cOye4UgRm_UF36pi3hJPfIdCnhxGeOH5J0r9zYEnTDs18YsIQedQOJ9jvGBLvDi8dJ3NRzof0hk9riVtSPV7H2EKhkEL67E5pccehsmZnha0ewYbZdgEstjzjwQ6qkZRmFLOBdP11yCDzgs3eDmnk0Ztewl22-WhhpumCfNgux5OEtcSu6hcC_gtsXQgTm4QV09fFZJAH8tyfFildcaycx0w6zG_tT47jBYIwVyEI-Mvv08qYw3ZN6558VgacYehFWake3ahdjDxZ8bO_tBtLMrFXmjRpibEIYbWZW2OPgBv-4-Z_EPXtLrDpJxYjD8bUxNgxwyqxAlyqZe0FUQVo1RTWV9hzvj4GcOG7wC-_t9aEEv5h9hg3sQXBxwKwIulPSsJlAeW3dygypohfIMKiUdjDERwhgvPsvB_vsJIaVpN3SJVfNWvMEFAIRxl0o0b4upYbISICcxav7YjxARlPcV_nqG6Lnj9-6MtHOzvmwMWpcM0Y_FFro9TqKAj8TkAiGaEMYyJ8Z5EMAsGd32HwMhmdeJbA9TxNpC8CIpeNlU0H9JeSDR3bl76oGAPDIc7bDmfKjcCL_8rZamAaZucmCI4Fkkjaqyl_k0TOHrxrc8EcYzbICfu2Xp9j5Bl_w7GErvNIbMsbJejezsJxt6CR71oex_OaL_DyxGJE6bOaWZFwF3WqhVWMoMEuRwy4Z11DIsqZ2pbxyArURVFG3mIHnBJ7ffjxYbofuuuw9Ce3S0W9AwEvXRlquPr3-wLesE-Y09JL2x63dPrsfx88itwaKSyGuJyvqpTu8NwpAR8d0bU6nXG38O2ysH6-xwvDGoeApjhGaTD71tv5hYcJj1X2M-GeWFi74NjG-PYBkamWVPk8v2uimVuB402YMgUAe5RtZcKVUfHczIcj7IWreTJr8JCLl4N_X48ji2KDuBuuaBRBUYdjkl8ltWE-AQzatqUi3DF2ZDEjEarQrk8K6QDaHNbMAEQwqxIcKVB7rX6pwR4EA2xN2VYmCskYAReAbKYyzbFKgx-_kbylwjO1CMcDTdhKYHnfEznxeaxzjwopfWQR5JQ_y_4OExcY6gh_FHXXyMOQdyzdcNMPFOZDvKAf4PiXg6BV6VVbvlssgImhEbhyfKlwhmbHkrD90BVSZOfwp0m_zd_xOfwSYckSwo8ef1K6DILkCmiUSc9wiCBBGHF8ex_0u3nepPICWg30NqJPii7moRYlXNi2hKgTB2Cy1njuP9pNFSD-8cOxrrAoAz6SaxdS4QqxjykSaRko3FibccYcSE_fkx7_WWBSW_1GOKTqQltkzHWMqTbu3wEjBAbnQjYGEWn8aTNzsAh1pezmZurCOdi9uL-cjIVavKPn23HhHGfS88f3pRdohcdlszyc74acnD6VgT0VnArfeYPNBWcliVDnCE3qYSvter4l5Fe4rH1qDISEq2ni1-uxNRJx6Ck3-5bWSZxHAgvc_2gC2O5qc9TU-akXvNSqLmNtKmO2FGFtBltwgyLc8bVWAJrNxuWQVCUxXlfSkxaGXtN18lGJX-SvmRn5IsqfhUitHzJjEASiI_YOVY9OoGEkK1a532FFGdO00mS07BQCPV0w_gldLncCOgt8VPaB5d5SjOF0_whIcVAIY95y5MrZEJWcbES4zg_jdGb5SRLlr9PENPbne9VYK4_ju-MCFNo0uWibQJzJcpaKU2rZ9sAsT2goR_lu-aLGCdeimhRmual5ISX_tyMRikPCDidsweqUeRzPcriSIRDKLcQfzA3P9Lt_Mo0ql-l1EX7TcwLgCsISBJ39jyhHyPvNPbBAFAlrlF9uRhz_ATonpUwgZrQHSlpsy6Mzh-O8f57HKQTRT0VigvfIeC3J1TR4EzLkHUdC7QF4JNlprKFQl-HUh9VIOpwXfQ7VwhbxUw-MThAn8fnFAKqd8S-4S76Yn4Ns3B0FA0wlDWp9AvfCSlm50bQHUgj8FEtwz8279OoIhBEIMnA_rHNwA1gPMSAl8aU4RO4L9wTbhwVEs32i77O1pQS93ZeNwOwXXoquAAVFZwusOXz2C3jxzKzB6IdrA9LE7-ALHDvmxB-y9KUe-RgCfFgjh9EE7rdwftpCOMj30we1IOtQ1XyFSwpbIK-y6e6itkyx73nB8UicYQEQHDnl2UPtxm3TLUe5bx_E0sisng5ZV2ISypN4_CiyoAbUPCapdHnGLh5VJtaPPq0NGIVA88MkPxnJC_dTfsZKzNVDywA36U6dGzcSH16QoTfJ-ZcUJhHAKJHizKtLpdxpNKlSugnNW0P0XwgrRYAehBBqJAWrmDc2vll-f5KYy6AFEWfIub9SODwuu3j3yfdoVAjpi6Tvm_e_w18ZBYKjtRrAAg38eTrwQwdDDovzBO6t7xmJkqOxsCFl0tz0WB7YxhVMfhC6qv0ojnXM4XrhX482Ew0yMUB9Ql2_2d7u9-aM7VztBqRf9dtPj0Fc1WdfiMD1d72U2D5NukpfdO0k74QL4xFcEWgq0qAPT1Xd35HaQhe9KfUYx0d7KtbBb1BrpQ3zZWS_ThLtfTHOvGZRQH9bQQyFkx7r9Lnal_GmnKw_w-Y5ecOTXwxvtB_XQNOo2i02MTPLpYHXMCWCFB6kHee4fhJVL4yQnaac8WOYkNDZeHf7y15M6Ezs0ieyusNjY-nfeAuXS1kJ_lf-qI-1xCpx4wmOy-W4Y4Xbr5YWS8Pe17115uh3ZGN9n88HuWj_fzZ0BcrgsT4p5LvSm9lntyD3oQ8pX17phhk3xqItrnJYAq8MfnLgifMDl6XucGJj1rhsvVGfr_ccjSHxohBb0HWL6g16xEvKsXnQe-PHn8Djtpc9doxqWWC1QeFnjIFJ38TnZd2v6S9irKu2D-YTw_9TvgRZTHMLgHH7pdFo2P_-mrKP74-OvYkn0O4aUVAZ6-bCXKIZ4ZzFgt-aO6l6vyUUfhcVrQKcnRdrZ4_GYfiRdxlBL1rvcZAkVpH-iitAdQ4N0xFHFL3MO3MH_EepQXLXSgciWBbbc9lzJnd4GkCRT-uH1SKKtquXZIO28ERVLB5yD9xkl6-ch9qTYNnNcBDNSAJQeFBwCHB5xZoyuYfN9p5v40vfSDAoJU9A_3_kaYMyUBVaxQWnKjZrrA5hWy2fjRUnVpeX7PDyAyb6eZDt7dKlkWGQxvhDXRFeN9yjohquhDj9OSS0JlHsPLobIYEPThAwpAYAEH9aspydpQDzH5LdB8aSUzTmFvdt87KW_OjCX2bAvPUj7a8bhfrITHuCUwOl_hNSIaxUX9EuHEifvRKi_KnQRZvkTyN6Ji93jcr1wYk2FOjZEVdUfC_lI-xzuQDSVWUUl6URvL2tfzx5FxqScbNiq3xnIqLrNONk-p4hi1QvPbgiYvXevv6-KgoCOBN5b7E0KUoVcBh8GBPzCeP2EZwA6C9k8u55Ul0Y6dohgm5HS8NQfXCSTt7QQgchGBOyOP96JR_uRbyLPJ18KaFr9QTxkQrxpuks_tWBdd9QD7GN2MU26S9veV2mrWHNXBiKY7NNZjYSkfNyzvjsg3VCwvxU9kzvkozJ_hQnkOnEmlI8bu34cFvYy1Ms4X5fLwaFLMmG3SnAIwBsCz3HxzKU05NBHikuB3B79BGskfQK_Fe-rkahNqJgG2ya6xgeIBivC2iuCuVjM1xcVN3jM0VuwQOCIVwjPpyDgWwjm5rpjX7LfEzwjyXynX5OR8PVugx7bAFwv0UNcbkBNLadJmL5hZfeXHzgPM5u8M1_PEpwxRddCDLbmbY-Y1naQwfaKRQp_c6KwJtT3IzkOJlaYsUlEeoLQKfQI-OFr7Jy6N9-tP3x_0OpecilN6J7UQLOTQEIeygISrIiIkSQgL8m7YCl7cRejrq3kF9UutkU2OIJFseVIFtIKZL92vc3WSxj6A8NkX-yqQ9LCFljVw_acJ9tUT7tNyOF7mFKBQJPa92WpaOGgzq4OCV2nJs4GFYjXgw7uE2NjQ2i9_auhXryGm3uD3G29NjUQ6Lkingi5trDZLCzoFKtQ_-2tWnf6sC4HBlShllmYDfCCorSX3Qc9WvEwxLbRvNX0CgPCEoxIKHAE9UzN9sfWZLD6BCXAtERDgNqc458B3xIrpXpk-hmIe-Res9HtuS43LqebcFiHjjKKiBuUEBCSxSEYQPYdEII9QMsBsp9IoCOKL7y6m5EgCfQzA7hiWLlE_Xrppv625MGLzebKWzu8CP1mOPWTp4FYwaXl6sm0rgbAoR5XtNLcBazT83ji0Qhc39dVR0nFyvdSe9L-EFw6dbYUPPbQDh0hQVzwnXZYFi4wgX8iFfyvfj1cAGrQNfx2yekQfLm-vhGK_sIlCRVZf2bjS6rwAbVIhhPFuTsQ5EaYCc3QbvJg-slvxMGfr3gpUkMV24EE0dCemwKRyRyf9zH-oswETPMyAFTQmlx715Ao-RESnFuc1Ebl13oTofrWpye9ZaqqsGko3Cimdifa716i5Gkq2FJNQRRRrp979uFgzdwm2AL3Wa_5I1t4aHY0hFNXzKU5u7gNmtiTDyLSOIWLGfd44msxBYFSE9YqSdU-7KpEtOLQRppx3FR1TQooT35XW13oPp37k91Uv2j8wLJPAid7msh1AUWmpGiq9vhair7EUlZhnjNIEvhlTr6sIwFzsJPRl9Dy838w_UqVXhKcA2wJpTCjgRWXL8R8b6L7Qs2v0H554fmrK3qcTm1BgmPf6d0aeO9wsgj_cSO2gI6HgI4zL6PUQTsMTzhIY8pN8MW1jPWVa89yWjGjaanxKT6WyzdkCGj6NcG3Yh5UoKGeehwa_5FQwggBfzXYMIAK3swXYvK1bVz_68c3eLtW96nYc1mnOw0QmcuQ7ajBPpwPVqQwH1iLRS3nEWbxznVbgvcdHS1Sv8LcVU8htWp9JheVP2OCiGQPFFScImnsLDC5WZxJNohrxFO6HHJ_6T3py6zz491E_zWqb0B89YapQO7LKc_D3pU7_3-ug2A-BmtjReN5-I0QAaNX86gN5o-LNW8yl7DmVU8rDBHQBV7vZ4uijVQhDvpifKk5mqhztr7B82gamJD6gUucjs6nA9V8i9496A3dTMHdtEjeEIE5zkvtbLe44WyaDxa5KiwZikk137DL-hp9w5b2-ZjwrGqcNJrYwpTQAjHigL12EWMHKEnPEsSXqmYujeWGfB2M9_VDmSgf3J-XAZroxarSzyVuead1XNLHtLqQgT0Prh-PS1lDJ8jH5y4_JzNS6lN78BaEi-rBl-hyhXqi7ZEzGEyZVB-H9rkmCE1jnuQsHj_iWUkZFeE5wJRemTSNTxF_GqZrFTkTD68qxdtMg7nWns8pXHaqDxpWAFaONRj8JdfPCeJhQ3W9qIdugEHXFlYYtZLEuXAlBGkHQQlnL2XeZ5aYE7xDC2JYQRJBj8c5fYfusrnqBgsz4EIO5ewfwmX-OAJg2d9Pm0UVxGrXtTW1H277sVslv-2FcU32cZwwls4YthQ6fyoIVLzJTyMOYJUrpFW32r5tG425wn_Q8ezmTs90EKuVrvVo8w92JL6MDKA-orDvhvQ3beb9l7Sgc5yy9cb90rjD-lyQBgcDfJ0xHFnhjnz4S8t0yga42xeRI3r_mXd0NvRzTUHkedNMtRAdU-W382jaFGRBxXL_4YziKyewh_nGh6BlW9EQ83Qf0oSwb43IN4k6GmK6KKvwr_KiERaBougue7YpwtYyqCrEoMiEEMn-Sog4CeLzg6IuYx4awivB7VYGGGwU6Bwc2IkZkKUFxVhJK63cAwQX5Gcve_j_-WcRRGlUhI9W4RvFhQFpl0YfC3cLUzRQZfV_fWH2MIwrJm6y4VCHhnvx8O87qetR0kM7el6lY4Nrk5bNtCdBeoyy_C1sz--DjsmM-z9i9IR8PqMCZcX3gBry0Sn_js4Ka0cXPsKpM-GpR6L0CLxge1FdKNDSFUOacsiEzh3-LTu-rUUYglWzQShuc8_dtZrIEvVocirTKZ3gaImQ1M1EylwXITBxzCUW19Io1X1mxKiFpXKHtzK7AvEs0kdicMBNl1HsKSn8OH3jxwLSHI4DwFIGYBxCQ0vvG3NN5ZZ_c4OnSfQ-nojlgmeCjMGykcA9E__NgeddsOdWxnG3fVQFIiMzoJ1AtYnxHoPRbtVZdyWB3dX1L9AKxlFep77w6KS48z70KzKseRnKLa6OCPZwfXgP5kEKA7FcKwpwIaMPNxCOedtULYeDhclbLeDtjK8LA2q7a8elVyK6YRvseXaZ4-nnd7iLYLZNOv807ZLaYGm51X7aFt0YRTimfsQIGztdkY9aakmyH_XQkqPmlNa75aE4xf8FqLjwa3AZ9PcIS8EpwX_Vw_pFA0NJcvJxCBgY4Iz98FxssnBRC9dJ1aAn4Kd8lgWvHIXS974MFCCGhfI8RRVDl4S0QO7W6vrGTIZB1ngY6VHZQ1JG9NJOGtomR_8RNH98FwcPzVNUzy9AhGeKBS3WECJCxk_gKjcGB-rBogS4EU0BVCfxzCoTMJF51ufpG1k4eWlEiEpOqUYgUWAN_3XYWNhphToFLg-h1xmQWWUBiVS6tV-XVvEOgKCKp_b8dMJ_99civ11moW0s3XQpzbxo02gCBR9LQYl2OPBcoRr1bVQfmS3sljBMCgtj5NodsMpz-rIZtgbzdchFe-RE6QK4qaMwAUY0oldGd7nIW9V1C3hnGg0kekWG3JKlxMhIB3IbDAVQ4jRJ90_JbLVaj8v0cNmhAwT0QwIwuTJJYFDGM1fYrocL0UKFsHEdPGZQFnfGAeFoMQwUt3I6zpmXbIqWA0VpRYwiUwTTRNTSsH1_eX-LWUnbXBsOmr6X38Sf9SQD2giVwmji2KBw4GSfRjUsbae5gpgZZbTcXH2ZF4FK79B7kM3RW1yKHcMrT3jXyZKjfEee008n6CJraHTc2sBDtV85wr-TQgic1VgACOfee02nwbPgPGhlUsN1e1cBwTGCJiIthec58AQtsEGIsqpTwh0axbKUmUaOj7zuUjDTg0imRCdYb_iMh8ya-YUncdYTabPkBJYlnbHzCB7aXmq42akqBQTTTgVgUsrRy22Q9gn7CkGltOZRbiPZ4Oa6Uzu-CYOsK-0JcD1xUgtTd9icWNNbAg5DCHh8FhryzVmRa5VUkC81OQryM3CgKdyzyw4xSH3qw2HcCMu7VHbHYhvVEXOQQtSaedW6w1shQMbPRKt0Bf_n3DTiyvSsfAgZmA3lrhQhRzd710dzxxljzkbfYEl3Q3SKg2CNM4Pu8SzAcJj9M4WubFMqDirRgVIMgL4xthq9u4qvIGxTERgAu1h7xhUcA9f0IvKiPzBkfExW_QIYR8c9kewkGILCplgqOHbvNBtqK5uXJrnscBUm-Su8yfc3gTiWWlsb1KBm2qwj6uXOBWQ-u4xyatyltsx8AJlshq-YB-K5oJuvlwCXkeXkU3hqRM4SRwLng3VyhdL0Jr5HUv_M1ENVemAJCR1W_6IXWxbChAYiRUFVnGQMCf2Jx46eQo1sNMaO-1r1LdtVSJo4ZELftKu2X0BMQC-l9iQ5EfDT2VEPZvl5JszWbqWIlkr_RY4jwbY_OeQCkPaMxE0eywBeG5zjdTYzmPLm0YjmK5J-_7tjM_678RIQ8qyuFPuNRGFUClznKIZ-T7SYMtFie6XAQ6j3q12Mh4-zEomU1jIOcy2EzZzTVgrpmqVtZUB9wzPIsNtq27VtLz231dh2i2fAfAZHdvIy_7XQsY7-JWltkQ-fY41Dw9QOIhDb_KJHhFNH2xa3g3NGh1WxZIiJNfPXXH2pMA0xU_FnJF0uPEr2u0rEcTWqTsDgHk4krHglASUYsJYneG_YgBCHWWrGXWzbQNGYsZryPJeXNcY3hw0wO49CxV7gb56BbUNBvNIfgS6SogajoeoPTkPQAICjtAVhnrgXyIFnQ38zu9Cwjwqxy10jt04Gwm1Q6xAh_CNQwcLgtJ7elaM7zi9uEGFskPfZHF35EOhpMwR6wBoPSv0ESs8PX1_WKhYSakFyW7SewR86-W3aCDR6xznTr57lJB7BnDb9_fF6rjfysDLSjofLGwjD8qC43OlMNZB9m868hgZoCUKvSnTpVW0B2NcAoM8lgXDox6cxZPtDsW65C2fMFUmt8yqLg9MOB9QRvr8jQVvgQ75GPADaHTVbcDukGOlpWsE8qHc0y8sbWnBRwGu4lUVpyOe3R-q2Y9DVCPonQoeUt3r6EfyIPeid7GaY1S-jCTuj5GlZA4Ridz6yYYZmGXzju_OqZL9TpH14-DvywWaBu8ZUqvz9kVamnK9P_M-jTDn6iz2zy37xyEGtzWT5Mv82avznCG1l0kSoG7HPg2kdA2ngIutv3-sn-D4_H3_Wzni52iLO-5CdMjEHyo8IRF2gsHDwR0mkF5uGdXv8RD_b5KZtgMy91QfiU-h1B1OTDWxxhfSPDO00EtPBW3UPQhkMJY2_MdHzKiG6i28PRjUTIYDcQjc1RrUZFuBmD6S679gKEzKw25fKmSbk6MBIhBfV1Q0h9uX9RauUq8yFRB7mV2EQgMRzrSZd0LVqNtBcOCU7TdrpzJzk0pZkfmjIVGOAJ37T234ICX4_M28IgaNiluXWNYvW8j7k_nTy6-8uRVw30AJnkQRswmxllkn8sE8pfxq2ACMG6LhiwkUeRJU7QYz8GMhtn1HcppGw27GGLZDbd1fHQ-X8EyC_pEx6wcSKdLWOZJ-TOqBWCDHZAJJ44G9MQ_eYCZKj78LA5pooQ1OQJeno7YefrhaY7gsJEY9LqHaDBBrDYPefTlMYgHPkHKxgkT6QtpbAHN81lB5uiiN-o2HPIgI45ODYY8pmvk7SY5BVsu-lJ0K3KZJOhOsfQsoK9CWB37yZj73eFNgWO9Wd5qmmiRVbUyBrjWSXc_dLnbEAKxB08xoITcG4hDIO1TSbTIF1QsBKXbyH11lwKM9Gr3bGckU_ni5H49T8MeAx2Cce-oeZ26dj5jDGQwwwgRbDf_9eKjzVzH0MtA32QPr-ZDqwIPJlpSAIswVKI7W6-TVHeKdYjBufEUoVhjsJ2kZLNnwsgUPySarkA7PjTLxcS7L5eXTIzBWpcSqQfY6eII492F_RPgaAzRnqRW7FA0lvNcCblQJoRK80DLGM_oZajzqytR-ZgfJvWQXY5UAcW0ywx1hVklrP5H9hxJBM6LujBC-bfK2gatWTUNoo7ciIWk8WPKZf9jCnGd2s9YQhwqJfIoYWLYZj2obHw-WfedxSpLOl72ucoXM_UvtvSjnnX18plcNrQ5lkO4f23N0gh_oZhdwYeyeb1N-KADIKIdY3_6tj1AFOqN_vXTuFtEAilg5YpHC5akZeMvfOGunAVza3qucicsRDEYutxcXggArT_nUZa_j9X5lp9EItKRVyGjBvRa8VKDwoHe0Qq9JYaDk2zA0Gqz2BsXKjxS5eArOJ4t-el3UdlFrsrGz0IIM53LsVDnYFGo7G8sQWzxQHD3LqVKhumuL4q0I6gBmOZBhAzzAb-j3dE8MFDXLKOzpMXj4yY_f1BqaSVhA2LxC9FXh8xlYclwHgweVkA98obGvKfW4iMNKJza4tQ5A1QDFPDwcsF1biEPK0svQmSnHNvjhOBM_hRoZK1YD_RXmIYPWzJnULt_2Nq4Fus7QlP0m4I7qSxDSUe3Ly_RtLefBaV3G7dUa62RQJfXVKgbGQTy_64COJ89TVWD5LIEPW_LRrYvSjVlsMD7LPexlQnh6J4g3zq0uRHxcWa1bDQDUQYrQp4Ud_6qc7d7FoQqYbQgib1M_MIbRyJezKZJFNXN8aZWzAkSjR6Luk43uWgogzv_PLON19AnvbC-eLg3fE4aUvJAueCiTQGGFkBb1O2IW1kc4i8wN_II3s1TkjQ6KSvre1kN4YMOTk73lEcC6L3NcgOd-o0tPDO2O9E6I8FG4yCWmnFPjPO1FFmEnjAUSgwhEs4KdKbQwRphNPnZQ6dWsjKPVM5AfmEiLx8drX7C2NFidylmW1dpC6T9L7Qcvd2YbocFGnNv3j4ztPjt-9Z2Y4fZq-02HVNkkuOO5AB4TdPTftjgiGipnbMaBmgBNMwbxkzHuWZ-avaQfSifAvfuePdugEVjmjhcS0NQuh0_hZ-K8m0-41A-EqQ6kzgfYTwKuQ8JdIWawuYoM1Q0G1bJGpwQxG9DPDB8c6y-WupSOZ8c5l2pWsRVw7UJ47hHhFIsoDHFHVDBT9N85Y2SIRbttX2pcnKj3nw7aj6ZcTRwpNPN-Qvu8YMMjMUVV0QoIn1CEyhim0x7jqidBvcSHLamlTSqYvzDfI4l9fSA8m4Yar_VZSMYMxls278D2sxVIEjXt-fqUbXc397qGzvNniARzqZcqrataPpzQoOM-bNj5LEJJdYPqSsHioJGOkhFzWXu49UuMFYUvyNxOhrbUy8h1N6GKiGDMSwe9k9wN-5WhvfEf3wPAztWl5R4PFRf306CPhL-FW83zhBr4c1UxU56taoVNnJtsblxuTTDJr8HgIiS0bqCLpL1s-ZYOgARzAgymuZCRdaxTmK4fdFhlTs6coahCbrSXO9Iehq58t6uw55hGhAqMjVvaRn2TpgwtHS2jvGMCsLFBYnkVXeeCDwA8uIEvujo_WcIUiT7STSP1IHMyllhlhU9tb0sD8wadR8caAgHBe2CuuE6YeO4qet9JIzOLTd3kJRE9Ev7aChlmuuAElJ0o-ktfVIvUbwVAwiWV3X6AcMlmVR_6HzhwZvc64Phapf84hPMYXvnIxBSI5UbvA0X5nHU2lnqPeRlhQI0mKXvLk4Z60WTgGrJoz6mjUQNep_zG1WTSkLwk4zlLwupc492MMc-M3x-vYQBmA0J2OfXEZjnuqAQ6az1hF9SaaF87c_W-Dkd5wgzUEkoUA2kjAfLtSItyltjCzxTnH5gGs7KaeoN_9V3bj_EAquWTrF9Vdr0DyN3fVdwrjU7oZhp_CVfondyy_VQO2wtxzBICKDcgraDmcBS1Pw_VPEIXvNm0ia52zwDDo6h53kRiKECACeOLLwif-WO5IBh4DZ_DFsiuaX1dJyUUO_7vk56KjmN0QEHxaNwpvKMuPtRGOMWkRAwIKezgkGJ-GRLXbeAA_1qqT0hLDsqJUal65fXdZ_J-qEnJH9xThlPem3WrWpAYKXeVOLOCxuA-7wxyxO2DxHqJdxsvzd16aErXTcIq7OgGXL14QQXLcpQIKermnxygZf06I83xy3pkfwEY07BVX6MnouU0ybMlqeFQgsWFnP_yjPuYGA0RQGOqsL_Cz_aq94VrHtzL1M8NTQt3Jhpr_L908QQMXN7kK6CKJnDkh9Rzykak8Lig_xmz8E42bPY-RWpAgAvpju1nggo6H4oH41IfQYW2gVzTviJq9EC1rP3FtJouq9gmSH5xDo5IW09XFskxJatkvOUIjgtZhCNG_VxtML1VdSDLZSrYjMT46SO8JjWJcn__4tR6gEmTrzRE2OSjbLuZpOksXgFrOgRDsZuPSeBAE8VKVpLtHvRQKWimJumFONfHJ7JxCOaUSBzpvk88Wg9em4x7YAd_SAChQoT7XRtjlwkRszQ-TwYfGsyOOGiTyG9dzCGGy_fsTugpowfedGCGBHJpuApn7cf5NNyLsafquuDtEyUly0NDpCwF2i4Dhma5jQsDEbKOlHnq8uzAkJXRe96IQBj0FWieRJyLU-pNsgXz2PqRxNXs__iId_f1X7avOZHN7FyBa-vE-u8RuYGXuLsUtQnnA0eYesQ0hCvGHa71I5E3-w1DCu9dLeY725SC1yVZ_vJ2WJmwEPXJIXKhVgTfvw8GIEml1VGxRFvb5kMQtGbXChL1tz7Y35ux-SRoX4A23pTZVEVquaXb2QjNFOprmA0tuFeYlsUdqD82ls4R1WzgzLVRRF4Z1Jh9AFgfYHqV-7UHwJAY0OpYK9iu6PPknBPAxWsxnLxyIxQ_rRnrbD-AyW-uFhBZ5d38zkvKw68Fr24Czq84U_OlBAvHtTWSzQa_6pc6tu5KT43QDCeWwiyWt1gdahuyoqGpJNgqyD6gh5xjSr1U-ahTJpXgVjnbNBkfOWecj9GK6CMLgvcI21qVrX2IHwG9kMyQgNmu--z0VHXt0WUtEuUcHMM4PzFM5AOZ_oxSVtIbvoYGDXjUgEI-xM7BOr4e1B4n8X0aoorefQhCLe1-Lv2pKRSeUlX60RlVuRN9GkoD_UoFqz59zJwL3h2uakwjt7iehx7DeI2pHUthZL03BqsYtJth9Emw5gsDKfBIR9BAjIzbSFRnnC_pthG2E1WMRMeeKThVkL_JYkmFj4Cr1xjqXXCTAI9QFwcTqRI4ZkRgem_jqVB7H9-BzVDrqgbQoxuWhNRn3_w-xfyzv_JtRcP150_7bEN2-gbBJCexcaF-0PbkopUuQqUjE3-WYKc9X9vLWcdkEehB0F7eqzdIWqRPTsnEat4SQhSvbaOp7EgY6Ypkvjkheer3fkPelAHN86SGviWWtaxDTWMBwHQjM866tuDKWOEnLQhMb_IjQDFKHrUKUnz42saPlPWfvbas8_Ymk7bX-E263Wzb5_MWXqPHMt6UTMSOtw86MTE46YEW9Ww-WW10cmatGb4jfoQHXa_JxCRry14AjwF7CmmQLP6dnm8r4_jm8AylHV8iKCG6r6csAhY1jQ3I-24iLu01EDB6H-_bIX3uiZDXpf4T1aGBJh7I7INB-Ad7d_IV7At-qaorPyE1xvTWeFVQLymsE87ZHY0J157ggITtT95e_Q8_SEiFYg0vxg89qBpuXygL2M_Pbrb5eYTCA6K6N86CxlOvFAb2AJnhAmxe8c_KHIsFZPL6lReDGQmMPBuvdCjjLPV7seEZX30ZMTuHYXNuD7IytEJ7X1o0_04eCmcqbivHBCoQGOzDhQ86DSoX2Omx-hmQl3hI2KgKnGcnfym2Ukd-3CmHAyCDAv2kDHm38H-JdcsO2DNk9QsYtAln6XRVl5kFDnWEhm9bRh-fg9Lmt_mNkwHSwZ0YrdYhAOCMkNlukUp0EYKKhBSY8lsY7a_TPbt8vkTMSCmi2sPr7NnuyaxMvw6Jblb9OD885lSOUp3oPpoH8QPkkhYUJ4-HVmmMGD8orSe0L3k7lLbyHzz5l1EmMahHWCCbnoMGGfO2QnxV4v9YcsMmIA_NX_1CjMUh_LYKrVWE2tfmhj7Zdprbop3nTylHV6YNet5h2MVUtpfj3CFTz-7V0AxKhqmTkSE9fMv5_XY9-QxFKf9B785SPTdj1xBiOsQ0uz3TJ2CPFHOtikiqYkNu9w2cUgYejqlM0crBDpQCuFmFJCFNKrfMa7eue_4H3RSh8Yu9Yw1LXbkAuGoFMGYhegcBEvcxcDSHfZ9f1HFT7IgimpuFuoGHwaNhPnlNc1uI1ILsFeRrrXide0q3L78aMAdu7eFfSSXHm-RcZypE9LHU8caoGqd0cr8hMAFvmAacrXiUE6RtzQUZjswSOziVVwlqyszgPXIuDsA4m0AcaLyEYQ8fEsRZAg7RyRbTgMGrlo-_L1Me2JMPPbiuNi2EtBXz_85Ylbaz45KQ45mdka24ouxzs3YK5aPi-Bv-fYL7FhoIWM6AiJH5ETjucj9KrhL5u-mnEi7sYh6ttj6I-MtSpCzOLrIB5HZ-tJktRhN78f2m8h6N4FBL9ooQXR4Y-QC1MG4eRlAiugn97K-r3MDGQZR5fVwC8SPW4Pt6UDvfaxXZek0HmjYPEk63MIxeMBOLaipBGR2ziR6YsoTUZ3NOopXjZr-UsGukdLw0OIJsxA-nGjmOZCr6iDgY-EfaCAVwAOxAv47u05VBTOP1xoUhMrxNefZ1lt8hEziCDaHInMkDdc4lQVeYv6H4rR2KugX0IXGsFc-C8sfQVnALLdQNjEg8_AfTsEmY3NqE_ECIUhFwxaW8s8aWBgX97Pi8SxkCwX6DyksH9fjA76rP4P5kpWl7ynaOaCfytRliE4j5uDXXywFfwN64DWKIQt4u2gDGo9d12CWUMGrWZZdn3qn8IgEDmUdr_CGXIGcPNuS-wxWoh4G8eGNhvMk1V9zhyhcxgbjoIJLl1T9MOZZ8JQVpiy-cPgClLI2jgIbKSVZTTZ8B6T93aQj5oEbOw87RZxArjYP2XeIHMNh6JUUOND97h1D-tXlI6hlFtFTouMxLzyOpVJLfdrUcr2p0bkbNPAyk3qzxwdRWegSWH2nojJVRP5dopYDUvX3a6sXVGUefUr6llKEtyQ9W84oVESDWyhWRv6GiBkpimAlkoolaGYFYCD72gUISM-ptvaWmVvNmXdZhR2JCSn3Ec5K9TZMg0ArIgFvnJeksow6nIwDSYZ_EXqtEgn9hjLaOcKZSrixLgvGqWY5phJcyYWP7kBsJTxc9U7xCIDh_RCU8fjZzAOAl4r3DtGTEntqzqhScZ_-Fx4ygPgpi4Ko84FM0RvNQGw5VSrOWADroETQVP-La2KyDOjYo4dTauA5ArmYnXyLatcyfbnvgE5KofVhMHwPq-QSV7QAaN9aM3KdDRxBXV7YtnjPx5DzLQE_61NLQkdC0iWFjHwLwM58comkNfrKAUw3vtLzWDiLHT1nPG0pxYBn0zAid0cdOFJ3JRJl2F6-GuMSeUK6kCqbX4mtShWXp1gn0YErlKR2PFjCDNj1o56a5ejMOYAB_SNIjRLO_O7uGofXv_Om9Uevp9XKu3ca86Qt6uOpwQsifkwS6j78cGRTJeU0SlIAGBjzi6b4aJN--CpFIqF6JpuZAxhiLzsHAXRAKik3Lu6Pmb_24KBL5_ktbQRcQX6GQjGi0A4gccSOF3hdJ9j1any3RaFOA1_0HRAv-ExWoiQEyUnWALcqaC1FmXgDTxYx_VUMjeb-MqxAV4eHjJsR7e1q9cJS8qhubSQbHMH72GccTJKlZYdLBHmc0Oqejf-JKgaBMxgkGX30uCXhT9B8dag8jVrDBemQV-wak7QHgbAveaWX74ZsZZF6ZuZ6YU1llAllJlLWPVNr4aaPj_wMfurz6YyOJDnCcVxcKFjBCJRuTBF1ACh9Ye1aj5wDUVwjeKXnjEy-quQNoB5c4clujc-G-ep6-EHj6WgHZefu1HYolZNprU9zHY3T_OrisT2jDBUByHv2RajGe3K7nDZprR-e1SPApINTcKQ42Fh8SfDQsXg0qOfvMdKbfKJqQizEQiCtvkQu1oXhlO8fC4J5UkN3qsPcdG_h1TQ-_zlAPDJ97B_92zV5NkIF3XFM2iQht1oWwZdN6xwKeDRqKmpER-qz7bxiy9Hh1IxU5T_Ac5c8B5xIxbQzgTJal2t1M-_cRvGT0CjpEBjRxqts-KliiGxFl48wNePKySRiGEfnn4Xfqmy4enbmmZgyHCmo-h--qxLIxBEykrcQurpumcrK29z2_jGUNichMpAaaT3UlzgVTbOVb3gVN3Qsu8ltR1RtlO5DM_Sc6q3GQ2QpdHafa2S8Z5D_A90PuohDCpyqvS7tA24KNQEKYM2W_ONMBNNEoyU2p7hZezbbj5T_HLHVRPUiVLgugGFQkNwZ5cRgrgYqstoKu9VJWFE-odBF8G9GwHGFFqyCdBL2CADSx9AnfEssP0TSarXyn-ALo1n5f6vpUFmkcuY-4gFSang5orkODd3k7hSmsCxs5NVMLfQxPtjJcTTrKR04H7xAVNnt79YJYVW73UaXEUammc_qu0GAuNwgeaX3wIQv8ieBeqJvGbfOoXd-U6c8b2xS7b_9BCWtTKZ1A8azUrXAqOr5rXlKkq6I31ht1XzyQAWq3_YWEc8MJahqr7bR5GQqOxRg_adTocY65i1qhxebStP6XWRRurHWyHzDhi9duKfGK_eC1bbuUIevXsNDHdQBDNE8_w1BBBlg4eFuM8vSDZWJEKPxvB4Vl7ciLOs6-diW3bj_JDo1BZlpdDQFKCwDuk5RtRJmr9hGUaIbF6nrjbFduzQFh6laU7VkD_3XyqJ2C3dCD1vOOhslfiVG1fBWHpTJvKsgfLa0u94IUipo6YWCz8K-LCeOymEufdrfaI1A5qutL6tF0CaPl48rmLRMayxqTf4ZGCCDe49C74wOS_kGmxchhr8DKGUgKwiWJWQjIQLIk2PzaHSQ4cE8uBQebBsCMzlrzNr1YhYzvzhje-qorpNcwCluQeaXkqp1WST9LbExS1jN8gmJhLgS8yAOd_yGdJchugXdbfPXWD_R4oVf40bCAv3HBB3MxQKq8dZeXg_9xqr_bhwqY1oUraAHLEol6kUS--0eDJ9PzaLed1ZQ_6j-pHR-mu-OkQUvtM-THVLuNMKWGSYKcBnOFYw_1NpEkwoWtcYCzk-nq-aHJ5XnijDKutRPJQ5W6RLMmhB8qFoZpRp_aDS5LJiqp-Q4g2QhtSCckgUwHN5GSDTLaYvjkR5jeIDI0Df_tQZQv7BiusW4M-iXMunM3qpOcdAdfnBTmODqjdeBAk4dRnayZtb2Ib-JKl5ywa6WUDhpA_UQA_sIlBBbTjetvlH2sChS0D17boDPANxqPYQLorzUflL42ay1DQFsRRdnxTiNvzN3nMOxzFdIUYqWEiY29KQmAFyuERLmtWNxvUB7KB9WqxV21mbJ-yIhTsuUTHve3HdcJuWPzEtbZemmvTyJr1wckTGBWVfeT20e24dPMpBbRN24Mpx_tMxfsioxNsXFYqKHzqWqZ8Tp-gj0TUMr-dATGUJHHQ2Un1nVUYhOfB-G-cycBf8zmgcnA9EsKkTOlZY1LRmvBIknw6thweHCggBJ8Ke5N7lgYjdTTPs9HXMZk-YcGJ8Q-TkB4_Dw35xq9_hnncS-Dl-_aTs3FD-V3fAbAd9eYbttpwk9kwVnc3GzF_d-eoCntwtxNH_iYmdeBZIqLZAoDwzvFnGfVunFP4RiUtLYepxu1m7HLhPSCAQn6SNcLwGg1U0jQpfYIYGZTL3Ntq91XYv3J9vy5O1apgQZic9XEMxzOuoYf0zDEU41PaVOmGv-H-mdrmH-MI0AquibmsDkD1GoUssNDqsqGVBgMMp1kc3N6irmLeIpdrSjOLUsW8eq0YGWoMXXxp32wIfDr1fad4KV22Slqlrfv4RC2v15WxVI6j8Cn2l6ymNxCj95fk55ibBk8IgObZEwbu-O4F6focQnbqXcLMSHipxWVOo0PNAnxeG8ER8AuVaimP1nXVWhNo77VuX_Yat85m9l4Avt0Q8tR6Rpqruw0cxZRH-3GRk97-svz5QsXMJgNZsDquzmeRT7ydwFrr8NK2Ei9NmlZ4pziY4xgIjVIJgIhgkY2wEH9EBDPLuqmYrA9z2RC4KUg5aMAvhRRZ1Jrxd4uv6C7iq9o9x6AOVwA3AzuM-A42325s1cNlnURin7VjQvoDg03eXsB-G-iSEUw_WoiFatKsO1U8bW4GP1-XwaZMD2w9-NXF9JCCGp2PaYNl79WZXpoNqtOv7CS-USx0vOF6DLllVZebsUhgMTBHg6I7dmJShzC1VLrCV_XjFCVlxfSdC-HkHceCUwQwQvkH7CzkW3Xxqn9onVcL1vMKgt-D7ov_952u8jsS6gkzEkUZgSFKNUMJGZv8J1rhg-ZNUi_50EsohJTlxy8H3xw8RFN9JsTZ7T7_O2yJ-yB5bCdSHldOwfQWtPvCw0df7yzUQtkMqMY384QRdKraWO3CwhrqD5_j-iqM1nw3AKDnqvUZ_pL_MrJT5OwqvaQLlIJpSymmfw642aXt7P1TzzFnwOYb0Myjc0geBp6JKLB4MetCiKUxmYP8M3hiH8FSZLv00jUmVJj-CPVj2IVml-IiAPyPU45_2W_Sek_l6JDqxgviPNU2QfLqXLOgs7-30-8ZhrtlZLC1AYco0hIEyVvFBQC5CjorAuillJuZ02YU5_kNwGG-Avbqb2zLhjw3gO7ZB1Lz68cv8F5YVsUvCvMgRhgpr5Wj_5uFtw23HGXHKY2Ejm3Kjya_Tw1EbrPl7t-UYyUxZkF6lUh-ZnndeOB7RWVO9lDvW-kuu5XuYFbAM6ouYOPd0Am1Te__qnJe0cYwKBaqopwTCE_7cu9EH37OBm3YWyGrthggmOrcK9jSI-xA40URX30vYvyuvNzZ-0f8PrZIfTtss2f0w9om6vDpwxsWhXRlTyz9qc0ntEgVwX6t6xWklLasPIwXZpahtO8PAA9Vqy2D3t-nMSyeBaPMhkZi_k5x3ckiLR9RHH1OmiAyYkGafn1_aB381MKMv_8AS4YGzeAvaHBwwfNDBlPpBhdupAGXoGPKFCM6d5W1QoDhwQyIZ9uFKuvoPtxntY8MwG5x-Vwmg3GhIDiSmoybRNIpfIqXUVzg5_a9p9b0-Go59h9B1ntMB0K1Q0X1EtZq-tVRlv1MRpSjOl8LFyGFQ8rYS0aY54cZgE_tdOaozg5NuXDJPQR515WrBf6NyJ2E66D3u1Fde7hd-zUMSiASQXMKwCLOAMNn4f3MWoj6UR3vKPjtBNwF1umNrE8P1tErywv40kYGz8-Zy5Jub9dMgKEfXbz1s6XIqZJEDSXngwVYNQx2fhaO-uGxt-eahjkVAkt1KoTe3sDxtkX7CFQNAaVBlsy4JEqRM1-Mxg0GfAP6M5l6MMhbqkJoN4oC4TVUlASghOUHqkCorULtgKctw01Ea9UnPzXz-KKpA4RllrWdUryiRH2A5RPs3KH6mTKVjJmzXvs-tHHeQphSLLm3QV1smoj9Z-oAJrz0C-f_Y0LE4Rsaw8Ag_7G9OOrBOD1odrNT2PbpvyeMCv2179maxKeUB3WRIU_Mz8b4_vi76gODzX6t-K5zDm1ukMlpNLfRtD2FZOEu2S9dGFFy-Ut3gB8Vnu_b1wnzETDDqWZJ-6bo9qRxrRAkH6q3TF5VTKv_hnYKY6QzcmotJrdTNPQvwCztcqj4c45FtJyax2tdOQo4lhoqDapMA9TawQMxunVToG8YmNP1YKJljFq-ZFttAxcnIpaTYq9scd3cfS0S63cnjaMT_H_LEBW9FedIR53Ko12fyQn9cLgErigUWMWwgdTmE2rPo3ygRky06cEcrh6zUtNb5E0Xt8FnmR0n53wZbJHsX9N6ficGSVwanB9ZBGJz5TmRHdF2aE6NrALFCVLZ_9mUP0XVz9HSUH9YbauXqYM8afLJ_R8XNm1WtqX6gWkCG4HulNtWURyTWgVuQT4jiB392QSDulnwnUnaFiroMxbHD6UENVgg78icspfeRQ3I_wEKLpCmngQSDvgNlV-vzVct_920i-n6DSDav6Ez6MgxCa0cgrF5Fbzak-koA7olgU2xqiyoAFv02H76alrTcE6Ooi0zNIBABz8McKSqmJDhJ3RTpCYQCmJ71Xq3xdeT-9-WBX9QgNEGQ9BAcZNT8IHY7yUocfYNOQS3XbCogSc0HR260BC8-8ijyyx1RfZB2kErTGpUCo3FQJLg8QNYU4cThUe1rmgzC1aJSHdYD8OLKHflJCHZiGGaYW_MA-tBWfHiEISIUcIghjbVjF2dBoMZBW5hlzvYWOV5y1QXW0zvTJ1Tw4R6kJGWNTK4wePkrh9W3t4wMu2QvyJQLGGwb4ltSDWefD44MtkWdfquG7OTbXqEiPr2KreJ2j3DASXuBDBD25RvlZc4bhLHFj9BUJ-lulsAvDWKCb2Bou0i6akOancevmmSZUwphs-hQM2b3ugNTsgsUEoF82dXWCJ70gyr1RFBfBsZCYDMDWbiqMYC221y5Pw2zoHRdQ40xDVCmTzDZZxzBr3ywIcE0Y_6c9tlm4e6EgOkdHg5KaAV9sV_uMLbBeSxyihQgJuxA4dzQnCo3Q_owAGtnkvhQp4UgYlx2AeclHenpTuFb_t-BsO1-DV6LgRplzfXH7ocQedgUXsd-gZtA61tnwNR2qRk9dbmtOikjI7qf7tFv8r0pRbe_d_mNadmgformlLzAtUn87xkZLmcMx_iH0g7gW7gbEXnkKmX9syage0xeQ12qnGvGF-p6mBKFUM7d_8ZBFt3pSd0M2Wl1zLnK9HQJVPXjWWBf8r9UecYdpyhtZAnxREWSqG1APYDP8cPpQcewy_QaCnVqyYZRFkf6X6ch-O9sJAwzR4MLElaZ31KyCxHTj8565hGC5bJUdg_I91UgH2yJArG54y_Yc5Dl6ALUn9QgPzbqDFFUOJjwU5o9uD2XyEBYzEErekT-GqxtSGOgCFSStNay_o8OmjolNWZVRc1_aFeMUOgh_GJCAnBMs8AVNU8rG-2bL8Yn_08Lfn-QpqpZIZIVsTZinG9cCIy-nuGGUtwHtPdG8xntWD7d5rNUtro9BCoxdrnbFOkSAwCQ365HHDHG-D0bnxTd70UQLYZcAb6rkxFrENHGBQFl5f1sOWZnGhofb6snJCirTWsgJcst54Dzu14XaX-57i-J3gi6pI0alrVQhxukhTtV3oj42A2TUGD6Qb2P_PjwhVbwpyfkd9tNTRT4YKbB6v7FviTl7JKRh_lMFAeLiNc10auLFBnXOdq28pbt64ilr05QoEABo-2qj0w1qRgK1RfdC_x2WRHcrI7zWIyDONsyqumIklidGqrEh8EXCSg3a1PBLMIrUfkfyV8C7LvTL_lifHl18bZO1BJtoksrMcCmPiwEJhCCMn1olm_DSh1YHahgEFrP9PhmLrFpJrymDuzXlWENX0QfqD8_bsiaIC7sqi4ZCnGI-KCnePmdiATIkO1ROI0ty_1kRce2LFztuwYFLY_z1yJlFflviLtyjU2z3F8Dl5JjO2dWm4n7bBCRT8wAqp5eztDZdaiuQUZKi9vhIuEnqFpL5zQVTUlDpMWodeYlcEZT0pQQamulicCkRslA7Z-CThZgOW3QWCv3eYTvOlZ0merHzQFxYq-8S_0rfwK9BEA1xck28GdMIXUd5cqBN1kUPd06qbwbCAgVBABucXvWbmkCeokCXOyfxb2BHl7381ZWy3_U6M0AnKzxhtYBSmBjY8sQAeJg1WTQ0ZpbMT651_b8ipPHAUl57j9rwVzxrdtmtai0VoUVNv4UEF6gDR_byb09xWMXgCWHrBMbbs7KNNC307cI7lmSHDwFDiWjxXcZtGMCix71kfh6uZsRBursMcnUoIaGvd_Pqv7SKeo3c1DXs8d4yraU5VqtmvHuodSmfcmOCEkzLb4lmVfBZPrsJQcLb9xFH8wunqxWYhr2ERzOJDZoLIKNwQnPDcxoK7UX_tLfbHKAO_CcfHWRgB_NkcPVvf8jViQRTrskD_19WqQFq241yN8yW4a61C6v-9og8yJyy8BWPQdiKESA180YGsfujYRx40jXR1u0g-WgRF35S97vOzm963EAkAmfCPBpRckAFxeDcb9DfBvhihOeaQEobt9UNhiDTNaiSN_Hl66wA5DIPIptw0_HQQLoVQ6HUevZymcwe9A5p7_AdCf86KBN-Z6cu7-5OTmctbwROcfjMYjlJLXI4vSE1fY_BdaYPBvPWsGaPKTNr9kwy0RyDrYd4a3hzDBzEOAGUJm14pdaOSbjtwoIJ0m5TeQRm-e-EBqxv4dcABhod1agzhWgyKZarIrtkDhGW7dkDqSdxHzPCxphtD1a7SD2MdKfz0IK_IkPRSr5N690e9kBMO8r0MmuMg85Jf4vA3w3-ywnIbaW865qXxkW-3CYgJ8RloGuBcJewQH13Ozoz1FAlt1Gt5Q-uHiMokLpmbCmvGVk7xPXqDu_sqRhQSjlEXRBjmGzeotBxxhTwmzqZfJxRXEdmGAtrfqva6gzYGgSdXFWo-_wfN2-DjBa1Z8FAxpmT-dRPNvaKwOmknS-tI5xi2i7kzmh-oIn8n-AJ6WanEBaFc5vTC9SnQNxnjnnbTu-bRMj_KlXXpw-ryvlGEGhdMOqfcgSWzQLPBSVMJpDU9rSZMfGl77Q-S3q9mRfjPnd6TqlNfOskpiQijqlKNvhC_D2S8SerwBOrWTSZ2i0W2NKgtAvkgn1v7wHkNIp6iJ9CU0mXIobg1uDrdvReirxIxuznqXyf9xma99oqKmQvh4dWfhlQH-a8AB1Hl624CTjEs4CcoZfCm2pMpcDie4gVvQiGkHQosnTdOA12IX3REq8peIyawJpoyI50ConQxCFuWqKfZkxvaLMfVAHcpvRNrNEF-jD1lf6R1emRB8jW6iQLCKYVueF6qfUsmb6Ql-gmKcakkB71QGMSGTa91eBg--S11MB79NFQdZhQDpYYc5GAAKTR3PF9Cj-xk_33qn0Xz3Xw5jRTZqm-qVcqPMwcdxcB9p8JhtWuhGcfyGmON9hM83JHg8xKGUn-1qPOnvF1yWoRcI6wv7Xe3jfo-_RHLEwbPTbihfw2H6ycYxEl_iz9zlG40_WNJwwWDdHn-jsau08fNxdR4WC9FEvC7lRAUeQPVxUWE3ziJjlDMeZGz2jy4daSi-LY-QZCzarHtQ4_olBcW11Q8gtV0lOBrkATxbd7YRAL7_dh54Xw9T6X0O7TlpofzzAVMZzIn0iTai8k0eAzuj3DT2FiCHAh4-RbKHr7mzyrPQ0MUmJp2PomCnzG25BUbYSlClBcjtotLGm6YuDPzB5X7Lu_vH9eRjxMEh7ZqIYO6m81D0dwZO9aVZSSwa_LBb1iBFrHijTsL8rHXXcBSnp_jIaZrGLyKkxMaJDegmLd8HdgACP3rOqVCDg1n_CVE3_jRaqwwHJVpani_j77aSGBmItjp7HqbcgZr_CVMCBHX3XfzlhuXZkvBoc8ZaYYifhvgGFGEg0jHEaxIIU0QDqm2L6dHqCH6yAlkkT8zRgWeLH4Pey8nR2KTAZP55YtaaU38cUPOqVlvTmPihzfNHH18h0vLfaPPjA712C9V3hvVACSpU5SsXQU7NfnnIO7_5ZcX-iCaEuDsSFlJcAJFaSyKJh5kcXsGdRCAM5nVfyH6_NFHzGiNWaIqc-E3Yl4a4pS07bpe74bsEUrxUfdgmY9XULfNwuGPVg4qBsSoS8coVBn5SxwVR6OITKjr8Iq6b8EZZxxc6qJJe2Xd5mExe6NxAW3sClorNhS_wwcBYwj6HUH8SmXpZ0xqADYVqky8bn-pa5j6RFNSH5zz9deI4_1ioLhkVtvpbRFHOxCPzm56wjqQnEci9QQd8axmpiKgHP8HnpTzLHO2MgqjjunSox4sXOz_BEEPWghInV_VpmFb0KN0B4UH_M0f9Yar4O1unjCGwlLF_ZfLfNfwmi8JoDRMYIyFn6D1PxQgdBBPKN0oC_Z11E28WQqTORvTJqusVY4qoZ4d1FOkd5E9srOWuvs0gBGweaIzUAZHdRGr4NygezGmf27uWSos68ZHaB2qOc79z_TpsXiVeik5uT-pSbt2R-GEIeg8cwCH1J2u7UHsWLmJFyUmBW3K372QeHxoW8UKinTNg4Zy6uF5acVZmom5E8s957-83Qcs_unrHFoUTPy_KWoiqRefrQcpmCHra-JYSYwNxfwgzoCp-EHgl2ypCIZ5BpRQHgKweWJWeRhioSBwGejT7evYEl3-L_FazZFY5W6tKyXFktO2jIySP0NMGxFL8S-PWQERH9cdm7l1KN849iSIqeMI8cROEUCWjUIhdh9pXJnY8vYhQBfbEjJ2fJFjOEtT8ARZe1jBPNUFdoRph8YXVXRkHn0uw826uIzZGnacbNgRwgNdilq-j1Rj5iirOQwXSQ1s_L2Y2Gl8O7YZ_tuEek0ovZnebzesmYKtoY_XhunbD_U-4afK57BtBTsmm1Ed_AwfhZNV_vqKC5DraEE6c6J_7d1f3NJEMVK-QDm-iMLGdLHjOr3bf8TjpeXNjITXiBZ0kJBb_qf7Y6Sze1UueGWd_23NVi5Ufe8w--C9fE3YT0Hl0wnSRJ1WvOGlLQf2Hgk8KaazMuCVbkNFzjojCQ_IrmsEz2sbWOSMDB_E2y-6JJyET54mCpfMYhdHXVhtbAH0sdBNtp2KGfh9206nOJU-lKwjo71lgNm4XoWV5Ux1LXYSeN9r7BSrpirkFIqxyQkJez9Ulcbiz5ES5t8oaTwCOnIDE28Vy324HhGPSi5W2QPkCOV_PjOWCeM8yjS_6w_FnGuO_26ecaOEkCNBZung5p0pHSmD9D0SeQ55YvwYvwMhT3smiwDo9dRcFa6sigkWHHKtBLW29sYLB4r5pNWtHd6CihJCcG9DTTbaE5qP0-eOF1l4GKEhtIUKDPGJGwEzYHjq9emeIy1uacdIcWTCJylvCVOHdWmLaD1HefI1tjSyga1LuX-uZPAYEu4H3BHd_8RhEhTIIR2W1Zi4pcy___Mg6UnxiELbieUU9M-kBKnEG8wm1_VCAJVg6GulXQG20z5Zq0Zr8HsRUEpcO6ULm-_3zF1WYWSPU-JDi_ZiKxGdLOidzU4gb-zzrrLYtA2USFwdncVimCESLHhKPSvv6r2xX5Hz0eTuLmhshN4wL2du7QNz_mLVnI0aIGrHWQgs_DEy06L1P4ANm_Y-0xdzookmfICUGKChRsnNFH5Ardfg5JWwzC_jQrW1XM_t8g-3Hnv_A-UzUyJWBl3ezae1NPikowsbMsIwLuHHteDmQmqb9-93yiUdXB9FxycWFgaPksF17KxTvI8FS2PPwZKsSOTXMQNCQyFd4fJDR60nQhm19DhQImTl_QPvqibTAg_p5zlhxlEFdMKoMEdSrqovWF0mKoOLbIHlGum-tDlq2Ll96PE2-CrnW8NyHVDdew8iZSZ5dahyl3prZnh_EiRB8nNBESy8uH9ppuSH6XlQ0TJXdhwI1ZdOJvFonZ-7IBR1TVb4ynvpzRt-oWE-tNx1-6qwSJGzrsKnn1EYkDQaRj7nfztiOa9af0LGUR5ejBaZVx-bQ-75PO-xBTxd0UpI5kyaEf9T3rUM19GzASEzvIwPCPRplhpopMmPORqBqg1oFxqI9vzahfzntnYmWEBLGc2ks1NZWq1gLcSZLw947_EEGgyqw51cFGXLaB1DeA85qa6WT1jRmS4Fjj747XLPynyNH73NU8RWsx03F0y_fvUpPGS_vaXWR8AhEy-gdBW5CCYbsPv7WB1Ls0_DJMBSHylHgNQvC_5knHobolZyERyyye0rwmLca0TnAJS0QhgywEwaoateT_H3_aqypXAFQdqP9aXzDLINETQH-jPND97CG-mhA5bh_mmulEvQMxHyt1e4d2IWPOJjYUvSj1gaxoNl8C_v-h8719rmYl7e5jedHHzYQuDgq-i4B8HlQxgLycD2vQqtt9F8fadudBvjaa4qaHQNw_AZc_8aWNUQ23FdSfC2ZSwJvYASGSz5iwwZotTwF92WMyzfnNvdjFyluEZR4D2RXnYP9GUuwGcg6LvtzjZDq4GoOG8cZEqgSQpSUFWN4-NUVBrb8GLY-SDo08tW7Q42PvN8h6h6cPCpFgrKFrqEuNupBiw_GvD-Ihj6S81070U74EpW3yin5jY5dVGJO_Q-8GBVsyfe9VyPGlDCt9p2-FwvgP6aMZnWAQys5HjDo7QxHaLXAUAJEB4HJatbd3sDYsC3S3Py-_NDzA9_JuOI4iqvOjwf96mS8xfOkoDY0CyKso6cn7BWBDbtgGL5yjjAOrsgyRzALWaUehhq0p48D45hMtJh40lBfgA2QkEqXaqlFdooXKlfyn0nePdsQPYJWxg4O42Up_ha9yeggy_bdTtWJQlR1bpgphhsDFFhPq3rrrD54e-AmMPvLS_KnhRHR22d8t80bo2yhrXzT612iv6Z_2_wxWbm8AnUB1L4t1pnI0BW9MLhU0EC55f52wZCJQ8wJdRcH4lbuUsZ4ioBA8J6X-UtP7YjjBTeXITfvyCaLvkwGseuU4DCiTHh6mkqIq6ynzsg9kXqjCB7oDfO8yZm82JEuzLWaReeZSub0J4FAyCUQImgs3Ui1shcwK6IVbk57-Gjywva17R7qQhkYxqeDCbrd64y3QLFBnhiYSN4TrR5AaPiNz3eCYFYPTdMjNCWa7HMb8wgI8Bix513uKuS7HenMc_h1QwCzrD146GKiiEZ0LT2IIDDO8h_gKx3Y-7N5B9Og7wjsDps624fXnr889NYznFOBwuVhNmT4aULq_L32VNXYO7bvGEm8T__RrBnigqlftf0nHzP2U7gN3kKnuCg0VryDRRs30No9mmIxpCzEkGfEDb3g8SxDiiyOjZEuFTG-doTdRDPfe8DqiPTfJdFWRfDkBKFbpnV46-Dy1PKe1HdpoF82ggBjtwT6N3GZ4MPq1UVYQ6aiwlk-vUpetZHohzn1AD15XlDE_NfnZHhvGrHGApPPUFCMmZRmqQTkNH4IEpUDQM4_SacoAIdkrgHO7PoUAFoHYMpumQ2pow4VTR3mj0tpvG-iIBbcxvqc5XLQQZhXuhDVAEl3p8HPTDKqFgxTxiKT_Ns2pfkp7zHS9-Qp6VzlZgoa1Kt-ipc-BOpwBzzeDqg5bOYvDF4mySuTfNy7RnMfX2F0WZKN0j0Rbo99iNUgkvxQNTAsicaZGuGWaUbgiQI5OT_kltLhbL0Lwk4AQpgKHQ0OBgIYC7ONSWNWlHqRTR0CGRYRPPB5tOfzJ9iVeKQKgTnH-PTukqdsxJyrwalRgF9I_b3qBXCFeY7Ea1JyqYhi2c1OLLoI8UJ1kNsH9Jsuww0WjthK7U5KQEHkQTZSjdEyoD3M-daQhocYGcPqRLqt_kfDWpA9fQYJVlMCUL9aQuMdYVz0ZzZwV4PhAoqep2MwxErhdjEUPhqyt4mVopZW-Zyigqpw7ef5K8lrBvtfLV3rt0hFTzuxACp1wQOWVsYvY36I0Yff9iHGHaOArfsR0KgDgbNK7E7D5CtFrHyOn5XGjWcdjLaYKvCJ8wKrIItOXpWEMxBCcKsKsj3bo_jJKiKYS5hVeaznfwc7pi0J21-4BAkb9Vs4XqIcooEFbUlqFSxWMuBokQAsxBEdeZ4ZEWbD_jZdx8NxELKLxPuKiYYmaljKyW4NqhyeGPgFxeHV7PC8fZ5O1Zg2sTMkW7J_BkZte3oGa9zeENRYMYmVp90gURGZ9vex7-GM362BBH-Uq9w9XYGL_yVfylRVU2PGoCEmMoxqgxsYTt6t--noIEO67jMxWhOdX-i2bLo4xdZnTBBDiiCwDLBM4SS5FWv9Q1b5NO8GL9ePjw0PEowJy6Lhq1MEBrQSR_AiNr7tAQPoJc-ltUMtBCn0FrDKT8UZchBVaMPazNXHJyJB__MZfJLc36Pr3xI3YG7C7plb4MOzJ2UU7knbHbcGM8WqKykYOBlde91ywezS-WEo8EUTO9rVUTDPwSPH2NjnuFnu9cEAmXYicqip9J5WLcnWxKuo51O53VaSXa3KOwkRsh86PPoxbN_6boEBx2b78eQOgVrE8T52OD8SryaCcj7GmHsA-nLWXhAZ98WTCCR_O3N3JZSMDB8NNKaTdyjILTThzcZBAMHpCZteh3JxXO2kiw9Q53cCVt-PNAVFwgANiyFFW00sGKI1VxK2SqsCXupmVQqzwJ_VN_KyQfh56xgMWxEucdcbneMoOWUzDZduKIBBhM3BiiaidHeflnpuDid8poBugQVdxNZdxxi27cdV7h0ieu0WAJj5G4DjNY5XI-S3cilYnTXUNg3nE4kQb6jVsjVPKwS7sur3AvwPld2qHJD5Zo5_63axnH-FQuiA2oF7pZxoYiz4IYY94ydG8gOOYteoiwEDD4tDi9_p-Vh19qsJ8NyAaC3sO1mKZUhLpGX4W5vXI9bONL6KfiZtpGsNOS0al73DiqdLiFtAcp68geOr3ym7Miq2xtthT-mCiNOn4HugT-rogZbzPlRK3aHEY3MsLL2BBcPue8ffnazWOosLQuThIGdGwHxSHwk9crZito6H3rfhy5FQYRZELbjkp6XwSzWqwGNh5PvS3a4WxLOImjdS_SdeFFztTbz643sos675Aodwntlo8e97352Zl54dJVBWQQQXZe92VNcHdywcaHzSA2NyLRWz9kJA4R4jHUBq0Kd_y-f_4LZMgcnSJyB_kxotskTdJvy8K4VSB7NSgMxkfzv-DWokMaWuZ6i9lhG6laXjt8SzVmZnBXx2fcGgveBZ0cEEy_ZAjwSaqkircbn6rIcmwjOLxsSvcyHHaB4371u2OZzhoM1eRQ6I_wXHJP2FW4zESJYPOhSWtJ6Apz4rHoUnlDCcg1MnT3Q6PvRNDq0jB26NCCl4ixvXlWtuWTa6_bXBARoDauSXsf9YAX-vnSTK2lOz0pOWgz_QjQw0Lx7nEi4sMXdnGvQNxkSiGAmExZzqAPZwMGbdAJUnjc0jW7Fi28MG3G8cHvO6fcGMo-IHUlH1hr7vMVCViYqjcZQOJ6YgAQNQNe6mXCcsSJij3_AeMXOJvC55N2l9GkRBkByX7-NO0zWRMGZdtYxe-25RMM46v4AZi3A2mH-31HphZ34kIlBH9yb-8Vw4cdUHpY42kEhnXusSk0gx_bGxqJRVVpVgo0EAAAkhSRkWSqJiccp5iZ1yZ2EpHOgEM1vthLyCualal7K-fTHBm5jSjNqNNiZ85xJF3tbnHSjLNdQ-sYcUnhDFedPfS1bzfVZrJBfzjp9_itNRPeJnHhYGe-K9d5TQqjrBAtwrGnMkGhpegfK6Ac2Nklvcl-yCdX0Fx_OYe6peI4slr4S9XmZBj3ZpG7PX4NdyAKDu0GwufKIcSATJlFk-1L17vj-b54H5iFj5472wPjh-E9NJ2UWS5GbEC8TPpqw5wQH_Q4KnOIE03lgzCcImIKW4jK52uCSsBljKI5CXQzgTj2lR2lf7OqqEwyuFP6KEm4Gbd98fASaqrgFmR3CBqJfFkaIeuluglEt6hbkIQU4KlhVJ1kwkOq23gcjyxC4TXYEBNake_62MYh17xz5yxky34x6cl8B-e14KXqOG5qG5ug3gsoD334ICr72xkt-m3mICgkUYOSBE83pb2AA7YuW5IqwTLStyt03wQhYmDXd_q4FBM7ZO-uwue_cT49vvpDHBAL7zwG9if6P_wwVVqO85qFfri0-S37JXpakkJ6_9SUpM18Yo4g2SbEoFLE_psEgmhRAVyGZjGMCU2Yb2Nh6eQaVhuiciWgij3Hf69IJYKZ7dgNmCuuTMp_VlJ0_bDWGlAQZUvZoXemSxVUvOEMjNj0JxhAnuo6Pi9eWLcpy018a71RUAcCrdI6NLvPBNr6qYJgZL2YE6lLe5kN2xxuxtNIm0PdkyvAo9N0OGwXOkQcY8KxwwhBPI01FGQ1ULM51ICIEBERqQD5-RkIAICNR6o8zZD-6Iqah6mvg2OOhpEWzyTuIV6y3d_hOKpYtdPZ0tYpmGdXjl0CM6UZmUyAxk43Frunx0UQg3pA_Awwu5YhXCPek64_gbjQve8bn5Dxl6ZAvBAk85VngWQNtjH4JNk2GABmghnZr2ZHWhO_GX-q3KKTyOqbUjACY1il-tUhIs0TkcQqrYLRMXRrSACeDKw1VWm6iTI_6IYfcUGs_H1Y0fgyCSI3lq3495MNy-dbp-G5WiAQCZI_mqzoxTcr0EifYsDKQuzpSs4e6e4beFerRgJmLVr9Jgo9heM988Va39i0Vo0AEIPlaZqLXrAz--eT1xxSdBi6JlxKS2uzYsl800ySl66rIKPUoXdkVni_F_20mmkwEGCAQ4ZJS1g52aDOSjCYPuP4nUfCCL1868DyocogHBIwr7PCQ4-_0e7rKflnzCoPtETbNRKJj55oRaiAlFdqaTWWSMp_LjH7w0GFXxzTtnuur3GA3QaeaCO9bIPf-kiFhBArunZ4iY6SdxqV2bu3ANgoc35zfPy7r4wZDnS2BfHFn6KXRHhns5yN5U-OVjT2pIBWbLxQj8J8TOrSGYkpcTwJ526XWPKA03qIn2pOEe4wUDkW0tkxyyIgt5cCjSPWhhQQLsYYKJ8rk2ojWvIHSdHSgIof0eVI51RGCW4jcg2pJ3I25sFIfpgqI5QipxB75eTIB32XCBtzWmK2E6dPAQfnHNPYITbjLmOrH2f6zbW1_LJ3LVtMMijseSomNhA0v4KUEBy5aOriMgwBRc2doCITBcWz0OD6TCXbcrNvW7g6BDK67Ym4Vpn6bl3B4tIH19TNQB4YhX4z2kAyhlOOlvwqMcfhtdiNxuSZ7BAqQYixn5dDpswpCqiI_MjH51TMikt-YBBCHTr-RGRIXaWxk2sTl01agDUdyWGJ8wsP1f0ndpLm3fHdejNab0MOn6osZGpP3ZgZIYoX0o7CoF_5lVDdc08Dt7L_yEmzk4ccF-JQ0JtbfYdzvc4OrUBm3zQfNVsdw_AQHE0H8y3wolZFgsPzAOF39j-_9SDKkZQAHkO42MKEBuDYNRANGd41ztyybua00Dn8XEYC7OiWofp6CNgeFts0oXhYM7YU-0A8h4n_xVYrk-0Rb-zpprX3pmPsLySXIDR0EBHRdi54BjFeutO1ODlZUI0JXKinpc3TEq1Q8Umhk5Yid-CmzYfaVtt65hsdKIybzDgZkBSqOZHNlU-qgtHZsZjB7HhlsQH_hsJMfO_GDYmvUyL61zZ_6i-kzVl9kQzarBALNWbFaReiu2SG9cY4n8raKYyXQxQXE31wFUrKaibEAXJlq26xQzmZmf12t4-3ZVxMi15PRbREWLYGzqNRARqU3mHd3_FPTeaLxcWy-KfufvSTVOIYkKoAXAbHfGckSZgQMlCPqKvao0Lss7N3bdcI04kJRmOcExYhAXvepyznGreKpfwWLm2YpoPgFuWq2cbkOg_KNOxeI-SCe8WL5geA7u7S-PPZZ89jarsvO7kPAIQXxHg7a46y9wzDLclZD7UcECTva6MEKRlMP5zsg4EfRkmZ8AQcykymQikio50dvSITkyqtD5XLkLYv2eypab6-1CHu3z-YUQSHYLOw4fsU6dR8lToK4I4pl9auL2j4z2FqwZTt-wnGkTXTevikprpz7BBaY78BYmJHquSGjIEoy59aBoFNWsKLhyB7r-JFAVRXgZAspE59-JmzJVSIfyNWXThYFzabEXW2VmUNRAcb2pRUP7KYWY8xqgZTvQZ2mtXQBY4GpAoXR6jgH-fmWg988kAQBxRnDoZgb0VqOUNQK29C5BIEt8CsHE97YSouTsqqGtATh9YQUinkIpjyHMAYRfnkMiywoFYeaJdEd4DFPIvJ_MmDWtg43nh4dbJahewqSfAzmFH1B-js9WAG7bivifCkEFdHfWcyDybAKICp2iZ4clqNYH9EoSgYJuDnUoyHrBvhWbaG4CZFi6bALdp68fj_7D6MCId76bo2D47SRj-q6bzrQFHvrbfK86EdM5KbJftG9ieNvuE7PjAEAheezl1fxBBKKZDCnxPzovqnmBX3mnEy_giFlxpBfUm7g0ot-FrszjXCMAcw4PNQchogsmtV8zQ8XZOo2Rlay3YmS9-nK2Z1jEBXckY8C8y2IavccKdbWAOUidl9LsHe0wLA0tC0YcAQH5HF1yfqhXeaUXmVA1tF7vJW6tBMsm443zWLqD3MvCjC6DoUb1O6IMaeSwvS7spYGuleZPr4OvXuWcylIBgHS8TlIwoo4P1zBFAlYOYCGsulS8TBKmLxOWskPS-grktYEBBK-uDxU9pVaKCMWy_l_LV8-r3z2HRajh54V3cEsSiG5CF5_EVeFJzAzQTGd79k-AjLERnGw7kNMs4LWMhPS-00_R3nRt_OPxiVnSY_vNyT3HHpf8Lf7NQnZQQ7jM6d3BBSmIUlvlECPBpaVgP6oc1FKSkSPs-6DGL-DkJW3Xo0WlcJKwl7rIXjCrM0t6n3ioRNkxBOg3grZKqF12fnWOn-jtqr0V0Iw4Lf-3Gh007OcyCIy1-RENp6DXM8JKsg1XwQTo7OfDfyf3ZSDWOLan4L6hrHPXKBKtk0m1fJvJQ9dwEM3jzPWJBilBQDI_09Nr2MCbLzNTGi2wzGMlMt4B8u7g6B5wmRWKDZchS0pSFgP8B6maEEZ8JH-c6p7wk6YfeMEC2Ih-KN9IEUvnsh-b6jj0FwcqtpWKlHBJFWJtGnXMT8rDuYX5Mm_-lAWornFLriTA8I9uu1ZOGiej0pWVgoQVWFawXYkYuoZRW5q4OGBwpiPtZIYAyDoZeAUOu7FAqrTBA2NfYfJr9vsXJOaDiYPDHRgf9IPb4xQHM0YSgpvkCDTERAkFVgQ0lLemlf2qcUXjgmQg2MNuI1NcMCu9A9o8-g15M6Sswsu2uLf8PD13MAUsf2bSudfdKaViZvkMCJ-VgQKsy2y-9J6nybC5tzJ9S3yfnlqMyHkbrxFAUf7NnocSzZcRtuRUpuGZsx20gb8xHIA7aUuwd41zsDvsOUpovILruvtFXnA2_18wbHXFKUGmKPHYYGLsz3rhJNtjs0dZF8EDD2XVmxsow3EHn4CXSQkJ8x3D5sDdyQE74fx_9l-BybhGK0-Ww_qLjHwwArVN6GcDacya-onH823CihgmmZKN3bg_XP0Q1c37IUApEO-R6ywQpAOWGv_re4uecj_1jmbBAxwRcvCNpNSwoGTm8_KSozpV6-vadvp_RC3TDHkH7f97yLxJ7ROIt5J8cQl-9eNJBHtVvWv0H0oe8V42gg4FsXB7_Fv8Ou9YUFWaJYb7FVU3IyWGVNYJyPoT662ImG2kQQHTzoNdHPdqTT_kh421XyfaJINAHA3KzKTcOq_4uNp3hq158xepsHM8HLizQKPI_oM3qvpSMxj-BuMVfkDGTnsX-JLAe3NA8yuFiZXyziuYw6hC4rMLuV5UTNJZnGS-3EEGSXXHCfghBQslnMt4jDj1X9FYwL8cJCmPPC9sEgpCfBdPYZCJUjoxwd2i4Nd2vweECi1KOOoFCdmTcDcp6WmlQxv06XLgfCiyC50yBmqw034Ukq2IsrYFPDsITQIQG_HBAe6k-2dxanLxJGlZK6CPCx2MKGElRlIESSqa99pCuUgzdvs-_ZbG-fjr42LTHtP0hHJy_ngCjrt8IgDmUKI3xEvlXZRnxnp4jkH-7FwZoKkh01DjFYkAscw5BjAlcWFqgQFnqle20OyaUTMaYIvjf-0ZUOpGi_wab0RYW1i5s61xvKyIk_2evZ87LyS57WccbcLy88MJ26kRxPMf9rOcEetd1aZxykk73d7A_pj7zxIrvjeExHyxUrM0XFgLN79kvoEAhyhFdZ_FZItdc98yLjaToxZPORBhTn1w0nj4spz5FjshbItFfVLfGCsAxgxRI88AO2oB8389PNPMe8tA4uMPMC2PFTqK795Hek8Vos_khmzeiXwo1BQaVfwLglOeKhUBAuoVvCyh93vTjhapy14oMAt24rP1eeHnQjee5Lfb_8p3gXOMQ39yxQ0Ts32B-CfxQzbPQrRQtJls8Y6lVDr0oOFz1gMHDWRrzA5z3tqHpj0Cxe3R1luIIQ06DHrv73dswQFCY6mYUsMfumIz3WAO0sa7s8fzbGRpG4zcA5_zxQpkwOEmTbBf8n_7vCRaS3weOMVJBuNSJCiQGBHR2eESoSSbV_ESxcoPGf-Wz_Fam4chWBty66ZX9gMqaAE1zWKAGMEF9zlemaUpKjF_NQJkTSbvh94a6Rtr-WR9QhWFzNxPBPIxItxGb5yNTiGZ6Ie-tQJE2Kyd1SmcfUY5fJnCdItfpnyXL4WSAbSsob9XVg4Op0uBGG4yXL__kme-X8WI0wABAACDV6iueeDk3PptXUV0BSR3PCdB9sa2FWGoPt81rhXS1voD5ApICH0CYlLLFnsnBNNi0fB0f7ZKC8y4286yDEl0NhkKDvq2n9HkwBGA_oiFOcGotvk5QXufiP82pBzLwQOow95Fx6OM7HK_uPVjzxxdawXQgSdHoQiMJwbUK2UYbfr0iYvGr8ERELWRTOOiBcZYsSsNhYHMvwVW5ahDFqpCiW8JJOq6gjlJmZ3cvwVWD7kgLmJXMnnRqtqaYl9Uk0EBEw6CZI8R0Fprd4sn-AM5SIgL6PkVm0AsR9FkBxFO5F6x3-DMWIZnbpEFcOjgpkwAtbmPtesiKe7w_XeKXSYKPfzCM5wyVZ7sq4BZaQSMzOEOgpFp7_W4kjVZuWL4HvPBA0eaJkqCCnO9CvTPynRPisSgqY5zcysrcKLAAHSQ247c1yi8smlgYsFznlptT_2rAD8h2xfxUSv9KDaokZ9LROVtS1pGJumZfwAKuHqEis6B5GAG1uZw8SgmRDB5-_dcAQWOP6jgn5PBB08RKA4xGMxzHTTF0iQgF1HMX4ScdvPmR2tC1g2_z9NYw5VvHewjIQTVUgKhl6WkLiggz4qCItjEQ-sQaFctZo2QgTphAAhAPbVVKGmXydWSPn9-MLyRxMEFd_MFPx0xEKWUtWopZnXoAnB6cuRUlaR7Ex1bd9kSJeRT-zS9vg6SmVVeqqF10HbBydZAp2CPsaAXMzrohNXkjT1tHa5DFsGCWN8Pl96gZ4XU0hcy0-v_g66wmMXmP7XBBUEh8wlJ2tg5_32LC9uz3mUecfSbUnNnM7jzPEBx0MWh0T5W4oXWkjl0JtkiRFaawUveTNuckzEnkGqxWKC3Pfi-4_c19f14CGUzZTVXhAWYKQD15Ldl65r6xU7U87dFAQUOHcEY6KUiQ-xEZztcLU_KDfunv1hTy9IE73SiYpIvhvSeus46KY7z9D_G1Hw7nQFhHgxspVLEjejdXY5Pms0wE_YhQ-bkrCOPXpnJxE194xSi57ykPsPH5TBygVP_fwEFAdqOPwiKKQ4MV-d2G2-omn1DCyqoL0Vc-bvCee7FYytR_RFO2_xikbrBZwnj_buFvANP_K1TtKf04nY7mjKJiSbrTdpywo8PvxNB2JpBD9gkVPuA2oMFvUFHHownN0jBA9yWmiKpQTY_ZqT2TR2bmCTmwL3sZEdPVl0oaBlPiFZbDTLGgF-4fBlm_xZl1OiAhj4KxXwB7w_DqvCS0V34A0o-Su4VjZzaEqO3cTuPCBuJRfnExkN0QMMtx-OMPaumAQSyZ7-x27l3q_-q2ABDt7hOImYxGar-1FLvfxxmv_aAUPWCKHHyEk-TpdjgaLYs3EWC2FD-DNMegViiW_kEhe5hNwBo_JVCn82HCUH14yb3mZwFNe2vAp5WvSVoSdkBCgEELEZw33U_IZSQ5fm0BtguhMiFPbE86oWsZYU3cs3LiC3hW-hEBIIiqIh3zxWg7Z8AcaoK_0hQeGI2DANl22GKyVTRdHgB6Vv2Ggz-KqB3NYkLJ3AirxooP_x_mqVVoIj"}}],"authentication":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"assertionMethod":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"keyAgreement":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"capabilityInvocation":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"capabilityDelegation":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"service":[{"id":"#TrustchainID","type":"Identity","serviceEndpoint":"https://identity.foundation/ion/trustchain-root-plus-2"},{"id":"#RSSPublicKey","type":"IPFSKey","serviceEndpoint":"QmdPZgcyqHJTiPeGMcAu2AAkZZ1U4KtdQXid1gdJQtpvyU"}]},"didDocumentMetadata":{"method":{"updateCommitment":"EiB8B_LS_O3NWo2P8fSuRwS32GODaXoLREZHdqpg6x86yA","published":true,"recoveryCommitment":"EiCy4pW16uB7H-ijA6V6jO6ddWfGCwqNcDSJpdv_USzoRA"},"canonicalId":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","proof":{"type":"JsonWebSignature2020","proofValue":"eyJhbGciOiJFUzI1NksifQ.IkVpQV91YUV2QjctR0FyRTlkeERuMk1rclRUa0t0VXN4eGJPc1NESzhwQjl0ZWci.X94wTgzsovLEAXU1CG5M0Gqs6Gu9oHklr4Zn7aEbrdtOI_WCSCrWJuYomkcdeF8X5dV_ApZ6Gh08pPcV2VSClQ","id":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"}}}"##; - const TEST_ROOT_PLUS_2_CHAIN: &str = r##"{"didChain":[{"@context":"https://w3id.org/did-resolution/v1","didDocument":{"@context":["https://www.w3.org/ns/did/v1",{"@base":"did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg"}],"id":"did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg","verificationMethod":[{"id":"#9CMTR3dvGvwm6KOyaXEEIOK8EOTtek-n7BV9SVBr2Es","type":"JsonWebSignature2020","controller":"did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg","publicKeyJwk":{"kty":"EC","crv":"secp256k1","x":"7ReQHHysGxbyuKEQmspQOjL7oQUqDTldTHuc9V3-yso","y":"kWvmS7ZOvDUhF8syO08PBzEpEk3BZMuukkvEJOKSjqE"}}],"authentication":["#9CMTR3dvGvwm6KOyaXEEIOK8EOTtek-n7BV9SVBr2Es"],"assertionMethod":["#9CMTR3dvGvwm6KOyaXEEIOK8EOTtek-n7BV9SVBr2Es"],"keyAgreement":["#9CMTR3dvGvwm6KOyaXEEIOK8EOTtek-n7BV9SVBr2Es"],"capabilityInvocation":["#9CMTR3dvGvwm6KOyaXEEIOK8EOTtek-n7BV9SVBr2Es"],"capabilityDelegation":["#9CMTR3dvGvwm6KOyaXEEIOK8EOTtek-n7BV9SVBr2Es"],"service":[{"id":"#TrustchainID","type":"Identity","serviceEndpoint":"https://identity.foundation/ion/trustchain-root"}]},"didDocumentMetadata":{"method":{"updateCommitment":"EiDVRETvZD9iSUnou-HUAz5Ymk_F3tpyzg7FG1jdRG-ZRg","published":true,"recoveryCommitment":"EiCymv17OGBAs7eLmm4BIXDCQBVhdOUAX5QdpIrN4SDE5w"},"canonicalId":"did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg"}},{"@context":"https://w3id.org/did-resolution/v1","didDocument":{"@context":["https://www.w3.org/ns/did/v1",{"@base":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"}],"id":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A","controller":"did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg","verificationMethod":[{"id":"#kjqrr3CTkmlzJZVo0uukxNs8vrK5OEsk_OcoBO4SeMQ","type":"JsonWebSignature2020","controller":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A","publicKeyJwk":{"kty":"EC","crv":"secp256k1","x":"aApKobPO8H8wOv-oGT8K3Na-8l-B1AE3uBZrWGT6FJU","y":"dspEqltAtlTKJ7cVRP_gMMknyDPqUw-JHlpwS2mFuh0"}}],"authentication":["#kjqrr3CTkmlzJZVo0uukxNs8vrK5OEsk_OcoBO4SeMQ"],"assertionMethod":["#kjqrr3CTkmlzJZVo0uukxNs8vrK5OEsk_OcoBO4SeMQ"],"keyAgreement":["#kjqrr3CTkmlzJZVo0uukxNs8vrK5OEsk_OcoBO4SeMQ"],"capabilityInvocation":["#kjqrr3CTkmlzJZVo0uukxNs8vrK5OEsk_OcoBO4SeMQ"],"capabilityDelegation":["#kjqrr3CTkmlzJZVo0uukxNs8vrK5OEsk_OcoBO4SeMQ"],"service":[{"id":"#TrustchainID","type":"Identity","serviceEndpoint":"https://identity.foundation/ion/trustchain-root-plus-1"}]},"didDocumentMetadata":{"proof":{"type":"JsonWebSignature2020","id":"did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg","proofValue":"eyJhbGciOiJFUzI1NksifQ.IkVpQXM5dkx2SmdaNkFHMk5XbUFmTnBrbl9EMlNSSUFSa2tCWE9kajZpMk84Umci.awNd-_O1N1ycZ6i_BxeLGV14ok51Ii2x9f1FBBCflyAWw773sqiHvQRGHIMBebKMnzbxVybFu2qUEPWUuRAC9g"},"method":{"published":true,"recoveryCommitment":"EiClOaWycGv1m-QejUjB0L18G6DVFVeTQCZCuTRrmzCBQg","updateCommitment":"EiA0-GpdeoAa4v0-K4YCHoNTjAPsoroDy7pleDIc4a3_QQ"},"canonicalId":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"}},{"@context":"https://w3id.org/did-resolution/v1","didDocument":{"@context":["https://www.w3.org/ns/did/v1",{"@base":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"}],"id":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","controller":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A","verificationMethod":[{"id":"#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","type":"JsonWebSignature2020","controller":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","publicKeyJwk":{"kty":"EC","crv":"secp256k1","x":"0nnR-pz2EZGfb7E1qfuHhnDR824HhBioxz4E-EBMnM4","y":"rWqDVJ3h16RT1N-Us7H7xRxvbC0UlMMQQgxmXOXd4bY"}},{"id":"#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA","type":"JsonWebSignature2020","controller":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","publicKeyJwk":{"kty":"OKP","crv":"RSSKey2023","x":"EyGvw3AkcUf2TZToBh6pddeaaocmvTuLCSLun_yYJpL7x0W3gVEzeKlj06J5Sej9Duk0W_yGhbOKCahOx16LszwTHVgnH9FjRk0nwOer4yKaKnjTZ2FlZsYI0OI__jhCGP9cbcOEd-1rfvUFu-ghsj6oHfSXDBm0Ekplkgs1IktoicuMsF-bD7I6tZRpP9tqFGqARUqvR2daQN-scwYUNsv5ap3XakBCDvOCBc_rPAwzapY_nuC3L6x60UGBAPtUBANdaMhAU0gxd-3JMjcSjFgwzAhw5Eorr7bIp1_od6OfBRYu3sIkij5Es6RDBLghUAx2Z3dznniJRh5Xlx_8zn4SYw_xhV1X04vY5U4O7-7veKMqKxzzoGOR7O137gSTtBk66ISXfE0k6LLsZK0Qkzi0B6YQ0Xo86d-COFNhRWQ_Lq3SCSiOaJ4lFP5_RVlHzgUXm6XY1X0jrkVPWdT42VxGjFvy_KX9f50dOkdPJTax8bGv1nEpDm-55UN8nrIzsRODaxMBooRL1y4OxyW1tpHaEdsoHvsZrLzM5g7FB2ah-62TCGkPcG3Yx84MPp50eRPIlj2omMFxMpnAZKBSRMGtk35A6xAZUI6KTYGfNI-IuWKdk0UOn6xL8W3EwMTxRgx1v7iklbgxKuCBoOeAK7FhoOVzL5YnUCHb1NUwAxDs9I5pNmrvaXsDDLKLIoz50hRAdnK92whifFoWoJOOJbQTb9sx43zmB1J7G_T28MG6UetI4dZljoNfWpXePl3vNwW979nNg7GU3N_V8ZE_slRmUv-rAw9jD0w9KXVCuZuwGIKoJ2Co8qjZxnhZUtmi3wFJin73V5BC684ebh40fnA9z-H1Kwa3ItX_mQSVYeMV-_1fydNULsdhlEnpwI5XNQ25LGqMNb4v-YRBXLSmN5CituV9rPXg5ZzQvy8VVE9qxWnicCxz2TzFrxFOOIhNTxf-YQT5Re5HJAvdy7Y9szo-i_PgskFdVm4UxMgH9ddrFUhDPNmVtVY8PoXlMzuU6gKR-1np9J6FBttHOIPu7LFFdO0Vd_Y3-Dl5mdBXFcP1Do1GN7ojcuRUB4rmB__upRAQQsqCApGurtGP1zgtMQm6ozF0gt_JpoXgvZEFK5kkm92vpedrSfDPBBn5NPIgmQgKSYfvmWRmADyr2J9bc6EjJr1-YD7QR1r2g_eGRBE1S6dexWceWTq-RktXQYOSJBnKLSkbqJniuoA70BMkjU4Jsj1EJB7oxE41RRMchA4BRlClSi31ga0T_bk31rNTLQNLGSrBrh0x2nlG8IZUZLB4fIKKweFD9pL1qhLMM-SQl3YR4-v2wxjlMXTrEDjz2xdwJsQhhzM5trtqhVdxfgBwB_ZBtU9KJqYvkB_3BhY3kYQSGDLhyCHbjyIVYl7saQGkTz_owGfj8tD3gU9oJlZHDyjf4p9AObfF4YXKjVBpPrPgwgNd-G4LAgUOn4DAVwGmGBjQaNWiLet4g4lRsLS3LkM1az1w_KyYCX_k9bptp4qLgwV6HqbLx1V5WkmubxLMpHlbV0tZFLzwThEaKpqNyz7M5qIyDvaSbTFtQ9feXhRHU7VN1MgH2AQmQzHiygXHs5qafdGSsKoMm6c_6R2-NXl3asM1TSUmD82yKonGYhSHHy60KvB4M2rVTKRENxR93u7gaYr_4cqFY9LlcqGUMzxmm6TadfSHz3rSj53C8c3Z3U9x9ftbKGOZeybdWhYbRGyES_HzmlXV5MFY5qHiE6INi_ao7Xxm8VRi5rdaHlVDWfBb8gJENbUHDDcsKQfae-4j_vXmvq4s_9L5It5kVLCT9f5NEf7jsxSP3mg9hqgwdY96ob73GsHO3HRoQARhPUt-2o7i1JzScqRH38AeDr9XnxC2Qu4LT6ffOmMKzA3qngyxKmkvyKmIl3_eEhDxpdTSf2ba6EGOD2GuzvGv2a_P9QHw52mvtEoCLNJAslzsxwxbLSnLIOkbJca1Ew26womAjSgnNwUvPCkz4lmSNTbyF63wvmNJJeD0UgkBTb2MxDw_39ukWvH0mOSJegpmENWzMhvKvxxMgB5Y1VY6Hq06V9mcg4iD0AdI-dM646yU8iLfMAAkB-EvwUUMXRE3KGU9Kx6dqhsSCrow4QDpzk0B4FCATLwawfGc1_rxQyumhF9nagl8jP1ITcLi-hlUyrOsKfSK_s3WKTw4j9iBoBWCzHrX1YC_2UTnq5XIdbY9tT4NajRzqwKLV3aYWRnqXLg_-l5k0H2GmwmRnm4ZqU-9YuAy8MQR5CM93H1gxE7oL_IWIyH_tCXrVH4hRhjd7GrWcA90s1AFpCHhBZs72ORxG_Rh8VcJpB5cTpbQfk1ESme0-UTXoSnuLPfNIQb6I6fwFkIvBx9YL7gxaVmjHMgk9BLR89iwuo3VsEsAs4ktbFfZ70l821y6q_xmOBPF-BxJzlVuHMq9hfyYVA-1ka8tBBeEy8NJ1PlYBMiVjHoKWMfqDKo0ONNv1Il_ThirUq-MM4pc0ENOqwCYkomNBFfFHdbS8L1Y5yIruufFxRbRPt6xC1TnDtq3K7JCpRjsTqv_1_u81WA4UIlW49NaruM-2lPlL6P7rWtBqG4axy6U9WYqom7aXBW0cbg31hY39xZb49G_SfSYewGr_pelurFdTag1R3ZL5VuDTggqErrppxKIBYHQP7M_reJ8fQf4JcXOmMkUOap1K7QJvvENxlQ_RQRj10d-t9spgDv5gki7uMDSA3fp4q4gf3HxZhYwPaImQ9J44zCCLUdo5dyhHsyd9neEeBniNZk5LDZRfX66ERlj49CO2dHmHLe-YQACZnMQDDug7LF0il3QHinPD-nedAAxpjfUus9Ay9vRx6nB3fHr-_9C76qx_NjCehMZHlsAOgZGU-yjdwY2uu8lvnb8dvmCbkIBYn4S_aWJ0qIOEjfWuADwWO9BXI5uzQZ0EhKuhALABMhOIi4pmnHqCE0Durvn9RaPiFz6ZKFhW2d85ZAkks_-ARI0phaKzggmB4E6k5EV3cLqkI63Oiiq21QY0VCvc0LuNoAVYzG8s4bx3udSSORrRJm2fOdURg3wtPlFq21m_7y8D09xKpHkXgEbuDJV3hWk52u0Rxv1MTY2V2_LkHIDF6my-MZLQQh0dQYnUjDfvQ3bTqj6UE4MZ07R6UZzl3Vjw53lM2x4gI17Trma17Ag6Yg6XiQA7QqgXKWy3jG6AuBLjuYRPeYo18lJm00D1D_Z_C--D6zMJKr5ohYrTi4ea_dh3CI82xBNwjeTAd95r6X0wzC3xodd7FSWJMCgt0MF6pz-MEL_jNi6sK9mIn05U4icLZLjBwl2lObaoiYxpyWEpnuMGy8J7dM1Z_aRpYt3J-Zw7i3Yf4JI2JV9u1Mo-ywQyXgRcRBhK3emrFT2fxH8SqkKwJCWn7frvbukOzSQiKD8RFuXA-SWK60mJ3erCRnka-xkGg3AiBxxeE8Prk8EGzLcB1UDRGQ_x1PXmMNtdBK65dtv1b0jGTM_uSHFndWXOrFALwi66JGyIca2WnCfQRQDR5EPyD2d2Naecbj_jMwFUsbYCxGTc76n46c1pI_QH1rxDBQ7j1Tj_rcQz6Bk7DMTNnlTFhJn2h7yVnoRPenlNCWZWZPRpr4vnvS6Ii30os5W2QaGHI_TqhhaXRFU8Z7K4PUUUVEv6u3KIZpvcuVxAbcx-ppLVkj-r2vM061Nx9aXEBFd2whV1Tw2rjf-6fm10N7U3ssLGC6sfHRpSVcsENk-ZjuYH7sY-zmN7Hf8zOYHIAZDUr1rjCgG2yCujbdOPFtPs4QKC_cFSzbpOjRmJ-urzi7duH_vH3_TBhMzM4jowgM70l1LoB9sjQ68wzlaAs74T04IroWMULoZOdaeIS54ugR79EhgqvukrIDLEoCekAY7jAs-iNW14YRPrtdul8zVUjLd4I_X3efx-IX7HvR4RUp-6lqMSN46IfvlScl0qBY_SBgCpdEw66SRo1OAIAuTy7VWX_mbvLtgZPPMkaVheFwYwBZnBLKQKyJHrNrKRQ5GdrSnJP89jdh-o6VEqG_whEec3cB1LwXipXb6v1vi-7jxU4kpU_BTMtEChb21tRhmfKGiQxHbOTRJbHVoQJ4NFlS14bTYAEuJm6yXnIW-GOVCLvlHShp5jeWc_9vvvBZnk4C7bDxY80GxadNmsKy_-AcEFN_QI9pt6lckDeTOQxgVz6Anz58RIkvJ1oPL8A5FZOl4iYuQGDAqTP6Yo-SdHbuVOuV3aM9K3L6RMgj5Z9z517O3oqsmthQdy5xtxhalD2bjV4fNsQrsXIGuNa4nAnFtfsi0uN4ahR1_YYVuQgfEQLOGSzJnw-bQ7m8tOxlDOP4MsXg6BFSBvo0LPwieTdNbZR_N4FueA59bt73HfANTd-xz6ycnZNRNO9DbxBRwXJnQogguwZQdLLLuZjqoglKwi3gmMHvCR-3QngZYQw46vAkTUuYfdG0OgaYuAAqtsEvJRaBVSud7q6pgMqM5UbG9eWv20h-bMQeBEpIuVG08HOEc9TeUzDOoE87PzBkfBqVu_s1tyItQQ-DqSvfCQBobT1pYeVsuyJSGXuaF5MXooxYfRpsAuysjWDKDNxAarmMCpioPCo5ebD0elYa6S1KV52RN15vaAZLPqNRiFkek3oy_M8C9Fi2nLzXG1Bjn_JlKzni0I3pofwFNE2ZJnoLSVpLwVLQUzzCB5GoS5P5C1DcPDxpjAr7e8pWb0QAyyIuz1EvSssczBargovo8iNxthV_MgoN4UGY3RtkDRyw2DPcFdji7AYXw_q3xlxXsWEZMfjTlkG0FfwSTHbhrL-BIXXw1u88y-w5SvjBBwk2wW0SjPVgm-qq8yonWXhnVfu4xRLMY7qNRltkzyB5pQ44rJ0iFr6tXtKus3rUTx2PbQOPNCYJynCWQnA8anAlOiTmIJV8G-MYkP3hH3g-VZSnWE8gQhbvXy9OY4YtyqX96TXRGuHNuZBDEHiPmNAvKkfgVdGE1xrxPnfZ5eN2RQWXAf5a8xgISY1bXxlt1prbFSiHTMLnikDpYNy95JBQnPEqdIYRhgzh29L_RQpIM2ItE6rPrJCl-NL0Mo3YZNdFepgL-5uOjFilpmO_EfAc06pm5sP-g6S3vOx8I9j4JrOnhygXvZx4Mr2D8-R_7s2F5QOYKCpcYmhKSqaPbdAX-q6oNQQ3fesRtmDJIVbBmioMmu5k3C8hh_L2RNAe6ItXT7XVCo-QFQ8fiUIOMWASrYHiy8qsbX4kKQJ98v070GnqCMpKVtB9522SHxJWv4h6Kpsmadh9WjAmzItl4tRV763mNcLeidWzlJFUcfZIVm9OrWbHinBUjKFnoeexpecTm2ncrzpUkMmJghWKv9hUzk6wGkQhsps-94GvQJT2ou4T5xLpeATQ3oenwez9tEwxQ07tB7FHEiIBpA4PFExNwdv8sxaEe2Zaoakh1iEjIbd4uBcEAd_E8eE3VSEPvB2_zT8nek2I9pcHEIHA52Q2_j979f-vAyJci99RN1Va8nvk3TyMz_g6OCknUZcqkhXK3lqigvhkUBl-IxjWqagdTwPfwGPtwV3JT71CZDfBWujVMLPGB_gT_dhsWlIN-sC_yiWL_thQrkgKFPqXPwQKCyz8r_iv4f8NnJIh3W6_hUURFsnu0NpVAlhi7iOU-B0cqk1NHN9BgNbT_zU2aVBEFBrlQetG5pyxxgyDSvrz-igEzZ9oqa7-EIgNv8P-0T0IUrlCIQSfPsiAUsbExwg5JwdgdQ_gD9HUt4U2Npk03XtaAySY1IXJCXeJLp0OIcc8hFeaiPMMv7Caif9RsIxjwnikwLFGtpNy70Ed6CkTMtxBR4uShDzbSz7Hk90gu5-jV5WGysOA9AbW24iqgfgCKjrjgfrod_MNG939PdD9KOV0x3MqbZJmBLB7jKCINC2ilgH3Ez4crHFZJEkuJ_Qq-KDXW7l7hjHUG_debtAu6qI1edYP09UkgmQtnZgLcGAWUhDxWhdf4XYOHfqXxfhiVu8tF-ly7iqWkmRCqhRGV5NmzUWuwvQ8-Jlh4kRa7nhpwb7ivyXiDubq85_tKuha0qKFzzz8gFuiefICHX_Uy3xM8m6Gy3KfYirumMAkuB5-IY7Dgr6IZK8YXGLZb3QEXmOjuwp8Rmm-bMnCXehgCJZplNtcWi7eQxsP4y0IoEUsmmC5Y1as1sAs8-R9XlxBfP3hdGWbOupZfS6FmMRiGD9HoWesUSVtRs_tgOUPPVav2HRIK2CLYBRwgI1NaeRcpnO8cOye4UgRm_UF36pi3hJPfIdCnhxGeOH5J0r9zYEnTDs18YsIQedQOJ9jvGBLvDi8dJ3NRzof0hk9riVtSPV7H2EKhkEL67E5pccehsmZnha0ewYbZdgEstjzjwQ6qkZRmFLOBdP11yCDzgs3eDmnk0Ztewl22-WhhpumCfNgux5OEtcSu6hcC_gtsXQgTm4QV09fFZJAH8tyfFildcaycx0w6zG_tT47jBYIwVyEI-Mvv08qYw3ZN6558VgacYehFWake3ahdjDxZ8bO_tBtLMrFXmjRpibEIYbWZW2OPgBv-4-Z_EPXtLrDpJxYjD8bUxNgxwyqxAlyqZe0FUQVo1RTWV9hzvj4GcOG7wC-_t9aEEv5h9hg3sQXBxwKwIulPSsJlAeW3dygypohfIMKiUdjDERwhgvPsvB_vsJIaVpN3SJVfNWvMEFAIRxl0o0b4upYbISICcxav7YjxARlPcV_nqG6Lnj9-6MtHOzvmwMWpcM0Y_FFro9TqKAj8TkAiGaEMYyJ8Z5EMAsGd32HwMhmdeJbA9TxNpC8CIpeNlU0H9JeSDR3bl76oGAPDIc7bDmfKjcCL_8rZamAaZucmCI4Fkkjaqyl_k0TOHrxrc8EcYzbICfu2Xp9j5Bl_w7GErvNIbMsbJejezsJxt6CR71oex_OaL_DyxGJE6bOaWZFwF3WqhVWMoMEuRwy4Z11DIsqZ2pbxyArURVFG3mIHnBJ7ffjxYbofuuuw9Ce3S0W9AwEvXRlquPr3-wLesE-Y09JL2x63dPrsfx88itwaKSyGuJyvqpTu8NwpAR8d0bU6nXG38O2ysH6-xwvDGoeApjhGaTD71tv5hYcJj1X2M-GeWFi74NjG-PYBkamWVPk8v2uimVuB402YMgUAe5RtZcKVUfHczIcj7IWreTJr8JCLl4N_X48ji2KDuBuuaBRBUYdjkl8ltWE-AQzatqUi3DF2ZDEjEarQrk8K6QDaHNbMAEQwqxIcKVB7rX6pwR4EA2xN2VYmCskYAReAbKYyzbFKgx-_kbylwjO1CMcDTdhKYHnfEznxeaxzjwopfWQR5JQ_y_4OExcY6gh_FHXXyMOQdyzdcNMPFOZDvKAf4PiXg6BV6VVbvlssgImhEbhyfKlwhmbHkrD90BVSZOfwp0m_zd_xOfwSYckSwo8ef1K6DILkCmiUSc9wiCBBGHF8ex_0u3nepPICWg30NqJPii7moRYlXNi2hKgTB2Cy1njuP9pNFSD-8cOxrrAoAz6SaxdS4QqxjykSaRko3FibccYcSE_fkx7_WWBSW_1GOKTqQltkzHWMqTbu3wEjBAbnQjYGEWn8aTNzsAh1pezmZurCOdi9uL-cjIVavKPn23HhHGfS88f3pRdohcdlszyc74acnD6VgT0VnArfeYPNBWcliVDnCE3qYSvter4l5Fe4rH1qDISEq2ni1-uxNRJx6Ck3-5bWSZxHAgvc_2gC2O5qc9TU-akXvNSqLmNtKmO2FGFtBltwgyLc8bVWAJrNxuWQVCUxXlfSkxaGXtN18lGJX-SvmRn5IsqfhUitHzJjEASiI_YOVY9OoGEkK1a532FFGdO00mS07BQCPV0w_gldLncCOgt8VPaB5d5SjOF0_whIcVAIY95y5MrZEJWcbES4zg_jdGb5SRLlr9PENPbne9VYK4_ju-MCFNo0uWibQJzJcpaKU2rZ9sAsT2goR_lu-aLGCdeimhRmual5ISX_tyMRikPCDidsweqUeRzPcriSIRDKLcQfzA3P9Lt_Mo0ql-l1EX7TcwLgCsISBJ39jyhHyPvNPbBAFAlrlF9uRhz_ATonpUwgZrQHSlpsy6Mzh-O8f57HKQTRT0VigvfIeC3J1TR4EzLkHUdC7QF4JNlprKFQl-HUh9VIOpwXfQ7VwhbxUw-MThAn8fnFAKqd8S-4S76Yn4Ns3B0FA0wlDWp9AvfCSlm50bQHUgj8FEtwz8279OoIhBEIMnA_rHNwA1gPMSAl8aU4RO4L9wTbhwVEs32i77O1pQS93ZeNwOwXXoquAAVFZwusOXz2C3jxzKzB6IdrA9LE7-ALHDvmxB-y9KUe-RgCfFgjh9EE7rdwftpCOMj30we1IOtQ1XyFSwpbIK-y6e6itkyx73nB8UicYQEQHDnl2UPtxm3TLUe5bx_E0sisng5ZV2ISypN4_CiyoAbUPCapdHnGLh5VJtaPPq0NGIVA88MkPxnJC_dTfsZKzNVDywA36U6dGzcSH16QoTfJ-ZcUJhHAKJHizKtLpdxpNKlSugnNW0P0XwgrRYAehBBqJAWrmDc2vll-f5KYy6AFEWfIub9SODwuu3j3yfdoVAjpi6Tvm_e_w18ZBYKjtRrAAg38eTrwQwdDDovzBO6t7xmJkqOxsCFl0tz0WB7YxhVMfhC6qv0ojnXM4XrhX482Ew0yMUB9Ql2_2d7u9-aM7VztBqRf9dtPj0Fc1WdfiMD1d72U2D5NukpfdO0k74QL4xFcEWgq0qAPT1Xd35HaQhe9KfUYx0d7KtbBb1BrpQ3zZWS_ThLtfTHOvGZRQH9bQQyFkx7r9Lnal_GmnKw_w-Y5ecOTXwxvtB_XQNOo2i02MTPLpYHXMCWCFB6kHee4fhJVL4yQnaac8WOYkNDZeHf7y15M6Ezs0ieyusNjY-nfeAuXS1kJ_lf-qI-1xCpx4wmOy-W4Y4Xbr5YWS8Pe17115uh3ZGN9n88HuWj_fzZ0BcrgsT4p5LvSm9lntyD3oQ8pX17phhk3xqItrnJYAq8MfnLgifMDl6XucGJj1rhsvVGfr_ccjSHxohBb0HWL6g16xEvKsXnQe-PHn8Djtpc9doxqWWC1QeFnjIFJ38TnZd2v6S9irKu2D-YTw_9TvgRZTHMLgHH7pdFo2P_-mrKP74-OvYkn0O4aUVAZ6-bCXKIZ4ZzFgt-aO6l6vyUUfhcVrQKcnRdrZ4_GYfiRdxlBL1rvcZAkVpH-iitAdQ4N0xFHFL3MO3MH_EepQXLXSgciWBbbc9lzJnd4GkCRT-uH1SKKtquXZIO28ERVLB5yD9xkl6-ch9qTYNnNcBDNSAJQeFBwCHB5xZoyuYfN9p5v40vfSDAoJU9A_3_kaYMyUBVaxQWnKjZrrA5hWy2fjRUnVpeX7PDyAyb6eZDt7dKlkWGQxvhDXRFeN9yjohquhDj9OSS0JlHsPLobIYEPThAwpAYAEH9aspydpQDzH5LdB8aSUzTmFvdt87KW_OjCX2bAvPUj7a8bhfrITHuCUwOl_hNSIaxUX9EuHEifvRKi_KnQRZvkTyN6Ji93jcr1wYk2FOjZEVdUfC_lI-xzuQDSVWUUl6URvL2tfzx5FxqScbNiq3xnIqLrNONk-p4hi1QvPbgiYvXevv6-KgoCOBN5b7E0KUoVcBh8GBPzCeP2EZwA6C9k8u55Ul0Y6dohgm5HS8NQfXCSTt7QQgchGBOyOP96JR_uRbyLPJ18KaFr9QTxkQrxpuks_tWBdd9QD7GN2MU26S9veV2mrWHNXBiKY7NNZjYSkfNyzvjsg3VCwvxU9kzvkozJ_hQnkOnEmlI8bu34cFvYy1Ms4X5fLwaFLMmG3SnAIwBsCz3HxzKU05NBHikuB3B79BGskfQK_Fe-rkahNqJgG2ya6xgeIBivC2iuCuVjM1xcVN3jM0VuwQOCIVwjPpyDgWwjm5rpjX7LfEzwjyXynX5OR8PVugx7bAFwv0UNcbkBNLadJmL5hZfeXHzgPM5u8M1_PEpwxRddCDLbmbY-Y1naQwfaKRQp_c6KwJtT3IzkOJlaYsUlEeoLQKfQI-OFr7Jy6N9-tP3x_0OpecilN6J7UQLOTQEIeygISrIiIkSQgL8m7YCl7cRejrq3kF9UutkU2OIJFseVIFtIKZL92vc3WSxj6A8NkX-yqQ9LCFljVw_acJ9tUT7tNyOF7mFKBQJPa92WpaOGgzq4OCV2nJs4GFYjXgw7uE2NjQ2i9_auhXryGm3uD3G29NjUQ6Lkingi5trDZLCzoFKtQ_-2tWnf6sC4HBlShllmYDfCCorSX3Qc9WvEwxLbRvNX0CgPCEoxIKHAE9UzN9sfWZLD6BCXAtERDgNqc458B3xIrpXpk-hmIe-Res9HtuS43LqebcFiHjjKKiBuUEBCSxSEYQPYdEII9QMsBsp9IoCOKL7y6m5EgCfQzA7hiWLlE_Xrppv625MGLzebKWzu8CP1mOPWTp4FYwaXl6sm0rgbAoR5XtNLcBazT83ji0Qhc39dVR0nFyvdSe9L-EFw6dbYUPPbQDh0hQVzwnXZYFi4wgX8iFfyvfj1cAGrQNfx2yekQfLm-vhGK_sIlCRVZf2bjS6rwAbVIhhPFuTsQ5EaYCc3QbvJg-slvxMGfr3gpUkMV24EE0dCemwKRyRyf9zH-oswETPMyAFTQmlx715Ao-RESnFuc1Ebl13oTofrWpye9ZaqqsGko3Cimdifa716i5Gkq2FJNQRRRrp979uFgzdwm2AL3Wa_5I1t4aHY0hFNXzKU5u7gNmtiTDyLSOIWLGfd44msxBYFSE9YqSdU-7KpEtOLQRppx3FR1TQooT35XW13oPp37k91Uv2j8wLJPAid7msh1AUWmpGiq9vhair7EUlZhnjNIEvhlTr6sIwFzsJPRl9Dy838w_UqVXhKcA2wJpTCjgRWXL8R8b6L7Qs2v0H554fmrK3qcTm1BgmPf6d0aeO9wsgj_cSO2gI6HgI4zL6PUQTsMTzhIY8pN8MW1jPWVa89yWjGjaanxKT6WyzdkCGj6NcG3Yh5UoKGeehwa_5FQwggBfzXYMIAK3swXYvK1bVz_68c3eLtW96nYc1mnOw0QmcuQ7ajBPpwPVqQwH1iLRS3nEWbxznVbgvcdHS1Sv8LcVU8htWp9JheVP2OCiGQPFFScImnsLDC5WZxJNohrxFO6HHJ_6T3py6zz491E_zWqb0B89YapQO7LKc_D3pU7_3-ug2A-BmtjReN5-I0QAaNX86gN5o-LNW8yl7DmVU8rDBHQBV7vZ4uijVQhDvpifKk5mqhztr7B82gamJD6gUucjs6nA9V8i9496A3dTMHdtEjeEIE5zkvtbLe44WyaDxa5KiwZikk137DL-hp9w5b2-ZjwrGqcNJrYwpTQAjHigL12EWMHKEnPEsSXqmYujeWGfB2M9_VDmSgf3J-XAZroxarSzyVuead1XNLHtLqQgT0Prh-PS1lDJ8jH5y4_JzNS6lN78BaEi-rBl-hyhXqi7ZEzGEyZVB-H9rkmCE1jnuQsHj_iWUkZFeE5wJRemTSNTxF_GqZrFTkTD68qxdtMg7nWns8pXHaqDxpWAFaONRj8JdfPCeJhQ3W9qIdugEHXFlYYtZLEuXAlBGkHQQlnL2XeZ5aYE7xDC2JYQRJBj8c5fYfusrnqBgsz4EIO5ewfwmX-OAJg2d9Pm0UVxGrXtTW1H277sVslv-2FcU32cZwwls4YthQ6fyoIVLzJTyMOYJUrpFW32r5tG425wn_Q8ezmTs90EKuVrvVo8w92JL6MDKA-orDvhvQ3beb9l7Sgc5yy9cb90rjD-lyQBgcDfJ0xHFnhjnz4S8t0yga42xeRI3r_mXd0NvRzTUHkedNMtRAdU-W382jaFGRBxXL_4YziKyewh_nGh6BlW9EQ83Qf0oSwb43IN4k6GmK6KKvwr_KiERaBougue7YpwtYyqCrEoMiEEMn-Sog4CeLzg6IuYx4awivB7VYGGGwU6Bwc2IkZkKUFxVhJK63cAwQX5Gcve_j_-WcRRGlUhI9W4RvFhQFpl0YfC3cLUzRQZfV_fWH2MIwrJm6y4VCHhnvx8O87qetR0kM7el6lY4Nrk5bNtCdBeoyy_C1sz--DjsmM-z9i9IR8PqMCZcX3gBry0Sn_js4Ka0cXPsKpM-GpR6L0CLxge1FdKNDSFUOacsiEzh3-LTu-rUUYglWzQShuc8_dtZrIEvVocirTKZ3gaImQ1M1EylwXITBxzCUW19Io1X1mxKiFpXKHtzK7AvEs0kdicMBNl1HsKSn8OH3jxwLSHI4DwFIGYBxCQ0vvG3NN5ZZ_c4OnSfQ-nojlgmeCjMGykcA9E__NgeddsOdWxnG3fVQFIiMzoJ1AtYnxHoPRbtVZdyWB3dX1L9AKxlFep77w6KS48z70KzKseRnKLa6OCPZwfXgP5kEKA7FcKwpwIaMPNxCOedtULYeDhclbLeDtjK8LA2q7a8elVyK6YRvseXaZ4-nnd7iLYLZNOv807ZLaYGm51X7aFt0YRTimfsQIGztdkY9aakmyH_XQkqPmlNa75aE4xf8FqLjwa3AZ9PcIS8EpwX_Vw_pFA0NJcvJxCBgY4Iz98FxssnBRC9dJ1aAn4Kd8lgWvHIXS974MFCCGhfI8RRVDl4S0QO7W6vrGTIZB1ngY6VHZQ1JG9NJOGtomR_8RNH98FwcPzVNUzy9AhGeKBS3WECJCxk_gKjcGB-rBogS4EU0BVCfxzCoTMJF51ufpG1k4eWlEiEpOqUYgUWAN_3XYWNhphToFLg-h1xmQWWUBiVS6tV-XVvEOgKCKp_b8dMJ_99civ11moW0s3XQpzbxo02gCBR9LQYl2OPBcoRr1bVQfmS3sljBMCgtj5NodsMpz-rIZtgbzdchFe-RE6QK4qaMwAUY0oldGd7nIW9V1C3hnGg0kekWG3JKlxMhIB3IbDAVQ4jRJ90_JbLVaj8v0cNmhAwT0QwIwuTJJYFDGM1fYrocL0UKFsHEdPGZQFnfGAeFoMQwUt3I6zpmXbIqWA0VpRYwiUwTTRNTSsH1_eX-LWUnbXBsOmr6X38Sf9SQD2giVwmji2KBw4GSfRjUsbae5gpgZZbTcXH2ZF4FK79B7kM3RW1yKHcMrT3jXyZKjfEee008n6CJraHTc2sBDtV85wr-TQgic1VgACOfee02nwbPgPGhlUsN1e1cBwTGCJiIthec58AQtsEGIsqpTwh0axbKUmUaOj7zuUjDTg0imRCdYb_iMh8ya-YUncdYTabPkBJYlnbHzCB7aXmq42akqBQTTTgVgUsrRy22Q9gn7CkGltOZRbiPZ4Oa6Uzu-CYOsK-0JcD1xUgtTd9icWNNbAg5DCHh8FhryzVmRa5VUkC81OQryM3CgKdyzyw4xSH3qw2HcCMu7VHbHYhvVEXOQQtSaedW6w1shQMbPRKt0Bf_n3DTiyvSsfAgZmA3lrhQhRzd710dzxxljzkbfYEl3Q3SKg2CNM4Pu8SzAcJj9M4WubFMqDirRgVIMgL4xthq9u4qvIGxTERgAu1h7xhUcA9f0IvKiPzBkfExW_QIYR8c9kewkGILCplgqOHbvNBtqK5uXJrnscBUm-Su8yfc3gTiWWlsb1KBm2qwj6uXOBWQ-u4xyatyltsx8AJlshq-YB-K5oJuvlwCXkeXkU3hqRM4SRwLng3VyhdL0Jr5HUv_M1ENVemAJCR1W_6IXWxbChAYiRUFVnGQMCf2Jx46eQo1sNMaO-1r1LdtVSJo4ZELftKu2X0BMQC-l9iQ5EfDT2VEPZvl5JszWbqWIlkr_RY4jwbY_OeQCkPaMxE0eywBeG5zjdTYzmPLm0YjmK5J-_7tjM_678RIQ8qyuFPuNRGFUClznKIZ-T7SYMtFie6XAQ6j3q12Mh4-zEomU1jIOcy2EzZzTVgrpmqVtZUB9wzPIsNtq27VtLz231dh2i2fAfAZHdvIy_7XQsY7-JWltkQ-fY41Dw9QOIhDb_KJHhFNH2xa3g3NGh1WxZIiJNfPXXH2pMA0xU_FnJF0uPEr2u0rEcTWqTsDgHk4krHglASUYsJYneG_YgBCHWWrGXWzbQNGYsZryPJeXNcY3hw0wO49CxV7gb56BbUNBvNIfgS6SogajoeoPTkPQAICjtAVhnrgXyIFnQ38zu9Cwjwqxy10jt04Gwm1Q6xAh_CNQwcLgtJ7elaM7zi9uEGFskPfZHF35EOhpMwR6wBoPSv0ESs8PX1_WKhYSakFyW7SewR86-W3aCDR6xznTr57lJB7BnDb9_fF6rjfysDLSjofLGwjD8qC43OlMNZB9m868hgZoCUKvSnTpVW0B2NcAoM8lgXDox6cxZPtDsW65C2fMFUmt8yqLg9MOB9QRvr8jQVvgQ75GPADaHTVbcDukGOlpWsE8qHc0y8sbWnBRwGu4lUVpyOe3R-q2Y9DVCPonQoeUt3r6EfyIPeid7GaY1S-jCTuj5GlZA4Ridz6yYYZmGXzju_OqZL9TpH14-DvywWaBu8ZUqvz9kVamnK9P_M-jTDn6iz2zy37xyEGtzWT5Mv82avznCG1l0kSoG7HPg2kdA2ngIutv3-sn-D4_H3_Wzni52iLO-5CdMjEHyo8IRF2gsHDwR0mkF5uGdXv8RD_b5KZtgMy91QfiU-h1B1OTDWxxhfSPDO00EtPBW3UPQhkMJY2_MdHzKiG6i28PRjUTIYDcQjc1RrUZFuBmD6S679gKEzKw25fKmSbk6MBIhBfV1Q0h9uX9RauUq8yFRB7mV2EQgMRzrSZd0LVqNtBcOCU7TdrpzJzk0pZkfmjIVGOAJ37T234ICX4_M28IgaNiluXWNYvW8j7k_nTy6-8uRVw30AJnkQRswmxllkn8sE8pfxq2ACMG6LhiwkUeRJU7QYz8GMhtn1HcppGw27GGLZDbd1fHQ-X8EyC_pEx6wcSKdLWOZJ-TOqBWCDHZAJJ44G9MQ_eYCZKj78LA5pooQ1OQJeno7YefrhaY7gsJEY9LqHaDBBrDYPefTlMYgHPkHKxgkT6QtpbAHN81lB5uiiN-o2HPIgI45ODYY8pmvk7SY5BVsu-lJ0K3KZJOhOsfQsoK9CWB37yZj73eFNgWO9Wd5qmmiRVbUyBrjWSXc_dLnbEAKxB08xoITcG4hDIO1TSbTIF1QsBKXbyH11lwKM9Gr3bGckU_ni5H49T8MeAx2Cce-oeZ26dj5jDGQwwwgRbDf_9eKjzVzH0MtA32QPr-ZDqwIPJlpSAIswVKI7W6-TVHeKdYjBufEUoVhjsJ2kZLNnwsgUPySarkA7PjTLxcS7L5eXTIzBWpcSqQfY6eII492F_RPgaAzRnqRW7FA0lvNcCblQJoRK80DLGM_oZajzqytR-ZgfJvWQXY5UAcW0ywx1hVklrP5H9hxJBM6LujBC-bfK2gatWTUNoo7ciIWk8WPKZf9jCnGd2s9YQhwqJfIoYWLYZj2obHw-WfedxSpLOl72ucoXM_UvtvSjnnX18plcNrQ5lkO4f23N0gh_oZhdwYeyeb1N-KADIKIdY3_6tj1AFOqN_vXTuFtEAilg5YpHC5akZeMvfOGunAVza3qucicsRDEYutxcXggArT_nUZa_j9X5lp9EItKRVyGjBvRa8VKDwoHe0Qq9JYaDk2zA0Gqz2BsXKjxS5eArOJ4t-el3UdlFrsrGz0IIM53LsVDnYFGo7G8sQWzxQHD3LqVKhumuL4q0I6gBmOZBhAzzAb-j3dE8MFDXLKOzpMXj4yY_f1BqaSVhA2LxC9FXh8xlYclwHgweVkA98obGvKfW4iMNKJza4tQ5A1QDFPDwcsF1biEPK0svQmSnHNvjhOBM_hRoZK1YD_RXmIYPWzJnULt_2Nq4Fus7QlP0m4I7qSxDSUe3Ly_RtLefBaV3G7dUa62RQJfXVKgbGQTy_64COJ89TVWD5LIEPW_LRrYvSjVlsMD7LPexlQnh6J4g3zq0uRHxcWa1bDQDUQYrQp4Ud_6qc7d7FoQqYbQgib1M_MIbRyJezKZJFNXN8aZWzAkSjR6Luk43uWgogzv_PLON19AnvbC-eLg3fE4aUvJAueCiTQGGFkBb1O2IW1kc4i8wN_II3s1TkjQ6KSvre1kN4YMOTk73lEcC6L3NcgOd-o0tPDO2O9E6I8FG4yCWmnFPjPO1FFmEnjAUSgwhEs4KdKbQwRphNPnZQ6dWsjKPVM5AfmEiLx8drX7C2NFidylmW1dpC6T9L7Qcvd2YbocFGnNv3j4ztPjt-9Z2Y4fZq-02HVNkkuOO5AB4TdPTftjgiGipnbMaBmgBNMwbxkzHuWZ-avaQfSifAvfuePdugEVjmjhcS0NQuh0_hZ-K8m0-41A-EqQ6kzgfYTwKuQ8JdIWawuYoM1Q0G1bJGpwQxG9DPDB8c6y-WupSOZ8c5l2pWsRVw7UJ47hHhFIsoDHFHVDBT9N85Y2SIRbttX2pcnKj3nw7aj6ZcTRwpNPN-Qvu8YMMjMUVV0QoIn1CEyhim0x7jqidBvcSHLamlTSqYvzDfI4l9fSA8m4Yar_VZSMYMxls278D2sxVIEjXt-fqUbXc397qGzvNniARzqZcqrataPpzQoOM-bNj5LEJJdYPqSsHioJGOkhFzWXu49UuMFYUvyNxOhrbUy8h1N6GKiGDMSwe9k9wN-5WhvfEf3wPAztWl5R4PFRf306CPhL-FW83zhBr4c1UxU56taoVNnJtsblxuTTDJr8HgIiS0bqCLpL1s-ZYOgARzAgymuZCRdaxTmK4fdFhlTs6coahCbrSXO9Iehq58t6uw55hGhAqMjVvaRn2TpgwtHS2jvGMCsLFBYnkVXeeCDwA8uIEvujo_WcIUiT7STSP1IHMyllhlhU9tb0sD8wadR8caAgHBe2CuuE6YeO4qet9JIzOLTd3kJRE9Ev7aChlmuuAElJ0o-ktfVIvUbwVAwiWV3X6AcMlmVR_6HzhwZvc64Phapf84hPMYXvnIxBSI5UbvA0X5nHU2lnqPeRlhQI0mKXvLk4Z60WTgGrJoz6mjUQNep_zG1WTSkLwk4zlLwupc492MMc-M3x-vYQBmA0J2OfXEZjnuqAQ6az1hF9SaaF87c_W-Dkd5wgzUEkoUA2kjAfLtSItyltjCzxTnH5gGs7KaeoN_9V3bj_EAquWTrF9Vdr0DyN3fVdwrjU7oZhp_CVfondyy_VQO2wtxzBICKDcgraDmcBS1Pw_VPEIXvNm0ia52zwDDo6h53kRiKECACeOLLwif-WO5IBh4DZ_DFsiuaX1dJyUUO_7vk56KjmN0QEHxaNwpvKMuPtRGOMWkRAwIKezgkGJ-GRLXbeAA_1qqT0hLDsqJUal65fXdZ_J-qEnJH9xThlPem3WrWpAYKXeVOLOCxuA-7wxyxO2DxHqJdxsvzd16aErXTcIq7OgGXL14QQXLcpQIKermnxygZf06I83xy3pkfwEY07BVX6MnouU0ybMlqeFQgsWFnP_yjPuYGA0RQGOqsL_Cz_aq94VrHtzL1M8NTQt3Jhpr_L908QQMXN7kK6CKJnDkh9Rzykak8Lig_xmz8E42bPY-RWpAgAvpju1nggo6H4oH41IfQYW2gVzTviJq9EC1rP3FtJouq9gmSH5xDo5IW09XFskxJatkvOUIjgtZhCNG_VxtML1VdSDLZSrYjMT46SO8JjWJcn__4tR6gEmTrzRE2OSjbLuZpOksXgFrOgRDsZuPSeBAE8VKVpLtHvRQKWimJumFONfHJ7JxCOaUSBzpvk88Wg9em4x7YAd_SAChQoT7XRtjlwkRszQ-TwYfGsyOOGiTyG9dzCGGy_fsTugpowfedGCGBHJpuApn7cf5NNyLsafquuDtEyUly0NDpCwF2i4Dhma5jQsDEbKOlHnq8uzAkJXRe96IQBj0FWieRJyLU-pNsgXz2PqRxNXs__iId_f1X7avOZHN7FyBa-vE-u8RuYGXuLsUtQnnA0eYesQ0hCvGHa71I5E3-w1DCu9dLeY725SC1yVZ_vJ2WJmwEPXJIXKhVgTfvw8GIEml1VGxRFvb5kMQtGbXChL1tz7Y35ux-SRoX4A23pTZVEVquaXb2QjNFOprmA0tuFeYlsUdqD82ls4R1WzgzLVRRF4Z1Jh9AFgfYHqV-7UHwJAY0OpYK9iu6PPknBPAxWsxnLxyIxQ_rRnrbD-AyW-uFhBZ5d38zkvKw68Fr24Czq84U_OlBAvHtTWSzQa_6pc6tu5KT43QDCeWwiyWt1gdahuyoqGpJNgqyD6gh5xjSr1U-ahTJpXgVjnbNBkfOWecj9GK6CMLgvcI21qVrX2IHwG9kMyQgNmu--z0VHXt0WUtEuUcHMM4PzFM5AOZ_oxSVtIbvoYGDXjUgEI-xM7BOr4e1B4n8X0aoorefQhCLe1-Lv2pKRSeUlX60RlVuRN9GkoD_UoFqz59zJwL3h2uakwjt7iehx7DeI2pHUthZL03BqsYtJth9Emw5gsDKfBIR9BAjIzbSFRnnC_pthG2E1WMRMeeKThVkL_JYkmFj4Cr1xjqXXCTAI9QFwcTqRI4ZkRgem_jqVB7H9-BzVDrqgbQoxuWhNRn3_w-xfyzv_JtRcP150_7bEN2-gbBJCexcaF-0PbkopUuQqUjE3-WYKc9X9vLWcdkEehB0F7eqzdIWqRPTsnEat4SQhSvbaOp7EgY6Ypkvjkheer3fkPelAHN86SGviWWtaxDTWMBwHQjM866tuDKWOEnLQhMb_IjQDFKHrUKUnz42saPlPWfvbas8_Ymk7bX-E263Wzb5_MWXqPHMt6UTMSOtw86MTE46YEW9Ww-WW10cmatGb4jfoQHXa_JxCRry14AjwF7CmmQLP6dnm8r4_jm8AylHV8iKCG6r6csAhY1jQ3I-24iLu01EDB6H-_bIX3uiZDXpf4T1aGBJh7I7INB-Ad7d_IV7At-qaorPyE1xvTWeFVQLymsE87ZHY0J157ggITtT95e_Q8_SEiFYg0vxg89qBpuXygL2M_Pbrb5eYTCA6K6N86CxlOvFAb2AJnhAmxe8c_KHIsFZPL6lReDGQmMPBuvdCjjLPV7seEZX30ZMTuHYXNuD7IytEJ7X1o0_04eCmcqbivHBCoQGOzDhQ86DSoX2Omx-hmQl3hI2KgKnGcnfym2Ukd-3CmHAyCDAv2kDHm38H-JdcsO2DNk9QsYtAln6XRVl5kFDnWEhm9bRh-fg9Lmt_mNkwHSwZ0YrdYhAOCMkNlukUp0EYKKhBSY8lsY7a_TPbt8vkTMSCmi2sPr7NnuyaxMvw6Jblb9OD885lSOUp3oPpoH8QPkkhYUJ4-HVmmMGD8orSe0L3k7lLbyHzz5l1EmMahHWCCbnoMGGfO2QnxV4v9YcsMmIA_NX_1CjMUh_LYKrVWE2tfmhj7Zdprbop3nTylHV6YNet5h2MVUtpfj3CFTz-7V0AxKhqmTkSE9fMv5_XY9-QxFKf9B785SPTdj1xBiOsQ0uz3TJ2CPFHOtikiqYkNu9w2cUgYejqlM0crBDpQCuFmFJCFNKrfMa7eue_4H3RSh8Yu9Yw1LXbkAuGoFMGYhegcBEvcxcDSHfZ9f1HFT7IgimpuFuoGHwaNhPnlNc1uI1ILsFeRrrXide0q3L78aMAdu7eFfSSXHm-RcZypE9LHU8caoGqd0cr8hMAFvmAacrXiUE6RtzQUZjswSOziVVwlqyszgPXIuDsA4m0AcaLyEYQ8fEsRZAg7RyRbTgMGrlo-_L1Me2JMPPbiuNi2EtBXz_85Ylbaz45KQ45mdka24ouxzs3YK5aPi-Bv-fYL7FhoIWM6AiJH5ETjucj9KrhL5u-mnEi7sYh6ttj6I-MtSpCzOLrIB5HZ-tJktRhN78f2m8h6N4FBL9ooQXR4Y-QC1MG4eRlAiugn97K-r3MDGQZR5fVwC8SPW4Pt6UDvfaxXZek0HmjYPEk63MIxeMBOLaipBGR2ziR6YsoTUZ3NOopXjZr-UsGukdLw0OIJsxA-nGjmOZCr6iDgY-EfaCAVwAOxAv47u05VBTOP1xoUhMrxNefZ1lt8hEziCDaHInMkDdc4lQVeYv6H4rR2KugX0IXGsFc-C8sfQVnALLdQNjEg8_AfTsEmY3NqE_ECIUhFwxaW8s8aWBgX97Pi8SxkCwX6DyksH9fjA76rP4P5kpWl7ynaOaCfytRliE4j5uDXXywFfwN64DWKIQt4u2gDGo9d12CWUMGrWZZdn3qn8IgEDmUdr_CGXIGcPNuS-wxWoh4G8eGNhvMk1V9zhyhcxgbjoIJLl1T9MOZZ8JQVpiy-cPgClLI2jgIbKSVZTTZ8B6T93aQj5oEbOw87RZxArjYP2XeIHMNh6JUUOND97h1D-tXlI6hlFtFTouMxLzyOpVJLfdrUcr2p0bkbNPAyk3qzxwdRWegSWH2nojJVRP5dopYDUvX3a6sXVGUefUr6llKEtyQ9W84oVESDWyhWRv6GiBkpimAlkoolaGYFYCD72gUISM-ptvaWmVvNmXdZhR2JCSn3Ec5K9TZMg0ArIgFvnJeksow6nIwDSYZ_EXqtEgn9hjLaOcKZSrixLgvGqWY5phJcyYWP7kBsJTxc9U7xCIDh_RCU8fjZzAOAl4r3DtGTEntqzqhScZ_-Fx4ygPgpi4Ko84FM0RvNQGw5VSrOWADroETQVP-La2KyDOjYo4dTauA5ArmYnXyLatcyfbnvgE5KofVhMHwPq-QSV7QAaN9aM3KdDRxBXV7YtnjPx5DzLQE_61NLQkdC0iWFjHwLwM58comkNfrKAUw3vtLzWDiLHT1nPG0pxYBn0zAid0cdOFJ3JRJl2F6-GuMSeUK6kCqbX4mtShWXp1gn0YErlKR2PFjCDNj1o56a5ejMOYAB_SNIjRLO_O7uGofXv_Om9Uevp9XKu3ca86Qt6uOpwQsifkwS6j78cGRTJeU0SlIAGBjzi6b4aJN--CpFIqF6JpuZAxhiLzsHAXRAKik3Lu6Pmb_24KBL5_ktbQRcQX6GQjGi0A4gccSOF3hdJ9j1any3RaFOA1_0HRAv-ExWoiQEyUnWALcqaC1FmXgDTxYx_VUMjeb-MqxAV4eHjJsR7e1q9cJS8qhubSQbHMH72GccTJKlZYdLBHmc0Oqejf-JKgaBMxgkGX30uCXhT9B8dag8jVrDBemQV-wak7QHgbAveaWX74ZsZZF6ZuZ6YU1llAllJlLWPVNr4aaPj_wMfurz6YyOJDnCcVxcKFjBCJRuTBF1ACh9Ye1aj5wDUVwjeKXnjEy-quQNoB5c4clujc-G-ep6-EHj6WgHZefu1HYolZNprU9zHY3T_OrisT2jDBUByHv2RajGe3K7nDZprR-e1SPApINTcKQ42Fh8SfDQsXg0qOfvMdKbfKJqQizEQiCtvkQu1oXhlO8fC4J5UkN3qsPcdG_h1TQ-_zlAPDJ97B_92zV5NkIF3XFM2iQht1oWwZdN6xwKeDRqKmpER-qz7bxiy9Hh1IxU5T_Ac5c8B5xIxbQzgTJal2t1M-_cRvGT0CjpEBjRxqts-KliiGxFl48wNePKySRiGEfnn4Xfqmy4enbmmZgyHCmo-h--qxLIxBEykrcQurpumcrK29z2_jGUNichMpAaaT3UlzgVTbOVb3gVN3Qsu8ltR1RtlO5DM_Sc6q3GQ2QpdHafa2S8Z5D_A90PuohDCpyqvS7tA24KNQEKYM2W_ONMBNNEoyU2p7hZezbbj5T_HLHVRPUiVLgugGFQkNwZ5cRgrgYqstoKu9VJWFE-odBF8G9GwHGFFqyCdBL2CADSx9AnfEssP0TSarXyn-ALo1n5f6vpUFmkcuY-4gFSang5orkODd3k7hSmsCxs5NVMLfQxPtjJcTTrKR04H7xAVNnt79YJYVW73UaXEUammc_qu0GAuNwgeaX3wIQv8ieBeqJvGbfOoXd-U6c8b2xS7b_9BCWtTKZ1A8azUrXAqOr5rXlKkq6I31ht1XzyQAWq3_YWEc8MJahqr7bR5GQqOxRg_adTocY65i1qhxebStP6XWRRurHWyHzDhi9duKfGK_eC1bbuUIevXsNDHdQBDNE8_w1BBBlg4eFuM8vSDZWJEKPxvB4Vl7ciLOs6-diW3bj_JDo1BZlpdDQFKCwDuk5RtRJmr9hGUaIbF6nrjbFduzQFh6laU7VkD_3XyqJ2C3dCD1vOOhslfiVG1fBWHpTJvKsgfLa0u94IUipo6YWCz8K-LCeOymEufdrfaI1A5qutL6tF0CaPl48rmLRMayxqTf4ZGCCDe49C74wOS_kGmxchhr8DKGUgKwiWJWQjIQLIk2PzaHSQ4cE8uBQebBsCMzlrzNr1YhYzvzhje-qorpNcwCluQeaXkqp1WST9LbExS1jN8gmJhLgS8yAOd_yGdJchugXdbfPXWD_R4oVf40bCAv3HBB3MxQKq8dZeXg_9xqr_bhwqY1oUraAHLEol6kUS--0eDJ9PzaLed1ZQ_6j-pHR-mu-OkQUvtM-THVLuNMKWGSYKcBnOFYw_1NpEkwoWtcYCzk-nq-aHJ5XnijDKutRPJQ5W6RLMmhB8qFoZpRp_aDS5LJiqp-Q4g2QhtSCckgUwHN5GSDTLaYvjkR5jeIDI0Df_tQZQv7BiusW4M-iXMunM3qpOcdAdfnBTmODqjdeBAk4dRnayZtb2Ib-JKl5ywa6WUDhpA_UQA_sIlBBbTjetvlH2sChS0D17boDPANxqPYQLorzUflL42ay1DQFsRRdnxTiNvzN3nMOxzFdIUYqWEiY29KQmAFyuERLmtWNxvUB7KB9WqxV21mbJ-yIhTsuUTHve3HdcJuWPzEtbZemmvTyJr1wckTGBWVfeT20e24dPMpBbRN24Mpx_tMxfsioxNsXFYqKHzqWqZ8Tp-gj0TUMr-dATGUJHHQ2Un1nVUYhOfB-G-cycBf8zmgcnA9EsKkTOlZY1LRmvBIknw6thweHCggBJ8Ke5N7lgYjdTTPs9HXMZk-YcGJ8Q-TkB4_Dw35xq9_hnncS-Dl-_aTs3FD-V3fAbAd9eYbttpwk9kwVnc3GzF_d-eoCntwtxNH_iYmdeBZIqLZAoDwzvFnGfVunFP4RiUtLYepxu1m7HLhPSCAQn6SNcLwGg1U0jQpfYIYGZTL3Ntq91XYv3J9vy5O1apgQZic9XEMxzOuoYf0zDEU41PaVOmGv-H-mdrmH-MI0AquibmsDkD1GoUssNDqsqGVBgMMp1kc3N6irmLeIpdrSjOLUsW8eq0YGWoMXXxp32wIfDr1fad4KV22Slqlrfv4RC2v15WxVI6j8Cn2l6ymNxCj95fk55ibBk8IgObZEwbu-O4F6focQnbqXcLMSHipxWVOo0PNAnxeG8ER8AuVaimP1nXVWhNo77VuX_Yat85m9l4Avt0Q8tR6Rpqruw0cxZRH-3GRk97-svz5QsXMJgNZsDquzmeRT7ydwFrr8NK2Ei9NmlZ4pziY4xgIjVIJgIhgkY2wEH9EBDPLuqmYrA9z2RC4KUg5aMAvhRRZ1Jrxd4uv6C7iq9o9x6AOVwA3AzuM-A42325s1cNlnURin7VjQvoDg03eXsB-G-iSEUw_WoiFatKsO1U8bW4GP1-XwaZMD2w9-NXF9JCCGp2PaYNl79WZXpoNqtOv7CS-USx0vOF6DLllVZebsUhgMTBHg6I7dmJShzC1VLrCV_XjFCVlxfSdC-HkHceCUwQwQvkH7CzkW3Xxqn9onVcL1vMKgt-D7ov_952u8jsS6gkzEkUZgSFKNUMJGZv8J1rhg-ZNUi_50EsohJTlxy8H3xw8RFN9JsTZ7T7_O2yJ-yB5bCdSHldOwfQWtPvCw0df7yzUQtkMqMY384QRdKraWO3CwhrqD5_j-iqM1nw3AKDnqvUZ_pL_MrJT5OwqvaQLlIJpSymmfw642aXt7P1TzzFnwOYb0Myjc0geBp6JKLB4MetCiKUxmYP8M3hiH8FSZLv00jUmVJj-CPVj2IVml-IiAPyPU45_2W_Sek_l6JDqxgviPNU2QfLqXLOgs7-30-8ZhrtlZLC1AYco0hIEyVvFBQC5CjorAuillJuZ02YU5_kNwGG-Avbqb2zLhjw3gO7ZB1Lz68cv8F5YVsUvCvMgRhgpr5Wj_5uFtw23HGXHKY2Ejm3Kjya_Tw1EbrPl7t-UYyUxZkF6lUh-ZnndeOB7RWVO9lDvW-kuu5XuYFbAM6ouYOPd0Am1Te__qnJe0cYwKBaqopwTCE_7cu9EH37OBm3YWyGrthggmOrcK9jSI-xA40URX30vYvyuvNzZ-0f8PrZIfTtss2f0w9om6vDpwxsWhXRlTyz9qc0ntEgVwX6t6xWklLasPIwXZpahtO8PAA9Vqy2D3t-nMSyeBaPMhkZi_k5x3ckiLR9RHH1OmiAyYkGafn1_aB381MKMv_8AS4YGzeAvaHBwwfNDBlPpBhdupAGXoGPKFCM6d5W1QoDhwQyIZ9uFKuvoPtxntY8MwG5x-Vwmg3GhIDiSmoybRNIpfIqXUVzg5_a9p9b0-Go59h9B1ntMB0K1Q0X1EtZq-tVRlv1MRpSjOl8LFyGFQ8rYS0aY54cZgE_tdOaozg5NuXDJPQR515WrBf6NyJ2E66D3u1Fde7hd-zUMSiASQXMKwCLOAMNn4f3MWoj6UR3vKPjtBNwF1umNrE8P1tErywv40kYGz8-Zy5Jub9dMgKEfXbz1s6XIqZJEDSXngwVYNQx2fhaO-uGxt-eahjkVAkt1KoTe3sDxtkX7CFQNAaVBlsy4JEqRM1-Mxg0GfAP6M5l6MMhbqkJoN4oC4TVUlASghOUHqkCorULtgKctw01Ea9UnPzXz-KKpA4RllrWdUryiRH2A5RPs3KH6mTKVjJmzXvs-tHHeQphSLLm3QV1smoj9Z-oAJrz0C-f_Y0LE4Rsaw8Ag_7G9OOrBOD1odrNT2PbpvyeMCv2179maxKeUB3WRIU_Mz8b4_vi76gODzX6t-K5zDm1ukMlpNLfRtD2FZOEu2S9dGFFy-Ut3gB8Vnu_b1wnzETDDqWZJ-6bo9qRxrRAkH6q3TF5VTKv_hnYKY6QzcmotJrdTNPQvwCztcqj4c45FtJyax2tdOQo4lhoqDapMA9TawQMxunVToG8YmNP1YKJljFq-ZFttAxcnIpaTYq9scd3cfS0S63cnjaMT_H_LEBW9FedIR53Ko12fyQn9cLgErigUWMWwgdTmE2rPo3ygRky06cEcrh6zUtNb5E0Xt8FnmR0n53wZbJHsX9N6ficGSVwanB9ZBGJz5TmRHdF2aE6NrALFCVLZ_9mUP0XVz9HSUH9YbauXqYM8afLJ_R8XNm1WtqX6gWkCG4HulNtWURyTWgVuQT4jiB392QSDulnwnUnaFiroMxbHD6UENVgg78icspfeRQ3I_wEKLpCmngQSDvgNlV-vzVct_920i-n6DSDav6Ez6MgxCa0cgrF5Fbzak-koA7olgU2xqiyoAFv02H76alrTcE6Ooi0zNIBABz8McKSqmJDhJ3RTpCYQCmJ71Xq3xdeT-9-WBX9QgNEGQ9BAcZNT8IHY7yUocfYNOQS3XbCogSc0HR260BC8-8ijyyx1RfZB2kErTGpUCo3FQJLg8QNYU4cThUe1rmgzC1aJSHdYD8OLKHflJCHZiGGaYW_MA-tBWfHiEISIUcIghjbVjF2dBoMZBW5hlzvYWOV5y1QXW0zvTJ1Tw4R6kJGWNTK4wePkrh9W3t4wMu2QvyJQLGGwb4ltSDWefD44MtkWdfquG7OTbXqEiPr2KreJ2j3DASXuBDBD25RvlZc4bhLHFj9BUJ-lulsAvDWKCb2Bou0i6akOancevmmSZUwphs-hQM2b3ugNTsgsUEoF82dXWCJ70gyr1RFBfBsZCYDMDWbiqMYC221y5Pw2zoHRdQ40xDVCmTzDZZxzBr3ywIcE0Y_6c9tlm4e6EgOkdHg5KaAV9sV_uMLbBeSxyihQgJuxA4dzQnCo3Q_owAGtnkvhQp4UgYlx2AeclHenpTuFb_t-BsO1-DV6LgRplzfXH7ocQedgUXsd-gZtA61tnwNR2qRk9dbmtOikjI7qf7tFv8r0pRbe_d_mNadmgformlLzAtUn87xkZLmcMx_iH0g7gW7gbEXnkKmX9syage0xeQ12qnGvGF-p6mBKFUM7d_8ZBFt3pSd0M2Wl1zLnK9HQJVPXjWWBf8r9UecYdpyhtZAnxREWSqG1APYDP8cPpQcewy_QaCnVqyYZRFkf6X6ch-O9sJAwzR4MLElaZ31KyCxHTj8565hGC5bJUdg_I91UgH2yJArG54y_Yc5Dl6ALUn9QgPzbqDFFUOJjwU5o9uD2XyEBYzEErekT-GqxtSGOgCFSStNay_o8OmjolNWZVRc1_aFeMUOgh_GJCAnBMs8AVNU8rG-2bL8Yn_08Lfn-QpqpZIZIVsTZinG9cCIy-nuGGUtwHtPdG8xntWD7d5rNUtro9BCoxdrnbFOkSAwCQ365HHDHG-D0bnxTd70UQLYZcAb6rkxFrENHGBQFl5f1sOWZnGhofb6snJCirTWsgJcst54Dzu14XaX-57i-J3gi6pI0alrVQhxukhTtV3oj42A2TUGD6Qb2P_PjwhVbwpyfkd9tNTRT4YKbB6v7FviTl7JKRh_lMFAeLiNc10auLFBnXOdq28pbt64ilr05QoEABo-2qj0w1qRgK1RfdC_x2WRHcrI7zWIyDONsyqumIklidGqrEh8EXCSg3a1PBLMIrUfkfyV8C7LvTL_lifHl18bZO1BJtoksrMcCmPiwEJhCCMn1olm_DSh1YHahgEFrP9PhmLrFpJrymDuzXlWENX0QfqD8_bsiaIC7sqi4ZCnGI-KCnePmdiATIkO1ROI0ty_1kRce2LFztuwYFLY_z1yJlFflviLtyjU2z3F8Dl5JjO2dWm4n7bBCRT8wAqp5eztDZdaiuQUZKi9vhIuEnqFpL5zQVTUlDpMWodeYlcEZT0pQQamulicCkRslA7Z-CThZgOW3QWCv3eYTvOlZ0merHzQFxYq-8S_0rfwK9BEA1xck28GdMIXUd5cqBN1kUPd06qbwbCAgVBABucXvWbmkCeokCXOyfxb2BHl7381ZWy3_U6M0AnKzxhtYBSmBjY8sQAeJg1WTQ0ZpbMT651_b8ipPHAUl57j9rwVzxrdtmtai0VoUVNv4UEF6gDR_byb09xWMXgCWHrBMbbs7KNNC307cI7lmSHDwFDiWjxXcZtGMCix71kfh6uZsRBursMcnUoIaGvd_Pqv7SKeo3c1DXs8d4yraU5VqtmvHuodSmfcmOCEkzLb4lmVfBZPrsJQcLb9xFH8wunqxWYhr2ERzOJDZoLIKNwQnPDcxoK7UX_tLfbHKAO_CcfHWRgB_NkcPVvf8jViQRTrskD_19WqQFq241yN8yW4a61C6v-9og8yJyy8BWPQdiKESA180YGsfujYRx40jXR1u0g-WgRF35S97vOzm963EAkAmfCPBpRckAFxeDcb9DfBvhihOeaQEobt9UNhiDTNaiSN_Hl66wA5DIPIptw0_HQQLoVQ6HUevZymcwe9A5p7_AdCf86KBN-Z6cu7-5OTmctbwROcfjMYjlJLXI4vSE1fY_BdaYPBvPWsGaPKTNr9kwy0RyDrYd4a3hzDBzEOAGUJm14pdaOSbjtwoIJ0m5TeQRm-e-EBqxv4dcABhod1agzhWgyKZarIrtkDhGW7dkDqSdxHzPCxphtD1a7SD2MdKfz0IK_IkPRSr5N690e9kBMO8r0MmuMg85Jf4vA3w3-ywnIbaW865qXxkW-3CYgJ8RloGuBcJewQH13Ozoz1FAlt1Gt5Q-uHiMokLpmbCmvGVk7xPXqDu_sqRhQSjlEXRBjmGzeotBxxhTwmzqZfJxRXEdmGAtrfqva6gzYGgSdXFWo-_wfN2-DjBa1Z8FAxpmT-dRPNvaKwOmknS-tI5xi2i7kzmh-oIn8n-AJ6WanEBaFc5vTC9SnQNxnjnnbTu-bRMj_KlXXpw-ryvlGEGhdMOqfcgSWzQLPBSVMJpDU9rSZMfGl77Q-S3q9mRfjPnd6TqlNfOskpiQijqlKNvhC_D2S8SerwBOrWTSZ2i0W2NKgtAvkgn1v7wHkNIp6iJ9CU0mXIobg1uDrdvReirxIxuznqXyf9xma99oqKmQvh4dWfhlQH-a8AB1Hl624CTjEs4CcoZfCm2pMpcDie4gVvQiGkHQosnTdOA12IX3REq8peIyawJpoyI50ConQxCFuWqKfZkxvaLMfVAHcpvRNrNEF-jD1lf6R1emRB8jW6iQLCKYVueF6qfUsmb6Ql-gmKcakkB71QGMSGTa91eBg--S11MB79NFQdZhQDpYYc5GAAKTR3PF9Cj-xk_33qn0Xz3Xw5jRTZqm-qVcqPMwcdxcB9p8JhtWuhGcfyGmON9hM83JHg8xKGUn-1qPOnvF1yWoRcI6wv7Xe3jfo-_RHLEwbPTbihfw2H6ycYxEl_iz9zlG40_WNJwwWDdHn-jsau08fNxdR4WC9FEvC7lRAUeQPVxUWE3ziJjlDMeZGz2jy4daSi-LY-QZCzarHtQ4_olBcW11Q8gtV0lOBrkATxbd7YRAL7_dh54Xw9T6X0O7TlpofzzAVMZzIn0iTai8k0eAzuj3DT2FiCHAh4-RbKHr7mzyrPQ0MUmJp2PomCnzG25BUbYSlClBcjtotLGm6YuDPzB5X7Lu_vH9eRjxMEh7ZqIYO6m81D0dwZO9aVZSSwa_LBb1iBFrHijTsL8rHXXcBSnp_jIaZrGLyKkxMaJDegmLd8HdgACP3rOqVCDg1n_CVE3_jRaqwwHJVpani_j77aSGBmItjp7HqbcgZr_CVMCBHX3XfzlhuXZkvBoc8ZaYYifhvgGFGEg0jHEaxIIU0QDqm2L6dHqCH6yAlkkT8zRgWeLH4Pey8nR2KTAZP55YtaaU38cUPOqVlvTmPihzfNHH18h0vLfaPPjA712C9V3hvVACSpU5SsXQU7NfnnIO7_5ZcX-iCaEuDsSFlJcAJFaSyKJh5kcXsGdRCAM5nVfyH6_NFHzGiNWaIqc-E3Yl4a4pS07bpe74bsEUrxUfdgmY9XULfNwuGPVg4qBsSoS8coVBn5SxwVR6OITKjr8Iq6b8EZZxxc6qJJe2Xd5mExe6NxAW3sClorNhS_wwcBYwj6HUH8SmXpZ0xqADYVqky8bn-pa5j6RFNSH5zz9deI4_1ioLhkVtvpbRFHOxCPzm56wjqQnEci9QQd8axmpiKgHP8HnpTzLHO2MgqjjunSox4sXOz_BEEPWghInV_VpmFb0KN0B4UH_M0f9Yar4O1unjCGwlLF_ZfLfNfwmi8JoDRMYIyFn6D1PxQgdBBPKN0oC_Z11E28WQqTORvTJqusVY4qoZ4d1FOkd5E9srOWuvs0gBGweaIzUAZHdRGr4NygezGmf27uWSos68ZHaB2qOc79z_TpsXiVeik5uT-pSbt2R-GEIeg8cwCH1J2u7UHsWLmJFyUmBW3K372QeHxoW8UKinTNg4Zy6uF5acVZmom5E8s957-83Qcs_unrHFoUTPy_KWoiqRefrQcpmCHra-JYSYwNxfwgzoCp-EHgl2ypCIZ5BpRQHgKweWJWeRhioSBwGejT7evYEl3-L_FazZFY5W6tKyXFktO2jIySP0NMGxFL8S-PWQERH9cdm7l1KN849iSIqeMI8cROEUCWjUIhdh9pXJnY8vYhQBfbEjJ2fJFjOEtT8ARZe1jBPNUFdoRph8YXVXRkHn0uw826uIzZGnacbNgRwgNdilq-j1Rj5iirOQwXSQ1s_L2Y2Gl8O7YZ_tuEek0ovZnebzesmYKtoY_XhunbD_U-4afK57BtBTsmm1Ed_AwfhZNV_vqKC5DraEE6c6J_7d1f3NJEMVK-QDm-iMLGdLHjOr3bf8TjpeXNjITXiBZ0kJBb_qf7Y6Sze1UueGWd_23NVi5Ufe8w--C9fE3YT0Hl0wnSRJ1WvOGlLQf2Hgk8KaazMuCVbkNFzjojCQ_IrmsEz2sbWOSMDB_E2y-6JJyET54mCpfMYhdHXVhtbAH0sdBNtp2KGfh9206nOJU-lKwjo71lgNm4XoWV5Ux1LXYSeN9r7BSrpirkFIqxyQkJez9Ulcbiz5ES5t8oaTwCOnIDE28Vy324HhGPSi5W2QPkCOV_PjOWCeM8yjS_6w_FnGuO_26ecaOEkCNBZung5p0pHSmD9D0SeQ55YvwYvwMhT3smiwDo9dRcFa6sigkWHHKtBLW29sYLB4r5pNWtHd6CihJCcG9DTTbaE5qP0-eOF1l4GKEhtIUKDPGJGwEzYHjq9emeIy1uacdIcWTCJylvCVOHdWmLaD1HefI1tjSyga1LuX-uZPAYEu4H3BHd_8RhEhTIIR2W1Zi4pcy___Mg6UnxiELbieUU9M-kBKnEG8wm1_VCAJVg6GulXQG20z5Zq0Zr8HsRUEpcO6ULm-_3zF1WYWSPU-JDi_ZiKxGdLOidzU4gb-zzrrLYtA2USFwdncVimCESLHhKPSvv6r2xX5Hz0eTuLmhshN4wL2du7QNz_mLVnI0aIGrHWQgs_DEy06L1P4ANm_Y-0xdzookmfICUGKChRsnNFH5Ardfg5JWwzC_jQrW1XM_t8g-3Hnv_A-UzUyJWBl3ezae1NPikowsbMsIwLuHHteDmQmqb9-93yiUdXB9FxycWFgaPksF17KxTvI8FS2PPwZKsSOTXMQNCQyFd4fJDR60nQhm19DhQImTl_QPvqibTAg_p5zlhxlEFdMKoMEdSrqovWF0mKoOLbIHlGum-tDlq2Ll96PE2-CrnW8NyHVDdew8iZSZ5dahyl3prZnh_EiRB8nNBESy8uH9ppuSH6XlQ0TJXdhwI1ZdOJvFonZ-7IBR1TVb4ynvpzRt-oWE-tNx1-6qwSJGzrsKnn1EYkDQaRj7nfztiOa9af0LGUR5ejBaZVx-bQ-75PO-xBTxd0UpI5kyaEf9T3rUM19GzASEzvIwPCPRplhpopMmPORqBqg1oFxqI9vzahfzntnYmWEBLGc2ks1NZWq1gLcSZLw947_EEGgyqw51cFGXLaB1DeA85qa6WT1jRmS4Fjj747XLPynyNH73NU8RWsx03F0y_fvUpPGS_vaXWR8AhEy-gdBW5CCYbsPv7WB1Ls0_DJMBSHylHgNQvC_5knHobolZyERyyye0rwmLca0TnAJS0QhgywEwaoateT_H3_aqypXAFQdqP9aXzDLINETQH-jPND97CG-mhA5bh_mmulEvQMxHyt1e4d2IWPOJjYUvSj1gaxoNl8C_v-h8719rmYl7e5jedHHzYQuDgq-i4B8HlQxgLycD2vQqtt9F8fadudBvjaa4qaHQNw_AZc_8aWNUQ23FdSfC2ZSwJvYASGSz5iwwZotTwF92WMyzfnNvdjFyluEZR4D2RXnYP9GUuwGcg6LvtzjZDq4GoOG8cZEqgSQpSUFWN4-NUVBrb8GLY-SDo08tW7Q42PvN8h6h6cPCpFgrKFrqEuNupBiw_GvD-Ihj6S81070U74EpW3yin5jY5dVGJO_Q-8GBVsyfe9VyPGlDCt9p2-FwvgP6aMZnWAQys5HjDo7QxHaLXAUAJEB4HJatbd3sDYsC3S3Py-_NDzA9_JuOI4iqvOjwf96mS8xfOkoDY0CyKso6cn7BWBDbtgGL5yjjAOrsgyRzALWaUehhq0p48D45hMtJh40lBfgA2QkEqXaqlFdooXKlfyn0nePdsQPYJWxg4O42Up_ha9yeggy_bdTtWJQlR1bpgphhsDFFhPq3rrrD54e-AmMPvLS_KnhRHR22d8t80bo2yhrXzT612iv6Z_2_wxWbm8AnUB1L4t1pnI0BW9MLhU0EC55f52wZCJQ8wJdRcH4lbuUsZ4ioBA8J6X-UtP7YjjBTeXITfvyCaLvkwGseuU4DCiTHh6mkqIq6ynzsg9kXqjCB7oDfO8yZm82JEuzLWaReeZSub0J4FAyCUQImgs3Ui1shcwK6IVbk57-Gjywva17R7qQhkYxqeDCbrd64y3QLFBnhiYSN4TrR5AaPiNz3eCYFYPTdMjNCWa7HMb8wgI8Bix513uKuS7HenMc_h1QwCzrD146GKiiEZ0LT2IIDDO8h_gKx3Y-7N5B9Og7wjsDps624fXnr889NYznFOBwuVhNmT4aULq_L32VNXYO7bvGEm8T__RrBnigqlftf0nHzP2U7gN3kKnuCg0VryDRRs30No9mmIxpCzEkGfEDb3g8SxDiiyOjZEuFTG-doTdRDPfe8DqiPTfJdFWRfDkBKFbpnV46-Dy1PKe1HdpoF82ggBjtwT6N3GZ4MPq1UVYQ6aiwlk-vUpetZHohzn1AD15XlDE_NfnZHhvGrHGApPPUFCMmZRmqQTkNH4IEpUDQM4_SacoAIdkrgHO7PoUAFoHYMpumQ2pow4VTR3mj0tpvG-iIBbcxvqc5XLQQZhXuhDVAEl3p8HPTDKqFgxTxiKT_Ns2pfkp7zHS9-Qp6VzlZgoa1Kt-ipc-BOpwBzzeDqg5bOYvDF4mySuTfNy7RnMfX2F0WZKN0j0Rbo99iNUgkvxQNTAsicaZGuGWaUbgiQI5OT_kltLhbL0Lwk4AQpgKHQ0OBgIYC7ONSWNWlHqRTR0CGRYRPPB5tOfzJ9iVeKQKgTnH-PTukqdsxJyrwalRgF9I_b3qBXCFeY7Ea1JyqYhi2c1OLLoI8UJ1kNsH9Jsuww0WjthK7U5KQEHkQTZSjdEyoD3M-daQhocYGcPqRLqt_kfDWpA9fQYJVlMCUL9aQuMdYVz0ZzZwV4PhAoqep2MwxErhdjEUPhqyt4mVopZW-Zyigqpw7ef5K8lrBvtfLV3rt0hFTzuxACp1wQOWVsYvY36I0Yff9iHGHaOArfsR0KgDgbNK7E7D5CtFrHyOn5XGjWcdjLaYKvCJ8wKrIItOXpWEMxBCcKsKsj3bo_jJKiKYS5hVeaznfwc7pi0J21-4BAkb9Vs4XqIcooEFbUlqFSxWMuBokQAsxBEdeZ4ZEWbD_jZdx8NxELKLxPuKiYYmaljKyW4NqhyeGPgFxeHV7PC8fZ5O1Zg2sTMkW7J_BkZte3oGa9zeENRYMYmVp90gURGZ9vex7-GM362BBH-Uq9w9XYGL_yVfylRVU2PGoCEmMoxqgxsYTt6t--noIEO67jMxWhOdX-i2bLo4xdZnTBBDiiCwDLBM4SS5FWv9Q1b5NO8GL9ePjw0PEowJy6Lhq1MEBrQSR_AiNr7tAQPoJc-ltUMtBCn0FrDKT8UZchBVaMPazNXHJyJB__MZfJLc36Pr3xI3YG7C7plb4MOzJ2UU7knbHbcGM8WqKykYOBlde91ywezS-WEo8EUTO9rVUTDPwSPH2NjnuFnu9cEAmXYicqip9J5WLcnWxKuo51O53VaSXa3KOwkRsh86PPoxbN_6boEBx2b78eQOgVrE8T52OD8SryaCcj7GmHsA-nLWXhAZ98WTCCR_O3N3JZSMDB8NNKaTdyjILTThzcZBAMHpCZteh3JxXO2kiw9Q53cCVt-PNAVFwgANiyFFW00sGKI1VxK2SqsCXupmVQqzwJ_VN_KyQfh56xgMWxEucdcbneMoOWUzDZduKIBBhM3BiiaidHeflnpuDid8poBugQVdxNZdxxi27cdV7h0ieu0WAJj5G4DjNY5XI-S3cilYnTXUNg3nE4kQb6jVsjVPKwS7sur3AvwPld2qHJD5Zo5_63axnH-FQuiA2oF7pZxoYiz4IYY94ydG8gOOYteoiwEDD4tDi9_p-Vh19qsJ8NyAaC3sO1mKZUhLpGX4W5vXI9bONL6KfiZtpGsNOS0al73DiqdLiFtAcp68geOr3ym7Miq2xtthT-mCiNOn4HugT-rogZbzPlRK3aHEY3MsLL2BBcPue8ffnazWOosLQuThIGdGwHxSHwk9crZito6H3rfhy5FQYRZELbjkp6XwSzWqwGNh5PvS3a4WxLOImjdS_SdeFFztTbz643sos675Aodwntlo8e97352Zl54dJVBWQQQXZe92VNcHdywcaHzSA2NyLRWz9kJA4R4jHUBq0Kd_y-f_4LZMgcnSJyB_kxotskTdJvy8K4VSB7NSgMxkfzv-DWokMaWuZ6i9lhG6laXjt8SzVmZnBXx2fcGgveBZ0cEEy_ZAjwSaqkircbn6rIcmwjOLxsSvcyHHaB4371u2OZzhoM1eRQ6I_wXHJP2FW4zESJYPOhSWtJ6Apz4rHoUnlDCcg1MnT3Q6PvRNDq0jB26NCCl4ixvXlWtuWTa6_bXBARoDauSXsf9YAX-vnSTK2lOz0pOWgz_QjQw0Lx7nEi4sMXdnGvQNxkSiGAmExZzqAPZwMGbdAJUnjc0jW7Fi28MG3G8cHvO6fcGMo-IHUlH1hr7vMVCViYqjcZQOJ6YgAQNQNe6mXCcsSJij3_AeMXOJvC55N2l9GkRBkByX7-NO0zWRMGZdtYxe-25RMM46v4AZi3A2mH-31HphZ34kIlBH9yb-8Vw4cdUHpY42kEhnXusSk0gx_bGxqJRVVpVgo0EAAAkhSRkWSqJiccp5iZ1yZ2EpHOgEM1vthLyCualal7K-fTHBm5jSjNqNNiZ85xJF3tbnHSjLNdQ-sYcUnhDFedPfS1bzfVZrJBfzjp9_itNRPeJnHhYGe-K9d5TQqjrBAtwrGnMkGhpegfK6Ac2Nklvcl-yCdX0Fx_OYe6peI4slr4S9XmZBj3ZpG7PX4NdyAKDu0GwufKIcSATJlFk-1L17vj-b54H5iFj5472wPjh-E9NJ2UWS5GbEC8TPpqw5wQH_Q4KnOIE03lgzCcImIKW4jK52uCSsBljKI5CXQzgTj2lR2lf7OqqEwyuFP6KEm4Gbd98fASaqrgFmR3CBqJfFkaIeuluglEt6hbkIQU4KlhVJ1kwkOq23gcjyxC4TXYEBNake_62MYh17xz5yxky34x6cl8B-e14KXqOG5qG5ug3gsoD334ICr72xkt-m3mICgkUYOSBE83pb2AA7YuW5IqwTLStyt03wQhYmDXd_q4FBM7ZO-uwue_cT49vvpDHBAL7zwG9if6P_wwVVqO85qFfri0-S37JXpakkJ6_9SUpM18Yo4g2SbEoFLE_psEgmhRAVyGZjGMCU2Yb2Nh6eQaVhuiciWgij3Hf69IJYKZ7dgNmCuuTMp_VlJ0_bDWGlAQZUvZoXemSxVUvOEMjNj0JxhAnuo6Pi9eWLcpy018a71RUAcCrdI6NLvPBNr6qYJgZL2YE6lLe5kN2xxuxtNIm0PdkyvAo9N0OGwXOkQcY8KxwwhBPI01FGQ1ULM51ICIEBERqQD5-RkIAICNR6o8zZD-6Iqah6mvg2OOhpEWzyTuIV6y3d_hOKpYtdPZ0tYpmGdXjl0CM6UZmUyAxk43Frunx0UQg3pA_Awwu5YhXCPek64_gbjQve8bn5Dxl6ZAvBAk85VngWQNtjH4JNk2GABmghnZr2ZHWhO_GX-q3KKTyOqbUjACY1il-tUhIs0TkcQqrYLRMXRrSACeDKw1VWm6iTI_6IYfcUGs_H1Y0fgyCSI3lq3495MNy-dbp-G5WiAQCZI_mqzoxTcr0EifYsDKQuzpSs4e6e4beFerRgJmLVr9Jgo9heM988Va39i0Vo0AEIPlaZqLXrAz--eT1xxSdBi6JlxKS2uzYsl800ySl66rIKPUoXdkVni_F_20mmkwEGCAQ4ZJS1g52aDOSjCYPuP4nUfCCL1868DyocogHBIwr7PCQ4-_0e7rKflnzCoPtETbNRKJj55oRaiAlFdqaTWWSMp_LjH7w0GFXxzTtnuur3GA3QaeaCO9bIPf-kiFhBArunZ4iY6SdxqV2bu3ANgoc35zfPy7r4wZDnS2BfHFn6KXRHhns5yN5U-OVjT2pIBWbLxQj8J8TOrSGYkpcTwJ526XWPKA03qIn2pOEe4wUDkW0tkxyyIgt5cCjSPWhhQQLsYYKJ8rk2ojWvIHSdHSgIof0eVI51RGCW4jcg2pJ3I25sFIfpgqI5QipxB75eTIB32XCBtzWmK2E6dPAQfnHNPYITbjLmOrH2f6zbW1_LJ3LVtMMijseSomNhA0v4KUEBy5aOriMgwBRc2doCITBcWz0OD6TCXbcrNvW7g6BDK67Ym4Vpn6bl3B4tIH19TNQB4YhX4z2kAyhlOOlvwqMcfhtdiNxuSZ7BAqQYixn5dDpswpCqiI_MjH51TMikt-YBBCHTr-RGRIXaWxk2sTl01agDUdyWGJ8wsP1f0ndpLm3fHdejNab0MOn6osZGpP3ZgZIYoX0o7CoF_5lVDdc08Dt7L_yEmzk4ccF-JQ0JtbfYdzvc4OrUBm3zQfNVsdw_AQHE0H8y3wolZFgsPzAOF39j-_9SDKkZQAHkO42MKEBuDYNRANGd41ztyybua00Dn8XEYC7OiWofp6CNgeFts0oXhYM7YU-0A8h4n_xVYrk-0Rb-zpprX3pmPsLySXIDR0EBHRdi54BjFeutO1ODlZUI0JXKinpc3TEq1Q8Umhk5Yid-CmzYfaVtt65hsdKIybzDgZkBSqOZHNlU-qgtHZsZjB7HhlsQH_hsJMfO_GDYmvUyL61zZ_6i-kzVl9kQzarBALNWbFaReiu2SG9cY4n8raKYyXQxQXE31wFUrKaibEAXJlq26xQzmZmf12t4-3ZVxMi15PRbREWLYGzqNRARqU3mHd3_FPTeaLxcWy-KfufvSTVOIYkKoAXAbHfGckSZgQMlCPqKvao0Lss7N3bdcI04kJRmOcExYhAXvepyznGreKpfwWLm2YpoPgFuWq2cbkOg_KNOxeI-SCe8WL5geA7u7S-PPZZ89jarsvO7kPAIQXxHg7a46y9wzDLclZD7UcECTva6MEKRlMP5zsg4EfRkmZ8AQcykymQikio50dvSITkyqtD5XLkLYv2eypab6-1CHu3z-YUQSHYLOw4fsU6dR8lToK4I4pl9auL2j4z2FqwZTt-wnGkTXTevikprpz7BBaY78BYmJHquSGjIEoy59aBoFNWsKLhyB7r-JFAVRXgZAspE59-JmzJVSIfyNWXThYFzabEXW2VmUNRAcb2pRUP7KYWY8xqgZTvQZ2mtXQBY4GpAoXR6jgH-fmWg988kAQBxRnDoZgb0VqOUNQK29C5BIEt8CsHE97YSouTsqqGtATh9YQUinkIpjyHMAYRfnkMiywoFYeaJdEd4DFPIvJ_MmDWtg43nh4dbJahewqSfAzmFH1B-js9WAG7bivifCkEFdHfWcyDybAKICp2iZ4clqNYH9EoSgYJuDnUoyHrBvhWbaG4CZFi6bALdp68fj_7D6MCId76bo2D47SRj-q6bzrQFHvrbfK86EdM5KbJftG9ieNvuE7PjAEAheezl1fxBBKKZDCnxPzovqnmBX3mnEy_giFlxpBfUm7g0ot-FrszjXCMAcw4PNQchogsmtV8zQ8XZOo2Rlay3YmS9-nK2Z1jEBXckY8C8y2IavccKdbWAOUidl9LsHe0wLA0tC0YcAQH5HF1yfqhXeaUXmVA1tF7vJW6tBMsm443zWLqD3MvCjC6DoUb1O6IMaeSwvS7spYGuleZPr4OvXuWcylIBgHS8TlIwoo4P1zBFAlYOYCGsulS8TBKmLxOWskPS-grktYEBBK-uDxU9pVaKCMWy_l_LV8-r3z2HRajh54V3cEsSiG5CF5_EVeFJzAzQTGd79k-AjLERnGw7kNMs4LWMhPS-00_R3nRt_OPxiVnSY_vNyT3HHpf8Lf7NQnZQQ7jM6d3BBSmIUlvlECPBpaVgP6oc1FKSkSPs-6DGL-DkJW3Xo0WlcJKwl7rIXjCrM0t6n3ioRNkxBOg3grZKqF12fnWOn-jtqr0V0Iw4Lf-3Gh007OcyCIy1-RENp6DXM8JKsg1XwQTo7OfDfyf3ZSDWOLan4L6hrHPXKBKtk0m1fJvJQ9dwEM3jzPWJBilBQDI_09Nr2MCbLzNTGi2wzGMlMt4B8u7g6B5wmRWKDZchS0pSFgP8B6maEEZ8JH-c6p7wk6YfeMEC2Ih-KN9IEUvnsh-b6jj0FwcqtpWKlHBJFWJtGnXMT8rDuYX5Mm_-lAWornFLriTA8I9uu1ZOGiej0pWVgoQVWFawXYkYuoZRW5q4OGBwpiPtZIYAyDoZeAUOu7FAqrTBA2NfYfJr9vsXJOaDiYPDHRgf9IPb4xQHM0YSgpvkCDTERAkFVgQ0lLemlf2qcUXjgmQg2MNuI1NcMCu9A9o8-g15M6Sswsu2uLf8PD13MAUsf2bSudfdKaViZvkMCJ-VgQKsy2y-9J6nybC5tzJ9S3yfnlqMyHkbrxFAUf7NnocSzZcRtuRUpuGZsx20gb8xHIA7aUuwd41zsDvsOUpovILruvtFXnA2_18wbHXFKUGmKPHYYGLsz3rhJNtjs0dZF8EDD2XVmxsow3EHn4CXSQkJ8x3D5sDdyQE74fx_9l-BybhGK0-Ww_qLjHwwArVN6GcDacya-onH823CihgmmZKN3bg_XP0Q1c37IUApEO-R6ywQpAOWGv_re4uecj_1jmbBAxwRcvCNpNSwoGTm8_KSozpV6-vadvp_RC3TDHkH7f97yLxJ7ROIt5J8cQl-9eNJBHtVvWv0H0oe8V42gg4FsXB7_Fv8Ou9YUFWaJYb7FVU3IyWGVNYJyPoT662ImG2kQQHTzoNdHPdqTT_kh421XyfaJINAHA3KzKTcOq_4uNp3hq158xepsHM8HLizQKPI_oM3qvpSMxj-BuMVfkDGTnsX-JLAe3NA8yuFiZXyziuYw6hC4rMLuV5UTNJZnGS-3EEGSXXHCfghBQslnMt4jDj1X9FYwL8cJCmPPC9sEgpCfBdPYZCJUjoxwd2i4Nd2vweECi1KOOoFCdmTcDcp6WmlQxv06XLgfCiyC50yBmqw034Ukq2IsrYFPDsITQIQG_HBAe6k-2dxanLxJGlZK6CPCx2MKGElRlIESSqa99pCuUgzdvs-_ZbG-fjr42LTHtP0hHJy_ngCjrt8IgDmUKI3xEvlXZRnxnp4jkH-7FwZoKkh01DjFYkAscw5BjAlcWFqgQFnqle20OyaUTMaYIvjf-0ZUOpGi_wab0RYW1i5s61xvKyIk_2evZ87LyS57WccbcLy88MJ26kRxPMf9rOcEetd1aZxykk73d7A_pj7zxIrvjeExHyxUrM0XFgLN79kvoEAhyhFdZ_FZItdc98yLjaToxZPORBhTn1w0nj4spz5FjshbItFfVLfGCsAxgxRI88AO2oB8389PNPMe8tA4uMPMC2PFTqK795Hek8Vos_khmzeiXwo1BQaVfwLglOeKhUBAuoVvCyh93vTjhapy14oMAt24rP1eeHnQjee5Lfb_8p3gXOMQ39yxQ0Ts32B-CfxQzbPQrRQtJls8Y6lVDr0oOFz1gMHDWRrzA5z3tqHpj0Cxe3R1luIIQ06DHrv73dswQFCY6mYUsMfumIz3WAO0sa7s8fzbGRpG4zcA5_zxQpkwOEmTbBf8n_7vCRaS3weOMVJBuNSJCiQGBHR2eESoSSbV_ESxcoPGf-Wz_Fam4chWBty66ZX9gMqaAE1zWKAGMEF9zlemaUpKjF_NQJkTSbvh94a6Rtr-WR9QhWFzNxPBPIxItxGb5yNTiGZ6Ie-tQJE2Kyd1SmcfUY5fJnCdItfpnyXL4WSAbSsob9XVg4Op0uBGG4yXL__kme-X8WI0wABAACDV6iueeDk3PptXUV0BSR3PCdB9sa2FWGoPt81rhXS1voD5ApICH0CYlLLFnsnBNNi0fB0f7ZKC8y4286yDEl0NhkKDvq2n9HkwBGA_oiFOcGotvk5QXufiP82pBzLwQOow95Fx6OM7HK_uPVjzxxdawXQgSdHoQiMJwbUK2UYbfr0iYvGr8ERELWRTOOiBcZYsSsNhYHMvwVW5ahDFqpCiW8JJOq6gjlJmZ3cvwVWD7kgLmJXMnnRqtqaYl9Uk0EBEw6CZI8R0Fprd4sn-AM5SIgL6PkVm0AsR9FkBxFO5F6x3-DMWIZnbpEFcOjgpkwAtbmPtesiKe7w_XeKXSYKPfzCM5wyVZ7sq4BZaQSMzOEOgpFp7_W4kjVZuWL4HvPBA0eaJkqCCnO9CvTPynRPisSgqY5zcysrcKLAAHSQ247c1yi8smlgYsFznlptT_2rAD8h2xfxUSv9KDaokZ9LROVtS1pGJumZfwAKuHqEis6B5GAG1uZw8SgmRDB5-_dcAQWOP6jgn5PBB08RKA4xGMxzHTTF0iQgF1HMX4ScdvPmR2tC1g2_z9NYw5VvHewjIQTVUgKhl6WkLiggz4qCItjEQ-sQaFctZo2QgTphAAhAPbVVKGmXydWSPn9-MLyRxMEFd_MFPx0xEKWUtWopZnXoAnB6cuRUlaR7Ex1bd9kSJeRT-zS9vg6SmVVeqqF10HbBydZAp2CPsaAXMzrohNXkjT1tHa5DFsGCWN8Pl96gZ4XU0hcy0-v_g66wmMXmP7XBBUEh8wlJ2tg5_32LC9uz3mUecfSbUnNnM7jzPEBx0MWh0T5W4oXWkjl0JtkiRFaawUveTNuckzEnkGqxWKC3Pfi-4_c19f14CGUzZTVXhAWYKQD15Ldl65r6xU7U87dFAQUOHcEY6KUiQ-xEZztcLU_KDfunv1hTy9IE73SiYpIvhvSeus46KY7z9D_G1Hw7nQFhHgxspVLEjejdXY5Pms0wE_YhQ-bkrCOPXpnJxE194xSi57ykPsPH5TBygVP_fwEFAdqOPwiKKQ4MV-d2G2-omn1DCyqoL0Vc-bvCee7FYytR_RFO2_xikbrBZwnj_buFvANP_K1TtKf04nY7mjKJiSbrTdpywo8PvxNB2JpBD9gkVPuA2oMFvUFHHownN0jBA9yWmiKpQTY_ZqT2TR2bmCTmwL3sZEdPVl0oaBlPiFZbDTLGgF-4fBlm_xZl1OiAhj4KxXwB7w_DqvCS0V34A0o-Su4VjZzaEqO3cTuPCBuJRfnExkN0QMMtx-OMPaumAQSyZ7-x27l3q_-q2ABDt7hOImYxGar-1FLvfxxmv_aAUPWCKHHyEk-TpdjgaLYs3EWC2FD-DNMegViiW_kEhe5hNwBo_JVCn82HCUH14yb3mZwFNe2vAp5WvSVoSdkBCgEELEZw33U_IZSQ5fm0BtguhMiFPbE86oWsZYU3cs3LiC3hW-hEBIIiqIh3zxWg7Z8AcaoK_0hQeGI2DANl22GKyVTRdHgB6Vv2Ggz-KqB3NYkLJ3AirxooP_x_mqVVoIj"}}],"authentication":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"assertionMethod":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"keyAgreement":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"capabilityInvocation":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"capabilityDelegation":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"service":[{"id":"#TrustchainID","type":"Identity","serviceEndpoint":"https://identity.foundation/ion/trustchain-root-plus-2"},{"id":"#RSSPublicKey","type":"IPFSKey","serviceEndpoint":"QmdPZgcyqHJTiPeGMcAu2AAkZZ1U4KtdQXid1gdJQtpvyU"}]},"didDocumentMetadata":{"canonicalId":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","proof":{"type":"JsonWebSignature2020","proofValue":"eyJhbGciOiJFUzI1NksifQ.IkVpQV91YUV2QjctR0FyRTlkeERuMk1rclRUa0t0VXN4eGJPc1NESzhwQjl0ZWci.X94wTgzsovLEAXU1CG5M0Gqs6Gu9oHklr4Zn7aEbrdtOI_WCSCrWJuYomkcdeF8X5dV_ApZ6Gh08pPcV2VSClQ","id":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"},"method":{"published":true,"recoveryCommitment":"EiCy4pW16uB7H-ijA6V6jO6ddWfGCwqNcDSJpdv_USzoRA","updateCommitment":"EiB8B_LS_O3NWo2P8fSuRwS32GODaXoLREZHdqpg6x86yA"}}}]}"##; + const TEST_ROOT_PLUS_2_CHAIN: &str = r##"{"didChain":[{"@context":"https://w3id.org/did-resolution/v1","didDocument":{"@context":["https://www.w3.org/ns/did/v1",{"@base":"did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg"}],"id":"did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg","verificationMethod":[{"id":"#9CMTR3dvGvwm6KOyaXEEIOK8EOTtek-n7BV9SVBr2Es","type":"JsonWebSignature2020","controller":"did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg","publicKeyJwk":{"kty":"EC","crv":"secp256k1","x":"7ReQHHysGxbyuKEQmspQOjL7oQUqDTldTHuc9V3-yso","y":"kWvmS7ZOvDUhF8syO08PBzEpEk3BZMuukkvEJOKSjqE"}}],"authentication":["#9CMTR3dvGvwm6KOyaXEEIOK8EOTtek-n7BV9SVBr2Es"],"assertionMethod":["#9CMTR3dvGvwm6KOyaXEEIOK8EOTtek-n7BV9SVBr2Es"],"keyAgreement":["#9CMTR3dvGvwm6KOyaXEEIOK8EOTtek-n7BV9SVBr2Es"],"capabilityInvocation":["#9CMTR3dvGvwm6KOyaXEEIOK8EOTtek-n7BV9SVBr2Es"],"capabilityDelegation":["#9CMTR3dvGvwm6KOyaXEEIOK8EOTtek-n7BV9SVBr2Es"],"service":[{"id":"#TrustchainID","type":"Identity","serviceEndpoint":"https://identity.foundation/ion/trustchain-root"}]},"didDocumentMetadata":{"method":{"updateCommitment":"EiDVRETvZD9iSUnou-HUAz5Ymk_F3tpyzg7FG1jdRG-ZRg","recoveryCommitment":"EiCymv17OGBAs7eLmm4BIXDCQBVhdOUAX5QdpIrN4SDE5w","published":true},"canonicalId":"did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg"}},{"@context":"https://w3id.org/did-resolution/v1","didDocument":{"@context":["https://www.w3.org/ns/did/v1",{"@base":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"}],"id":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A","controller":"did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg","verificationMethod":[{"id":"#kjqrr3CTkmlzJZVo0uukxNs8vrK5OEsk_OcoBO4SeMQ","type":"JsonWebSignature2020","controller":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A","publicKeyJwk":{"kty":"EC","crv":"secp256k1","x":"aApKobPO8H8wOv-oGT8K3Na-8l-B1AE3uBZrWGT6FJU","y":"dspEqltAtlTKJ7cVRP_gMMknyDPqUw-JHlpwS2mFuh0"}}],"authentication":["#kjqrr3CTkmlzJZVo0uukxNs8vrK5OEsk_OcoBO4SeMQ"],"assertionMethod":["#kjqrr3CTkmlzJZVo0uukxNs8vrK5OEsk_OcoBO4SeMQ"],"keyAgreement":["#kjqrr3CTkmlzJZVo0uukxNs8vrK5OEsk_OcoBO4SeMQ"],"capabilityInvocation":["#kjqrr3CTkmlzJZVo0uukxNs8vrK5OEsk_OcoBO4SeMQ"],"capabilityDelegation":["#kjqrr3CTkmlzJZVo0uukxNs8vrK5OEsk_OcoBO4SeMQ"],"service":[{"id":"#TrustchainID","type":"Identity","serviceEndpoint":"https://identity.foundation/ion/trustchain-root-plus-1"},{"id":"#TrustchainAttestation","type":"AttestationEndpoint","serviceEndpoint":"http://localhost:8081"}]},"didDocumentMetadata":{"method":{"recoveryCommitment":"EiClOaWycGv1m-QejUjB0L18G6DVFVeTQCZCuTRrmzCBQg","updateCommitment":"EiBCBZ5TkPXA7i0X_bgcY2AR3Q1mOYOdpG7AREos6GxZqA","published":true},"canonicalId":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A","proof":{"id":"did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg","proofValue":"eyJhbGciOiJFUzI1NksifQ.IkVpRC1tZHk5UWhoR3Nzd1lNbG9FeHR0cXFNVHlEajhUbjdRT3RpTVItalc2MWci.LutefXAigkrHZSfNkz7JQadsyTAmLGU9KeT1LDtUfs4jslp_5xfz_Y153fUTs3WiQgPLUdvuXHFjQ3INP-OfbQ","type":"JsonWebSignature2020"}}},{"@context":"https://w3id.org/did-resolution/v1","didDocument":{"@context":["https://www.w3.org/ns/did/v1",{"@base":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"}],"id":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","controller":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A","verificationMethod":[{"id":"#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","type":"JsonWebSignature2020","controller":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","publicKeyJwk":{"kty":"EC","crv":"secp256k1","x":"0nnR-pz2EZGfb7E1qfuHhnDR824HhBioxz4E-EBMnM4","y":"rWqDVJ3h16RT1N-Us7H7xRxvbC0UlMMQQgxmXOXd4bY"}},{"id":"#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA","type":"JsonWebSignature2020","controller":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","publicKeyJwk":{"kty":"OKP","crv":"RSSKey2023","x":"EyGvw3AkcUf2TZToBh6pddeaaocmvTuLCSLun_yYJpL7x0W3gVEzeKlj06J5Sej9Duk0W_yGhbOKCahOx16LszwTHVgnH9FjRk0nwOer4yKaKnjTZ2FlZsYI0OI__jhCGP9cbcOEd-1rfvUFu-ghsj6oHfSXDBm0Ekplkgs1IktoicuMsF-bD7I6tZRpP9tqFGqARUqvR2daQN-scwYUNsv5ap3XakBCDvOCBc_rPAwzapY_nuC3L6x60UGBAPtUBANdaMhAU0gxd-3JMjcSjFgwzAhw5Eorr7bIp1_od6OfBRYu3sIkij5Es6RDBLghUAx2Z3dznniJRh5Xlx_8zn4SYw_xhV1X04vY5U4O7-7veKMqKxzzoGOR7O137gSTtBk66ISXfE0k6LLsZK0Qkzi0B6YQ0Xo86d-COFNhRWQ_Lq3SCSiOaJ4lFP5_RVlHzgUXm6XY1X0jrkVPWdT42VxGjFvy_KX9f50dOkdPJTax8bGv1nEpDm-55UN8nrIzsRODaxMBooRL1y4OxyW1tpHaEdsoHvsZrLzM5g7FB2ah-62TCGkPcG3Yx84MPp50eRPIlj2omMFxMpnAZKBSRMGtk35A6xAZUI6KTYGfNI-IuWKdk0UOn6xL8W3EwMTxRgx1v7iklbgxKuCBoOeAK7FhoOVzL5YnUCHb1NUwAxDs9I5pNmrvaXsDDLKLIoz50hRAdnK92whifFoWoJOOJbQTb9sx43zmB1J7G_T28MG6UetI4dZljoNfWpXePl3vNwW979nNg7GU3N_V8ZE_slRmUv-rAw9jD0w9KXVCuZuwGIKoJ2Co8qjZxnhZUtmi3wFJin73V5BC684ebh40fnA9z-H1Kwa3ItX_mQSVYeMV-_1fydNULsdhlEnpwI5XNQ25LGqMNb4v-YRBXLSmN5CituV9rPXg5ZzQvy8VVE9qxWnicCxz2TzFrxFOOIhNTxf-YQT5Re5HJAvdy7Y9szo-i_PgskFdVm4UxMgH9ddrFUhDPNmVtVY8PoXlMzuU6gKR-1np9J6FBttHOIPu7LFFdO0Vd_Y3-Dl5mdBXFcP1Do1GN7ojcuRUB4rmB__upRAQQsqCApGurtGP1zgtMQm6ozF0gt_JpoXgvZEFK5kkm92vpedrSfDPBBn5NPIgmQgKSYfvmWRmADyr2J9bc6EjJr1-YD7QR1r2g_eGRBE1S6dexWceWTq-RktXQYOSJBnKLSkbqJniuoA70BMkjU4Jsj1EJB7oxE41RRMchA4BRlClSi31ga0T_bk31rNTLQNLGSrBrh0x2nlG8IZUZLB4fIKKweFD9pL1qhLMM-SQl3YR4-v2wxjlMXTrEDjz2xdwJsQhhzM5trtqhVdxfgBwB_ZBtU9KJqYvkB_3BhY3kYQSGDLhyCHbjyIVYl7saQGkTz_owGfj8tD3gU9oJlZHDyjf4p9AObfF4YXKjVBpPrPgwgNd-G4LAgUOn4DAVwGmGBjQaNWiLet4g4lRsLS3LkM1az1w_KyYCX_k9bptp4qLgwV6HqbLx1V5WkmubxLMpHlbV0tZFLzwThEaKpqNyz7M5qIyDvaSbTFtQ9feXhRHU7VN1MgH2AQmQzHiygXHs5qafdGSsKoMm6c_6R2-NXl3asM1TSUmD82yKonGYhSHHy60KvB4M2rVTKRENxR93u7gaYr_4cqFY9LlcqGUMzxmm6TadfSHz3rSj53C8c3Z3U9x9ftbKGOZeybdWhYbRGyES_HzmlXV5MFY5qHiE6INi_ao7Xxm8VRi5rdaHlVDWfBb8gJENbUHDDcsKQfae-4j_vXmvq4s_9L5It5kVLCT9f5NEf7jsxSP3mg9hqgwdY96ob73GsHO3HRoQARhPUt-2o7i1JzScqRH38AeDr9XnxC2Qu4LT6ffOmMKzA3qngyxKmkvyKmIl3_eEhDxpdTSf2ba6EGOD2GuzvGv2a_P9QHw52mvtEoCLNJAslzsxwxbLSnLIOkbJca1Ew26womAjSgnNwUvPCkz4lmSNTbyF63wvmNJJeD0UgkBTb2MxDw_39ukWvH0mOSJegpmENWzMhvKvxxMgB5Y1VY6Hq06V9mcg4iD0AdI-dM646yU8iLfMAAkB-EvwUUMXRE3KGU9Kx6dqhsSCrow4QDpzk0B4FCATLwawfGc1_rxQyumhF9nagl8jP1ITcLi-hlUyrOsKfSK_s3WKTw4j9iBoBWCzHrX1YC_2UTnq5XIdbY9tT4NajRzqwKLV3aYWRnqXLg_-l5k0H2GmwmRnm4ZqU-9YuAy8MQR5CM93H1gxE7oL_IWIyH_tCXrVH4hRhjd7GrWcA90s1AFpCHhBZs72ORxG_Rh8VcJpB5cTpbQfk1ESme0-UTXoSnuLPfNIQb6I6fwFkIvBx9YL7gxaVmjHMgk9BLR89iwuo3VsEsAs4ktbFfZ70l821y6q_xmOBPF-BxJzlVuHMq9hfyYVA-1ka8tBBeEy8NJ1PlYBMiVjHoKWMfqDKo0ONNv1Il_ThirUq-MM4pc0ENOqwCYkomNBFfFHdbS8L1Y5yIruufFxRbRPt6xC1TnDtq3K7JCpRjsTqv_1_u81WA4UIlW49NaruM-2lPlL6P7rWtBqG4axy6U9WYqom7aXBW0cbg31hY39xZb49G_SfSYewGr_pelurFdTag1R3ZL5VuDTggqErrppxKIBYHQP7M_reJ8fQf4JcXOmMkUOap1K7QJvvENxlQ_RQRj10d-t9spgDv5gki7uMDSA3fp4q4gf3HxZhYwPaImQ9J44zCCLUdo5dyhHsyd9neEeBniNZk5LDZRfX66ERlj49CO2dHmHLe-YQACZnMQDDug7LF0il3QHinPD-nedAAxpjfUus9Ay9vRx6nB3fHr-_9C76qx_NjCehMZHlsAOgZGU-yjdwY2uu8lvnb8dvmCbkIBYn4S_aWJ0qIOEjfWuADwWO9BXI5uzQZ0EhKuhALABMhOIi4pmnHqCE0Durvn9RaPiFz6ZKFhW2d85ZAkks_-ARI0phaKzggmB4E6k5EV3cLqkI63Oiiq21QY0VCvc0LuNoAVYzG8s4bx3udSSORrRJm2fOdURg3wtPlFq21m_7y8D09xKpHkXgEbuDJV3hWk52u0Rxv1MTY2V2_LkHIDF6my-MZLQQh0dQYnUjDfvQ3bTqj6UE4MZ07R6UZzl3Vjw53lM2x4gI17Trma17Ag6Yg6XiQA7QqgXKWy3jG6AuBLjuYRPeYo18lJm00D1D_Z_C--D6zMJKr5ohYrTi4ea_dh3CI82xBNwjeTAd95r6X0wzC3xodd7FSWJMCgt0MF6pz-MEL_jNi6sK9mIn05U4icLZLjBwl2lObaoiYxpyWEpnuMGy8J7dM1Z_aRpYt3J-Zw7i3Yf4JI2JV9u1Mo-ywQyXgRcRBhK3emrFT2fxH8SqkKwJCWn7frvbukOzSQiKD8RFuXA-SWK60mJ3erCRnka-xkGg3AiBxxeE8Prk8EGzLcB1UDRGQ_x1PXmMNtdBK65dtv1b0jGTM_uSHFndWXOrFALwi66JGyIca2WnCfQRQDR5EPyD2d2Naecbj_jMwFUsbYCxGTc76n46c1pI_QH1rxDBQ7j1Tj_rcQz6Bk7DMTNnlTFhJn2h7yVnoRPenlNCWZWZPRpr4vnvS6Ii30os5W2QaGHI_TqhhaXRFU8Z7K4PUUUVEv6u3KIZpvcuVxAbcx-ppLVkj-r2vM061Nx9aXEBFd2whV1Tw2rjf-6fm10N7U3ssLGC6sfHRpSVcsENk-ZjuYH7sY-zmN7Hf8zOYHIAZDUr1rjCgG2yCujbdOPFtPs4QKC_cFSzbpOjRmJ-urzi7duH_vH3_TBhMzM4jowgM70l1LoB9sjQ68wzlaAs74T04IroWMULoZOdaeIS54ugR79EhgqvukrIDLEoCekAY7jAs-iNW14YRPrtdul8zVUjLd4I_X3efx-IX7HvR4RUp-6lqMSN46IfvlScl0qBY_SBgCpdEw66SRo1OAIAuTy7VWX_mbvLtgZPPMkaVheFwYwBZnBLKQKyJHrNrKRQ5GdrSnJP89jdh-o6VEqG_whEec3cB1LwXipXb6v1vi-7jxU4kpU_BTMtEChb21tRhmfKGiQxHbOTRJbHVoQJ4NFlS14bTYAEuJm6yXnIW-GOVCLvlHShp5jeWc_9vvvBZnk4C7bDxY80GxadNmsKy_-AcEFN_QI9pt6lckDeTOQxgVz6Anz58RIkvJ1oPL8A5FZOl4iYuQGDAqTP6Yo-SdHbuVOuV3aM9K3L6RMgj5Z9z517O3oqsmthQdy5xtxhalD2bjV4fNsQrsXIGuNa4nAnFtfsi0uN4ahR1_YYVuQgfEQLOGSzJnw-bQ7m8tOxlDOP4MsXg6BFSBvo0LPwieTdNbZR_N4FueA59bt73HfANTd-xz6ycnZNRNO9DbxBRwXJnQogguwZQdLLLuZjqoglKwi3gmMHvCR-3QngZYQw46vAkTUuYfdG0OgaYuAAqtsEvJRaBVSud7q6pgMqM5UbG9eWv20h-bMQeBEpIuVG08HOEc9TeUzDOoE87PzBkfBqVu_s1tyItQQ-DqSvfCQBobT1pYeVsuyJSGXuaF5MXooxYfRpsAuysjWDKDNxAarmMCpioPCo5ebD0elYa6S1KV52RN15vaAZLPqNRiFkek3oy_M8C9Fi2nLzXG1Bjn_JlKzni0I3pofwFNE2ZJnoLSVpLwVLQUzzCB5GoS5P5C1DcPDxpjAr7e8pWb0QAyyIuz1EvSssczBargovo8iNxthV_MgoN4UGY3RtkDRyw2DPcFdji7AYXw_q3xlxXsWEZMfjTlkG0FfwSTHbhrL-BIXXw1u88y-w5SvjBBwk2wW0SjPVgm-qq8yonWXhnVfu4xRLMY7qNRltkzyB5pQ44rJ0iFr6tXtKus3rUTx2PbQOPNCYJynCWQnA8anAlOiTmIJV8G-MYkP3hH3g-VZSnWE8gQhbvXy9OY4YtyqX96TXRGuHNuZBDEHiPmNAvKkfgVdGE1xrxPnfZ5eN2RQWXAf5a8xgISY1bXxlt1prbFSiHTMLnikDpYNy95JBQnPEqdIYRhgzh29L_RQpIM2ItE6rPrJCl-NL0Mo3YZNdFepgL-5uOjFilpmO_EfAc06pm5sP-g6S3vOx8I9j4JrOnhygXvZx4Mr2D8-R_7s2F5QOYKCpcYmhKSqaPbdAX-q6oNQQ3fesRtmDJIVbBmioMmu5k3C8hh_L2RNAe6ItXT7XVCo-QFQ8fiUIOMWASrYHiy8qsbX4kKQJ98v070GnqCMpKVtB9522SHxJWv4h6Kpsmadh9WjAmzItl4tRV763mNcLeidWzlJFUcfZIVm9OrWbHinBUjKFnoeexpecTm2ncrzpUkMmJghWKv9hUzk6wGkQhsps-94GvQJT2ou4T5xLpeATQ3oenwez9tEwxQ07tB7FHEiIBpA4PFExNwdv8sxaEe2Zaoakh1iEjIbd4uBcEAd_E8eE3VSEPvB2_zT8nek2I9pcHEIHA52Q2_j979f-vAyJci99RN1Va8nvk3TyMz_g6OCknUZcqkhXK3lqigvhkUBl-IxjWqagdTwPfwGPtwV3JT71CZDfBWujVMLPGB_gT_dhsWlIN-sC_yiWL_thQrkgKFPqXPwQKCyz8r_iv4f8NnJIh3W6_hUURFsnu0NpVAlhi7iOU-B0cqk1NHN9BgNbT_zU2aVBEFBrlQetG5pyxxgyDSvrz-igEzZ9oqa7-EIgNv8P-0T0IUrlCIQSfPsiAUsbExwg5JwdgdQ_gD9HUt4U2Npk03XtaAySY1IXJCXeJLp0OIcc8hFeaiPMMv7Caif9RsIxjwnikwLFGtpNy70Ed6CkTMtxBR4uShDzbSz7Hk90gu5-jV5WGysOA9AbW24iqgfgCKjrjgfrod_MNG939PdD9KOV0x3MqbZJmBLB7jKCINC2ilgH3Ez4crHFZJEkuJ_Qq-KDXW7l7hjHUG_debtAu6qI1edYP09UkgmQtnZgLcGAWUhDxWhdf4XYOHfqXxfhiVu8tF-ly7iqWkmRCqhRGV5NmzUWuwvQ8-Jlh4kRa7nhpwb7ivyXiDubq85_tKuha0qKFzzz8gFuiefICHX_Uy3xM8m6Gy3KfYirumMAkuB5-IY7Dgr6IZK8YXGLZb3QEXmOjuwp8Rmm-bMnCXehgCJZplNtcWi7eQxsP4y0IoEUsmmC5Y1as1sAs8-R9XlxBfP3hdGWbOupZfS6FmMRiGD9HoWesUSVtRs_tgOUPPVav2HRIK2CLYBRwgI1NaeRcpnO8cOye4UgRm_UF36pi3hJPfIdCnhxGeOH5J0r9zYEnTDs18YsIQedQOJ9jvGBLvDi8dJ3NRzof0hk9riVtSPV7H2EKhkEL67E5pccehsmZnha0ewYbZdgEstjzjwQ6qkZRmFLOBdP11yCDzgs3eDmnk0Ztewl22-WhhpumCfNgux5OEtcSu6hcC_gtsXQgTm4QV09fFZJAH8tyfFildcaycx0w6zG_tT47jBYIwVyEI-Mvv08qYw3ZN6558VgacYehFWake3ahdjDxZ8bO_tBtLMrFXmjRpibEIYbWZW2OPgBv-4-Z_EPXtLrDpJxYjD8bUxNgxwyqxAlyqZe0FUQVo1RTWV9hzvj4GcOG7wC-_t9aEEv5h9hg3sQXBxwKwIulPSsJlAeW3dygypohfIMKiUdjDERwhgvPsvB_vsJIaVpN3SJVfNWvMEFAIRxl0o0b4upYbISICcxav7YjxARlPcV_nqG6Lnj9-6MtHOzvmwMWpcM0Y_FFro9TqKAj8TkAiGaEMYyJ8Z5EMAsGd32HwMhmdeJbA9TxNpC8CIpeNlU0H9JeSDR3bl76oGAPDIc7bDmfKjcCL_8rZamAaZucmCI4Fkkjaqyl_k0TOHrxrc8EcYzbICfu2Xp9j5Bl_w7GErvNIbMsbJejezsJxt6CR71oex_OaL_DyxGJE6bOaWZFwF3WqhVWMoMEuRwy4Z11DIsqZ2pbxyArURVFG3mIHnBJ7ffjxYbofuuuw9Ce3S0W9AwEvXRlquPr3-wLesE-Y09JL2x63dPrsfx88itwaKSyGuJyvqpTu8NwpAR8d0bU6nXG38O2ysH6-xwvDGoeApjhGaTD71tv5hYcJj1X2M-GeWFi74NjG-PYBkamWVPk8v2uimVuB402YMgUAe5RtZcKVUfHczIcj7IWreTJr8JCLl4N_X48ji2KDuBuuaBRBUYdjkl8ltWE-AQzatqUi3DF2ZDEjEarQrk8K6QDaHNbMAEQwqxIcKVB7rX6pwR4EA2xN2VYmCskYAReAbKYyzbFKgx-_kbylwjO1CMcDTdhKYHnfEznxeaxzjwopfWQR5JQ_y_4OExcY6gh_FHXXyMOQdyzdcNMPFOZDvKAf4PiXg6BV6VVbvlssgImhEbhyfKlwhmbHkrD90BVSZOfwp0m_zd_xOfwSYckSwo8ef1K6DILkCmiUSc9wiCBBGHF8ex_0u3nepPICWg30NqJPii7moRYlXNi2hKgTB2Cy1njuP9pNFSD-8cOxrrAoAz6SaxdS4QqxjykSaRko3FibccYcSE_fkx7_WWBSW_1GOKTqQltkzHWMqTbu3wEjBAbnQjYGEWn8aTNzsAh1pezmZurCOdi9uL-cjIVavKPn23HhHGfS88f3pRdohcdlszyc74acnD6VgT0VnArfeYPNBWcliVDnCE3qYSvter4l5Fe4rH1qDISEq2ni1-uxNRJx6Ck3-5bWSZxHAgvc_2gC2O5qc9TU-akXvNSqLmNtKmO2FGFtBltwgyLc8bVWAJrNxuWQVCUxXlfSkxaGXtN18lGJX-SvmRn5IsqfhUitHzJjEASiI_YOVY9OoGEkK1a532FFGdO00mS07BQCPV0w_gldLncCOgt8VPaB5d5SjOF0_whIcVAIY95y5MrZEJWcbES4zg_jdGb5SRLlr9PENPbne9VYK4_ju-MCFNo0uWibQJzJcpaKU2rZ9sAsT2goR_lu-aLGCdeimhRmual5ISX_tyMRikPCDidsweqUeRzPcriSIRDKLcQfzA3P9Lt_Mo0ql-l1EX7TcwLgCsISBJ39jyhHyPvNPbBAFAlrlF9uRhz_ATonpUwgZrQHSlpsy6Mzh-O8f57HKQTRT0VigvfIeC3J1TR4EzLkHUdC7QF4JNlprKFQl-HUh9VIOpwXfQ7VwhbxUw-MThAn8fnFAKqd8S-4S76Yn4Ns3B0FA0wlDWp9AvfCSlm50bQHUgj8FEtwz8279OoIhBEIMnA_rHNwA1gPMSAl8aU4RO4L9wTbhwVEs32i77O1pQS93ZeNwOwXXoquAAVFZwusOXz2C3jxzKzB6IdrA9LE7-ALHDvmxB-y9KUe-RgCfFgjh9EE7rdwftpCOMj30we1IOtQ1XyFSwpbIK-y6e6itkyx73nB8UicYQEQHDnl2UPtxm3TLUe5bx_E0sisng5ZV2ISypN4_CiyoAbUPCapdHnGLh5VJtaPPq0NGIVA88MkPxnJC_dTfsZKzNVDywA36U6dGzcSH16QoTfJ-ZcUJhHAKJHizKtLpdxpNKlSugnNW0P0XwgrRYAehBBqJAWrmDc2vll-f5KYy6AFEWfIub9SODwuu3j3yfdoVAjpi6Tvm_e_w18ZBYKjtRrAAg38eTrwQwdDDovzBO6t7xmJkqOxsCFl0tz0WB7YxhVMfhC6qv0ojnXM4XrhX482Ew0yMUB9Ql2_2d7u9-aM7VztBqRf9dtPj0Fc1WdfiMD1d72U2D5NukpfdO0k74QL4xFcEWgq0qAPT1Xd35HaQhe9KfUYx0d7KtbBb1BrpQ3zZWS_ThLtfTHOvGZRQH9bQQyFkx7r9Lnal_GmnKw_w-Y5ecOTXwxvtB_XQNOo2i02MTPLpYHXMCWCFB6kHee4fhJVL4yQnaac8WOYkNDZeHf7y15M6Ezs0ieyusNjY-nfeAuXS1kJ_lf-qI-1xCpx4wmOy-W4Y4Xbr5YWS8Pe17115uh3ZGN9n88HuWj_fzZ0BcrgsT4p5LvSm9lntyD3oQ8pX17phhk3xqItrnJYAq8MfnLgifMDl6XucGJj1rhsvVGfr_ccjSHxohBb0HWL6g16xEvKsXnQe-PHn8Djtpc9doxqWWC1QeFnjIFJ38TnZd2v6S9irKu2D-YTw_9TvgRZTHMLgHH7pdFo2P_-mrKP74-OvYkn0O4aUVAZ6-bCXKIZ4ZzFgt-aO6l6vyUUfhcVrQKcnRdrZ4_GYfiRdxlBL1rvcZAkVpH-iitAdQ4N0xFHFL3MO3MH_EepQXLXSgciWBbbc9lzJnd4GkCRT-uH1SKKtquXZIO28ERVLB5yD9xkl6-ch9qTYNnNcBDNSAJQeFBwCHB5xZoyuYfN9p5v40vfSDAoJU9A_3_kaYMyUBVaxQWnKjZrrA5hWy2fjRUnVpeX7PDyAyb6eZDt7dKlkWGQxvhDXRFeN9yjohquhDj9OSS0JlHsPLobIYEPThAwpAYAEH9aspydpQDzH5LdB8aSUzTmFvdt87KW_OjCX2bAvPUj7a8bhfrITHuCUwOl_hNSIaxUX9EuHEifvRKi_KnQRZvkTyN6Ji93jcr1wYk2FOjZEVdUfC_lI-xzuQDSVWUUl6URvL2tfzx5FxqScbNiq3xnIqLrNONk-p4hi1QvPbgiYvXevv6-KgoCOBN5b7E0KUoVcBh8GBPzCeP2EZwA6C9k8u55Ul0Y6dohgm5HS8NQfXCSTt7QQgchGBOyOP96JR_uRbyLPJ18KaFr9QTxkQrxpuks_tWBdd9QD7GN2MU26S9veV2mrWHNXBiKY7NNZjYSkfNyzvjsg3VCwvxU9kzvkozJ_hQnkOnEmlI8bu34cFvYy1Ms4X5fLwaFLMmG3SnAIwBsCz3HxzKU05NBHikuB3B79BGskfQK_Fe-rkahNqJgG2ya6xgeIBivC2iuCuVjM1xcVN3jM0VuwQOCIVwjPpyDgWwjm5rpjX7LfEzwjyXynX5OR8PVugx7bAFwv0UNcbkBNLadJmL5hZfeXHzgPM5u8M1_PEpwxRddCDLbmbY-Y1naQwfaKRQp_c6KwJtT3IzkOJlaYsUlEeoLQKfQI-OFr7Jy6N9-tP3x_0OpecilN6J7UQLOTQEIeygISrIiIkSQgL8m7YCl7cRejrq3kF9UutkU2OIJFseVIFtIKZL92vc3WSxj6A8NkX-yqQ9LCFljVw_acJ9tUT7tNyOF7mFKBQJPa92WpaOGgzq4OCV2nJs4GFYjXgw7uE2NjQ2i9_auhXryGm3uD3G29NjUQ6Lkingi5trDZLCzoFKtQ_-2tWnf6sC4HBlShllmYDfCCorSX3Qc9WvEwxLbRvNX0CgPCEoxIKHAE9UzN9sfWZLD6BCXAtERDgNqc458B3xIrpXpk-hmIe-Res9HtuS43LqebcFiHjjKKiBuUEBCSxSEYQPYdEII9QMsBsp9IoCOKL7y6m5EgCfQzA7hiWLlE_Xrppv625MGLzebKWzu8CP1mOPWTp4FYwaXl6sm0rgbAoR5XtNLcBazT83ji0Qhc39dVR0nFyvdSe9L-EFw6dbYUPPbQDh0hQVzwnXZYFi4wgX8iFfyvfj1cAGrQNfx2yekQfLm-vhGK_sIlCRVZf2bjS6rwAbVIhhPFuTsQ5EaYCc3QbvJg-slvxMGfr3gpUkMV24EE0dCemwKRyRyf9zH-oswETPMyAFTQmlx715Ao-RESnFuc1Ebl13oTofrWpye9ZaqqsGko3Cimdifa716i5Gkq2FJNQRRRrp979uFgzdwm2AL3Wa_5I1t4aHY0hFNXzKU5u7gNmtiTDyLSOIWLGfd44msxBYFSE9YqSdU-7KpEtOLQRppx3FR1TQooT35XW13oPp37k91Uv2j8wLJPAid7msh1AUWmpGiq9vhair7EUlZhnjNIEvhlTr6sIwFzsJPRl9Dy838w_UqVXhKcA2wJpTCjgRWXL8R8b6L7Qs2v0H554fmrK3qcTm1BgmPf6d0aeO9wsgj_cSO2gI6HgI4zL6PUQTsMTzhIY8pN8MW1jPWVa89yWjGjaanxKT6WyzdkCGj6NcG3Yh5UoKGeehwa_5FQwggBfzXYMIAK3swXYvK1bVz_68c3eLtW96nYc1mnOw0QmcuQ7ajBPpwPVqQwH1iLRS3nEWbxznVbgvcdHS1Sv8LcVU8htWp9JheVP2OCiGQPFFScImnsLDC5WZxJNohrxFO6HHJ_6T3py6zz491E_zWqb0B89YapQO7LKc_D3pU7_3-ug2A-BmtjReN5-I0QAaNX86gN5o-LNW8yl7DmVU8rDBHQBV7vZ4uijVQhDvpifKk5mqhztr7B82gamJD6gUucjs6nA9V8i9496A3dTMHdtEjeEIE5zkvtbLe44WyaDxa5KiwZikk137DL-hp9w5b2-ZjwrGqcNJrYwpTQAjHigL12EWMHKEnPEsSXqmYujeWGfB2M9_VDmSgf3J-XAZroxarSzyVuead1XNLHtLqQgT0Prh-PS1lDJ8jH5y4_JzNS6lN78BaEi-rBl-hyhXqi7ZEzGEyZVB-H9rkmCE1jnuQsHj_iWUkZFeE5wJRemTSNTxF_GqZrFTkTD68qxdtMg7nWns8pXHaqDxpWAFaONRj8JdfPCeJhQ3W9qIdugEHXFlYYtZLEuXAlBGkHQQlnL2XeZ5aYE7xDC2JYQRJBj8c5fYfusrnqBgsz4EIO5ewfwmX-OAJg2d9Pm0UVxGrXtTW1H277sVslv-2FcU32cZwwls4YthQ6fyoIVLzJTyMOYJUrpFW32r5tG425wn_Q8ezmTs90EKuVrvVo8w92JL6MDKA-orDvhvQ3beb9l7Sgc5yy9cb90rjD-lyQBgcDfJ0xHFnhjnz4S8t0yga42xeRI3r_mXd0NvRzTUHkedNMtRAdU-W382jaFGRBxXL_4YziKyewh_nGh6BlW9EQ83Qf0oSwb43IN4k6GmK6KKvwr_KiERaBougue7YpwtYyqCrEoMiEEMn-Sog4CeLzg6IuYx4awivB7VYGGGwU6Bwc2IkZkKUFxVhJK63cAwQX5Gcve_j_-WcRRGlUhI9W4RvFhQFpl0YfC3cLUzRQZfV_fWH2MIwrJm6y4VCHhnvx8O87qetR0kM7el6lY4Nrk5bNtCdBeoyy_C1sz--DjsmM-z9i9IR8PqMCZcX3gBry0Sn_js4Ka0cXPsKpM-GpR6L0CLxge1FdKNDSFUOacsiEzh3-LTu-rUUYglWzQShuc8_dtZrIEvVocirTKZ3gaImQ1M1EylwXITBxzCUW19Io1X1mxKiFpXKHtzK7AvEs0kdicMBNl1HsKSn8OH3jxwLSHI4DwFIGYBxCQ0vvG3NN5ZZ_c4OnSfQ-nojlgmeCjMGykcA9E__NgeddsOdWxnG3fVQFIiMzoJ1AtYnxHoPRbtVZdyWB3dX1L9AKxlFep77w6KS48z70KzKseRnKLa6OCPZwfXgP5kEKA7FcKwpwIaMPNxCOedtULYeDhclbLeDtjK8LA2q7a8elVyK6YRvseXaZ4-nnd7iLYLZNOv807ZLaYGm51X7aFt0YRTimfsQIGztdkY9aakmyH_XQkqPmlNa75aE4xf8FqLjwa3AZ9PcIS8EpwX_Vw_pFA0NJcvJxCBgY4Iz98FxssnBRC9dJ1aAn4Kd8lgWvHIXS974MFCCGhfI8RRVDl4S0QO7W6vrGTIZB1ngY6VHZQ1JG9NJOGtomR_8RNH98FwcPzVNUzy9AhGeKBS3WECJCxk_gKjcGB-rBogS4EU0BVCfxzCoTMJF51ufpG1k4eWlEiEpOqUYgUWAN_3XYWNhphToFLg-h1xmQWWUBiVS6tV-XVvEOgKCKp_b8dMJ_99civ11moW0s3XQpzbxo02gCBR9LQYl2OPBcoRr1bVQfmS3sljBMCgtj5NodsMpz-rIZtgbzdchFe-RE6QK4qaMwAUY0oldGd7nIW9V1C3hnGg0kekWG3JKlxMhIB3IbDAVQ4jRJ90_JbLVaj8v0cNmhAwT0QwIwuTJJYFDGM1fYrocL0UKFsHEdPGZQFnfGAeFoMQwUt3I6zpmXbIqWA0VpRYwiUwTTRNTSsH1_eX-LWUnbXBsOmr6X38Sf9SQD2giVwmji2KBw4GSfRjUsbae5gpgZZbTcXH2ZF4FK79B7kM3RW1yKHcMrT3jXyZKjfEee008n6CJraHTc2sBDtV85wr-TQgic1VgACOfee02nwbPgPGhlUsN1e1cBwTGCJiIthec58AQtsEGIsqpTwh0axbKUmUaOj7zuUjDTg0imRCdYb_iMh8ya-YUncdYTabPkBJYlnbHzCB7aXmq42akqBQTTTgVgUsrRy22Q9gn7CkGltOZRbiPZ4Oa6Uzu-CYOsK-0JcD1xUgtTd9icWNNbAg5DCHh8FhryzVmRa5VUkC81OQryM3CgKdyzyw4xSH3qw2HcCMu7VHbHYhvVEXOQQtSaedW6w1shQMbPRKt0Bf_n3DTiyvSsfAgZmA3lrhQhRzd710dzxxljzkbfYEl3Q3SKg2CNM4Pu8SzAcJj9M4WubFMqDirRgVIMgL4xthq9u4qvIGxTERgAu1h7xhUcA9f0IvKiPzBkfExW_QIYR8c9kewkGILCplgqOHbvNBtqK5uXJrnscBUm-Su8yfc3gTiWWlsb1KBm2qwj6uXOBWQ-u4xyatyltsx8AJlshq-YB-K5oJuvlwCXkeXkU3hqRM4SRwLng3VyhdL0Jr5HUv_M1ENVemAJCR1W_6IXWxbChAYiRUFVnGQMCf2Jx46eQo1sNMaO-1r1LdtVSJo4ZELftKu2X0BMQC-l9iQ5EfDT2VEPZvl5JszWbqWIlkr_RY4jwbY_OeQCkPaMxE0eywBeG5zjdTYzmPLm0YjmK5J-_7tjM_678RIQ8qyuFPuNRGFUClznKIZ-T7SYMtFie6XAQ6j3q12Mh4-zEomU1jIOcy2EzZzTVgrpmqVtZUB9wzPIsNtq27VtLz231dh2i2fAfAZHdvIy_7XQsY7-JWltkQ-fY41Dw9QOIhDb_KJHhFNH2xa3g3NGh1WxZIiJNfPXXH2pMA0xU_FnJF0uPEr2u0rEcTWqTsDgHk4krHglASUYsJYneG_YgBCHWWrGXWzbQNGYsZryPJeXNcY3hw0wO49CxV7gb56BbUNBvNIfgS6SogajoeoPTkPQAICjtAVhnrgXyIFnQ38zu9Cwjwqxy10jt04Gwm1Q6xAh_CNQwcLgtJ7elaM7zi9uEGFskPfZHF35EOhpMwR6wBoPSv0ESs8PX1_WKhYSakFyW7SewR86-W3aCDR6xznTr57lJB7BnDb9_fF6rjfysDLSjofLGwjD8qC43OlMNZB9m868hgZoCUKvSnTpVW0B2NcAoM8lgXDox6cxZPtDsW65C2fMFUmt8yqLg9MOB9QRvr8jQVvgQ75GPADaHTVbcDukGOlpWsE8qHc0y8sbWnBRwGu4lUVpyOe3R-q2Y9DVCPonQoeUt3r6EfyIPeid7GaY1S-jCTuj5GlZA4Ridz6yYYZmGXzju_OqZL9TpH14-DvywWaBu8ZUqvz9kVamnK9P_M-jTDn6iz2zy37xyEGtzWT5Mv82avznCG1l0kSoG7HPg2kdA2ngIutv3-sn-D4_H3_Wzni52iLO-5CdMjEHyo8IRF2gsHDwR0mkF5uGdXv8RD_b5KZtgMy91QfiU-h1B1OTDWxxhfSPDO00EtPBW3UPQhkMJY2_MdHzKiG6i28PRjUTIYDcQjc1RrUZFuBmD6S679gKEzKw25fKmSbk6MBIhBfV1Q0h9uX9RauUq8yFRB7mV2EQgMRzrSZd0LVqNtBcOCU7TdrpzJzk0pZkfmjIVGOAJ37T234ICX4_M28IgaNiluXWNYvW8j7k_nTy6-8uRVw30AJnkQRswmxllkn8sE8pfxq2ACMG6LhiwkUeRJU7QYz8GMhtn1HcppGw27GGLZDbd1fHQ-X8EyC_pEx6wcSKdLWOZJ-TOqBWCDHZAJJ44G9MQ_eYCZKj78LA5pooQ1OQJeno7YefrhaY7gsJEY9LqHaDBBrDYPefTlMYgHPkHKxgkT6QtpbAHN81lB5uiiN-o2HPIgI45ODYY8pmvk7SY5BVsu-lJ0K3KZJOhOsfQsoK9CWB37yZj73eFNgWO9Wd5qmmiRVbUyBrjWSXc_dLnbEAKxB08xoITcG4hDIO1TSbTIF1QsBKXbyH11lwKM9Gr3bGckU_ni5H49T8MeAx2Cce-oeZ26dj5jDGQwwwgRbDf_9eKjzVzH0MtA32QPr-ZDqwIPJlpSAIswVKI7W6-TVHeKdYjBufEUoVhjsJ2kZLNnwsgUPySarkA7PjTLxcS7L5eXTIzBWpcSqQfY6eII492F_RPgaAzRnqRW7FA0lvNcCblQJoRK80DLGM_oZajzqytR-ZgfJvWQXY5UAcW0ywx1hVklrP5H9hxJBM6LujBC-bfK2gatWTUNoo7ciIWk8WPKZf9jCnGd2s9YQhwqJfIoYWLYZj2obHw-WfedxSpLOl72ucoXM_UvtvSjnnX18plcNrQ5lkO4f23N0gh_oZhdwYeyeb1N-KADIKIdY3_6tj1AFOqN_vXTuFtEAilg5YpHC5akZeMvfOGunAVza3qucicsRDEYutxcXggArT_nUZa_j9X5lp9EItKRVyGjBvRa8VKDwoHe0Qq9JYaDk2zA0Gqz2BsXKjxS5eArOJ4t-el3UdlFrsrGz0IIM53LsVDnYFGo7G8sQWzxQHD3LqVKhumuL4q0I6gBmOZBhAzzAb-j3dE8MFDXLKOzpMXj4yY_f1BqaSVhA2LxC9FXh8xlYclwHgweVkA98obGvKfW4iMNKJza4tQ5A1QDFPDwcsF1biEPK0svQmSnHNvjhOBM_hRoZK1YD_RXmIYPWzJnULt_2Nq4Fus7QlP0m4I7qSxDSUe3Ly_RtLefBaV3G7dUa62RQJfXVKgbGQTy_64COJ89TVWD5LIEPW_LRrYvSjVlsMD7LPexlQnh6J4g3zq0uRHxcWa1bDQDUQYrQp4Ud_6qc7d7FoQqYbQgib1M_MIbRyJezKZJFNXN8aZWzAkSjR6Luk43uWgogzv_PLON19AnvbC-eLg3fE4aUvJAueCiTQGGFkBb1O2IW1kc4i8wN_II3s1TkjQ6KSvre1kN4YMOTk73lEcC6L3NcgOd-o0tPDO2O9E6I8FG4yCWmnFPjPO1FFmEnjAUSgwhEs4KdKbQwRphNPnZQ6dWsjKPVM5AfmEiLx8drX7C2NFidylmW1dpC6T9L7Qcvd2YbocFGnNv3j4ztPjt-9Z2Y4fZq-02HVNkkuOO5AB4TdPTftjgiGipnbMaBmgBNMwbxkzHuWZ-avaQfSifAvfuePdugEVjmjhcS0NQuh0_hZ-K8m0-41A-EqQ6kzgfYTwKuQ8JdIWawuYoM1Q0G1bJGpwQxG9DPDB8c6y-WupSOZ8c5l2pWsRVw7UJ47hHhFIsoDHFHVDBT9N85Y2SIRbttX2pcnKj3nw7aj6ZcTRwpNPN-Qvu8YMMjMUVV0QoIn1CEyhim0x7jqidBvcSHLamlTSqYvzDfI4l9fSA8m4Yar_VZSMYMxls278D2sxVIEjXt-fqUbXc397qGzvNniARzqZcqrataPpzQoOM-bNj5LEJJdYPqSsHioJGOkhFzWXu49UuMFYUvyNxOhrbUy8h1N6GKiGDMSwe9k9wN-5WhvfEf3wPAztWl5R4PFRf306CPhL-FW83zhBr4c1UxU56taoVNnJtsblxuTTDJr8HgIiS0bqCLpL1s-ZYOgARzAgymuZCRdaxTmK4fdFhlTs6coahCbrSXO9Iehq58t6uw55hGhAqMjVvaRn2TpgwtHS2jvGMCsLFBYnkVXeeCDwA8uIEvujo_WcIUiT7STSP1IHMyllhlhU9tb0sD8wadR8caAgHBe2CuuE6YeO4qet9JIzOLTd3kJRE9Ev7aChlmuuAElJ0o-ktfVIvUbwVAwiWV3X6AcMlmVR_6HzhwZvc64Phapf84hPMYXvnIxBSI5UbvA0X5nHU2lnqPeRlhQI0mKXvLk4Z60WTgGrJoz6mjUQNep_zG1WTSkLwk4zlLwupc492MMc-M3x-vYQBmA0J2OfXEZjnuqAQ6az1hF9SaaF87c_W-Dkd5wgzUEkoUA2kjAfLtSItyltjCzxTnH5gGs7KaeoN_9V3bj_EAquWTrF9Vdr0DyN3fVdwrjU7oZhp_CVfondyy_VQO2wtxzBICKDcgraDmcBS1Pw_VPEIXvNm0ia52zwDDo6h53kRiKECACeOLLwif-WO5IBh4DZ_DFsiuaX1dJyUUO_7vk56KjmN0QEHxaNwpvKMuPtRGOMWkRAwIKezgkGJ-GRLXbeAA_1qqT0hLDsqJUal65fXdZ_J-qEnJH9xThlPem3WrWpAYKXeVOLOCxuA-7wxyxO2DxHqJdxsvzd16aErXTcIq7OgGXL14QQXLcpQIKermnxygZf06I83xy3pkfwEY07BVX6MnouU0ybMlqeFQgsWFnP_yjPuYGA0RQGOqsL_Cz_aq94VrHtzL1M8NTQt3Jhpr_L908QQMXN7kK6CKJnDkh9Rzykak8Lig_xmz8E42bPY-RWpAgAvpju1nggo6H4oH41IfQYW2gVzTviJq9EC1rP3FtJouq9gmSH5xDo5IW09XFskxJatkvOUIjgtZhCNG_VxtML1VdSDLZSrYjMT46SO8JjWJcn__4tR6gEmTrzRE2OSjbLuZpOksXgFrOgRDsZuPSeBAE8VKVpLtHvRQKWimJumFONfHJ7JxCOaUSBzpvk88Wg9em4x7YAd_SAChQoT7XRtjlwkRszQ-TwYfGsyOOGiTyG9dzCGGy_fsTugpowfedGCGBHJpuApn7cf5NNyLsafquuDtEyUly0NDpCwF2i4Dhma5jQsDEbKOlHnq8uzAkJXRe96IQBj0FWieRJyLU-pNsgXz2PqRxNXs__iId_f1X7avOZHN7FyBa-vE-u8RuYGXuLsUtQnnA0eYesQ0hCvGHa71I5E3-w1DCu9dLeY725SC1yVZ_vJ2WJmwEPXJIXKhVgTfvw8GIEml1VGxRFvb5kMQtGbXChL1tz7Y35ux-SRoX4A23pTZVEVquaXb2QjNFOprmA0tuFeYlsUdqD82ls4R1WzgzLVRRF4Z1Jh9AFgfYHqV-7UHwJAY0OpYK9iu6PPknBPAxWsxnLxyIxQ_rRnrbD-AyW-uFhBZ5d38zkvKw68Fr24Czq84U_OlBAvHtTWSzQa_6pc6tu5KT43QDCeWwiyWt1gdahuyoqGpJNgqyD6gh5xjSr1U-ahTJpXgVjnbNBkfOWecj9GK6CMLgvcI21qVrX2IHwG9kMyQgNmu--z0VHXt0WUtEuUcHMM4PzFM5AOZ_oxSVtIbvoYGDXjUgEI-xM7BOr4e1B4n8X0aoorefQhCLe1-Lv2pKRSeUlX60RlVuRN9GkoD_UoFqz59zJwL3h2uakwjt7iehx7DeI2pHUthZL03BqsYtJth9Emw5gsDKfBIR9BAjIzbSFRnnC_pthG2E1WMRMeeKThVkL_JYkmFj4Cr1xjqXXCTAI9QFwcTqRI4ZkRgem_jqVB7H9-BzVDrqgbQoxuWhNRn3_w-xfyzv_JtRcP150_7bEN2-gbBJCexcaF-0PbkopUuQqUjE3-WYKc9X9vLWcdkEehB0F7eqzdIWqRPTsnEat4SQhSvbaOp7EgY6Ypkvjkheer3fkPelAHN86SGviWWtaxDTWMBwHQjM866tuDKWOEnLQhMb_IjQDFKHrUKUnz42saPlPWfvbas8_Ymk7bX-E263Wzb5_MWXqPHMt6UTMSOtw86MTE46YEW9Ww-WW10cmatGb4jfoQHXa_JxCRry14AjwF7CmmQLP6dnm8r4_jm8AylHV8iKCG6r6csAhY1jQ3I-24iLu01EDB6H-_bIX3uiZDXpf4T1aGBJh7I7INB-Ad7d_IV7At-qaorPyE1xvTWeFVQLymsE87ZHY0J157ggITtT95e_Q8_SEiFYg0vxg89qBpuXygL2M_Pbrb5eYTCA6K6N86CxlOvFAb2AJnhAmxe8c_KHIsFZPL6lReDGQmMPBuvdCjjLPV7seEZX30ZMTuHYXNuD7IytEJ7X1o0_04eCmcqbivHBCoQGOzDhQ86DSoX2Omx-hmQl3hI2KgKnGcnfym2Ukd-3CmHAyCDAv2kDHm38H-JdcsO2DNk9QsYtAln6XRVl5kFDnWEhm9bRh-fg9Lmt_mNkwHSwZ0YrdYhAOCMkNlukUp0EYKKhBSY8lsY7a_TPbt8vkTMSCmi2sPr7NnuyaxMvw6Jblb9OD885lSOUp3oPpoH8QPkkhYUJ4-HVmmMGD8orSe0L3k7lLbyHzz5l1EmMahHWCCbnoMGGfO2QnxV4v9YcsMmIA_NX_1CjMUh_LYKrVWE2tfmhj7Zdprbop3nTylHV6YNet5h2MVUtpfj3CFTz-7V0AxKhqmTkSE9fMv5_XY9-QxFKf9B785SPTdj1xBiOsQ0uz3TJ2CPFHOtikiqYkNu9w2cUgYejqlM0crBDpQCuFmFJCFNKrfMa7eue_4H3RSh8Yu9Yw1LXbkAuGoFMGYhegcBEvcxcDSHfZ9f1HFT7IgimpuFuoGHwaNhPnlNc1uI1ILsFeRrrXide0q3L78aMAdu7eFfSSXHm-RcZypE9LHU8caoGqd0cr8hMAFvmAacrXiUE6RtzQUZjswSOziVVwlqyszgPXIuDsA4m0AcaLyEYQ8fEsRZAg7RyRbTgMGrlo-_L1Me2JMPPbiuNi2EtBXz_85Ylbaz45KQ45mdka24ouxzs3YK5aPi-Bv-fYL7FhoIWM6AiJH5ETjucj9KrhL5u-mnEi7sYh6ttj6I-MtSpCzOLrIB5HZ-tJktRhN78f2m8h6N4FBL9ooQXR4Y-QC1MG4eRlAiugn97K-r3MDGQZR5fVwC8SPW4Pt6UDvfaxXZek0HmjYPEk63MIxeMBOLaipBGR2ziR6YsoTUZ3NOopXjZr-UsGukdLw0OIJsxA-nGjmOZCr6iDgY-EfaCAVwAOxAv47u05VBTOP1xoUhMrxNefZ1lt8hEziCDaHInMkDdc4lQVeYv6H4rR2KugX0IXGsFc-C8sfQVnALLdQNjEg8_AfTsEmY3NqE_ECIUhFwxaW8s8aWBgX97Pi8SxkCwX6DyksH9fjA76rP4P5kpWl7ynaOaCfytRliE4j5uDXXywFfwN64DWKIQt4u2gDGo9d12CWUMGrWZZdn3qn8IgEDmUdr_CGXIGcPNuS-wxWoh4G8eGNhvMk1V9zhyhcxgbjoIJLl1T9MOZZ8JQVpiy-cPgClLI2jgIbKSVZTTZ8B6T93aQj5oEbOw87RZxArjYP2XeIHMNh6JUUOND97h1D-tXlI6hlFtFTouMxLzyOpVJLfdrUcr2p0bkbNPAyk3qzxwdRWegSWH2nojJVRP5dopYDUvX3a6sXVGUefUr6llKEtyQ9W84oVESDWyhWRv6GiBkpimAlkoolaGYFYCD72gUISM-ptvaWmVvNmXdZhR2JCSn3Ec5K9TZMg0ArIgFvnJeksow6nIwDSYZ_EXqtEgn9hjLaOcKZSrixLgvGqWY5phJcyYWP7kBsJTxc9U7xCIDh_RCU8fjZzAOAl4r3DtGTEntqzqhScZ_-Fx4ygPgpi4Ko84FM0RvNQGw5VSrOWADroETQVP-La2KyDOjYo4dTauA5ArmYnXyLatcyfbnvgE5KofVhMHwPq-QSV7QAaN9aM3KdDRxBXV7YtnjPx5DzLQE_61NLQkdC0iWFjHwLwM58comkNfrKAUw3vtLzWDiLHT1nPG0pxYBn0zAid0cdOFJ3JRJl2F6-GuMSeUK6kCqbX4mtShWXp1gn0YErlKR2PFjCDNj1o56a5ejMOYAB_SNIjRLO_O7uGofXv_Om9Uevp9XKu3ca86Qt6uOpwQsifkwS6j78cGRTJeU0SlIAGBjzi6b4aJN--CpFIqF6JpuZAxhiLzsHAXRAKik3Lu6Pmb_24KBL5_ktbQRcQX6GQjGi0A4gccSOF3hdJ9j1any3RaFOA1_0HRAv-ExWoiQEyUnWALcqaC1FmXgDTxYx_VUMjeb-MqxAV4eHjJsR7e1q9cJS8qhubSQbHMH72GccTJKlZYdLBHmc0Oqejf-JKgaBMxgkGX30uCXhT9B8dag8jVrDBemQV-wak7QHgbAveaWX74ZsZZF6ZuZ6YU1llAllJlLWPVNr4aaPj_wMfurz6YyOJDnCcVxcKFjBCJRuTBF1ACh9Ye1aj5wDUVwjeKXnjEy-quQNoB5c4clujc-G-ep6-EHj6WgHZefu1HYolZNprU9zHY3T_OrisT2jDBUByHv2RajGe3K7nDZprR-e1SPApINTcKQ42Fh8SfDQsXg0qOfvMdKbfKJqQizEQiCtvkQu1oXhlO8fC4J5UkN3qsPcdG_h1TQ-_zlAPDJ97B_92zV5NkIF3XFM2iQht1oWwZdN6xwKeDRqKmpER-qz7bxiy9Hh1IxU5T_Ac5c8B5xIxbQzgTJal2t1M-_cRvGT0CjpEBjRxqts-KliiGxFl48wNePKySRiGEfnn4Xfqmy4enbmmZgyHCmo-h--qxLIxBEykrcQurpumcrK29z2_jGUNichMpAaaT3UlzgVTbOVb3gVN3Qsu8ltR1RtlO5DM_Sc6q3GQ2QpdHafa2S8Z5D_A90PuohDCpyqvS7tA24KNQEKYM2W_ONMBNNEoyU2p7hZezbbj5T_HLHVRPUiVLgugGFQkNwZ5cRgrgYqstoKu9VJWFE-odBF8G9GwHGFFqyCdBL2CADSx9AnfEssP0TSarXyn-ALo1n5f6vpUFmkcuY-4gFSang5orkODd3k7hSmsCxs5NVMLfQxPtjJcTTrKR04H7xAVNnt79YJYVW73UaXEUammc_qu0GAuNwgeaX3wIQv8ieBeqJvGbfOoXd-U6c8b2xS7b_9BCWtTKZ1A8azUrXAqOr5rXlKkq6I31ht1XzyQAWq3_YWEc8MJahqr7bR5GQqOxRg_adTocY65i1qhxebStP6XWRRurHWyHzDhi9duKfGK_eC1bbuUIevXsNDHdQBDNE8_w1BBBlg4eFuM8vSDZWJEKPxvB4Vl7ciLOs6-diW3bj_JDo1BZlpdDQFKCwDuk5RtRJmr9hGUaIbF6nrjbFduzQFh6laU7VkD_3XyqJ2C3dCD1vOOhslfiVG1fBWHpTJvKsgfLa0u94IUipo6YWCz8K-LCeOymEufdrfaI1A5qutL6tF0CaPl48rmLRMayxqTf4ZGCCDe49C74wOS_kGmxchhr8DKGUgKwiWJWQjIQLIk2PzaHSQ4cE8uBQebBsCMzlrzNr1YhYzvzhje-qorpNcwCluQeaXkqp1WST9LbExS1jN8gmJhLgS8yAOd_yGdJchugXdbfPXWD_R4oVf40bCAv3HBB3MxQKq8dZeXg_9xqr_bhwqY1oUraAHLEol6kUS--0eDJ9PzaLed1ZQ_6j-pHR-mu-OkQUvtM-THVLuNMKWGSYKcBnOFYw_1NpEkwoWtcYCzk-nq-aHJ5XnijDKutRPJQ5W6RLMmhB8qFoZpRp_aDS5LJiqp-Q4g2QhtSCckgUwHN5GSDTLaYvjkR5jeIDI0Df_tQZQv7BiusW4M-iXMunM3qpOcdAdfnBTmODqjdeBAk4dRnayZtb2Ib-JKl5ywa6WUDhpA_UQA_sIlBBbTjetvlH2sChS0D17boDPANxqPYQLorzUflL42ay1DQFsRRdnxTiNvzN3nMOxzFdIUYqWEiY29KQmAFyuERLmtWNxvUB7KB9WqxV21mbJ-yIhTsuUTHve3HdcJuWPzEtbZemmvTyJr1wckTGBWVfeT20e24dPMpBbRN24Mpx_tMxfsioxNsXFYqKHzqWqZ8Tp-gj0TUMr-dATGUJHHQ2Un1nVUYhOfB-G-cycBf8zmgcnA9EsKkTOlZY1LRmvBIknw6thweHCggBJ8Ke5N7lgYjdTTPs9HXMZk-YcGJ8Q-TkB4_Dw35xq9_hnncS-Dl-_aTs3FD-V3fAbAd9eYbttpwk9kwVnc3GzF_d-eoCntwtxNH_iYmdeBZIqLZAoDwzvFnGfVunFP4RiUtLYepxu1m7HLhPSCAQn6SNcLwGg1U0jQpfYIYGZTL3Ntq91XYv3J9vy5O1apgQZic9XEMxzOuoYf0zDEU41PaVOmGv-H-mdrmH-MI0AquibmsDkD1GoUssNDqsqGVBgMMp1kc3N6irmLeIpdrSjOLUsW8eq0YGWoMXXxp32wIfDr1fad4KV22Slqlrfv4RC2v15WxVI6j8Cn2l6ymNxCj95fk55ibBk8IgObZEwbu-O4F6focQnbqXcLMSHipxWVOo0PNAnxeG8ER8AuVaimP1nXVWhNo77VuX_Yat85m9l4Avt0Q8tR6Rpqruw0cxZRH-3GRk97-svz5QsXMJgNZsDquzmeRT7ydwFrr8NK2Ei9NmlZ4pziY4xgIjVIJgIhgkY2wEH9EBDPLuqmYrA9z2RC4KUg5aMAvhRRZ1Jrxd4uv6C7iq9o9x6AOVwA3AzuM-A42325s1cNlnURin7VjQvoDg03eXsB-G-iSEUw_WoiFatKsO1U8bW4GP1-XwaZMD2w9-NXF9JCCGp2PaYNl79WZXpoNqtOv7CS-USx0vOF6DLllVZebsUhgMTBHg6I7dmJShzC1VLrCV_XjFCVlxfSdC-HkHceCUwQwQvkH7CzkW3Xxqn9onVcL1vMKgt-D7ov_952u8jsS6gkzEkUZgSFKNUMJGZv8J1rhg-ZNUi_50EsohJTlxy8H3xw8RFN9JsTZ7T7_O2yJ-yB5bCdSHldOwfQWtPvCw0df7yzUQtkMqMY384QRdKraWO3CwhrqD5_j-iqM1nw3AKDnqvUZ_pL_MrJT5OwqvaQLlIJpSymmfw642aXt7P1TzzFnwOYb0Myjc0geBp6JKLB4MetCiKUxmYP8M3hiH8FSZLv00jUmVJj-CPVj2IVml-IiAPyPU45_2W_Sek_l6JDqxgviPNU2QfLqXLOgs7-30-8ZhrtlZLC1AYco0hIEyVvFBQC5CjorAuillJuZ02YU5_kNwGG-Avbqb2zLhjw3gO7ZB1Lz68cv8F5YVsUvCvMgRhgpr5Wj_5uFtw23HGXHKY2Ejm3Kjya_Tw1EbrPl7t-UYyUxZkF6lUh-ZnndeOB7RWVO9lDvW-kuu5XuYFbAM6ouYOPd0Am1Te__qnJe0cYwKBaqopwTCE_7cu9EH37OBm3YWyGrthggmOrcK9jSI-xA40URX30vYvyuvNzZ-0f8PrZIfTtss2f0w9om6vDpwxsWhXRlTyz9qc0ntEgVwX6t6xWklLasPIwXZpahtO8PAA9Vqy2D3t-nMSyeBaPMhkZi_k5x3ckiLR9RHH1OmiAyYkGafn1_aB381MKMv_8AS4YGzeAvaHBwwfNDBlPpBhdupAGXoGPKFCM6d5W1QoDhwQyIZ9uFKuvoPtxntY8MwG5x-Vwmg3GhIDiSmoybRNIpfIqXUVzg5_a9p9b0-Go59h9B1ntMB0K1Q0X1EtZq-tVRlv1MRpSjOl8LFyGFQ8rYS0aY54cZgE_tdOaozg5NuXDJPQR515WrBf6NyJ2E66D3u1Fde7hd-zUMSiASQXMKwCLOAMNn4f3MWoj6UR3vKPjtBNwF1umNrE8P1tErywv40kYGz8-Zy5Jub9dMgKEfXbz1s6XIqZJEDSXngwVYNQx2fhaO-uGxt-eahjkVAkt1KoTe3sDxtkX7CFQNAaVBlsy4JEqRM1-Mxg0GfAP6M5l6MMhbqkJoN4oC4TVUlASghOUHqkCorULtgKctw01Ea9UnPzXz-KKpA4RllrWdUryiRH2A5RPs3KH6mTKVjJmzXvs-tHHeQphSLLm3QV1smoj9Z-oAJrz0C-f_Y0LE4Rsaw8Ag_7G9OOrBOD1odrNT2PbpvyeMCv2179maxKeUB3WRIU_Mz8b4_vi76gODzX6t-K5zDm1ukMlpNLfRtD2FZOEu2S9dGFFy-Ut3gB8Vnu_b1wnzETDDqWZJ-6bo9qRxrRAkH6q3TF5VTKv_hnYKY6QzcmotJrdTNPQvwCztcqj4c45FtJyax2tdOQo4lhoqDapMA9TawQMxunVToG8YmNP1YKJljFq-ZFttAxcnIpaTYq9scd3cfS0S63cnjaMT_H_LEBW9FedIR53Ko12fyQn9cLgErigUWMWwgdTmE2rPo3ygRky06cEcrh6zUtNb5E0Xt8FnmR0n53wZbJHsX9N6ficGSVwanB9ZBGJz5TmRHdF2aE6NrALFCVLZ_9mUP0XVz9HSUH9YbauXqYM8afLJ_R8XNm1WtqX6gWkCG4HulNtWURyTWgVuQT4jiB392QSDulnwnUnaFiroMxbHD6UENVgg78icspfeRQ3I_wEKLpCmngQSDvgNlV-vzVct_920i-n6DSDav6Ez6MgxCa0cgrF5Fbzak-koA7olgU2xqiyoAFv02H76alrTcE6Ooi0zNIBABz8McKSqmJDhJ3RTpCYQCmJ71Xq3xdeT-9-WBX9QgNEGQ9BAcZNT8IHY7yUocfYNOQS3XbCogSc0HR260BC8-8ijyyx1RfZB2kErTGpUCo3FQJLg8QNYU4cThUe1rmgzC1aJSHdYD8OLKHflJCHZiGGaYW_MA-tBWfHiEISIUcIghjbVjF2dBoMZBW5hlzvYWOV5y1QXW0zvTJ1Tw4R6kJGWNTK4wePkrh9W3t4wMu2QvyJQLGGwb4ltSDWefD44MtkWdfquG7OTbXqEiPr2KreJ2j3DASXuBDBD25RvlZc4bhLHFj9BUJ-lulsAvDWKCb2Bou0i6akOancevmmSZUwphs-hQM2b3ugNTsgsUEoF82dXWCJ70gyr1RFBfBsZCYDMDWbiqMYC221y5Pw2zoHRdQ40xDVCmTzDZZxzBr3ywIcE0Y_6c9tlm4e6EgOkdHg5KaAV9sV_uMLbBeSxyihQgJuxA4dzQnCo3Q_owAGtnkvhQp4UgYlx2AeclHenpTuFb_t-BsO1-DV6LgRplzfXH7ocQedgUXsd-gZtA61tnwNR2qRk9dbmtOikjI7qf7tFv8r0pRbe_d_mNadmgformlLzAtUn87xkZLmcMx_iH0g7gW7gbEXnkKmX9syage0xeQ12qnGvGF-p6mBKFUM7d_8ZBFt3pSd0M2Wl1zLnK9HQJVPXjWWBf8r9UecYdpyhtZAnxREWSqG1APYDP8cPpQcewy_QaCnVqyYZRFkf6X6ch-O9sJAwzR4MLElaZ31KyCxHTj8565hGC5bJUdg_I91UgH2yJArG54y_Yc5Dl6ALUn9QgPzbqDFFUOJjwU5o9uD2XyEBYzEErekT-GqxtSGOgCFSStNay_o8OmjolNWZVRc1_aFeMUOgh_GJCAnBMs8AVNU8rG-2bL8Yn_08Lfn-QpqpZIZIVsTZinG9cCIy-nuGGUtwHtPdG8xntWD7d5rNUtro9BCoxdrnbFOkSAwCQ365HHDHG-D0bnxTd70UQLYZcAb6rkxFrENHGBQFl5f1sOWZnGhofb6snJCirTWsgJcst54Dzu14XaX-57i-J3gi6pI0alrVQhxukhTtV3oj42A2TUGD6Qb2P_PjwhVbwpyfkd9tNTRT4YKbB6v7FviTl7JKRh_lMFAeLiNc10auLFBnXOdq28pbt64ilr05QoEABo-2qj0w1qRgK1RfdC_x2WRHcrI7zWIyDONsyqumIklidGqrEh8EXCSg3a1PBLMIrUfkfyV8C7LvTL_lifHl18bZO1BJtoksrMcCmPiwEJhCCMn1olm_DSh1YHahgEFrP9PhmLrFpJrymDuzXlWENX0QfqD8_bsiaIC7sqi4ZCnGI-KCnePmdiATIkO1ROI0ty_1kRce2LFztuwYFLY_z1yJlFflviLtyjU2z3F8Dl5JjO2dWm4n7bBCRT8wAqp5eztDZdaiuQUZKi9vhIuEnqFpL5zQVTUlDpMWodeYlcEZT0pQQamulicCkRslA7Z-CThZgOW3QWCv3eYTvOlZ0merHzQFxYq-8S_0rfwK9BEA1xck28GdMIXUd5cqBN1kUPd06qbwbCAgVBABucXvWbmkCeokCXOyfxb2BHl7381ZWy3_U6M0AnKzxhtYBSmBjY8sQAeJg1WTQ0ZpbMT651_b8ipPHAUl57j9rwVzxrdtmtai0VoUVNv4UEF6gDR_byb09xWMXgCWHrBMbbs7KNNC307cI7lmSHDwFDiWjxXcZtGMCix71kfh6uZsRBursMcnUoIaGvd_Pqv7SKeo3c1DXs8d4yraU5VqtmvHuodSmfcmOCEkzLb4lmVfBZPrsJQcLb9xFH8wunqxWYhr2ERzOJDZoLIKNwQnPDcxoK7UX_tLfbHKAO_CcfHWRgB_NkcPVvf8jViQRTrskD_19WqQFq241yN8yW4a61C6v-9og8yJyy8BWPQdiKESA180YGsfujYRx40jXR1u0g-WgRF35S97vOzm963EAkAmfCPBpRckAFxeDcb9DfBvhihOeaQEobt9UNhiDTNaiSN_Hl66wA5DIPIptw0_HQQLoVQ6HUevZymcwe9A5p7_AdCf86KBN-Z6cu7-5OTmctbwROcfjMYjlJLXI4vSE1fY_BdaYPBvPWsGaPKTNr9kwy0RyDrYd4a3hzDBzEOAGUJm14pdaOSbjtwoIJ0m5TeQRm-e-EBqxv4dcABhod1agzhWgyKZarIrtkDhGW7dkDqSdxHzPCxphtD1a7SD2MdKfz0IK_IkPRSr5N690e9kBMO8r0MmuMg85Jf4vA3w3-ywnIbaW865qXxkW-3CYgJ8RloGuBcJewQH13Ozoz1FAlt1Gt5Q-uHiMokLpmbCmvGVk7xPXqDu_sqRhQSjlEXRBjmGzeotBxxhTwmzqZfJxRXEdmGAtrfqva6gzYGgSdXFWo-_wfN2-DjBa1Z8FAxpmT-dRPNvaKwOmknS-tI5xi2i7kzmh-oIn8n-AJ6WanEBaFc5vTC9SnQNxnjnnbTu-bRMj_KlXXpw-ryvlGEGhdMOqfcgSWzQLPBSVMJpDU9rSZMfGl77Q-S3q9mRfjPnd6TqlNfOskpiQijqlKNvhC_D2S8SerwBOrWTSZ2i0W2NKgtAvkgn1v7wHkNIp6iJ9CU0mXIobg1uDrdvReirxIxuznqXyf9xma99oqKmQvh4dWfhlQH-a8AB1Hl624CTjEs4CcoZfCm2pMpcDie4gVvQiGkHQosnTdOA12IX3REq8peIyawJpoyI50ConQxCFuWqKfZkxvaLMfVAHcpvRNrNEF-jD1lf6R1emRB8jW6iQLCKYVueF6qfUsmb6Ql-gmKcakkB71QGMSGTa91eBg--S11MB79NFQdZhQDpYYc5GAAKTR3PF9Cj-xk_33qn0Xz3Xw5jRTZqm-qVcqPMwcdxcB9p8JhtWuhGcfyGmON9hM83JHg8xKGUn-1qPOnvF1yWoRcI6wv7Xe3jfo-_RHLEwbPTbihfw2H6ycYxEl_iz9zlG40_WNJwwWDdHn-jsau08fNxdR4WC9FEvC7lRAUeQPVxUWE3ziJjlDMeZGz2jy4daSi-LY-QZCzarHtQ4_olBcW11Q8gtV0lOBrkATxbd7YRAL7_dh54Xw9T6X0O7TlpofzzAVMZzIn0iTai8k0eAzuj3DT2FiCHAh4-RbKHr7mzyrPQ0MUmJp2PomCnzG25BUbYSlClBcjtotLGm6YuDPzB5X7Lu_vH9eRjxMEh7ZqIYO6m81D0dwZO9aVZSSwa_LBb1iBFrHijTsL8rHXXcBSnp_jIaZrGLyKkxMaJDegmLd8HdgACP3rOqVCDg1n_CVE3_jRaqwwHJVpani_j77aSGBmItjp7HqbcgZr_CVMCBHX3XfzlhuXZkvBoc8ZaYYifhvgGFGEg0jHEaxIIU0QDqm2L6dHqCH6yAlkkT8zRgWeLH4Pey8nR2KTAZP55YtaaU38cUPOqVlvTmPihzfNHH18h0vLfaPPjA712C9V3hvVACSpU5SsXQU7NfnnIO7_5ZcX-iCaEuDsSFlJcAJFaSyKJh5kcXsGdRCAM5nVfyH6_NFHzGiNWaIqc-E3Yl4a4pS07bpe74bsEUrxUfdgmY9XULfNwuGPVg4qBsSoS8coVBn5SxwVR6OITKjr8Iq6b8EZZxxc6qJJe2Xd5mExe6NxAW3sClorNhS_wwcBYwj6HUH8SmXpZ0xqADYVqky8bn-pa5j6RFNSH5zz9deI4_1ioLhkVtvpbRFHOxCPzm56wjqQnEci9QQd8axmpiKgHP8HnpTzLHO2MgqjjunSox4sXOz_BEEPWghInV_VpmFb0KN0B4UH_M0f9Yar4O1unjCGwlLF_ZfLfNfwmi8JoDRMYIyFn6D1PxQgdBBPKN0oC_Z11E28WQqTORvTJqusVY4qoZ4d1FOkd5E9srOWuvs0gBGweaIzUAZHdRGr4NygezGmf27uWSos68ZHaB2qOc79z_TpsXiVeik5uT-pSbt2R-GEIeg8cwCH1J2u7UHsWLmJFyUmBW3K372QeHxoW8UKinTNg4Zy6uF5acVZmom5E8s957-83Qcs_unrHFoUTPy_KWoiqRefrQcpmCHra-JYSYwNxfwgzoCp-EHgl2ypCIZ5BpRQHgKweWJWeRhioSBwGejT7evYEl3-L_FazZFY5W6tKyXFktO2jIySP0NMGxFL8S-PWQERH9cdm7l1KN849iSIqeMI8cROEUCWjUIhdh9pXJnY8vYhQBfbEjJ2fJFjOEtT8ARZe1jBPNUFdoRph8YXVXRkHn0uw826uIzZGnacbNgRwgNdilq-j1Rj5iirOQwXSQ1s_L2Y2Gl8O7YZ_tuEek0ovZnebzesmYKtoY_XhunbD_U-4afK57BtBTsmm1Ed_AwfhZNV_vqKC5DraEE6c6J_7d1f3NJEMVK-QDm-iMLGdLHjOr3bf8TjpeXNjITXiBZ0kJBb_qf7Y6Sze1UueGWd_23NVi5Ufe8w--C9fE3YT0Hl0wnSRJ1WvOGlLQf2Hgk8KaazMuCVbkNFzjojCQ_IrmsEz2sbWOSMDB_E2y-6JJyET54mCpfMYhdHXVhtbAH0sdBNtp2KGfh9206nOJU-lKwjo71lgNm4XoWV5Ux1LXYSeN9r7BSrpirkFIqxyQkJez9Ulcbiz5ES5t8oaTwCOnIDE28Vy324HhGPSi5W2QPkCOV_PjOWCeM8yjS_6w_FnGuO_26ecaOEkCNBZung5p0pHSmD9D0SeQ55YvwYvwMhT3smiwDo9dRcFa6sigkWHHKtBLW29sYLB4r5pNWtHd6CihJCcG9DTTbaE5qP0-eOF1l4GKEhtIUKDPGJGwEzYHjq9emeIy1uacdIcWTCJylvCVOHdWmLaD1HefI1tjSyga1LuX-uZPAYEu4H3BHd_8RhEhTIIR2W1Zi4pcy___Mg6UnxiELbieUU9M-kBKnEG8wm1_VCAJVg6GulXQG20z5Zq0Zr8HsRUEpcO6ULm-_3zF1WYWSPU-JDi_ZiKxGdLOidzU4gb-zzrrLYtA2USFwdncVimCESLHhKPSvv6r2xX5Hz0eTuLmhshN4wL2du7QNz_mLVnI0aIGrHWQgs_DEy06L1P4ANm_Y-0xdzookmfICUGKChRsnNFH5Ardfg5JWwzC_jQrW1XM_t8g-3Hnv_A-UzUyJWBl3ezae1NPikowsbMsIwLuHHteDmQmqb9-93yiUdXB9FxycWFgaPksF17KxTvI8FS2PPwZKsSOTXMQNCQyFd4fJDR60nQhm19DhQImTl_QPvqibTAg_p5zlhxlEFdMKoMEdSrqovWF0mKoOLbIHlGum-tDlq2Ll96PE2-CrnW8NyHVDdew8iZSZ5dahyl3prZnh_EiRB8nNBESy8uH9ppuSH6XlQ0TJXdhwI1ZdOJvFonZ-7IBR1TVb4ynvpzRt-oWE-tNx1-6qwSJGzrsKnn1EYkDQaRj7nfztiOa9af0LGUR5ejBaZVx-bQ-75PO-xBTxd0UpI5kyaEf9T3rUM19GzASEzvIwPCPRplhpopMmPORqBqg1oFxqI9vzahfzntnYmWEBLGc2ks1NZWq1gLcSZLw947_EEGgyqw51cFGXLaB1DeA85qa6WT1jRmS4Fjj747XLPynyNH73NU8RWsx03F0y_fvUpPGS_vaXWR8AhEy-gdBW5CCYbsPv7WB1Ls0_DJMBSHylHgNQvC_5knHobolZyERyyye0rwmLca0TnAJS0QhgywEwaoateT_H3_aqypXAFQdqP9aXzDLINETQH-jPND97CG-mhA5bh_mmulEvQMxHyt1e4d2IWPOJjYUvSj1gaxoNl8C_v-h8719rmYl7e5jedHHzYQuDgq-i4B8HlQxgLycD2vQqtt9F8fadudBvjaa4qaHQNw_AZc_8aWNUQ23FdSfC2ZSwJvYASGSz5iwwZotTwF92WMyzfnNvdjFyluEZR4D2RXnYP9GUuwGcg6LvtzjZDq4GoOG8cZEqgSQpSUFWN4-NUVBrb8GLY-SDo08tW7Q42PvN8h6h6cPCpFgrKFrqEuNupBiw_GvD-Ihj6S81070U74EpW3yin5jY5dVGJO_Q-8GBVsyfe9VyPGlDCt9p2-FwvgP6aMZnWAQys5HjDo7QxHaLXAUAJEB4HJatbd3sDYsC3S3Py-_NDzA9_JuOI4iqvOjwf96mS8xfOkoDY0CyKso6cn7BWBDbtgGL5yjjAOrsgyRzALWaUehhq0p48D45hMtJh40lBfgA2QkEqXaqlFdooXKlfyn0nePdsQPYJWxg4O42Up_ha9yeggy_bdTtWJQlR1bpgphhsDFFhPq3rrrD54e-AmMPvLS_KnhRHR22d8t80bo2yhrXzT612iv6Z_2_wxWbm8AnUB1L4t1pnI0BW9MLhU0EC55f52wZCJQ8wJdRcH4lbuUsZ4ioBA8J6X-UtP7YjjBTeXITfvyCaLvkwGseuU4DCiTHh6mkqIq6ynzsg9kXqjCB7oDfO8yZm82JEuzLWaReeZSub0J4FAyCUQImgs3Ui1shcwK6IVbk57-Gjywva17R7qQhkYxqeDCbrd64y3QLFBnhiYSN4TrR5AaPiNz3eCYFYPTdMjNCWa7HMb8wgI8Bix513uKuS7HenMc_h1QwCzrD146GKiiEZ0LT2IIDDO8h_gKx3Y-7N5B9Og7wjsDps624fXnr889NYznFOBwuVhNmT4aULq_L32VNXYO7bvGEm8T__RrBnigqlftf0nHzP2U7gN3kKnuCg0VryDRRs30No9mmIxpCzEkGfEDb3g8SxDiiyOjZEuFTG-doTdRDPfe8DqiPTfJdFWRfDkBKFbpnV46-Dy1PKe1HdpoF82ggBjtwT6N3GZ4MPq1UVYQ6aiwlk-vUpetZHohzn1AD15XlDE_NfnZHhvGrHGApPPUFCMmZRmqQTkNH4IEpUDQM4_SacoAIdkrgHO7PoUAFoHYMpumQ2pow4VTR3mj0tpvG-iIBbcxvqc5XLQQZhXuhDVAEl3p8HPTDKqFgxTxiKT_Ns2pfkp7zHS9-Qp6VzlZgoa1Kt-ipc-BOpwBzzeDqg5bOYvDF4mySuTfNy7RnMfX2F0WZKN0j0Rbo99iNUgkvxQNTAsicaZGuGWaUbgiQI5OT_kltLhbL0Lwk4AQpgKHQ0OBgIYC7ONSWNWlHqRTR0CGRYRPPB5tOfzJ9iVeKQKgTnH-PTukqdsxJyrwalRgF9I_b3qBXCFeY7Ea1JyqYhi2c1OLLoI8UJ1kNsH9Jsuww0WjthK7U5KQEHkQTZSjdEyoD3M-daQhocYGcPqRLqt_kfDWpA9fQYJVlMCUL9aQuMdYVz0ZzZwV4PhAoqep2MwxErhdjEUPhqyt4mVopZW-Zyigqpw7ef5K8lrBvtfLV3rt0hFTzuxACp1wQOWVsYvY36I0Yff9iHGHaOArfsR0KgDgbNK7E7D5CtFrHyOn5XGjWcdjLaYKvCJ8wKrIItOXpWEMxBCcKsKsj3bo_jJKiKYS5hVeaznfwc7pi0J21-4BAkb9Vs4XqIcooEFbUlqFSxWMuBokQAsxBEdeZ4ZEWbD_jZdx8NxELKLxPuKiYYmaljKyW4NqhyeGPgFxeHV7PC8fZ5O1Zg2sTMkW7J_BkZte3oGa9zeENRYMYmVp90gURGZ9vex7-GM362BBH-Uq9w9XYGL_yVfylRVU2PGoCEmMoxqgxsYTt6t--noIEO67jMxWhOdX-i2bLo4xdZnTBBDiiCwDLBM4SS5FWv9Q1b5NO8GL9ePjw0PEowJy6Lhq1MEBrQSR_AiNr7tAQPoJc-ltUMtBCn0FrDKT8UZchBVaMPazNXHJyJB__MZfJLc36Pr3xI3YG7C7plb4MOzJ2UU7knbHbcGM8WqKykYOBlde91ywezS-WEo8EUTO9rVUTDPwSPH2NjnuFnu9cEAmXYicqip9J5WLcnWxKuo51O53VaSXa3KOwkRsh86PPoxbN_6boEBx2b78eQOgVrE8T52OD8SryaCcj7GmHsA-nLWXhAZ98WTCCR_O3N3JZSMDB8NNKaTdyjILTThzcZBAMHpCZteh3JxXO2kiw9Q53cCVt-PNAVFwgANiyFFW00sGKI1VxK2SqsCXupmVQqzwJ_VN_KyQfh56xgMWxEucdcbneMoOWUzDZduKIBBhM3BiiaidHeflnpuDid8poBugQVdxNZdxxi27cdV7h0ieu0WAJj5G4DjNY5XI-S3cilYnTXUNg3nE4kQb6jVsjVPKwS7sur3AvwPld2qHJD5Zo5_63axnH-FQuiA2oF7pZxoYiz4IYY94ydG8gOOYteoiwEDD4tDi9_p-Vh19qsJ8NyAaC3sO1mKZUhLpGX4W5vXI9bONL6KfiZtpGsNOS0al73DiqdLiFtAcp68geOr3ym7Miq2xtthT-mCiNOn4HugT-rogZbzPlRK3aHEY3MsLL2BBcPue8ffnazWOosLQuThIGdGwHxSHwk9crZito6H3rfhy5FQYRZELbjkp6XwSzWqwGNh5PvS3a4WxLOImjdS_SdeFFztTbz643sos675Aodwntlo8e97352Zl54dJVBWQQQXZe92VNcHdywcaHzSA2NyLRWz9kJA4R4jHUBq0Kd_y-f_4LZMgcnSJyB_kxotskTdJvy8K4VSB7NSgMxkfzv-DWokMaWuZ6i9lhG6laXjt8SzVmZnBXx2fcGgveBZ0cEEy_ZAjwSaqkircbn6rIcmwjOLxsSvcyHHaB4371u2OZzhoM1eRQ6I_wXHJP2FW4zESJYPOhSWtJ6Apz4rHoUnlDCcg1MnT3Q6PvRNDq0jB26NCCl4ixvXlWtuWTa6_bXBARoDauSXsf9YAX-vnSTK2lOz0pOWgz_QjQw0Lx7nEi4sMXdnGvQNxkSiGAmExZzqAPZwMGbdAJUnjc0jW7Fi28MG3G8cHvO6fcGMo-IHUlH1hr7vMVCViYqjcZQOJ6YgAQNQNe6mXCcsSJij3_AeMXOJvC55N2l9GkRBkByX7-NO0zWRMGZdtYxe-25RMM46v4AZi3A2mH-31HphZ34kIlBH9yb-8Vw4cdUHpY42kEhnXusSk0gx_bGxqJRVVpVgo0EAAAkhSRkWSqJiccp5iZ1yZ2EpHOgEM1vthLyCualal7K-fTHBm5jSjNqNNiZ85xJF3tbnHSjLNdQ-sYcUnhDFedPfS1bzfVZrJBfzjp9_itNRPeJnHhYGe-K9d5TQqjrBAtwrGnMkGhpegfK6Ac2Nklvcl-yCdX0Fx_OYe6peI4slr4S9XmZBj3ZpG7PX4NdyAKDu0GwufKIcSATJlFk-1L17vj-b54H5iFj5472wPjh-E9NJ2UWS5GbEC8TPpqw5wQH_Q4KnOIE03lgzCcImIKW4jK52uCSsBljKI5CXQzgTj2lR2lf7OqqEwyuFP6KEm4Gbd98fASaqrgFmR3CBqJfFkaIeuluglEt6hbkIQU4KlhVJ1kwkOq23gcjyxC4TXYEBNake_62MYh17xz5yxky34x6cl8B-e14KXqOG5qG5ug3gsoD334ICr72xkt-m3mICgkUYOSBE83pb2AA7YuW5IqwTLStyt03wQhYmDXd_q4FBM7ZO-uwue_cT49vvpDHBAL7zwG9if6P_wwVVqO85qFfri0-S37JXpakkJ6_9SUpM18Yo4g2SbEoFLE_psEgmhRAVyGZjGMCU2Yb2Nh6eQaVhuiciWgij3Hf69IJYKZ7dgNmCuuTMp_VlJ0_bDWGlAQZUvZoXemSxVUvOEMjNj0JxhAnuo6Pi9eWLcpy018a71RUAcCrdI6NLvPBNr6qYJgZL2YE6lLe5kN2xxuxtNIm0PdkyvAo9N0OGwXOkQcY8KxwwhBPI01FGQ1ULM51ICIEBERqQD5-RkIAICNR6o8zZD-6Iqah6mvg2OOhpEWzyTuIV6y3d_hOKpYtdPZ0tYpmGdXjl0CM6UZmUyAxk43Frunx0UQg3pA_Awwu5YhXCPek64_gbjQve8bn5Dxl6ZAvBAk85VngWQNtjH4JNk2GABmghnZr2ZHWhO_GX-q3KKTyOqbUjACY1il-tUhIs0TkcQqrYLRMXRrSACeDKw1VWm6iTI_6IYfcUGs_H1Y0fgyCSI3lq3495MNy-dbp-G5WiAQCZI_mqzoxTcr0EifYsDKQuzpSs4e6e4beFerRgJmLVr9Jgo9heM988Va39i0Vo0AEIPlaZqLXrAz--eT1xxSdBi6JlxKS2uzYsl800ySl66rIKPUoXdkVni_F_20mmkwEGCAQ4ZJS1g52aDOSjCYPuP4nUfCCL1868DyocogHBIwr7PCQ4-_0e7rKflnzCoPtETbNRKJj55oRaiAlFdqaTWWSMp_LjH7w0GFXxzTtnuur3GA3QaeaCO9bIPf-kiFhBArunZ4iY6SdxqV2bu3ANgoc35zfPy7r4wZDnS2BfHFn6KXRHhns5yN5U-OVjT2pIBWbLxQj8J8TOrSGYkpcTwJ526XWPKA03qIn2pOEe4wUDkW0tkxyyIgt5cCjSPWhhQQLsYYKJ8rk2ojWvIHSdHSgIof0eVI51RGCW4jcg2pJ3I25sFIfpgqI5QipxB75eTIB32XCBtzWmK2E6dPAQfnHNPYITbjLmOrH2f6zbW1_LJ3LVtMMijseSomNhA0v4KUEBy5aOriMgwBRc2doCITBcWz0OD6TCXbcrNvW7g6BDK67Ym4Vpn6bl3B4tIH19TNQB4YhX4z2kAyhlOOlvwqMcfhtdiNxuSZ7BAqQYixn5dDpswpCqiI_MjH51TMikt-YBBCHTr-RGRIXaWxk2sTl01agDUdyWGJ8wsP1f0ndpLm3fHdejNab0MOn6osZGpP3ZgZIYoX0o7CoF_5lVDdc08Dt7L_yEmzk4ccF-JQ0JtbfYdzvc4OrUBm3zQfNVsdw_AQHE0H8y3wolZFgsPzAOF39j-_9SDKkZQAHkO42MKEBuDYNRANGd41ztyybua00Dn8XEYC7OiWofp6CNgeFts0oXhYM7YU-0A8h4n_xVYrk-0Rb-zpprX3pmPsLySXIDR0EBHRdi54BjFeutO1ODlZUI0JXKinpc3TEq1Q8Umhk5Yid-CmzYfaVtt65hsdKIybzDgZkBSqOZHNlU-qgtHZsZjB7HhlsQH_hsJMfO_GDYmvUyL61zZ_6i-kzVl9kQzarBALNWbFaReiu2SG9cY4n8raKYyXQxQXE31wFUrKaibEAXJlq26xQzmZmf12t4-3ZVxMi15PRbREWLYGzqNRARqU3mHd3_FPTeaLxcWy-KfufvSTVOIYkKoAXAbHfGckSZgQMlCPqKvao0Lss7N3bdcI04kJRmOcExYhAXvepyznGreKpfwWLm2YpoPgFuWq2cbkOg_KNOxeI-SCe8WL5geA7u7S-PPZZ89jarsvO7kPAIQXxHg7a46y9wzDLclZD7UcECTva6MEKRlMP5zsg4EfRkmZ8AQcykymQikio50dvSITkyqtD5XLkLYv2eypab6-1CHu3z-YUQSHYLOw4fsU6dR8lToK4I4pl9auL2j4z2FqwZTt-wnGkTXTevikprpz7BBaY78BYmJHquSGjIEoy59aBoFNWsKLhyB7r-JFAVRXgZAspE59-JmzJVSIfyNWXThYFzabEXW2VmUNRAcb2pRUP7KYWY8xqgZTvQZ2mtXQBY4GpAoXR6jgH-fmWg988kAQBxRnDoZgb0VqOUNQK29C5BIEt8CsHE97YSouTsqqGtATh9YQUinkIpjyHMAYRfnkMiywoFYeaJdEd4DFPIvJ_MmDWtg43nh4dbJahewqSfAzmFH1B-js9WAG7bivifCkEFdHfWcyDybAKICp2iZ4clqNYH9EoSgYJuDnUoyHrBvhWbaG4CZFi6bALdp68fj_7D6MCId76bo2D47SRj-q6bzrQFHvrbfK86EdM5KbJftG9ieNvuE7PjAEAheezl1fxBBKKZDCnxPzovqnmBX3mnEy_giFlxpBfUm7g0ot-FrszjXCMAcw4PNQchogsmtV8zQ8XZOo2Rlay3YmS9-nK2Z1jEBXckY8C8y2IavccKdbWAOUidl9LsHe0wLA0tC0YcAQH5HF1yfqhXeaUXmVA1tF7vJW6tBMsm443zWLqD3MvCjC6DoUb1O6IMaeSwvS7spYGuleZPr4OvXuWcylIBgHS8TlIwoo4P1zBFAlYOYCGsulS8TBKmLxOWskPS-grktYEBBK-uDxU9pVaKCMWy_l_LV8-r3z2HRajh54V3cEsSiG5CF5_EVeFJzAzQTGd79k-AjLERnGw7kNMs4LWMhPS-00_R3nRt_OPxiVnSY_vNyT3HHpf8Lf7NQnZQQ7jM6d3BBSmIUlvlECPBpaVgP6oc1FKSkSPs-6DGL-DkJW3Xo0WlcJKwl7rIXjCrM0t6n3ioRNkxBOg3grZKqF12fnWOn-jtqr0V0Iw4Lf-3Gh007OcyCIy1-RENp6DXM8JKsg1XwQTo7OfDfyf3ZSDWOLan4L6hrHPXKBKtk0m1fJvJQ9dwEM3jzPWJBilBQDI_09Nr2MCbLzNTGi2wzGMlMt4B8u7g6B5wmRWKDZchS0pSFgP8B6maEEZ8JH-c6p7wk6YfeMEC2Ih-KN9IEUvnsh-b6jj0FwcqtpWKlHBJFWJtGnXMT8rDuYX5Mm_-lAWornFLriTA8I9uu1ZOGiej0pWVgoQVWFawXYkYuoZRW5q4OGBwpiPtZIYAyDoZeAUOu7FAqrTBA2NfYfJr9vsXJOaDiYPDHRgf9IPb4xQHM0YSgpvkCDTERAkFVgQ0lLemlf2qcUXjgmQg2MNuI1NcMCu9A9o8-g15M6Sswsu2uLf8PD13MAUsf2bSudfdKaViZvkMCJ-VgQKsy2y-9J6nybC5tzJ9S3yfnlqMyHkbrxFAUf7NnocSzZcRtuRUpuGZsx20gb8xHIA7aUuwd41zsDvsOUpovILruvtFXnA2_18wbHXFKUGmKPHYYGLsz3rhJNtjs0dZF8EDD2XVmxsow3EHn4CXSQkJ8x3D5sDdyQE74fx_9l-BybhGK0-Ww_qLjHwwArVN6GcDacya-onH823CihgmmZKN3bg_XP0Q1c37IUApEO-R6ywQpAOWGv_re4uecj_1jmbBAxwRcvCNpNSwoGTm8_KSozpV6-vadvp_RC3TDHkH7f97yLxJ7ROIt5J8cQl-9eNJBHtVvWv0H0oe8V42gg4FsXB7_Fv8Ou9YUFWaJYb7FVU3IyWGVNYJyPoT662ImG2kQQHTzoNdHPdqTT_kh421XyfaJINAHA3KzKTcOq_4uNp3hq158xepsHM8HLizQKPI_oM3qvpSMxj-BuMVfkDGTnsX-JLAe3NA8yuFiZXyziuYw6hC4rMLuV5UTNJZnGS-3EEGSXXHCfghBQslnMt4jDj1X9FYwL8cJCmPPC9sEgpCfBdPYZCJUjoxwd2i4Nd2vweECi1KOOoFCdmTcDcp6WmlQxv06XLgfCiyC50yBmqw034Ukq2IsrYFPDsITQIQG_HBAe6k-2dxanLxJGlZK6CPCx2MKGElRlIESSqa99pCuUgzdvs-_ZbG-fjr42LTHtP0hHJy_ngCjrt8IgDmUKI3xEvlXZRnxnp4jkH-7FwZoKkh01DjFYkAscw5BjAlcWFqgQFnqle20OyaUTMaYIvjf-0ZUOpGi_wab0RYW1i5s61xvKyIk_2evZ87LyS57WccbcLy88MJ26kRxPMf9rOcEetd1aZxykk73d7A_pj7zxIrvjeExHyxUrM0XFgLN79kvoEAhyhFdZ_FZItdc98yLjaToxZPORBhTn1w0nj4spz5FjshbItFfVLfGCsAxgxRI88AO2oB8389PNPMe8tA4uMPMC2PFTqK795Hek8Vos_khmzeiXwo1BQaVfwLglOeKhUBAuoVvCyh93vTjhapy14oMAt24rP1eeHnQjee5Lfb_8p3gXOMQ39yxQ0Ts32B-CfxQzbPQrRQtJls8Y6lVDr0oOFz1gMHDWRrzA5z3tqHpj0Cxe3R1luIIQ06DHrv73dswQFCY6mYUsMfumIz3WAO0sa7s8fzbGRpG4zcA5_zxQpkwOEmTbBf8n_7vCRaS3weOMVJBuNSJCiQGBHR2eESoSSbV_ESxcoPGf-Wz_Fam4chWBty66ZX9gMqaAE1zWKAGMEF9zlemaUpKjF_NQJkTSbvh94a6Rtr-WR9QhWFzNxPBPIxItxGb5yNTiGZ6Ie-tQJE2Kyd1SmcfUY5fJnCdItfpnyXL4WSAbSsob9XVg4Op0uBGG4yXL__kme-X8WI0wABAACDV6iueeDk3PptXUV0BSR3PCdB9sa2FWGoPt81rhXS1voD5ApICH0CYlLLFnsnBNNi0fB0f7ZKC8y4286yDEl0NhkKDvq2n9HkwBGA_oiFOcGotvk5QXufiP82pBzLwQOow95Fx6OM7HK_uPVjzxxdawXQgSdHoQiMJwbUK2UYbfr0iYvGr8ERELWRTOOiBcZYsSsNhYHMvwVW5ahDFqpCiW8JJOq6gjlJmZ3cvwVWD7kgLmJXMnnRqtqaYl9Uk0EBEw6CZI8R0Fprd4sn-AM5SIgL6PkVm0AsR9FkBxFO5F6x3-DMWIZnbpEFcOjgpkwAtbmPtesiKe7w_XeKXSYKPfzCM5wyVZ7sq4BZaQSMzOEOgpFp7_W4kjVZuWL4HvPBA0eaJkqCCnO9CvTPynRPisSgqY5zcysrcKLAAHSQ247c1yi8smlgYsFznlptT_2rAD8h2xfxUSv9KDaokZ9LROVtS1pGJumZfwAKuHqEis6B5GAG1uZw8SgmRDB5-_dcAQWOP6jgn5PBB08RKA4xGMxzHTTF0iQgF1HMX4ScdvPmR2tC1g2_z9NYw5VvHewjIQTVUgKhl6WkLiggz4qCItjEQ-sQaFctZo2QgTphAAhAPbVVKGmXydWSPn9-MLyRxMEFd_MFPx0xEKWUtWopZnXoAnB6cuRUlaR7Ex1bd9kSJeRT-zS9vg6SmVVeqqF10HbBydZAp2CPsaAXMzrohNXkjT1tHa5DFsGCWN8Pl96gZ4XU0hcy0-v_g66wmMXmP7XBBUEh8wlJ2tg5_32LC9uz3mUecfSbUnNnM7jzPEBx0MWh0T5W4oXWkjl0JtkiRFaawUveTNuckzEnkGqxWKC3Pfi-4_c19f14CGUzZTVXhAWYKQD15Ldl65r6xU7U87dFAQUOHcEY6KUiQ-xEZztcLU_KDfunv1hTy9IE73SiYpIvhvSeus46KY7z9D_G1Hw7nQFhHgxspVLEjejdXY5Pms0wE_YhQ-bkrCOPXpnJxE194xSi57ykPsPH5TBygVP_fwEFAdqOPwiKKQ4MV-d2G2-omn1DCyqoL0Vc-bvCee7FYytR_RFO2_xikbrBZwnj_buFvANP_K1TtKf04nY7mjKJiSbrTdpywo8PvxNB2JpBD9gkVPuA2oMFvUFHHownN0jBA9yWmiKpQTY_ZqT2TR2bmCTmwL3sZEdPVl0oaBlPiFZbDTLGgF-4fBlm_xZl1OiAhj4KxXwB7w_DqvCS0V34A0o-Su4VjZzaEqO3cTuPCBuJRfnExkN0QMMtx-OMPaumAQSyZ7-x27l3q_-q2ABDt7hOImYxGar-1FLvfxxmv_aAUPWCKHHyEk-TpdjgaLYs3EWC2FD-DNMegViiW_kEhe5hNwBo_JVCn82HCUH14yb3mZwFNe2vAp5WvSVoSdkBCgEELEZw33U_IZSQ5fm0BtguhMiFPbE86oWsZYU3cs3LiC3hW-hEBIIiqIh3zxWg7Z8AcaoK_0hQeGI2DANl22GKyVTRdHgB6Vv2Ggz-KqB3NYkLJ3AirxooP_x_mqVVoIj"}}],"authentication":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"assertionMethod":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"keyAgreement":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"capabilityInvocation":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"capabilityDelegation":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"service":[{"id":"#TrustchainID","type":"Identity","serviceEndpoint":"https://identity.foundation/ion/trustchain-root-plus-2"},{"id":"#RSSPublicKey","type":"IPFSKey","serviceEndpoint":"QmdPZgcyqHJTiPeGMcAu2AAkZZ1U4KtdQXid1gdJQtpvyU"}]},"didDocumentMetadata":{"canonicalId":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","proof":{"id":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A","type":"JsonWebSignature2020","proofValue":"eyJhbGciOiJFUzI1NksifQ.IkVpQV91YUV2QjctR0FyRTlkeERuMk1rclRUa0t0VXN4eGJPc1NESzhwQjl0ZWci.X94wTgzsovLEAXU1CG5M0Gqs6Gu9oHklr4Zn7aEbrdtOI_WCSCrWJuYomkcdeF8X5dV_ApZ6Gh08pPcV2VSClQ"},"method":{"updateCommitment":"EiB8B_LS_O3NWo2P8fSuRwS32GODaXoLREZHdqpg6x86yA","published":true,"recoveryCommitment":"EiCy4pW16uB7H-ijA6V6jO6ddWfGCwqNcDSJpdv_USzoRA"}}}]}"##; const TEST_ROOT_PLUS_2_BUNDLE: &str = r##"{"did_doc":{"@context":["https://www.w3.org/ns/did/v1",{"@base":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"}],"id":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","controller":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A","verificationMethod":[{"id":"#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","type":"JsonWebSignature2020","controller":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","publicKeyJwk":{"kty":"EC","crv":"secp256k1","x":"0nnR-pz2EZGfb7E1qfuHhnDR824HhBioxz4E-EBMnM4","y":"rWqDVJ3h16RT1N-Us7H7xRxvbC0UlMMQQgxmXOXd4bY"}},{"id":"#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA","type":"JsonWebSignature2020","controller":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","publicKeyJwk":{"kty":"OKP","crv":"RSSKey2023","x":"EyGvw3AkcUf2TZToBh6pddeaaocmvTuLCSLun_yYJpL7x0W3gVEzeKlj06J5Sej9Duk0W_yGhbOKCahOx16LszwTHVgnH9FjRk0nwOer4yKaKnjTZ2FlZsYI0OI__jhCGP9cbcOEd-1rfvUFu-ghsj6oHfSXDBm0Ekplkgs1IktoicuMsF-bD7I6tZRpP9tqFGqARUqvR2daQN-scwYUNsv5ap3XakBCDvOCBc_rPAwzapY_nuC3L6x60UGBAPtUBANdaMhAU0gxd-3JMjcSjFgwzAhw5Eorr7bIp1_od6OfBRYu3sIkij5Es6RDBLghUAx2Z3dznniJRh5Xlx_8zn4SYw_xhV1X04vY5U4O7-7veKMqKxzzoGOR7O137gSTtBk66ISXfE0k6LLsZK0Qkzi0B6YQ0Xo86d-COFNhRWQ_Lq3SCSiOaJ4lFP5_RVlHzgUXm6XY1X0jrkVPWdT42VxGjFvy_KX9f50dOkdPJTax8bGv1nEpDm-55UN8nrIzsRODaxMBooRL1y4OxyW1tpHaEdsoHvsZrLzM5g7FB2ah-62TCGkPcG3Yx84MPp50eRPIlj2omMFxMpnAZKBSRMGtk35A6xAZUI6KTYGfNI-IuWKdk0UOn6xL8W3EwMTxRgx1v7iklbgxKuCBoOeAK7FhoOVzL5YnUCHb1NUwAxDs9I5pNmrvaXsDDLKLIoz50hRAdnK92whifFoWoJOOJbQTb9sx43zmB1J7G_T28MG6UetI4dZljoNfWpXePl3vNwW979nNg7GU3N_V8ZE_slRmUv-rAw9jD0w9KXVCuZuwGIKoJ2Co8qjZxnhZUtmi3wFJin73V5BC684ebh40fnA9z-H1Kwa3ItX_mQSVYeMV-_1fydNULsdhlEnpwI5XNQ25LGqMNb4v-YRBXLSmN5CituV9rPXg5ZzQvy8VVE9qxWnicCxz2TzFrxFOOIhNTxf-YQT5Re5HJAvdy7Y9szo-i_PgskFdVm4UxMgH9ddrFUhDPNmVtVY8PoXlMzuU6gKR-1np9J6FBttHOIPu7LFFdO0Vd_Y3-Dl5mdBXFcP1Do1GN7ojcuRUB4rmB__upRAQQsqCApGurtGP1zgtMQm6ozF0gt_JpoXgvZEFK5kkm92vpedrSfDPBBn5NPIgmQgKSYfvmWRmADyr2J9bc6EjJr1-YD7QR1r2g_eGRBE1S6dexWceWTq-RktXQYOSJBnKLSkbqJniuoA70BMkjU4Jsj1EJB7oxE41RRMchA4BRlClSi31ga0T_bk31rNTLQNLGSrBrh0x2nlG8IZUZLB4fIKKweFD9pL1qhLMM-SQl3YR4-v2wxjlMXTrEDjz2xdwJsQhhzM5trtqhVdxfgBwB_ZBtU9KJqYvkB_3BhY3kYQSGDLhyCHbjyIVYl7saQGkTz_owGfj8tD3gU9oJlZHDyjf4p9AObfF4YXKjVBpPrPgwgNd-G4LAgUOn4DAVwGmGBjQaNWiLet4g4lRsLS3LkM1az1w_KyYCX_k9bptp4qLgwV6HqbLx1V5WkmubxLMpHlbV0tZFLzwThEaKpqNyz7M5qIyDvaSbTFtQ9feXhRHU7VN1MgH2AQmQzHiygXHs5qafdGSsKoMm6c_6R2-NXl3asM1TSUmD82yKonGYhSHHy60KvB4M2rVTKRENxR93u7gaYr_4cqFY9LlcqGUMzxmm6TadfSHz3rSj53C8c3Z3U9x9ftbKGOZeybdWhYbRGyES_HzmlXV5MFY5qHiE6INi_ao7Xxm8VRi5rdaHlVDWfBb8gJENbUHDDcsKQfae-4j_vXmvq4s_9L5It5kVLCT9f5NEf7jsxSP3mg9hqgwdY96ob73GsHO3HRoQARhPUt-2o7i1JzScqRH38AeDr9XnxC2Qu4LT6ffOmMKzA3qngyxKmkvyKmIl3_eEhDxpdTSf2ba6EGOD2GuzvGv2a_P9QHw52mvtEoCLNJAslzsxwxbLSnLIOkbJca1Ew26womAjSgnNwUvPCkz4lmSNTbyF63wvmNJJeD0UgkBTb2MxDw_39ukWvH0mOSJegpmENWzMhvKvxxMgB5Y1VY6Hq06V9mcg4iD0AdI-dM646yU8iLfMAAkB-EvwUUMXRE3KGU9Kx6dqhsSCrow4QDpzk0B4FCATLwawfGc1_rxQyumhF9nagl8jP1ITcLi-hlUyrOsKfSK_s3WKTw4j9iBoBWCzHrX1YC_2UTnq5XIdbY9tT4NajRzqwKLV3aYWRnqXLg_-l5k0H2GmwmRnm4ZqU-9YuAy8MQR5CM93H1gxE7oL_IWIyH_tCXrVH4hRhjd7GrWcA90s1AFpCHhBZs72ORxG_Rh8VcJpB5cTpbQfk1ESme0-UTXoSnuLPfNIQb6I6fwFkIvBx9YL7gxaVmjHMgk9BLR89iwuo3VsEsAs4ktbFfZ70l821y6q_xmOBPF-BxJzlVuHMq9hfyYVA-1ka8tBBeEy8NJ1PlYBMiVjHoKWMfqDKo0ONNv1Il_ThirUq-MM4pc0ENOqwCYkomNBFfFHdbS8L1Y5yIruufFxRbRPt6xC1TnDtq3K7JCpRjsTqv_1_u81WA4UIlW49NaruM-2lPlL6P7rWtBqG4axy6U9WYqom7aXBW0cbg31hY39xZb49G_SfSYewGr_pelurFdTag1R3ZL5VuDTggqErrppxKIBYHQP7M_reJ8fQf4JcXOmMkUOap1K7QJvvENxlQ_RQRj10d-t9spgDv5gki7uMDSA3fp4q4gf3HxZhYwPaImQ9J44zCCLUdo5dyhHsyd9neEeBniNZk5LDZRfX66ERlj49CO2dHmHLe-YQACZnMQDDug7LF0il3QHinPD-nedAAxpjfUus9Ay9vRx6nB3fHr-_9C76qx_NjCehMZHlsAOgZGU-yjdwY2uu8lvnb8dvmCbkIBYn4S_aWJ0qIOEjfWuADwWO9BXI5uzQZ0EhKuhALABMhOIi4pmnHqCE0Durvn9RaPiFz6ZKFhW2d85ZAkks_-ARI0phaKzggmB4E6k5EV3cLqkI63Oiiq21QY0VCvc0LuNoAVYzG8s4bx3udSSORrRJm2fOdURg3wtPlFq21m_7y8D09xKpHkXgEbuDJV3hWk52u0Rxv1MTY2V2_LkHIDF6my-MZLQQh0dQYnUjDfvQ3bTqj6UE4MZ07R6UZzl3Vjw53lM2x4gI17Trma17Ag6Yg6XiQA7QqgXKWy3jG6AuBLjuYRPeYo18lJm00D1D_Z_C--D6zMJKr5ohYrTi4ea_dh3CI82xBNwjeTAd95r6X0wzC3xodd7FSWJMCgt0MF6pz-MEL_jNi6sK9mIn05U4icLZLjBwl2lObaoiYxpyWEpnuMGy8J7dM1Z_aRpYt3J-Zw7i3Yf4JI2JV9u1Mo-ywQyXgRcRBhK3emrFT2fxH8SqkKwJCWn7frvbukOzSQiKD8RFuXA-SWK60mJ3erCRnka-xkGg3AiBxxeE8Prk8EGzLcB1UDRGQ_x1PXmMNtdBK65dtv1b0jGTM_uSHFndWXOrFALwi66JGyIca2WnCfQRQDR5EPyD2d2Naecbj_jMwFUsbYCxGTc76n46c1pI_QH1rxDBQ7j1Tj_rcQz6Bk7DMTNnlTFhJn2h7yVnoRPenlNCWZWZPRpr4vnvS6Ii30os5W2QaGHI_TqhhaXRFU8Z7K4PUUUVEv6u3KIZpvcuVxAbcx-ppLVkj-r2vM061Nx9aXEBFd2whV1Tw2rjf-6fm10N7U3ssLGC6sfHRpSVcsENk-ZjuYH7sY-zmN7Hf8zOYHIAZDUr1rjCgG2yCujbdOPFtPs4QKC_cFSzbpOjRmJ-urzi7duH_vH3_TBhMzM4jowgM70l1LoB9sjQ68wzlaAs74T04IroWMULoZOdaeIS54ugR79EhgqvukrIDLEoCekAY7jAs-iNW14YRPrtdul8zVUjLd4I_X3efx-IX7HvR4RUp-6lqMSN46IfvlScl0qBY_SBgCpdEw66SRo1OAIAuTy7VWX_mbvLtgZPPMkaVheFwYwBZnBLKQKyJHrNrKRQ5GdrSnJP89jdh-o6VEqG_whEec3cB1LwXipXb6v1vi-7jxU4kpU_BTMtEChb21tRhmfKGiQxHbOTRJbHVoQJ4NFlS14bTYAEuJm6yXnIW-GOVCLvlHShp5jeWc_9vvvBZnk4C7bDxY80GxadNmsKy_-AcEFN_QI9pt6lckDeTOQxgVz6Anz58RIkvJ1oPL8A5FZOl4iYuQGDAqTP6Yo-SdHbuVOuV3aM9K3L6RMgj5Z9z517O3oqsmthQdy5xtxhalD2bjV4fNsQrsXIGuNa4nAnFtfsi0uN4ahR1_YYVuQgfEQLOGSzJnw-bQ7m8tOxlDOP4MsXg6BFSBvo0LPwieTdNbZR_N4FueA59bt73HfANTd-xz6ycnZNRNO9DbxBRwXJnQogguwZQdLLLuZjqoglKwi3gmMHvCR-3QngZYQw46vAkTUuYfdG0OgaYuAAqtsEvJRaBVSud7q6pgMqM5UbG9eWv20h-bMQeBEpIuVG08HOEc9TeUzDOoE87PzBkfBqVu_s1tyItQQ-DqSvfCQBobT1pYeVsuyJSGXuaF5MXooxYfRpsAuysjWDKDNxAarmMCpioPCo5ebD0elYa6S1KV52RN15vaAZLPqNRiFkek3oy_M8C9Fi2nLzXG1Bjn_JlKzni0I3pofwFNE2ZJnoLSVpLwVLQUzzCB5GoS5P5C1DcPDxpjAr7e8pWb0QAyyIuz1EvSssczBargovo8iNxthV_MgoN4UGY3RtkDRyw2DPcFdji7AYXw_q3xlxXsWEZMfjTlkG0FfwSTHbhrL-BIXXw1u88y-w5SvjBBwk2wW0SjPVgm-qq8yonWXhnVfu4xRLMY7qNRltkzyB5pQ44rJ0iFr6tXtKus3rUTx2PbQOPNCYJynCWQnA8anAlOiTmIJV8G-MYkP3hH3g-VZSnWE8gQhbvXy9OY4YtyqX96TXRGuHNuZBDEHiPmNAvKkfgVdGE1xrxPnfZ5eN2RQWXAf5a8xgISY1bXxlt1prbFSiHTMLnikDpYNy95JBQnPEqdIYRhgzh29L_RQpIM2ItE6rPrJCl-NL0Mo3YZNdFepgL-5uOjFilpmO_EfAc06pm5sP-g6S3vOx8I9j4JrOnhygXvZx4Mr2D8-R_7s2F5QOYKCpcYmhKSqaPbdAX-q6oNQQ3fesRtmDJIVbBmioMmu5k3C8hh_L2RNAe6ItXT7XVCo-QFQ8fiUIOMWASrYHiy8qsbX4kKQJ98v070GnqCMpKVtB9522SHxJWv4h6Kpsmadh9WjAmzItl4tRV763mNcLeidWzlJFUcfZIVm9OrWbHinBUjKFnoeexpecTm2ncrzpUkMmJghWKv9hUzk6wGkQhsps-94GvQJT2ou4T5xLpeATQ3oenwez9tEwxQ07tB7FHEiIBpA4PFExNwdv8sxaEe2Zaoakh1iEjIbd4uBcEAd_E8eE3VSEPvB2_zT8nek2I9pcHEIHA52Q2_j979f-vAyJci99RN1Va8nvk3TyMz_g6OCknUZcqkhXK3lqigvhkUBl-IxjWqagdTwPfwGPtwV3JT71CZDfBWujVMLPGB_gT_dhsWlIN-sC_yiWL_thQrkgKFPqXPwQKCyz8r_iv4f8NnJIh3W6_hUURFsnu0NpVAlhi7iOU-B0cqk1NHN9BgNbT_zU2aVBEFBrlQetG5pyxxgyDSvrz-igEzZ9oqa7-EIgNv8P-0T0IUrlCIQSfPsiAUsbExwg5JwdgdQ_gD9HUt4U2Npk03XtaAySY1IXJCXeJLp0OIcc8hFeaiPMMv7Caif9RsIxjwnikwLFGtpNy70Ed6CkTMtxBR4uShDzbSz7Hk90gu5-jV5WGysOA9AbW24iqgfgCKjrjgfrod_MNG939PdD9KOV0x3MqbZJmBLB7jKCINC2ilgH3Ez4crHFZJEkuJ_Qq-KDXW7l7hjHUG_debtAu6qI1edYP09UkgmQtnZgLcGAWUhDxWhdf4XYOHfqXxfhiVu8tF-ly7iqWkmRCqhRGV5NmzUWuwvQ8-Jlh4kRa7nhpwb7ivyXiDubq85_tKuha0qKFzzz8gFuiefICHX_Uy3xM8m6Gy3KfYirumMAkuB5-IY7Dgr6IZK8YXGLZb3QEXmOjuwp8Rmm-bMnCXehgCJZplNtcWi7eQxsP4y0IoEUsmmC5Y1as1sAs8-R9XlxBfP3hdGWbOupZfS6FmMRiGD9HoWesUSVtRs_tgOUPPVav2HRIK2CLYBRwgI1NaeRcpnO8cOye4UgRm_UF36pi3hJPfIdCnhxGeOH5J0r9zYEnTDs18YsIQedQOJ9jvGBLvDi8dJ3NRzof0hk9riVtSPV7H2EKhkEL67E5pccehsmZnha0ewYbZdgEstjzjwQ6qkZRmFLOBdP11yCDzgs3eDmnk0Ztewl22-WhhpumCfNgux5OEtcSu6hcC_gtsXQgTm4QV09fFZJAH8tyfFildcaycx0w6zG_tT47jBYIwVyEI-Mvv08qYw3ZN6558VgacYehFWake3ahdjDxZ8bO_tBtLMrFXmjRpibEIYbWZW2OPgBv-4-Z_EPXtLrDpJxYjD8bUxNgxwyqxAlyqZe0FUQVo1RTWV9hzvj4GcOG7wC-_t9aEEv5h9hg3sQXBxwKwIulPSsJlAeW3dygypohfIMKiUdjDERwhgvPsvB_vsJIaVpN3SJVfNWvMEFAIRxl0o0b4upYbISICcxav7YjxARlPcV_nqG6Lnj9-6MtHOzvmwMWpcM0Y_FFro9TqKAj8TkAiGaEMYyJ8Z5EMAsGd32HwMhmdeJbA9TxNpC8CIpeNlU0H9JeSDR3bl76oGAPDIc7bDmfKjcCL_8rZamAaZucmCI4Fkkjaqyl_k0TOHrxrc8EcYzbICfu2Xp9j5Bl_w7GErvNIbMsbJejezsJxt6CR71oex_OaL_DyxGJE6bOaWZFwF3WqhVWMoMEuRwy4Z11DIsqZ2pbxyArURVFG3mIHnBJ7ffjxYbofuuuw9Ce3S0W9AwEvXRlquPr3-wLesE-Y09JL2x63dPrsfx88itwaKSyGuJyvqpTu8NwpAR8d0bU6nXG38O2ysH6-xwvDGoeApjhGaTD71tv5hYcJj1X2M-GeWFi74NjG-PYBkamWVPk8v2uimVuB402YMgUAe5RtZcKVUfHczIcj7IWreTJr8JCLl4N_X48ji2KDuBuuaBRBUYdjkl8ltWE-AQzatqUi3DF2ZDEjEarQrk8K6QDaHNbMAEQwqxIcKVB7rX6pwR4EA2xN2VYmCskYAReAbKYyzbFKgx-_kbylwjO1CMcDTdhKYHnfEznxeaxzjwopfWQR5JQ_y_4OExcY6gh_FHXXyMOQdyzdcNMPFOZDvKAf4PiXg6BV6VVbvlssgImhEbhyfKlwhmbHkrD90BVSZOfwp0m_zd_xOfwSYckSwo8ef1K6DILkCmiUSc9wiCBBGHF8ex_0u3nepPICWg30NqJPii7moRYlXNi2hKgTB2Cy1njuP9pNFSD-8cOxrrAoAz6SaxdS4QqxjykSaRko3FibccYcSE_fkx7_WWBSW_1GOKTqQltkzHWMqTbu3wEjBAbnQjYGEWn8aTNzsAh1pezmZurCOdi9uL-cjIVavKPn23HhHGfS88f3pRdohcdlszyc74acnD6VgT0VnArfeYPNBWcliVDnCE3qYSvter4l5Fe4rH1qDISEq2ni1-uxNRJx6Ck3-5bWSZxHAgvc_2gC2O5qc9TU-akXvNSqLmNtKmO2FGFtBltwgyLc8bVWAJrNxuWQVCUxXlfSkxaGXtN18lGJX-SvmRn5IsqfhUitHzJjEASiI_YOVY9OoGEkK1a532FFGdO00mS07BQCPV0w_gldLncCOgt8VPaB5d5SjOF0_whIcVAIY95y5MrZEJWcbES4zg_jdGb5SRLlr9PENPbne9VYK4_ju-MCFNo0uWibQJzJcpaKU2rZ9sAsT2goR_lu-aLGCdeimhRmual5ISX_tyMRikPCDidsweqUeRzPcriSIRDKLcQfzA3P9Lt_Mo0ql-l1EX7TcwLgCsISBJ39jyhHyPvNPbBAFAlrlF9uRhz_ATonpUwgZrQHSlpsy6Mzh-O8f57HKQTRT0VigvfIeC3J1TR4EzLkHUdC7QF4JNlprKFQl-HUh9VIOpwXfQ7VwhbxUw-MThAn8fnFAKqd8S-4S76Yn4Ns3B0FA0wlDWp9AvfCSlm50bQHUgj8FEtwz8279OoIhBEIMnA_rHNwA1gPMSAl8aU4RO4L9wTbhwVEs32i77O1pQS93ZeNwOwXXoquAAVFZwusOXz2C3jxzKzB6IdrA9LE7-ALHDvmxB-y9KUe-RgCfFgjh9EE7rdwftpCOMj30we1IOtQ1XyFSwpbIK-y6e6itkyx73nB8UicYQEQHDnl2UPtxm3TLUe5bx_E0sisng5ZV2ISypN4_CiyoAbUPCapdHnGLh5VJtaPPq0NGIVA88MkPxnJC_dTfsZKzNVDywA36U6dGzcSH16QoTfJ-ZcUJhHAKJHizKtLpdxpNKlSugnNW0P0XwgrRYAehBBqJAWrmDc2vll-f5KYy6AFEWfIub9SODwuu3j3yfdoVAjpi6Tvm_e_w18ZBYKjtRrAAg38eTrwQwdDDovzBO6t7xmJkqOxsCFl0tz0WB7YxhVMfhC6qv0ojnXM4XrhX482Ew0yMUB9Ql2_2d7u9-aM7VztBqRf9dtPj0Fc1WdfiMD1d72U2D5NukpfdO0k74QL4xFcEWgq0qAPT1Xd35HaQhe9KfUYx0d7KtbBb1BrpQ3zZWS_ThLtfTHOvGZRQH9bQQyFkx7r9Lnal_GmnKw_w-Y5ecOTXwxvtB_XQNOo2i02MTPLpYHXMCWCFB6kHee4fhJVL4yQnaac8WOYkNDZeHf7y15M6Ezs0ieyusNjY-nfeAuXS1kJ_lf-qI-1xCpx4wmOy-W4Y4Xbr5YWS8Pe17115uh3ZGN9n88HuWj_fzZ0BcrgsT4p5LvSm9lntyD3oQ8pX17phhk3xqItrnJYAq8MfnLgifMDl6XucGJj1rhsvVGfr_ccjSHxohBb0HWL6g16xEvKsXnQe-PHn8Djtpc9doxqWWC1QeFnjIFJ38TnZd2v6S9irKu2D-YTw_9TvgRZTHMLgHH7pdFo2P_-mrKP74-OvYkn0O4aUVAZ6-bCXKIZ4ZzFgt-aO6l6vyUUfhcVrQKcnRdrZ4_GYfiRdxlBL1rvcZAkVpH-iitAdQ4N0xFHFL3MO3MH_EepQXLXSgciWBbbc9lzJnd4GkCRT-uH1SKKtquXZIO28ERVLB5yD9xkl6-ch9qTYNnNcBDNSAJQeFBwCHB5xZoyuYfN9p5v40vfSDAoJU9A_3_kaYMyUBVaxQWnKjZrrA5hWy2fjRUnVpeX7PDyAyb6eZDt7dKlkWGQxvhDXRFeN9yjohquhDj9OSS0JlHsPLobIYEPThAwpAYAEH9aspydpQDzH5LdB8aSUzTmFvdt87KW_OjCX2bAvPUj7a8bhfrITHuCUwOl_hNSIaxUX9EuHEifvRKi_KnQRZvkTyN6Ji93jcr1wYk2FOjZEVdUfC_lI-xzuQDSVWUUl6URvL2tfzx5FxqScbNiq3xnIqLrNONk-p4hi1QvPbgiYvXevv6-KgoCOBN5b7E0KUoVcBh8GBPzCeP2EZwA6C9k8u55Ul0Y6dohgm5HS8NQfXCSTt7QQgchGBOyOP96JR_uRbyLPJ18KaFr9QTxkQrxpuks_tWBdd9QD7GN2MU26S9veV2mrWHNXBiKY7NNZjYSkfNyzvjsg3VCwvxU9kzvkozJ_hQnkOnEmlI8bu34cFvYy1Ms4X5fLwaFLMmG3SnAIwBsCz3HxzKU05NBHikuB3B79BGskfQK_Fe-rkahNqJgG2ya6xgeIBivC2iuCuVjM1xcVN3jM0VuwQOCIVwjPpyDgWwjm5rpjX7LfEzwjyXynX5OR8PVugx7bAFwv0UNcbkBNLadJmL5hZfeXHzgPM5u8M1_PEpwxRddCDLbmbY-Y1naQwfaKRQp_c6KwJtT3IzkOJlaYsUlEeoLQKfQI-OFr7Jy6N9-tP3x_0OpecilN6J7UQLOTQEIeygISrIiIkSQgL8m7YCl7cRejrq3kF9UutkU2OIJFseVIFtIKZL92vc3WSxj6A8NkX-yqQ9LCFljVw_acJ9tUT7tNyOF7mFKBQJPa92WpaOGgzq4OCV2nJs4GFYjXgw7uE2NjQ2i9_auhXryGm3uD3G29NjUQ6Lkingi5trDZLCzoFKtQ_-2tWnf6sC4HBlShllmYDfCCorSX3Qc9WvEwxLbRvNX0CgPCEoxIKHAE9UzN9sfWZLD6BCXAtERDgNqc458B3xIrpXpk-hmIe-Res9HtuS43LqebcFiHjjKKiBuUEBCSxSEYQPYdEII9QMsBsp9IoCOKL7y6m5EgCfQzA7hiWLlE_Xrppv625MGLzebKWzu8CP1mOPWTp4FYwaXl6sm0rgbAoR5XtNLcBazT83ji0Qhc39dVR0nFyvdSe9L-EFw6dbYUPPbQDh0hQVzwnXZYFi4wgX8iFfyvfj1cAGrQNfx2yekQfLm-vhGK_sIlCRVZf2bjS6rwAbVIhhPFuTsQ5EaYCc3QbvJg-slvxMGfr3gpUkMV24EE0dCemwKRyRyf9zH-oswETPMyAFTQmlx715Ao-RESnFuc1Ebl13oTofrWpye9ZaqqsGko3Cimdifa716i5Gkq2FJNQRRRrp979uFgzdwm2AL3Wa_5I1t4aHY0hFNXzKU5u7gNmtiTDyLSOIWLGfd44msxBYFSE9YqSdU-7KpEtOLQRppx3FR1TQooT35XW13oPp37k91Uv2j8wLJPAid7msh1AUWmpGiq9vhair7EUlZhnjNIEvhlTr6sIwFzsJPRl9Dy838w_UqVXhKcA2wJpTCjgRWXL8R8b6L7Qs2v0H554fmrK3qcTm1BgmPf6d0aeO9wsgj_cSO2gI6HgI4zL6PUQTsMTzhIY8pN8MW1jPWVa89yWjGjaanxKT6WyzdkCGj6NcG3Yh5UoKGeehwa_5FQwggBfzXYMIAK3swXYvK1bVz_68c3eLtW96nYc1mnOw0QmcuQ7ajBPpwPVqQwH1iLRS3nEWbxznVbgvcdHS1Sv8LcVU8htWp9JheVP2OCiGQPFFScImnsLDC5WZxJNohrxFO6HHJ_6T3py6zz491E_zWqb0B89YapQO7LKc_D3pU7_3-ug2A-BmtjReN5-I0QAaNX86gN5o-LNW8yl7DmVU8rDBHQBV7vZ4uijVQhDvpifKk5mqhztr7B82gamJD6gUucjs6nA9V8i9496A3dTMHdtEjeEIE5zkvtbLe44WyaDxa5KiwZikk137DL-hp9w5b2-ZjwrGqcNJrYwpTQAjHigL12EWMHKEnPEsSXqmYujeWGfB2M9_VDmSgf3J-XAZroxarSzyVuead1XNLHtLqQgT0Prh-PS1lDJ8jH5y4_JzNS6lN78BaEi-rBl-hyhXqi7ZEzGEyZVB-H9rkmCE1jnuQsHj_iWUkZFeE5wJRemTSNTxF_GqZrFTkTD68qxdtMg7nWns8pXHaqDxpWAFaONRj8JdfPCeJhQ3W9qIdugEHXFlYYtZLEuXAlBGkHQQlnL2XeZ5aYE7xDC2JYQRJBj8c5fYfusrnqBgsz4EIO5ewfwmX-OAJg2d9Pm0UVxGrXtTW1H277sVslv-2FcU32cZwwls4YthQ6fyoIVLzJTyMOYJUrpFW32r5tG425wn_Q8ezmTs90EKuVrvVo8w92JL6MDKA-orDvhvQ3beb9l7Sgc5yy9cb90rjD-lyQBgcDfJ0xHFnhjnz4S8t0yga42xeRI3r_mXd0NvRzTUHkedNMtRAdU-W382jaFGRBxXL_4YziKyewh_nGh6BlW9EQ83Qf0oSwb43IN4k6GmK6KKvwr_KiERaBougue7YpwtYyqCrEoMiEEMn-Sog4CeLzg6IuYx4awivB7VYGGGwU6Bwc2IkZkKUFxVhJK63cAwQX5Gcve_j_-WcRRGlUhI9W4RvFhQFpl0YfC3cLUzRQZfV_fWH2MIwrJm6y4VCHhnvx8O87qetR0kM7el6lY4Nrk5bNtCdBeoyy_C1sz--DjsmM-z9i9IR8PqMCZcX3gBry0Sn_js4Ka0cXPsKpM-GpR6L0CLxge1FdKNDSFUOacsiEzh3-LTu-rUUYglWzQShuc8_dtZrIEvVocirTKZ3gaImQ1M1EylwXITBxzCUW19Io1X1mxKiFpXKHtzK7AvEs0kdicMBNl1HsKSn8OH3jxwLSHI4DwFIGYBxCQ0vvG3NN5ZZ_c4OnSfQ-nojlgmeCjMGykcA9E__NgeddsOdWxnG3fVQFIiMzoJ1AtYnxHoPRbtVZdyWB3dX1L9AKxlFep77w6KS48z70KzKseRnKLa6OCPZwfXgP5kEKA7FcKwpwIaMPNxCOedtULYeDhclbLeDtjK8LA2q7a8elVyK6YRvseXaZ4-nnd7iLYLZNOv807ZLaYGm51X7aFt0YRTimfsQIGztdkY9aakmyH_XQkqPmlNa75aE4xf8FqLjwa3AZ9PcIS8EpwX_Vw_pFA0NJcvJxCBgY4Iz98FxssnBRC9dJ1aAn4Kd8lgWvHIXS974MFCCGhfI8RRVDl4S0QO7W6vrGTIZB1ngY6VHZQ1JG9NJOGtomR_8RNH98FwcPzVNUzy9AhGeKBS3WECJCxk_gKjcGB-rBogS4EU0BVCfxzCoTMJF51ufpG1k4eWlEiEpOqUYgUWAN_3XYWNhphToFLg-h1xmQWWUBiVS6tV-XVvEOgKCKp_b8dMJ_99civ11moW0s3XQpzbxo02gCBR9LQYl2OPBcoRr1bVQfmS3sljBMCgtj5NodsMpz-rIZtgbzdchFe-RE6QK4qaMwAUY0oldGd7nIW9V1C3hnGg0kekWG3JKlxMhIB3IbDAVQ4jRJ90_JbLVaj8v0cNmhAwT0QwIwuTJJYFDGM1fYrocL0UKFsHEdPGZQFnfGAeFoMQwUt3I6zpmXbIqWA0VpRYwiUwTTRNTSsH1_eX-LWUnbXBsOmr6X38Sf9SQD2giVwmji2KBw4GSfRjUsbae5gpgZZbTcXH2ZF4FK79B7kM3RW1yKHcMrT3jXyZKjfEee008n6CJraHTc2sBDtV85wr-TQgic1VgACOfee02nwbPgPGhlUsN1e1cBwTGCJiIthec58AQtsEGIsqpTwh0axbKUmUaOj7zuUjDTg0imRCdYb_iMh8ya-YUncdYTabPkBJYlnbHzCB7aXmq42akqBQTTTgVgUsrRy22Q9gn7CkGltOZRbiPZ4Oa6Uzu-CYOsK-0JcD1xUgtTd9icWNNbAg5DCHh8FhryzVmRa5VUkC81OQryM3CgKdyzyw4xSH3qw2HcCMu7VHbHYhvVEXOQQtSaedW6w1shQMbPRKt0Bf_n3DTiyvSsfAgZmA3lrhQhRzd710dzxxljzkbfYEl3Q3SKg2CNM4Pu8SzAcJj9M4WubFMqDirRgVIMgL4xthq9u4qvIGxTERgAu1h7xhUcA9f0IvKiPzBkfExW_QIYR8c9kewkGILCplgqOHbvNBtqK5uXJrnscBUm-Su8yfc3gTiWWlsb1KBm2qwj6uXOBWQ-u4xyatyltsx8AJlshq-YB-K5oJuvlwCXkeXkU3hqRM4SRwLng3VyhdL0Jr5HUv_M1ENVemAJCR1W_6IXWxbChAYiRUFVnGQMCf2Jx46eQo1sNMaO-1r1LdtVSJo4ZELftKu2X0BMQC-l9iQ5EfDT2VEPZvl5JszWbqWIlkr_RY4jwbY_OeQCkPaMxE0eywBeG5zjdTYzmPLm0YjmK5J-_7tjM_678RIQ8qyuFPuNRGFUClznKIZ-T7SYMtFie6XAQ6j3q12Mh4-zEomU1jIOcy2EzZzTVgrpmqVtZUB9wzPIsNtq27VtLz231dh2i2fAfAZHdvIy_7XQsY7-JWltkQ-fY41Dw9QOIhDb_KJHhFNH2xa3g3NGh1WxZIiJNfPXXH2pMA0xU_FnJF0uPEr2u0rEcTWqTsDgHk4krHglASUYsJYneG_YgBCHWWrGXWzbQNGYsZryPJeXNcY3hw0wO49CxV7gb56BbUNBvNIfgS6SogajoeoPTkPQAICjtAVhnrgXyIFnQ38zu9Cwjwqxy10jt04Gwm1Q6xAh_CNQwcLgtJ7elaM7zi9uEGFskPfZHF35EOhpMwR6wBoPSv0ESs8PX1_WKhYSakFyW7SewR86-W3aCDR6xznTr57lJB7BnDb9_fF6rjfysDLSjofLGwjD8qC43OlMNZB9m868hgZoCUKvSnTpVW0B2NcAoM8lgXDox6cxZPtDsW65C2fMFUmt8yqLg9MOB9QRvr8jQVvgQ75GPADaHTVbcDukGOlpWsE8qHc0y8sbWnBRwGu4lUVpyOe3R-q2Y9DVCPonQoeUt3r6EfyIPeid7GaY1S-jCTuj5GlZA4Ridz6yYYZmGXzju_OqZL9TpH14-DvywWaBu8ZUqvz9kVamnK9P_M-jTDn6iz2zy37xyEGtzWT5Mv82avznCG1l0kSoG7HPg2kdA2ngIutv3-sn-D4_H3_Wzni52iLO-5CdMjEHyo8IRF2gsHDwR0mkF5uGdXv8RD_b5KZtgMy91QfiU-h1B1OTDWxxhfSPDO00EtPBW3UPQhkMJY2_MdHzKiG6i28PRjUTIYDcQjc1RrUZFuBmD6S679gKEzKw25fKmSbk6MBIhBfV1Q0h9uX9RauUq8yFRB7mV2EQgMRzrSZd0LVqNtBcOCU7TdrpzJzk0pZkfmjIVGOAJ37T234ICX4_M28IgaNiluXWNYvW8j7k_nTy6-8uRVw30AJnkQRswmxllkn8sE8pfxq2ACMG6LhiwkUeRJU7QYz8GMhtn1HcppGw27GGLZDbd1fHQ-X8EyC_pEx6wcSKdLWOZJ-TOqBWCDHZAJJ44G9MQ_eYCZKj78LA5pooQ1OQJeno7YefrhaY7gsJEY9LqHaDBBrDYPefTlMYgHPkHKxgkT6QtpbAHN81lB5uiiN-o2HPIgI45ODYY8pmvk7SY5BVsu-lJ0K3KZJOhOsfQsoK9CWB37yZj73eFNgWO9Wd5qmmiRVbUyBrjWSXc_dLnbEAKxB08xoITcG4hDIO1TSbTIF1QsBKXbyH11lwKM9Gr3bGckU_ni5H49T8MeAx2Cce-oeZ26dj5jDGQwwwgRbDf_9eKjzVzH0MtA32QPr-ZDqwIPJlpSAIswVKI7W6-TVHeKdYjBufEUoVhjsJ2kZLNnwsgUPySarkA7PjTLxcS7L5eXTIzBWpcSqQfY6eII492F_RPgaAzRnqRW7FA0lvNcCblQJoRK80DLGM_oZajzqytR-ZgfJvWQXY5UAcW0ywx1hVklrP5H9hxJBM6LujBC-bfK2gatWTUNoo7ciIWk8WPKZf9jCnGd2s9YQhwqJfIoYWLYZj2obHw-WfedxSpLOl72ucoXM_UvtvSjnnX18plcNrQ5lkO4f23N0gh_oZhdwYeyeb1N-KADIKIdY3_6tj1AFOqN_vXTuFtEAilg5YpHC5akZeMvfOGunAVza3qucicsRDEYutxcXggArT_nUZa_j9X5lp9EItKRVyGjBvRa8VKDwoHe0Qq9JYaDk2zA0Gqz2BsXKjxS5eArOJ4t-el3UdlFrsrGz0IIM53LsVDnYFGo7G8sQWzxQHD3LqVKhumuL4q0I6gBmOZBhAzzAb-j3dE8MFDXLKOzpMXj4yY_f1BqaSVhA2LxC9FXh8xlYclwHgweVkA98obGvKfW4iMNKJza4tQ5A1QDFPDwcsF1biEPK0svQmSnHNvjhOBM_hRoZK1YD_RXmIYPWzJnULt_2Nq4Fus7QlP0m4I7qSxDSUe3Ly_RtLefBaV3G7dUa62RQJfXVKgbGQTy_64COJ89TVWD5LIEPW_LRrYvSjVlsMD7LPexlQnh6J4g3zq0uRHxcWa1bDQDUQYrQp4Ud_6qc7d7FoQqYbQgib1M_MIbRyJezKZJFNXN8aZWzAkSjR6Luk43uWgogzv_PLON19AnvbC-eLg3fE4aUvJAueCiTQGGFkBb1O2IW1kc4i8wN_II3s1TkjQ6KSvre1kN4YMOTk73lEcC6L3NcgOd-o0tPDO2O9E6I8FG4yCWmnFPjPO1FFmEnjAUSgwhEs4KdKbQwRphNPnZQ6dWsjKPVM5AfmEiLx8drX7C2NFidylmW1dpC6T9L7Qcvd2YbocFGnNv3j4ztPjt-9Z2Y4fZq-02HVNkkuOO5AB4TdPTftjgiGipnbMaBmgBNMwbxkzHuWZ-avaQfSifAvfuePdugEVjmjhcS0NQuh0_hZ-K8m0-41A-EqQ6kzgfYTwKuQ8JdIWawuYoM1Q0G1bJGpwQxG9DPDB8c6y-WupSOZ8c5l2pWsRVw7UJ47hHhFIsoDHFHVDBT9N85Y2SIRbttX2pcnKj3nw7aj6ZcTRwpNPN-Qvu8YMMjMUVV0QoIn1CEyhim0x7jqidBvcSHLamlTSqYvzDfI4l9fSA8m4Yar_VZSMYMxls278D2sxVIEjXt-fqUbXc397qGzvNniARzqZcqrataPpzQoOM-bNj5LEJJdYPqSsHioJGOkhFzWXu49UuMFYUvyNxOhrbUy8h1N6GKiGDMSwe9k9wN-5WhvfEf3wPAztWl5R4PFRf306CPhL-FW83zhBr4c1UxU56taoVNnJtsblxuTTDJr8HgIiS0bqCLpL1s-ZYOgARzAgymuZCRdaxTmK4fdFhlTs6coahCbrSXO9Iehq58t6uw55hGhAqMjVvaRn2TpgwtHS2jvGMCsLFBYnkVXeeCDwA8uIEvujo_WcIUiT7STSP1IHMyllhlhU9tb0sD8wadR8caAgHBe2CuuE6YeO4qet9JIzOLTd3kJRE9Ev7aChlmuuAElJ0o-ktfVIvUbwVAwiWV3X6AcMlmVR_6HzhwZvc64Phapf84hPMYXvnIxBSI5UbvA0X5nHU2lnqPeRlhQI0mKXvLk4Z60WTgGrJoz6mjUQNep_zG1WTSkLwk4zlLwupc492MMc-M3x-vYQBmA0J2OfXEZjnuqAQ6az1hF9SaaF87c_W-Dkd5wgzUEkoUA2kjAfLtSItyltjCzxTnH5gGs7KaeoN_9V3bj_EAquWTrF9Vdr0DyN3fVdwrjU7oZhp_CVfondyy_VQO2wtxzBICKDcgraDmcBS1Pw_VPEIXvNm0ia52zwDDo6h53kRiKECACeOLLwif-WO5IBh4DZ_DFsiuaX1dJyUUO_7vk56KjmN0QEHxaNwpvKMuPtRGOMWkRAwIKezgkGJ-GRLXbeAA_1qqT0hLDsqJUal65fXdZ_J-qEnJH9xThlPem3WrWpAYKXeVOLOCxuA-7wxyxO2DxHqJdxsvzd16aErXTcIq7OgGXL14QQXLcpQIKermnxygZf06I83xy3pkfwEY07BVX6MnouU0ybMlqeFQgsWFnP_yjPuYGA0RQGOqsL_Cz_aq94VrHtzL1M8NTQt3Jhpr_L908QQMXN7kK6CKJnDkh9Rzykak8Lig_xmz8E42bPY-RWpAgAvpju1nggo6H4oH41IfQYW2gVzTviJq9EC1rP3FtJouq9gmSH5xDo5IW09XFskxJatkvOUIjgtZhCNG_VxtML1VdSDLZSrYjMT46SO8JjWJcn__4tR6gEmTrzRE2OSjbLuZpOksXgFrOgRDsZuPSeBAE8VKVpLtHvRQKWimJumFONfHJ7JxCOaUSBzpvk88Wg9em4x7YAd_SAChQoT7XRtjlwkRszQ-TwYfGsyOOGiTyG9dzCGGy_fsTugpowfedGCGBHJpuApn7cf5NNyLsafquuDtEyUly0NDpCwF2i4Dhma5jQsDEbKOlHnq8uzAkJXRe96IQBj0FWieRJyLU-pNsgXz2PqRxNXs__iId_f1X7avOZHN7FyBa-vE-u8RuYGXuLsUtQnnA0eYesQ0hCvGHa71I5E3-w1DCu9dLeY725SC1yVZ_vJ2WJmwEPXJIXKhVgTfvw8GIEml1VGxRFvb5kMQtGbXChL1tz7Y35ux-SRoX4A23pTZVEVquaXb2QjNFOprmA0tuFeYlsUdqD82ls4R1WzgzLVRRF4Z1Jh9AFgfYHqV-7UHwJAY0OpYK9iu6PPknBPAxWsxnLxyIxQ_rRnrbD-AyW-uFhBZ5d38zkvKw68Fr24Czq84U_OlBAvHtTWSzQa_6pc6tu5KT43QDCeWwiyWt1gdahuyoqGpJNgqyD6gh5xjSr1U-ahTJpXgVjnbNBkfOWecj9GK6CMLgvcI21qVrX2IHwG9kMyQgNmu--z0VHXt0WUtEuUcHMM4PzFM5AOZ_oxSVtIbvoYGDXjUgEI-xM7BOr4e1B4n8X0aoorefQhCLe1-Lv2pKRSeUlX60RlVuRN9GkoD_UoFqz59zJwL3h2uakwjt7iehx7DeI2pHUthZL03BqsYtJth9Emw5gsDKfBIR9BAjIzbSFRnnC_pthG2E1WMRMeeKThVkL_JYkmFj4Cr1xjqXXCTAI9QFwcTqRI4ZkRgem_jqVB7H9-BzVDrqgbQoxuWhNRn3_w-xfyzv_JtRcP150_7bEN2-gbBJCexcaF-0PbkopUuQqUjE3-WYKc9X9vLWcdkEehB0F7eqzdIWqRPTsnEat4SQhSvbaOp7EgY6Ypkvjkheer3fkPelAHN86SGviWWtaxDTWMBwHQjM866tuDKWOEnLQhMb_IjQDFKHrUKUnz42saPlPWfvbas8_Ymk7bX-E263Wzb5_MWXqPHMt6UTMSOtw86MTE46YEW9Ww-WW10cmatGb4jfoQHXa_JxCRry14AjwF7CmmQLP6dnm8r4_jm8AylHV8iKCG6r6csAhY1jQ3I-24iLu01EDB6H-_bIX3uiZDXpf4T1aGBJh7I7INB-Ad7d_IV7At-qaorPyE1xvTWeFVQLymsE87ZHY0J157ggITtT95e_Q8_SEiFYg0vxg89qBpuXygL2M_Pbrb5eYTCA6K6N86CxlOvFAb2AJnhAmxe8c_KHIsFZPL6lReDGQmMPBuvdCjjLPV7seEZX30ZMTuHYXNuD7IytEJ7X1o0_04eCmcqbivHBCoQGOzDhQ86DSoX2Omx-hmQl3hI2KgKnGcnfym2Ukd-3CmHAyCDAv2kDHm38H-JdcsO2DNk9QsYtAln6XRVl5kFDnWEhm9bRh-fg9Lmt_mNkwHSwZ0YrdYhAOCMkNlukUp0EYKKhBSY8lsY7a_TPbt8vkTMSCmi2sPr7NnuyaxMvw6Jblb9OD885lSOUp3oPpoH8QPkkhYUJ4-HVmmMGD8orSe0L3k7lLbyHzz5l1EmMahHWCCbnoMGGfO2QnxV4v9YcsMmIA_NX_1CjMUh_LYKrVWE2tfmhj7Zdprbop3nTylHV6YNet5h2MVUtpfj3CFTz-7V0AxKhqmTkSE9fMv5_XY9-QxFKf9B785SPTdj1xBiOsQ0uz3TJ2CPFHOtikiqYkNu9w2cUgYejqlM0crBDpQCuFmFJCFNKrfMa7eue_4H3RSh8Yu9Yw1LXbkAuGoFMGYhegcBEvcxcDSHfZ9f1HFT7IgimpuFuoGHwaNhPnlNc1uI1ILsFeRrrXide0q3L78aMAdu7eFfSSXHm-RcZypE9LHU8caoGqd0cr8hMAFvmAacrXiUE6RtzQUZjswSOziVVwlqyszgPXIuDsA4m0AcaLyEYQ8fEsRZAg7RyRbTgMGrlo-_L1Me2JMPPbiuNi2EtBXz_85Ylbaz45KQ45mdka24ouxzs3YK5aPi-Bv-fYL7FhoIWM6AiJH5ETjucj9KrhL5u-mnEi7sYh6ttj6I-MtSpCzOLrIB5HZ-tJktRhN78f2m8h6N4FBL9ooQXR4Y-QC1MG4eRlAiugn97K-r3MDGQZR5fVwC8SPW4Pt6UDvfaxXZek0HmjYPEk63MIxeMBOLaipBGR2ziR6YsoTUZ3NOopXjZr-UsGukdLw0OIJsxA-nGjmOZCr6iDgY-EfaCAVwAOxAv47u05VBTOP1xoUhMrxNefZ1lt8hEziCDaHInMkDdc4lQVeYv6H4rR2KugX0IXGsFc-C8sfQVnALLdQNjEg8_AfTsEmY3NqE_ECIUhFwxaW8s8aWBgX97Pi8SxkCwX6DyksH9fjA76rP4P5kpWl7ynaOaCfytRliE4j5uDXXywFfwN64DWKIQt4u2gDGo9d12CWUMGrWZZdn3qn8IgEDmUdr_CGXIGcPNuS-wxWoh4G8eGNhvMk1V9zhyhcxgbjoIJLl1T9MOZZ8JQVpiy-cPgClLI2jgIbKSVZTTZ8B6T93aQj5oEbOw87RZxArjYP2XeIHMNh6JUUOND97h1D-tXlI6hlFtFTouMxLzyOpVJLfdrUcr2p0bkbNPAyk3qzxwdRWegSWH2nojJVRP5dopYDUvX3a6sXVGUefUr6llKEtyQ9W84oVESDWyhWRv6GiBkpimAlkoolaGYFYCD72gUISM-ptvaWmVvNmXdZhR2JCSn3Ec5K9TZMg0ArIgFvnJeksow6nIwDSYZ_EXqtEgn9hjLaOcKZSrixLgvGqWY5phJcyYWP7kBsJTxc9U7xCIDh_RCU8fjZzAOAl4r3DtGTEntqzqhScZ_-Fx4ygPgpi4Ko84FM0RvNQGw5VSrOWADroETQVP-La2KyDOjYo4dTauA5ArmYnXyLatcyfbnvgE5KofVhMHwPq-QSV7QAaN9aM3KdDRxBXV7YtnjPx5DzLQE_61NLQkdC0iWFjHwLwM58comkNfrKAUw3vtLzWDiLHT1nPG0pxYBn0zAid0cdOFJ3JRJl2F6-GuMSeUK6kCqbX4mtShWXp1gn0YErlKR2PFjCDNj1o56a5ejMOYAB_SNIjRLO_O7uGofXv_Om9Uevp9XKu3ca86Qt6uOpwQsifkwS6j78cGRTJeU0SlIAGBjzi6b4aJN--CpFIqF6JpuZAxhiLzsHAXRAKik3Lu6Pmb_24KBL5_ktbQRcQX6GQjGi0A4gccSOF3hdJ9j1any3RaFOA1_0HRAv-ExWoiQEyUnWALcqaC1FmXgDTxYx_VUMjeb-MqxAV4eHjJsR7e1q9cJS8qhubSQbHMH72GccTJKlZYdLBHmc0Oqejf-JKgaBMxgkGX30uCXhT9B8dag8jVrDBemQV-wak7QHgbAveaWX74ZsZZF6ZuZ6YU1llAllJlLWPVNr4aaPj_wMfurz6YyOJDnCcVxcKFjBCJRuTBF1ACh9Ye1aj5wDUVwjeKXnjEy-quQNoB5c4clujc-G-ep6-EHj6WgHZefu1HYolZNprU9zHY3T_OrisT2jDBUByHv2RajGe3K7nDZprR-e1SPApINTcKQ42Fh8SfDQsXg0qOfvMdKbfKJqQizEQiCtvkQu1oXhlO8fC4J5UkN3qsPcdG_h1TQ-_zlAPDJ97B_92zV5NkIF3XFM2iQht1oWwZdN6xwKeDRqKmpER-qz7bxiy9Hh1IxU5T_Ac5c8B5xIxbQzgTJal2t1M-_cRvGT0CjpEBjRxqts-KliiGxFl48wNePKySRiGEfnn4Xfqmy4enbmmZgyHCmo-h--qxLIxBEykrcQurpumcrK29z2_jGUNichMpAaaT3UlzgVTbOVb3gVN3Qsu8ltR1RtlO5DM_Sc6q3GQ2QpdHafa2S8Z5D_A90PuohDCpyqvS7tA24KNQEKYM2W_ONMBNNEoyU2p7hZezbbj5T_HLHVRPUiVLgugGFQkNwZ5cRgrgYqstoKu9VJWFE-odBF8G9GwHGFFqyCdBL2CADSx9AnfEssP0TSarXyn-ALo1n5f6vpUFmkcuY-4gFSang5orkODd3k7hSmsCxs5NVMLfQxPtjJcTTrKR04H7xAVNnt79YJYVW73UaXEUammc_qu0GAuNwgeaX3wIQv8ieBeqJvGbfOoXd-U6c8b2xS7b_9BCWtTKZ1A8azUrXAqOr5rXlKkq6I31ht1XzyQAWq3_YWEc8MJahqr7bR5GQqOxRg_adTocY65i1qhxebStP6XWRRurHWyHzDhi9duKfGK_eC1bbuUIevXsNDHdQBDNE8_w1BBBlg4eFuM8vSDZWJEKPxvB4Vl7ciLOs6-diW3bj_JDo1BZlpdDQFKCwDuk5RtRJmr9hGUaIbF6nrjbFduzQFh6laU7VkD_3XyqJ2C3dCD1vOOhslfiVG1fBWHpTJvKsgfLa0u94IUipo6YWCz8K-LCeOymEufdrfaI1A5qutL6tF0CaPl48rmLRMayxqTf4ZGCCDe49C74wOS_kGmxchhr8DKGUgKwiWJWQjIQLIk2PzaHSQ4cE8uBQebBsCMzlrzNr1YhYzvzhje-qorpNcwCluQeaXkqp1WST9LbExS1jN8gmJhLgS8yAOd_yGdJchugXdbfPXWD_R4oVf40bCAv3HBB3MxQKq8dZeXg_9xqr_bhwqY1oUraAHLEol6kUS--0eDJ9PzaLed1ZQ_6j-pHR-mu-OkQUvtM-THVLuNMKWGSYKcBnOFYw_1NpEkwoWtcYCzk-nq-aHJ5XnijDKutRPJQ5W6RLMmhB8qFoZpRp_aDS5LJiqp-Q4g2QhtSCckgUwHN5GSDTLaYvjkR5jeIDI0Df_tQZQv7BiusW4M-iXMunM3qpOcdAdfnBTmODqjdeBAk4dRnayZtb2Ib-JKl5ywa6WUDhpA_UQA_sIlBBbTjetvlH2sChS0D17boDPANxqPYQLorzUflL42ay1DQFsRRdnxTiNvzN3nMOxzFdIUYqWEiY29KQmAFyuERLmtWNxvUB7KB9WqxV21mbJ-yIhTsuUTHve3HdcJuWPzEtbZemmvTyJr1wckTGBWVfeT20e24dPMpBbRN24Mpx_tMxfsioxNsXFYqKHzqWqZ8Tp-gj0TUMr-dATGUJHHQ2Un1nVUYhOfB-G-cycBf8zmgcnA9EsKkTOlZY1LRmvBIknw6thweHCggBJ8Ke5N7lgYjdTTPs9HXMZk-YcGJ8Q-TkB4_Dw35xq9_hnncS-Dl-_aTs3FD-V3fAbAd9eYbttpwk9kwVnc3GzF_d-eoCntwtxNH_iYmdeBZIqLZAoDwzvFnGfVunFP4RiUtLYepxu1m7HLhPSCAQn6SNcLwGg1U0jQpfYIYGZTL3Ntq91XYv3J9vy5O1apgQZic9XEMxzOuoYf0zDEU41PaVOmGv-H-mdrmH-MI0AquibmsDkD1GoUssNDqsqGVBgMMp1kc3N6irmLeIpdrSjOLUsW8eq0YGWoMXXxp32wIfDr1fad4KV22Slqlrfv4RC2v15WxVI6j8Cn2l6ymNxCj95fk55ibBk8IgObZEwbu-O4F6focQnbqXcLMSHipxWVOo0PNAnxeG8ER8AuVaimP1nXVWhNo77VuX_Yat85m9l4Avt0Q8tR6Rpqruw0cxZRH-3GRk97-svz5QsXMJgNZsDquzmeRT7ydwFrr8NK2Ei9NmlZ4pziY4xgIjVIJgIhgkY2wEH9EBDPLuqmYrA9z2RC4KUg5aMAvhRRZ1Jrxd4uv6C7iq9o9x6AOVwA3AzuM-A42325s1cNlnURin7VjQvoDg03eXsB-G-iSEUw_WoiFatKsO1U8bW4GP1-XwaZMD2w9-NXF9JCCGp2PaYNl79WZXpoNqtOv7CS-USx0vOF6DLllVZebsUhgMTBHg6I7dmJShzC1VLrCV_XjFCVlxfSdC-HkHceCUwQwQvkH7CzkW3Xxqn9onVcL1vMKgt-D7ov_952u8jsS6gkzEkUZgSFKNUMJGZv8J1rhg-ZNUi_50EsohJTlxy8H3xw8RFN9JsTZ7T7_O2yJ-yB5bCdSHldOwfQWtPvCw0df7yzUQtkMqMY384QRdKraWO3CwhrqD5_j-iqM1nw3AKDnqvUZ_pL_MrJT5OwqvaQLlIJpSymmfw642aXt7P1TzzFnwOYb0Myjc0geBp6JKLB4MetCiKUxmYP8M3hiH8FSZLv00jUmVJj-CPVj2IVml-IiAPyPU45_2W_Sek_l6JDqxgviPNU2QfLqXLOgs7-30-8ZhrtlZLC1AYco0hIEyVvFBQC5CjorAuillJuZ02YU5_kNwGG-Avbqb2zLhjw3gO7ZB1Lz68cv8F5YVsUvCvMgRhgpr5Wj_5uFtw23HGXHKY2Ejm3Kjya_Tw1EbrPl7t-UYyUxZkF6lUh-ZnndeOB7RWVO9lDvW-kuu5XuYFbAM6ouYOPd0Am1Te__qnJe0cYwKBaqopwTCE_7cu9EH37OBm3YWyGrthggmOrcK9jSI-xA40URX30vYvyuvNzZ-0f8PrZIfTtss2f0w9om6vDpwxsWhXRlTyz9qc0ntEgVwX6t6xWklLasPIwXZpahtO8PAA9Vqy2D3t-nMSyeBaPMhkZi_k5x3ckiLR9RHH1OmiAyYkGafn1_aB381MKMv_8AS4YGzeAvaHBwwfNDBlPpBhdupAGXoGPKFCM6d5W1QoDhwQyIZ9uFKuvoPtxntY8MwG5x-Vwmg3GhIDiSmoybRNIpfIqXUVzg5_a9p9b0-Go59h9B1ntMB0K1Q0X1EtZq-tVRlv1MRpSjOl8LFyGFQ8rYS0aY54cZgE_tdOaozg5NuXDJPQR515WrBf6NyJ2E66D3u1Fde7hd-zUMSiASQXMKwCLOAMNn4f3MWoj6UR3vKPjtBNwF1umNrE8P1tErywv40kYGz8-Zy5Jub9dMgKEfXbz1s6XIqZJEDSXngwVYNQx2fhaO-uGxt-eahjkVAkt1KoTe3sDxtkX7CFQNAaVBlsy4JEqRM1-Mxg0GfAP6M5l6MMhbqkJoN4oC4TVUlASghOUHqkCorULtgKctw01Ea9UnPzXz-KKpA4RllrWdUryiRH2A5RPs3KH6mTKVjJmzXvs-tHHeQphSLLm3QV1smoj9Z-oAJrz0C-f_Y0LE4Rsaw8Ag_7G9OOrBOD1odrNT2PbpvyeMCv2179maxKeUB3WRIU_Mz8b4_vi76gODzX6t-K5zDm1ukMlpNLfRtD2FZOEu2S9dGFFy-Ut3gB8Vnu_b1wnzETDDqWZJ-6bo9qRxrRAkH6q3TF5VTKv_hnYKY6QzcmotJrdTNPQvwCztcqj4c45FtJyax2tdOQo4lhoqDapMA9TawQMxunVToG8YmNP1YKJljFq-ZFttAxcnIpaTYq9scd3cfS0S63cnjaMT_H_LEBW9FedIR53Ko12fyQn9cLgErigUWMWwgdTmE2rPo3ygRky06cEcrh6zUtNb5E0Xt8FnmR0n53wZbJHsX9N6ficGSVwanB9ZBGJz5TmRHdF2aE6NrALFCVLZ_9mUP0XVz9HSUH9YbauXqYM8afLJ_R8XNm1WtqX6gWkCG4HulNtWURyTWgVuQT4jiB392QSDulnwnUnaFiroMxbHD6UENVgg78icspfeRQ3I_wEKLpCmngQSDvgNlV-vzVct_920i-n6DSDav6Ez6MgxCa0cgrF5Fbzak-koA7olgU2xqiyoAFv02H76alrTcE6Ooi0zNIBABz8McKSqmJDhJ3RTpCYQCmJ71Xq3xdeT-9-WBX9QgNEGQ9BAcZNT8IHY7yUocfYNOQS3XbCogSc0HR260BC8-8ijyyx1RfZB2kErTGpUCo3FQJLg8QNYU4cThUe1rmgzC1aJSHdYD8OLKHflJCHZiGGaYW_MA-tBWfHiEISIUcIghjbVjF2dBoMZBW5hlzvYWOV5y1QXW0zvTJ1Tw4R6kJGWNTK4wePkrh9W3t4wMu2QvyJQLGGwb4ltSDWefD44MtkWdfquG7OTbXqEiPr2KreJ2j3DASXuBDBD25RvlZc4bhLHFj9BUJ-lulsAvDWKCb2Bou0i6akOancevmmSZUwphs-hQM2b3ugNTsgsUEoF82dXWCJ70gyr1RFBfBsZCYDMDWbiqMYC221y5Pw2zoHRdQ40xDVCmTzDZZxzBr3ywIcE0Y_6c9tlm4e6EgOkdHg5KaAV9sV_uMLbBeSxyihQgJuxA4dzQnCo3Q_owAGtnkvhQp4UgYlx2AeclHenpTuFb_t-BsO1-DV6LgRplzfXH7ocQedgUXsd-gZtA61tnwNR2qRk9dbmtOikjI7qf7tFv8r0pRbe_d_mNadmgformlLzAtUn87xkZLmcMx_iH0g7gW7gbEXnkKmX9syage0xeQ12qnGvGF-p6mBKFUM7d_8ZBFt3pSd0M2Wl1zLnK9HQJVPXjWWBf8r9UecYdpyhtZAnxREWSqG1APYDP8cPpQcewy_QaCnVqyYZRFkf6X6ch-O9sJAwzR4MLElaZ31KyCxHTj8565hGC5bJUdg_I91UgH2yJArG54y_Yc5Dl6ALUn9QgPzbqDFFUOJjwU5o9uD2XyEBYzEErekT-GqxtSGOgCFSStNay_o8OmjolNWZVRc1_aFeMUOgh_GJCAnBMs8AVNU8rG-2bL8Yn_08Lfn-QpqpZIZIVsTZinG9cCIy-nuGGUtwHtPdG8xntWD7d5rNUtro9BCoxdrnbFOkSAwCQ365HHDHG-D0bnxTd70UQLYZcAb6rkxFrENHGBQFl5f1sOWZnGhofb6snJCirTWsgJcst54Dzu14XaX-57i-J3gi6pI0alrVQhxukhTtV3oj42A2TUGD6Qb2P_PjwhVbwpyfkd9tNTRT4YKbB6v7FviTl7JKRh_lMFAeLiNc10auLFBnXOdq28pbt64ilr05QoEABo-2qj0w1qRgK1RfdC_x2WRHcrI7zWIyDONsyqumIklidGqrEh8EXCSg3a1PBLMIrUfkfyV8C7LvTL_lifHl18bZO1BJtoksrMcCmPiwEJhCCMn1olm_DSh1YHahgEFrP9PhmLrFpJrymDuzXlWENX0QfqD8_bsiaIC7sqi4ZCnGI-KCnePmdiATIkO1ROI0ty_1kRce2LFztuwYFLY_z1yJlFflviLtyjU2z3F8Dl5JjO2dWm4n7bBCRT8wAqp5eztDZdaiuQUZKi9vhIuEnqFpL5zQVTUlDpMWodeYlcEZT0pQQamulicCkRslA7Z-CThZgOW3QWCv3eYTvOlZ0merHzQFxYq-8S_0rfwK9BEA1xck28GdMIXUd5cqBN1kUPd06qbwbCAgVBABucXvWbmkCeokCXOyfxb2BHl7381ZWy3_U6M0AnKzxhtYBSmBjY8sQAeJg1WTQ0ZpbMT651_b8ipPHAUl57j9rwVzxrdtmtai0VoUVNv4UEF6gDR_byb09xWMXgCWHrBMbbs7KNNC307cI7lmSHDwFDiWjxXcZtGMCix71kfh6uZsRBursMcnUoIaGvd_Pqv7SKeo3c1DXs8d4yraU5VqtmvHuodSmfcmOCEkzLb4lmVfBZPrsJQcLb9xFH8wunqxWYhr2ERzOJDZoLIKNwQnPDcxoK7UX_tLfbHKAO_CcfHWRgB_NkcPVvf8jViQRTrskD_19WqQFq241yN8yW4a61C6v-9og8yJyy8BWPQdiKESA180YGsfujYRx40jXR1u0g-WgRF35S97vOzm963EAkAmfCPBpRckAFxeDcb9DfBvhihOeaQEobt9UNhiDTNaiSN_Hl66wA5DIPIptw0_HQQLoVQ6HUevZymcwe9A5p7_AdCf86KBN-Z6cu7-5OTmctbwROcfjMYjlJLXI4vSE1fY_BdaYPBvPWsGaPKTNr9kwy0RyDrYd4a3hzDBzEOAGUJm14pdaOSbjtwoIJ0m5TeQRm-e-EBqxv4dcABhod1agzhWgyKZarIrtkDhGW7dkDqSdxHzPCxphtD1a7SD2MdKfz0IK_IkPRSr5N690e9kBMO8r0MmuMg85Jf4vA3w3-ywnIbaW865qXxkW-3CYgJ8RloGuBcJewQH13Ozoz1FAlt1Gt5Q-uHiMokLpmbCmvGVk7xPXqDu_sqRhQSjlEXRBjmGzeotBxxhTwmzqZfJxRXEdmGAtrfqva6gzYGgSdXFWo-_wfN2-DjBa1Z8FAxpmT-dRPNvaKwOmknS-tI5xi2i7kzmh-oIn8n-AJ6WanEBaFc5vTC9SnQNxnjnnbTu-bRMj_KlXXpw-ryvlGEGhdMOqfcgSWzQLPBSVMJpDU9rSZMfGl77Q-S3q9mRfjPnd6TqlNfOskpiQijqlKNvhC_D2S8SerwBOrWTSZ2i0W2NKgtAvkgn1v7wHkNIp6iJ9CU0mXIobg1uDrdvReirxIxuznqXyf9xma99oqKmQvh4dWfhlQH-a8AB1Hl624CTjEs4CcoZfCm2pMpcDie4gVvQiGkHQosnTdOA12IX3REq8peIyawJpoyI50ConQxCFuWqKfZkxvaLMfVAHcpvRNrNEF-jD1lf6R1emRB8jW6iQLCKYVueF6qfUsmb6Ql-gmKcakkB71QGMSGTa91eBg--S11MB79NFQdZhQDpYYc5GAAKTR3PF9Cj-xk_33qn0Xz3Xw5jRTZqm-qVcqPMwcdxcB9p8JhtWuhGcfyGmON9hM83JHg8xKGUn-1qPOnvF1yWoRcI6wv7Xe3jfo-_RHLEwbPTbihfw2H6ycYxEl_iz9zlG40_WNJwwWDdHn-jsau08fNxdR4WC9FEvC7lRAUeQPVxUWE3ziJjlDMeZGz2jy4daSi-LY-QZCzarHtQ4_olBcW11Q8gtV0lOBrkATxbd7YRAL7_dh54Xw9T6X0O7TlpofzzAVMZzIn0iTai8k0eAzuj3DT2FiCHAh4-RbKHr7mzyrPQ0MUmJp2PomCnzG25BUbYSlClBcjtotLGm6YuDPzB5X7Lu_vH9eRjxMEh7ZqIYO6m81D0dwZO9aVZSSwa_LBb1iBFrHijTsL8rHXXcBSnp_jIaZrGLyKkxMaJDegmLd8HdgACP3rOqVCDg1n_CVE3_jRaqwwHJVpani_j77aSGBmItjp7HqbcgZr_CVMCBHX3XfzlhuXZkvBoc8ZaYYifhvgGFGEg0jHEaxIIU0QDqm2L6dHqCH6yAlkkT8zRgWeLH4Pey8nR2KTAZP55YtaaU38cUPOqVlvTmPihzfNHH18h0vLfaPPjA712C9V3hvVACSpU5SsXQU7NfnnIO7_5ZcX-iCaEuDsSFlJcAJFaSyKJh5kcXsGdRCAM5nVfyH6_NFHzGiNWaIqc-E3Yl4a4pS07bpe74bsEUrxUfdgmY9XULfNwuGPVg4qBsSoS8coVBn5SxwVR6OITKjr8Iq6b8EZZxxc6qJJe2Xd5mExe6NxAW3sClorNhS_wwcBYwj6HUH8SmXpZ0xqADYVqky8bn-pa5j6RFNSH5zz9deI4_1ioLhkVtvpbRFHOxCPzm56wjqQnEci9QQd8axmpiKgHP8HnpTzLHO2MgqjjunSox4sXOz_BEEPWghInV_VpmFb0KN0B4UH_M0f9Yar4O1unjCGwlLF_ZfLfNfwmi8JoDRMYIyFn6D1PxQgdBBPKN0oC_Z11E28WQqTORvTJqusVY4qoZ4d1FOkd5E9srOWuvs0gBGweaIzUAZHdRGr4NygezGmf27uWSos68ZHaB2qOc79z_TpsXiVeik5uT-pSbt2R-GEIeg8cwCH1J2u7UHsWLmJFyUmBW3K372QeHxoW8UKinTNg4Zy6uF5acVZmom5E8s957-83Qcs_unrHFoUTPy_KWoiqRefrQcpmCHra-JYSYwNxfwgzoCp-EHgl2ypCIZ5BpRQHgKweWJWeRhioSBwGejT7evYEl3-L_FazZFY5W6tKyXFktO2jIySP0NMGxFL8S-PWQERH9cdm7l1KN849iSIqeMI8cROEUCWjUIhdh9pXJnY8vYhQBfbEjJ2fJFjOEtT8ARZe1jBPNUFdoRph8YXVXRkHn0uw826uIzZGnacbNgRwgNdilq-j1Rj5iirOQwXSQ1s_L2Y2Gl8O7YZ_tuEek0ovZnebzesmYKtoY_XhunbD_U-4afK57BtBTsmm1Ed_AwfhZNV_vqKC5DraEE6c6J_7d1f3NJEMVK-QDm-iMLGdLHjOr3bf8TjpeXNjITXiBZ0kJBb_qf7Y6Sze1UueGWd_23NVi5Ufe8w--C9fE3YT0Hl0wnSRJ1WvOGlLQf2Hgk8KaazMuCVbkNFzjojCQ_IrmsEz2sbWOSMDB_E2y-6JJyET54mCpfMYhdHXVhtbAH0sdBNtp2KGfh9206nOJU-lKwjo71lgNm4XoWV5Ux1LXYSeN9r7BSrpirkFIqxyQkJez9Ulcbiz5ES5t8oaTwCOnIDE28Vy324HhGPSi5W2QPkCOV_PjOWCeM8yjS_6w_FnGuO_26ecaOEkCNBZung5p0pHSmD9D0SeQ55YvwYvwMhT3smiwDo9dRcFa6sigkWHHKtBLW29sYLB4r5pNWtHd6CihJCcG9DTTbaE5qP0-eOF1l4GKEhtIUKDPGJGwEzYHjq9emeIy1uacdIcWTCJylvCVOHdWmLaD1HefI1tjSyga1LuX-uZPAYEu4H3BHd_8RhEhTIIR2W1Zi4pcy___Mg6UnxiELbieUU9M-kBKnEG8wm1_VCAJVg6GulXQG20z5Zq0Zr8HsRUEpcO6ULm-_3zF1WYWSPU-JDi_ZiKxGdLOidzU4gb-zzrrLYtA2USFwdncVimCESLHhKPSvv6r2xX5Hz0eTuLmhshN4wL2du7QNz_mLVnI0aIGrHWQgs_DEy06L1P4ANm_Y-0xdzookmfICUGKChRsnNFH5Ardfg5JWwzC_jQrW1XM_t8g-3Hnv_A-UzUyJWBl3ezae1NPikowsbMsIwLuHHteDmQmqb9-93yiUdXB9FxycWFgaPksF17KxTvI8FS2PPwZKsSOTXMQNCQyFd4fJDR60nQhm19DhQImTl_QPvqibTAg_p5zlhxlEFdMKoMEdSrqovWF0mKoOLbIHlGum-tDlq2Ll96PE2-CrnW8NyHVDdew8iZSZ5dahyl3prZnh_EiRB8nNBESy8uH9ppuSH6XlQ0TJXdhwI1ZdOJvFonZ-7IBR1TVb4ynvpzRt-oWE-tNx1-6qwSJGzrsKnn1EYkDQaRj7nfztiOa9af0LGUR5ejBaZVx-bQ-75PO-xBTxd0UpI5kyaEf9T3rUM19GzASEzvIwPCPRplhpopMmPORqBqg1oFxqI9vzahfzntnYmWEBLGc2ks1NZWq1gLcSZLw947_EEGgyqw51cFGXLaB1DeA85qa6WT1jRmS4Fjj747XLPynyNH73NU8RWsx03F0y_fvUpPGS_vaXWR8AhEy-gdBW5CCYbsPv7WB1Ls0_DJMBSHylHgNQvC_5knHobolZyERyyye0rwmLca0TnAJS0QhgywEwaoateT_H3_aqypXAFQdqP9aXzDLINETQH-jPND97CG-mhA5bh_mmulEvQMxHyt1e4d2IWPOJjYUvSj1gaxoNl8C_v-h8719rmYl7e5jedHHzYQuDgq-i4B8HlQxgLycD2vQqtt9F8fadudBvjaa4qaHQNw_AZc_8aWNUQ23FdSfC2ZSwJvYASGSz5iwwZotTwF92WMyzfnNvdjFyluEZR4D2RXnYP9GUuwGcg6LvtzjZDq4GoOG8cZEqgSQpSUFWN4-NUVBrb8GLY-SDo08tW7Q42PvN8h6h6cPCpFgrKFrqEuNupBiw_GvD-Ihj6S81070U74EpW3yin5jY5dVGJO_Q-8GBVsyfe9VyPGlDCt9p2-FwvgP6aMZnWAQys5HjDo7QxHaLXAUAJEB4HJatbd3sDYsC3S3Py-_NDzA9_JuOI4iqvOjwf96mS8xfOkoDY0CyKso6cn7BWBDbtgGL5yjjAOrsgyRzALWaUehhq0p48D45hMtJh40lBfgA2QkEqXaqlFdooXKlfyn0nePdsQPYJWxg4O42Up_ha9yeggy_bdTtWJQlR1bpgphhsDFFhPq3rrrD54e-AmMPvLS_KnhRHR22d8t80bo2yhrXzT612iv6Z_2_wxWbm8AnUB1L4t1pnI0BW9MLhU0EC55f52wZCJQ8wJdRcH4lbuUsZ4ioBA8J6X-UtP7YjjBTeXITfvyCaLvkwGseuU4DCiTHh6mkqIq6ynzsg9kXqjCB7oDfO8yZm82JEuzLWaReeZSub0J4FAyCUQImgs3Ui1shcwK6IVbk57-Gjywva17R7qQhkYxqeDCbrd64y3QLFBnhiYSN4TrR5AaPiNz3eCYFYPTdMjNCWa7HMb8wgI8Bix513uKuS7HenMc_h1QwCzrD146GKiiEZ0LT2IIDDO8h_gKx3Y-7N5B9Og7wjsDps624fXnr889NYznFOBwuVhNmT4aULq_L32VNXYO7bvGEm8T__RrBnigqlftf0nHzP2U7gN3kKnuCg0VryDRRs30No9mmIxpCzEkGfEDb3g8SxDiiyOjZEuFTG-doTdRDPfe8DqiPTfJdFWRfDkBKFbpnV46-Dy1PKe1HdpoF82ggBjtwT6N3GZ4MPq1UVYQ6aiwlk-vUpetZHohzn1AD15XlDE_NfnZHhvGrHGApPPUFCMmZRmqQTkNH4IEpUDQM4_SacoAIdkrgHO7PoUAFoHYMpumQ2pow4VTR3mj0tpvG-iIBbcxvqc5XLQQZhXuhDVAEl3p8HPTDKqFgxTxiKT_Ns2pfkp7zHS9-Qp6VzlZgoa1Kt-ipc-BOpwBzzeDqg5bOYvDF4mySuTfNy7RnMfX2F0WZKN0j0Rbo99iNUgkvxQNTAsicaZGuGWaUbgiQI5OT_kltLhbL0Lwk4AQpgKHQ0OBgIYC7ONSWNWlHqRTR0CGRYRPPB5tOfzJ9iVeKQKgTnH-PTukqdsxJyrwalRgF9I_b3qBXCFeY7Ea1JyqYhi2c1OLLoI8UJ1kNsH9Jsuww0WjthK7U5KQEHkQTZSjdEyoD3M-daQhocYGcPqRLqt_kfDWpA9fQYJVlMCUL9aQuMdYVz0ZzZwV4PhAoqep2MwxErhdjEUPhqyt4mVopZW-Zyigqpw7ef5K8lrBvtfLV3rt0hFTzuxACp1wQOWVsYvY36I0Yff9iHGHaOArfsR0KgDgbNK7E7D5CtFrHyOn5XGjWcdjLaYKvCJ8wKrIItOXpWEMxBCcKsKsj3bo_jJKiKYS5hVeaznfwc7pi0J21-4BAkb9Vs4XqIcooEFbUlqFSxWMuBokQAsxBEdeZ4ZEWbD_jZdx8NxELKLxPuKiYYmaljKyW4NqhyeGPgFxeHV7PC8fZ5O1Zg2sTMkW7J_BkZte3oGa9zeENRYMYmVp90gURGZ9vex7-GM362BBH-Uq9w9XYGL_yVfylRVU2PGoCEmMoxqgxsYTt6t--noIEO67jMxWhOdX-i2bLo4xdZnTBBDiiCwDLBM4SS5FWv9Q1b5NO8GL9ePjw0PEowJy6Lhq1MEBrQSR_AiNr7tAQPoJc-ltUMtBCn0FrDKT8UZchBVaMPazNXHJyJB__MZfJLc36Pr3xI3YG7C7plb4MOzJ2UU7knbHbcGM8WqKykYOBlde91ywezS-WEo8EUTO9rVUTDPwSPH2NjnuFnu9cEAmXYicqip9J5WLcnWxKuo51O53VaSXa3KOwkRsh86PPoxbN_6boEBx2b78eQOgVrE8T52OD8SryaCcj7GmHsA-nLWXhAZ98WTCCR_O3N3JZSMDB8NNKaTdyjILTThzcZBAMHpCZteh3JxXO2kiw9Q53cCVt-PNAVFwgANiyFFW00sGKI1VxK2SqsCXupmVQqzwJ_VN_KyQfh56xgMWxEucdcbneMoOWUzDZduKIBBhM3BiiaidHeflnpuDid8poBugQVdxNZdxxi27cdV7h0ieu0WAJj5G4DjNY5XI-S3cilYnTXUNg3nE4kQb6jVsjVPKwS7sur3AvwPld2qHJD5Zo5_63axnH-FQuiA2oF7pZxoYiz4IYY94ydG8gOOYteoiwEDD4tDi9_p-Vh19qsJ8NyAaC3sO1mKZUhLpGX4W5vXI9bONL6KfiZtpGsNOS0al73DiqdLiFtAcp68geOr3ym7Miq2xtthT-mCiNOn4HugT-rogZbzPlRK3aHEY3MsLL2BBcPue8ffnazWOosLQuThIGdGwHxSHwk9crZito6H3rfhy5FQYRZELbjkp6XwSzWqwGNh5PvS3a4WxLOImjdS_SdeFFztTbz643sos675Aodwntlo8e97352Zl54dJVBWQQQXZe92VNcHdywcaHzSA2NyLRWz9kJA4R4jHUBq0Kd_y-f_4LZMgcnSJyB_kxotskTdJvy8K4VSB7NSgMxkfzv-DWokMaWuZ6i9lhG6laXjt8SzVmZnBXx2fcGgveBZ0cEEy_ZAjwSaqkircbn6rIcmwjOLxsSvcyHHaB4371u2OZzhoM1eRQ6I_wXHJP2FW4zESJYPOhSWtJ6Apz4rHoUnlDCcg1MnT3Q6PvRNDq0jB26NCCl4ixvXlWtuWTa6_bXBARoDauSXsf9YAX-vnSTK2lOz0pOWgz_QjQw0Lx7nEi4sMXdnGvQNxkSiGAmExZzqAPZwMGbdAJUnjc0jW7Fi28MG3G8cHvO6fcGMo-IHUlH1hr7vMVCViYqjcZQOJ6YgAQNQNe6mXCcsSJij3_AeMXOJvC55N2l9GkRBkByX7-NO0zWRMGZdtYxe-25RMM46v4AZi3A2mH-31HphZ34kIlBH9yb-8Vw4cdUHpY42kEhnXusSk0gx_bGxqJRVVpVgo0EAAAkhSRkWSqJiccp5iZ1yZ2EpHOgEM1vthLyCualal7K-fTHBm5jSjNqNNiZ85xJF3tbnHSjLNdQ-sYcUnhDFedPfS1bzfVZrJBfzjp9_itNRPeJnHhYGe-K9d5TQqjrBAtwrGnMkGhpegfK6Ac2Nklvcl-yCdX0Fx_OYe6peI4slr4S9XmZBj3ZpG7PX4NdyAKDu0GwufKIcSATJlFk-1L17vj-b54H5iFj5472wPjh-E9NJ2UWS5GbEC8TPpqw5wQH_Q4KnOIE03lgzCcImIKW4jK52uCSsBljKI5CXQzgTj2lR2lf7OqqEwyuFP6KEm4Gbd98fASaqrgFmR3CBqJfFkaIeuluglEt6hbkIQU4KlhVJ1kwkOq23gcjyxC4TXYEBNake_62MYh17xz5yxky34x6cl8B-e14KXqOG5qG5ug3gsoD334ICr72xkt-m3mICgkUYOSBE83pb2AA7YuW5IqwTLStyt03wQhYmDXd_q4FBM7ZO-uwue_cT49vvpDHBAL7zwG9if6P_wwVVqO85qFfri0-S37JXpakkJ6_9SUpM18Yo4g2SbEoFLE_psEgmhRAVyGZjGMCU2Yb2Nh6eQaVhuiciWgij3Hf69IJYKZ7dgNmCuuTMp_VlJ0_bDWGlAQZUvZoXemSxVUvOEMjNj0JxhAnuo6Pi9eWLcpy018a71RUAcCrdI6NLvPBNr6qYJgZL2YE6lLe5kN2xxuxtNIm0PdkyvAo9N0OGwXOkQcY8KxwwhBPI01FGQ1ULM51ICIEBERqQD5-RkIAICNR6o8zZD-6Iqah6mvg2OOhpEWzyTuIV6y3d_hOKpYtdPZ0tYpmGdXjl0CM6UZmUyAxk43Frunx0UQg3pA_Awwu5YhXCPek64_gbjQve8bn5Dxl6ZAvBAk85VngWQNtjH4JNk2GABmghnZr2ZHWhO_GX-q3KKTyOqbUjACY1il-tUhIs0TkcQqrYLRMXRrSACeDKw1VWm6iTI_6IYfcUGs_H1Y0fgyCSI3lq3495MNy-dbp-G5WiAQCZI_mqzoxTcr0EifYsDKQuzpSs4e6e4beFerRgJmLVr9Jgo9heM988Va39i0Vo0AEIPlaZqLXrAz--eT1xxSdBi6JlxKS2uzYsl800ySl66rIKPUoXdkVni_F_20mmkwEGCAQ4ZJS1g52aDOSjCYPuP4nUfCCL1868DyocogHBIwr7PCQ4-_0e7rKflnzCoPtETbNRKJj55oRaiAlFdqaTWWSMp_LjH7w0GFXxzTtnuur3GA3QaeaCO9bIPf-kiFhBArunZ4iY6SdxqV2bu3ANgoc35zfPy7r4wZDnS2BfHFn6KXRHhns5yN5U-OVjT2pIBWbLxQj8J8TOrSGYkpcTwJ526XWPKA03qIn2pOEe4wUDkW0tkxyyIgt5cCjSPWhhQQLsYYKJ8rk2ojWvIHSdHSgIof0eVI51RGCW4jcg2pJ3I25sFIfpgqI5QipxB75eTIB32XCBtzWmK2E6dPAQfnHNPYITbjLmOrH2f6zbW1_LJ3LVtMMijseSomNhA0v4KUEBy5aOriMgwBRc2doCITBcWz0OD6TCXbcrNvW7g6BDK67Ym4Vpn6bl3B4tIH19TNQB4YhX4z2kAyhlOOlvwqMcfhtdiNxuSZ7BAqQYixn5dDpswpCqiI_MjH51TMikt-YBBCHTr-RGRIXaWxk2sTl01agDUdyWGJ8wsP1f0ndpLm3fHdejNab0MOn6osZGpP3ZgZIYoX0o7CoF_5lVDdc08Dt7L_yEmzk4ccF-JQ0JtbfYdzvc4OrUBm3zQfNVsdw_AQHE0H8y3wolZFgsPzAOF39j-_9SDKkZQAHkO42MKEBuDYNRANGd41ztyybua00Dn8XEYC7OiWofp6CNgeFts0oXhYM7YU-0A8h4n_xVYrk-0Rb-zpprX3pmPsLySXIDR0EBHRdi54BjFeutO1ODlZUI0JXKinpc3TEq1Q8Umhk5Yid-CmzYfaVtt65hsdKIybzDgZkBSqOZHNlU-qgtHZsZjB7HhlsQH_hsJMfO_GDYmvUyL61zZ_6i-kzVl9kQzarBALNWbFaReiu2SG9cY4n8raKYyXQxQXE31wFUrKaibEAXJlq26xQzmZmf12t4-3ZVxMi15PRbREWLYGzqNRARqU3mHd3_FPTeaLxcWy-KfufvSTVOIYkKoAXAbHfGckSZgQMlCPqKvao0Lss7N3bdcI04kJRmOcExYhAXvepyznGreKpfwWLm2YpoPgFuWq2cbkOg_KNOxeI-SCe8WL5geA7u7S-PPZZ89jarsvO7kPAIQXxHg7a46y9wzDLclZD7UcECTva6MEKRlMP5zsg4EfRkmZ8AQcykymQikio50dvSITkyqtD5XLkLYv2eypab6-1CHu3z-YUQSHYLOw4fsU6dR8lToK4I4pl9auL2j4z2FqwZTt-wnGkTXTevikprpz7BBaY78BYmJHquSGjIEoy59aBoFNWsKLhyB7r-JFAVRXgZAspE59-JmzJVSIfyNWXThYFzabEXW2VmUNRAcb2pRUP7KYWY8xqgZTvQZ2mtXQBY4GpAoXR6jgH-fmWg988kAQBxRnDoZgb0VqOUNQK29C5BIEt8CsHE97YSouTsqqGtATh9YQUinkIpjyHMAYRfnkMiywoFYeaJdEd4DFPIvJ_MmDWtg43nh4dbJahewqSfAzmFH1B-js9WAG7bivifCkEFdHfWcyDybAKICp2iZ4clqNYH9EoSgYJuDnUoyHrBvhWbaG4CZFi6bALdp68fj_7D6MCId76bo2D47SRj-q6bzrQFHvrbfK86EdM5KbJftG9ieNvuE7PjAEAheezl1fxBBKKZDCnxPzovqnmBX3mnEy_giFlxpBfUm7g0ot-FrszjXCMAcw4PNQchogsmtV8zQ8XZOo2Rlay3YmS9-nK2Z1jEBXckY8C8y2IavccKdbWAOUidl9LsHe0wLA0tC0YcAQH5HF1yfqhXeaUXmVA1tF7vJW6tBMsm443zWLqD3MvCjC6DoUb1O6IMaeSwvS7spYGuleZPr4OvXuWcylIBgHS8TlIwoo4P1zBFAlYOYCGsulS8TBKmLxOWskPS-grktYEBBK-uDxU9pVaKCMWy_l_LV8-r3z2HRajh54V3cEsSiG5CF5_EVeFJzAzQTGd79k-AjLERnGw7kNMs4LWMhPS-00_R3nRt_OPxiVnSY_vNyT3HHpf8Lf7NQnZQQ7jM6d3BBSmIUlvlECPBpaVgP6oc1FKSkSPs-6DGL-DkJW3Xo0WlcJKwl7rIXjCrM0t6n3ioRNkxBOg3grZKqF12fnWOn-jtqr0V0Iw4Lf-3Gh007OcyCIy1-RENp6DXM8JKsg1XwQTo7OfDfyf3ZSDWOLan4L6hrHPXKBKtk0m1fJvJQ9dwEM3jzPWJBilBQDI_09Nr2MCbLzNTGi2wzGMlMt4B8u7g6B5wmRWKDZchS0pSFgP8B6maEEZ8JH-c6p7wk6YfeMEC2Ih-KN9IEUvnsh-b6jj0FwcqtpWKlHBJFWJtGnXMT8rDuYX5Mm_-lAWornFLriTA8I9uu1ZOGiej0pWVgoQVWFawXYkYuoZRW5q4OGBwpiPtZIYAyDoZeAUOu7FAqrTBA2NfYfJr9vsXJOaDiYPDHRgf9IPb4xQHM0YSgpvkCDTERAkFVgQ0lLemlf2qcUXjgmQg2MNuI1NcMCu9A9o8-g15M6Sswsu2uLf8PD13MAUsf2bSudfdKaViZvkMCJ-VgQKsy2y-9J6nybC5tzJ9S3yfnlqMyHkbrxFAUf7NnocSzZcRtuRUpuGZsx20gb8xHIA7aUuwd41zsDvsOUpovILruvtFXnA2_18wbHXFKUGmKPHYYGLsz3rhJNtjs0dZF8EDD2XVmxsow3EHn4CXSQkJ8x3D5sDdyQE74fx_9l-BybhGK0-Ww_qLjHwwArVN6GcDacya-onH823CihgmmZKN3bg_XP0Q1c37IUApEO-R6ywQpAOWGv_re4uecj_1jmbBAxwRcvCNpNSwoGTm8_KSozpV6-vadvp_RC3TDHkH7f97yLxJ7ROIt5J8cQl-9eNJBHtVvWv0H0oe8V42gg4FsXB7_Fv8Ou9YUFWaJYb7FVU3IyWGVNYJyPoT662ImG2kQQHTzoNdHPdqTT_kh421XyfaJINAHA3KzKTcOq_4uNp3hq158xepsHM8HLizQKPI_oM3qvpSMxj-BuMVfkDGTnsX-JLAe3NA8yuFiZXyziuYw6hC4rMLuV5UTNJZnGS-3EEGSXXHCfghBQslnMt4jDj1X9FYwL8cJCmPPC9sEgpCfBdPYZCJUjoxwd2i4Nd2vweECi1KOOoFCdmTcDcp6WmlQxv06XLgfCiyC50yBmqw034Ukq2IsrYFPDsITQIQG_HBAe6k-2dxanLxJGlZK6CPCx2MKGElRlIESSqa99pCuUgzdvs-_ZbG-fjr42LTHtP0hHJy_ngCjrt8IgDmUKI3xEvlXZRnxnp4jkH-7FwZoKkh01DjFYkAscw5BjAlcWFqgQFnqle20OyaUTMaYIvjf-0ZUOpGi_wab0RYW1i5s61xvKyIk_2evZ87LyS57WccbcLy88MJ26kRxPMf9rOcEetd1aZxykk73d7A_pj7zxIrvjeExHyxUrM0XFgLN79kvoEAhyhFdZ_FZItdc98yLjaToxZPORBhTn1w0nj4spz5FjshbItFfVLfGCsAxgxRI88AO2oB8389PNPMe8tA4uMPMC2PFTqK795Hek8Vos_khmzeiXwo1BQaVfwLglOeKhUBAuoVvCyh93vTjhapy14oMAt24rP1eeHnQjee5Lfb_8p3gXOMQ39yxQ0Ts32B-CfxQzbPQrRQtJls8Y6lVDr0oOFz1gMHDWRrzA5z3tqHpj0Cxe3R1luIIQ06DHrv73dswQFCY6mYUsMfumIz3WAO0sa7s8fzbGRpG4zcA5_zxQpkwOEmTbBf8n_7vCRaS3weOMVJBuNSJCiQGBHR2eESoSSbV_ESxcoPGf-Wz_Fam4chWBty66ZX9gMqaAE1zWKAGMEF9zlemaUpKjF_NQJkTSbvh94a6Rtr-WR9QhWFzNxPBPIxItxGb5yNTiGZ6Ie-tQJE2Kyd1SmcfUY5fJnCdItfpnyXL4WSAbSsob9XVg4Op0uBGG4yXL__kme-X8WI0wABAACDV6iueeDk3PptXUV0BSR3PCdB9sa2FWGoPt81rhXS1voD5ApICH0CYlLLFnsnBNNi0fB0f7ZKC8y4286yDEl0NhkKDvq2n9HkwBGA_oiFOcGotvk5QXufiP82pBzLwQOow95Fx6OM7HK_uPVjzxxdawXQgSdHoQiMJwbUK2UYbfr0iYvGr8ERELWRTOOiBcZYsSsNhYHMvwVW5ahDFqpCiW8JJOq6gjlJmZ3cvwVWD7kgLmJXMnnRqtqaYl9Uk0EBEw6CZI8R0Fprd4sn-AM5SIgL6PkVm0AsR9FkBxFO5F6x3-DMWIZnbpEFcOjgpkwAtbmPtesiKe7w_XeKXSYKPfzCM5wyVZ7sq4BZaQSMzOEOgpFp7_W4kjVZuWL4HvPBA0eaJkqCCnO9CvTPynRPisSgqY5zcysrcKLAAHSQ247c1yi8smlgYsFznlptT_2rAD8h2xfxUSv9KDaokZ9LROVtS1pGJumZfwAKuHqEis6B5GAG1uZw8SgmRDB5-_dcAQWOP6jgn5PBB08RKA4xGMxzHTTF0iQgF1HMX4ScdvPmR2tC1g2_z9NYw5VvHewjIQTVUgKhl6WkLiggz4qCItjEQ-sQaFctZo2QgTphAAhAPbVVKGmXydWSPn9-MLyRxMEFd_MFPx0xEKWUtWopZnXoAnB6cuRUlaR7Ex1bd9kSJeRT-zS9vg6SmVVeqqF10HbBydZAp2CPsaAXMzrohNXkjT1tHa5DFsGCWN8Pl96gZ4XU0hcy0-v_g66wmMXmP7XBBUEh8wlJ2tg5_32LC9uz3mUecfSbUnNnM7jzPEBx0MWh0T5W4oXWkjl0JtkiRFaawUveTNuckzEnkGqxWKC3Pfi-4_c19f14CGUzZTVXhAWYKQD15Ldl65r6xU7U87dFAQUOHcEY6KUiQ-xEZztcLU_KDfunv1hTy9IE73SiYpIvhvSeus46KY7z9D_G1Hw7nQFhHgxspVLEjejdXY5Pms0wE_YhQ-bkrCOPXpnJxE194xSi57ykPsPH5TBygVP_fwEFAdqOPwiKKQ4MV-d2G2-omn1DCyqoL0Vc-bvCee7FYytR_RFO2_xikbrBZwnj_buFvANP_K1TtKf04nY7mjKJiSbrTdpywo8PvxNB2JpBD9gkVPuA2oMFvUFHHownN0jBA9yWmiKpQTY_ZqT2TR2bmCTmwL3sZEdPVl0oaBlPiFZbDTLGgF-4fBlm_xZl1OiAhj4KxXwB7w_DqvCS0V34A0o-Su4VjZzaEqO3cTuPCBuJRfnExkN0QMMtx-OMPaumAQSyZ7-x27l3q_-q2ABDt7hOImYxGar-1FLvfxxmv_aAUPWCKHHyEk-TpdjgaLYs3EWC2FD-DNMegViiW_kEhe5hNwBo_JVCn82HCUH14yb3mZwFNe2vAp5WvSVoSdkBCgEELEZw33U_IZSQ5fm0BtguhMiFPbE86oWsZYU3cs3LiC3hW-hEBIIiqIh3zxWg7Z8AcaoK_0hQeGI2DANl22GKyVTRdHgB6Vv2Ggz-KqB3NYkLJ3AirxooP_x_mqVVoIj"}}],"authentication":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"assertionMethod":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"keyAgreement":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"capabilityInvocation":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"capabilityDelegation":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"service":[{"id":"#TrustchainID","type":"Identity","serviceEndpoint":"https://identity.foundation/ion/trustchain-root-plus-2"},{"id":"#RSSPublicKey","type":"IPFSKey","serviceEndpoint":"QmdPZgcyqHJTiPeGMcAu2AAkZZ1U4KtdQXid1gdJQtpvyU"}]},"did_doc_meta":{"method":{"recoveryCommitment":"EiCy4pW16uB7H-ijA6V6jO6ddWfGCwqNcDSJpdv_USzoRA","updateCommitment":"EiB8B_LS_O3NWo2P8fSuRwS32GODaXoLREZHdqpg6x86yA","published":true},"canonicalId":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","proof":{"type":"JsonWebSignature2020","id":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A","proofValue":"eyJhbGciOiJFUzI1NksifQ.IkVpQV91YUV2QjctR0FyRTlkeERuMk1rclRUa0t0VXN4eGJPc1NESzhwQjl0ZWci.X94wTgzsovLEAXU1CG5M0Gqs6Gu9oHklr4Zn7aEbrdtOI_WCSCrWJuYomkcdeF8X5dV_ApZ6Gh08pPcV2VSClQ"}},"chunk_file":[31,139,8,0,0,0,0,0,0,3,229,147,79,115,162,76,16,198,191,11,231,240,70,197,8,230,38,50,138,18,64,16,212,184,149,178,6,152,0,242,103,6,102,64,97,43,223,125,199,36,149,125,79,123,217,220,246,48,85,93,205,211,221,195,211,191,249,41,68,40,103,144,10,143,63,126,10,4,178,48,65,31,49,12,89,138,75,225,81,168,17,201,97,136,132,59,33,194,97,83,160,146,9,143,92,218,4,121,26,26,168,251,80,167,17,87,78,231,166,231,74,81,187,108,47,197,196,176,59,120,0,96,101,27,10,176,61,134,50,177,148,213,221,116,187,83,235,17,160,188,27,235,8,226,69,107,138,203,61,10,182,105,92,66,214,212,104,52,24,13,248,215,175,254,235,75,118,155,23,214,45,23,83,20,146,209,195,36,27,114,69,198,58,158,1,115,30,94,121,32,187,200,209,245,142,46,175,65,215,24,192,41,40,113,236,243,147,140,29,191,210,188,60,242,244,38,156,238,36,177,163,152,87,220,74,179,125,91,108,229,163,221,106,126,178,80,104,103,15,148,141,218,3,2,50,73,61,154,77,147,101,45,88,219,198,246,92,1,225,237,118,163,154,96,250,238,142,0,41,69,245,205,31,19,177,4,71,188,33,108,88,194,173,73,67,248,110,27,191,30,234,102,113,141,208,187,97,119,66,8,9,12,210,60,101,221,170,108,241,151,234,119,90,67,57,138,63,210,47,111,47,119,252,87,235,54,13,209,255,236,245,234,134,242,253,192,180,92,105,191,253,91,69,183,177,220,139,175,18,80,70,4,167,183,53,9,9,99,132,62,222,223,167,159,162,255,94,113,83,70,239,83,238,111,135,125,181,20,107,140,153,240,246,242,118,155,221,16,174,65,115,92,20,41,251,88,184,0,82,109,231,2,175,61,106,211,116,235,151,184,17,117,127,214,63,60,23,217,105,33,49,210,245,177,188,88,14,207,145,187,20,143,110,204,253,250,107,154,178,115,85,215,210,220,203,138,188,95,31,119,120,192,55,114,181,168,210,214,198,131,13,104,118,178,67,172,218,227,45,50,157,239,167,9,206,136,129,131,141,173,232,202,197,110,69,188,244,20,67,178,160,168,228,162,58,156,1,169,81,143,245,126,233,77,22,107,255,147,166,136,18,80,229,108,198,114,207,88,203,225,206,221,156,98,211,204,202,78,219,84,254,69,92,235,57,185,108,71,197,162,73,6,255,8,77,34,201,27,42,14,255,8,213,124,0,162,75,239,132,149,249,108,29,78,18,172,240,209,242,97,51,158,25,246,147,20,31,156,135,77,47,205,188,116,88,157,210,217,119,64,133,54,221,129,66,171,135,74,208,236,39,177,117,128,120,121,28,200,79,166,119,141,159,230,83,67,14,3,184,58,175,210,222,91,125,63,84,131,178,116,69,210,143,192,113,249,26,200,96,88,189,54,122,82,106,174,50,26,235,137,154,226,107,63,6,34,80,205,210,28,127,66,85,239,43,109,183,150,146,225,196,245,134,150,232,83,89,151,175,238,181,13,230,3,63,55,77,199,137,175,197,193,62,68,227,224,249,159,130,106,244,71,168,84,237,149,26,173,10,183,179,103,254,128,137,44,163,205,212,210,109,226,239,93,51,9,125,203,92,91,186,167,237,29,235,50,186,220,26,253,2,202,202,18,61,4,7,0,0],"provisional_index_file":[31,139,8,0,0,0,0,0,0,3,171,86,74,206,40,205,203,46,86,178,138,174,134,48,221,50,115,82,67,139,50,149,172,148,2,115,195,83,189,77,3,146,188,29,131,43,253,178,92,35,189,189,204,140,44,243,204,42,74,203,115,19,139,162,66,34,205,10,138,82,12,45,29,253,10,74,204,10,43,253,148,106,99,107,1,80,57,150,45,78,0,0,0],"core_index_file":[31,139,8,0,0,0,0,0,0,3,133,144,221,142,162,48,0,70,223,165,215,67,34,136,69,189,107,65,20,196,65,228,71,119,55,27,211,129,162,229,183,219,34,35,24,223,125,221,125,128,153,235,47,57,231,228,123,0,46,218,158,73,214,54,164,114,154,140,222,109,86,209,88,48,176,4,65,157,159,16,209,118,114,35,121,26,133,131,184,233,173,250,81,236,131,149,231,49,168,73,161,113,180,101,163,45,59,114,15,37,120,3,45,167,130,116,47,148,4,203,7,72,5,37,29,5,203,95,15,32,111,121,206,238,22,233,200,191,33,163,85,71,54,68,94,95,142,21,195,37,58,45,6,37,146,103,201,118,99,52,150,57,26,247,219,189,227,124,224,201,116,186,175,236,201,161,234,237,133,49,100,238,229,229,16,52,109,123,42,6,179,173,107,214,213,180,233,254,83,204,161,238,85,195,95,99,36,13,234,213,181,142,157,147,101,6,56,185,102,126,140,78,179,32,227,142,120,215,67,107,53,251,4,207,231,219,55,77,184,236,39,197,108,110,96,43,140,10,183,115,122,205,114,109,223,223,148,16,106,205,34,110,175,106,223,97,242,99,234,110,209,23,77,149,79,142,67,186,238,213,90,9,104,17,23,120,226,169,243,53,180,18,59,161,81,96,254,52,111,209,65,212,163,137,131,203,247,77,86,68,108,228,159,9,133,83,87,247,118,136,163,157,178,72,80,59,111,46,179,121,20,113,109,171,104,66,109,104,9,171,224,171,159,116,126,84,225,13,27,27,133,21,8,38,176,240,97,150,29,243,181,249,249,231,61,181,66,151,103,253,57,14,199,246,128,94,77,191,159,207,191,142,167,192,117,34,2,0,0],"transaction":[2,0,0,0,1,113,221,4,189,16,26,231,2,48,224,28,93,57,7,140,195,149,161,45,117,110,230,205,103,61,52,184,254,125,243,83,89,1,0,0,0,106,71,48,68,2,32,33,204,63,234,205,220,221,165,43,15,131,19,214,231,83,195,252,217,246,170,251,83,229,47,78,58,174,92,91,222,243,186,2,32,71,116,233,174,111,54,233,197,138,99,93,100,175,153,165,194,166,101,203,26,217,146,169,131,208,230,247,254,171,12,5,2,1,33,3,210,138,101,166,212,146,135,234,245,80,56,11,62,159,113,207,113,16,105,102,75,44,32,130,109,119,241,154,12,3,85,7,255,255,255,255,2,0,0,0,0,0,0,0,0,54,106,52,105,111,110,58,51,46,81,109,82,118,103,90,109,52,74,51,74,83,120,102,107,52,119,82,106,69,50,117,50,72,105,50,85,55,86,109,111,98,89,110,112,113,104,113,72,53,81,80,54,74,57,55,109,76,238,0,0,0,0,0,25,118,169,20,199,246,99,10,196,245,226,169,38,84,22,59,206,40,9,49,99,20,24,221,136,172,0,0,0,0],"merkle_block":[0,224,228,44,50,91,136,90,53,184,101,89,134,219,136,40,143,2,100,212,246,127,92,201,14,109,13,17,39,0,0,0,0,0,0,0,105,173,156,82,17,65,101,68,32,6,152,112,104,119,198,46,124,201,58,41,245,245,163,29,5,181,212,9,82,121,206,125,61,49,81,99,192,255,63,25,113,234,45,246,29,0,0,0,6,3,211,202,105,163,97,74,203,69,161,73,102,200,18,205,158,224,52,199,5,242,15,172,61,175,143,121,108,153,244,216,5,165,253,142,118,26,226,235,158,11,14,77,98,209,149,153,88,111,185,142,138,123,230,252,113,19,68,30,85,111,179,31,248,44,156,234,132,87,199,197,126,65,242,234,243,46,166,97,119,197,11,227,194,64,83,68,66,52,146,13,149,202,60,196,157,0,163,31,110,109,24,100,1,127,156,249,212,139,81,39,72,113,196,112,14,112,145,223,239,20,175,156,146,197,52,2,21,183,216,140,200,32,33,136,227,131,123,23,29,186,20,255,237,232,241,69,178,200,124,29,188,54,66,102,153,48,81,121,88,251,117,66,156,69,172,170,81,196,22,178,131,96,77,81,95,128,249,93,219,79,97,14,141,219,120,118,152,87,19,135,118,2,175,0],"block_header":[0,224,228,44,50,91,136,90,53,184,101,89,134,219,136,40,143,2,100,212,246,127,92,201,14,109,13,17,39,0,0,0,0,0,0,0,105,173,156,82,17,65,101,68,32,6,152,112,104,119,198,46,124,201,58,41,245,245,163,29,5,181,212,9,82,121,206,125,61,49,81,99,192,255,63,25,113,234,45,246]}"##; #[tokio::test] diff --git a/trustchain-http/src/state.rs b/trustchain-http/src/state.rs index a25b7c24..df7d0e61 100644 --- a/trustchain-http/src/state.rs +++ b/trustchain-http/src/state.rs @@ -35,7 +35,7 @@ impl AppState { .unwrap_or_default() .as_slice(), ) - .expect("Credential cache could not be deserialized."); + .unwrap_or_default(); let root_candidates = RwLock::new(HashMap::new()); let presentation_requests: HashMap = serde_json::from_reader( std::fs::read(std::path::Path::new(&path).join("presentations/requests/cache.json")) @@ -43,7 +43,7 @@ impl AppState { .unwrap_or_default() .as_slice(), ) - .expect("Presentation cache could not be deserialized."); + .unwrap_or_default(); Self { config, verifier, diff --git a/trustchain-http/src/utils.rs b/trustchain-http/src/utils.rs new file mode 100644 index 00000000..6e29f52a --- /dev/null +++ b/trustchain-http/src/utils.rs @@ -0,0 +1,28 @@ +use crate::config::HTTPConfig; +use std::sync::Once; +use tokio::runtime::Runtime; +use trustchain_core::utils::init; + +static INIT_HTTP: Once = Once::new(); +pub fn init_http() { + INIT_HTTP.call_once(|| { + init(); + let http_config = HTTPConfig { + host: "127.0.0.1".parse().unwrap(), + port: 8081, + server_did: Some( + "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A".to_owned(), + ), + root_event_time: Some(1666265405), + ..Default::default() + }; + + // Run test server in own thread + std::thread::spawn(|| { + let rt = Runtime::new().unwrap(); + rt.block_on(async { + crate::server::http_server(http_config).await.unwrap(); + }); + }); + }); +} From 2a6d0fc7b50576018e1e9ff0f8578c1d46ec69fc Mon Sep 17 00:00:00 2001 From: pwochner Date: Tue, 28 Nov 2023 15:44:36 +0000 Subject: [PATCH 47/86] Attestation CR integration test for part 2. Automate most of part 2. Remove cli commands that are redundant as a consequence of automation. --- trustchain-cli/src/bin/main.rs | 42 ++------ trustchain-http/src/attestation_utils.rs | 6 ++ trustchain-http/src/attestor.rs | 14 +-- trustchain-http/src/requester.rs | 129 ++++++++++++----------- trustchain-http/tests/attestation.rs | 97 +++++++++++------ 5 files changed, 151 insertions(+), 137 deletions(-) diff --git a/trustchain-cli/src/bin/main.rs b/trustchain-cli/src/bin/main.rs index ba58f4cd..fb230bd9 100644 --- a/trustchain-cli/src/bin/main.rs +++ b/trustchain-cli/src/bin/main.rs @@ -18,7 +18,7 @@ use trustchain_http::{ attestation_utils::{ ElementwiseSerializeDeserialize, IdentityCRInitiation, TrustchainCRError, CRState }, - requester::{initiate_identity_challenge, identity_response, initiate_content_challenge, content_response}, attestation_encryption_utils::ssi_to_josekit_jwk, attestor::present_identity_challenge, + requester::{initiate_identity_challenge, identity_response, initiate_content_challenge}, attestation_encryption_utils::ssi_to_josekit_jwk, attestor::present_identity_challenge, }; use trustchain_ion::{ attest::attest_operation, create::create_operation, get_ion_resolver, verifier::IONVerifier, @@ -132,14 +132,7 @@ fn cli() -> Command { .arg(arg!(-d --ddid ).required(true)) .arg(arg!(-p --path ).required(true)) ) - .subcommand( - Command::new("respond") - .about("Respond to content challenge.") - .arg(arg!(-v - -verbose).action(ArgAction::Count)) - .arg(arg!(-d --did ).required(true)) - .arg(arg!(-d --ddid ).required(true)) - .arg(arg!(-p --path ).required(true)) - )) + ) .subcommand( Command::new("complete") .about("Check if challenge-response for attestation request has been completed.") @@ -437,7 +430,7 @@ async fn main() -> Result<(), Box> { // service endpoint let services = doc.service.unwrap(); println!("Path: {:?}", path); - let identity_challenge_response = identity_response(&path, &services, public_key).await?; + let identity_challenge_response = identity_response(&path, &services, &public_key).await?; // serialise struct identity_challenge_response.elementwise_serialize(&path)?; } @@ -455,35 +448,18 @@ async fn main() -> Result<(), Box> { if !path.exists() { panic!("Provided attestation request not found. Path does not exist."); } - - // resolve DID and generate challenge + // resolve DID, get services and attestor public key let (_, doc, _) = TrustchainAPI::resolve(did, resolver).await?; let doc = doc.unwrap(); - let services = doc.service.unwrap(); - let result = initiate_content_challenge(path, ddid, &services).await?; - println!("Result: {:?}", result); - } - Some(("respond", sub_matches)) => { - // get provided input arguments - let trustchain_dir: String = std::env::var(TRUSTCHAIN_DATA).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; - let path_to_check = sub_matches.get_one::("path").unwrap(); - let path = PathBuf::new().join(trustchain_dir).join("attestation_requests").join(path_to_check); - if !path.exists() { - panic!("Provided attestation request not found. Path does not exist."); - } - let did = sub_matches.get_one::("did").unwrap(); - let ddid = sub_matches.get_one::("did").unwrap(); - let (_, doc, _) = TrustchainAPI::resolve(did, resolver).await?; - let doc = doc.unwrap(); - // extract attestor public key from did document let public_keys = extract_keys(&doc); let attestor_public_key_ssi = public_keys.first().unwrap(); let attestor_public_key = ssi_to_josekit_jwk(attestor_public_key_ssi).unwrap(); - // service endpoint - let services = doc.service.unwrap(); - let result = content_response(path, services, attestor_public_key, ddid).await?; - println!("Result: {:?}", result); + let services = &doc.service.unwrap(); + + let (content_initiation, content_challenge) = initiate_content_challenge(&path, ddid, &services, &attestor_public_key).await?; + content_initiation.elementwise_serialize(&path)?; + content_challenge.elementwise_serialize(&path)?; } _ => panic!("Unrecognised CR content subcommand."),}, Some(("complete", sub_matches)) => { diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index c93dc111..b8300ab8 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -90,6 +90,12 @@ impl From for TrustchainCRError { } } +#[derive(Serialize, Deserialize)] +pub struct CustomResponse { + pub message: String, + pub data: Option, +} + #[derive(Debug, PartialEq)] pub enum CurrentCRState { NotStarted, diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index 6ef6a118..dadd5ca4 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -3,7 +3,7 @@ use crate::attestation_encryption_utils::{ SignEncrypt, }; use crate::attestation_utils::{ - attestation_request_path, CRContentChallenge, CRIdentityChallenge, + attestation_request_path, CRContentChallenge, CRIdentityChallenge, CustomResponse, ElementwiseSerializeDeserialize, IdentityCRInitiation, Nonce, TrustchainCRError, }; use crate::state::AppState; @@ -18,7 +18,7 @@ use josekit::jwk::Jwk; use josekit::jwt::JwtPayload; use log::info; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use trustchain_api::api::TrustchainDIDAPI; use trustchain_api::TrustchainAPI; use trustchain_core::verifier::Verifier; @@ -35,12 +35,6 @@ use trustchain_ion::attestor::IONAttestor; // Encryption: https://github.com/hidekatsu-izuno/josekit-rs#signing-a-jwt-by-ecdsa -#[derive(Serialize)] -struct CustomResponse { - message: String, - data: Option, -} - #[async_trait] pub trait TrustchainAttestorHTTP {} @@ -164,8 +158,8 @@ impl TrustchainAttestorHTTPHandler { (Path(key_id), Json(ddid)): (Path, Json), app_state: Arc, ) -> impl IntoResponse { - // TODO: Do this properly (get endpoint from config). let did = app_state.config.server_did.as_ref().unwrap().to_owned(); + // resolve candidate DID let result = TrustchainAPI::resolve(&ddid, app_state.verifier.resolver()).await; let candidate_doc = match result { Ok((_, doc, _)) => doc.unwrap(), @@ -290,6 +284,8 @@ impl TrustchainAttestorHTTPHandler { serde_json::from_value(payload.claim("nonces").unwrap().clone()).unwrap(); // verify nonces if nonces_map.eq(&expected_nonce) { + println!("nonces map: {:?}", nonces_map); + println!("expected nonces map: {:?}", expected_nonce); content_challenge.content_response_signature = Some(response.clone()); content_challenge.elementwise_serialize(&path).unwrap(); let response = CustomResponse { diff --git a/trustchain-http/src/requester.rs b/trustchain-http/src/requester.rs index 596d5baa..9a168a0e 100644 --- a/trustchain-http/src/requester.rs +++ b/trustchain-http/src/requester.rs @@ -2,10 +2,7 @@ use std::{collections::HashMap, path::PathBuf}; use josekit::{jwk::Jwk, jwt::JwtPayload}; use serde_json::Value; -use ssi::{ - did::{Service, ServiceEndpoint}, - vc::OneOrMany, -}; +use ssi::did::Service; use trustchain_core::utils::generate_key; use trustchain_ion::attestor::IONAttestor; @@ -18,7 +15,7 @@ use crate::{ ContentCRInitiation, ElementwiseSerializeDeserialize, IdentityCRInitiation, RequesterDetails, }, - attestation_utils::{Nonce, TrustchainCRError}, + attestation_utils::{CustomResponse, Nonce, TrustchainCRError}, ATTESTATION_FRAGMENT, }; @@ -30,7 +27,7 @@ use crate::{ pub async fn initiate_identity_challenge( org_name: &str, op_name: &str, - services: &Vec, + services: &[Service], ) -> Result<(), TrustchainCRError> { // generate temp key let temp_s_key_ssi = generate_key(); @@ -90,7 +87,7 @@ pub async fn initiate_identity_challenge( pub async fn identity_response( path: &PathBuf, services: &[Service], - attestor_p_key: Jwk, + attestor_p_key: &Jwk, ) -> Result { // deserialise challenge struct from file let result = CRIdentityChallenge::new().elementwise_deserialize(path); @@ -124,6 +121,7 @@ pub async fn identity_response( let endpoint = matching_endpoint(services, ATTESTATION_FRAGMENT).unwrap(); let url_path = "/did/attestor/identity/respond"; let uri = format!("{}{}/{}", endpoint, url_path, key_id); + println!("URI identity response: {}", uri); // POST response let client = reqwest::Client::new(); let result = client @@ -144,7 +142,7 @@ pub async fn identity_response( .unwrap(); let nonce = Nonce::from(String::from(nonce_str)); // update struct - identity_challenge.update_p_key = Some(attestor_p_key); + identity_challenge.update_p_key = Some(attestor_p_key.clone()); identity_challenge.identity_nonce = Some(nonce); identity_challenge.identity_response_signature = Some(signed_encrypted_response); @@ -156,10 +154,11 @@ pub async fn identity_response( /// This function makes a POST request with the candidate DID (dDID) to the attestor endpoint, using the url path received during /// the identity challenge-response. pub async fn initiate_content_challenge( - path: PathBuf, - ddid: &String, - services: &Vec, -) -> Result<(), TrustchainCRError> { + path: &PathBuf, + ddid: &str, + services: &[Service], + attestor_p_key: &Jwk, +) -> Result<(ContentCRInitiation, CRContentChallenge), TrustchainCRError> { // deserialise identity_cr_initiation and get key id let identity_cr_initiation = IdentityCRInitiation::new() .elementwise_deserialize(&path) @@ -169,19 +168,13 @@ pub async fn initiate_content_challenge( let key_id = temp_s_key_ssi.to_public().thumbprint().unwrap(); let content_cr_initiation = ContentCRInitiation { - requester_did: Some(ddid.clone()), + requester_did: Some(ddid.to_owned()), }; - // get uri for POST request response - let endpoint = &services.first().unwrap().service_endpoint; - let endpoint = match endpoint { - Some(OneOrMany::One(ServiceEndpoint::URI(uri))) => uri, - - _ => Err(TrustchainCRError::InvalidServiceEndpoint)?, - }; + let endpoint = matching_endpoint(services, ATTESTATION_FRAGMENT).unwrap(); let url_path = "/did/attestor/content/initiate"; let uri = format!("{}{}/{}", endpoint, url_path, key_id); - println!("URI: {}", uri); + println!("URI content challenge: {}", uri); // make POST request to endpoint let client = reqwest::Client::new(); let result = client @@ -190,15 +183,40 @@ pub async fn initiate_content_challenge( .send() .await .map_err(|err| TrustchainCRError::Reqwest(err))?; - if result.status() != 200 { println!("Status code: {}", result.status()); - return Err(TrustchainCRError::FailedToInitiateCR); + return Err(TrustchainCRError::FailedToRespond(result)); } - // serialise struct to file - content_cr_initiation.elementwise_serialize(&path)?; - Ok(()) + // TODO: extract challenge from response if OK. Then call response function. + // let response_json = response + // .json::() + // .await + // .map_err(|err| TrustchainCRError::Reqwest(err))?; + let response_body: CustomResponse = result + .json() + .await + .map_err(|err| TrustchainCRError::Reqwest(err))?; + let data = response_body.data.unwrap(); + + // response + let result = content_response( + &path, + &data.to_string(), + services, + attestor_p_key.clone(), + &ddid.to_owned(), + ) + .await; + let (nonces, response) = result.unwrap(); + let content_challenge = CRContentChallenge { + content_nonce: Some(nonces), + content_challenge_signature: Some(data.to_string()), + content_response_signature: Some(response), + }; + // content_cr_initiation.elementwise_serialize(&path)?; + // // TODO: return initiation struct and challenge struct + Ok((content_cr_initiation, content_challenge)) } /// Generates the response for the content challenge-response process and makes a POST request to @@ -209,38 +227,26 @@ pub async fn initiate_content_challenge( /// signing key from the requestor's candidate DID (dDID) document, before posting the signed (temporary secret key) /// and encrypted (attestor's public key) response to the attestor's endpoint, using the provided url path. pub async fn content_response( - path: PathBuf, - services: Vec, + path: &PathBuf, + challenge: &str, + services: &[Service], attestor_p_key: Jwk, ddid: &String, -) -> Result<(), TrustchainCRError> { - // deserialise challenge struct from file - let result = CRContentChallenge::new().elementwise_deserialize(&path); - let mut content_challenge = result.unwrap().unwrap(); - let challenge = content_challenge - .content_challenge_signature - .clone() - .unwrap(); - +) -> Result<(HashMap, String), TrustchainCRError> { // get keys let identity_initiation = IdentityCRInitiation::new().elementwise_deserialize(&path); let temp_s_key = identity_initiation.unwrap().unwrap().temp_s_key.unwrap(); let temp_s_key_ssi = josekit_to_ssi_jwk(&temp_s_key).unwrap(); // get endpoint let key_id = temp_s_key_ssi.to_public().thumbprint().unwrap(); - let endpoint = &services.first().unwrap().service_endpoint; - let endpoint = match endpoint { - Some(OneOrMany::One(ServiceEndpoint::URI(uri))) => uri, - - _ => Err(TrustchainCRError::InvalidServiceEndpoint)?, - }; + let endpoint = matching_endpoint(services, ATTESTATION_FRAGMENT).unwrap(); let url_path = "/did/attestor/content/respond"; let uri = format!("{}{}/{}", endpoint, url_path, key_id); // decrypt and verify payload let requester = Entity {}; let decrypted_verified_payload = requester - .decrypt_and_verify(challenge, &temp_s_key, &attestor_p_key) + .decrypt_and_verify(challenge.to_owned(), &temp_s_key, &attestor_p_key) .unwrap(); // extract map with decrypted nonces from payload and decrypt each nonce let challenges_map: HashMap = serde_json::from_value( @@ -262,35 +268,37 @@ pub async fn content_response( signing_keys_map.insert(key_id, jwk); } - let decrypted_nonces: HashMap = + let decrypted_nonces: HashMap = challenges_map .iter() .fold(HashMap::new(), |mut acc, (key_id, nonce)| { acc.insert( String::from(key_id), - requester - .decrypt( - &Some(Value::from(nonce.clone())).unwrap(), - signing_keys_map.get(key_id).unwrap(), - ) - .unwrap() - .claim("nonce") - .unwrap() - .as_str() - .unwrap() - .to_string(), + Nonce::from( + requester + .decrypt( + &Some(Value::from(nonce.clone())).unwrap(), + signing_keys_map.get(key_id).unwrap(), + ) + .unwrap() + .claim("nonce") + .unwrap() + .as_str() + .unwrap() + .to_string(), + ), ); acc }); // sign and encrypt response - let value: serde_json::Value = serde_json::to_value(decrypted_nonces).unwrap(); + let value: serde_json::Value = serde_json::to_value(&decrypted_nonces).unwrap(); let mut payload = JwtPayload::new(); payload.set_claim("nonces", Some(value)).unwrap(); let signed_encrypted_response = requester .sign_and_encrypt_claim(&payload, &temp_s_key, &attestor_p_key) .unwrap(); - // post respone to endpoint + // post response to endpoint let client = reqwest::Client::new(); let result = client .post(uri) @@ -302,8 +310,5 @@ pub async fn content_response( println!("Status code: {}", result.status()); return Err(TrustchainCRError::FailedToRespond(result)); } - // serialise - content_challenge.content_response_signature = Some(signed_encrypted_response); - content_challenge.elementwise_serialize(&path)?; - Ok(()) + Ok((decrypted_nonces, signed_encrypted_response)) } diff --git a/trustchain-http/tests/attestation.rs b/trustchain-http/tests/attestation.rs index 5f753d37..19393021 100644 --- a/trustchain-http/tests/attestation.rs +++ b/trustchain-http/tests/attestation.rs @@ -1,12 +1,13 @@ -use axum::http::request; -use rand::rngs::mock; +/// Integration test for attestation challenge-response process. use trustchain_core::verifier::Verifier; use trustchain_core::TRUSTCHAIN_DATA; use trustchain_http::attestation_encryption_utils::ssi_to_josekit_jwk; use trustchain_http::attestation_utils::{ElementwiseSerializeDeserialize, IdentityCRInitiation}; use trustchain_http::attestor::present_identity_challenge; -use trustchain_http::requester::{identity_response, initiate_identity_challenge}; -/// Integration test for attestation challenge-response process. +use trustchain_http::requester::{ + identity_response, initiate_content_challenge, initiate_identity_challenge, +}; + use trustchain_ion::{get_ion_resolver, verifier::IONVerifier}; // The root event time of DID documents used in integration test below. @@ -15,7 +16,6 @@ const ROOT_EVENT_TIME_1: u64 = 1666265405; use hyper::Server; use mockall::automock; use std::fs; -use std::sync::Once; use std::{net::TcpListener, path::PathBuf}; use tower::make::Shared; use trustchain_core::utils::{extract_keys, init}; @@ -31,8 +31,8 @@ pub trait AttestationUtils { // lazy_static! { // static ref HANDLE = // } - -async fn start_server() { +use tokio::task::JoinHandle; +async fn start_server() -> JoinHandle<()> { let listener = TcpListener::bind("127.0.0.1:8081").expect("Could not bind ephemeral socket"); let addr = listener.local_addr().unwrap(); let port = addr.port(); @@ -48,21 +48,28 @@ async fn start_server() { TrustchainRouter::from(http_config).into_router(), )); server.await.expect("server error"); - }); + }) } +// use lazy_static::lazy_static; +// use std::future::Future; +// lazy_static! { +// pub static ref HANDLE: impl Future> = start_server(); +// } #[tokio::test] #[ignore] async fn attestation_challenge_response() { // Set-up: init test paths, get upstream info - // init_http(); init(); start_server().await; + // |--------------------------------------------------------------| + // |------------| Part 1: identity challenge-response |------------| + // |--------------------------------------------------------------| + // |------------| requester |------------| // Use ROOT_PLUS_1 as attestor. Run server on localhost:8081. let attestor_did = "did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; - // let attestor_did = "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"; let resolver = get_ion_resolver("http://localhost:8081/"); let verifier = IONVerifier::new(resolver); let resolver = verifier.resolver(); @@ -76,9 +83,10 @@ async fn attestation_challenge_response() { let (_, attestor_doc, _) = result.unwrap(); let attestor_doc = attestor_doc.as_ref().unwrap(); let services = attestor_doc.service.as_ref().unwrap(); - println!("services: {:?}", services); - // Part 1.1: Initiate attestation request (identity initiation). + // Part 1.1: The requester initiates the attestation request (identity initiation). + // The requester generates a temporary key pair and sends the public key to the attestor via + // a POST request, together with the organization name and operator name. let expected_org_name = String::from("My Org"); let expected_operator_name = String::from("Some Operator"); @@ -87,8 +95,10 @@ async fn attestation_challenge_response() { assert!(result.is_ok()); // |------------| attestor |------------| - // Part 1.2: check the serialized data matches that received in 1.1 (this step is done manually) - // by the upstream in deployment using `trustchain-cli` + // Part 1.2: check the serialized data matches that received in 1.1. In deployment, this step is + // done manually using `trustchain-cli`, where the attestor has to confirm that they recognize + // the requester and that they want to proceed with challenge-response process + // for attestation. let path = std::env::var(TRUSTCHAIN_DATA).unwrap(); let attestation_requests_path = PathBuf::from(path).join("attestation_requests"); @@ -96,12 +106,11 @@ async fn attestation_challenge_response() { let paths = fs::read_dir(attestation_requests_path).unwrap(); let request_path: PathBuf = paths.map(|path| path.unwrap().path()).collect(); - // TODO: Deserialized received information and check that it is correct. + // Deserialized received information and check that it is correct. let identity_initiation = IdentityCRInitiation::new() .elementwise_deserialize(&request_path) .unwrap() .unwrap(); - println!("identity_initiation: {:?}", identity_initiation); let org_name = identity_initiation .requester_details .clone() @@ -114,7 +123,7 @@ async fn attestation_challenge_response() { .operator_name; assert_eq!(expected_org_name, org_name); assert_eq!(expected_operator_name, operator_name); - // Present identity challenge payload. + // If data matches, proceed with presenting signed and encrypted identity challenge payload. let temp_p_key = identity_initiation.clone().temp_p_key.unwrap(); let identity_challenge_attestor = present_identity_challenge(&attestor_did, &temp_p_key).unwrap(); @@ -123,31 +132,53 @@ async fn attestation_challenge_response() { .as_ref() .unwrap(); - // // Write payload - // std::fs::write( - // request_path.join("identity_challenge_signature.json"), - // payload, - // ) - // .unwrap(); - - // TODO: remove as only need payload - // Write payload as downstream (this step would done manually or by GUI) for use in subsequent - // response. However, as secret key for decrypting response in part 1.3 is required, serialise + // Write payload as requester (this step would done manually or by GUI, since in deployment + // challenge payload is sent via alternative channel) for use in subsequent response. + // However, as nonce for verifying response is required in part 1.3, serialise // full struct instead. identity_challenge_attestor .elementwise_serialize(&request_path) .unwrap(); - // println!("result: {:?}", result); - - // Part 1.3: Downstream responds to challenge + // Part 1.3: Requester responds to challenge. The received challenge is first decrypted and + // verified, before the requester signs the challenge nonce and encrypts it with the attestor's + // public key. This response is sent to attestor via a POST request. + // Upon receiving the request, the attestor decrypts the response and verifies the signature, + // before comparing the nonce from the response with the nonce from the challenge. // |------------| requester |------------| let public_keys = extract_keys(&attestor_doc); let attestor_public_key_ssi = public_keys.first().unwrap(); let attestor_public_key = ssi_to_josekit_jwk(attestor_public_key_ssi).unwrap(); // Check nonce component is captured with the response being Ok - let result = identity_response(&request_path, services, attestor_public_key).await; - println!("result: {:?}", result); - // Pat + let result = identity_response(&request_path, services, &attestor_public_key).await; + assert!(result.is_ok()); + + // |--------------------------------------------------------------| + // |------------| Part 2: content challenge-response |------------| + // |--------------------------------------------------------------| + // + // |------------| requester |------------| + // After publishing a candidate DID (dDID) to be attested to (not covered in this test), + // the requester initiates the content challenge-response process by a POST with the dDID to the + // attestor's endpoint. + // Upon receiving the POST request the attestor resolves dDID, extracts the signing keys from it + // and returns to the requester a signed and encrypted challenge payload with a hashmap that + // contains an encrypted nonce per signing key. + // The requester decrypts the challenge payload and verifies the signature. It then decrypts + // each nonce with the corresponding signing key and collects them in a hashmap. This + // hashmap is signed and encrypted and sent back to the attestor via POST request. + // The attestor decrypts the response and verifies the signature. It then compares the received + // hashmap of nonces with the one sent to requester. + // The entire process is automated and is kicked off with the content CR initiation request. + let requester_did = "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"; + let result = initiate_content_challenge( + &request_path, + requester_did, + &services, + &attestor_public_key, + ) + .await; + // Check nonces is captured with the response being Ok + assert!(result.is_ok()); } From d17fbe15e73b3c81d69f66e016e9e7468b488f9b Mon Sep 17 00:00:00 2001 From: pwochner Date: Tue, 28 Nov 2023 16:19:40 +0000 Subject: [PATCH 48/86] Docstrings for attestor. --- trustchain-http/src/attestor.rs | 45 ++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index dadd5ca4..a34ca742 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -66,7 +66,10 @@ impl TrustchainAttestorHTTP for TrustchainAttestorHTTPHandler { } impl TrustchainAttestorHTTPHandler { - /// Processes initial attestation request and provided data. + /// Handles a POST request for identity initiation (part 1 attestation CR). + /// + /// This function saves the attestation initiation to a file. The directory to which the information + /// is saved is determined by the temp public key of the attestation initiation. pub async fn post_identity_initiation( Json(attestation_initiation): Json, ) -> impl IntoResponse { @@ -95,7 +98,14 @@ impl TrustchainAttestorHTTPHandler { } } - /// Processes response to identity challenge. + /// Handles a POST request for identity response. + /// + /// This function receives the key ID of the temporary public key and the response JSON. + /// It verifies the response using the attestor's secret key (assuming attestor DID is also + /// the `server_did` in the config file) and decrypts it with temporary public key + /// received in previous initiation request. + /// If the verification is successful, it saves the response to the file and returns + /// status code OK along with information for the requester on how to proceed. pub async fn post_identity_response( (Path(key_id), Json(response)): (Path, Json), app_state: Arc, @@ -153,7 +163,14 @@ impl TrustchainAttestorHTTPHandler { } } - /// Processes initiation of second part of attestation request (content challenge-response). + /// Handles a POST request for content initiation (part 2 attestation CR). + /// + /// This function receives the key ID of the temporary public key and the candidate DID. + /// It resolves the candidate DID and extracts the public signing keys from the document. + /// It generates a challenge nonce per key and encrypts it with the corresponding + /// signing key. It then signs (attestor's secret key, assuming attestor DID is also + /// the `server_did` in the config file) and encrypts (temporary public key) + /// the challenges and returns them to the requester. pub async fn post_content_initiation( (Path(key_id), Json(ddid)): (Path, Json), app_state: Arc, @@ -245,7 +262,13 @@ impl TrustchainAttestorHTTPHandler { } } } - /// Processes response to second part of attestation request (content challenge-response). + /// Handles a POST request for content response. + /// + /// This function receives the key ID of the temporary public key and the response JSON. + /// It verifies the response using the attestor's secret key (assuming attestor DID is also + /// the `server_did` in the config file) and decrypts it with temporary public key. It then + /// compares the received nonces with the expected nonces and if they match, it saves the + /// response to the file and returns status code OK. pub async fn post_content_response( (Path(key_id), Json(response)): (Path, Json), app_state: Arc, @@ -303,7 +326,13 @@ impl TrustchainAttestorHTTPHandler { } } -/// Generates challenge for first part of attestation request (identity challenge-response). +/// Generates challenge for part 1 of attestation request (identity challenge-response). +/// +/// This function generates a new key pair for the update key and nonce for the challenge. +/// It then adds the update public key and nonce to a payload and signs it with the secret +/// signing key from provided did and encrypts it with the provided temporary public key. +/// It returns a ```CRIdentityChallenge``` struct containing the signed and encrypted challenge +/// payload. pub fn present_identity_challenge( did: &str, temp_p_key: &Jwk, @@ -345,7 +374,11 @@ pub fn present_identity_challenge( Ok(identity_challenge) } -/// Verifies nonce for challenge-response. +/// Verifies nonce for part 1 of attestation request (identity challenge-response). +/// +/// This function receives a payload provided by requester and the path to the directory +/// where information about the attestation request is stored. It deserialises the expected +/// nonce from the file and compares it with the nonce from the payload. fn verify_nonce(payload: JwtPayload, path: &PathBuf) -> Result<(), TrustchainCRError> { // get nonce from payload let nonce = payload.claim("identity_nonce").unwrap().as_str().unwrap(); From 8a6a387f84a8c9475af358db2893fe3920b19145 Mon Sep 17 00:00:00 2001 From: pwochner Date: Wed, 29 Nov 2023 09:35:37 +0000 Subject: [PATCH 49/86] Rename structs. Tidy up code. More docstrings. --- trustchain-core/src/attestor.rs | 2 +- .../src/attestation_encryption_utils.rs | 4 +- trustchain-http/src/attestation_utils.rs | 58 ++++++++--------- trustchain-http/src/attestor.rs | 16 ++--- trustchain-http/src/data.rs | 2 +- trustchain-http/src/requester.rs | 64 ++++++++----------- 6 files changed, 69 insertions(+), 77 deletions(-) diff --git a/trustchain-core/src/attestor.rs b/trustchain-core/src/attestor.rs index 99217048..646141fb 100644 --- a/trustchain-core/src/attestor.rs +++ b/trustchain-core/src/attestor.rs @@ -23,7 +23,7 @@ pub enum AttestorError { SigningError(String, String), } -/// An upstream entity that attests to a downstream DID. +/// An upstream entity that attests to a downstream DID (attestor). pub trait Attestor: Subject { /// Attests to a DID Document. Subject attests to a DID document by signing the document with (one of) its private signing key(s). /// It doesn't matter which signing key you use, there's the option to pick one using the key index. diff --git a/trustchain-http/src/attestation_encryption_utils.rs b/trustchain-http/src/attestation_encryption_utils.rs index bac8eaa1..3c131634 100644 --- a/trustchain-http/src/attestation_encryption_utils.rs +++ b/trustchain-http/src/attestation_encryption_utils.rs @@ -134,8 +134,8 @@ pub fn extract_key_ids_and_jwk( mod tests { use super::*; use crate::data::{ - TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS, TEST_SIGNING_KEY_1, TEST_SIGNING_KEY_2, - TEST_TEMP_KEY, TEST_UPDATE_KEY, TEST_UPSTREAM_KEY, + TEST_ATTESTOR_KEY, TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS, TEST_SIGNING_KEY_1, + TEST_SIGNING_KEY_2, TEST_TEMP_KEY, TEST_UPDATE_KEY, }; #[test] fn test_sign_encrypt_and_decrypt_verify() { diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index b8300ab8..a0deaf28 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -107,7 +107,7 @@ pub enum CurrentCRState { ContentResponseComplete, } -// pub struct Nonce([u8; N]); +// TODO: Impose additional constraints on the nonce type. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct Nonce(String); @@ -290,7 +290,7 @@ impl ElementwiseSerializeDeserialize for IdentityCRInitiation { #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, IsEmpty)] -pub struct CRIdentityChallenge { +pub struct IdentityCRChallenge { pub update_p_key: Option, pub update_s_key: Option, pub identity_nonce: Option, // make own Nonce type @@ -299,7 +299,7 @@ pub struct CRIdentityChallenge { pub identity_response_signature: Option, } -impl CRIdentityChallenge { +impl IdentityCRChallenge { pub fn new() -> Self { Self { update_p_key: None, @@ -321,12 +321,12 @@ impl CRIdentityChallenge { } } -impl ElementwiseSerializeDeserialize for CRIdentityChallenge { +impl ElementwiseSerializeDeserialize for IdentityCRChallenge { /// Deserialize each field of the struct from a file. Fields are optional. If no files are found, return None. fn elementwise_deserialize( mut self, path: &PathBuf, - ) -> Result, TrustchainCRError> { + ) -> Result, TrustchainCRError> { // update public key let mut full_path = path.join("update_p_key.json"); self.update_p_key = match File::open(&full_path) { @@ -384,9 +384,9 @@ impl ElementwiseSerializeDeserialize for CRIdentityChallenge { } } -impl TryFrom<&CRIdentityChallenge> for JwtPayload { +impl TryFrom<&IdentityCRChallenge> for JwtPayload { type Error = TrustchainCRError; - fn try_from(value: &CRIdentityChallenge) -> Result { + fn try_from(value: &IdentityCRChallenge) -> Result { let mut payload = JwtPayload::new(); payload.set_claim( "identity_nonce", @@ -404,10 +404,10 @@ impl TryFrom<&CRIdentityChallenge> for JwtPayload { } } -impl TryFrom<&JwtPayload> for CRIdentityChallenge { +impl TryFrom<&JwtPayload> for IdentityCRChallenge { type Error = TrustchainCRError; fn try_from(value: &JwtPayload) -> Result { - let mut challenge = CRIdentityChallenge { + let mut challenge = IdentityCRChallenge { update_p_key: None, update_s_key: None, identity_nonce: None, @@ -475,13 +475,13 @@ impl ElementwiseSerializeDeserialize for ContentCRInitiation { } #[derive(Debug, Serialize, Deserialize, IsEmpty)] -pub struct CRContentChallenge { +pub struct ContentCRChallenge { pub content_nonce: Option>, pub content_challenge_signature: Option, pub content_response_signature: Option, } -impl CRContentChallenge { +impl ContentCRChallenge { pub fn new() -> Self { Self { content_nonce: None, @@ -499,12 +499,12 @@ impl CRContentChallenge { } } -impl ElementwiseSerializeDeserialize for CRContentChallenge { +impl ElementwiseSerializeDeserialize for ContentCRChallenge { /// Deserialize each field of the struct from a file. Fields are optional. If no files are found, return None. fn elementwise_deserialize( mut self, path: &PathBuf, - ) -> Result, TrustchainCRError> { + ) -> Result, TrustchainCRError> { // content nonce(s) let mut full_path = path.join("content_nonce.json"); self.content_nonce = match File::open(&full_path) { @@ -555,9 +555,9 @@ impl ElementwiseSerializeDeserialize for CRContentChallenge { #[derive(Debug, Serialize, Deserialize, IsEmpty)] pub struct CRState { pub identity_cr_initiation: Option, - pub identity_challenge_response: Option, + pub identity_challenge_response: Option, pub content_cr_initiation: Option, - pub content_challenge_response: Option, + pub content_challenge_response: Option, } impl CRState { @@ -689,10 +689,10 @@ impl ElementwiseSerializeDeserialize for CRState { ) -> Result, TrustchainCRError> { self.identity_cr_initiation = IdentityCRInitiation::new().elementwise_deserialize(path)?; self.identity_challenge_response = - CRIdentityChallenge::new().elementwise_deserialize(path)?; + IdentityCRChallenge::new().elementwise_deserialize(path)?; self.content_cr_initiation = ContentCRInitiation::new().elementwise_deserialize(path)?; self.content_challenge_response = - CRContentChallenge::new().elementwise_deserialize(path)?; + ContentCRChallenge::new().elementwise_deserialize(path)?; Ok(Some(self)) } } @@ -785,7 +785,7 @@ mod tests { }; // identity challenge - let identity_challenge = CRIdentityChallenge { + let identity_challenge = IdentityCRChallenge { update_p_key: serde_json::from_str(TEST_UPDATE_KEY).unwrap(), update_s_key: None, identity_nonce: Some(Nonce::new()), @@ -810,7 +810,7 @@ mod tests { acc.insert(String::from(key_id), Nonce::new()); acc }); - let content_challenge_response = CRContentChallenge { + let content_challenge_response = ContentCRChallenge { content_nonce: Some(nonces), content_challenge_signature: Some(String::from( "some content challenge signature string", @@ -889,7 +889,7 @@ mod tests { #[test] fn test_elementwise_deserialize_identity_challenge() { - let identity_challenge = CRIdentityChallenge::new(); + let identity_challenge = IdentityCRChallenge::new(); let temp_path = tempdir().unwrap().into_path(); // Test case 1: None of the json files exist @@ -903,7 +903,7 @@ mod tests { let update_p_key_file = File::create(&update_p_key_path).unwrap(); let update_p_key: Jwk = serde_json::from_str(TEST_UPDATE_KEY).unwrap(); serde_json::to_writer(update_p_key_file, &update_p_key).unwrap(); - let identity_challenge = CRIdentityChallenge::new(); + let identity_challenge = IdentityCRChallenge::new(); let result = identity_challenge.elementwise_deserialize(&temp_path); assert!(result.is_ok()); let identity_challenge = result.unwrap().unwrap(); @@ -916,7 +916,7 @@ mod tests { let identity_nonce_path = temp_path.join("identity_nonce.json"); let identity_nonce_file = File::create(&identity_nonce_path).unwrap(); serde_json::to_writer(identity_nonce_file, &42).unwrap(); - let identity_challenge = CRIdentityChallenge::new(); + let identity_challenge = IdentityCRChallenge::new(); let result = identity_challenge.elementwise_deserialize(&temp_path); assert!(result.is_err()); println!("Error: {:?}", result.unwrap_err()); @@ -924,7 +924,7 @@ mod tests { #[test] fn test_elementwise_deserialize_content_challenge() { - let content_challenge = CRContentChallenge::new(); + let content_challenge = ContentCRChallenge::new(); let temp_path = tempdir().unwrap().into_path(); // Test case 1: None of the json files exist @@ -933,7 +933,7 @@ mod tests { assert!(result.unwrap().is_none()); // Test case 2: Only one json file exists and can be deserialized - let content_challenge = CRContentChallenge::new(); + let content_challenge = ContentCRChallenge::new(); let content_nonce_path = temp_path.join("content_nonce.json"); let content_nonce_file = File::create(&content_nonce_path).unwrap(); let mut nonces_map: HashMap<&str, Nonce> = HashMap::new(); @@ -969,7 +969,7 @@ mod tests { }), }; let _ = identity_initiatiation.elementwise_serialize(&path); - let identity_challenge = CRIdentityChallenge { + let identity_challenge = IdentityCRChallenge { update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), update_s_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), identity_nonce: Some(Nonce::new()), @@ -1079,7 +1079,7 @@ mod tests { operator_name: String::from("John Doe"), }), }); - cr_state.identity_challenge_response = Some(CRIdentityChallenge { + cr_state.identity_challenge_response = Some(IdentityCRChallenge { update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), update_s_key: None, identity_nonce: Some(Nonce::new()), @@ -1090,7 +1090,7 @@ mod tests { assert_eq!(result.unwrap(), CurrentCRState::IdentityChallengeComplete); // Test case 4: Identity challenge response complete, content challenge initiated - cr_state.identity_challenge_response = Some(CRIdentityChallenge { + cr_state.identity_challenge_response = Some(IdentityCRChallenge { // Same key used here for testing purposes update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), update_s_key: None, @@ -1107,7 +1107,7 @@ mod tests { assert_eq!(result.unwrap(), CurrentCRState::ContentCRInitiated); // Test case 5: Content challenge-response complete - cr_state.content_challenge_response = Some(CRContentChallenge { + cr_state.content_challenge_response = Some(ContentCRChallenge { content_nonce: Some(HashMap::new()), content_challenge_signature: Some(String::from( "some content challenge signature string", @@ -1122,7 +1122,7 @@ mod tests { #[test] fn test_check_cr_status_inconsistent_order() { let mut cr_state = CRState::new(); - cr_state.identity_challenge_response = Some(CRIdentityChallenge { + cr_state.identity_challenge_response = Some(IdentityCRChallenge { update_s_key: None, update_p_key: Some(serde_json::from_str(TEST_UPDATE_KEY).unwrap()), identity_nonce: Some(Nonce::new()), diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index a34ca742..072a7c1f 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -3,8 +3,8 @@ use crate::attestation_encryption_utils::{ SignEncrypt, }; use crate::attestation_utils::{ - attestation_request_path, CRContentChallenge, CRIdentityChallenge, CustomResponse, - ElementwiseSerializeDeserialize, IdentityCRInitiation, Nonce, TrustchainCRError, + attestation_request_path, ContentCRChallenge, CustomResponse, ElementwiseSerializeDeserialize, + IdentityCRChallenge, IdentityCRInitiation, Nonce, TrustchainCRError, }; use crate::state::AppState; use async_trait::async_trait; @@ -118,7 +118,7 @@ impl TrustchainAttestorHTTPHandler { if !path.exists() { panic!("Provided attestation request not found. Path does not exist."); } - let mut identity_challenge = CRIdentityChallenge::new() + let mut identity_challenge = IdentityCRChallenge::new() .elementwise_deserialize(&path) .unwrap() .unwrap(); @@ -241,7 +241,7 @@ impl TrustchainAttestorHTTPHandler { match signed_encrypted_challenges { Ok(signed_encrypted_challenges) => { - let content_challenge = CRContentChallenge { + let content_challenge = ContentCRChallenge { content_nonce: Some(nonces), content_challenge_signature: Some(signed_encrypted_challenges.clone()), content_response_signature: None, @@ -282,7 +282,7 @@ impl TrustchainAttestorHTTPHandler { .elementwise_deserialize(&path) .unwrap() .unwrap(); - let mut content_challenge = CRContentChallenge::new() + let mut content_challenge = ContentCRChallenge::new() .elementwise_deserialize(&path) .unwrap() .unwrap(); @@ -336,7 +336,7 @@ impl TrustchainAttestorHTTPHandler { pub fn present_identity_challenge( did: &str, temp_p_key: &Jwk, -) -> Result { +) -> Result { // generate nonce and update key let nonce = Nonce::new(); let update_s_key_ssi = generate_key(); @@ -347,7 +347,7 @@ pub fn present_identity_challenge( .map_err(|_| TrustchainCRError::FailedToGenerateKey)?; // let update_p_key_string = serde_json::to_string_pretty(&update_p_key)?; - let mut identity_challenge = CRIdentityChallenge { + let mut identity_challenge = IdentityCRChallenge { update_p_key: Some(update_p_key), update_s_key: Some(update_s_key), identity_nonce: Some(nonce), @@ -439,7 +439,7 @@ mod tests { fn test_verify_nonce() { let temp_path = tempdir().unwrap().into_path(); let expected_nonce = Nonce::from(String::from("test_nonce")); - let identity_challenge = CRIdentityChallenge { + let identity_challenge = IdentityCRChallenge { update_p_key: serde_json::from_str(TEST_UPDATE_KEY).unwrap(), update_s_key: None, identity_nonce: Some(expected_nonce.clone()), diff --git a/trustchain-http/src/data.rs b/trustchain-http/src/data.rs index d50a29da..9e30e0cb 100644 --- a/trustchain-http/src/data.rs +++ b/trustchain-http/src/data.rs @@ -87,6 +87,6 @@ pub const TEST_SIGNING_KEY_2: &str = r##" } "##; -pub const TEST_UPSTREAM_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JEV4WMgoJekTa5RQD5M92P1oLjdpMNYETQ3nbtKSnLQ","y":"dRfg_5i5wcMg1lxAffQORHpzgtm2yEIqgJoUk5ZklvI","d":"DZDZd9bxopCv2YJelMpQm_BJ0awvzpT6xWdWbaQlIJI"}"#; +pub const TEST_ATTESTOR_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JEV4WMgoJekTa5RQD5M92P1oLjdpMNYETQ3nbtKSnLQ","y":"dRfg_5i5wcMg1lxAffQORHpzgtm2yEIqgJoUk5ZklvI","d":"DZDZd9bxopCv2YJelMpQm_BJ0awvzpT6xWdWbaQlIJI"}"#; pub const TEST_TEMP_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg","d":"CfdUwQ-CcBQkWpIDPjhSJAq2SCg6hAGdcvLmCj0aA-c"}"#; pub const TEST_UPDATE_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"AB1b_4-XSem0uiPGGuW_hf_AuPArukMuD2S95ypGDSE","y":"suvBnCbhicPdYZeqgxJfPFmiNHGYDjPiW8XkYHxwgBU"}"#; diff --git a/trustchain-http/src/requester.rs b/trustchain-http/src/requester.rs index 9a168a0e..ab258f42 100644 --- a/trustchain-http/src/requester.rs +++ b/trustchain-http/src/requester.rs @@ -11,15 +11,15 @@ use crate::{ josekit_to_ssi_jwk, ssi_to_josekit_jwk, DecryptVerify, Entity, SignEncrypt, }, attestation_utils::{ - attestation_request_path, matching_endpoint, CRContentChallenge, CRIdentityChallenge, - ContentCRInitiation, ElementwiseSerializeDeserialize, IdentityCRInitiation, + attestation_request_path, matching_endpoint, ContentCRChallenge, ContentCRInitiation, + ElementwiseSerializeDeserialize, IdentityCRChallenge, IdentityCRInitiation, RequesterDetails, }, attestation_utils::{CustomResponse, Nonce, TrustchainCRError}, ATTESTATION_FRAGMENT, }; -/// Initiates the identity challenge-response process by sending a POST request to the attestor endpoint. +/// Initiates part 1 attestation request (identity challenge-response). /// /// This function generates a temporary key to use as an identifier throughout the challenge-response process. /// It prompts the user to provide the organization name and operator name, which are included in the POST request @@ -52,7 +52,6 @@ pub async fn initiate_identity_challenge( let url_path = "/did/attestor/identity/initiate"; let endpoint = matching_endpoint(services, ATTESTATION_FRAGMENT).unwrap(); let uri = format!("{}{}", endpoint, url_path); - println!("URI: {}", uri); // make POST request to endpoint let client = reqwest::Client::new(); @@ -63,7 +62,6 @@ pub async fn initiate_identity_challenge( .await .map_err(|err| TrustchainCRError::Reqwest(err))?; - println!("Status code: {}", result.status()); if result.status() != 200 { return Err(TrustchainCRError::FailedToInitiateCR); } @@ -79,18 +77,19 @@ pub async fn initiate_identity_challenge( Ok(()) } -/// Generates the response for the identity challenge-response process and makes a POST request to the attestor endpoint. +/// Generates and posts response for part 1 of attesation process (identity challenge-response). /// -/// This function first decrypts and verifies the challenge received from attestor to extract challenge nonce. -/// It then signs the nonce with the requester's temporary secret key and encrypts it with the attestor's public key, -/// before posting the response to the attestor's endpoint, using the provided url path. +/// This function first decrypts and verifies the challenge received from attestor to extract +/// challenge nonce. It then signs the nonce with the requester's temporary secret key and +/// encrypts it with the attestor's public key, before posting the response to the attestor. +/// If post request is successful, the updated ```CRIdentityChallenge``` is returned. pub async fn identity_response( path: &PathBuf, services: &[Service], attestor_p_key: &Jwk, -) -> Result { +) -> Result { // deserialise challenge struct from file - let result = CRIdentityChallenge::new().elementwise_deserialize(path); + let result = IdentityCRChallenge::new().elementwise_deserialize(path); let mut identity_challenge = result.unwrap().unwrap(); let identity_initiation = IdentityCRInitiation::new().elementwise_deserialize(path); let temp_s_key = identity_initiation.unwrap().unwrap().temp_s_key.unwrap(); @@ -112,16 +111,11 @@ pub async fn identity_response( let signed_encrypted_response = requester .sign_and_encrypt_claim(&decrypted_verified_payload, &temp_s_key, &attestor_p_key) .unwrap(); - println!( - "Signed and encrypted response: {:?}", - signed_encrypted_response - ); let key_id = temp_s_key_ssi.to_public().thumbprint().unwrap(); // get uri for POST request response let endpoint = matching_endpoint(services, ATTESTATION_FRAGMENT).unwrap(); let url_path = "/did/attestor/identity/respond"; let uri = format!("{}{}/{}", endpoint, url_path, key_id); - println!("URI identity response: {}", uri); // POST response let client = reqwest::Client::new(); let result = client @@ -130,7 +124,6 @@ pub async fn identity_response( .send() .await .map_err(|err| TrustchainCRError::Reqwest(err))?; - println!("Status code: {}", result.status()); if result.status() != 200 { return Err(TrustchainCRError::FailedToRespond(result)); } @@ -149,16 +142,20 @@ pub async fn identity_response( Ok(identity_challenge) } -/// Initiates the content challenge-response process by sending a POST request to the attestor endpoint. +/// Initiates part 2 attestation request (content challenge-response). /// -/// This function makes a POST request with the candidate DID (dDID) to the attestor endpoint, using the url path received during -/// the identity challenge-response. +/// This function posts the to be attested to candidate DID (dDID) to the attestor's endpoint. +/// If the post request is successful, the response body contains the signed and encrypted +/// challenge payload with a hashmap that contains an encrypted nonce per signing key. +/// The response to the challenge is generated and posted to the attestor's endpoint. +/// If the post request and the verification of the response are successful, the +/// ```ContentCRInitiation``` and ```CRContentChallenge``` structs are returned. pub async fn initiate_content_challenge( path: &PathBuf, ddid: &str, services: &[Service], attestor_p_key: &Jwk, -) -> Result<(ContentCRInitiation, CRContentChallenge), TrustchainCRError> { +) -> Result<(ContentCRInitiation, ContentCRChallenge), TrustchainCRError> { // deserialise identity_cr_initiation and get key id let identity_cr_initiation = IdentityCRInitiation::new() .elementwise_deserialize(&path) @@ -174,7 +171,6 @@ pub async fn initiate_content_challenge( let endpoint = matching_endpoint(services, ATTESTATION_FRAGMENT).unwrap(); let url_path = "/did/attestor/content/initiate"; let uri = format!("{}{}/{}", endpoint, url_path, key_id); - println!("URI content challenge: {}", uri); // make POST request to endpoint let client = reqwest::Client::new(); let result = client @@ -188,34 +184,28 @@ pub async fn initiate_content_challenge( return Err(TrustchainCRError::FailedToRespond(result)); } - // TODO: extract challenge from response if OK. Then call response function. - // let response_json = response - // .json::() - // .await - // .map_err(|err| TrustchainCRError::Reqwest(err))?; let response_body: CustomResponse = result .json() .await .map_err(|err| TrustchainCRError::Reqwest(err))?; - let data = response_body.data.unwrap(); + let signed_encrypted_challenge = response_body.data.unwrap(); // response let result = content_response( &path, - &data.to_string(), + &signed_encrypted_challenge.to_string(), services, attestor_p_key.clone(), &ddid.to_owned(), ) .await; + // TODO: better error handling let (nonces, response) = result.unwrap(); - let content_challenge = CRContentChallenge { + let content_challenge = ContentCRChallenge { content_nonce: Some(nonces), - content_challenge_signature: Some(data.to_string()), + content_challenge_signature: Some(signed_encrypted_challenge.to_string()), content_response_signature: Some(response), }; - // content_cr_initiation.elementwise_serialize(&path)?; - // // TODO: return initiation struct and challenge struct Ok((content_cr_initiation, content_challenge)) } @@ -223,9 +213,11 @@ pub async fn initiate_content_challenge( /// the attestor endpoint. /// /// This function first decrypts (temporary secret key) and verifies (attestor's public key) the -/// challenge received from attestor to extract challenge nonces. It then decrypts each nonce with the corresponding -/// signing key from the requestor's candidate DID (dDID) document, before posting the signed (temporary secret key) -/// and encrypted (attestor's public key) response to the attestor's endpoint, using the provided url path. +/// challenge received from attestor to extract challenge nonces. It then decrypts each nonce with +/// the corresponding signing key from the requestor's candidate DID (dDID) document, before +/// posting the signed (temporary secret key) and encrypted (attestor's public key) response to +/// the attestor's endpoint. +/// If successful, the nonces and the (signed and encrypted) response are returned. pub async fn content_response( path: &PathBuf, challenge: &str, From 990f17a8035e6fcdfc31410d8add63900ebdb197 Mon Sep 17 00:00:00 2001 From: pwochner Date: Wed, 29 Nov 2023 09:41:27 +0000 Subject: [PATCH 50/86] Remove unused imports --- trustchain-http/src/attestation_utils.rs | 1 - trustchain-http/src/attestor.rs | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index a0deaf28..156659dd 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -10,7 +10,6 @@ use is_empty::IsEmpty; use josekit::JoseError; use josekit::{jwk::Jwk, jwt::JwtPayload}; use rand::{distributions::Alphanumeric, thread_rng, Rng}; -use reqwest::Response; use serde::{Deserialize, Serialize}; use serde_json::{to_string_pretty as to_json, Value}; use serde_with::skip_serializing_none; diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index 072a7c1f..7618af76 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -9,16 +9,12 @@ use crate::attestation_utils::{ use crate::state::AppState; use async_trait::async_trait; use axum::extract::Path; -use axum::{ - response::{Html, IntoResponse}, - Json, -}; +use axum::{response::IntoResponse, Json}; use hyper::StatusCode; use josekit::jwk::Jwk; use josekit::jwt::JwtPayload; use log::info; -use serde::{Deserialize, Serialize}; use trustchain_api::api::TrustchainDIDAPI; use trustchain_api::TrustchainAPI; use trustchain_core::verifier::Verifier; From e2607cd272fe507f229214b3ec97a587501bc488 Mon Sep 17 00:00:00 2001 From: pwochner Date: Wed, 29 Nov 2023 15:54:06 +0000 Subject: [PATCH 51/86] Use elemenwise_deserialize to read temp keys. --- trustchain-http/src/attestor.rs | 30 ++++++++++++---------------- trustchain-http/tests/attestation.rs | 1 + 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index 7618af76..df3a5412 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -125,14 +125,12 @@ impl TrustchainAttestorHTTPHandler { let signing_key_ssi = signing_keys.first().unwrap(); let signing_key = ssi_to_josekit_jwk(&signing_key_ssi); // get temp public key - let temp_key_path = path.join("temp_p_key.json"); - let file = File::open(&temp_key_path).unwrap(); - let reader = BufReader::new(file); - let temp_p_key_ssi = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize) + // TODO: use elementise_deserialize to extract temp_p_key + let identity_initiation = IdentityCRInitiation::new() + .elementwise_deserialize(&path) + .unwrap() .unwrap(); - let temp_p_key = ssi_to_josekit_jwk(&temp_p_key_ssi).unwrap(); - + let temp_p_key = identity_initiation.temp_p_key.unwrap(); // verify response let attestor = Entity {}; let payload = attestor @@ -282,7 +280,7 @@ impl TrustchainAttestorHTTPHandler { .elementwise_deserialize(&path) .unwrap() .unwrap(); - let expected_nonce = content_challenge.content_nonce.clone().unwrap(); + let expected_nonces = content_challenge.content_nonce.clone().unwrap(); // get signing key from ION attestor let did = app_state.config.server_did.as_ref().unwrap().to_owned(); let ion_attestor = IONAttestor::new(&did); @@ -302,9 +300,9 @@ impl TrustchainAttestorHTTPHandler { let nonces_map: HashMap = serde_json::from_value(payload.claim("nonces").unwrap().clone()).unwrap(); // verify nonces - if nonces_map.eq(&expected_nonce) { + if nonces_map.eq(&expected_nonces) { println!("nonces map: {:?}", nonces_map); - println!("expected nonces map: {:?}", expected_nonce); + println!("expected nonces map: {:?}", expected_nonces); content_challenge.content_response_signature = Some(response.clone()); content_challenge.elementwise_serialize(&path).unwrap(); let response = CustomResponse { @@ -341,7 +339,6 @@ pub fn present_identity_challenge( .map_err(|_| TrustchainCRError::FailedToGenerateKey)?; let update_p_key = ssi_to_josekit_jwk(&update_p_key_ssi) .map_err(|_| TrustchainCRError::FailedToGenerateKey)?; - // let update_p_key_string = serde_json::to_string_pretty(&update_p_key)?; let mut identity_challenge = IdentityCRChallenge { update_p_key: Some(update_p_key), @@ -379,12 +376,11 @@ fn verify_nonce(payload: JwtPayload, path: &PathBuf) -> Result<(), TrustchainCRE // get nonce from payload let nonce = payload.claim("identity_nonce").unwrap().as_str().unwrap(); // deserialise expected nonce - let nonce_path = path.join("identity_nonce.json"); - let file = File::open(&nonce_path).unwrap(); - let reader = BufReader::new(file); - let expected_nonce: String = - serde_json::from_reader(reader).map_err(|_| TrustchainCRError::FailedToDeserialize)?; - + let identity_challenge = IdentityCRChallenge::new() + .elementwise_deserialize(&path) + .unwrap() + .unwrap(); + let expected_nonce = identity_challenge.identity_nonce.unwrap().to_string(); if nonce != expected_nonce { return Err(TrustchainCRError::FailedToVerifyNonce); } diff --git a/trustchain-http/tests/attestation.rs b/trustchain-http/tests/attestation.rs index 19393021..bedc29d5 100644 --- a/trustchain-http/tests/attestation.rs +++ b/trustchain-http/tests/attestation.rs @@ -172,6 +172,7 @@ async fn attestation_challenge_response() { // hashmap of nonces with the one sent to requester. // The entire process is automated and is kicked off with the content CR initiation request. let requester_did = "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"; + // let requester_did = "did:ion:test:EiCDmY0qxsde9AdIwMf2tUKOiMo4aHnoWaPBRCeGt7iMHA"; let result = initiate_content_challenge( &request_path, requester_did, From 4293ee9d436a980728c531b2a36012ee6c5d33d5 Mon Sep 17 00:00:00 2001 From: pwochner Date: Wed, 29 Nov 2023 16:16:55 +0000 Subject: [PATCH 52/86] Fix tests. --- trustchain-http/src/attestation_utils.rs | 15 +++++++++------ trustchain-http/src/attestor.rs | 3 +-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index 156659dd..9b3160c1 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -1007,7 +1007,7 @@ mod tests { fn test_matching_endpoint() { let services = vec![ Service { - id: String::from("did:example:123456789abcdefghi#service-1"), + id: String::from("#service-1"), service_endpoint: Some(OneOrMany::One(ServiceEndpoint::URI(String::from( "https://example.com/endpoint-1", )))), @@ -1015,7 +1015,7 @@ mod tests { property_set: None, }, Service { - id: String::from("did:example:123456789abcdefghi#service-2"), + id: String::from("#service-2"), service_endpoint: Some(OneOrMany::One(ServiceEndpoint::URI(String::from( "https://example.com/endpoint-2", )))), @@ -1023,15 +1023,18 @@ mod tests { property_set: None, }, ]; - let result = matching_endpoint(&services, "service-1"); + let result = matching_endpoint(&services, "#service-1"); assert_eq!(result.unwrap(), "https://example.com/endpoint-1"); + let result = matching_endpoint(&services, "service-1"); + assert!(result.is_err()); } #[test] fn test_matching_endpoint_multiple_endpoints_found() { + // Test case: multiple endpoints found should throw error let services = vec![ Service { - id: String::from("did:example:123456789abcdefghi#service-1"), + id: String::from("#service-1"), service_endpoint: Some(OneOrMany::One(ServiceEndpoint::URI(String::from( "https://example.com/endpoint-1", )))), @@ -1039,7 +1042,7 @@ mod tests { property_set: None, }, Service { - id: String::from("did:example:123456789abcdefghi#service-1"), + id: String::from("#service-1"), service_endpoint: Some(OneOrMany::One(ServiceEndpoint::URI(String::from( "https://example.com/endpoint-2", )))), @@ -1047,7 +1050,7 @@ mod tests { property_set: None, }, ]; - let result = matching_endpoint(&services, "service-1"); + let result = matching_endpoint(&services, "#service-1"); assert!(result.is_err()); } diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index df3a5412..a512b7d2 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -402,7 +402,6 @@ mod tests { use crate::data::TEST_TEMP_KEY; // Attestor integration tests - // TODO: make test better #[tokio::test] #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] async fn test_post_initiation() { @@ -419,7 +418,7 @@ mod tests { let initiation_json = serde_json::to_string_pretty(&attestation_initiation).unwrap(); println!("Attestation initiation: {:?}", initiation_json); let app = TrustchainRouter::from(HTTPConfig::default()).into_router(); - let uri = "/did/attestor/initiate".to_string(); + let uri = "/did/attestor/identity/initiate".to_string(); let client = TestClient::new(app); let response = client.post(&uri).json(&attestation_initiation).send().await; From b99ab0600c796cf0e70f48fe7508f81544c6fc22 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Wed, 29 Nov 2023 17:02:52 +0000 Subject: [PATCH 53/86] Use root_plus_2_candidate in test, fmt --- trustchain-cli/src/bin/main.rs | 110 ++++++++++++++++++--------- trustchain-core/src/utils.rs | 4 + trustchain-http/tests/attestation.rs | 4 +- 3 files changed, 81 insertions(+), 37 deletions(-) diff --git a/trustchain-cli/src/bin/main.rs b/trustchain-cli/src/bin/main.rs index 20b71b73..233714be 100644 --- a/trustchain-cli/src/bin/main.rs +++ b/trustchain-cli/src/bin/main.rs @@ -13,12 +13,16 @@ use trustchain_api::{ TrustchainAPI, }; use trustchain_cli::config::cli_config; -use trustchain_core::{vc::CredentialError, verifier::Verifier, TRUSTCHAIN_DATA, utils:: extract_keys}; +use trustchain_core::{ + utils::extract_keys, vc::CredentialError, verifier::Verifier, TRUSTCHAIN_DATA, +}; use trustchain_http::{ + attestation_encryption_utils::ssi_to_josekit_jwk, attestation_utils::{ - ElementwiseSerializeDeserialize, IdentityCRInitiation, TrustchainCRError, CRState + CRState, ElementwiseSerializeDeserialize, IdentityCRInitiation, TrustchainCRError, }, - requester::{initiate_identity_challenge, identity_response, initiate_content_challenge}, attestation_encryption_utils::ssi_to_josekit_jwk, attestor::present_identity_challenge, + attestor::present_identity_challenge, + requester::{identity_response, initiate_content_challenge, initiate_identity_challenge}, }; use trustchain_ion::{ attest::attest_operation, @@ -91,7 +95,7 @@ fn cli() -> Command { .arg(arg!(-t --root_event_time ).required(false)) ), ) - .subcommand( + .subcommand( Command::new("cr") .about("Challenge-response functionality for attestation challenge response process (identity and content challenge-response).") .subcommand_required(true) @@ -143,7 +147,7 @@ fn cli() -> Command { .arg(arg!(-v - -verbose).action(ArgAction::SetTrue)) .arg(arg!(-p --path ).required(true)) ) - + ) } @@ -353,7 +357,7 @@ async fn main() -> Result<(), Box> { let (_, doc, _) = TrustchainAPI::resolve(did, resolver).await?; let services = doc.unwrap().service; println!("Services: {:?}", services); - + // user promt for org name and operator name println!("Please enter your organisation name: "); let mut org_name = String::new(); @@ -380,20 +384,32 @@ async fn main() -> Result<(), Box> { } Some(("present", sub_matches)) => { // get attestation request path from provided input - let trustchain_dir: String = std::env::var(TRUSTCHAIN_DATA).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; + let trustchain_dir: String = std::env::var(TRUSTCHAIN_DATA) + .map_err(|_| TrustchainCRError::FailedAttestationRequest)?; let path_to_check = sub_matches.get_one::("path").unwrap(); let did = sub_matches.get_one::("did").unwrap(); - let path = PathBuf::new().join(trustchain_dir).join("attestation_requests").join(path_to_check); + let path = PathBuf::new() + .join(trustchain_dir) + .join("attestation_requests") + .join(path_to_check); if !path.exists() { - panic!("Provided attestation request not found. Path does not exist."); + panic!("Provided attestation request not found. Path does not exist."); } let identity_initiation = IdentityCRInitiation::new() .elementwise_deserialize(&path) .unwrap(); - // Show requester information to user and ask for confirmation to proceed + // Show requester information to user and ask for confirmation to proceed println!("---------------------------------"); println!("Requester information: "); - println!("{:?}", identity_initiation.as_ref().unwrap().requester_details.as_ref().unwrap()); + println!( + "{:?}", + identity_initiation + .as_ref() + .unwrap() + .requester_details + .as_ref() + .unwrap() + ); println!("---------------------------------"); println!("Recognise this attestation request and want to proceed? (y/n)"); let mut prompt = String::new(); @@ -404,19 +420,21 @@ async fn main() -> Result<(), Box> { if prompt != "y" && prompt != "yes" { println!("Aborting attestation request."); return Ok(()); - } - + let temp_p_key = identity_initiation.unwrap().temp_p_key.unwrap(); // call function to present challenge let identity_challenge = present_identity_challenge(&did, &temp_p_key)?; // print signed and encrypted payload to terminal - let payload = identity_challenge.identity_challenge_signature.as_ref().unwrap(); + let payload = identity_challenge + .identity_challenge_signature + .as_ref() + .unwrap(); println!("---------------------------------"); - println!("Payload:"); - println!("{:?}", payload); + println!("Payload:"); + println!("{:?}", payload); println!("Path: /did/attestor/identity/respond/"); println!("---------------------------------"); println!("Please send the above payload and path to the requester via alternative channels."); @@ -428,11 +446,15 @@ async fn main() -> Result<(), Box> { } Some(("respond", sub_matches)) => { // get attestation request path from provided input - let trustchain_dir: String = std::env::var(TRUSTCHAIN_DATA).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; + let trustchain_dir: String = std::env::var(TRUSTCHAIN_DATA) + .map_err(|_| TrustchainCRError::FailedAttestationRequest)?; let path_to_check = sub_matches.get_one::("path").unwrap(); - let path = PathBuf::new().join(trustchain_dir).join("attestation_requests").join(path_to_check); + let path = PathBuf::new() + .join(trustchain_dir) + .join("attestation_requests") + .join(path_to_check); if !path.exists() { - panic!("Provided attestation request not found. Path does not exist."); + panic!("Provided attestation request not found. Path does not exist."); } let did = sub_matches.get_one::("did").unwrap(); let (_, doc, _) = TrustchainAPI::resolve(did, resolver).await?; @@ -444,7 +466,8 @@ async fn main() -> Result<(), Box> { // service endpoint let services = doc.service.unwrap(); println!("Path: {:?}", path); - let identity_challenge_response = identity_response(&path, &services, &public_key).await?; + let identity_challenge_response = + identity_response(&path, &services, &public_key).await?; // serialise struct identity_challenge_response.elementwise_serialize(&path)?; } @@ -455,14 +478,18 @@ async fn main() -> Result<(), Box> { let did = sub_matches.get_one::("did").unwrap(); let ddid = sub_matches.get_one::("ddid").unwrap(); let path_to_check = sub_matches.get_one::("path").unwrap(); - + // check attestation request path - let trustchain_dir: String = std::env::var(TRUSTCHAIN_DATA).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; - let path = PathBuf::new().join(trustchain_dir).join("attestation_requests").join(path_to_check); + let trustchain_dir: String = std::env::var(TRUSTCHAIN_DATA) + .map_err(|_| TrustchainCRError::FailedAttestationRequest)?; + let path = PathBuf::new() + .join(trustchain_dir) + .join("attestation_requests") + .join(path_to_check); if !path.exists() { - panic!("Provided attestation request not found. Path does not exist."); + panic!("Provided attestation request not found. Path does not exist."); } - + // resolve DID, get services and attestor public key let (_, doc, _) = TrustchainAPI::resolve(did, resolver).await?; let doc = doc.unwrap(); @@ -470,23 +497,36 @@ async fn main() -> Result<(), Box> { let attestor_public_key_ssi = public_keys.first().unwrap(); let attestor_public_key = ssi_to_josekit_jwk(attestor_public_key_ssi).unwrap(); let services = &doc.service.unwrap(); - - let (content_initiation, content_challenge) = initiate_content_challenge(&path, ddid, &services, &attestor_public_key).await?; + + let (content_initiation, content_challenge) = + initiate_content_challenge(&path, ddid, &services, &attestor_public_key) + .await?; content_initiation.elementwise_serialize(&path)?; content_challenge.elementwise_serialize(&path)?; } - _ => panic!("Unrecognised CR content subcommand."),}, + _ => panic!("Unrecognised CR content subcommand."), + }, Some(("complete", sub_matches)) => { let path_to_check = sub_matches.get_one::("path").unwrap(); - let trustchain_dir: String = std::env::var(TRUSTCHAIN_DATA).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; - let path = PathBuf::new().join(trustchain_dir).join("attestation_requests").join(path_to_check); - let cr_state = CRState::new().elementwise_deserialize(&path).unwrap().unwrap(); + let trustchain_dir: String = std::env::var(TRUSTCHAIN_DATA) + .map_err(|_| TrustchainCRError::FailedAttestationRequest)?; + let path = PathBuf::new() + .join(trustchain_dir) + .join("attestation_requests") + .join(path_to_check); + let cr_state = CRState::new() + .elementwise_deserialize(&path) + .unwrap() + .unwrap(); let current_state = cr_state.check_cr_status().unwrap(); - - println!("State of attestation challenge-response process: {:?}", current_state); - }, + + println!( + "State of attestation challenge-response process: {:?}", + current_state + ); + } _ => panic!("Unrecognised CR subcommand."), - } + }, _ => panic!("Unrecognised subcommand."), } Ok(()) diff --git a/trustchain-core/src/utils.rs b/trustchain-core/src/utils.rs index 899d7c99..7360639f 100644 --- a/trustchain-core/src/utils.rs +++ b/trustchain-core/src/utils.rs @@ -35,12 +35,16 @@ pub fn init() { // Include test signing keys for two resolvable DIDs let root_plus_1_did_suffix = "EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"; let root_plus_2_did_suffix = "EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"; + let root_plus_2_candidate_did_suffix = "EiCDmY0qxsde9AdIwMf2tUKOiMo4aHnoWaPBRCeGt7iMHA"; let root_plus_1_signing_key: &str = r#"{"kty":"EC","crv":"secp256k1","x":"aApKobPO8H8wOv-oGT8K3Na-8l-B1AE3uBZrWGT6FJU","y":"dspEqltAtlTKJ7cVRP_gMMknyDPqUw-JHlpwS2mFuh0","d":"HbjLQf4tnwJR6861-91oGpERu8vmxDpW8ZroDCkmFvY"}"#; let root_plus_2_signing_key: &str = r#"{"kty":"EC","crv":"secp256k1","x":"0nnR-pz2EZGfb7E1qfuHhnDR824HhBioxz4E-EBMnM4","y":"rWqDVJ3h16RT1N-Us7H7xRxvbC0UlMMQQgxmXOXd4bY","d":"bJnhIQgj0eQoRXIw5Xna6LErnili2ajMstoJLI21HiQ"}"#; + let root_plus_2_candidate_signing_key: &str = r#"{"kty":"EC","crv":"secp256k1","x":"WzbWcgvvq21xKDTsvANakBSI3nJKDSmNa99usFmYJ0E","y":"vAFo1gkFqgEE3QsX1xlmHcoKxs5AuDqc18kkYEGVwDk","d":"LHt66ri5ykeVqEZwbzboJevbh5UEZkT8r8etsjg3KeE"}"#; let root_plus_1_signing_jwk: JWK= serde_json::from_str(root_plus_1_signing_key).unwrap(); let root_plus_2_signing_jwk: JWK= serde_json::from_str(root_plus_2_signing_key).unwrap(); + let root_plus_2_candidate_signing_jwk: JWK = serde_json::from_str(root_plus_2_candidate_signing_key).unwrap(); utils_key_manager.save_keys(root_plus_1_did_suffix, KeyType::SigningKey, &OneOrMany::One(root_plus_1_signing_jwk), false).unwrap(); utils_key_manager.save_keys(root_plus_2_did_suffix, KeyType::SigningKey, &OneOrMany::One(root_plus_2_signing_jwk), false).unwrap(); + utils_key_manager.save_keys(root_plus_2_candidate_did_suffix, KeyType::SigningKey, &OneOrMany::One(root_plus_2_candidate_signing_jwk), false).unwrap(); }); } diff --git a/trustchain-http/tests/attestation.rs b/trustchain-http/tests/attestation.rs index 6ed25339..3ee2d457 100644 --- a/trustchain-http/tests/attestation.rs +++ b/trustchain-http/tests/attestation.rs @@ -171,8 +171,8 @@ async fn attestation_challenge_response() { // The attestor decrypts the response and verifies the signature. It then compares the received // hashmap of nonces with the one sent to requester. // The entire process is automated and is kicked off with the content CR initiation request. - let requester_did = "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"; - // let requester_did = "did:ion:test:EiCDmY0qxsde9AdIwMf2tUKOiMo4aHnoWaPBRCeGt7iMHA"; + // let requester_did = "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"; + let requester_did = "did:ion:test:EiCDmY0qxsde9AdIwMf2tUKOiMo4aHnoWaPBRCeGt7iMHA"; let result = initiate_content_challenge( &request_path, requester_did, From 509e380681b3172709786249d8374bee88c797d4 Mon Sep 17 00:00:00 2001 From: pwochner Date: Thu, 30 Nov 2023 14:43:42 +0000 Subject: [PATCH 54/86] Distinguish between attestor and requester path for attestation CR requests. Update integration test accordingly. --- trustchain-cli/src/bin/main.rs | 6 +- trustchain-http/src/attestation_utils.rs | 74 +++++++++++--- trustchain-http/src/attestor.rs | 33 +++---- trustchain-http/src/requester.rs | 23 ++--- trustchain-http/tests/attestation.rs | 121 +++++++++++++++++------ 5 files changed, 179 insertions(+), 78 deletions(-) diff --git a/trustchain-cli/src/bin/main.rs b/trustchain-cli/src/bin/main.rs index 233714be..5b3537c0 100644 --- a/trustchain-cli/src/bin/main.rs +++ b/trustchain-cli/src/bin/main.rs @@ -374,13 +374,15 @@ async fn main() -> Result<(), Box> { println!("Organisation name: {}", org_name); println!("Operator name: {}", op_name); // initiate identity challenge - let result = initiate_identity_challenge( + let (identity_cr_initiation, path) = initiate_identity_challenge( org_name.trim(), op_name.trim(), &services.unwrap(), ) .await?; - println!("Result: {:?}", result); + identity_cr_initiation.elementwise_serialize(&path)?; + println!("Successfully initiated attestation request."); + println!("You will receive more information on the challenge-response process via alternative communication channel."); } Some(("present", sub_matches)) => { // get attestation request path from provided input diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index 9b3160c1..23795724 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -231,8 +231,9 @@ impl IdentityCRInitiation { requester_details: None, } } - - fn is_complete(&self) -> bool { + /// Returns true if all fields required for the initiation have a non-null value. + /// Note: temp_s_key is optional since only requester has it. + pub fn is_complete(&self) -> bool { return self.temp_p_key.is_some() && self.requester_details.is_some(); } } @@ -309,13 +310,14 @@ impl IdentityCRChallenge { } } /// Returns true if all fields required for the challenge have a non-null value. + /// Note: update_s_key is optional since only attestor has it. fn challenge_complete(&self) -> bool { return self.update_p_key.is_some() && self.identity_nonce.is_some() && self.identity_challenge_signature.is_some(); } - /// Returns true if all fields of the challenge-response have a non-null value. - fn response_complete(&self) -> bool { + /// Returns true if challenge-response is complete. + fn is_complete(&self) -> bool { return self.challenge_complete() && self.identity_response_signature.is_some(); } } @@ -337,6 +339,17 @@ impl ElementwiseSerializeDeserialize for IdentityCRChallenge { } Err(_) => None, }; + // update secret key + let mut full_path = path.join("update_s_key.json"); + self.update_s_key = match File::open(&full_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + let deserialized = serde_json::from_reader(reader) + .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + Some(deserialized) + } + Err(_) => None, + }; // identity nonce full_path = path.join("identity_nonce.json"); self.identity_nonce = match File::open(&full_path) { @@ -493,7 +506,7 @@ impl ContentCRChallenge { return self.content_nonce.is_some() && self.content_challenge_signature.is_some(); } /// Returns true if all fields required for the challenge-response have a non-null value. - fn response_complete(&self) -> bool { + fn is_complete(&self) -> bool { return self.challenge_complete() && self.content_response_signature.is_some(); } } @@ -569,11 +582,33 @@ impl CRState { } } /// Returns true if all fields have a non-null value. + // pub fn is_complete(&self) -> bool { + // if self.identity_cr_initiation.is_some() + // && self.identity_challenge_response.is_some() + // && self.content_cr_initiation.is_some() + // && self.content_challenge_response.is_some() + // { + // return true; + // } + // return false; + // } pub fn is_complete(&self) -> bool { - if self.identity_cr_initiation.is_some() - && self.identity_challenge_response.is_some() - && self.content_cr_initiation.is_some() - && self.content_challenge_response.is_some() + if (self.identity_cr_initiation.is_some() + && self.identity_cr_initiation.as_ref().unwrap().is_complete()) + && (self.identity_challenge_response.is_some() + && self + .identity_challenge_response + .as_ref() + .unwrap() + .is_complete()) + && (self.content_cr_initiation.is_some() + && self.content_cr_initiation.as_ref().unwrap().is_complete()) + && (self.content_challenge_response.is_some() + && self + .content_challenge_response + .as_ref() + .unwrap() + .is_complete()) { return true; } @@ -624,7 +659,7 @@ impl CRState { .identity_challenge_response .as_ref() .unwrap() - .response_complete() + .is_complete() { return Ok(current_state); } @@ -655,7 +690,7 @@ impl CRState { .content_challenge_response .as_ref() .unwrap() - .response_complete() + .is_complete() { return Ok(current_state); } @@ -749,16 +784,23 @@ pub fn matching_endpoint( } /// Returns unique path name for a specific attestation request derived from public key for the interaction. -pub fn attestation_request_path(key: &JWK) -> Result { +pub fn attestation_request_path(key: &JWK, prefix: &str) -> Result { // Root path in TRUSTCHAIN_DATA - let path: String = - std::env::var(TRUSTCHAIN_DATA).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; + let path = attestation_request_basepath(prefix) + .map_err(|_| TrustchainCRError::FailedAttestationRequest)?; let key_id = key .thumbprint() .map_err(|_| TrustchainCRError::MissingJWK)?; // Use hash of temp_pub_key + Ok(path.join(key_id)) +} + +pub fn attestation_request_basepath(prefix: &str) -> Result { + // Root path in TRUSTCHAIN_DATA + let path: String = + std::env::var(TRUSTCHAIN_DATA).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; Ok(Path::new(path.as_str()) - .join("attestation_requests") - .join(key_id)) + .join(prefix) + .join("attestation_responses")) } #[cfg(test)] diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index a512b7d2..ae118ec3 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -3,8 +3,9 @@ use crate::attestation_encryption_utils::{ SignEncrypt, }; use crate::attestation_utils::{ - attestation_request_path, ContentCRChallenge, CustomResponse, ElementwiseSerializeDeserialize, - IdentityCRChallenge, IdentityCRInitiation, Nonce, TrustchainCRError, + attestation_request_basepath, attestation_request_path, ContentCRChallenge, + ContentCRInitiation, CustomResponse, ElementwiseSerializeDeserialize, IdentityCRChallenge, + IdentityCRInitiation, Nonce, TrustchainCRError, }; use crate::state::AppState; use async_trait::async_trait; @@ -72,7 +73,7 @@ impl TrustchainAttestorHTTPHandler { info!("Received attestation info: {:?}", attestation_initiation); let temp_p_key_ssi = josekit_to_ssi_jwk(attestation_initiation.temp_p_key.as_ref().unwrap()); - let path = attestation_request_path(&temp_p_key_ssi.unwrap()).unwrap(); + let path = attestation_request_path(&temp_p_key_ssi.unwrap(), "attestor").unwrap(); // create directory and save attestation initation to file let _ = std::fs::create_dir_all(&path); let result = attestation_initiation.elementwise_serialize(&path); @@ -106,11 +107,8 @@ impl TrustchainAttestorHTTPHandler { (Path(key_id), Json(response)): (Path, Json), app_state: Arc, ) -> impl IntoResponse { - let trustchain_dir: String = std::env::var(TRUSTCHAIN_DATA).unwrap(); - let path = PathBuf::new() - .join(trustchain_dir) - .join("attestation_requests") - .join(&key_id); + let pathbase = attestation_request_basepath("attestor").unwrap(); + let path = pathbase.join(&key_id); if !path.exists() { panic!("Provided attestation request not found. Path does not exist."); } @@ -125,7 +123,6 @@ impl TrustchainAttestorHTTPHandler { let signing_key_ssi = signing_keys.first().unwrap(); let signing_key = ssi_to_josekit_jwk(&signing_key_ssi); // get temp public key - // TODO: use elementise_deserialize to extract temp_p_key let identity_initiation = IdentityCRInitiation::new() .elementwise_deserialize(&path) .unwrap() @@ -169,6 +166,8 @@ impl TrustchainAttestorHTTPHandler { (Path(key_id), Json(ddid)): (Path, Json), app_state: Arc, ) -> impl IntoResponse { + let pathbase = attestation_request_basepath("attestor").unwrap(); + let path = pathbase.join(&key_id); let did = app_state.config.server_did.as_ref().unwrap().to_owned(); // resolve candidate DID let result = TrustchainAPI::resolve(&ddid, app_state.verifier.resolver()).await; @@ -182,7 +181,11 @@ impl TrustchainAttestorHTTPHandler { return (StatusCode::BAD_REQUEST, Json(respone)); } }; - + // serialize content initiation request + let content_initiation = ContentCRInitiation { + requester_did: Some(ddid), + }; + content_initiation.elementwise_serialize(&path); // extract map of keys from candidate document and generate a nonce per key let requester_keys = extract_key_ids_and_jwk(&candidate_doc).unwrap(); let attestor = Entity {}; @@ -210,10 +213,6 @@ impl TrustchainAttestorHTTPHandler { acc }); // get public and secret keys - let path = PathBuf::new() - .join(std::env::var(TRUSTCHAIN_DATA).unwrap()) - .join("attestation_requests") - .join(&key_id); let identity_cr_initiation = IdentityCRInitiation::new() .elementwise_deserialize(&path) .unwrap() @@ -268,10 +267,8 @@ impl TrustchainAttestorHTTPHandler { app_state: Arc, ) -> impl IntoResponse { // deserialise expected nonce map - let path = PathBuf::new() - .join(std::env::var(TRUSTCHAIN_DATA).unwrap()) - .join("attestation_requests") - .join(&key_id); + let pathbase = attestation_request_basepath("attestor").unwrap(); + let path = pathbase.join(&key_id); let identity_cr_initiation = IdentityCRInitiation::new() .elementwise_deserialize(&path) .unwrap() diff --git a/trustchain-http/src/requester.rs b/trustchain-http/src/requester.rs index ab258f42..9a6311df 100644 --- a/trustchain-http/src/requester.rs +++ b/trustchain-http/src/requester.rs @@ -28,7 +28,7 @@ pub async fn initiate_identity_challenge( org_name: &str, op_name: &str, services: &[Service], -) -> Result<(), TrustchainCRError> { +) -> Result<(IdentityCRInitiation, PathBuf), TrustchainCRError> { // generate temp key let temp_s_key_ssi = generate_key(); let temp_p_key_ssi = temp_s_key_ssi.to_public(); @@ -65,16 +65,14 @@ pub async fn initiate_identity_challenge( if result.status() != 200 { return Err(TrustchainCRError::FailedToInitiateCR); } - // create new directory - let directory = attestation_request_path(&temp_s_key_ssi.to_public())?; - std::fs::create_dir_all(&directory).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; + // create new directory for attestation request + let path = attestation_request_path(&temp_s_key_ssi.to_public(), "requester")?; + std::fs::create_dir_all(&path).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; - // serialise identity_cr_initiation struct to file + // Add secret key to struct identity_cr_initiation.temp_s_key = Some(temp_s_key); - identity_cr_initiation.elementwise_serialize(&directory)?; - println!("Successfully initiated attestation request."); - println!("You will receive more information on the challenge-response process via alternative communication channel."); - Ok(()) + + Ok((identity_cr_initiation, path)) } /// Generates and posts response for part 1 of attesation process (identity challenge-response). @@ -91,6 +89,7 @@ pub async fn identity_response( // deserialise challenge struct from file let result = IdentityCRChallenge::new().elementwise_deserialize(path); let mut identity_challenge = result.unwrap().unwrap(); + // get temp secret key from file let identity_initiation = IdentityCRInitiation::new().elementwise_deserialize(path); let temp_s_key = identity_initiation.unwrap().unwrap().temp_s_key.unwrap(); let temp_s_key_ssi = josekit_to_ssi_jwk(&temp_s_key).unwrap(); @@ -191,16 +190,14 @@ pub async fn initiate_content_challenge( let signed_encrypted_challenge = response_body.data.unwrap(); // response - let result = content_response( + let (nonces, response) = content_response( &path, &signed_encrypted_challenge.to_string(), services, attestor_p_key.clone(), &ddid.to_owned(), ) - .await; - // TODO: better error handling - let (nonces, response) = result.unwrap(); + .await?; let content_challenge = ContentCRChallenge { content_nonce: Some(nonces), content_challenge_signature: Some(signed_encrypted_challenge.to_string()), diff --git a/trustchain-http/tests/attestation.rs b/trustchain-http/tests/attestation.rs index 3ee2d457..2a194a38 100644 --- a/trustchain-http/tests/attestation.rs +++ b/trustchain-http/tests/attestation.rs @@ -1,8 +1,10 @@ /// Integration test for attestation challenge-response process. use trustchain_core::verifier::Verifier; -use trustchain_core::TRUSTCHAIN_DATA; -use trustchain_http::attestation_encryption_utils::ssi_to_josekit_jwk; -use trustchain_http::attestation_utils::{ElementwiseSerializeDeserialize, IdentityCRInitiation}; +use trustchain_http::attestation_encryption_utils::{josekit_to_ssi_jwk, ssi_to_josekit_jwk}; +use trustchain_http::attestation_utils::{ + attestation_request_path, CRState, ElementwiseSerializeDeserialize, IdentityCRChallenge, + IdentityCRInitiation, +}; use trustchain_http::attestor::present_identity_challenge; use trustchain_http::requester::{ identity_response, initiate_content_challenge, initiate_identity_challenge, @@ -92,6 +94,10 @@ async fn attestation_challenge_response() { let result = initiate_identity_challenge(&expected_org_name, &expected_operator_name, &services).await; + // Make sure initiation was successful and information is complete before serializing. + assert!(result.is_ok()); + let (identity_initiation_requester, requester_path) = result.unwrap(); + let result = identity_initiation_requester.elementwise_serialize(&requester_path); assert!(result.is_ok()); // |------------| attestor |------------| @@ -99,45 +105,51 @@ async fn attestation_challenge_response() { // done manually using `trustchain-cli`, where the attestor has to confirm that they recognize // the requester and that they want to proceed with challenge-response process // for attestation. - let path = std::env::var(TRUSTCHAIN_DATA).unwrap(); - let attestation_requests_path = PathBuf::from(path).join("attestation_requests"); - - // For the test, there should be only one attestation request (subdirectory). - let paths = fs::read_dir(attestation_requests_path).unwrap(); - let request_path: PathBuf = paths.map(|path| path.unwrap().path()).collect(); + let temp_p_key = + josekit_to_ssi_jwk(&identity_initiation_requester.clone().temp_p_key.unwrap()).unwrap(); + let attestor_path = attestation_request_path(&temp_p_key, "attestor").unwrap(); // Deserialized received information and check that it is correct. - let identity_initiation = IdentityCRInitiation::new() - .elementwise_deserialize(&request_path) + let identity_initiation_attestor = IdentityCRInitiation::new() + .elementwise_deserialize(&attestor_path) .unwrap() .unwrap(); - let org_name = identity_initiation + // Make sure that attestor has all required information about initiation (but not secret key). + assert_eq!(identity_initiation_attestor.is_complete(), true); + assert!(identity_initiation_attestor.temp_s_key.is_none()); + let org_name = identity_initiation_attestor .requester_details .clone() .unwrap() .requester_org; - let operator_name = identity_initiation + let operator_name = identity_initiation_attestor .requester_details .clone() .unwrap() .operator_name; assert_eq!(expected_org_name, org_name); assert_eq!(expected_operator_name, operator_name); + // If data matches, proceed with presenting signed and encrypted identity challenge payload. - let temp_p_key = identity_initiation.clone().temp_p_key.unwrap(); - let identity_challenge_attestor = - present_identity_challenge(&attestor_did, &temp_p_key).unwrap(); - let payload = identity_challenge_attestor - .identity_challenge_signature - .as_ref() - .unwrap(); + let temp_p_key = identity_initiation_attestor.clone().temp_p_key.unwrap(); + let result = present_identity_challenge(&attestor_did, &temp_p_key); + assert!(result.is_ok()); + let identity_challenge_attestor = result.unwrap(); + let _ = identity_challenge_attestor.elementwise_serialize(&attestor_path); - // Write payload as requester (this step would done manually or by GUI, since in deployment - // challenge payload is sent via alternative channel) for use in subsequent response. - // However, as nonce for verifying response is required in part 1.3, serialise - // full struct instead. - identity_challenge_attestor - .elementwise_serialize(&request_path) + // |------------| requester |------------| + // Write signed and encrypted challenge to file to requester path (this step would done manually + // or by GUI, since in deployment + // challenge is sent via alternative channel) for use in subsequent response. + let identity_challenge_requester = IdentityCRChallenge { + update_p_key: None, + update_s_key: None, + identity_challenge_signature: identity_challenge_attestor.identity_challenge_signature, + identity_nonce: None, + identity_response_signature: None, + }; + identity_challenge_requester + .elementwise_serialize(&requester_path) .unwrap(); // Part 1.3: Requester responds to challenge. The received challenge is first decrypted and @@ -145,14 +157,18 @@ async fn attestation_challenge_response() { // public key. This response is sent to attestor via a POST request. // Upon receiving the request, the attestor decrypts the response and verifies the signature, // before comparing the nonce from the response with the nonce from the challenge. - // |------------| requester |------------| + let public_keys = extract_keys(&attestor_doc); let attestor_public_key_ssi = public_keys.first().unwrap(); let attestor_public_key = ssi_to_josekit_jwk(attestor_public_key_ssi).unwrap(); // Check nonce component is captured with the response being Ok - let result = identity_response(&request_path, &services, &attestor_public_key).await; + let result = identity_response(&requester_path, &services, &attestor_public_key).await; assert!(result.is_ok()); + let identity_challenge_requester = result.unwrap(); + identity_challenge_requester + .elementwise_serialize(&requester_path) + .unwrap(); // |--------------------------------------------------------------| // |------------| Part 2: content challenge-response |------------| @@ -174,7 +190,7 @@ async fn attestation_challenge_response() { // let requester_did = "did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"; let requester_did = "did:ion:test:EiCDmY0qxsde9AdIwMf2tUKOiMo4aHnoWaPBRCeGt7iMHA"; let result = initiate_content_challenge( - &request_path, + &requester_path, requester_did, &services, &attestor_public_key, @@ -182,4 +198,51 @@ async fn attestation_challenge_response() { .await; // Check nonces is captured with the response being Ok assert!(result.is_ok()); + let (content_cr_initiation, content_cr_challenge) = result.unwrap(); + content_cr_initiation + .elementwise_serialize(&requester_path) + .unwrap(); + content_cr_challenge + .elementwise_serialize(&requester_path) + .unwrap(); + + // Check that requester has all attestation challenge-response information it should have. + let cr_state_requester = CRState::new() + .elementwise_deserialize(&requester_path) + .unwrap() + .unwrap(); + let result = cr_state_requester.is_complete(); + assert_eq!(result, true); + + // Check that requester has temp_s_key but not update_s_key. + assert!(cr_state_requester + .identity_cr_initiation + .unwrap() + .temp_s_key + .is_some()); + assert!(cr_state_requester + .identity_challenge_response + .unwrap() + .update_s_key + .is_none()); + + // |------------| attestor |------------| + // Check that attestor has all attestation challenge-response information it should have. + let cr_state_attestor = CRState::new() + .elementwise_deserialize(&attestor_path) + .unwrap() + .unwrap(); + let result = cr_state_attestor.is_complete(); + assert_eq!(result, true); + // Check that attestor does not have temp_s_key but update_s_key. + assert!(cr_state_attestor + .identity_cr_initiation + .unwrap() + .temp_s_key + .is_none()); + assert!(cr_state_attestor + .identity_challenge_response + .unwrap() + .update_s_key + .is_some()); } From 8711c7b7887950ff04302457f9ca87da357beb37 Mon Sep 17 00:00:00 2001 From: pwochner Date: Thu, 30 Nov 2023 15:53:29 +0000 Subject: [PATCH 55/86] Remove unnecessary print statements. --- trustchain-http/src/attestation_utils.rs | 1 - trustchain-http/src/attestor.rs | 2 -- trustchain-http/src/requester.rs | 21 +++++++++++++++------ trustchain-http/tests/attestation.rs | 2 +- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index 23795724..03810413 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -766,7 +766,6 @@ pub fn matching_endpoint( ) -> Result { let mut endpoints = Vec::new(); for service in services { - println!("service id: {}", service.id); if service.id.eq(fragment) { match &service.service_endpoint { Some(OneOrMany::One(ServiceEndpoint::URI(uri))) => { diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index ae118ec3..b00904a6 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -298,8 +298,6 @@ impl TrustchainAttestorHTTPHandler { serde_json::from_value(payload.claim("nonces").unwrap().clone()).unwrap(); // verify nonces if nonces_map.eq(&expected_nonces) { - println!("nonces map: {:?}", nonces_map); - println!("expected nonces map: {:?}", expected_nonces); content_challenge.content_response_signature = Some(response.clone()); content_challenge.elementwise_serialize(&path).unwrap(); let response = CustomResponse { diff --git a/trustchain-http/src/requester.rs b/trustchain-http/src/requester.rs index 9a6311df..ea581995 100644 --- a/trustchain-http/src/requester.rs +++ b/trustchain-http/src/requester.rs @@ -250,12 +250,21 @@ pub async fn content_response( let ion_attestor = IONAttestor::new(&ddid); let signing_keys = ion_attestor.signing_keys().unwrap(); // iterate over all keys, convert to Jwk (josekit) -> TODO: functional - let mut signing_keys_map: HashMap = HashMap::new(); - for key in signing_keys { - let key_id = key.thumbprint().unwrap(); - let jwk = ssi_to_josekit_jwk(&key).unwrap(); - signing_keys_map.insert(key_id, jwk); - } + // let mut signing_keys_map: HashMap = HashMap::new(); + // for key in signing_keys { + // let key_id = key.thumbprint().unwrap(); + // let jwk = ssi_to_josekit_jwk(&key).unwrap(); + // signing_keys_map.insert(key_id, jwk); + // } + + let signing_keys_map = signing_keys + .into_iter() + .fold(HashMap::new(), |mut acc, key| { + let key_id = key.thumbprint().unwrap(); + let jwk = ssi_to_josekit_jwk(&key).unwrap(); + acc.insert(key_id, jwk); + acc + }); let decrypted_nonces: HashMap = challenges_map diff --git a/trustchain-http/tests/attestation.rs b/trustchain-http/tests/attestation.rs index 2a194a38..b8c333bc 100644 --- a/trustchain-http/tests/attestation.rs +++ b/trustchain-http/tests/attestation.rs @@ -180,7 +180,7 @@ async fn attestation_challenge_response() { // attestor's endpoint. // Upon receiving the POST request the attestor resolves dDID, extracts the signing keys from it // and returns to the requester a signed and encrypted challenge payload with a hashmap that - // contains an encrypted nonce per signing key. + // contains an encrypted nonce pecurr signing key. // The requester decrypts the challenge payload and verifies the signature. It then decrypts // each nonce with the corresponding signing key and collects them in a hashmap. This // hashmap is signed and encrypted and sent back to the attestor via POST request. From 736b686a992963a92c29e34661f556f34987d7b9 Mon Sep 17 00:00:00 2001 From: pwochner Date: Thu, 14 Dec 2023 10:04:10 +0000 Subject: [PATCH 56/86] Adds docstrings. --- trustchain-http/src/attestation_utils.rs | 40 +++++++++++------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index 03810413..0f774fb3 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -42,9 +42,9 @@ pub enum TrustchainCRError { /// Failed to open file. #[error("Failed to open file.")] FailedToOpen, - /// Failed to save to file. - #[error("Failed to save to file.")] - FailedToSave, + /// Failed to serialize to file. + #[error("Failed to serialize to file.")] + FailedToSerialize, /// Failed to set permissions on file. #[error("Failed to set permissions on file.")] FailedToSetPermissions, @@ -90,12 +90,14 @@ impl From for TrustchainCRError { } #[derive(Serialize, Deserialize)] +/// Type for implementing custom response returned by the server. Provides a message and optional data field. pub struct CustomResponse { pub message: String, pub data: Option, } #[derive(Debug, PartialEq)] +/// Enumerates the possible states of the challenge-response process. pub enum CurrentCRState { NotStarted, IdentityCRInitiated, @@ -108,6 +110,7 @@ pub enum CurrentCRState { // TODO: Impose additional constraints on the nonce type. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +/// Nonce type for challenge-response. pub struct Nonce(String); impl Nonce { @@ -157,7 +160,7 @@ where /// Serialize each field of the struct to a file. fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { let serialized = - serde_json::to_value(&self).map_err(|_| TrustchainCRError::FailedToSave)?; + serde_json::to_value(&self).map_err(|_| TrustchainCRError::FailedToSerialize)?; if let Value::Object(fields) = serialized { for (field_name, field_value) in fields { if !field_value.is_null() { @@ -199,17 +202,18 @@ where .map_err(|_| TrustchainCRError::FailedToSetPermissions)?; Ok(()) } - Err(_) => Err(TrustchainCRError::FailedToSave), + Err(_) => Err(TrustchainCRError::FailedToSerialize), } } - Err(_) => Err(TrustchainCRError::FailedToSave), + Err(_) => Err(TrustchainCRError::FailedToSerialize), } } } #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone)] +/// Type for storing details of the requester. pub struct RequesterDetails { pub requester_org: String, pub operator_name: String, @@ -217,6 +221,7 @@ pub struct RequesterDetails { #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, IsEmpty, Clone)] +/// Type for storing initiation details of the attestation request. pub struct IdentityCRInitiation { pub temp_p_key: Option, pub temp_s_key: Option, @@ -290,6 +295,7 @@ impl ElementwiseSerializeDeserialize for IdentityCRInitiation { #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, IsEmpty)] +/// Type for storing details of part one (identity challenge) of the challenge-response process. pub struct IdentityCRChallenge { pub update_p_key: Option, pub update_s_key: Option, @@ -442,6 +448,7 @@ impl TryFrom<&JwtPayload> for IdentityCRChallenge { } #[derive(Debug, Serialize, Deserialize, Clone, IsEmpty)] +/// Type for storing initiation details of part two (content challenge) of the challenge-response process. pub struct ContentCRInitiation { pub requester_did: Option, } @@ -475,9 +482,6 @@ impl ElementwiseSerializeDeserialize for ContentCRInitiation { Err(_) => None, }; - // if self.temp_p_key.is_none() && self.requester_did.is_none() { - // return Ok(None); - // } if self.requester_did.is_none() { return Ok(None); } @@ -487,6 +491,7 @@ impl ElementwiseSerializeDeserialize for ContentCRInitiation { } #[derive(Debug, Serialize, Deserialize, IsEmpty)] +/// Type for storing details of part two (content challenge) of the challenge-response process. pub struct ContentCRChallenge { pub content_nonce: Option>, pub content_challenge_signature: Option, @@ -565,6 +570,8 @@ impl ElementwiseSerializeDeserialize for ContentCRChallenge { #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, IsEmpty)] +/// Type for representing the state of the challenge-response process. Holds information about both +/// identity (part one) and content challenge-response (part two) and their respective initiation. pub struct CRState { pub identity_cr_initiation: Option, pub identity_challenge_response: Option, @@ -581,17 +588,7 @@ impl CRState { content_challenge_response: None, } } - /// Returns true if all fields have a non-null value. - // pub fn is_complete(&self) -> bool { - // if self.identity_cr_initiation.is_some() - // && self.identity_challenge_response.is_some() - // && self.content_cr_initiation.is_some() - // && self.content_challenge_response.is_some() - // { - // return true; - // } - // return false; - // } + /// Returns true if all fields are complete. pub fn is_complete(&self) -> bool { if (self.identity_cr_initiation.is_some() && self.identity_cr_initiation.as_ref().unwrap().is_complete()) @@ -793,13 +790,14 @@ pub fn attestation_request_path(key: &JWK, prefix: &str) -> Result Result { // Root path in TRUSTCHAIN_DATA let path: String = std::env::var(TRUSTCHAIN_DATA).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; Ok(Path::new(path.as_str()) .join(prefix) - .join("attestation_responses")) + .join("attestation_requests")) } #[cfg(test)] From 935e73c5c91aa284473a5da63acc07be2bebc490 Mon Sep 17 00:00:00 2001 From: pwochner Date: Thu, 14 Dec 2023 17:24:59 +0000 Subject: [PATCH 57/86] Fixed bugs in cli. --- trustchain-cli/src/bin/main.rs | 27 +++++++++++++----------- trustchain-http/src/attestation_utils.rs | 12 ++++++++++- trustchain-http/src/attestor.rs | 20 +++++++++--------- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/trustchain-cli/src/bin/main.rs b/trustchain-cli/src/bin/main.rs index 5b3537c0..cd407d78 100644 --- a/trustchain-cli/src/bin/main.rs +++ b/trustchain-cli/src/bin/main.rs @@ -116,14 +116,14 @@ fn cli() -> Command { Command::new("present") .about("Produce challenge for identity CR to be presented to requestor.") .arg(arg!(-v - -verbose).action(ArgAction::Count)) - .arg(arg!(-p --path ).required(true)) + .arg(arg!(-p --path ).required(true)) .arg(arg!(-d --did ).required(true)) ) .subcommand( Command::new("respond") .about("Produce response for identity challenge to be posted to attestor.") .arg(arg!(-v - -verbose).action(ArgAction::Count)) - .arg(arg!(-p --path ).required(true)) + .arg(arg!(-p --path ).required(true)) .arg(arg!(-d --did ).required(true)) ) ) @@ -137,15 +137,16 @@ fn cli() -> Command { .about("Initiates the content challenge-response process.") .arg(arg!(-v - -verbose).action(ArgAction::Count)) .arg(arg!(-d --did ).required(true)) - .arg(arg!(-d --ddid ).required(true)) - .arg(arg!(-p --path ).required(true)) + .arg(arg!(--ddid ).required(true)) + .arg(arg!(-p --path ).required(true)) ) ) .subcommand( Command::new("complete") .about("Check if challenge-response for attestation request has been completed.") .arg(arg!(-v - -verbose).action(ArgAction::SetTrue)) - .arg(arg!(-p --path ).required(true)) + .arg(arg!(-p --path ).required(true)) + .arg(arg!(-e --entity ).required(true)) ) ) @@ -356,7 +357,6 @@ async fn main() -> Result<(), Box> { let _result = verifier.verify(did, root_event_time.into()).await?; let (_, doc, _) = TrustchainAPI::resolve(did, resolver).await?; let services = doc.unwrap().service; - println!("Services: {:?}", services); // user promt for org name and operator name println!("Please enter your organisation name: "); @@ -392,6 +392,7 @@ async fn main() -> Result<(), Box> { let did = sub_matches.get_one::("did").unwrap(); let path = PathBuf::new() .join(trustchain_dir) + .join("attestor") .join("attestation_requests") .join(path_to_check); if !path.exists() { @@ -435,13 +436,12 @@ async fn main() -> Result<(), Box> { .as_ref() .unwrap(); println!("---------------------------------"); - println!("Payload:"); + println!("Signed and encrypted challenge:"); println!("{:?}", payload); - println!("Path: /did/attestor/identity/respond/"); println!("---------------------------------"); - println!("Please send the above payload and path to the requester via alternative channels."); - println!("To respond, the requester posts it to the provided path, which has to be appended to the attestor endpoint."); - println!(" has to be replaced by the key_id of the temporary public key provided in the initial request."); + println!("Please send the above challenge to the requester via alternative channels."); + println!("Before responding using the 'respond' subcommand, the requester has to save the challenge to a file named 'identity_challenge_signature.json' in the corresponding attestation request directory."); + println!("---------------------------------"); // serialise struct identity_challenge.elementwise_serialize(&path)?; @@ -453,6 +453,7 @@ async fn main() -> Result<(), Box> { let path_to_check = sub_matches.get_one::("path").unwrap(); let path = PathBuf::new() .join(trustchain_dir) + .join("requester") .join("attestation_requests") .join(path_to_check); if !path.exists() { @@ -467,7 +468,6 @@ async fn main() -> Result<(), Box> { let public_key = ssi_to_josekit_jwk(attestor_public_key_ssi).unwrap(); // service endpoint let services = doc.service.unwrap(); - println!("Path: {:?}", path); let identity_challenge_response = identity_response(&path, &services, &public_key).await?; // serialise struct @@ -486,6 +486,7 @@ async fn main() -> Result<(), Box> { .map_err(|_| TrustchainCRError::FailedAttestationRequest)?; let path = PathBuf::new() .join(trustchain_dir) + .join("requester") .join("attestation_requests") .join(path_to_check); if !path.exists() { @@ -510,10 +511,12 @@ async fn main() -> Result<(), Box> { }, Some(("complete", sub_matches)) => { let path_to_check = sub_matches.get_one::("path").unwrap(); + let entity = sub_matches.get_one::("entity").unwrap(); let trustchain_dir: String = std::env::var(TRUSTCHAIN_DATA) .map_err(|_| TrustchainCRError::FailedAttestationRequest)?; let path = PathBuf::new() .join(trustchain_dir) + .join(entity) .join("attestation_requests") .join(path_to_check); let cr_state = CRState::new() diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index 0f774fb3..9464502a 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -259,6 +259,16 @@ impl ElementwiseSerializeDeserialize for IdentityCRInitiation { } Err(_) => None, }; + // TODO: complete refactor + // if !Path::new(&temp_p_key_path).exists() { + // self.temp_p_key = None; + // } + // let deserialized = serde_json::from_str( + // &fs::read_to_string(&temp_p_key_path) + // .map_err(|_| TrustchainCRError::FailedToDeserialize)?, + // ) + // .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + // self.temp_p_key = Some(deserialized); let temp_s_key_path = path.join("temp_s_key.json"); self.temp_s_key = match File::open(&temp_s_key_path) { @@ -732,7 +742,7 @@ impl ElementwiseSerializeDeserialize for CRState { fn get_status_message(current_state: &CurrentCRState) -> String { match current_state { CurrentCRState::NotStarted => { - return String::from("No records found for this challenge-response identifier. \nThe challenge-response process has not been initiated yet."); + return String::from("No records found for this challenge-response identifier or entity. \nThe challenge-response process has not been initiated yet."); } CurrentCRState::IdentityCRInitiated => { return String::from("Identity challenge-response initiated. Await response."); diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index b00904a6..62198cd8 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -21,21 +21,19 @@ use trustchain_api::TrustchainAPI; use trustchain_core::verifier::Verifier; use std::collections::HashMap; -use std::fs::File; -use std::io::BufReader; use std::path::PathBuf; use std::sync::Arc; use trustchain_core::utils::generate_key; -use trustchain_core::TRUSTCHAIN_DATA; use trustchain_ion::attestor::IONAttestor; // Encryption: https://github.com/hidekatsu-izuno/josekit-rs#signing-a-jwt-by-ecdsa #[async_trait] +/// An API for a Trustchain attestor server. pub trait TrustchainAttestorHTTP {} -/// Type for implementing the TrustchainIssuerHTTP trait that will contain additional handler methods. +/// Type for implementing the TrustchainAttestorHTTP trait that will contain additional handler methods. pub struct TrustchainAttestorHTTPHandler; #[async_trait] @@ -63,7 +61,7 @@ impl TrustchainAttestorHTTP for TrustchainAttestorHTTPHandler { } impl TrustchainAttestorHTTPHandler { - /// Handles a POST request for identity initiation (part 1 attestation CR). + /// Handles a POST request for identity initiation (part one attestation CR). /// /// This function saves the attestation initiation to a file. The directory to which the information /// is saved is determined by the temp public key of the attestation initiation. @@ -121,7 +119,7 @@ impl TrustchainAttestorHTTPHandler { let ion_attestor = IONAttestor::new(&did); let signing_keys = ion_attestor.signing_keys().unwrap(); let signing_key_ssi = signing_keys.first().unwrap(); - let signing_key = ssi_to_josekit_jwk(&signing_key_ssi); + let signing_key = ssi_to_josekit_jwk(&signing_key_ssi).unwrap(); // get temp public key let identity_initiation = IdentityCRInitiation::new() .elementwise_deserialize(&path) @@ -131,7 +129,7 @@ impl TrustchainAttestorHTTPHandler { // verify response let attestor = Entity {}; let payload = attestor - .decrypt_and_verify(response.clone(), &signing_key.unwrap(), &temp_p_key) + .decrypt_and_verify(response.clone(), &signing_key, &temp_p_key) .unwrap(); let result = verify_nonce(payload, &path); match result { @@ -154,7 +152,7 @@ impl TrustchainAttestorHTTPHandler { } } - /// Handles a POST request for content initiation (part 2 attestation CR). + /// Handles a POST request for content initiation (part two attestation CR). /// /// This function receives the key ID of the temporary public key and the candidate DID. /// It resolves the candidate DID and extracts the public signing keys from the document. @@ -181,6 +179,8 @@ impl TrustchainAttestorHTTPHandler { return (StatusCode::BAD_REQUEST, Json(respone)); } }; + // TODO: check if resolved candidate DID contains expected update_p_key + // serialize content initiation request let content_initiation = ContentCRInitiation { requester_did: Some(ddid), @@ -315,7 +315,7 @@ impl TrustchainAttestorHTTPHandler { } } -/// Generates challenge for part 1 of attestation request (identity challenge-response). +/// Generates challenge for part one of attestation request (identity challenge-response). /// /// This function generates a new key pair for the update key and nonce for the challenge. /// It then adds the update public key and nonce to a payload and signs it with the secret @@ -362,7 +362,7 @@ pub fn present_identity_challenge( Ok(identity_challenge) } -/// Verifies nonce for part 1 of attestation request (identity challenge-response). +/// Verifies nonce for part one of attestation request (identity challenge-response). /// /// This function receives a payload provided by requester and the path to the directory /// where information about the attestation request is stored. It deserialises the expected From bfe36560d215011374bbe2b38613298e9d117ed9 Mon Sep 17 00:00:00 2001 From: Pamela Wochner Date: Mon, 1 Jul 2024 17:48:37 +0200 Subject: [PATCH 58/86] Better error handling: replace unwrap and map_err where possible. --- trustchain-http/src/attestation_utils.rs | 72 ++++++++++++++---------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index 33b985aa..f68f123c 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -36,6 +36,9 @@ pub enum TrustchainCRError { /// Claim not found in JWTPayload. #[error("Claim not found in JWTPayload.")] ClaimNotFound, + /// Claim cannot be constructed + #[error("Claim cannot be constructed from: {0}")] + ClaimCannotBeConstructed(String), /// Nonce type invalid. #[error("Invalid nonce type.")] InvalidNonceType, @@ -51,6 +54,9 @@ pub enum TrustchainCRError { /// Failed deserialize from file. #[error("Failed to deserialize.")] FailedToDeserialize, + /// Failed deserialize from file. + #[error("Failed to deserialize with error: {0}.")] + FailedToDeserializeWithError(serde_json::Error), /// Failed to check CR status. #[error("Failed to determine CR status.")] FailedStatusCheck, @@ -152,6 +158,12 @@ impl TryFrom<&Nonce> for JwtPayload { } } +impl From for TrustchainCRError { + fn from(value: serde_json::Error) -> Self { + TrustchainCRError::FailedToDeserializeWithError(value) + } +} + /// Interface for serializing and deserializing each field of structs to/from files. pub trait ElementwiseSerializeDeserialize where @@ -159,15 +171,13 @@ where { /// Serialize each field of the struct to a file. fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { - let serialized = - serde_json::to_value(&self).map_err(|_| TrustchainCRError::FailedToSerialize)?; + let serialized = serde_json::to_value(&self)?; if let Value::Object(fields) = serialized { for (field_name, field_value) in fields { if !field_value.is_null() { let json_filename = format!("{}.json", field_name); let file_path = path.join(json_filename); - - self.save_to_file(&file_path, &to_json(&field_value).unwrap())?; + self.save_to_file(&file_path, &to_json(&field_value)?)?; } } } @@ -253,8 +263,7 @@ impl ElementwiseSerializeDeserialize for IdentityCRInitiation { self.temp_p_key = match File::open(&temp_p_key_path) { Ok(file) => { let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + let deserialized = serde_json::from_reader(reader)?; Some(deserialized) } Err(_) => None, @@ -274,8 +283,7 @@ impl ElementwiseSerializeDeserialize for IdentityCRInitiation { self.temp_s_key = match File::open(&temp_s_key_path) { Ok(file) => { let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + let deserialized = serde_json::from_reader(reader)?; Some(deserialized) } Err(_) => None, @@ -285,8 +293,7 @@ impl ElementwiseSerializeDeserialize for IdentityCRInitiation { self.requester_details = match File::open(&requester_details_path) { Ok(file) => { let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + let deserialized = serde_json::from_reader(reader)?; Some(deserialized) } Err(_) => None, @@ -349,8 +356,7 @@ impl ElementwiseSerializeDeserialize for IdentityCRChallenge { self.update_p_key = match File::open(&full_path) { Ok(file) => { let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + let deserialized = serde_json::from_reader(reader)?; Some(deserialized) } Err(_) => None, @@ -360,8 +366,7 @@ impl ElementwiseSerializeDeserialize for IdentityCRChallenge { self.update_s_key = match File::open(&full_path) { Ok(file) => { let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + let deserialized = serde_json::from_reader(reader)?; Some(deserialized) } Err(_) => None, @@ -371,8 +376,7 @@ impl ElementwiseSerializeDeserialize for IdentityCRChallenge { self.identity_nonce = match File::open(&full_path) { Ok(file) => { let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + let deserialized = serde_json::from_reader(reader)?; Some(deserialized) } Err(_) => None, @@ -382,8 +386,7 @@ impl ElementwiseSerializeDeserialize for IdentityCRChallenge { self.identity_challenge_signature = match File::open(&full_path) { Ok(file) => { let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + let deserialized = serde_json::from_reader(reader)?; Some(deserialized) } Err(_) => None, @@ -393,8 +396,7 @@ impl ElementwiseSerializeDeserialize for IdentityCRChallenge { self.identity_response_signature = match File::open(&full_path) { Ok(file) => { let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + let deserialized = serde_json::from_reader(reader)?; Some(deserialized) } Err(_) => None, @@ -419,13 +421,27 @@ impl TryFrom<&IdentityCRChallenge> for JwtPayload { payload.set_claim( "identity_nonce", Some(Value::from( - value.identity_nonce.as_ref().unwrap().to_string(), + value + .identity_nonce + .as_ref() + .ok_or(TrustchainCRError::ClaimCannotBeConstructed( + "`identity_nonce` field in `IdentityCRChallenge` is missing (`None`)" + .to_string(), + ))? + .to_string(), )), )?; payload.set_claim( "update_p_key", Some(Value::from( - value.update_p_key.as_ref().unwrap().to_string(), + value + .update_p_key + .as_ref() + .ok_or(TrustchainCRError::ClaimCannotBeConstructed( + "`update_p_key` field in `IdentityCRChallenge` is missing (`None`)" + .to_string(), + ))? + .to_string(), )), )?; Ok(payload) @@ -485,8 +501,7 @@ impl ElementwiseSerializeDeserialize for ContentCRInitiation { self.requester_did = match File::open(&requester_details_path) { Ok(file) => { let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + let deserialized = serde_json::from_reader(reader)?; Some(deserialized) } Err(_) => None, @@ -537,8 +552,7 @@ impl ElementwiseSerializeDeserialize for ContentCRChallenge { self.content_nonce = match File::open(&full_path) { Ok(file) => { let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + let deserialized = serde_json::from_reader(reader)?; Some(deserialized) } Err(_) => None, @@ -549,8 +563,7 @@ impl ElementwiseSerializeDeserialize for ContentCRChallenge { self.content_challenge_signature = match File::open(&full_path) { Ok(file) => { let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + let deserialized = serde_json::from_reader(reader)?; Some(deserialized) } Err(_) => None, @@ -560,8 +573,7 @@ impl ElementwiseSerializeDeserialize for ContentCRChallenge { self.content_response_signature = match File::open(&full_path) { Ok(file) => { let reader = std::io::BufReader::new(file); - let deserialized = serde_json::from_reader(reader) - .map_err(|_| TrustchainCRError::FailedToDeserialize)?; + let deserialized = serde_json::from_reader(reader)?; Some(deserialized) } Err(_) => None, From 20ca85ece169d26dd002c7f8ad7f640a252b1f24 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Fri, 12 Jul 2024 17:45:12 +0100 Subject: [PATCH 59/86] Removing unwraps, add error variant --- .../src/attestation_encryption_utils.rs | 52 +++++++++++-------- trustchain-http/src/attestation_utils.rs | 3 ++ 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/trustchain-http/src/attestation_encryption_utils.rs b/trustchain-http/src/attestation_encryption_utils.rs index be6bf46b..bc4a9228 100644 --- a/trustchain-http/src/attestation_encryption_utils.rs +++ b/trustchain-http/src/attestation_encryption_utils.rs @@ -22,7 +22,7 @@ pub trait SignEncrypt { fn sign(&self, payload: &JwtPayload, secret_key: &Jwk) -> Result { let mut header = JwsHeader::new(); header.set_token_type("JWT"); - let signer = ES256K.signer_from_jwk(&secret_key)?; + let signer = ES256K.signer_from_jwk(secret_key)?; let signed_jwt = jwt::encode_with_signer(payload, &header, &signer)?; Ok(signed_jwt) } @@ -35,7 +35,7 @@ pub trait SignEncrypt { header.set_content_encryption("A128CBC-HS256"); header.set_content_encryption("A256GCM"); - let encrypter = ECDH_ES.encrypter_from_jwk(&public_key)?; + let encrypter = ECDH_ES.encrypter_from_jwk(public_key)?; let encrypted_jwt = jwt::encode_with_encrypter(payload, &header, &encrypter)?; Ok(encrypted_jwt) } @@ -49,15 +49,20 @@ pub trait SignEncrypt { let signed_payload = self.sign(payload, secret_key)?; let mut claims = JwtPayload::new(); claims.set_claim("claim", Some(Value::from(signed_payload)))?; - self.encrypt(&claims, &public_key) + self.encrypt(&claims, public_key) } } /// Interface for decrypting and then verifying data. pub trait DecryptVerify { /// Decrypts a payload with a secret key. fn decrypt(&self, value: &Value, secret_key: &Jwk) -> Result { - let decrypter = ECDH_ES.decrypter_from_jwk(&secret_key)?; - let (payload, _) = jwt::decode_with_decrypter(value.as_str().unwrap(), &decrypter)?; + let decrypter = ECDH_ES.decrypter_from_jwk(secret_key)?; + let (payload, _) = jwt::decode_with_decrypter( + value + .as_str() + .ok_or(TrustchainCRError::FailedToConvertToStr(value.clone()))?, + &decrypter, + )?; Ok(payload) } /// Wrapper function that combines decrypting a payload with a secret key and then verifying it with a public key. @@ -71,8 +76,13 @@ pub trait DecryptVerify { let (payload, _) = jwt::decode_with_decrypter(input, &decrypter)?; let verifier = ES256K.verifier_from_jwk(public_key)?; + let claim = payload + .claim("claim") + .ok_or(TrustchainCRError::ClaimNotFound)?; let (payload, _) = jwt::decode_with_verifier( - &payload.claim("claim").unwrap().as_str().unwrap(), + claim + .as_str() + .ok_or(TrustchainCRError::FailedToConvertToStr(claim.clone()))?, &verifier, )?; Ok(payload) @@ -81,14 +91,14 @@ pub trait DecryptVerify { /// Converts key from josekit Jwk into ssi JWK pub fn josekit_to_ssi_jwk(key: &Jwk) -> Result { - let key_as_str: &str = &serde_json::to_string(&key).unwrap(); - let ssi_key: JWK = serde_json::from_str(key_as_str).unwrap(); + let key_as_str: &str = &serde_json::to_string(&key)?; + let ssi_key: JWK = serde_json::from_str(key_as_str)?; Ok(ssi_key) } /// Converts key from ssi JWK into josekit Jwk pub fn ssi_to_josekit_jwk(key: &JWK) -> Result { - let key_as_str: &str = &serde_json::to_string(&key).unwrap(); - let ssi_key: Jwk = serde_json::from_str(key_as_str).unwrap(); + let key_as_str: &str = &serde_json::to_string(&key)?; + let ssi_key: Jwk = serde_json::from_str(key_as_str)?; Ok(ssi_key) } @@ -111,19 +121,15 @@ pub fn extract_key_ids_and_jwk( // TODO: consider rewriting functional with filter, partition, fold over returned error // variants. for vm in vms { - match vm { - VerificationMethod::Map(vm_map) => { - let key = vm_map - .get_jwk() - .map_err(|_| TrustchainCRError::MissingJWK)?; - let id = key - .thumbprint() - .map_err(|_| TrustchainCRError::MissingJWK)?; - let key_jose = - ssi_to_josekit_jwk(&key).map_err(|err| TrustchainCRError::Serde(err))?; - my_map.insert(id, key_jose); - } - _ => (), + if let VerificationMethod::Map(vm_map) = vm { + let key = vm_map + .get_jwk() + .map_err(|_| TrustchainCRError::MissingJWK)?; + let id = key + .thumbprint() + .map_err(|_| TrustchainCRError::MissingJWK)?; + let key_jose = ssi_to_josekit_jwk(&key).map_err(TrustchainCRError::Serde)?; + my_map.insert(id, key_jose); } } } diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index f68f123c..fa1f0f58 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -54,6 +54,9 @@ pub enum TrustchainCRError { /// Failed deserialize from file. #[error("Failed to deserialize.")] FailedToDeserialize, + /// Value is not a string. + #[error("Value is not a string: {0}")] + FailedToConvertToStr(Value), /// Failed deserialize from file. #[error("Failed to deserialize with error: {0}.")] FailedToDeserializeWithError(serde_json::Error), From 5563650ce002a481c2ee4b18515873e0d54cbade Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 15 Jul 2024 10:02:34 +0100 Subject: [PATCH 60/86] Remove references where automatically dereferenced --- trustchain-http/src/attestor.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index 62198cd8..ace02edb 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -106,7 +106,7 @@ impl TrustchainAttestorHTTPHandler { app_state: Arc, ) -> impl IntoResponse { let pathbase = attestation_request_basepath("attestor").unwrap(); - let path = pathbase.join(&key_id); + let path = pathbase.join(key_id); if !path.exists() { panic!("Provided attestation request not found. Path does not exist."); } @@ -119,7 +119,7 @@ impl TrustchainAttestorHTTPHandler { let ion_attestor = IONAttestor::new(&did); let signing_keys = ion_attestor.signing_keys().unwrap(); let signing_key_ssi = signing_keys.first().unwrap(); - let signing_key = ssi_to_josekit_jwk(&signing_key_ssi).unwrap(); + let signing_key = ssi_to_josekit_jwk(signing_key_ssi).unwrap(); // get temp public key let identity_initiation = IdentityCRInitiation::new() .elementwise_deserialize(&path) @@ -268,7 +268,7 @@ impl TrustchainAttestorHTTPHandler { ) -> impl IntoResponse { // deserialise expected nonce map let pathbase = attestation_request_basepath("attestor").unwrap(); - let path = pathbase.join(&key_id); + let path = pathbase.join(key_id); let identity_cr_initiation = IdentityCRInitiation::new() .elementwise_deserialize(&path) .unwrap() @@ -283,7 +283,7 @@ impl TrustchainAttestorHTTPHandler { let ion_attestor = IONAttestor::new(&did); let signing_keys = ion_attestor.signing_keys().unwrap(); let signing_key_ssi = signing_keys.first().unwrap(); - let signing_key = ssi_to_josekit_jwk(&signing_key_ssi).unwrap(); + let signing_key = ssi_to_josekit_jwk(signing_key_ssi).unwrap(); // decrypt and verify response => nonces map let attestor = Entity {}; @@ -351,12 +351,12 @@ pub fn present_identity_challenge( let signing_keys = ion_attestor.signing_keys().unwrap(); let signing_key_ssi = signing_keys.first().unwrap(); let signing_key = - ssi_to_josekit_jwk(&signing_key_ssi).map_err(|_| TrustchainCRError::FailedToGenerateKey)?; + ssi_to_josekit_jwk(signing_key_ssi).map_err(|_| TrustchainCRError::FailedToGenerateKey)?; // sign (with pub key) and encrypt (with temp_p_key) payload let attestor = Entity {}; let signed_encrypted_challenge = - attestor.sign_and_encrypt_claim(&payload, &signing_key, &temp_p_key); + attestor.sign_and_encrypt_claim(&payload, &signing_key, temp_p_key); identity_challenge.identity_challenge_signature = Some(signed_encrypted_challenge?); Ok(identity_challenge) @@ -372,7 +372,7 @@ fn verify_nonce(payload: JwtPayload, path: &PathBuf) -> Result<(), TrustchainCRE let nonce = payload.claim("identity_nonce").unwrap().as_str().unwrap(); // deserialise expected nonce let identity_challenge = IdentityCRChallenge::new() - .elementwise_deserialize(&path) + .elementwise_deserialize(path) .unwrap() .unwrap(); let expected_nonce = identity_challenge.identity_nonce.unwrap().to_string(); From 071b01f25a4f7c2e2e5bfc3103e49f4860485dbd Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 15 Jul 2024 10:04:39 +0100 Subject: [PATCH 61/86] Fix response return for post_identity_response --- trustchain-http/src/attestor.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index ace02edb..ea301cf9 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -136,18 +136,18 @@ impl TrustchainAttestorHTTPHandler { Ok(_) => { identity_challenge.identity_response_signature = Some(response.clone()); identity_challenge.elementwise_serialize(&path).unwrap(); - let respone = CustomResponse { + let response = CustomResponse { message: "Verification successful. Please use the provided path to initiate the second part of the attestation process.".to_string(), data: None }; - (StatusCode::OK, respone); + (StatusCode::OK, Json(response)) } Err(_) => { let response = CustomResponse { message: "Verification failed. Please try again.".to_string(), data: None, }; - (StatusCode::BAD_REQUEST, response); + (StatusCode::BAD_REQUEST, Json(response)) } } } From 642aee9a9020b4a760b5cbbf623c7bb67a23a2b5 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 15 Jul 2024 10:06:08 +0100 Subject: [PATCH 62/86] Remove references that are automatically dereferenced --- trustchain-http/src/attestor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index ea301cf9..44e78c9f 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -206,7 +206,7 @@ impl TrustchainAttestorHTTPHandler { attestor .encrypt( &JwtPayload::try_from(nonce).unwrap(), - &requester_keys.get(key_id).unwrap(), + requester_keys.get(key_id).unwrap(), ) .unwrap(), ); @@ -220,7 +220,7 @@ impl TrustchainAttestorHTTPHandler { let ion_attestor = IONAttestor::new(&did); let signing_keys = ion_attestor.signing_keys().unwrap(); let signing_key_ssi = signing_keys.first().unwrap(); - let signing_key = ssi_to_josekit_jwk(&signing_key_ssi).unwrap(); + let signing_key = ssi_to_josekit_jwk(signing_key_ssi).unwrap(); // sign and encrypt challenges let value: serde_json::Value = serde_json::to_value(challenges).unwrap(); From 6177e01da91c172719f18f8b75842d449a69031c Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 15 Jul 2024 15:53:07 +0100 Subject: [PATCH 63/86] Add TrustchainCRError to TrustchainHTTPError, replace unwraps --- trustchain-http/src/attestor.rs | 75 +++++++++++++++++++++------------ trustchain-http/src/errors.rs | 13 ++++++ 2 files changed, 60 insertions(+), 28 deletions(-) diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index 44e78c9f..b0252aa3 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -7,6 +7,7 @@ use crate::attestation_utils::{ ContentCRInitiation, CustomResponse, ElementwiseSerializeDeserialize, IdentityCRChallenge, IdentityCRInitiation, Nonce, TrustchainCRError, }; +use crate::errors::TrustchainHTTPError; use crate::state::AppState; use async_trait::async_trait; use axum::extract::Path; @@ -18,6 +19,8 @@ use log::info; use trustchain_api::api::TrustchainDIDAPI; use trustchain_api::TrustchainAPI; +use trustchain_core::attestor::AttestorError; +use trustchain_core::key_manager::KeyManagerError; use trustchain_core::verifier::Verifier; use std::collections::HashMap; @@ -163,20 +166,28 @@ impl TrustchainAttestorHTTPHandler { pub async fn post_content_initiation( (Path(key_id), Json(ddid)): (Path, Json), app_state: Arc, - ) -> impl IntoResponse { - let pathbase = attestation_request_basepath("attestor").unwrap(); + ) -> Result<(StatusCode, String), TrustchainHTTPError> { + let pathbase = attestation_request_basepath("attestor")?; let path = pathbase.join(&key_id); - let did = app_state.config.server_did.as_ref().unwrap().to_owned(); + let did = app_state + .config + .server_did + .as_ref() + .expect("Server DID must be set for challenge-response content initiation.") + .to_owned(); // resolve candidate DID let result = TrustchainAPI::resolve(&ddid, app_state.verifier.resolver()).await; let candidate_doc = match result { Ok((_, doc, _)) => doc.unwrap(), Err(_) => { - let respone = CustomResponse { + let response = CustomResponse { message: "Resolution of candidate DID failed.".to_string(), data: None, }; - return (StatusCode::BAD_REQUEST, Json(respone)); + return Ok(( + StatusCode::BAD_REQUEST, + serde_json::to_string(&response).map_err(TrustchainCRError::Serde)?, + )); } }; // TODO: check if resolved candidate DID contains expected update_p_key @@ -185,9 +196,11 @@ impl TrustchainAttestorHTTPHandler { let content_initiation = ContentCRInitiation { requester_did: Some(ddid), }; - content_initiation.elementwise_serialize(&path); + content_initiation + .elementwise_serialize(&path) + .map_err(TrustchainHTTPError::CRError)?; // extract map of keys from candidate document and generate a nonce per key - let requester_keys = extract_key_ids_and_jwk(&candidate_doc).unwrap(); + let requester_keys = extract_key_ids_and_jwk(&candidate_doc)?; let attestor = Entity {}; let nonces: HashMap = requester_keys @@ -198,32 +211,35 @@ impl TrustchainAttestorHTTPHandler { }); // sign and encrypt nonces to generate challenges - let challenges = nonces - .iter() - .fold(HashMap::new(), |mut acc, (key_id, nonce)| { - acc.insert( - String::from(key_id), - attestor - .encrypt( - &JwtPayload::try_from(nonce).unwrap(), - requester_keys.get(key_id).unwrap(), - ) - .unwrap(), - ); - acc - }); + let mut challenges = HashMap::new(); + for (key_id, nonce) in nonces.iter() { + challenges.insert( + String::from(key_id), + attestor.encrypt( + &JwtPayload::try_from(nonce)?, + requester_keys + .get(key_id) + .ok_or(TrustchainCRError::KeyNotFound)?, + )?, + ); + } // get public and secret keys let identity_cr_initiation = IdentityCRInitiation::new() - .elementwise_deserialize(&path) - .unwrap() + .elementwise_deserialize(&path)? .unwrap(); let ion_attestor = IONAttestor::new(&did); - let signing_keys = ion_attestor.signing_keys().unwrap(); - let signing_key_ssi = signing_keys.first().unwrap(); + let signing_keys = ion_attestor.signing_keys()?; + let signing_key_ssi = signing_keys + .first() + .ok_or(AttestorError::NoSigningKey(format!( + "No signing keys for ION attestor with DID: {did}" + ))) + .unwrap(); let signing_key = ssi_to_josekit_jwk(signing_key_ssi).unwrap(); // sign and encrypt challenges - let value: serde_json::Value = serde_json::to_value(challenges).unwrap(); + let value: serde_json::Value = + serde_json::to_value(challenges).map_err(TrustchainCRError::Serde)?; let mut payload = JwtPayload::new(); payload.set_claim("challenges", Some(value)).unwrap(); let signed_encrypted_challenges = attestor.sign_and_encrypt_claim( @@ -244,14 +260,17 @@ impl TrustchainAttestorHTTPHandler { message: "Challenges generated successfully.".to_string(), data: Some(signed_encrypted_challenges), }; - (StatusCode::OK, Json(response)) + Ok((StatusCode::OK, serde_json::to_string(&response).unwrap())) } Err(_) => { let response = CustomResponse { message: "Failed to generate challenges.".to_string(), data: None, }; - (StatusCode::BAD_REQUEST, Json(response)) + Ok(( + StatusCode::BAD_REQUEST, + serde_json::to_string(&response).unwrap(), + )) } } } diff --git a/trustchain-http/src/errors.rs b/trustchain-http/src/errors.rs index 0d36e816..d5505900 100644 --- a/trustchain-http/src/errors.rs +++ b/trustchain-http/src/errors.rs @@ -8,6 +8,8 @@ use trustchain_core::{ }; use trustchain_ion::root::TrustchainRootError; +use crate::attestation_utils::TrustchainCRError; + // TODO: refine and add doc comments for error variants #[derive(Error, Debug)] pub enum TrustchainHTTPError { @@ -30,6 +32,8 @@ pub enum TrustchainHTTPError { // JoseError(JoseError), #[error("Trustchain key manager error: {0}")] KeyManagerError(KeyManagerError), + #[error("Trustchain challenge-response error: {0}")] + CRError(TrustchainCRError), #[error("Credential does not exist.")] CredentialDoesNotExist, #[error("No issuer available.")] @@ -92,6 +96,12 @@ impl From for TrustchainHTTPError { } } +impl From for TrustchainHTTPError { + fn from(err: TrustchainCRError) -> Self { + TrustchainHTTPError::CRError(err) + } +} + // See axum IntoRespone example: // https://github.com/tokio-rs/axum/blob/main/examples/jwt/src/main.rs#L147-L160 @@ -130,6 +140,9 @@ impl IntoResponse for TrustchainHTTPError { err @ TrustchainHTTPError::KeyManagerError(_) => { (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) } + err @ TrustchainHTTPError::CRError(_) => { + (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) + } err @ TrustchainHTTPError::CredentialDoesNotExist => { (StatusCode::BAD_REQUEST, err.to_string()) } From 855bc3f8d7174a132ef73c14a8040fe98f7e18e2 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 15 Jul 2024 16:34:40 +0100 Subject: [PATCH 64/86] Add venv to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index ac6a2265..6bb6b50c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ outputs/ # dist paths **/dist + +# Python venv +.venv From c4b705619f9df17f871785776b669d6fd4ebf786 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 15 Jul 2024 16:35:41 +0100 Subject: [PATCH 65/86] Replace map_err with error propagation Co-authored-by: pwochner --- trustchain-http/src/attestation_utils.rs | 28 ++++++++++++++++-------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index fa1f0f58..87260a76 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -60,6 +60,8 @@ pub enum TrustchainCRError { /// Failed deserialize from file. #[error("Failed to deserialize with error: {0}.")] FailedToDeserializeWithError(serde_json::Error), + #[error("Wrapped SSI JWK error: {0}.")] + WrappedSSIJWKError(ssi::jwk::Error), /// Failed to check CR status. #[error("Failed to determine CR status.")] FailedStatusCheck, @@ -167,6 +169,12 @@ impl From for TrustchainCRError { } } +impl From for TrustchainCRError { + fn from(value: ssi::jwk::Error) -> Self { + TrustchainCRError::WrappedSSIJWKError(value) + } +} + /// Interface for serializing and deserializing each field of structs to/from files. pub trait ElementwiseSerializeDeserialize where @@ -198,7 +206,12 @@ where } // Open the new file if it doesn't exist yet - let new_file = OpenOptions::new().create(true).write(true).open(path); + let new_file = OpenOptions::new() + .create(true) + .append(false) + .truncate(false) + .write(true) + .open(path); // Write key to file match new_file { @@ -801,25 +814,22 @@ pub fn matching_endpoint( if endpoints.len() != 1 { return Err(TrustchainCRError::InvalidServiceEndpoint); } - return Ok(endpoints[0].clone()); + Ok(endpoints[0].clone()) } /// Returns unique path name for a specific attestation request derived from public key for the interaction. pub fn attestation_request_path(key: &JWK, prefix: &str) -> Result { // Root path in TRUSTCHAIN_DATA - let path = attestation_request_basepath(prefix) - .map_err(|_| TrustchainCRError::FailedAttestationRequest)?; - let key_id = key - .thumbprint() - .map_err(|_| TrustchainCRError::MissingJWK)?; // Use hash of temp_pub_key + let path = attestation_request_basepath(prefix)?; + let key_id = key.thumbprint()?; // Use hash of temp_pub_key Ok(path.join(key_id)) } /// Returns the root path for storing attestation requests. pub fn attestation_request_basepath(prefix: &str) -> Result { // Root path in TRUSTCHAIN_DATA - let path: String = - std::env::var(TRUSTCHAIN_DATA).map_err(|_| TrustchainCRError::FailedAttestationRequest)?; + let path: String = std::env::var(TRUSTCHAIN_DATA) + .expect("`TRUSTCHAIN_DATA` environment variable must be set."); Ok(Path::new(path.as_str()) .join(prefix) .join("attestation_requests")) From 49ab67492d1484a08bb9405294aa9a5f41f00322 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 15 Jul 2024 17:17:49 +0100 Subject: [PATCH 66/86] Replace unwraps and add comments where unwrap safe Co-authored-by: pwochner --- trustchain-http/src/attestation_utils.rs | 73 +++++++++++++----------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index 87260a76..614f644d 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -474,15 +474,20 @@ impl TryFrom<&JwtPayload> for IdentityCRChallenge { identity_challenge_signature: None, identity_response_signature: None, }; - challenge.update_p_key = Some( - serde_json::from_str(value.claim("update_p_key").unwrap().as_str().unwrap()).unwrap(), - ); + let claim = value + .claim("update_p_key") + .ok_or(TrustchainCRError::ClaimNotFound)?; + challenge.update_p_key = + Some(serde_json::from_str(claim.as_str().ok_or( + TrustchainCRError::FailedToConvertToStr(claim.clone()), + )?)?); + let claim = value + .claim("identity_nonce") + .ok_or(TrustchainCRError::ClaimNotFound)?; challenge.identity_nonce = Some(Nonce::from( - value - .claim("identity_nonce") - .unwrap() + claim .as_str() - .unwrap() + .ok_or(TrustchainCRError::FailedToConvertToStr(claim.clone()))? .to_string(), )); Ok(challenge) @@ -628,26 +633,18 @@ impl CRState { } /// Returns true if all fields are complete. pub fn is_complete(&self) -> bool { - if (self.identity_cr_initiation.is_some() - && self.identity_cr_initiation.as_ref().unwrap().is_complete()) - && (self.identity_challenge_response.is_some() - && self - .identity_challenge_response - .as_ref() - .unwrap() - .is_complete()) - && (self.content_cr_initiation.is_some() - && self.content_cr_initiation.as_ref().unwrap().is_complete()) - && (self.content_challenge_response.is_some() - && self - .content_challenge_response - .as_ref() - .unwrap() - .is_complete()) - { - return true; + if let (Some(ici), Some(icr), Some(cci), Some(ccr)) = ( + self.identity_cr_initiation.as_ref(), + self.identity_challenge_response.as_ref(), + self.content_cr_initiation.as_ref(), + self.content_challenge_response.as_ref(), + ) { + return ici.is_complete() + && icr.is_complete() + && cci.is_complete() + && ccr.is_complete(); } - return false; + false } /// Determines current status of the challenge response process and accordingly prints messages to the console. pub fn check_cr_status(&self) -> Result { @@ -668,6 +665,7 @@ impl CRState { // Identity CR initation if self.identity_cr_initiation.is_none() + // Unwrap: first condition ensures is not None || !self.identity_cr_initiation.as_ref().unwrap().is_complete() { println!("{}", get_status_message(¤t_state)); @@ -678,6 +676,7 @@ impl CRState { // Identity challenge if self.identity_challenge_response.is_none() + // Unwrap: first condition ensures is not None || !self .identity_challenge_response .as_ref() @@ -690,7 +689,11 @@ impl CRState { println!("{}", get_status_message(¤t_state)); // Identity response - if !self + if self + .identity_challenge_response + .is_none() + // Unwrap: first condition ensures is not None + || !self .identity_challenge_response .as_ref() .unwrap() @@ -702,6 +705,7 @@ impl CRState { // Content CR initation if self.content_cr_initiation.is_none() + // Unwrap: first condition ensures is not None || !self.content_cr_initiation.as_ref().unwrap().is_complete() { return Ok(current_state); @@ -710,6 +714,7 @@ impl CRState { // Content challenge if self.content_challenge_response.is_none() + // Unwrap: first condition ensures is not None || !self .content_challenge_response .as_ref() @@ -721,16 +726,18 @@ impl CRState { current_state = CurrentCRState::ContentChallengeComplete; // Content response - if !self - .content_challenge_response - .as_ref() - .unwrap() - .is_complete() + if self.content_challenge_response.is_none() + // Unwrap: first condition ensures is not None + || !self + .content_challenge_response + .as_ref() + .unwrap() + .is_complete() { return Ok(current_state); } - return Ok(current_state); + Ok(current_state) } } From 46ac3e50e28a26849deb53a92ca2c519a24173f1 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 15 Jul 2024 17:32:00 +0100 Subject: [PATCH 67/86] Clippy Co-authored-by: pwochner --- trustchain-api/src/api.rs | 2 +- trustchain-cli/src/bin/main.rs | 4 +- trustchain-core/src/graph.rs | 2 +- trustchain-http/src/attestation_utils.rs | 88 +++++++++++++++++------- trustchain-http/src/attestor.rs | 1 - trustchain-http/src/requester.rs | 27 ++++---- trustchain-http/tests/attestation.rs | 20 +++--- trustchain-ion/src/commitment.rs | 2 +- 8 files changed, 88 insertions(+), 58 deletions(-) diff --git a/trustchain-api/src/api.rs b/trustchain-api/src/api.rs index 31e87b9d..55362152 100644 --- a/trustchain-api/src/api.rs +++ b/trustchain-api/src/api.rs @@ -331,7 +331,7 @@ mod tests { assert!(res.is_ok()); // Change credential to make signature invalid - vc_with_proof.expiration_date = Some(VCDateTime::try_from(now_ns()).unwrap()); + vc_with_proof.expiration_date = Some(VCDateTime::from(now_ns())); // Verify: expect no warnings and a signature error as VC has changed let resolver = trustchain_resolver("http://localhost:3000/"); diff --git a/trustchain-cli/src/bin/main.rs b/trustchain-cli/src/bin/main.rs index f5086b2f..7ac301f2 100644 --- a/trustchain-cli/src/bin/main.rs +++ b/trustchain-cli/src/bin/main.rs @@ -436,7 +436,7 @@ async fn main() -> Result<(), Box> { let temp_p_key = identity_initiation.unwrap().temp_p_key.unwrap(); // call function to present challenge - let identity_challenge = present_identity_challenge(&did, &temp_p_key)?; + let identity_challenge = present_identity_challenge(did, &temp_p_key)?; // print signed and encrypted payload to terminal let payload = identity_challenge @@ -510,7 +510,7 @@ async fn main() -> Result<(), Box> { let services = &doc.service.unwrap(); let (content_initiation, content_challenge) = - initiate_content_challenge(&path, ddid, &services, &attestor_public_key) + initiate_content_challenge(&path, ddid, services, &attestor_public_key) .await?; content_initiation.elementwise_serialize(&path)?; content_challenge.elementwise_serialize(&path)?; diff --git a/trustchain-core/src/graph.rs b/trustchain-core/src/graph.rs index 229753d4..cc3912eb 100644 --- a/trustchain-core/src/graph.rs +++ b/trustchain-core/src/graph.rs @@ -62,7 +62,7 @@ fn read_chains(chains: &Vec, label_width: usize) -> DiGraph Self { + Self::new() + } +} + impl Nonce { pub fn new() -> Self { Self( @@ -182,7 +188,7 @@ where { /// Serialize each field of the struct to a file. fn elementwise_serialize(&self, path: &PathBuf) -> Result<(), TrustchainCRError> { - let serialized = serde_json::to_value(&self)?; + let serialized = serde_json::to_value(self)?; if let Value::Object(fields) = serialized { for (field_name, field_value) in fields { if !field_value.is_null() { @@ -254,6 +260,12 @@ pub struct IdentityCRInitiation { pub requester_details: Option, } +impl Default for IdentityCRInitiation { + fn default() -> Self { + Self::new() + } +} + impl IdentityCRInitiation { pub fn new() -> Self { Self { @@ -265,7 +277,7 @@ impl IdentityCRInitiation { /// Returns true if all fields required for the initiation have a non-null value. /// Note: temp_s_key is optional since only requester has it. pub fn is_complete(&self) -> bool { - return self.temp_p_key.is_some() && self.requester_details.is_some(); + self.temp_p_key.is_some() && self.requester_details.is_some() } } @@ -276,7 +288,7 @@ impl ElementwiseSerializeDeserialize for IdentityCRInitiation { path: &PathBuf, ) -> Result, TrustchainCRError> { let temp_p_key_path = path.join("temp_p_key.json"); - self.temp_p_key = match File::open(&temp_p_key_path) { + self.temp_p_key = match File::open(temp_p_key_path) { Ok(file) => { let reader = std::io::BufReader::new(file); let deserialized = serde_json::from_reader(reader)?; @@ -296,7 +308,7 @@ impl ElementwiseSerializeDeserialize for IdentityCRInitiation { // self.temp_p_key = Some(deserialized); let temp_s_key_path = path.join("temp_s_key.json"); - self.temp_s_key = match File::open(&temp_s_key_path) { + self.temp_s_key = match File::open(temp_s_key_path) { Ok(file) => { let reader = std::io::BufReader::new(file); let deserialized = serde_json::from_reader(reader)?; @@ -306,7 +318,7 @@ impl ElementwiseSerializeDeserialize for IdentityCRInitiation { }; let requester_details_path = path.join("requester_details.json"); - self.requester_details = match File::open(&requester_details_path) { + self.requester_details = match File::open(requester_details_path) { Ok(file) => { let reader = std::io::BufReader::new(file); let deserialized = serde_json::from_reader(reader)?; @@ -338,6 +350,12 @@ pub struct IdentityCRChallenge { pub identity_response_signature: Option, } +impl Default for IdentityCRChallenge { + fn default() -> Self { + Self::new() + } +} + impl IdentityCRChallenge { pub fn new() -> Self { Self { @@ -351,13 +369,13 @@ impl IdentityCRChallenge { /// Returns true if all fields required for the challenge have a non-null value. /// Note: update_s_key is optional since only attestor has it. fn challenge_complete(&self) -> bool { - return self.update_p_key.is_some() + self.update_p_key.is_some() && self.identity_nonce.is_some() - && self.identity_challenge_signature.is_some(); + && self.identity_challenge_signature.is_some() } /// Returns true if challenge-response is complete. fn is_complete(&self) -> bool { - return self.challenge_complete() && self.identity_response_signature.is_some(); + self.challenge_complete() && self.identity_response_signature.is_some() } } @@ -369,7 +387,7 @@ impl ElementwiseSerializeDeserialize for IdentityCRChallenge { ) -> Result, TrustchainCRError> { // update public key let full_path = path.join("update_p_key.json"); - self.update_p_key = match File::open(&full_path) { + self.update_p_key = match File::open(full_path) { Ok(file) => { let reader = std::io::BufReader::new(file); let deserialized = serde_json::from_reader(reader)?; @@ -500,6 +518,12 @@ pub struct ContentCRInitiation { pub requester_did: Option, } +impl Default for ContentCRInitiation { + fn default() -> Self { + Self::new() + } +} + impl ContentCRInitiation { pub fn new() -> Self { Self { @@ -508,7 +532,7 @@ impl ContentCRInitiation { } fn is_complete(&self) -> bool { - return self.requester_did.is_some(); + self.requester_did.is_some() } } @@ -519,7 +543,7 @@ impl ElementwiseSerializeDeserialize for ContentCRInitiation { path: &PathBuf, ) -> Result, TrustchainCRError> { let requester_details_path = path.join("requester_did.json"); - self.requester_did = match File::open(&requester_details_path) { + self.requester_did = match File::open(requester_details_path) { Ok(file) => { let reader = std::io::BufReader::new(file); let deserialized = serde_json::from_reader(reader)?; @@ -544,6 +568,12 @@ pub struct ContentCRChallenge { pub content_response_signature: Option, } +impl Default for ContentCRChallenge { + fn default() -> Self { + Self::new() + } +} + impl ContentCRChallenge { pub fn new() -> Self { Self { @@ -554,11 +584,11 @@ impl ContentCRChallenge { } /// Returns true if all fields required for the challenge have a non-null value. fn challenge_complete(&self) -> bool { - return self.content_nonce.is_some() && self.content_challenge_signature.is_some(); + self.content_nonce.is_some() && self.content_challenge_signature.is_some() } /// Returns true if all fields required for the challenge-response have a non-null value. fn is_complete(&self) -> bool { - return self.challenge_complete() && self.content_response_signature.is_some(); + self.challenge_complete() && self.content_response_signature.is_some() } } @@ -622,6 +652,12 @@ pub struct CRState { pub content_challenge_response: Option, } +impl Default for CRState { + fn default() -> Self { + Self::new() + } +} + impl CRState { pub fn new() -> Self { Self { @@ -777,25 +813,25 @@ impl ElementwiseSerializeDeserialize for CRState { fn get_status_message(current_state: &CurrentCRState) -> String { match current_state { CurrentCRState::NotStarted => { - return String::from("No records found for this challenge-response identifier or entity. \nThe challenge-response process has not been initiated yet."); + String::from("No records found for this challenge-response identifier or entity. \nThe challenge-response process has not been initiated yet.") } CurrentCRState::IdentityCRInitiated => { - return String::from("Identity challenge-response initiated. Await response."); + String::from("Identity challenge-response initiated. Await response.") } CurrentCRState::IdentityChallengeComplete => { - return String::from("Identity challenge has been presented. Await response."); + String::from("Identity challenge has been presented. Await response.") } CurrentCRState::IdentityResponseComplete => { - return String::from("Identity challenge-response complete."); + String::from("Identity challenge-response complete.") } CurrentCRState::ContentCRInitiated => { - return String::from("Content challenge-response initiated. Await response."); + String::from("Content challenge-response initiated. Await response.") } CurrentCRState::ContentChallengeComplete => { - return String::from("Content challenge has been presented. Await response."); + String::from("Content challenge has been presented. Await response.") } CurrentCRState::ContentResponseComplete => { - return String::from("Challenge-response complete."); + String::from("Challenge-response complete.") } } } @@ -910,11 +946,11 @@ mod tests { // write to file let path = tempdir().unwrap().into_path(); let result = cr_state.elementwise_serialize(&path); - assert_eq!(result.is_ok(), true); + assert!(result.is_ok()); // try to write to file again let result = cr_state.elementwise_serialize(&path); - assert_eq!(result.is_ok(), true); + assert!(result.is_ok()); } #[test] @@ -945,7 +981,7 @@ mod tests { // Test case 3: Both json files exist and can be deserialized let cr_initiation = IdentityCRInitiation::new(); let requester_details_path = temp_path.join("requester_details.json"); - let requester_details_file = File::create(&requester_details_path).unwrap(); + let requester_details_file = File::create(requester_details_path).unwrap(); let requester_details = RequesterDetails { requester_org: String::from("My Org"), operator_name: String::from("John Doe"), @@ -980,7 +1016,7 @@ mod tests { // Test case 2: Only one json file exists and can be deserialized let update_p_key_path = temp_path.join("update_p_key.json"); - let update_p_key_file = File::create(&update_p_key_path).unwrap(); + let update_p_key_file = File::create(update_p_key_path).unwrap(); let update_p_key: Jwk = serde_json::from_str(TEST_UPDATE_KEY).unwrap(); serde_json::to_writer(update_p_key_file, &update_p_key).unwrap(); let identity_challenge = IdentityCRChallenge::new(); @@ -994,7 +1030,7 @@ mod tests { // Test case 3: One file exists but cannot be deserialized let identity_nonce_path = temp_path.join("identity_nonce.json"); - let identity_nonce_file = File::create(&identity_nonce_path).unwrap(); + let identity_nonce_file = File::create(identity_nonce_path).unwrap(); serde_json::to_writer(identity_nonce_file, &42).unwrap(); let identity_challenge = IdentityCRChallenge::new(); let result = identity_challenge.elementwise_deserialize(&temp_path); @@ -1078,7 +1114,7 @@ mod tests { // Test case 2: one file cannot be deserialized let identity_nonce_path = path.join("content_nonce.json"); - let identity_nonce_file = File::create(&identity_nonce_path).unwrap(); + let identity_nonce_file = File::create(identity_nonce_path).unwrap(); serde_json::to_writer(identity_nonce_file, &42).unwrap(); let challenge_state = CRState::new().elementwise_deserialize(&path); assert!(challenge_state.is_err()); diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index b0252aa3..545d635f 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -20,7 +20,6 @@ use log::info; use trustchain_api::api::TrustchainDIDAPI; use trustchain_api::TrustchainAPI; use trustchain_core::attestor::AttestorError; -use trustchain_core::key_manager::KeyManagerError; use trustchain_core::verifier::Verifier; use std::collections::HashMap; diff --git a/trustchain-http/src/requester.rs b/trustchain-http/src/requester.rs index ea581995..5a6c1178 100644 --- a/trustchain-http/src/requester.rs +++ b/trustchain-http/src/requester.rs @@ -60,7 +60,7 @@ pub async fn initiate_identity_challenge( .json(&identity_cr_initiation) .send() .await - .map_err(|err| TrustchainCRError::Reqwest(err))?; + .map_err(TrustchainCRError::Reqwest)?; if result.status() != 200 { return Err(TrustchainCRError::FailedToInitiateCR); @@ -103,12 +103,12 @@ pub async fn identity_response( .clone() .unwrap(), &temp_s_key, - &attestor_p_key, + attestor_p_key, ) .unwrap(); // sign and encrypt response let signed_encrypted_response = requester - .sign_and_encrypt_claim(&decrypted_verified_payload, &temp_s_key, &attestor_p_key) + .sign_and_encrypt_claim(&decrypted_verified_payload, &temp_s_key, attestor_p_key) .unwrap(); let key_id = temp_s_key_ssi.to_public().thumbprint().unwrap(); // get uri for POST request response @@ -122,7 +122,7 @@ pub async fn identity_response( .json(&signed_encrypted_response) .send() .await - .map_err(|err| TrustchainCRError::Reqwest(err))?; + .map_err(TrustchainCRError::Reqwest)?; if result.status() != 200 { return Err(TrustchainCRError::FailedToRespond(result)); } @@ -157,7 +157,7 @@ pub async fn initiate_content_challenge( ) -> Result<(ContentCRInitiation, ContentCRChallenge), TrustchainCRError> { // deserialise identity_cr_initiation and get key id let identity_cr_initiation = IdentityCRInitiation::new() - .elementwise_deserialize(&path) + .elementwise_deserialize(path) .unwrap() .unwrap(); let temp_s_key_ssi = josekit_to_ssi_jwk(&identity_cr_initiation.temp_s_key.unwrap()).unwrap(); @@ -177,21 +177,18 @@ pub async fn initiate_content_challenge( .json(&ddid) .send() .await - .map_err(|err| TrustchainCRError::Reqwest(err))?; + .map_err(TrustchainCRError::Reqwest)?; if result.status() != 200 { println!("Status code: {}", result.status()); return Err(TrustchainCRError::FailedToRespond(result)); } - let response_body: CustomResponse = result - .json() - .await - .map_err(|err| TrustchainCRError::Reqwest(err))?; + let response_body: CustomResponse = result.json().await.map_err(TrustchainCRError::Reqwest)?; let signed_encrypted_challenge = response_body.data.unwrap(); // response let (nonces, response) = content_response( - &path, + path, &signed_encrypted_challenge.to_string(), services, attestor_p_key.clone(), @@ -223,7 +220,7 @@ pub async fn content_response( ddid: &String, ) -> Result<(HashMap, String), TrustchainCRError> { // get keys - let identity_initiation = IdentityCRInitiation::new().elementwise_deserialize(&path); + let identity_initiation = IdentityCRInitiation::new().elementwise_deserialize(path); let temp_s_key = identity_initiation.unwrap().unwrap().temp_s_key.unwrap(); let temp_s_key_ssi = josekit_to_ssi_jwk(&temp_s_key).unwrap(); // get endpoint @@ -247,7 +244,7 @@ pub async fn content_response( .unwrap(); // keymap with requester secret keys - let ion_attestor = IONAttestor::new(&ddid); + let ion_attestor = IONAttestor::new(ddid); let signing_keys = ion_attestor.signing_keys().unwrap(); // iterate over all keys, convert to Jwk (josekit) -> TODO: functional // let mut signing_keys_map: HashMap = HashMap::new(); @@ -275,7 +272,7 @@ pub async fn content_response( Nonce::from( requester .decrypt( - &Some(Value::from(nonce.clone())).unwrap(), + &Value::from(nonce.clone()), signing_keys_map.get(key_id).unwrap(), ) .unwrap() @@ -303,7 +300,7 @@ pub async fn content_response( .json(&signed_encrypted_response) .send() .await - .map_err(|err| TrustchainCRError::Reqwest(err))?; + .map_err(TrustchainCRError::Reqwest)?; if result.status() != 200 { println!("Status code: {}", result.status()); return Err(TrustchainCRError::FailedToRespond(result)); diff --git a/trustchain-http/tests/attestation.rs b/trustchain-http/tests/attestation.rs index 7021052f..c671a23c 100644 --- a/trustchain-http/tests/attestation.rs +++ b/trustchain-http/tests/attestation.rs @@ -17,9 +17,7 @@ use trustchain_ion::{trustchain_resolver, verifier::TrustchainVerifier}; const ROOT_EVENT_TIME_1: u64 = 1666265405; use mockall::automock; -use std::fs; -use std::path::PathBuf; -use trustchain_core::utils::{extract_keys, init}; +use trustchain_core::utils::extract_keys; #[automock] pub trait AttestationUtils { @@ -60,7 +58,7 @@ async fn attestation_challenge_response() { let expected_operator_name = String::from("Some Operator"); let result = - initiate_identity_challenge(&expected_org_name, &expected_operator_name, &services).await; + initiate_identity_challenge(&expected_org_name, &expected_operator_name, services).await; // Make sure initiation was successful and information is complete before serializing. assert!(result.is_ok()); let (identity_initiation_requester, requester_path) = result.unwrap(); @@ -82,7 +80,7 @@ async fn attestation_challenge_response() { .unwrap() .unwrap(); // Make sure that attestor has all required information about initiation (but not secret key). - assert_eq!(identity_initiation_attestor.is_complete(), true); + assert!(identity_initiation_attestor.is_complete()); assert!(identity_initiation_attestor.temp_s_key.is_none()); let org_name = identity_initiation_attestor .requester_details @@ -99,7 +97,7 @@ async fn attestation_challenge_response() { // If data matches, proceed with presenting signed and encrypted identity challenge payload. let temp_p_key = identity_initiation_attestor.clone().temp_p_key.unwrap(); - let result = present_identity_challenge(&attestor_did, &temp_p_key); + let result = present_identity_challenge(attestor_did, &temp_p_key); assert!(result.is_ok()); let identity_challenge_attestor = result.unwrap(); let _ = identity_challenge_attestor.elementwise_serialize(&attestor_path); @@ -125,12 +123,12 @@ async fn attestation_challenge_response() { // Upon receiving the request, the attestor decrypts the response and verifies the signature, // before comparing the nonce from the response with the nonce from the challenge. - let public_keys = extract_keys(&attestor_doc); + let public_keys = extract_keys(attestor_doc); let attestor_public_key_ssi = public_keys.first().unwrap(); let attestor_public_key = ssi_to_josekit_jwk(attestor_public_key_ssi).unwrap(); // Check nonce component is captured with the response being Ok - let result = identity_response(&requester_path, &services, &attestor_public_key).await; + let result = identity_response(&requester_path, services, &attestor_public_key).await; assert!(result.is_ok()); let identity_challenge_requester = result.unwrap(); identity_challenge_requester @@ -159,7 +157,7 @@ async fn attestation_challenge_response() { let result = initiate_content_challenge( &requester_path, requester_did, - &services, + services, &attestor_public_key, ) .await; @@ -179,7 +177,7 @@ async fn attestation_challenge_response() { .unwrap() .unwrap(); let result = cr_state_requester.is_complete(); - assert_eq!(result, true); + assert!(result); // Check that requester has temp_s_key but not update_s_key. assert!(cr_state_requester @@ -200,7 +198,7 @@ async fn attestation_challenge_response() { .unwrap() .unwrap(); let result = cr_state_attestor.is_complete(); - assert_eq!(result, true); + assert!(result); // Check that attestor does not have temp_s_key but update_s_key. assert!(cr_state_attestor .identity_cr_initiation diff --git a/trustchain-ion/src/commitment.rs b/trustchain-ion/src/commitment.rs index bbc2303f..b63a7548 100644 --- a/trustchain-ion/src/commitment.rs +++ b/trustchain-ion/src/commitment.rs @@ -931,7 +931,7 @@ mod tests { // The first one commits to the chunk file CID and is expected // to contain the same data as the iterated commitment. - let chunk_file_commitment = commitments.get(0).unwrap(); + let chunk_file_commitment = commitments.first().unwrap(); assert_eq!(chunk_file_commitment.hash().unwrap(), chunk_file_cid); assert_eq!(expected_data, chunk_file_commitment.expected_data()); From 1e49d8d94bbe2b2502c07de496bc8f02bfcaa4c9 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Fri, 6 Sep 2024 17:57:34 +0100 Subject: [PATCH 68/86] Change return type for error propagation with axum Change return type to Result to disambiguate the return type --- trustchain-http/src/attestation_utils.rs | 14 +++++++ trustchain-http/src/attestor.rs | 52 ++++++++++++------------ 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index fa1f0f58..5172d7d2 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -90,6 +90,9 @@ pub enum TrustchainCRError { // Failed to verify nonce #[error("Failed to verify nonce.")] FailedToVerifyNonce, + /// Wrapped IO error + #[error("IO error: {0}")] + IOError(std::io::Error), } impl From for TrustchainCRError { @@ -254,6 +257,17 @@ impl IdentityCRInitiation { pub fn is_complete(&self) -> bool { return self.temp_p_key.is_some() && self.requester_details.is_some(); } + + pub fn temp_p_key(&self) -> Result<&Jwk, TrustchainCRError> { + self.temp_p_key + .as_ref() + .ok_or(TrustchainCRError::KeyNotFound) + } + pub fn temp_s_key(&self) -> Result<&Jwk, TrustchainCRError> { + self.temp_s_key + .as_ref() + .ok_or(TrustchainCRError::KeyNotFound) + } } impl ElementwiseSerializeDeserialize for IdentityCRInitiation { diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index b0252aa3..08c2c651 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -20,7 +20,6 @@ use log::info; use trustchain_api::api::TrustchainDIDAPI; use trustchain_api::TrustchainAPI; use trustchain_core::attestor::AttestorError; -use trustchain_core::key_manager::KeyManagerError; use trustchain_core::verifier::Verifier; use std::collections::HashMap; @@ -70,13 +69,12 @@ impl TrustchainAttestorHTTPHandler { /// is saved is determined by the temp public key of the attestation initiation. pub async fn post_identity_initiation( Json(attestation_initiation): Json, - ) -> impl IntoResponse { + ) -> Result { info!("Received attestation info: {:?}", attestation_initiation); - let temp_p_key_ssi = - josekit_to_ssi_jwk(attestation_initiation.temp_p_key.as_ref().unwrap()); - let path = attestation_request_path(&temp_p_key_ssi.unwrap(), "attestor").unwrap(); + let temp_p_key_ssi = josekit_to_ssi_jwk(attestation_initiation.temp_p_key()?); + let path = attestation_request_path(&temp_p_key_ssi.unwrap(), "attestor")?; // create directory and save attestation initation to file - let _ = std::fs::create_dir_all(&path); + std::fs::create_dir_all(&path).map_err(TrustchainCRError::IOError)?; let result = attestation_initiation.elementwise_serialize(&path); match result { Ok(_) => { @@ -84,14 +82,14 @@ impl TrustchainAttestorHTTPHandler { message: "Received attestation request. Please wait for operator to contact you through an alternative channel.".to_string(), data: None, }; - (StatusCode::OK, Json(response)) + Ok((StatusCode::OK, Json(response))) } Err(_) => { let response = CustomResponse { message: "Attestation request failed.".to_string(), data: None, }; - (StatusCode::BAD_REQUEST, Json(response)) + Ok((StatusCode::BAD_REQUEST, Json(response))) } } } @@ -107,50 +105,54 @@ impl TrustchainAttestorHTTPHandler { pub async fn post_identity_response( (Path(key_id), Json(response)): (Path, Json), app_state: Arc, - ) -> impl IntoResponse { - let pathbase = attestation_request_basepath("attestor").unwrap(); + ) -> Result { + let pathbase = attestation_request_basepath("attestor")?; let path = pathbase.join(key_id); if !path.exists() { panic!("Provided attestation request not found. Path does not exist."); } let mut identity_challenge = IdentityCRChallenge::new() - .elementwise_deserialize(&path) - .unwrap() - .unwrap(); + .elementwise_deserialize(&path)? + .ok_or(TrustchainCRError::FailedToDeserialize)?; // get signing key from ION attestor - let did = app_state.config.server_did.as_ref().unwrap().to_owned(); + let did = app_state + .config + .server_did + .as_ref() + .expect("CR requires server DID.") + .to_owned(); let ion_attestor = IONAttestor::new(&did); + // TODO: impl From for TrustchainCRError let signing_keys = ion_attestor.signing_keys().unwrap(); + // TODO: consider passing a key_id, first key used as arbitrary choice currently + // Unwrap: ok since signing keys cannot be empty. let signing_key_ssi = signing_keys.first().unwrap(); - let signing_key = ssi_to_josekit_jwk(signing_key_ssi).unwrap(); + let signing_key = ssi_to_josekit_jwk(signing_key_ssi)?; // get temp public key let identity_initiation = IdentityCRInitiation::new() - .elementwise_deserialize(&path) - .unwrap() - .unwrap(); - let temp_p_key = identity_initiation.temp_p_key.unwrap(); + .elementwise_deserialize(&path)? + .ok_or(TrustchainCRError::FailedToDeserialize)?; + let temp_p_key = identity_initiation.temp_p_key()?; // verify response let attestor = Entity {}; - let payload = attestor - .decrypt_and_verify(response.clone(), &signing_key, &temp_p_key) - .unwrap(); + let payload = attestor.decrypt_and_verify(response.clone(), &signing_key, &temp_p_key)?; let result = verify_nonce(payload, &path); match result { Ok(_) => { identity_challenge.identity_response_signature = Some(response.clone()); - identity_challenge.elementwise_serialize(&path).unwrap(); + identity_challenge.elementwise_serialize(&path)?; let response = CustomResponse { message: "Verification successful. Please use the provided path to initiate the second part of the attestation process.".to_string(), data: None }; - (StatusCode::OK, Json(response)) + Ok((StatusCode::OK, Json(response))) } Err(_) => { let response = CustomResponse { message: "Verification failed. Please try again.".to_string(), data: None, }; - (StatusCode::BAD_REQUEST, Json(response)) + Ok((StatusCode::BAD_REQUEST, Json(response))) } } } From 062ea426fd2fcf12cc000b181bb2c09c940f32d1 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Sat, 7 Sep 2024 11:00:30 +0100 Subject: [PATCH 69/86] Fix handler for post_identity_response --- trustchain-http/src/attestor.rs | 13 ++++++++----- trustchain-http/src/errors.rs | 2 +- trustchain-http/src/server.rs | 9 ++++++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index 08c2c651..faa84615 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -105,7 +105,7 @@ impl TrustchainAttestorHTTPHandler { pub async fn post_identity_response( (Path(key_id), Json(response)): (Path, Json), app_state: Arc, - ) -> Result { + ) -> Result { let pathbase = attestation_request_basepath("attestor")?; let path = pathbase.join(key_id); if !path.exists() { @@ -135,15 +135,18 @@ impl TrustchainAttestorHTTPHandler { let temp_p_key = identity_initiation.temp_p_key()?; // verify response let attestor = Entity {}; - let payload = attestor.decrypt_and_verify(response.clone(), &signing_key, &temp_p_key)?; + let payload = attestor.decrypt_and_verify(response.clone(), &signing_key, temp_p_key)?; let result = verify_nonce(payload, &path); match result { Ok(_) => { identity_challenge.identity_response_signature = Some(response.clone()); identity_challenge.elementwise_serialize(&path)?; let response = CustomResponse { - message: "Verification successful. Please use the provided path to initiate the second part of the attestation process.".to_string(), - data: None + message: "\ + Verification successful. Please use the provided path to initiate the second \ + part of the attestation process." + .to_string(), + data: None, }; Ok((StatusCode::OK, Json(response))) } @@ -168,7 +171,7 @@ impl TrustchainAttestorHTTPHandler { pub async fn post_content_initiation( (Path(key_id), Json(ddid)): (Path, Json), app_state: Arc, - ) -> Result<(StatusCode, String), TrustchainHTTPError> { + ) -> Result { let pathbase = attestation_request_basepath("attestor")?; let path = pathbase.join(&key_id); let did = app_state diff --git a/trustchain-http/src/errors.rs b/trustchain-http/src/errors.rs index d5505900..14b0b8bf 100644 --- a/trustchain-http/src/errors.rs +++ b/trustchain-http/src/errors.rs @@ -47,7 +47,7 @@ pub enum TrustchainHTTPError { #[error("Request does not exist.")] RequestDoesNotExist, #[error("Could not deserialize data: {0}")] - FailedToDeserialize(serde_json::Error), + FailedToDeserialize(#[from] serde_json::Error), #[error("Root event time not configured for verification.")] RootEventTimeNotSet, #[error("Attestation request failed.")] diff --git a/trustchain-http/src/server.rs b/trustchain-http/src/server.rs index e34aac79..6683f912 100644 --- a/trustchain-http/src/server.rs +++ b/trustchain-http/src/server.rs @@ -1,10 +1,13 @@ +use crate::attestation_utils::CustomResponse; use crate::attestor; use crate::config::http_config; use crate::middleware::validate_did; use crate::{ config::HTTPConfig, issuer, resolver, root, state::AppState, static_handlers, verifier, }; +use axum::extract::Path; use axum::routing::{post, IntoMakeService}; +use axum::Json; use axum::{middleware, routing::get, Router}; use axum_server::tls_rustls::RustlsConfig; use hyper::server::conn::AddrIncoming; @@ -130,12 +133,12 @@ impl TrustchainRouter { ) .route( "/did/attestor/identity/respond/:key_id", - // post(attestor::TrustchainAttestorHTTPHandler::post_response), post({ let state = shared_state.clone(); - move |key_id| { + move |(key_id, response)| { attestor::TrustchainAttestorHTTPHandler::post_identity_response( - key_id, state, + (key_id, response), + state, ) } }), From aa58312a8cca3b348264e2c85f8fc59442383831 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Sat, 7 Sep 2024 11:01:54 +0100 Subject: [PATCH 70/86] Fix warnings --- trustchain-http/src/server.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/trustchain-http/src/server.rs b/trustchain-http/src/server.rs index 6683f912..a405590c 100644 --- a/trustchain-http/src/server.rs +++ b/trustchain-http/src/server.rs @@ -1,13 +1,10 @@ -use crate::attestation_utils::CustomResponse; use crate::attestor; use crate::config::http_config; use crate::middleware::validate_did; use crate::{ config::HTTPConfig, issuer, resolver, root, state::AppState, static_handlers, verifier, }; -use axum::extract::Path; use axum::routing::{post, IntoMakeService}; -use axum::Json; use axum::{middleware, routing::get, Router}; use axum_server::tls_rustls::RustlsConfig; use hyper::server::conn::AddrIncoming; From 33ac7f1675fe4f9be6c6799a4f24b3398c6e2c9b Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Sat, 7 Sep 2024 11:06:24 +0100 Subject: [PATCH 71/86] Add wrapped key manager error variant --- trustchain-http/src/attestation_utils.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index 5172d7d2..ae8aa608 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -17,7 +17,7 @@ use ssi::{did::Service, jwk::JWK}; use ssi::{did::ServiceEndpoint, one_or_many::OneOrMany}; use std::fs::OpenOptions; use thiserror::Error; -use trustchain_core::TRUSTCHAIN_DATA; +use trustchain_core::{key_manager::KeyManagerError, TRUSTCHAIN_DATA}; #[derive(Error, Debug)] pub enum TrustchainCRError { @@ -87,12 +87,15 @@ pub enum TrustchainCRError { /// Field to respond #[error("Response to challenge failed.")] FailedToRespond(reqwest::Response), - // Failed to verify nonce + /// Failed to verify nonce #[error("Failed to verify nonce.")] FailedToVerifyNonce, /// Wrapped IO error #[error("IO error: {0}")] IOError(std::io::Error), + /// Wrapped KeyManager error + #[error("KeyManager error: {0}")] + KeyManagerError(#[from] KeyManagerError), } impl From for TrustchainCRError { From 588aa3d941dbd767c041b89c0a731aa0d1ea608f Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Sat, 7 Sep 2024 11:12:35 +0100 Subject: [PATCH 72/86] Propagate error, match Some(doc) --- trustchain-http/src/attestor.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index faa84615..a1e9c196 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -122,8 +122,7 @@ impl TrustchainAttestorHTTPHandler { .expect("CR requires server DID.") .to_owned(); let ion_attestor = IONAttestor::new(&did); - // TODO: impl From for TrustchainCRError - let signing_keys = ion_attestor.signing_keys().unwrap(); + let signing_keys = ion_attestor.signing_keys()?; // TODO: consider passing a key_id, first key used as arbitrary choice currently // Unwrap: ok since signing keys cannot be empty. let signing_key_ssi = signing_keys.first().unwrap(); @@ -183,8 +182,8 @@ impl TrustchainAttestorHTTPHandler { // resolve candidate DID let result = TrustchainAPI::resolve(&ddid, app_state.verifier.resolver()).await; let candidate_doc = match result { - Ok((_, doc, _)) => doc.unwrap(), - Err(_) => { + Ok((_, Some(doc), _)) => doc, + Ok((_, None, _)) | Err(_) => { let response = CustomResponse { message: "Resolution of candidate DID failed.".to_string(), data: None, From 70254b4ab7ffb9f9fb6b346bb65114377ac16799 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Sat, 7 Sep 2024 11:17:58 +0100 Subject: [PATCH 73/86] Propagate errors, add wrapped AttestorError and conversion --- trustchain-http/src/attestor.rs | 15 +++++++++------ trustchain-http/src/errors.rs | 10 ++++++++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index a1e9c196..c1cc4ef8 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -72,7 +72,7 @@ impl TrustchainAttestorHTTPHandler { ) -> Result { info!("Received attestation info: {:?}", attestation_initiation); let temp_p_key_ssi = josekit_to_ssi_jwk(attestation_initiation.temp_p_key()?); - let path = attestation_request_path(&temp_p_key_ssi.unwrap(), "attestor")?; + let path = attestation_request_path(&temp_p_key_ssi?, "attestor")?; // create directory and save attestation initation to file std::fs::create_dir_all(&path).map_err(TrustchainCRError::IOError)?; let result = attestation_initiation.elementwise_serialize(&path); @@ -124,8 +124,11 @@ impl TrustchainAttestorHTTPHandler { let ion_attestor = IONAttestor::new(&did); let signing_keys = ion_attestor.signing_keys()?; // TODO: consider passing a key_id, first key used as arbitrary choice currently - // Unwrap: ok since signing keys cannot be empty. - let signing_key_ssi = signing_keys.first().unwrap(); + let signing_key_ssi = signing_keys + .first() + .ok_or(AttestorError::NoSigningKey(format!( + "No signing keys for ION attestor with DID: {did}" + )))?; let signing_key = ssi_to_josekit_jwk(signing_key_ssi)?; // get temp public key let identity_initiation = IdentityCRInitiation::new() @@ -230,15 +233,15 @@ impl TrustchainAttestorHTTPHandler { // get public and secret keys let identity_cr_initiation = IdentityCRInitiation::new() .elementwise_deserialize(&path)? - .unwrap(); + .ok_or(TrustchainCRError::FailedToDeserialize)?; let ion_attestor = IONAttestor::new(&did); let signing_keys = ion_attestor.signing_keys()?; let signing_key_ssi = signing_keys .first() .ok_or(AttestorError::NoSigningKey(format!( "No signing keys for ION attestor with DID: {did}" - ))) - .unwrap(); + )))?; + let signing_key = ssi_to_josekit_jwk(signing_key_ssi).unwrap(); // sign and encrypt challenges diff --git a/trustchain-http/src/errors.rs b/trustchain-http/src/errors.rs index 14b0b8bf..c630d9cb 100644 --- a/trustchain-http/src/errors.rs +++ b/trustchain-http/src/errors.rs @@ -3,8 +3,9 @@ use hyper::StatusCode; use serde_json::json; use thiserror::Error; use trustchain_core::{ - commitment::CommitmentError, issuer::IssuerError, key_manager::KeyManagerError, - resolver::ResolverError, vc::CredentialError, verifier::VerifierError, vp::PresentationError, + attestor::AttestorError, commitment::CommitmentError, issuer::IssuerError, + key_manager::KeyManagerError, resolver::ResolverError, vc::CredentialError, + verifier::VerifierError, vp::PresentationError, }; use trustchain_ion::root::TrustchainRootError; @@ -27,6 +28,8 @@ pub enum TrustchainHTTPError { RootError(TrustchainRootError), #[error("Trustchain presentation error: {0}")] PresentationError(PresentationError), + #[error("Trustchain attestor error: {0}")] + AttestorError(#[from] AttestorError), // TODO: once needed in http propagate // #[error("Jose error: {0}")] // JoseError(JoseError), @@ -122,6 +125,9 @@ impl IntoResponse for TrustchainHTTPError { err @ TrustchainHTTPError::IssuerError(_) => { (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) } + err @ TrustchainHTTPError::AttestorError(_) => { + (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) + } err @ TrustchainHTTPError::CommitmentError(_) => { (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) } From 158d9008e812b05ce092002f158d4ab7864ca391 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Sat, 7 Sep 2024 11:20:11 +0100 Subject: [PATCH 74/86] Add wrapped jose error --- trustchain-http/src/attestor.rs | 4 ++-- trustchain-http/src/errors.rs | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index c1cc4ef8..5be45b1a 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -242,13 +242,13 @@ impl TrustchainAttestorHTTPHandler { "No signing keys for ION attestor with DID: {did}" )))?; - let signing_key = ssi_to_josekit_jwk(signing_key_ssi).unwrap(); + let signing_key = ssi_to_josekit_jwk(signing_key_ssi)?; // sign and encrypt challenges let value: serde_json::Value = serde_json::to_value(challenges).map_err(TrustchainCRError::Serde)?; let mut payload = JwtPayload::new(); - payload.set_claim("challenges", Some(value)).unwrap(); + payload.set_claim("challenges", Some(value))?; let signed_encrypted_challenges = attestor.sign_and_encrypt_claim( &payload, &signing_key, diff --git a/trustchain-http/src/errors.rs b/trustchain-http/src/errors.rs index c630d9cb..6b1d7f70 100644 --- a/trustchain-http/src/errors.rs +++ b/trustchain-http/src/errors.rs @@ -1,5 +1,6 @@ use axum::{response::IntoResponse, Json}; use hyper::StatusCode; +use josekit::JoseError; use serde_json::json; use thiserror::Error; use trustchain_core::{ @@ -31,8 +32,8 @@ pub enum TrustchainHTTPError { #[error("Trustchain attestor error: {0}")] AttestorError(#[from] AttestorError), // TODO: once needed in http propagate - // #[error("Jose error: {0}")] - // JoseError(JoseError), + #[error("Jose error: {0}")] + JoseError(#[from] JoseError), #[error("Trustchain key manager error: {0}")] KeyManagerError(KeyManagerError), #[error("Trustchain challenge-response error: {0}")] @@ -146,6 +147,9 @@ impl IntoResponse for TrustchainHTTPError { err @ TrustchainHTTPError::KeyManagerError(_) => { (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) } + err @ TrustchainHTTPError::JoseError(_) => { + (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) + } err @ TrustchainHTTPError::CRError(_) => { (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) } From 492725204b58591f79b966d5c5e7edc9c90690df Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Sat, 7 Sep 2024 11:48:45 +0100 Subject: [PATCH 75/86] Remove unwraps, add helper functions, add variant --- trustchain-http/src/attestation_utils.rs | 5 +- trustchain-http/src/attestor.rs | 131 +++++++++++++---------- 2 files changed, 78 insertions(+), 58 deletions(-) diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index ae8aa608..9d20f194 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -17,7 +17,7 @@ use ssi::{did::Service, jwk::JWK}; use ssi::{did::ServiceEndpoint, one_or_many::OneOrMany}; use std::fs::OpenOptions; use thiserror::Error; -use trustchain_core::{key_manager::KeyManagerError, TRUSTCHAIN_DATA}; +use trustchain_core::{attestor::AttestorError, key_manager::KeyManagerError, TRUSTCHAIN_DATA}; #[derive(Error, Debug)] pub enum TrustchainCRError { @@ -96,6 +96,9 @@ pub enum TrustchainCRError { /// Wrapped KeyManager error #[error("KeyManager error: {0}")] KeyManagerError(#[from] KeyManagerError), + /// Wrapped Attestor error + #[error("Attestor error: {0}")] + AttestorError(#[from] AttestorError), } impl From for TrustchainCRError { diff --git a/trustchain-http/src/attestor.rs b/trustchain-http/src/attestor.rs index 5be45b1a..e5281d1b 100644 --- a/trustchain-http/src/attestor.rs +++ b/trustchain-http/src/attestor.rs @@ -17,6 +17,8 @@ use josekit::jwk::Jwk; use josekit::jwt::JwtPayload; use log::info; +use ssi::jwk::JWK; +use ssi::vc::OneOrMany; use trustchain_api::api::TrustchainDIDAPI; use trustchain_api::TrustchainAPI; use trustchain_core::attestor::AttestorError; @@ -29,6 +31,26 @@ use std::sync::Arc; use trustchain_core::utils::generate_key; use trustchain_ion::attestor::IONAttestor; +fn server_did(app_state: Arc) -> String { + app_state + .config + .server_did + .as_ref() + .expect("Server DID must be set for challenge-response content initiation.") + .to_owned() +} + +fn first_signing_key<'a>( + signing_keys: &'a OneOrMany, + did: &str, +) -> Result<&'a JWK, AttestorError> { + signing_keys + .first() + .ok_or(AttestorError::NoSigningKey(format!( + "No signing keys for ION attestor with DID: {did}" + ))) +} + // Encryption: https://github.com/hidekatsu-izuno/josekit-rs#signing-a-jwt-by-ecdsa #[async_trait] @@ -115,20 +137,11 @@ impl TrustchainAttestorHTTPHandler { .elementwise_deserialize(&path)? .ok_or(TrustchainCRError::FailedToDeserialize)?; // get signing key from ION attestor - let did = app_state - .config - .server_did - .as_ref() - .expect("CR requires server DID.") - .to_owned(); + let did = server_did(app_state); let ion_attestor = IONAttestor::new(&did); let signing_keys = ion_attestor.signing_keys()?; // TODO: consider passing a key_id, first key used as arbitrary choice currently - let signing_key_ssi = signing_keys - .first() - .ok_or(AttestorError::NoSigningKey(format!( - "No signing keys for ION attestor with DID: {did}" - )))?; + let signing_key_ssi = first_signing_key(&signing_keys, &did)?; let signing_key = ssi_to_josekit_jwk(signing_key_ssi)?; // get temp public key let identity_initiation = IdentityCRInitiation::new() @@ -236,12 +249,7 @@ impl TrustchainAttestorHTTPHandler { .ok_or(TrustchainCRError::FailedToDeserialize)?; let ion_attestor = IONAttestor::new(&did); let signing_keys = ion_attestor.signing_keys()?; - let signing_key_ssi = signing_keys - .first() - .ok_or(AttestorError::NoSigningKey(format!( - "No signing keys for ION attestor with DID: {did}" - )))?; - + let signing_key_ssi = first_signing_key(&signing_keys, &did)?; let signing_key = ssi_to_josekit_jwk(signing_key_ssi)?; // sign and encrypt challenges @@ -252,7 +260,7 @@ impl TrustchainAttestorHTTPHandler { let signed_encrypted_challenges = attestor.sign_and_encrypt_claim( &payload, &signing_key, - &identity_cr_initiation.temp_p_key.unwrap(), + identity_cr_initiation.temp_p_key()?, ); match signed_encrypted_challenges { @@ -262,22 +270,19 @@ impl TrustchainAttestorHTTPHandler { content_challenge_signature: Some(signed_encrypted_challenges.clone()), content_response_signature: None, }; - content_challenge.elementwise_serialize(&path).unwrap(); + content_challenge.elementwise_serialize(&path)?; let response = CustomResponse { message: "Challenges generated successfully.".to_string(), data: Some(signed_encrypted_challenges), }; - Ok((StatusCode::OK, serde_json::to_string(&response).unwrap())) + Ok((StatusCode::OK, serde_json::to_string(&response)?)) } Err(_) => { let response = CustomResponse { message: "Failed to generate challenges.".to_string(), data: None, }; - Ok(( - StatusCode::BAD_REQUEST, - serde_json::to_string(&response).unwrap(), - )) + Ok((StatusCode::BAD_REQUEST, serde_json::to_string(&response)?)) } } } @@ -291,53 +296,56 @@ impl TrustchainAttestorHTTPHandler { pub async fn post_content_response( (Path(key_id), Json(response)): (Path, Json), app_state: Arc, - ) -> impl IntoResponse { + ) -> Result { // deserialise expected nonce map - let pathbase = attestation_request_basepath("attestor").unwrap(); + let pathbase = attestation_request_basepath("attestor")?; let path = pathbase.join(key_id); let identity_cr_initiation = IdentityCRInitiation::new() - .elementwise_deserialize(&path) - .unwrap() - .unwrap(); + .elementwise_deserialize(&path)? + .ok_or(TrustchainCRError::FailedToDeserialize)?; let mut content_challenge = ContentCRChallenge::new() - .elementwise_deserialize(&path) - .unwrap() - .unwrap(); - let expected_nonces = content_challenge.content_nonce.clone().unwrap(); + .elementwise_deserialize(&path)? + .ok_or(TrustchainCRError::FailedToDeserialize)?; + let expected_nonces = content_challenge + .content_nonce + .clone() + .ok_or(TrustchainCRError::FieldNotFound)?; // get signing key from ION attestor - let did = app_state.config.server_did.as_ref().unwrap().to_owned(); + let did = server_did(app_state); let ion_attestor = IONAttestor::new(&did); - let signing_keys = ion_attestor.signing_keys().unwrap(); - let signing_key_ssi = signing_keys.first().unwrap(); - let signing_key = ssi_to_josekit_jwk(signing_key_ssi).unwrap(); + let signing_keys = ion_attestor.signing_keys()?; + let signing_key_ssi = first_signing_key(&signing_keys, &did)?; + let signing_key = ssi_to_josekit_jwk(signing_key_ssi)?; // decrypt and verify response => nonces map let attestor = Entity {}; - let payload = attestor - .decrypt_and_verify( - response.clone(), - &signing_key, - &identity_cr_initiation.temp_p_key.unwrap(), - ) - .unwrap(); - let nonces_map: HashMap = - serde_json::from_value(payload.claim("nonces").unwrap().clone()).unwrap(); + let payload = attestor.decrypt_and_verify( + response.clone(), + &signing_key, + identity_cr_initiation.temp_p_key()?, + )?; + let nonces_map: HashMap = serde_json::from_value( + payload + .claim("nonces") + .ok_or(TrustchainCRError::ClaimNotFound)? + .clone(), + )?; // verify nonces if nonces_map.eq(&expected_nonces) { content_challenge.content_response_signature = Some(response.clone()); - content_challenge.elementwise_serialize(&path).unwrap(); + content_challenge.elementwise_serialize(&path)?; let response = CustomResponse { message: "Attestation request successful.".to_string(), data: None, }; - return (StatusCode::OK, Json(response)); + return Ok((StatusCode::OK, Json(response))); } let response = CustomResponse { message: "Verification failed. Attestation request unsuccessful.".to_string(), data: None, }; - (StatusCode::BAD_REQUEST, Json(response)) + Ok((StatusCode::BAD_REQUEST, Json(response))) } } @@ -370,12 +378,12 @@ pub fn present_identity_challenge( }; // make payload - let payload = JwtPayload::try_from(&identity_challenge).unwrap(); + let payload = JwtPayload::try_from(&identity_challenge)?; // get signing key from ION attestor let ion_attestor = IONAttestor::new(did); - let signing_keys = ion_attestor.signing_keys().unwrap(); - let signing_key_ssi = signing_keys.first().unwrap(); + let signing_keys = ion_attestor.signing_keys()?; + let signing_key_ssi = first_signing_key(&signing_keys, did)?; let signing_key = ssi_to_josekit_jwk(signing_key_ssi).map_err(|_| TrustchainCRError::FailedToGenerateKey)?; @@ -395,13 +403,22 @@ pub fn present_identity_challenge( /// nonce from the file and compares it with the nonce from the payload. fn verify_nonce(payload: JwtPayload, path: &PathBuf) -> Result<(), TrustchainCRError> { // get nonce from payload - let nonce = payload.claim("identity_nonce").unwrap().as_str().unwrap(); + let nonce = payload + .claim("identity_nonce") + .ok_or(TrustchainCRError::ClaimNotFound)? + .as_str() + .ok_or(TrustchainCRError::FailedToConvertToStr( + // Unwrap: not None since error would have propagated above if None + payload.claim("identity_nonce").unwrap().clone(), + ))?; // deserialise expected nonce let identity_challenge = IdentityCRChallenge::new() - .elementwise_deserialize(path) - .unwrap() - .unwrap(); - let expected_nonce = identity_challenge.identity_nonce.unwrap().to_string(); + .elementwise_deserialize(path)? + .ok_or(TrustchainCRError::FailedToDeserialize)?; + let expected_nonce = identity_challenge + .identity_nonce + .ok_or(TrustchainCRError::FieldNotFound)? + .to_string(); if nonce != expected_nonce { return Err(TrustchainCRError::FailedToVerifyNonce); } From 6475616ac3bb360623f13bf3cd1d09b0d5971cf1 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 16 Sep 2024 16:28:33 +0100 Subject: [PATCH 76/86] Remove unwrap in a TryFrom Co-authored-by: pwochner Co-authored-by: Tim Hobson --- trustchain-http/src/attestation_utils.rs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index 9d20f194..9869939a 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -481,15 +481,27 @@ impl TryFrom<&JwtPayload> for IdentityCRChallenge { identity_challenge_signature: None, identity_response_signature: None, }; - challenge.update_p_key = Some( - serde_json::from_str(value.claim("update_p_key").unwrap().as_str().unwrap()).unwrap(), - ); + challenge.update_p_key = Some(serde_json::from_str( + value + .claim("update_p_key") + .ok_or(TrustchainCRError::ClaimNotFound)? + .as_str() + .ok_or(TrustchainCRError::FailedToConvertToStr( + // Unwrap: not None since error would have propagated above if None + value.claim("update_p_key").unwrap().clone(), + ))?, + )?); challenge.identity_nonce = Some(Nonce::from( + // TODO: refactor into function for a given payload and claim field, + // returns a Result value .claim("identity_nonce") - .unwrap() + .ok_or(TrustchainCRError::ClaimNotFound)? .as_str() - .unwrap() + .ok_or(TrustchainCRError::FailedToConvertToStr( + // Unwrap: not None since error would have propagated above if None + value.claim("identity_nonce").unwrap().clone(), + ))? .to_string(), )); Ok(challenge) From 93415baf170ef674b86cd7f1c07ee4d15712f979 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 16 Sep 2024 16:32:14 +0100 Subject: [PATCH 77/86] Remove obsolete module as now included in `tests/attestation.rs` --- trustchain-http/src/challenge_response.rs | 233 ---------------------- 1 file changed, 233 deletions(-) delete mode 100644 trustchain-http/src/challenge_response.rs diff --git a/trustchain-http/src/challenge_response.rs b/trustchain-http/src/challenge_response.rs deleted file mode 100644 index d0fbc6ef..00000000 --- a/trustchain-http/src/challenge_response.rs +++ /dev/null @@ -1,233 +0,0 @@ -#[cfg(test)] -mod tests { - - // use ssi::vc::URI; - use tempfile::tempdir; - - use std::str; - - use crate::data::{ - TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS, TEST_SIGNING_KEY_1, TEST_SIGNING_KEY_2, - TEST_TEMP_KEY, TEST_UPDATE_KEY, TEST_UPSTREAM_KEY, - }; - - use super::*; - - #[test] - fn test_identity_challenge_response() { - // ==========| UE - generate challenge | ============== - let upstream_s_key: Jwk = serde_json::from_str(TEST_UPSTREAM_KEY).unwrap(); - let update_key: Jwk = serde_json::from_str(TEST_UPDATE_KEY).unwrap(); - let temp_s_key: Jwk = serde_json::from_str(TEST_TEMP_KEY).unwrap(); - let temp_p_key = temp_s_key.to_public_key().unwrap(); - - // generate challenge - let request_initiation = IdentityCRInitiation { - temp_p_key: Some(temp_p_key.clone()), - requester_details: Some(RequesterDetails { - requester_org: String::from("My Org"), - operator_name: String::from("John Doe"), - }), - }; - - let mut upstream_identity_challenge_response = CRIdentityChallenge { - update_p_key: Some(update_key.clone()), - identity_nonce: Some(Nonce::new()), - identity_challenge_signature: None, - identity_response_signature: None, - }; - - // sign and encrypt - let upstream_entity = Entity {}; - - let payload = JwtPayload::try_from(&upstream_identity_challenge_response).unwrap(); - let signed_encrypted_challenge = upstream_entity - .sign_and_encrypt_claim( - &payload, - &upstream_s_key, - &request_initiation.temp_p_key.unwrap(), - ) - .unwrap(); - - upstream_identity_challenge_response.identity_challenge_signature = - Some(signed_encrypted_challenge); - - // ==========| DE - generate response | ============== - - // decrypt and verify - let downstream_entity = Entity {}; - let upstream_p_key = upstream_s_key.to_public_key().unwrap(); - let signed_encrypted_challenge = upstream_identity_challenge_response - .identity_challenge_signature - .clone() - .unwrap(); - - let decrypted_verified_challenge = downstream_entity - .decrypt_and_verify(signed_encrypted_challenge, &temp_s_key, &upstream_p_key) - .unwrap(); - let downstream_identity_challenge = - CRIdentityChallenge::try_from(&decrypted_verified_challenge).unwrap(); - - // generate response - let mut payload = JwtPayload::new(); - payload - .set_claim( - "identity_nonce", - Some(Value::from( - downstream_identity_challenge - .identity_nonce - .as_ref() - .unwrap() - .to_string(), - )), - ) - .unwrap(); - let signed_encrypted_response = downstream_entity - .sign_and_encrypt_claim(&payload, &temp_s_key, &upstream_p_key) - .unwrap(); - - // ==========| UE - verify response | ============== - - // decrypt and verify signature - let decrypted_verified_response = upstream_entity - .decrypt_and_verify(signed_encrypted_response, &upstream_s_key, &temp_p_key) - .unwrap(); - - let nonce = decrypted_verified_response - .claim("identity_nonce") - .unwrap() - .as_str() - .unwrap(); - - let expected_nonce = upstream_identity_challenge_response - .identity_nonce - .unwrap() - .to_string(); - assert_eq!(nonce, expected_nonce); - } - - #[test] - fn test_content_challenge_response() { - // ==========| UE - generate challenge | ============== - let upstream_entity = Entity {}; - let upstream_s_key: Jwk = serde_json::from_str(TEST_UPSTREAM_KEY).unwrap(); - let temp_s_key: Jwk = serde_json::from_str(TEST_TEMP_KEY).unwrap(); - let temp_p_key = temp_s_key.to_public_key().unwrap(); - // get signing keys for DE from did document - let doc: Document = serde_json::from_str(TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS).unwrap(); - let test_keys_map = extract_key_ids_and_jwk(&doc).unwrap(); - - // generate map with unencrypted nonces so UE can store them for later verification - let nonces: HashMap = - test_keys_map - .iter() - .fold(HashMap::new(), |mut acc, (key_id, _)| { - acc.insert(String::from(key_id), Nonce::new()); - acc - }); - - for (_, val) in &nonces { - println!("{:?}", val); - } - - // turn nonces into challenges by encrypting them with the public keys of UE - let challenges = nonces - .iter() - .fold(HashMap::new(), |mut acc, (key_id, nonce)| { - acc.insert( - String::from(key_id), - upstream_entity - .encrypt( - &JwtPayload::try_from(nonce).unwrap(), - &test_keys_map.get(key_id).unwrap(), - ) - .unwrap(), - ); - acc - }); - - // sign (UE private key) and encrypt (DE temp public key) entire challenge - let value: serde_json::Value = serde_json::to_value(challenges).unwrap(); - let mut payload = JwtPayload::new(); - payload.set_claim("challenges", Some(value)).unwrap(); - let signed_encrypted_challenges = upstream_entity - .sign_and_encrypt_claim(&payload, &upstream_s_key, &temp_p_key) - .unwrap(); - - // ==========| DE - generate response | ============== - let downstream_entity = Entity {}; - let upstream_p_key = upstream_s_key.to_public_key().unwrap(); - - // decrypt and verify signature on challenges - let decrypted_verified_challenges = downstream_entity - .decrypt_and_verify(signed_encrypted_challenges, &temp_s_key, &upstream_p_key) - .unwrap(); - - // decrypt nonces from challenges - let challenges_map: HashMap = serde_json::from_value( - decrypted_verified_challenges - .claim("challenges") - .unwrap() - .clone(), - ) - .unwrap(); - - let downstream_s_key_1: Jwk = serde_json::from_str(TEST_SIGNING_KEY_1).unwrap(); - let downstream_s_key_2: Jwk = serde_json::from_str(TEST_SIGNING_KEY_2).unwrap(); - let downstream_key_id_1 = josekit_to_ssi_jwk(&downstream_s_key_1) - .unwrap() - .thumbprint() - .unwrap(); - let downstream_key_id_2 = josekit_to_ssi_jwk(&downstream_s_key_2) - .unwrap() - .thumbprint() - .unwrap(); - - let mut downstream_s_keys_map: HashMap = HashMap::new(); - downstream_s_keys_map.insert(downstream_key_id_1, downstream_s_key_1); - downstream_s_keys_map.insert(downstream_key_id_2, downstream_s_key_2); - - let decrypted_nonces: HashMap = - challenges_map - .iter() - .fold(HashMap::new(), |mut acc, (key_id, nonce)| { - acc.insert( - String::from(key_id), - downstream_entity - .decrypt( - &Some(Value::from(nonce.clone())).unwrap(), - downstream_s_keys_map.get(key_id).unwrap(), - ) - .unwrap() - .claim("nonce") - .unwrap() - .as_str() - .unwrap() - .to_string(), - ); - - acc - }); - // sign and encrypt response - let value: serde_json::Value = serde_json::to_value(decrypted_nonces).unwrap(); - let mut payload = JwtPayload::new(); - payload.set_claim("nonces", Some(value)).unwrap(); - let signed_encrypted_response = downstream_entity - .sign_and_encrypt_claim(&payload, &temp_s_key, &upstream_p_key) - .unwrap(); - - // ==========| UE - verify response | ============== - let decrypted_verified_response = upstream_entity - .decrypt_and_verify(signed_encrypted_response, &upstream_s_key, &temp_p_key) - .unwrap(); - println!( - "Decrypted and verified response: {:?}", - decrypted_verified_response - ); - let verified_response_map: HashMap = - serde_json::from_value(decrypted_verified_response.claim("nonces").unwrap().clone()) - .unwrap(); - println!("Verified response map: {:?}", verified_response_map); - assert_eq!(verified_response_map, nonces); - } -} From 73ca38b5f42114511825d19df42e1e5205beb612 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 16 Sep 2024 16:37:28 +0100 Subject: [PATCH 78/86] Remove obsolete module --- trustchain-http/src/encryption.rs | 598 ------------------------------ 1 file changed, 598 deletions(-) delete mode 100644 trustchain-http/src/encryption.rs diff --git a/trustchain-http/src/encryption.rs b/trustchain-http/src/encryption.rs deleted file mode 100644 index 73570adc..00000000 --- a/trustchain-http/src/encryption.rs +++ /dev/null @@ -1,598 +0,0 @@ -use std::{collections::HashMap, str::FromStr}; - -use josekit::{ - jwe::JweHeader, - jwe::ECDH_ES, - jwk::Jwk, - jws::{JwsHeader, ES256K}, - jwt::{self, JwtPayload}, - JoseError, -}; -use rand::{distributions::Alphanumeric, thread_rng, Rng}; -use serde::{Deserialize, Serialize}; -use serde_json::{from_value, Value}; -use sha2::{Digest, Sha256}; -use ssi::did::Document; -use ssi::did::VerificationMethod; -use ssi::jwk::JWK; -use thiserror::Error; - -const TEMP_PRIVATE_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg","d":"CfdUwQ-CcBQkWpIDPjhSJAq2SCg6hAGdcvLmCj0aA-c"}"#; -const TEMP_PUB_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg"}"#; -const UPSTREAM_PRIVATE_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JEV4WMgoJekTa5RQD5M92P1oLjdpMNYETQ3nbtKSnLQ","y":"dRfg_5i5wcMg1lxAffQORHpzgtm2yEIqgJoUk5ZklvI","d":"DZDZd9bxopCv2YJelMpQm_BJ0awvzpT6xWdWbaQlIJI"}"#; -const UPSTREAM_PUB_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JEV4WMgoJekTa5RQD5M92P1oLjdpMNYETQ3nbtKSnLQ","y":"dRfg_5i5wcMg1lxAffQORHpzgtm2yEIqgJoUk5ZklvI"}"#; -const DOWNSTREAM_PRIV_KEY_1: &str = r#"{"kty":"EC","crv":"secp256k1","x":"Lt2ys7LE0ELccVtCETtVjMFavgjwYDjDBtuV_XCH7-g","y":"TdTT8oXUSXMvFbhnsYrqwOkL7-niHWFxW0vaBSnUMnI","d":"B7csdham680yGiIdxeyllmczap7-h6_LtKunRhRqfic"}"#; -const DOWNSTREAM_PUB_KEY_1: &str = r#"{"kty":"EC","crv":"secp256k1","x":"Lt2ys7LE0ELccVtCETtVjMFavgjwYDjDBtuV_XCH7-g","y":"TdTT8oXUSXMvFbhnsYrqwOkL7-niHWFxW0vaBSnUMnI"}"#; -const DOWNSTREAM_PRIV_KEY_2: &str = r#"{"kty":"EC","crv":"secp256k1","x":"AB1b_4-XSem0uiPGGuW_hf_AuPArukMuD2S95ypGDSE","y":"suvBnCbhicPdYZeqgxJfPFmiNHGYDjPiW8XkYHxwgBU","d":"V3zmieRjP9LYa1v8l8lYXh4LqU87bPspSAGqq34Up1Q"}"#; -const DOWNSTREAM_PUB_KEY_2: &str = r#"{"kty":"EC","crv":"secp256k1","x":"AB1b_4-XSem0uiPGGuW_hf_AuPArukMuD2S95ypGDSE","y":"suvBnCbhicPdYZeqgxJfPFmiNHGYDjPiW8XkYHxwgBU"}"#; - -#[derive(Error, Debug)] -pub enum TrustchainCRError { - /// Serde JSON error. - #[error("Wrapped serialization error: {0}")] - Serde(serde_json::Error), - /// Wrapped jose error. - #[error("Wrapped jose error: {0}")] - Jose(JoseError), - /// Missing JWK from verification method - #[error("Missing JWK from verification method of a DID document.")] - MissingJWK, - /// Key not found in hashmap - #[error("Key id not found.")] - KeyNotFound, -} - -pub struct IdentityChallenge { - nonce: String, // Maybe create a new Nonce type - update_commitment: String, // TODO: this should be a key, format??? -} -#[derive(Debug, Serialize, Deserialize)] -pub struct ContentChallengeItem { - encrypted_nonce: String, - hash_public_key: String, -} - -pub struct KeysCR { - private_key: Jwk, - public_key: Jwk, -} - -pub struct KeyPairs { - private_key: Jwk, - public_key: Jwk, -} - -// Orphan rule: need new trait in crate or new type. -// New trait: -trait ToJwk { - fn to_jwk(&self) -> Jwk { - todo!() - } -} - -// New type: -// Or we make our own key type (wrapper) -// Named field version -pub struct MyJWKNamedField { - key: Jwk, -} -// Tuple struct version -pub struct MyJWK(Jwk); - -impl From for Jwk { - fn from(value: MyJWK) -> Self { - value.0 - } -} - -impl From for JWK { - fn from(value: MyJWK) -> Self { - josekit_to_ssi_jwk(&value.0).unwrap() // copy code of function in here - } -} - -impl From for MyJWK { - fn from(value: JWK) -> Self { - todo!() - } -} - -impl From for MyJWK { - fn from(value: Jwk) -> Self { - todo!() - } -} - -// Ideas for structs: -/// A type for upstream entity? -struct UE { - keys_cr: KeysCR, -} - -/// A type for downstream entity? -struct DE; - -// pub trait CRStateIO { -// // read() returns any struct that implements the CRState trait (eg. Step2Claim) -// // (the Box<> is needed because the different structs that could be returned will likely have -// // different sizes) -// fn read(&self) -> Box; -// fn write(&self, payload: &str); -// } - -// An empty trait implimented by all data types, eg. Step2Claim? -// trait CRState { -// fn status(&self) { -// println!("Ok"); -// } -// } - -/// A type for a nonce -struct Nonce(String); - -// // Data type to be read/written to file? -// struct Step2Claim { -// nonce: Nonce, -// temp_pub_key: Jwk, -// } -// impl CRState for Step2Claim {} - -// // Give the ability to DE to read and write CRState data files -// impl CRStateIO for DE { -// fn read(&self) -> Box { -// todo!() -// } -// fn write(&self, payload: &str) { -// todo!() -// } -// } - -// TODO: own type for nonce - -trait ChallengeResponse { - fn sign_and_encrypt(&self, payload: &JwtPayload) -> Result; - fn decrypt_and_verify(&self, input: String) -> Result; -} - -impl ChallengeResponse for KeysCR { - fn sign_and_encrypt(&self, payload: &JwtPayload) -> Result { - // Sign payload... - let mut header = JwsHeader::new(); - header.set_token_type("JWT"); - let signer = ES256K.signer_from_jwk(&self.private_key)?; - let signed_jwt = jwt::encode_with_signer(payload, &header, &signer)?; - - // ... then encrypt - let mut header = JweHeader::new(); - header.set_token_type("JWT"); - header.set_content_encryption("A128CBC-HS256"); - header.set_content_encryption("A256GCM"); - - let mut payload = JwtPayload::new(); // TODO: new name instead of reuse? - payload.set_claim("signed_jwt", Some(Value::from(signed_jwt.clone())))?; - - let encrypter = ECDH_ES.encrypter_from_jwk(&self.public_key)?; - let encrypted_jwt = jwt::encode_with_encrypter(&payload, &header, &encrypter)?; - Ok(encrypted_jwt) - } - fn decrypt_and_verify(&self, input: String) -> Result { - // Decrypt ... - let decrypter = ECDH_ES.decrypter_from_jwk(&self.private_key)?; - let (payload, header) = jwt::decode_with_decrypter(input, &decrypter)?; - - // ... then verify signature on decrypted content - let verifier = ES256K.verifier_from_jwk(&self.public_key)?; - let (payload, header) = jwt::decode_with_verifier( - &payload.claim("signed_jwt").unwrap().as_str().unwrap(), - &verifier, - )?; - Ok(payload) - } -} - -fn generate_nonce() -> String { - thread_rng() - .sample_iter(&Alphanumeric) - .take(32) - .map(char::from) - .collect() -} - -fn josekit_to_ssi_jwk(key: &Jwk) -> Result { - let key_as_str: &str = &serde_json::to_string(&key).unwrap(); - let ssi_key: JWK = serde_json::from_str(key_as_str).unwrap(); - Ok(ssi_key) -} - -fn ssi_to_josekit_jwk(key: &JWK) -> Result { - let key_as_str: &str = &serde_json::to_string(&key).unwrap(); - let ssi_key: Jwk = serde_json::from_str(key_as_str).unwrap(); - Ok(ssi_key) -} - -fn present_identity_challenge( - challenge: &IdentityChallenge, - keys: &KeysCR, -) -> Result { - let mut payload = JwtPayload::new(); - payload.set_claim("nonce", Some(Value::from(challenge.nonce.clone())))?; // is this a good idea? - payload.set_claim( - "update_commitment", - Some(Value::from(challenge.update_commitment.clone())), - )?; - - let encrypted_challenge = keys.sign_and_encrypt(&payload).unwrap(); - println!("Please copy + paste this challenge and send it to the responsible operator via alternative channels."); - println!("Challenge:"); - println!("{}", encrypted_challenge); - Ok(encrypted_challenge) -} - -fn generate_challenge(key: &Jwk) -> Result { - let nonce = generate_nonce(); - println!("Nonce: {}", nonce); - - let encrypted_challenge = encrypt(nonce, &key).unwrap(); - - Ok(encrypted_challenge) -} - -/// Extracts challenge nonce -fn present_response(challenge: String, keys: &KeysCR) -> Result { - let decrypted_challenge = keys.decrypt_and_verify(challenge).unwrap(); - - let nonce = decrypted_challenge - .claim("nonce") - .unwrap() - .as_str() - .unwrap(); - let mut payload = JwtPayload::new(); - payload.set_claim("nonce", Some(Value::from(nonce)))?; - let response = keys.sign_and_encrypt(&payload).unwrap(); - - Ok(response) -} - -/// Verifies if nonce is valid -fn verify_response(response: String, keys: &KeysCR) -> Result { - // TODO: only returns payload, we don't verify if nonce correct at this point - let payload = keys.decrypt_and_verify(response).unwrap(); - - Ok(payload) -} - -fn present_content_challenge( - keys: &KeysCR, - downstream_pub_keys: Vec<&Jwk>, -) -> Result { - // get number of keys - - // generate one nonce per key and encrypt it with key - // let challenges: HashMap = - // test_keys_map - // .iter() - // .fold(HashMap::new(), |mut acc, (key_id, key)| { - // acc.insert(String::from(key_id), generate_challenge(&key).unwrap()); - // acc - // }); - todo!() -} - -fn sign(payload: &JwtPayload, key: &Jwk) -> Result { - let mut header = JwsHeader::new(); - header.set_token_type("JWT"); - let signer = ES256K.signer_from_jwk(key)?; - let signed_jwt = jwt::encode_with_signer(payload, &header, &signer)?; - Ok(signed_jwt) -} - -fn encrypt(value: String, key: &Jwk) -> Result { - let mut header = JweHeader::new(); - header.set_token_type("JWT"); - header.set_content_encryption("A128CBC-HS256"); - header.set_content_encryption("A256GCM"); - - let mut payload = JwtPayload::new(); - payload.set_claim("nonce", Some(Value::from(value.clone())))?; - - let encrypter = ECDH_ES.encrypter_from_jwk(&key)?; - let encrypted_jwt = jwt::encode_with_encrypter(&payload, &header, &encrypter)?; - Ok(encrypted_jwt) -} - -fn decrypt(input: &Value, key: &Jwk) -> Result { - let decrypter = ECDH_ES.decrypter_from_jwk(&key)?; - let (payload, header) = jwt::decode_with_decrypter(input.as_str().unwrap(), &decrypter)?; - Ok(payload) -} - -/// Extract public keys from did document together with corresponding key ids -fn extract_key_ids_and_jwk(document: &Document) -> Result, TrustchainCRError> { - let mut my_map = HashMap::::new(); - if let Some(vms) = &document.verification_method { - // TODO: leave the commented code - // vms.iter().for_each(|vm| match vm { - // VerificationMethod::Map(vm_map) => { - // let id = vm_map.id; - // let key = vm_map.get_jwk().unwrap(); - // let key_jose = ssi_to_josekit_jwk(&key).unwrap(); - // my_map.insert(id, key_jose); - // } - // _ => (), - // }); - // TODO: consider rewriting functional with filter, partition, fold over returned error - // variants. - for vm in vms { - match vm { - VerificationMethod::Map(vm_map) => { - let id = vm_map.id.clone(); // TODo: use JWK::thumbprint() instead - let key = vm_map - .get_jwk() - .map_err(|_| TrustchainCRError::MissingJWK)?; - // let id = key - // .thumbprint() - // .map_err(|_| TrustchainCRError::MissingJWK)?; //TODO: different error variant? - let key_jose = - ssi_to_josekit_jwk(&key).map_err(|err| TrustchainCRError::Serde(err))?; - my_map.insert(id, key_jose); - } - _ => (), - } - } - } - Ok(my_map) -} - -fn generate_content_response( - challenges: HashMap, - did_keys_priv: HashMap, - cr_keys: &KeysCR, -) -> Result { - let decrypted_nonces: HashMap = - challenges - .iter() - .fold(HashMap::new(), |mut acc, (key_id, nonce)| { - acc.insert( - String::from(key_id), - decrypt( - &Some(Value::from(nonce.clone())).unwrap(), - did_keys_priv.get(key_id).unwrap(), - ) - .unwrap() - .claim("nonce") - .unwrap() - .as_str() - .unwrap() - .to_string(), - ); - - acc - }); - // Ok(decrypted_nonces) - // make payload - let value: serde_json::Value = serde_json::to_value(decrypted_nonces).unwrap(); - let mut payload = JwtPayload::new(); - payload.set_claim("nonces", Some(value)).unwrap(); - // sign (temp private key) and encrypt (UE public key) - let encrypted_response = cr_keys.sign_and_encrypt(&payload).unwrap(); - - Ok(encrypted_response) -} - -fn verify_content_response( - response: String, - cr_keys: &KeysCR, -) -> Result, TrustchainCRError> { - // verify signature and decrypt response - let decrypted_response = cr_keys.decrypt_and_verify(response).unwrap(); - - // extract response hashmap - let response_hashmap: HashMap = - serde_json::from_value(decrypted_response.claim("nonces").unwrap().clone()).unwrap(); - - Ok(response_hashmap) -} - -/// Reads key that corresponds to given key id from file -fn get_private_key(key_id: &str) -> Result { - todo!() -} - -#[cfg(test)] -mod tests { - - use serde_json::from_str; - - use super::*; - use crate::data::{ - TEST_KEY_ID_1, TEST_KEY_ID_2, TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS, TEST_SIGNING_KEY_1, - TEST_SIGNING_KEY_2, - }; - - #[test] - fn test_extract_key_ids_and_jwk() { - let doc: Document = serde_json::from_str(TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS).unwrap(); - let test_keys_map = extract_key_ids_and_jwk(&doc).unwrap(); - println!("Hash map of DE public keys: {:?}", test_keys_map); - - let expected_key = "#V8jt_0c-aFlq40Uti2R_WiquxuzxyB8kn1cfWmXIU84"; - let first_key = test_keys_map.keys().next().expect("HashMap empty!"); - assert_eq!( - first_key, expected_key, - "The first key of the HashMap is not the expected key id." - ); - } - - #[test] - fn test_josekit_to_ssi_jwk() { - let expected_ssi_pub_key: JWK = serde_json::from_str(TEMP_PUB_KEY).unwrap(); - let expected_josekit_pub_key: Jwk = serde_json::from_str(TEMP_PUB_KEY).unwrap(); - - let ssi_pub_jwk = josekit_to_ssi_jwk(&expected_josekit_pub_key).unwrap(); - assert!(ssi_pub_jwk.equals_public(&expected_ssi_pub_key)); - - let expected_ssi_priv_key: JWK = serde_json::from_str(TEMP_PRIVATE_KEY).unwrap(); - let expected_josekit_priv_key: Jwk = serde_json::from_str(TEMP_PRIVATE_KEY).unwrap(); - - let ssi_priv_jwk = josekit_to_ssi_jwk(&expected_josekit_priv_key).unwrap(); - assert_eq!(ssi_priv_jwk, expected_ssi_priv_key); - - let wrong_expected_ssi_priv_key: JWK = serde_json::from_str(UPSTREAM_PRIVATE_KEY).unwrap(); - assert_ne!(ssi_priv_jwk, wrong_expected_ssi_priv_key); - } - - #[test] - fn test_ssi_to_josekit_jwk() { - let expected_ssi_pub_key: JWK = serde_json::from_str(TEMP_PUB_KEY).unwrap(); - let expected_josekit_pub_key: Jwk = serde_json::from_str(TEMP_PUB_KEY).unwrap(); - - let josekit_pub_jwk = ssi_to_josekit_jwk(&expected_ssi_pub_key).unwrap(); - assert_eq!(josekit_pub_jwk, expected_josekit_pub_key); - - let expected_ssi_priv_key: JWK = serde_json::from_str(TEMP_PRIVATE_KEY).unwrap(); - let expected_josekit_priv_key: Jwk = serde_json::from_str(TEMP_PRIVATE_KEY).unwrap(); - - let josekit_priv_jwk = ssi_to_josekit_jwk(&expected_ssi_priv_key).unwrap(); - assert_eq!(josekit_priv_jwk, expected_josekit_priv_key); - } - - #[test] - fn test_identity_challenge_response() { - // get challenge components and keys ready - let upstream_cr_keys = KeysCR { - private_key: serde_json::from_str(UPSTREAM_PRIVATE_KEY).unwrap(), - public_key: serde_json::from_str(TEMP_PUB_KEY).unwrap(), - }; - - let test_challenge = IdentityChallenge { - nonce: generate_nonce(), - update_commitment: String::from("somerandomstringfornow"), - }; - println!("======================"); - println!("The nonce is: {}", test_challenge.nonce); - println!("======================"); - let presented_challenge = - present_identity_challenge(&test_challenge, &upstream_cr_keys).unwrap(); - - // get keys for response ready - let downstream_cr_keys = KeysCR { - private_key: serde_json::from_str(TEMP_PRIVATE_KEY).unwrap(), - public_key: serde_json::from_str(UPSTREAM_PUB_KEY).unwrap(), - }; - let response = present_response(presented_challenge, &downstream_cr_keys).unwrap(); - - let verified_response = verify_response(response, &upstream_cr_keys).unwrap(); - let nonce_from_response = verified_response.claim("nonce").unwrap().as_str().unwrap(); - println!("======================"); - println!("Verified response: {}", nonce_from_response); - println!("======================"); - assert_eq!(test_challenge.nonce, nonce_from_response); - } - - #[test] - fn test_content_response() { - // keys the UE needs - let upstream_cr_keys = KeysCR { - private_key: serde_json::from_str(UPSTREAM_PRIVATE_KEY).unwrap(), - public_key: serde_json::from_str(TEMP_PUB_KEY).unwrap(), - }; - - // extract DE public keys from did document -> Vec<&KeyPairs> - let doc: Document = serde_json::from_str(TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS).unwrap(); - let test_keys_map = extract_key_ids_and_jwk(&doc).unwrap(); - // let mut test_keys_map: HashMap = HashMap::new(); - // test_keys_map.insert( - // String::from("key_1"), - // serde_json::from_str(DOWNSTREAM_PUB_KEY_1).unwrap(), - // ); - // test_keys_map.insert( - // String::from("key_2"), - // serde_json::from_str(DOWNSTREAM_PUB_KEY_2).unwrap(), - // ); - - // map with unencrypted nonces so UE can store them for later verification - let nonces: HashMap = - test_keys_map - .iter() - .fold(HashMap::new(), |mut acc, (key_id, _)| { - acc.insert(String::from(key_id), generate_nonce()); - acc - }); - - for (_, val) in &nonces { - println!("{}", val); - } - - let challenges = nonces - .iter() - .fold(HashMap::new(), |mut acc, (key_id, nonce)| { - acc.insert( - String::from(key_id), - encrypt(nonce.clone(), &test_keys_map.get(key_id).unwrap()).unwrap(), - ); - acc - }); - - // sign (UE private key) and encrypt (DE temp public key) entire challenge - let value: serde_json::Value = serde_json::to_value(challenges).unwrap(); - let mut payload = JwtPayload::new(); - payload.set_claim("challenges", Some(value)).unwrap(); - - let encrypted_challenge = upstream_cr_keys.sign_and_encrypt(&payload).unwrap(); - - // verify and decrypt - let downstream_cr_keys = KeysCR { - private_key: serde_json::from_str(TEMP_PRIVATE_KEY).unwrap(), - public_key: serde_json::from_str(UPSTREAM_PUB_KEY).unwrap(), - }; - let decrypted_challenge = downstream_cr_keys - .decrypt_and_verify(encrypted_challenge) - .unwrap(); - - // extract challenge hashmap - let challenges_hashmap: HashMap = - serde_json::from_value(decrypted_challenge.claim("challenges").unwrap().clone()) - .unwrap(); - - // Decrypt each challenge nonce - let mut test_priv_keys_map: HashMap = HashMap::new(); - test_priv_keys_map.insert( - String::from(TEST_KEY_ID_1), - serde_json::from_str(TEST_SIGNING_KEY_1).unwrap(), - ); - test_priv_keys_map.insert( - String::from(TEST_KEY_ID_2), - serde_json::from_str(TEST_SIGNING_KEY_2).unwrap(), - ); - - let response = - generate_content_response(challenges_hashmap, test_priv_keys_map, &downstream_cr_keys) - .unwrap(); - - // UE: verify response - let verified_response = verify_content_response(response, &upstream_cr_keys).unwrap(); - - assert_eq!( - verified_response.get(TEST_KEY_ID_1).unwrap(), - nonces.get(TEST_KEY_ID_1).unwrap() - ); - assert_eq!( - verified_response.get(TEST_KEY_ID_2).unwrap(), - nonces.get(TEST_KEY_ID_2).unwrap() - ); - } - #[test] - fn test_jwk_thumbprint() { - let doc: Document = serde_json::from_str(TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS).unwrap(); - let test_keys_map = extract_key_ids_and_jwk(&doc).unwrap(); - for (key_id, value) in test_keys_map { - println!("{}", key_id); - let ssi_key = josekit_to_ssi_jwk(&value).unwrap(); - let key_thumbprint = ssi_key.thumbprint().unwrap(); - println!("Thumbprint: {}", key_thumbprint); - } - } -} - -// todo -// - add update commitment to identity CR From 6867015f5d28b3d768a9b5ff3b752bdedc26babb Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 16 Sep 2024 17:20:36 +0100 Subject: [PATCH 79/86] Remove unwraps from requester Co-authored-by: pwochner Co-authored-by: Tim Hobson --- trustchain-http/src/attestation_utils.rs | 6 + trustchain-http/src/requester.rs | 181 ++++++++++++----------- 2 files changed, 102 insertions(+), 85 deletions(-) diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index 9869939a..4a94d3b4 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -99,6 +99,12 @@ pub enum TrustchainCRError { /// Wrapped Attestor error #[error("Attestor error: {0}")] AttestorError(#[from] AttestorError), + /// Wrapped SSI JWK error + #[error("SSI JWK error: {0}")] + SSIJwkError(#[from] ssi::jwk::Error), + /// Response from a `CustomResponse` must contain data + #[error("Must contain data but custom response contained no data")] + ResponseMustContainData, } impl From for TrustchainCRError { diff --git a/trustchain-http/src/requester.rs b/trustchain-http/src/requester.rs index ea581995..7917ea58 100644 --- a/trustchain-http/src/requester.rs +++ b/trustchain-http/src/requester.rs @@ -50,7 +50,7 @@ pub async fn initiate_identity_challenge( // get endpoint and uri let url_path = "/did/attestor/identity/initiate"; - let endpoint = matching_endpoint(services, ATTESTATION_FRAGMENT).unwrap(); + let endpoint = matching_endpoint(services, ATTESTATION_FRAGMENT)?; let uri = format!("{}{}", endpoint, url_path); // make POST request to endpoint @@ -60,7 +60,7 @@ pub async fn initiate_identity_challenge( .json(&identity_cr_initiation) .send() .await - .map_err(|err| TrustchainCRError::Reqwest(err))?; + .map_err(TrustchainCRError::Reqwest)?; if result.status() != 200 { return Err(TrustchainCRError::FailedToInitiateCR); @@ -87,32 +87,35 @@ pub async fn identity_response( attestor_p_key: &Jwk, ) -> Result { // deserialise challenge struct from file - let result = IdentityCRChallenge::new().elementwise_deserialize(path); - let mut identity_challenge = result.unwrap().unwrap(); + let mut identity_challenge = IdentityCRChallenge::new() + .elementwise_deserialize(path)? + .ok_or(TrustchainCRError::FailedToDeserialize)?; // get temp secret key from file - let identity_initiation = IdentityCRInitiation::new().elementwise_deserialize(path); - let temp_s_key = identity_initiation.unwrap().unwrap().temp_s_key.unwrap(); - let temp_s_key_ssi = josekit_to_ssi_jwk(&temp_s_key).unwrap(); + let identity_initiation = IdentityCRInitiation::new() + .elementwise_deserialize(path)? + .ok_or(TrustchainCRError::FailedToDeserialize)?; + let temp_s_key = identity_initiation.temp_s_key()?; + let temp_s_key_ssi = josekit_to_ssi_jwk(&temp_s_key)?; // decrypt and verify challenge let requester = Entity {}; - let decrypted_verified_payload = requester - .decrypt_and_verify( - identity_challenge - .identity_challenge_signature - .clone() - .unwrap(), - &temp_s_key, - &attestor_p_key, - ) - .unwrap(); + let decrypted_verified_payload = requester.decrypt_and_verify( + identity_challenge + .identity_challenge_signature + .clone() + .ok_or(TrustchainCRError::FieldNotFound)?, + &temp_s_key, + &attestor_p_key, + )?; // sign and encrypt response - let signed_encrypted_response = requester - .sign_and_encrypt_claim(&decrypted_verified_payload, &temp_s_key, &attestor_p_key) - .unwrap(); - let key_id = temp_s_key_ssi.to_public().thumbprint().unwrap(); + let signed_encrypted_response = requester.sign_and_encrypt_claim( + &decrypted_verified_payload, + &temp_s_key, + &attestor_p_key, + )?; + let key_id = temp_s_key_ssi.to_public().thumbprint()?; // get uri for POST request response - let endpoint = matching_endpoint(services, ATTESTATION_FRAGMENT).unwrap(); + let endpoint = matching_endpoint(services, ATTESTATION_FRAGMENT)?; let url_path = "/did/attestor/identity/respond"; let uri = format!("{}{}/{}", endpoint, url_path, key_id); // POST response @@ -129,9 +132,15 @@ pub async fn identity_response( // extract nonce let nonce_str = decrypted_verified_payload .claim("identity_nonce") - .unwrap() + .ok_or(TrustchainCRError::ClaimNotFound)? .as_str() - .unwrap(); + .ok_or(TrustchainCRError::FailedToConvertToStr( + // Unwrap: not None since error would have propagated above if None + decrypted_verified_payload + .claim("identity_nonce") + .unwrap() + .clone(), + ))?; let nonce = Nonce::from(String::from(nonce_str)); // update struct identity_challenge.update_p_key = Some(attestor_p_key.clone()); @@ -157,17 +166,16 @@ pub async fn initiate_content_challenge( ) -> Result<(ContentCRInitiation, ContentCRChallenge), TrustchainCRError> { // deserialise identity_cr_initiation and get key id let identity_cr_initiation = IdentityCRInitiation::new() - .elementwise_deserialize(&path) - .unwrap() - .unwrap(); - let temp_s_key_ssi = josekit_to_ssi_jwk(&identity_cr_initiation.temp_s_key.unwrap()).unwrap(); - let key_id = temp_s_key_ssi.to_public().thumbprint().unwrap(); + .elementwise_deserialize(&path)? + .ok_or(TrustchainCRError::FailedToDeserialize)?; + let temp_s_key_ssi = josekit_to_ssi_jwk(&identity_cr_initiation.temp_s_key().cloned()?)?; + let key_id = temp_s_key_ssi.to_public().thumbprint()?; let content_cr_initiation = ContentCRInitiation { requester_did: Some(ddid.to_owned()), }; // get uri for POST request response - let endpoint = matching_endpoint(services, ATTESTATION_FRAGMENT).unwrap(); + let endpoint = matching_endpoint(services, ATTESTATION_FRAGMENT)?; let url_path = "/did/attestor/content/initiate"; let uri = format!("{}{}/{}", endpoint, url_path, key_id); // make POST request to endpoint @@ -187,7 +195,9 @@ pub async fn initiate_content_challenge( .json() .await .map_err(|err| TrustchainCRError::Reqwest(err))?; - let signed_encrypted_challenge = response_body.data.unwrap(); + let signed_encrypted_challenge = response_body + .data + .ok_or(TrustchainCRError::ResponseMustContainData)?; // response let (nonces, response) = content_response( @@ -223,79 +233,80 @@ pub async fn content_response( ddid: &String, ) -> Result<(HashMap, String), TrustchainCRError> { // get keys - let identity_initiation = IdentityCRInitiation::new().elementwise_deserialize(&path); - let temp_s_key = identity_initiation.unwrap().unwrap().temp_s_key.unwrap(); - let temp_s_key_ssi = josekit_to_ssi_jwk(&temp_s_key).unwrap(); + let identity_initiation = IdentityCRInitiation::new() + .elementwise_deserialize(path)? + .ok_or(TrustchainCRError::FailedToDeserialize)?; + let temp_s_key = identity_initiation.temp_s_key()?; + let temp_s_key_ssi = josekit_to_ssi_jwk(temp_s_key)?; // get endpoint - let key_id = temp_s_key_ssi.to_public().thumbprint().unwrap(); - let endpoint = matching_endpoint(services, ATTESTATION_FRAGMENT).unwrap(); + let key_id = temp_s_key_ssi.to_public().thumbprint()?; + let endpoint = matching_endpoint(services, ATTESTATION_FRAGMENT)?; let url_path = "/did/attestor/content/respond"; let uri = format!("{}{}/{}", endpoint, url_path, key_id); // decrypt and verify payload let requester = Entity {}; - let decrypted_verified_payload = requester - .decrypt_and_verify(challenge.to_owned(), &temp_s_key, &attestor_p_key) - .unwrap(); + let decrypted_verified_payload = + requester.decrypt_and_verify(challenge.to_owned(), &temp_s_key, &attestor_p_key)?; // extract map with decrypted nonces from payload and decrypt each nonce let challenges_map: HashMap = serde_json::from_value( decrypted_verified_payload .claim("challenges") - .unwrap() + .ok_or(TrustchainCRError::ClaimNotFound)? .clone(), - ) - .unwrap(); + )?; // keymap with requester secret keys let ion_attestor = IONAttestor::new(&ddid); - let signing_keys = ion_attestor.signing_keys().unwrap(); - // iterate over all keys, convert to Jwk (josekit) -> TODO: functional - // let mut signing_keys_map: HashMap = HashMap::new(); - // for key in signing_keys { - // let key_id = key.thumbprint().unwrap(); - // let jwk = ssi_to_josekit_jwk(&key).unwrap(); - // signing_keys_map.insert(key_id, jwk); - // } + let signing_keys = ion_attestor.signing_keys()?; + // iterate over all keys, convert to Jwk (josekit) + let mut signing_keys_map: HashMap = HashMap::new(); + for key in signing_keys { + let key_id = key.thumbprint()?; + let jwk = ssi_to_josekit_jwk(&key)?; + signing_keys_map.insert(key_id, jwk); + } - let signing_keys_map = signing_keys - .into_iter() - .fold(HashMap::new(), |mut acc, key| { - let key_id = key.thumbprint().unwrap(); - let jwk = ssi_to_josekit_jwk(&key).unwrap(); - acc.insert(key_id, jwk); - acc - }); + // TODO: make functional version work with error propagation for HashMap fold + // let signing_keys_map = signing_keys + // .into_iter() + // .fold(HashMap::new(), |mut acc, key| { + // let key_id = key.thumbprint().unwrap(); + // let jwk = ssi_to_josekit_jwk(&key); + // acc.insert(key_id, jwk); + // acc + // }); - let decrypted_nonces: HashMap = - challenges_map - .iter() - .fold(HashMap::new(), |mut acc, (key_id, nonce)| { - acc.insert( - String::from(key_id), - Nonce::from( - requester - .decrypt( - &Some(Value::from(nonce.clone())).unwrap(), - signing_keys_map.get(key_id).unwrap(), - ) - .unwrap() - .claim("nonce") - .unwrap() - .as_str() - .unwrap() - .to_string(), - ), - ); + let mut decrypted_nonces: HashMap = HashMap::new(); + for (key_id, nonce) in challenges_map.iter() { + let payload = requester.decrypt( + &Value::from(nonce.clone()), + signing_keys_map + .get(key_id) + .ok_or(TrustchainCRError::KeyNotFound)?, + )?; + decrypted_nonces.insert( + String::from(key_id), + Nonce::from( + payload + .claim("nonce") + .ok_or(TrustchainCRError::ClaimNotFound)? + .as_str() + .ok_or(TrustchainCRError::FailedToConvertToStr( + // Unwrap: not None since error would have propagated above if None + payload.claim("nonce").unwrap().clone(), + ))? + .to_string(), + ), + ); + } - acc - }); // sign and encrypt response - let value: serde_json::Value = serde_json::to_value(&decrypted_nonces).unwrap(); + let value: serde_json::Value = serde_json::to_value(&decrypted_nonces)?; let mut payload = JwtPayload::new(); - payload.set_claim("nonces", Some(value)).unwrap(); - let signed_encrypted_response = requester - .sign_and_encrypt_claim(&payload, &temp_s_key, &attestor_p_key) - .unwrap(); + payload.set_claim("nonces", Some(value))?; + let signed_encrypted_response = + requester.sign_and_encrypt_claim(&payload, &temp_s_key, &attestor_p_key)?; // post response to endpoint let client = reqwest::Client::new(); let result = client From 1d9c1fe58411e88e159077269791b955dce29743 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 23 Sep 2024 16:06:22 +0100 Subject: [PATCH 80/86] Add todo for reading files Co-authored-by: pwochner Co-authored-by: Tim Hobson --- trustchain-http/src/attestation_utils.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index 6350a342..2e397ad7 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -308,6 +308,7 @@ impl ElementwiseSerializeDeserialize for IdentityCRInitiation { path: &PathBuf, ) -> Result, TrustchainCRError> { let temp_p_key_path = path.join("temp_p_key.json"); + // TODO: refactor with e.g. std::fs::read_to_string self.temp_p_key = match File::open(temp_p_key_path) { Ok(file) => { let reader = std::io::BufReader::new(file); From 00640f6dbe649f97f2e5f12202c4c360c3836373 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 23 Sep 2024 16:13:56 +0100 Subject: [PATCH 81/86] Clippy --- trustchain-http/src/requester.rs | 12 ++++++------ trustchain-ion/src/commitment.rs | 10 ++-------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/trustchain-http/src/requester.rs b/trustchain-http/src/requester.rs index 5e20e0fc..90742874 100644 --- a/trustchain-http/src/requester.rs +++ b/trustchain-http/src/requester.rs @@ -95,7 +95,7 @@ pub async fn identity_response( .elementwise_deserialize(path)? .ok_or(TrustchainCRError::FailedToDeserialize)?; let temp_s_key = identity_initiation.temp_s_key()?; - let temp_s_key_ssi = josekit_to_ssi_jwk(&temp_s_key)?; + let temp_s_key_ssi = josekit_to_ssi_jwk(temp_s_key)?; // decrypt and verify challenge let requester = Entity {}; @@ -104,13 +104,13 @@ pub async fn identity_response( .identity_challenge_signature .clone() .ok_or(TrustchainCRError::FieldNotFound)?, - &temp_s_key, - &attestor_p_key, + temp_s_key, + attestor_p_key, )?; // sign and encrypt response let signed_encrypted_response = requester.sign_and_encrypt_claim( &decrypted_verified_payload, - &temp_s_key, + temp_s_key, attestor_p_key, )?; let key_id = temp_s_key_ssi.to_public().thumbprint()?; @@ -244,7 +244,7 @@ pub async fn content_response( // decrypt and verify payload let requester = Entity {}; let decrypted_verified_payload = - requester.decrypt_and_verify(challenge.to_owned(), &temp_s_key, &attestor_p_key)?; + requester.decrypt_and_verify(challenge.to_owned(), temp_s_key, &attestor_p_key)?; // extract map with decrypted nonces from payload and decrypt each nonce let challenges_map: HashMap = serde_json::from_value( decrypted_verified_payload @@ -303,7 +303,7 @@ pub async fn content_response( let mut payload = JwtPayload::new(); payload.set_claim("nonces", Some(value))?; let signed_encrypted_response = - requester.sign_and_encrypt_claim(&payload, &temp_s_key, &attestor_p_key)?; + requester.sign_and_encrypt_claim(&payload, temp_s_key, &attestor_p_key)?; // post response to endpoint let client = reqwest::Client::new(); let result = client diff --git a/trustchain-ion/src/commitment.rs b/trustchain-ion/src/commitment.rs index b63a7548..b0f86dca 100644 --- a/trustchain-ion/src/commitment.rs +++ b/trustchain-ion/src/commitment.rs @@ -463,14 +463,8 @@ impl IONCommitment { block_header: Vec, ) -> CommitmentResult { // Extract the public keys and endpoints as the expected data. - let keys = match did_doc.get_keys() { - Some(x) => x, - None => vec![], - }; - let endpoints = match did_doc.get_endpoints() { - Some(x) => x, - None => vec![], - }; + let keys = did_doc.get_keys().unwrap_or_default(); + let endpoints = did_doc.get_endpoints().unwrap_or_default(); let expected_data = json!([keys, endpoints]); // Construct the core index file commitment first, to get the index of the chunk file delta for this DID. From 6ad52baee6914ab923fa896cfa93a5074bd51163 Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 23 Sep 2024 16:15:32 +0100 Subject: [PATCH 82/86] Fix datetime warning --- trustchain-ion/src/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trustchain-ion/src/utils.rs b/trustchain-ion/src/utils.rs index ead01dfd..4676864c 100644 --- a/trustchain-ion/src/utils.rs +++ b/trustchain-ion/src/utils.rs @@ -331,7 +331,7 @@ pub fn time_at_block_height( /// Returns the unix timestamp at 00h:00m:00s UTC on the given date. fn first_unixtime_on(date: NaiveDate) -> i64 { let datetime = date.and_hms_opt(0, 0, 0).unwrap(); - datetime.timestamp() + datetime.and_utc().timestamp() } /// Returns the height of the last block mined before the given date. From cc4160134c582ad6842cad1f1b281edc15598ded Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 23 Sep 2024 16:37:01 +0100 Subject: [PATCH 83/86] Rename test fixture variable name --- trustchain-http/src/attestation_encryption_utils.rs | 7 ++----- trustchain-http/src/attestation_utils.rs | 4 ++-- trustchain-http/src/data.rs | 3 +-- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/trustchain-http/src/attestation_encryption_utils.rs b/trustchain-http/src/attestation_encryption_utils.rs index bc4a9228..8127472d 100644 --- a/trustchain-http/src/attestation_encryption_utils.rs +++ b/trustchain-http/src/attestation_encryption_utils.rs @@ -139,9 +139,7 @@ pub fn extract_key_ids_and_jwk( #[cfg(test)] mod tests { use super::*; - use crate::data::{ - TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS, TEST_SIGNING_KEY_1, TEST_SIGNING_KEY_2, - }; + use crate::data::{TEST_CANDIDATE_DDID_DOCUMENT, TEST_SIGNING_KEY_1, TEST_SIGNING_KEY_2}; #[test] fn test_sign_encrypt_and_decrypt_verify() { let entity = Entity {}; @@ -173,8 +171,7 @@ mod tests { #[test] fn test_extract_key_ids_and_jwk() { - let document: Document = - serde_json::from_str(TEST_SIDETREE_DOCUMENT_MULTIPLE_KEYS).unwrap(); + let document: Document = serde_json::from_str(TEST_CANDIDATE_DDID_DOCUMENT).unwrap(); let key_ids_and_jwk = extract_key_ids_and_jwk(&document).unwrap(); assert_eq!(key_ids_and_jwk.len(), 2); } diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index 2e397ad7..40b964c5 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -909,7 +909,7 @@ pub fn attestation_request_basepath(prefix: &str) -> Result Date: Mon, 23 Sep 2024 16:47:08 +0100 Subject: [PATCH 84/86] Remove unused test fixtures --- trustchain-http/src/data.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/trustchain-http/src/data.rs b/trustchain-http/src/data.rs index 1f09d62c..55d1ae32 100644 --- a/trustchain-http/src/data.rs +++ b/trustchain-http/src/data.rs @@ -1,6 +1,6 @@ //! Test fixtures. -pub const TEST_CANDIDATE_DDID_DOCUMENT: &str = r##" +pub(crate) const TEST_CANDIDATE_DDID_DOCUMENT: &str = r##" { "authentication" : [ "#bZdi2pQK5dk6YF8uVKz_P7SvRgZJ6DUT1KcsLM7L1QA", @@ -63,9 +63,8 @@ pub const TEST_CANDIDATE_DDID_DOCUMENT: &str = r##" } "##; -pub const TEST_KEY_ID_1: &str = r##"#bZdi2pQK5dk6YF8uVKz_P7SvRgZJ6DUT1KcsLM7L1QA"##; // key_id: #bZdi2pQK5dk6YF8uVKz_P7SvRgZJ6DUT1KcsLM7L1QA -pub const TEST_SIGNING_KEY_1: &str = r##" +pub(crate) const TEST_SIGNING_KEY_1: &str = r##" { "kty": "EC", "crv": "secp256k1", @@ -75,9 +74,8 @@ pub const TEST_SIGNING_KEY_1: &str = r##" } "##; -pub const TEST_KEY_ID_2: &str = r##"#a9vxpkAsksMUOXqjAdnZhQiVOKY-a0QDOdnrDL6lw40"##; // key_id: #a9vxpkAsksMUOXqjAdnZhQiVOKY-a0QDOdnrDL6lw40 -pub const TEST_SIGNING_KEY_2: &str = r##" +pub(crate) const TEST_SIGNING_KEY_2: &str = r##" { "kty": "EC", "crv": "secp256k1", @@ -86,10 +84,8 @@ pub const TEST_SIGNING_KEY_2: &str = r##" "d": "YoSojHkEat0RefQxbzeS-X2JIW3BCJTgc8-VM6ombWk" } "##; - -pub const TEST_ATTESTOR_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JEV4WMgoJekTa5RQD5M92P1oLjdpMNYETQ3nbtKSnLQ","y":"dRfg_5i5wcMg1lxAffQORHpzgtm2yEIqgJoUk5ZklvI","d":"DZDZd9bxopCv2YJelMpQm_BJ0awvzpT6xWdWbaQlIJI"}"#; -pub const TEST_TEMP_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg","d":"CfdUwQ-CcBQkWpIDPjhSJAq2SCg6hAGdcvLmCj0aA-c"}"#; -pub const TEST_UPDATE_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"AB1b_4-XSem0uiPGGuW_hf_AuPArukMuD2S95ypGDSE","y":"suvBnCbhicPdYZeqgxJfPFmiNHGYDjPiW8XkYHxwgBU"}"#; +pub(crate) const TEST_TEMP_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"JokHTNHd1lIw2EXUTV1RJL3wvWMgoIRHPaWxTHcyH9U","y":"z737jJY7kxW_lpE1eZur-9n9_HUEGFyBGsTdChzI4Kg","d":"CfdUwQ-CcBQkWpIDPjhSJAq2SCg6hAGdcvLmCj0aA-c"}"#; +pub(crate) const TEST_UPDATE_KEY: &str = r#"{"kty":"EC","crv":"secp256k1","x":"AB1b_4-XSem0uiPGGuW_hf_AuPArukMuD2S95ypGDSE","y":"suvBnCbhicPdYZeqgxJfPFmiNHGYDjPiW8XkYHxwgBU"}"#; pub(crate) const TEST_ROOT_PLUS_2_RESOLVED: &str = r##"{"@context":"https://w3id.org/did-resolution/v1","didDocument":{"@context":["https://www.w3.org/ns/did/v1",{"@base":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"}],"id":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","controller":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A","verificationMethod":[{"id":"#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","type":"JsonWebSignature2020","controller":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","publicKeyJwk":{"kty":"EC","crv":"secp256k1","x":"0nnR-pz2EZGfb7E1qfuHhnDR824HhBioxz4E-EBMnM4","y":"rWqDVJ3h16RT1N-Us7H7xRxvbC0UlMMQQgxmXOXd4bY"}},{"id":"#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA","type":"JsonWebSignature2020","controller":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","publicKeyJwk":{"kty":"OKP","crv":"RSSKey2023","x":"EyGvw3AkcUf2TZToBh6pddeaaocmvTuLCSLun_yYJpL7x0W3gVEzeKlj06J5Sej9Duk0W_yGhbOKCahOx16LszwTHVgnH9FjRk0nwOer4yKaKnjTZ2FlZsYI0OI__jhCGP9cbcOEd-1rfvUFu-ghsj6oHfSXDBm0Ekplkgs1IktoicuMsF-bD7I6tZRpP9tqFGqARUqvR2daQN-scwYUNsv5ap3XakBCDvOCBc_rPAwzapY_nuC3L6x60UGBAPtUBANdaMhAU0gxd-3JMjcSjFgwzAhw5Eorr7bIp1_od6OfBRYu3sIkij5Es6RDBLghUAx2Z3dznniJRh5Xlx_8zn4SYw_xhV1X04vY5U4O7-7veKMqKxzzoGOR7O137gSTtBk66ISXfE0k6LLsZK0Qkzi0B6YQ0Xo86d-COFNhRWQ_Lq3SCSiOaJ4lFP5_RVlHzgUXm6XY1X0jrkVPWdT42VxGjFvy_KX9f50dOkdPJTax8bGv1nEpDm-55UN8nrIzsRODaxMBooRL1y4OxyW1tpHaEdsoHvsZrLzM5g7FB2ah-62TCGkPcG3Yx84MPp50eRPIlj2omMFxMpnAZKBSRMGtk35A6xAZUI6KTYGfNI-IuWKdk0UOn6xL8W3EwMTxRgx1v7iklbgxKuCBoOeAK7FhoOVzL5YnUCHb1NUwAxDs9I5pNmrvaXsDDLKLIoz50hRAdnK92whifFoWoJOOJbQTb9sx43zmB1J7G_T28MG6UetI4dZljoNfWpXePl3vNwW979nNg7GU3N_V8ZE_slRmUv-rAw9jD0w9KXVCuZuwGIKoJ2Co8qjZxnhZUtmi3wFJin73V5BC684ebh40fnA9z-H1Kwa3ItX_mQSVYeMV-_1fydNULsdhlEnpwI5XNQ25LGqMNb4v-YRBXLSmN5CituV9rPXg5ZzQvy8VVE9qxWnicCxz2TzFrxFOOIhNTxf-YQT5Re5HJAvdy7Y9szo-i_PgskFdVm4UxMgH9ddrFUhDPNmVtVY8PoXlMzuU6gKR-1np9J6FBttHOIPu7LFFdO0Vd_Y3-Dl5mdBXFcP1Do1GN7ojcuRUB4rmB__upRAQQsqCApGurtGP1zgtMQm6ozF0gt_JpoXgvZEFK5kkm92vpedrSfDPBBn5NPIgmQgKSYfvmWRmADyr2J9bc6EjJr1-YD7QR1r2g_eGRBE1S6dexWceWTq-RktXQYOSJBnKLSkbqJniuoA70BMkjU4Jsj1EJB7oxE41RRMchA4BRlClSi31ga0T_bk31rNTLQNLGSrBrh0x2nlG8IZUZLB4fIKKweFD9pL1qhLMM-SQl3YR4-v2wxjlMXTrEDjz2xdwJsQhhzM5trtqhVdxfgBwB_ZBtU9KJqYvkB_3BhY3kYQSGDLhyCHbjyIVYl7saQGkTz_owGfj8tD3gU9oJlZHDyjf4p9AObfF4YXKjVBpPrPgwgNd-G4LAgUOn4DAVwGmGBjQaNWiLet4g4lRsLS3LkM1az1w_KyYCX_k9bptp4qLgwV6HqbLx1V5WkmubxLMpHlbV0tZFLzwThEaKpqNyz7M5qIyDvaSbTFtQ9feXhRHU7VN1MgH2AQmQzHiygXHs5qafdGSsKoMm6c_6R2-NXl3asM1TSUmD82yKonGYhSHHy60KvB4M2rVTKRENxR93u7gaYr_4cqFY9LlcqGUMzxmm6TadfSHz3rSj53C8c3Z3U9x9ftbKGOZeybdWhYbRGyES_HzmlXV5MFY5qHiE6INi_ao7Xxm8VRi5rdaHlVDWfBb8gJENbUHDDcsKQfae-4j_vXmvq4s_9L5It5kVLCT9f5NEf7jsxSP3mg9hqgwdY96ob73GsHO3HRoQARhPUt-2o7i1JzScqRH38AeDr9XnxC2Qu4LT6ffOmMKzA3qngyxKmkvyKmIl3_eEhDxpdTSf2ba6EGOD2GuzvGv2a_P9QHw52mvtEoCLNJAslzsxwxbLSnLIOkbJca1Ew26womAjSgnNwUvPCkz4lmSNTbyF63wvmNJJeD0UgkBTb2MxDw_39ukWvH0mOSJegpmENWzMhvKvxxMgB5Y1VY6Hq06V9mcg4iD0AdI-dM646yU8iLfMAAkB-EvwUUMXRE3KGU9Kx6dqhsSCrow4QDpzk0B4FCATLwawfGc1_rxQyumhF9nagl8jP1ITcLi-hlUyrOsKfSK_s3WKTw4j9iBoBWCzHrX1YC_2UTnq5XIdbY9tT4NajRzqwKLV3aYWRnqXLg_-l5k0H2GmwmRnm4ZqU-9YuAy8MQR5CM93H1gxE7oL_IWIyH_tCXrVH4hRhjd7GrWcA90s1AFpCHhBZs72ORxG_Rh8VcJpB5cTpbQfk1ESme0-UTXoSnuLPfNIQb6I6fwFkIvBx9YL7gxaVmjHMgk9BLR89iwuo3VsEsAs4ktbFfZ70l821y6q_xmOBPF-BxJzlVuHMq9hfyYVA-1ka8tBBeEy8NJ1PlYBMiVjHoKWMfqDKo0ONNv1Il_ThirUq-MM4pc0ENOqwCYkomNBFfFHdbS8L1Y5yIruufFxRbRPt6xC1TnDtq3K7JCpRjsTqv_1_u81WA4UIlW49NaruM-2lPlL6P7rWtBqG4axy6U9WYqom7aXBW0cbg31hY39xZb49G_SfSYewGr_pelurFdTag1R3ZL5VuDTggqErrppxKIBYHQP7M_reJ8fQf4JcXOmMkUOap1K7QJvvENxlQ_RQRj10d-t9spgDv5gki7uMDSA3fp4q4gf3HxZhYwPaImQ9J44zCCLUdo5dyhHsyd9neEeBniNZk5LDZRfX66ERlj49CO2dHmHLe-YQACZnMQDDug7LF0il3QHinPD-nedAAxpjfUus9Ay9vRx6nB3fHr-_9C76qx_NjCehMZHlsAOgZGU-yjdwY2uu8lvnb8dvmCbkIBYn4S_aWJ0qIOEjfWuADwWO9BXI5uzQZ0EhKuhALABMhOIi4pmnHqCE0Durvn9RaPiFz6ZKFhW2d85ZAkks_-ARI0phaKzggmB4E6k5EV3cLqkI63Oiiq21QY0VCvc0LuNoAVYzG8s4bx3udSSORrRJm2fOdURg3wtPlFq21m_7y8D09xKpHkXgEbuDJV3hWk52u0Rxv1MTY2V2_LkHIDF6my-MZLQQh0dQYnUjDfvQ3bTqj6UE4MZ07R6UZzl3Vjw53lM2x4gI17Trma17Ag6Yg6XiQA7QqgXKWy3jG6AuBLjuYRPeYo18lJm00D1D_Z_C--D6zMJKr5ohYrTi4ea_dh3CI82xBNwjeTAd95r6X0wzC3xodd7FSWJMCgt0MF6pz-MEL_jNi6sK9mIn05U4icLZLjBwl2lObaoiYxpyWEpnuMGy8J7dM1Z_aRpYt3J-Zw7i3Yf4JI2JV9u1Mo-ywQyXgRcRBhK3emrFT2fxH8SqkKwJCWn7frvbukOzSQiKD8RFuXA-SWK60mJ3erCRnka-xkGg3AiBxxeE8Prk8EGzLcB1UDRGQ_x1PXmMNtdBK65dtv1b0jGTM_uSHFndWXOrFALwi66JGyIca2WnCfQRQDR5EPyD2d2Naecbj_jMwFUsbYCxGTc76n46c1pI_QH1rxDBQ7j1Tj_rcQz6Bk7DMTNnlTFhJn2h7yVnoRPenlNCWZWZPRpr4vnvS6Ii30os5W2QaGHI_TqhhaXRFU8Z7K4PUUUVEv6u3KIZpvcuVxAbcx-ppLVkj-r2vM061Nx9aXEBFd2whV1Tw2rjf-6fm10N7U3ssLGC6sfHRpSVcsENk-ZjuYH7sY-zmN7Hf8zOYHIAZDUr1rjCgG2yCujbdOPFtPs4QKC_cFSzbpOjRmJ-urzi7duH_vH3_TBhMzM4jowgM70l1LoB9sjQ68wzlaAs74T04IroWMULoZOdaeIS54ugR79EhgqvukrIDLEoCekAY7jAs-iNW14YRPrtdul8zVUjLd4I_X3efx-IX7HvR4RUp-6lqMSN46IfvlScl0qBY_SBgCpdEw66SRo1OAIAuTy7VWX_mbvLtgZPPMkaVheFwYwBZnBLKQKyJHrNrKRQ5GdrSnJP89jdh-o6VEqG_whEec3cB1LwXipXb6v1vi-7jxU4kpU_BTMtEChb21tRhmfKGiQxHbOTRJbHVoQJ4NFlS14bTYAEuJm6yXnIW-GOVCLvlHShp5jeWc_9vvvBZnk4C7bDxY80GxadNmsKy_-AcEFN_QI9pt6lckDeTOQxgVz6Anz58RIkvJ1oPL8A5FZOl4iYuQGDAqTP6Yo-SdHbuVOuV3aM9K3L6RMgj5Z9z517O3oqsmthQdy5xtxhalD2bjV4fNsQrsXIGuNa4nAnFtfsi0uN4ahR1_YYVuQgfEQLOGSzJnw-bQ7m8tOxlDOP4MsXg6BFSBvo0LPwieTdNbZR_N4FueA59bt73HfANTd-xz6ycnZNRNO9DbxBRwXJnQogguwZQdLLLuZjqoglKwi3gmMHvCR-3QngZYQw46vAkTUuYfdG0OgaYuAAqtsEvJRaBVSud7q6pgMqM5UbG9eWv20h-bMQeBEpIuVG08HOEc9TeUzDOoE87PzBkfBqVu_s1tyItQQ-DqSvfCQBobT1pYeVsuyJSGXuaF5MXooxYfRpsAuysjWDKDNxAarmMCpioPCo5ebD0elYa6S1KV52RN15vaAZLPqNRiFkek3oy_M8C9Fi2nLzXG1Bjn_JlKzni0I3pofwFNE2ZJnoLSVpLwVLQUzzCB5GoS5P5C1DcPDxpjAr7e8pWb0QAyyIuz1EvSssczBargovo8iNxthV_MgoN4UGY3RtkDRyw2DPcFdji7AYXw_q3xlxXsWEZMfjTlkG0FfwSTHbhrL-BIXXw1u88y-w5SvjBBwk2wW0SjPVgm-qq8yonWXhnVfu4xRLMY7qNRltkzyB5pQ44rJ0iFr6tXtKus3rUTx2PbQOPNCYJynCWQnA8anAlOiTmIJV8G-MYkP3hH3g-VZSnWE8gQhbvXy9OY4YtyqX96TXRGuHNuZBDEHiPmNAvKkfgVdGE1xrxPnfZ5eN2RQWXAf5a8xgISY1bXxlt1prbFSiHTMLnikDpYNy95JBQnPEqdIYRhgzh29L_RQpIM2ItE6rPrJCl-NL0Mo3YZNdFepgL-5uOjFilpmO_EfAc06pm5sP-g6S3vOx8I9j4JrOnhygXvZx4Mr2D8-R_7s2F5QOYKCpcYmhKSqaPbdAX-q6oNQQ3fesRtmDJIVbBmioMmu5k3C8hh_L2RNAe6ItXT7XVCo-QFQ8fiUIOMWASrYHiy8qsbX4kKQJ98v070GnqCMpKVtB9522SHxJWv4h6Kpsmadh9WjAmzItl4tRV763mNcLeidWzlJFUcfZIVm9OrWbHinBUjKFnoeexpecTm2ncrzpUkMmJghWKv9hUzk6wGkQhsps-94GvQJT2ou4T5xLpeATQ3oenwez9tEwxQ07tB7FHEiIBpA4PFExNwdv8sxaEe2Zaoakh1iEjIbd4uBcEAd_E8eE3VSEPvB2_zT8nek2I9pcHEIHA52Q2_j979f-vAyJci99RN1Va8nvk3TyMz_g6OCknUZcqkhXK3lqigvhkUBl-IxjWqagdTwPfwGPtwV3JT71CZDfBWujVMLPGB_gT_dhsWlIN-sC_yiWL_thQrkgKFPqXPwQKCyz8r_iv4f8NnJIh3W6_hUURFsnu0NpVAlhi7iOU-B0cqk1NHN9BgNbT_zU2aVBEFBrlQetG5pyxxgyDSvrz-igEzZ9oqa7-EIgNv8P-0T0IUrlCIQSfPsiAUsbExwg5JwdgdQ_gD9HUt4U2Npk03XtaAySY1IXJCXeJLp0OIcc8hFeaiPMMv7Caif9RsIxjwnikwLFGtpNy70Ed6CkTMtxBR4uShDzbSz7Hk90gu5-jV5WGysOA9AbW24iqgfgCKjrjgfrod_MNG939PdD9KOV0x3MqbZJmBLB7jKCINC2ilgH3Ez4crHFZJEkuJ_Qq-KDXW7l7hjHUG_debtAu6qI1edYP09UkgmQtnZgLcGAWUhDxWhdf4XYOHfqXxfhiVu8tF-ly7iqWkmRCqhRGV5NmzUWuwvQ8-Jlh4kRa7nhpwb7ivyXiDubq85_tKuha0qKFzzz8gFuiefICHX_Uy3xM8m6Gy3KfYirumMAkuB5-IY7Dgr6IZK8YXGLZb3QEXmOjuwp8Rmm-bMnCXehgCJZplNtcWi7eQxsP4y0IoEUsmmC5Y1as1sAs8-R9XlxBfP3hdGWbOupZfS6FmMRiGD9HoWesUSVtRs_tgOUPPVav2HRIK2CLYBRwgI1NaeRcpnO8cOye4UgRm_UF36pi3hJPfIdCnhxGeOH5J0r9zYEnTDs18YsIQedQOJ9jvGBLvDi8dJ3NRzof0hk9riVtSPV7H2EKhkEL67E5pccehsmZnha0ewYbZdgEstjzjwQ6qkZRmFLOBdP11yCDzgs3eDmnk0Ztewl22-WhhpumCfNgux5OEtcSu6hcC_gtsXQgTm4QV09fFZJAH8tyfFildcaycx0w6zG_tT47jBYIwVyEI-Mvv08qYw3ZN6558VgacYehFWake3ahdjDxZ8bO_tBtLMrFXmjRpibEIYbWZW2OPgBv-4-Z_EPXtLrDpJxYjD8bUxNgxwyqxAlyqZe0FUQVo1RTWV9hzvj4GcOG7wC-_t9aEEv5h9hg3sQXBxwKwIulPSsJlAeW3dygypohfIMKiUdjDERwhgvPsvB_vsJIaVpN3SJVfNWvMEFAIRxl0o0b4upYbISICcxav7YjxARlPcV_nqG6Lnj9-6MtHOzvmwMWpcM0Y_FFro9TqKAj8TkAiGaEMYyJ8Z5EMAsGd32HwMhmdeJbA9TxNpC8CIpeNlU0H9JeSDR3bl76oGAPDIc7bDmfKjcCL_8rZamAaZucmCI4Fkkjaqyl_k0TOHrxrc8EcYzbICfu2Xp9j5Bl_w7GErvNIbMsbJejezsJxt6CR71oex_OaL_DyxGJE6bOaWZFwF3WqhVWMoMEuRwy4Z11DIsqZ2pbxyArURVFG3mIHnBJ7ffjxYbofuuuw9Ce3S0W9AwEvXRlquPr3-wLesE-Y09JL2x63dPrsfx88itwaKSyGuJyvqpTu8NwpAR8d0bU6nXG38O2ysH6-xwvDGoeApjhGaTD71tv5hYcJj1X2M-GeWFi74NjG-PYBkamWVPk8v2uimVuB402YMgUAe5RtZcKVUfHczIcj7IWreTJr8JCLl4N_X48ji2KDuBuuaBRBUYdjkl8ltWE-AQzatqUi3DF2ZDEjEarQrk8K6QDaHNbMAEQwqxIcKVB7rX6pwR4EA2xN2VYmCskYAReAbKYyzbFKgx-_kbylwjO1CMcDTdhKYHnfEznxeaxzjwopfWQR5JQ_y_4OExcY6gh_FHXXyMOQdyzdcNMPFOZDvKAf4PiXg6BV6VVbvlssgImhEbhyfKlwhmbHkrD90BVSZOfwp0m_zd_xOfwSYckSwo8ef1K6DILkCmiUSc9wiCBBGHF8ex_0u3nepPICWg30NqJPii7moRYlXNi2hKgTB2Cy1njuP9pNFSD-8cOxrrAoAz6SaxdS4QqxjykSaRko3FibccYcSE_fkx7_WWBSW_1GOKTqQltkzHWMqTbu3wEjBAbnQjYGEWn8aTNzsAh1pezmZurCOdi9uL-cjIVavKPn23HhHGfS88f3pRdohcdlszyc74acnD6VgT0VnArfeYPNBWcliVDnCE3qYSvter4l5Fe4rH1qDISEq2ni1-uxNRJx6Ck3-5bWSZxHAgvc_2gC2O5qc9TU-akXvNSqLmNtKmO2FGFtBltwgyLc8bVWAJrNxuWQVCUxXlfSkxaGXtN18lGJX-SvmRn5IsqfhUitHzJjEASiI_YOVY9OoGEkK1a532FFGdO00mS07BQCPV0w_gldLncCOgt8VPaB5d5SjOF0_whIcVAIY95y5MrZEJWcbES4zg_jdGb5SRLlr9PENPbne9VYK4_ju-MCFNo0uWibQJzJcpaKU2rZ9sAsT2goR_lu-aLGCdeimhRmual5ISX_tyMRikPCDidsweqUeRzPcriSIRDKLcQfzA3P9Lt_Mo0ql-l1EX7TcwLgCsISBJ39jyhHyPvNPbBAFAlrlF9uRhz_ATonpUwgZrQHSlpsy6Mzh-O8f57HKQTRT0VigvfIeC3J1TR4EzLkHUdC7QF4JNlprKFQl-HUh9VIOpwXfQ7VwhbxUw-MThAn8fnFAKqd8S-4S76Yn4Ns3B0FA0wlDWp9AvfCSlm50bQHUgj8FEtwz8279OoIhBEIMnA_rHNwA1gPMSAl8aU4RO4L9wTbhwVEs32i77O1pQS93ZeNwOwXXoquAAVFZwusOXz2C3jxzKzB6IdrA9LE7-ALHDvmxB-y9KUe-RgCfFgjh9EE7rdwftpCOMj30we1IOtQ1XyFSwpbIK-y6e6itkyx73nB8UicYQEQHDnl2UPtxm3TLUe5bx_E0sisng5ZV2ISypN4_CiyoAbUPCapdHnGLh5VJtaPPq0NGIVA88MkPxnJC_dTfsZKzNVDywA36U6dGzcSH16QoTfJ-ZcUJhHAKJHizKtLpdxpNKlSugnNW0P0XwgrRYAehBBqJAWrmDc2vll-f5KYy6AFEWfIub9SODwuu3j3yfdoVAjpi6Tvm_e_w18ZBYKjtRrAAg38eTrwQwdDDovzBO6t7xmJkqOxsCFl0tz0WB7YxhVMfhC6qv0ojnXM4XrhX482Ew0yMUB9Ql2_2d7u9-aM7VztBqRf9dtPj0Fc1WdfiMD1d72U2D5NukpfdO0k74QL4xFcEWgq0qAPT1Xd35HaQhe9KfUYx0d7KtbBb1BrpQ3zZWS_ThLtfTHOvGZRQH9bQQyFkx7r9Lnal_GmnKw_w-Y5ecOTXwxvtB_XQNOo2i02MTPLpYHXMCWCFB6kHee4fhJVL4yQnaac8WOYkNDZeHf7y15M6Ezs0ieyusNjY-nfeAuXS1kJ_lf-qI-1xCpx4wmOy-W4Y4Xbr5YWS8Pe17115uh3ZGN9n88HuWj_fzZ0BcrgsT4p5LvSm9lntyD3oQ8pX17phhk3xqItrnJYAq8MfnLgifMDl6XucGJj1rhsvVGfr_ccjSHxohBb0HWL6g16xEvKsXnQe-PHn8Djtpc9doxqWWC1QeFnjIFJ38TnZd2v6S9irKu2D-YTw_9TvgRZTHMLgHH7pdFo2P_-mrKP74-OvYkn0O4aUVAZ6-bCXKIZ4ZzFgt-aO6l6vyUUfhcVrQKcnRdrZ4_GYfiRdxlBL1rvcZAkVpH-iitAdQ4N0xFHFL3MO3MH_EepQXLXSgciWBbbc9lzJnd4GkCRT-uH1SKKtquXZIO28ERVLB5yD9xkl6-ch9qTYNnNcBDNSAJQeFBwCHB5xZoyuYfN9p5v40vfSDAoJU9A_3_kaYMyUBVaxQWnKjZrrA5hWy2fjRUnVpeX7PDyAyb6eZDt7dKlkWGQxvhDXRFeN9yjohquhDj9OSS0JlHsPLobIYEPThAwpAYAEH9aspydpQDzH5LdB8aSUzTmFvdt87KW_OjCX2bAvPUj7a8bhfrITHuCUwOl_hNSIaxUX9EuHEifvRKi_KnQRZvkTyN6Ji93jcr1wYk2FOjZEVdUfC_lI-xzuQDSVWUUl6URvL2tfzx5FxqScbNiq3xnIqLrNONk-p4hi1QvPbgiYvXevv6-KgoCOBN5b7E0KUoVcBh8GBPzCeP2EZwA6C9k8u55Ul0Y6dohgm5HS8NQfXCSTt7QQgchGBOyOP96JR_uRbyLPJ18KaFr9QTxkQrxpuks_tWBdd9QD7GN2MU26S9veV2mrWHNXBiKY7NNZjYSkfNyzvjsg3VCwvxU9kzvkozJ_hQnkOnEmlI8bu34cFvYy1Ms4X5fLwaFLMmG3SnAIwBsCz3HxzKU05NBHikuB3B79BGskfQK_Fe-rkahNqJgG2ya6xgeIBivC2iuCuVjM1xcVN3jM0VuwQOCIVwjPpyDgWwjm5rpjX7LfEzwjyXynX5OR8PVugx7bAFwv0UNcbkBNLadJmL5hZfeXHzgPM5u8M1_PEpwxRddCDLbmbY-Y1naQwfaKRQp_c6KwJtT3IzkOJlaYsUlEeoLQKfQI-OFr7Jy6N9-tP3x_0OpecilN6J7UQLOTQEIeygISrIiIkSQgL8m7YCl7cRejrq3kF9UutkU2OIJFseVIFtIKZL92vc3WSxj6A8NkX-yqQ9LCFljVw_acJ9tUT7tNyOF7mFKBQJPa92WpaOGgzq4OCV2nJs4GFYjXgw7uE2NjQ2i9_auhXryGm3uD3G29NjUQ6Lkingi5trDZLCzoFKtQ_-2tWnf6sC4HBlShllmYDfCCorSX3Qc9WvEwxLbRvNX0CgPCEoxIKHAE9UzN9sfWZLD6BCXAtERDgNqc458B3xIrpXpk-hmIe-Res9HtuS43LqebcFiHjjKKiBuUEBCSxSEYQPYdEII9QMsBsp9IoCOKL7y6m5EgCfQzA7hiWLlE_Xrppv625MGLzebKWzu8CP1mOPWTp4FYwaXl6sm0rgbAoR5XtNLcBazT83ji0Qhc39dVR0nFyvdSe9L-EFw6dbYUPPbQDh0hQVzwnXZYFi4wgX8iFfyvfj1cAGrQNfx2yekQfLm-vhGK_sIlCRVZf2bjS6rwAbVIhhPFuTsQ5EaYCc3QbvJg-slvxMGfr3gpUkMV24EE0dCemwKRyRyf9zH-oswETPMyAFTQmlx715Ao-RESnFuc1Ebl13oTofrWpye9ZaqqsGko3Cimdifa716i5Gkq2FJNQRRRrp979uFgzdwm2AL3Wa_5I1t4aHY0hFNXzKU5u7gNmtiTDyLSOIWLGfd44msxBYFSE9YqSdU-7KpEtOLQRppx3FR1TQooT35XW13oPp37k91Uv2j8wLJPAid7msh1AUWmpGiq9vhair7EUlZhnjNIEvhlTr6sIwFzsJPRl9Dy838w_UqVXhKcA2wJpTCjgRWXL8R8b6L7Qs2v0H554fmrK3qcTm1BgmPf6d0aeO9wsgj_cSO2gI6HgI4zL6PUQTsMTzhIY8pN8MW1jPWVa89yWjGjaanxKT6WyzdkCGj6NcG3Yh5UoKGeehwa_5FQwggBfzXYMIAK3swXYvK1bVz_68c3eLtW96nYc1mnOw0QmcuQ7ajBPpwPVqQwH1iLRS3nEWbxznVbgvcdHS1Sv8LcVU8htWp9JheVP2OCiGQPFFScImnsLDC5WZxJNohrxFO6HHJ_6T3py6zz491E_zWqb0B89YapQO7LKc_D3pU7_3-ug2A-BmtjReN5-I0QAaNX86gN5o-LNW8yl7DmVU8rDBHQBV7vZ4uijVQhDvpifKk5mqhztr7B82gamJD6gUucjs6nA9V8i9496A3dTMHdtEjeEIE5zkvtbLe44WyaDxa5KiwZikk137DL-hp9w5b2-ZjwrGqcNJrYwpTQAjHigL12EWMHKEnPEsSXqmYujeWGfB2M9_VDmSgf3J-XAZroxarSzyVuead1XNLHtLqQgT0Prh-PS1lDJ8jH5y4_JzNS6lN78BaEi-rBl-hyhXqi7ZEzGEyZVB-H9rkmCE1jnuQsHj_iWUkZFeE5wJRemTSNTxF_GqZrFTkTD68qxdtMg7nWns8pXHaqDxpWAFaONRj8JdfPCeJhQ3W9qIdugEHXFlYYtZLEuXAlBGkHQQlnL2XeZ5aYE7xDC2JYQRJBj8c5fYfusrnqBgsz4EIO5ewfwmX-OAJg2d9Pm0UVxGrXtTW1H277sVslv-2FcU32cZwwls4YthQ6fyoIVLzJTyMOYJUrpFW32r5tG425wn_Q8ezmTs90EKuVrvVo8w92JL6MDKA-orDvhvQ3beb9l7Sgc5yy9cb90rjD-lyQBgcDfJ0xHFnhjnz4S8t0yga42xeRI3r_mXd0NvRzTUHkedNMtRAdU-W382jaFGRBxXL_4YziKyewh_nGh6BlW9EQ83Qf0oSwb43IN4k6GmK6KKvwr_KiERaBougue7YpwtYyqCrEoMiEEMn-Sog4CeLzg6IuYx4awivB7VYGGGwU6Bwc2IkZkKUFxVhJK63cAwQX5Gcve_j_-WcRRGlUhI9W4RvFhQFpl0YfC3cLUzRQZfV_fWH2MIwrJm6y4VCHhnvx8O87qetR0kM7el6lY4Nrk5bNtCdBeoyy_C1sz--DjsmM-z9i9IR8PqMCZcX3gBry0Sn_js4Ka0cXPsKpM-GpR6L0CLxge1FdKNDSFUOacsiEzh3-LTu-rUUYglWzQShuc8_dtZrIEvVocirTKZ3gaImQ1M1EylwXITBxzCUW19Io1X1mxKiFpXKHtzK7AvEs0kdicMBNl1HsKSn8OH3jxwLSHI4DwFIGYBxCQ0vvG3NN5ZZ_c4OnSfQ-nojlgmeCjMGykcA9E__NgeddsOdWxnG3fVQFIiMzoJ1AtYnxHoPRbtVZdyWB3dX1L9AKxlFep77w6KS48z70KzKseRnKLa6OCPZwfXgP5kEKA7FcKwpwIaMPNxCOedtULYeDhclbLeDtjK8LA2q7a8elVyK6YRvseXaZ4-nnd7iLYLZNOv807ZLaYGm51X7aFt0YRTimfsQIGztdkY9aakmyH_XQkqPmlNa75aE4xf8FqLjwa3AZ9PcIS8EpwX_Vw_pFA0NJcvJxCBgY4Iz98FxssnBRC9dJ1aAn4Kd8lgWvHIXS974MFCCGhfI8RRVDl4S0QO7W6vrGTIZB1ngY6VHZQ1JG9NJOGtomR_8RNH98FwcPzVNUzy9AhGeKBS3WECJCxk_gKjcGB-rBogS4EU0BVCfxzCoTMJF51ufpG1k4eWlEiEpOqUYgUWAN_3XYWNhphToFLg-h1xmQWWUBiVS6tV-XVvEOgKCKp_b8dMJ_99civ11moW0s3XQpzbxo02gCBR9LQYl2OPBcoRr1bVQfmS3sljBMCgtj5NodsMpz-rIZtgbzdchFe-RE6QK4qaMwAUY0oldGd7nIW9V1C3hnGg0kekWG3JKlxMhIB3IbDAVQ4jRJ90_JbLVaj8v0cNmhAwT0QwIwuTJJYFDGM1fYrocL0UKFsHEdPGZQFnfGAeFoMQwUt3I6zpmXbIqWA0VpRYwiUwTTRNTSsH1_eX-LWUnbXBsOmr6X38Sf9SQD2giVwmji2KBw4GSfRjUsbae5gpgZZbTcXH2ZF4FK79B7kM3RW1yKHcMrT3jXyZKjfEee008n6CJraHTc2sBDtV85wr-TQgic1VgACOfee02nwbPgPGhlUsN1e1cBwTGCJiIthec58AQtsEGIsqpTwh0axbKUmUaOj7zuUjDTg0imRCdYb_iMh8ya-YUncdYTabPkBJYlnbHzCB7aXmq42akqBQTTTgVgUsrRy22Q9gn7CkGltOZRbiPZ4Oa6Uzu-CYOsK-0JcD1xUgtTd9icWNNbAg5DCHh8FhryzVmRa5VUkC81OQryM3CgKdyzyw4xSH3qw2HcCMu7VHbHYhvVEXOQQtSaedW6w1shQMbPRKt0Bf_n3DTiyvSsfAgZmA3lrhQhRzd710dzxxljzkbfYEl3Q3SKg2CNM4Pu8SzAcJj9M4WubFMqDirRgVIMgL4xthq9u4qvIGxTERgAu1h7xhUcA9f0IvKiPzBkfExW_QIYR8c9kewkGILCplgqOHbvNBtqK5uXJrnscBUm-Su8yfc3gTiWWlsb1KBm2qwj6uXOBWQ-u4xyatyltsx8AJlshq-YB-K5oJuvlwCXkeXkU3hqRM4SRwLng3VyhdL0Jr5HUv_M1ENVemAJCR1W_6IXWxbChAYiRUFVnGQMCf2Jx46eQo1sNMaO-1r1LdtVSJo4ZELftKu2X0BMQC-l9iQ5EfDT2VEPZvl5JszWbqWIlkr_RY4jwbY_OeQCkPaMxE0eywBeG5zjdTYzmPLm0YjmK5J-_7tjM_678RIQ8qyuFPuNRGFUClznKIZ-T7SYMtFie6XAQ6j3q12Mh4-zEomU1jIOcy2EzZzTVgrpmqVtZUB9wzPIsNtq27VtLz231dh2i2fAfAZHdvIy_7XQsY7-JWltkQ-fY41Dw9QOIhDb_KJHhFNH2xa3g3NGh1WxZIiJNfPXXH2pMA0xU_FnJF0uPEr2u0rEcTWqTsDgHk4krHglASUYsJYneG_YgBCHWWrGXWzbQNGYsZryPJeXNcY3hw0wO49CxV7gb56BbUNBvNIfgS6SogajoeoPTkPQAICjtAVhnrgXyIFnQ38zu9Cwjwqxy10jt04Gwm1Q6xAh_CNQwcLgtJ7elaM7zi9uEGFskPfZHF35EOhpMwR6wBoPSv0ESs8PX1_WKhYSakFyW7SewR86-W3aCDR6xznTr57lJB7BnDb9_fF6rjfysDLSjofLGwjD8qC43OlMNZB9m868hgZoCUKvSnTpVW0B2NcAoM8lgXDox6cxZPtDsW65C2fMFUmt8yqLg9MOB9QRvr8jQVvgQ75GPADaHTVbcDukGOlpWsE8qHc0y8sbWnBRwGu4lUVpyOe3R-q2Y9DVCPonQoeUt3r6EfyIPeid7GaY1S-jCTuj5GlZA4Ridz6yYYZmGXzju_OqZL9TpH14-DvywWaBu8ZUqvz9kVamnK9P_M-jTDn6iz2zy37xyEGtzWT5Mv82avznCG1l0kSoG7HPg2kdA2ngIutv3-sn-D4_H3_Wzni52iLO-5CdMjEHyo8IRF2gsHDwR0mkF5uGdXv8RD_b5KZtgMy91QfiU-h1B1OTDWxxhfSPDO00EtPBW3UPQhkMJY2_MdHzKiG6i28PRjUTIYDcQjc1RrUZFuBmD6S679gKEzKw25fKmSbk6MBIhBfV1Q0h9uX9RauUq8yFRB7mV2EQgMRzrSZd0LVqNtBcOCU7TdrpzJzk0pZkfmjIVGOAJ37T234ICX4_M28IgaNiluXWNYvW8j7k_nTy6-8uRVw30AJnkQRswmxllkn8sE8pfxq2ACMG6LhiwkUeRJU7QYz8GMhtn1HcppGw27GGLZDbd1fHQ-X8EyC_pEx6wcSKdLWOZJ-TOqBWCDHZAJJ44G9MQ_eYCZKj78LA5pooQ1OQJeno7YefrhaY7gsJEY9LqHaDBBrDYPefTlMYgHPkHKxgkT6QtpbAHN81lB5uiiN-o2HPIgI45ODYY8pmvk7SY5BVsu-lJ0K3KZJOhOsfQsoK9CWB37yZj73eFNgWO9Wd5qmmiRVbUyBrjWSXc_dLnbEAKxB08xoITcG4hDIO1TSbTIF1QsBKXbyH11lwKM9Gr3bGckU_ni5H49T8MeAx2Cce-oeZ26dj5jDGQwwwgRbDf_9eKjzVzH0MtA32QPr-ZDqwIPJlpSAIswVKI7W6-TVHeKdYjBufEUoVhjsJ2kZLNnwsgUPySarkA7PjTLxcS7L5eXTIzBWpcSqQfY6eII492F_RPgaAzRnqRW7FA0lvNcCblQJoRK80DLGM_oZajzqytR-ZgfJvWQXY5UAcW0ywx1hVklrP5H9hxJBM6LujBC-bfK2gatWTUNoo7ciIWk8WPKZf9jCnGd2s9YQhwqJfIoYWLYZj2obHw-WfedxSpLOl72ucoXM_UvtvSjnnX18plcNrQ5lkO4f23N0gh_oZhdwYeyeb1N-KADIKIdY3_6tj1AFOqN_vXTuFtEAilg5YpHC5akZeMvfOGunAVza3qucicsRDEYutxcXggArT_nUZa_j9X5lp9EItKRVyGjBvRa8VKDwoHe0Qq9JYaDk2zA0Gqz2BsXKjxS5eArOJ4t-el3UdlFrsrGz0IIM53LsVDnYFGo7G8sQWzxQHD3LqVKhumuL4q0I6gBmOZBhAzzAb-j3dE8MFDXLKOzpMXj4yY_f1BqaSVhA2LxC9FXh8xlYclwHgweVkA98obGvKfW4iMNKJza4tQ5A1QDFPDwcsF1biEPK0svQmSnHNvjhOBM_hRoZK1YD_RXmIYPWzJnULt_2Nq4Fus7QlP0m4I7qSxDSUe3Ly_RtLefBaV3G7dUa62RQJfXVKgbGQTy_64COJ89TVWD5LIEPW_LRrYvSjVlsMD7LPexlQnh6J4g3zq0uRHxcWa1bDQDUQYrQp4Ud_6qc7d7FoQqYbQgib1M_MIbRyJezKZJFNXN8aZWzAkSjR6Luk43uWgogzv_PLON19AnvbC-eLg3fE4aUvJAueCiTQGGFkBb1O2IW1kc4i8wN_II3s1TkjQ6KSvre1kN4YMOTk73lEcC6L3NcgOd-o0tPDO2O9E6I8FG4yCWmnFPjPO1FFmEnjAUSgwhEs4KdKbQwRphNPnZQ6dWsjKPVM5AfmEiLx8drX7C2NFidylmW1dpC6T9L7Qcvd2YbocFGnNv3j4ztPjt-9Z2Y4fZq-02HVNkkuOO5AB4TdPTftjgiGipnbMaBmgBNMwbxkzHuWZ-avaQfSifAvfuePdugEVjmjhcS0NQuh0_hZ-K8m0-41A-EqQ6kzgfYTwKuQ8JdIWawuYoM1Q0G1bJGpwQxG9DPDB8c6y-WupSOZ8c5l2pWsRVw7UJ47hHhFIsoDHFHVDBT9N85Y2SIRbttX2pcnKj3nw7aj6ZcTRwpNPN-Qvu8YMMjMUVV0QoIn1CEyhim0x7jqidBvcSHLamlTSqYvzDfI4l9fSA8m4Yar_VZSMYMxls278D2sxVIEjXt-fqUbXc397qGzvNniARzqZcqrataPpzQoOM-bNj5LEJJdYPqSsHioJGOkhFzWXu49UuMFYUvyNxOhrbUy8h1N6GKiGDMSwe9k9wN-5WhvfEf3wPAztWl5R4PFRf306CPhL-FW83zhBr4c1UxU56taoVNnJtsblxuTTDJr8HgIiS0bqCLpL1s-ZYOgARzAgymuZCRdaxTmK4fdFhlTs6coahCbrSXO9Iehq58t6uw55hGhAqMjVvaRn2TpgwtHS2jvGMCsLFBYnkVXeeCDwA8uIEvujo_WcIUiT7STSP1IHMyllhlhU9tb0sD8wadR8caAgHBe2CuuE6YeO4qet9JIzOLTd3kJRE9Ev7aChlmuuAElJ0o-ktfVIvUbwVAwiWV3X6AcMlmVR_6HzhwZvc64Phapf84hPMYXvnIxBSI5UbvA0X5nHU2lnqPeRlhQI0mKXvLk4Z60WTgGrJoz6mjUQNep_zG1WTSkLwk4zlLwupc492MMc-M3x-vYQBmA0J2OfXEZjnuqAQ6az1hF9SaaF87c_W-Dkd5wgzUEkoUA2kjAfLtSItyltjCzxTnH5gGs7KaeoN_9V3bj_EAquWTrF9Vdr0DyN3fVdwrjU7oZhp_CVfondyy_VQO2wtxzBICKDcgraDmcBS1Pw_VPEIXvNm0ia52zwDDo6h53kRiKECACeOLLwif-WO5IBh4DZ_DFsiuaX1dJyUUO_7vk56KjmN0QEHxaNwpvKMuPtRGOMWkRAwIKezgkGJ-GRLXbeAA_1qqT0hLDsqJUal65fXdZ_J-qEnJH9xThlPem3WrWpAYKXeVOLOCxuA-7wxyxO2DxHqJdxsvzd16aErXTcIq7OgGXL14QQXLcpQIKermnxygZf06I83xy3pkfwEY07BVX6MnouU0ybMlqeFQgsWFnP_yjPuYGA0RQGOqsL_Cz_aq94VrHtzL1M8NTQt3Jhpr_L908QQMXN7kK6CKJnDkh9Rzykak8Lig_xmz8E42bPY-RWpAgAvpju1nggo6H4oH41IfQYW2gVzTviJq9EC1rP3FtJouq9gmSH5xDo5IW09XFskxJatkvOUIjgtZhCNG_VxtML1VdSDLZSrYjMT46SO8JjWJcn__4tR6gEmTrzRE2OSjbLuZpOksXgFrOgRDsZuPSeBAE8VKVpLtHvRQKWimJumFONfHJ7JxCOaUSBzpvk88Wg9em4x7YAd_SAChQoT7XRtjlwkRszQ-TwYfGsyOOGiTyG9dzCGGy_fsTugpowfedGCGBHJpuApn7cf5NNyLsafquuDtEyUly0NDpCwF2i4Dhma5jQsDEbKOlHnq8uzAkJXRe96IQBj0FWieRJyLU-pNsgXz2PqRxNXs__iId_f1X7avOZHN7FyBa-vE-u8RuYGXuLsUtQnnA0eYesQ0hCvGHa71I5E3-w1DCu9dLeY725SC1yVZ_vJ2WJmwEPXJIXKhVgTfvw8GIEml1VGxRFvb5kMQtGbXChL1tz7Y35ux-SRoX4A23pTZVEVquaXb2QjNFOprmA0tuFeYlsUdqD82ls4R1WzgzLVRRF4Z1Jh9AFgfYHqV-7UHwJAY0OpYK9iu6PPknBPAxWsxnLxyIxQ_rRnrbD-AyW-uFhBZ5d38zkvKw68Fr24Czq84U_OlBAvHtTWSzQa_6pc6tu5KT43QDCeWwiyWt1gdahuyoqGpJNgqyD6gh5xjSr1U-ahTJpXgVjnbNBkfOWecj9GK6CMLgvcI21qVrX2IHwG9kMyQgNmu--z0VHXt0WUtEuUcHMM4PzFM5AOZ_oxSVtIbvoYGDXjUgEI-xM7BOr4e1B4n8X0aoorefQhCLe1-Lv2pKRSeUlX60RlVuRN9GkoD_UoFqz59zJwL3h2uakwjt7iehx7DeI2pHUthZL03BqsYtJth9Emw5gsDKfBIR9BAjIzbSFRnnC_pthG2E1WMRMeeKThVkL_JYkmFj4Cr1xjqXXCTAI9QFwcTqRI4ZkRgem_jqVB7H9-BzVDrqgbQoxuWhNRn3_w-xfyzv_JtRcP150_7bEN2-gbBJCexcaF-0PbkopUuQqUjE3-WYKc9X9vLWcdkEehB0F7eqzdIWqRPTsnEat4SQhSvbaOp7EgY6Ypkvjkheer3fkPelAHN86SGviWWtaxDTWMBwHQjM866tuDKWOEnLQhMb_IjQDFKHrUKUnz42saPlPWfvbas8_Ymk7bX-E263Wzb5_MWXqPHMt6UTMSOtw86MTE46YEW9Ww-WW10cmatGb4jfoQHXa_JxCRry14AjwF7CmmQLP6dnm8r4_jm8AylHV8iKCG6r6csAhY1jQ3I-24iLu01EDB6H-_bIX3uiZDXpf4T1aGBJh7I7INB-Ad7d_IV7At-qaorPyE1xvTWeFVQLymsE87ZHY0J157ggITtT95e_Q8_SEiFYg0vxg89qBpuXygL2M_Pbrb5eYTCA6K6N86CxlOvFAb2AJnhAmxe8c_KHIsFZPL6lReDGQmMPBuvdCjjLPV7seEZX30ZMTuHYXNuD7IytEJ7X1o0_04eCmcqbivHBCoQGOzDhQ86DSoX2Omx-hmQl3hI2KgKnGcnfym2Ukd-3CmHAyCDAv2kDHm38H-JdcsO2DNk9QsYtAln6XRVl5kFDnWEhm9bRh-fg9Lmt_mNkwHSwZ0YrdYhAOCMkNlukUp0EYKKhBSY8lsY7a_TPbt8vkTMSCmi2sPr7NnuyaxMvw6Jblb9OD885lSOUp3oPpoH8QPkkhYUJ4-HVmmMGD8orSe0L3k7lLbyHzz5l1EmMahHWCCbnoMGGfO2QnxV4v9YcsMmIA_NX_1CjMUh_LYKrVWE2tfmhj7Zdprbop3nTylHV6YNet5h2MVUtpfj3CFTz-7V0AxKhqmTkSE9fMv5_XY9-QxFKf9B785SPTdj1xBiOsQ0uz3TJ2CPFHOtikiqYkNu9w2cUgYejqlM0crBDpQCuFmFJCFNKrfMa7eue_4H3RSh8Yu9Yw1LXbkAuGoFMGYhegcBEvcxcDSHfZ9f1HFT7IgimpuFuoGHwaNhPnlNc1uI1ILsFeRrrXide0q3L78aMAdu7eFfSSXHm-RcZypE9LHU8caoGqd0cr8hMAFvmAacrXiUE6RtzQUZjswSOziVVwlqyszgPXIuDsA4m0AcaLyEYQ8fEsRZAg7RyRbTgMGrlo-_L1Me2JMPPbiuNi2EtBXz_85Ylbaz45KQ45mdka24ouxzs3YK5aPi-Bv-fYL7FhoIWM6AiJH5ETjucj9KrhL5u-mnEi7sYh6ttj6I-MtSpCzOLrIB5HZ-tJktRhN78f2m8h6N4FBL9ooQXR4Y-QC1MG4eRlAiugn97K-r3MDGQZR5fVwC8SPW4Pt6UDvfaxXZek0HmjYPEk63MIxeMBOLaipBGR2ziR6YsoTUZ3NOopXjZr-UsGukdLw0OIJsxA-nGjmOZCr6iDgY-EfaCAVwAOxAv47u05VBTOP1xoUhMrxNefZ1lt8hEziCDaHInMkDdc4lQVeYv6H4rR2KugX0IXGsFc-C8sfQVnALLdQNjEg8_AfTsEmY3NqE_ECIUhFwxaW8s8aWBgX97Pi8SxkCwX6DyksH9fjA76rP4P5kpWl7ynaOaCfytRliE4j5uDXXywFfwN64DWKIQt4u2gDGo9d12CWUMGrWZZdn3qn8IgEDmUdr_CGXIGcPNuS-wxWoh4G8eGNhvMk1V9zhyhcxgbjoIJLl1T9MOZZ8JQVpiy-cPgClLI2jgIbKSVZTTZ8B6T93aQj5oEbOw87RZxArjYP2XeIHMNh6JUUOND97h1D-tXlI6hlFtFTouMxLzyOpVJLfdrUcr2p0bkbNPAyk3qzxwdRWegSWH2nojJVRP5dopYDUvX3a6sXVGUefUr6llKEtyQ9W84oVESDWyhWRv6GiBkpimAlkoolaGYFYCD72gUISM-ptvaWmVvNmXdZhR2JCSn3Ec5K9TZMg0ArIgFvnJeksow6nIwDSYZ_EXqtEgn9hjLaOcKZSrixLgvGqWY5phJcyYWP7kBsJTxc9U7xCIDh_RCU8fjZzAOAl4r3DtGTEntqzqhScZ_-Fx4ygPgpi4Ko84FM0RvNQGw5VSrOWADroETQVP-La2KyDOjYo4dTauA5ArmYnXyLatcyfbnvgE5KofVhMHwPq-QSV7QAaN9aM3KdDRxBXV7YtnjPx5DzLQE_61NLQkdC0iWFjHwLwM58comkNfrKAUw3vtLzWDiLHT1nPG0pxYBn0zAid0cdOFJ3JRJl2F6-GuMSeUK6kCqbX4mtShWXp1gn0YErlKR2PFjCDNj1o56a5ejMOYAB_SNIjRLO_O7uGofXv_Om9Uevp9XKu3ca86Qt6uOpwQsifkwS6j78cGRTJeU0SlIAGBjzi6b4aJN--CpFIqF6JpuZAxhiLzsHAXRAKik3Lu6Pmb_24KBL5_ktbQRcQX6GQjGi0A4gccSOF3hdJ9j1any3RaFOA1_0HRAv-ExWoiQEyUnWALcqaC1FmXgDTxYx_VUMjeb-MqxAV4eHjJsR7e1q9cJS8qhubSQbHMH72GccTJKlZYdLBHmc0Oqejf-JKgaBMxgkGX30uCXhT9B8dag8jVrDBemQV-wak7QHgbAveaWX74ZsZZF6ZuZ6YU1llAllJlLWPVNr4aaPj_wMfurz6YyOJDnCcVxcKFjBCJRuTBF1ACh9Ye1aj5wDUVwjeKXnjEy-quQNoB5c4clujc-G-ep6-EHj6WgHZefu1HYolZNprU9zHY3T_OrisT2jDBUByHv2RajGe3K7nDZprR-e1SPApINTcKQ42Fh8SfDQsXg0qOfvMdKbfKJqQizEQiCtvkQu1oXhlO8fC4J5UkN3qsPcdG_h1TQ-_zlAPDJ97B_92zV5NkIF3XFM2iQht1oWwZdN6xwKeDRqKmpER-qz7bxiy9Hh1IxU5T_Ac5c8B5xIxbQzgTJal2t1M-_cRvGT0CjpEBjRxqts-KliiGxFl48wNePKySRiGEfnn4Xfqmy4enbmmZgyHCmo-h--qxLIxBEykrcQurpumcrK29z2_jGUNichMpAaaT3UlzgVTbOVb3gVN3Qsu8ltR1RtlO5DM_Sc6q3GQ2QpdHafa2S8Z5D_A90PuohDCpyqvS7tA24KNQEKYM2W_ONMBNNEoyU2p7hZezbbj5T_HLHVRPUiVLgugGFQkNwZ5cRgrgYqstoKu9VJWFE-odBF8G9GwHGFFqyCdBL2CADSx9AnfEssP0TSarXyn-ALo1n5f6vpUFmkcuY-4gFSang5orkODd3k7hSmsCxs5NVMLfQxPtjJcTTrKR04H7xAVNnt79YJYVW73UaXEUammc_qu0GAuNwgeaX3wIQv8ieBeqJvGbfOoXd-U6c8b2xS7b_9BCWtTKZ1A8azUrXAqOr5rXlKkq6I31ht1XzyQAWq3_YWEc8MJahqr7bR5GQqOxRg_adTocY65i1qhxebStP6XWRRurHWyHzDhi9duKfGK_eC1bbuUIevXsNDHdQBDNE8_w1BBBlg4eFuM8vSDZWJEKPxvB4Vl7ciLOs6-diW3bj_JDo1BZlpdDQFKCwDuk5RtRJmr9hGUaIbF6nrjbFduzQFh6laU7VkD_3XyqJ2C3dCD1vOOhslfiVG1fBWHpTJvKsgfLa0u94IUipo6YWCz8K-LCeOymEufdrfaI1A5qutL6tF0CaPl48rmLRMayxqTf4ZGCCDe49C74wOS_kGmxchhr8DKGUgKwiWJWQjIQLIk2PzaHSQ4cE8uBQebBsCMzlrzNr1YhYzvzhje-qorpNcwCluQeaXkqp1WST9LbExS1jN8gmJhLgS8yAOd_yGdJchugXdbfPXWD_R4oVf40bCAv3HBB3MxQKq8dZeXg_9xqr_bhwqY1oUraAHLEol6kUS--0eDJ9PzaLed1ZQ_6j-pHR-mu-OkQUvtM-THVLuNMKWGSYKcBnOFYw_1NpEkwoWtcYCzk-nq-aHJ5XnijDKutRPJQ5W6RLMmhB8qFoZpRp_aDS5LJiqp-Q4g2QhtSCckgUwHN5GSDTLaYvjkR5jeIDI0Df_tQZQv7BiusW4M-iXMunM3qpOcdAdfnBTmODqjdeBAk4dRnayZtb2Ib-JKl5ywa6WUDhpA_UQA_sIlBBbTjetvlH2sChS0D17boDPANxqPYQLorzUflL42ay1DQFsRRdnxTiNvzN3nMOxzFdIUYqWEiY29KQmAFyuERLmtWNxvUB7KB9WqxV21mbJ-yIhTsuUTHve3HdcJuWPzEtbZemmvTyJr1wckTGBWVfeT20e24dPMpBbRN24Mpx_tMxfsioxNsXFYqKHzqWqZ8Tp-gj0TUMr-dATGUJHHQ2Un1nVUYhOfB-G-cycBf8zmgcnA9EsKkTOlZY1LRmvBIknw6thweHCggBJ8Ke5N7lgYjdTTPs9HXMZk-YcGJ8Q-TkB4_Dw35xq9_hnncS-Dl-_aTs3FD-V3fAbAd9eYbttpwk9kwVnc3GzF_d-eoCntwtxNH_iYmdeBZIqLZAoDwzvFnGfVunFP4RiUtLYepxu1m7HLhPSCAQn6SNcLwGg1U0jQpfYIYGZTL3Ntq91XYv3J9vy5O1apgQZic9XEMxzOuoYf0zDEU41PaVOmGv-H-mdrmH-MI0AquibmsDkD1GoUssNDqsqGVBgMMp1kc3N6irmLeIpdrSjOLUsW8eq0YGWoMXXxp32wIfDr1fad4KV22Slqlrfv4RC2v15WxVI6j8Cn2l6ymNxCj95fk55ibBk8IgObZEwbu-O4F6focQnbqXcLMSHipxWVOo0PNAnxeG8ER8AuVaimP1nXVWhNo77VuX_Yat85m9l4Avt0Q8tR6Rpqruw0cxZRH-3GRk97-svz5QsXMJgNZsDquzmeRT7ydwFrr8NK2Ei9NmlZ4pziY4xgIjVIJgIhgkY2wEH9EBDPLuqmYrA9z2RC4KUg5aMAvhRRZ1Jrxd4uv6C7iq9o9x6AOVwA3AzuM-A42325s1cNlnURin7VjQvoDg03eXsB-G-iSEUw_WoiFatKsO1U8bW4GP1-XwaZMD2w9-NXF9JCCGp2PaYNl79WZXpoNqtOv7CS-USx0vOF6DLllVZebsUhgMTBHg6I7dmJShzC1VLrCV_XjFCVlxfSdC-HkHceCUwQwQvkH7CzkW3Xxqn9onVcL1vMKgt-D7ov_952u8jsS6gkzEkUZgSFKNUMJGZv8J1rhg-ZNUi_50EsohJTlxy8H3xw8RFN9JsTZ7T7_O2yJ-yB5bCdSHldOwfQWtPvCw0df7yzUQtkMqMY384QRdKraWO3CwhrqD5_j-iqM1nw3AKDnqvUZ_pL_MrJT5OwqvaQLlIJpSymmfw642aXt7P1TzzFnwOYb0Myjc0geBp6JKLB4MetCiKUxmYP8M3hiH8FSZLv00jUmVJj-CPVj2IVml-IiAPyPU45_2W_Sek_l6JDqxgviPNU2QfLqXLOgs7-30-8ZhrtlZLC1AYco0hIEyVvFBQC5CjorAuillJuZ02YU5_kNwGG-Avbqb2zLhjw3gO7ZB1Lz68cv8F5YVsUvCvMgRhgpr5Wj_5uFtw23HGXHKY2Ejm3Kjya_Tw1EbrPl7t-UYyUxZkF6lUh-ZnndeOB7RWVO9lDvW-kuu5XuYFbAM6ouYOPd0Am1Te__qnJe0cYwKBaqopwTCE_7cu9EH37OBm3YWyGrthggmOrcK9jSI-xA40URX30vYvyuvNzZ-0f8PrZIfTtss2f0w9om6vDpwxsWhXRlTyz9qc0ntEgVwX6t6xWklLasPIwXZpahtO8PAA9Vqy2D3t-nMSyeBaPMhkZi_k5x3ckiLR9RHH1OmiAyYkGafn1_aB381MKMv_8AS4YGzeAvaHBwwfNDBlPpBhdupAGXoGPKFCM6d5W1QoDhwQyIZ9uFKuvoPtxntY8MwG5x-Vwmg3GhIDiSmoybRNIpfIqXUVzg5_a9p9b0-Go59h9B1ntMB0K1Q0X1EtZq-tVRlv1MRpSjOl8LFyGFQ8rYS0aY54cZgE_tdOaozg5NuXDJPQR515WrBf6NyJ2E66D3u1Fde7hd-zUMSiASQXMKwCLOAMNn4f3MWoj6UR3vKPjtBNwF1umNrE8P1tErywv40kYGz8-Zy5Jub9dMgKEfXbz1s6XIqZJEDSXngwVYNQx2fhaO-uGxt-eahjkVAkt1KoTe3sDxtkX7CFQNAaVBlsy4JEqRM1-Mxg0GfAP6M5l6MMhbqkJoN4oC4TVUlASghOUHqkCorULtgKctw01Ea9UnPzXz-KKpA4RllrWdUryiRH2A5RPs3KH6mTKVjJmzXvs-tHHeQphSLLm3QV1smoj9Z-oAJrz0C-f_Y0LE4Rsaw8Ag_7G9OOrBOD1odrNT2PbpvyeMCv2179maxKeUB3WRIU_Mz8b4_vi76gODzX6t-K5zDm1ukMlpNLfRtD2FZOEu2S9dGFFy-Ut3gB8Vnu_b1wnzETDDqWZJ-6bo9qRxrRAkH6q3TF5VTKv_hnYKY6QzcmotJrdTNPQvwCztcqj4c45FtJyax2tdOQo4lhoqDapMA9TawQMxunVToG8YmNP1YKJljFq-ZFttAxcnIpaTYq9scd3cfS0S63cnjaMT_H_LEBW9FedIR53Ko12fyQn9cLgErigUWMWwgdTmE2rPo3ygRky06cEcrh6zUtNb5E0Xt8FnmR0n53wZbJHsX9N6ficGSVwanB9ZBGJz5TmRHdF2aE6NrALFCVLZ_9mUP0XVz9HSUH9YbauXqYM8afLJ_R8XNm1WtqX6gWkCG4HulNtWURyTWgVuQT4jiB392QSDulnwnUnaFiroMxbHD6UENVgg78icspfeRQ3I_wEKLpCmngQSDvgNlV-vzVct_920i-n6DSDav6Ez6MgxCa0cgrF5Fbzak-koA7olgU2xqiyoAFv02H76alrTcE6Ooi0zNIBABz8McKSqmJDhJ3RTpCYQCmJ71Xq3xdeT-9-WBX9QgNEGQ9BAcZNT8IHY7yUocfYNOQS3XbCogSc0HR260BC8-8ijyyx1RfZB2kErTGpUCo3FQJLg8QNYU4cThUe1rmgzC1aJSHdYD8OLKHflJCHZiGGaYW_MA-tBWfHiEISIUcIghjbVjF2dBoMZBW5hlzvYWOV5y1QXW0zvTJ1Tw4R6kJGWNTK4wePkrh9W3t4wMu2QvyJQLGGwb4ltSDWefD44MtkWdfquG7OTbXqEiPr2KreJ2j3DASXuBDBD25RvlZc4bhLHFj9BUJ-lulsAvDWKCb2Bou0i6akOancevmmSZUwphs-hQM2b3ugNTsgsUEoF82dXWCJ70gyr1RFBfBsZCYDMDWbiqMYC221y5Pw2zoHRdQ40xDVCmTzDZZxzBr3ywIcE0Y_6c9tlm4e6EgOkdHg5KaAV9sV_uMLbBeSxyihQgJuxA4dzQnCo3Q_owAGtnkvhQp4UgYlx2AeclHenpTuFb_t-BsO1-DV6LgRplzfXH7ocQedgUXsd-gZtA61tnwNR2qRk9dbmtOikjI7qf7tFv8r0pRbe_d_mNadmgformlLzAtUn87xkZLmcMx_iH0g7gW7gbEXnkKmX9syage0xeQ12qnGvGF-p6mBKFUM7d_8ZBFt3pSd0M2Wl1zLnK9HQJVPXjWWBf8r9UecYdpyhtZAnxREWSqG1APYDP8cPpQcewy_QaCnVqyYZRFkf6X6ch-O9sJAwzR4MLElaZ31KyCxHTj8565hGC5bJUdg_I91UgH2yJArG54y_Yc5Dl6ALUn9QgPzbqDFFUOJjwU5o9uD2XyEBYzEErekT-GqxtSGOgCFSStNay_o8OmjolNWZVRc1_aFeMUOgh_GJCAnBMs8AVNU8rG-2bL8Yn_08Lfn-QpqpZIZIVsTZinG9cCIy-nuGGUtwHtPdG8xntWD7d5rNUtro9BCoxdrnbFOkSAwCQ365HHDHG-D0bnxTd70UQLYZcAb6rkxFrENHGBQFl5f1sOWZnGhofb6snJCirTWsgJcst54Dzu14XaX-57i-J3gi6pI0alrVQhxukhTtV3oj42A2TUGD6Qb2P_PjwhVbwpyfkd9tNTRT4YKbB6v7FviTl7JKRh_lMFAeLiNc10auLFBnXOdq28pbt64ilr05QoEABo-2qj0w1qRgK1RfdC_x2WRHcrI7zWIyDONsyqumIklidGqrEh8EXCSg3a1PBLMIrUfkfyV8C7LvTL_lifHl18bZO1BJtoksrMcCmPiwEJhCCMn1olm_DSh1YHahgEFrP9PhmLrFpJrymDuzXlWENX0QfqD8_bsiaIC7sqi4ZCnGI-KCnePmdiATIkO1ROI0ty_1kRce2LFztuwYFLY_z1yJlFflviLtyjU2z3F8Dl5JjO2dWm4n7bBCRT8wAqp5eztDZdaiuQUZKi9vhIuEnqFpL5zQVTUlDpMWodeYlcEZT0pQQamulicCkRslA7Z-CThZgOW3QWCv3eYTvOlZ0merHzQFxYq-8S_0rfwK9BEA1xck28GdMIXUd5cqBN1kUPd06qbwbCAgVBABucXvWbmkCeokCXOyfxb2BHl7381ZWy3_U6M0AnKzxhtYBSmBjY8sQAeJg1WTQ0ZpbMT651_b8ipPHAUl57j9rwVzxrdtmtai0VoUVNv4UEF6gDR_byb09xWMXgCWHrBMbbs7KNNC307cI7lmSHDwFDiWjxXcZtGMCix71kfh6uZsRBursMcnUoIaGvd_Pqv7SKeo3c1DXs8d4yraU5VqtmvHuodSmfcmOCEkzLb4lmVfBZPrsJQcLb9xFH8wunqxWYhr2ERzOJDZoLIKNwQnPDcxoK7UX_tLfbHKAO_CcfHWRgB_NkcPVvf8jViQRTrskD_19WqQFq241yN8yW4a61C6v-9og8yJyy8BWPQdiKESA180YGsfujYRx40jXR1u0g-WgRF35S97vOzm963EAkAmfCPBpRckAFxeDcb9DfBvhihOeaQEobt9UNhiDTNaiSN_Hl66wA5DIPIptw0_HQQLoVQ6HUevZymcwe9A5p7_AdCf86KBN-Z6cu7-5OTmctbwROcfjMYjlJLXI4vSE1fY_BdaYPBvPWsGaPKTNr9kwy0RyDrYd4a3hzDBzEOAGUJm14pdaOSbjtwoIJ0m5TeQRm-e-EBqxv4dcABhod1agzhWgyKZarIrtkDhGW7dkDqSdxHzPCxphtD1a7SD2MdKfz0IK_IkPRSr5N690e9kBMO8r0MmuMg85Jf4vA3w3-ywnIbaW865qXxkW-3CYgJ8RloGuBcJewQH13Ozoz1FAlt1Gt5Q-uHiMokLpmbCmvGVk7xPXqDu_sqRhQSjlEXRBjmGzeotBxxhTwmzqZfJxRXEdmGAtrfqva6gzYGgSdXFWo-_wfN2-DjBa1Z8FAxpmT-dRPNvaKwOmknS-tI5xi2i7kzmh-oIn8n-AJ6WanEBaFc5vTC9SnQNxnjnnbTu-bRMj_KlXXpw-ryvlGEGhdMOqfcgSWzQLPBSVMJpDU9rSZMfGl77Q-S3q9mRfjPnd6TqlNfOskpiQijqlKNvhC_D2S8SerwBOrWTSZ2i0W2NKgtAvkgn1v7wHkNIp6iJ9CU0mXIobg1uDrdvReirxIxuznqXyf9xma99oqKmQvh4dWfhlQH-a8AB1Hl624CTjEs4CcoZfCm2pMpcDie4gVvQiGkHQosnTdOA12IX3REq8peIyawJpoyI50ConQxCFuWqKfZkxvaLMfVAHcpvRNrNEF-jD1lf6R1emRB8jW6iQLCKYVueF6qfUsmb6Ql-gmKcakkB71QGMSGTa91eBg--S11MB79NFQdZhQDpYYc5GAAKTR3PF9Cj-xk_33qn0Xz3Xw5jRTZqm-qVcqPMwcdxcB9p8JhtWuhGcfyGmON9hM83JHg8xKGUn-1qPOnvF1yWoRcI6wv7Xe3jfo-_RHLEwbPTbihfw2H6ycYxEl_iz9zlG40_WNJwwWDdHn-jsau08fNxdR4WC9FEvC7lRAUeQPVxUWE3ziJjlDMeZGz2jy4daSi-LY-QZCzarHtQ4_olBcW11Q8gtV0lOBrkATxbd7YRAL7_dh54Xw9T6X0O7TlpofzzAVMZzIn0iTai8k0eAzuj3DT2FiCHAh4-RbKHr7mzyrPQ0MUmJp2PomCnzG25BUbYSlClBcjtotLGm6YuDPzB5X7Lu_vH9eRjxMEh7ZqIYO6m81D0dwZO9aVZSSwa_LBb1iBFrHijTsL8rHXXcBSnp_jIaZrGLyKkxMaJDegmLd8HdgACP3rOqVCDg1n_CVE3_jRaqwwHJVpani_j77aSGBmItjp7HqbcgZr_CVMCBHX3XfzlhuXZkvBoc8ZaYYifhvgGFGEg0jHEaxIIU0QDqm2L6dHqCH6yAlkkT8zRgWeLH4Pey8nR2KTAZP55YtaaU38cUPOqVlvTmPihzfNHH18h0vLfaPPjA712C9V3hvVACSpU5SsXQU7NfnnIO7_5ZcX-iCaEuDsSFlJcAJFaSyKJh5kcXsGdRCAM5nVfyH6_NFHzGiNWaIqc-E3Yl4a4pS07bpe74bsEUrxUfdgmY9XULfNwuGPVg4qBsSoS8coVBn5SxwVR6OITKjr8Iq6b8EZZxxc6qJJe2Xd5mExe6NxAW3sClorNhS_wwcBYwj6HUH8SmXpZ0xqADYVqky8bn-pa5j6RFNSH5zz9deI4_1ioLhkVtvpbRFHOxCPzm56wjqQnEci9QQd8axmpiKgHP8HnpTzLHO2MgqjjunSox4sXOz_BEEPWghInV_VpmFb0KN0B4UH_M0f9Yar4O1unjCGwlLF_ZfLfNfwmi8JoDRMYIyFn6D1PxQgdBBPKN0oC_Z11E28WQqTORvTJqusVY4qoZ4d1FOkd5E9srOWuvs0gBGweaIzUAZHdRGr4NygezGmf27uWSos68ZHaB2qOc79z_TpsXiVeik5uT-pSbt2R-GEIeg8cwCH1J2u7UHsWLmJFyUmBW3K372QeHxoW8UKinTNg4Zy6uF5acVZmom5E8s957-83Qcs_unrHFoUTPy_KWoiqRefrQcpmCHra-JYSYwNxfwgzoCp-EHgl2ypCIZ5BpRQHgKweWJWeRhioSBwGejT7evYEl3-L_FazZFY5W6tKyXFktO2jIySP0NMGxFL8S-PWQERH9cdm7l1KN849iSIqeMI8cROEUCWjUIhdh9pXJnY8vYhQBfbEjJ2fJFjOEtT8ARZe1jBPNUFdoRph8YXVXRkHn0uw826uIzZGnacbNgRwgNdilq-j1Rj5iirOQwXSQ1s_L2Y2Gl8O7YZ_tuEek0ovZnebzesmYKtoY_XhunbD_U-4afK57BtBTsmm1Ed_AwfhZNV_vqKC5DraEE6c6J_7d1f3NJEMVK-QDm-iMLGdLHjOr3bf8TjpeXNjITXiBZ0kJBb_qf7Y6Sze1UueGWd_23NVi5Ufe8w--C9fE3YT0Hl0wnSRJ1WvOGlLQf2Hgk8KaazMuCVbkNFzjojCQ_IrmsEz2sbWOSMDB_E2y-6JJyET54mCpfMYhdHXVhtbAH0sdBNtp2KGfh9206nOJU-lKwjo71lgNm4XoWV5Ux1LXYSeN9r7BSrpirkFIqxyQkJez9Ulcbiz5ES5t8oaTwCOnIDE28Vy324HhGPSi5W2QPkCOV_PjOWCeM8yjS_6w_FnGuO_26ecaOEkCNBZung5p0pHSmD9D0SeQ55YvwYvwMhT3smiwDo9dRcFa6sigkWHHKtBLW29sYLB4r5pNWtHd6CihJCcG9DTTbaE5qP0-eOF1l4GKEhtIUKDPGJGwEzYHjq9emeIy1uacdIcWTCJylvCVOHdWmLaD1HefI1tjSyga1LuX-uZPAYEu4H3BHd_8RhEhTIIR2W1Zi4pcy___Mg6UnxiELbieUU9M-kBKnEG8wm1_VCAJVg6GulXQG20z5Zq0Zr8HsRUEpcO6ULm-_3zF1WYWSPU-JDi_ZiKxGdLOidzU4gb-zzrrLYtA2USFwdncVimCESLHhKPSvv6r2xX5Hz0eTuLmhshN4wL2du7QNz_mLVnI0aIGrHWQgs_DEy06L1P4ANm_Y-0xdzookmfICUGKChRsnNFH5Ardfg5JWwzC_jQrW1XM_t8g-3Hnv_A-UzUyJWBl3ezae1NPikowsbMsIwLuHHteDmQmqb9-93yiUdXB9FxycWFgaPksF17KxTvI8FS2PPwZKsSOTXMQNCQyFd4fJDR60nQhm19DhQImTl_QPvqibTAg_p5zlhxlEFdMKoMEdSrqovWF0mKoOLbIHlGum-tDlq2Ll96PE2-CrnW8NyHVDdew8iZSZ5dahyl3prZnh_EiRB8nNBESy8uH9ppuSH6XlQ0TJXdhwI1ZdOJvFonZ-7IBR1TVb4ynvpzRt-oWE-tNx1-6qwSJGzrsKnn1EYkDQaRj7nfztiOa9af0LGUR5ejBaZVx-bQ-75PO-xBTxd0UpI5kyaEf9T3rUM19GzASEzvIwPCPRplhpopMmPORqBqg1oFxqI9vzahfzntnYmWEBLGc2ks1NZWq1gLcSZLw947_EEGgyqw51cFGXLaB1DeA85qa6WT1jRmS4Fjj747XLPynyNH73NU8RWsx03F0y_fvUpPGS_vaXWR8AhEy-gdBW5CCYbsPv7WB1Ls0_DJMBSHylHgNQvC_5knHobolZyERyyye0rwmLca0TnAJS0QhgywEwaoateT_H3_aqypXAFQdqP9aXzDLINETQH-jPND97CG-mhA5bh_mmulEvQMxHyt1e4d2IWPOJjYUvSj1gaxoNl8C_v-h8719rmYl7e5jedHHzYQuDgq-i4B8HlQxgLycD2vQqtt9F8fadudBvjaa4qaHQNw_AZc_8aWNUQ23FdSfC2ZSwJvYASGSz5iwwZotTwF92WMyzfnNvdjFyluEZR4D2RXnYP9GUuwGcg6LvtzjZDq4GoOG8cZEqgSQpSUFWN4-NUVBrb8GLY-SDo08tW7Q42PvN8h6h6cPCpFgrKFrqEuNupBiw_GvD-Ihj6S81070U74EpW3yin5jY5dVGJO_Q-8GBVsyfe9VyPGlDCt9p2-FwvgP6aMZnWAQys5HjDo7QxHaLXAUAJEB4HJatbd3sDYsC3S3Py-_NDzA9_JuOI4iqvOjwf96mS8xfOkoDY0CyKso6cn7BWBDbtgGL5yjjAOrsgyRzALWaUehhq0p48D45hMtJh40lBfgA2QkEqXaqlFdooXKlfyn0nePdsQPYJWxg4O42Up_ha9yeggy_bdTtWJQlR1bpgphhsDFFhPq3rrrD54e-AmMPvLS_KnhRHR22d8t80bo2yhrXzT612iv6Z_2_wxWbm8AnUB1L4t1pnI0BW9MLhU0EC55f52wZCJQ8wJdRcH4lbuUsZ4ioBA8J6X-UtP7YjjBTeXITfvyCaLvkwGseuU4DCiTHh6mkqIq6ynzsg9kXqjCB7oDfO8yZm82JEuzLWaReeZSub0J4FAyCUQImgs3Ui1shcwK6IVbk57-Gjywva17R7qQhkYxqeDCbrd64y3QLFBnhiYSN4TrR5AaPiNz3eCYFYPTdMjNCWa7HMb8wgI8Bix513uKuS7HenMc_h1QwCzrD146GKiiEZ0LT2IIDDO8h_gKx3Y-7N5B9Og7wjsDps624fXnr889NYznFOBwuVhNmT4aULq_L32VNXYO7bvGEm8T__RrBnigqlftf0nHzP2U7gN3kKnuCg0VryDRRs30No9mmIxpCzEkGfEDb3g8SxDiiyOjZEuFTG-doTdRDPfe8DqiPTfJdFWRfDkBKFbpnV46-Dy1PKe1HdpoF82ggBjtwT6N3GZ4MPq1UVYQ6aiwlk-vUpetZHohzn1AD15XlDE_NfnZHhvGrHGApPPUFCMmZRmqQTkNH4IEpUDQM4_SacoAIdkrgHO7PoUAFoHYMpumQ2pow4VTR3mj0tpvG-iIBbcxvqc5XLQQZhXuhDVAEl3p8HPTDKqFgxTxiKT_Ns2pfkp7zHS9-Qp6VzlZgoa1Kt-ipc-BOpwBzzeDqg5bOYvDF4mySuTfNy7RnMfX2F0WZKN0j0Rbo99iNUgkvxQNTAsicaZGuGWaUbgiQI5OT_kltLhbL0Lwk4AQpgKHQ0OBgIYC7ONSWNWlHqRTR0CGRYRPPB5tOfzJ9iVeKQKgTnH-PTukqdsxJyrwalRgF9I_b3qBXCFeY7Ea1JyqYhi2c1OLLoI8UJ1kNsH9Jsuww0WjthK7U5KQEHkQTZSjdEyoD3M-daQhocYGcPqRLqt_kfDWpA9fQYJVlMCUL9aQuMdYVz0ZzZwV4PhAoqep2MwxErhdjEUPhqyt4mVopZW-Zyigqpw7ef5K8lrBvtfLV3rt0hFTzuxACp1wQOWVsYvY36I0Yff9iHGHaOArfsR0KgDgbNK7E7D5CtFrHyOn5XGjWcdjLaYKvCJ8wKrIItOXpWEMxBCcKsKsj3bo_jJKiKYS5hVeaznfwc7pi0J21-4BAkb9Vs4XqIcooEFbUlqFSxWMuBokQAsxBEdeZ4ZEWbD_jZdx8NxELKLxPuKiYYmaljKyW4NqhyeGPgFxeHV7PC8fZ5O1Zg2sTMkW7J_BkZte3oGa9zeENRYMYmVp90gURGZ9vex7-GM362BBH-Uq9w9XYGL_yVfylRVU2PGoCEmMoxqgxsYTt6t--noIEO67jMxWhOdX-i2bLo4xdZnTBBDiiCwDLBM4SS5FWv9Q1b5NO8GL9ePjw0PEowJy6Lhq1MEBrQSR_AiNr7tAQPoJc-ltUMtBCn0FrDKT8UZchBVaMPazNXHJyJB__MZfJLc36Pr3xI3YG7C7plb4MOzJ2UU7knbHbcGM8WqKykYOBlde91ywezS-WEo8EUTO9rVUTDPwSPH2NjnuFnu9cEAmXYicqip9J5WLcnWxKuo51O53VaSXa3KOwkRsh86PPoxbN_6boEBx2b78eQOgVrE8T52OD8SryaCcj7GmHsA-nLWXhAZ98WTCCR_O3N3JZSMDB8NNKaTdyjILTThzcZBAMHpCZteh3JxXO2kiw9Q53cCVt-PNAVFwgANiyFFW00sGKI1VxK2SqsCXupmVQqzwJ_VN_KyQfh56xgMWxEucdcbneMoOWUzDZduKIBBhM3BiiaidHeflnpuDid8poBugQVdxNZdxxi27cdV7h0ieu0WAJj5G4DjNY5XI-S3cilYnTXUNg3nE4kQb6jVsjVPKwS7sur3AvwPld2qHJD5Zo5_63axnH-FQuiA2oF7pZxoYiz4IYY94ydG8gOOYteoiwEDD4tDi9_p-Vh19qsJ8NyAaC3sO1mKZUhLpGX4W5vXI9bONL6KfiZtpGsNOS0al73DiqdLiFtAcp68geOr3ym7Miq2xtthT-mCiNOn4HugT-rogZbzPlRK3aHEY3MsLL2BBcPue8ffnazWOosLQuThIGdGwHxSHwk9crZito6H3rfhy5FQYRZELbjkp6XwSzWqwGNh5PvS3a4WxLOImjdS_SdeFFztTbz643sos675Aodwntlo8e97352Zl54dJVBWQQQXZe92VNcHdywcaHzSA2NyLRWz9kJA4R4jHUBq0Kd_y-f_4LZMgcnSJyB_kxotskTdJvy8K4VSB7NSgMxkfzv-DWokMaWuZ6i9lhG6laXjt8SzVmZnBXx2fcGgveBZ0cEEy_ZAjwSaqkircbn6rIcmwjOLxsSvcyHHaB4371u2OZzhoM1eRQ6I_wXHJP2FW4zESJYPOhSWtJ6Apz4rHoUnlDCcg1MnT3Q6PvRNDq0jB26NCCl4ixvXlWtuWTa6_bXBARoDauSXsf9YAX-vnSTK2lOz0pOWgz_QjQw0Lx7nEi4sMXdnGvQNxkSiGAmExZzqAPZwMGbdAJUnjc0jW7Fi28MG3G8cHvO6fcGMo-IHUlH1hr7vMVCViYqjcZQOJ6YgAQNQNe6mXCcsSJij3_AeMXOJvC55N2l9GkRBkByX7-NO0zWRMGZdtYxe-25RMM46v4AZi3A2mH-31HphZ34kIlBH9yb-8Vw4cdUHpY42kEhnXusSk0gx_bGxqJRVVpVgo0EAAAkhSRkWSqJiccp5iZ1yZ2EpHOgEM1vthLyCualal7K-fTHBm5jSjNqNNiZ85xJF3tbnHSjLNdQ-sYcUnhDFedPfS1bzfVZrJBfzjp9_itNRPeJnHhYGe-K9d5TQqjrBAtwrGnMkGhpegfK6Ac2Nklvcl-yCdX0Fx_OYe6peI4slr4S9XmZBj3ZpG7PX4NdyAKDu0GwufKIcSATJlFk-1L17vj-b54H5iFj5472wPjh-E9NJ2UWS5GbEC8TPpqw5wQH_Q4KnOIE03lgzCcImIKW4jK52uCSsBljKI5CXQzgTj2lR2lf7OqqEwyuFP6KEm4Gbd98fASaqrgFmR3CBqJfFkaIeuluglEt6hbkIQU4KlhVJ1kwkOq23gcjyxC4TXYEBNake_62MYh17xz5yxky34x6cl8B-e14KXqOG5qG5ug3gsoD334ICr72xkt-m3mICgkUYOSBE83pb2AA7YuW5IqwTLStyt03wQhYmDXd_q4FBM7ZO-uwue_cT49vvpDHBAL7zwG9if6P_wwVVqO85qFfri0-S37JXpakkJ6_9SUpM18Yo4g2SbEoFLE_psEgmhRAVyGZjGMCU2Yb2Nh6eQaVhuiciWgij3Hf69IJYKZ7dgNmCuuTMp_VlJ0_bDWGlAQZUvZoXemSxVUvOEMjNj0JxhAnuo6Pi9eWLcpy018a71RUAcCrdI6NLvPBNr6qYJgZL2YE6lLe5kN2xxuxtNIm0PdkyvAo9N0OGwXOkQcY8KxwwhBPI01FGQ1ULM51ICIEBERqQD5-RkIAICNR6o8zZD-6Iqah6mvg2OOhpEWzyTuIV6y3d_hOKpYtdPZ0tYpmGdXjl0CM6UZmUyAxk43Frunx0UQg3pA_Awwu5YhXCPek64_gbjQve8bn5Dxl6ZAvBAk85VngWQNtjH4JNk2GABmghnZr2ZHWhO_GX-q3KKTyOqbUjACY1il-tUhIs0TkcQqrYLRMXRrSACeDKw1VWm6iTI_6IYfcUGs_H1Y0fgyCSI3lq3495MNy-dbp-G5WiAQCZI_mqzoxTcr0EifYsDKQuzpSs4e6e4beFerRgJmLVr9Jgo9heM988Va39i0Vo0AEIPlaZqLXrAz--eT1xxSdBi6JlxKS2uzYsl800ySl66rIKPUoXdkVni_F_20mmkwEGCAQ4ZJS1g52aDOSjCYPuP4nUfCCL1868DyocogHBIwr7PCQ4-_0e7rKflnzCoPtETbNRKJj55oRaiAlFdqaTWWSMp_LjH7w0GFXxzTtnuur3GA3QaeaCO9bIPf-kiFhBArunZ4iY6SdxqV2bu3ANgoc35zfPy7r4wZDnS2BfHFn6KXRHhns5yN5U-OVjT2pIBWbLxQj8J8TOrSGYkpcTwJ526XWPKA03qIn2pOEe4wUDkW0tkxyyIgt5cCjSPWhhQQLsYYKJ8rk2ojWvIHSdHSgIof0eVI51RGCW4jcg2pJ3I25sFIfpgqI5QipxB75eTIB32XCBtzWmK2E6dPAQfnHNPYITbjLmOrH2f6zbW1_LJ3LVtMMijseSomNhA0v4KUEBy5aOriMgwBRc2doCITBcWz0OD6TCXbcrNvW7g6BDK67Ym4Vpn6bl3B4tIH19TNQB4YhX4z2kAyhlOOlvwqMcfhtdiNxuSZ7BAqQYixn5dDpswpCqiI_MjH51TMikt-YBBCHTr-RGRIXaWxk2sTl01agDUdyWGJ8wsP1f0ndpLm3fHdejNab0MOn6osZGpP3ZgZIYoX0o7CoF_5lVDdc08Dt7L_yEmzk4ccF-JQ0JtbfYdzvc4OrUBm3zQfNVsdw_AQHE0H8y3wolZFgsPzAOF39j-_9SDKkZQAHkO42MKEBuDYNRANGd41ztyybua00Dn8XEYC7OiWofp6CNgeFts0oXhYM7YU-0A8h4n_xVYrk-0Rb-zpprX3pmPsLySXIDR0EBHRdi54BjFeutO1ODlZUI0JXKinpc3TEq1Q8Umhk5Yid-CmzYfaVtt65hsdKIybzDgZkBSqOZHNlU-qgtHZsZjB7HhlsQH_hsJMfO_GDYmvUyL61zZ_6i-kzVl9kQzarBALNWbFaReiu2SG9cY4n8raKYyXQxQXE31wFUrKaibEAXJlq26xQzmZmf12t4-3ZVxMi15PRbREWLYGzqNRARqU3mHd3_FPTeaLxcWy-KfufvSTVOIYkKoAXAbHfGckSZgQMlCPqKvao0Lss7N3bdcI04kJRmOcExYhAXvepyznGreKpfwWLm2YpoPgFuWq2cbkOg_KNOxeI-SCe8WL5geA7u7S-PPZZ89jarsvO7kPAIQXxHg7a46y9wzDLclZD7UcECTva6MEKRlMP5zsg4EfRkmZ8AQcykymQikio50dvSITkyqtD5XLkLYv2eypab6-1CHu3z-YUQSHYLOw4fsU6dR8lToK4I4pl9auL2j4z2FqwZTt-wnGkTXTevikprpz7BBaY78BYmJHquSGjIEoy59aBoFNWsKLhyB7r-JFAVRXgZAspE59-JmzJVSIfyNWXThYFzabEXW2VmUNRAcb2pRUP7KYWY8xqgZTvQZ2mtXQBY4GpAoXR6jgH-fmWg988kAQBxRnDoZgb0VqOUNQK29C5BIEt8CsHE97YSouTsqqGtATh9YQUinkIpjyHMAYRfnkMiywoFYeaJdEd4DFPIvJ_MmDWtg43nh4dbJahewqSfAzmFH1B-js9WAG7bivifCkEFdHfWcyDybAKICp2iZ4clqNYH9EoSgYJuDnUoyHrBvhWbaG4CZFi6bALdp68fj_7D6MCId76bo2D47SRj-q6bzrQFHvrbfK86EdM5KbJftG9ieNvuE7PjAEAheezl1fxBBKKZDCnxPzovqnmBX3mnEy_giFlxpBfUm7g0ot-FrszjXCMAcw4PNQchogsmtV8zQ8XZOo2Rlay3YmS9-nK2Z1jEBXckY8C8y2IavccKdbWAOUidl9LsHe0wLA0tC0YcAQH5HF1yfqhXeaUXmVA1tF7vJW6tBMsm443zWLqD3MvCjC6DoUb1O6IMaeSwvS7spYGuleZPr4OvXuWcylIBgHS8TlIwoo4P1zBFAlYOYCGsulS8TBKmLxOWskPS-grktYEBBK-uDxU9pVaKCMWy_l_LV8-r3z2HRajh54V3cEsSiG5CF5_EVeFJzAzQTGd79k-AjLERnGw7kNMs4LWMhPS-00_R3nRt_OPxiVnSY_vNyT3HHpf8Lf7NQnZQQ7jM6d3BBSmIUlvlECPBpaVgP6oc1FKSkSPs-6DGL-DkJW3Xo0WlcJKwl7rIXjCrM0t6n3ioRNkxBOg3grZKqF12fnWOn-jtqr0V0Iw4Lf-3Gh007OcyCIy1-RENp6DXM8JKsg1XwQTo7OfDfyf3ZSDWOLan4L6hrHPXKBKtk0m1fJvJQ9dwEM3jzPWJBilBQDI_09Nr2MCbLzNTGi2wzGMlMt4B8u7g6B5wmRWKDZchS0pSFgP8B6maEEZ8JH-c6p7wk6YfeMEC2Ih-KN9IEUvnsh-b6jj0FwcqtpWKlHBJFWJtGnXMT8rDuYX5Mm_-lAWornFLriTA8I9uu1ZOGiej0pWVgoQVWFawXYkYuoZRW5q4OGBwpiPtZIYAyDoZeAUOu7FAqrTBA2NfYfJr9vsXJOaDiYPDHRgf9IPb4xQHM0YSgpvkCDTERAkFVgQ0lLemlf2qcUXjgmQg2MNuI1NcMCu9A9o8-g15M6Sswsu2uLf8PD13MAUsf2bSudfdKaViZvkMCJ-VgQKsy2y-9J6nybC5tzJ9S3yfnlqMyHkbrxFAUf7NnocSzZcRtuRUpuGZsx20gb8xHIA7aUuwd41zsDvsOUpovILruvtFXnA2_18wbHXFKUGmKPHYYGLsz3rhJNtjs0dZF8EDD2XVmxsow3EHn4CXSQkJ8x3D5sDdyQE74fx_9l-BybhGK0-Ww_qLjHwwArVN6GcDacya-onH823CihgmmZKN3bg_XP0Q1c37IUApEO-R6ywQpAOWGv_re4uecj_1jmbBAxwRcvCNpNSwoGTm8_KSozpV6-vadvp_RC3TDHkH7f97yLxJ7ROIt5J8cQl-9eNJBHtVvWv0H0oe8V42gg4FsXB7_Fv8Ou9YUFWaJYb7FVU3IyWGVNYJyPoT662ImG2kQQHTzoNdHPdqTT_kh421XyfaJINAHA3KzKTcOq_4uNp3hq158xepsHM8HLizQKPI_oM3qvpSMxj-BuMVfkDGTnsX-JLAe3NA8yuFiZXyziuYw6hC4rMLuV5UTNJZnGS-3EEGSXXHCfghBQslnMt4jDj1X9FYwL8cJCmPPC9sEgpCfBdPYZCJUjoxwd2i4Nd2vweECi1KOOoFCdmTcDcp6WmlQxv06XLgfCiyC50yBmqw034Ukq2IsrYFPDsITQIQG_HBAe6k-2dxanLxJGlZK6CPCx2MKGElRlIESSqa99pCuUgzdvs-_ZbG-fjr42LTHtP0hHJy_ngCjrt8IgDmUKI3xEvlXZRnxnp4jkH-7FwZoKkh01DjFYkAscw5BjAlcWFqgQFnqle20OyaUTMaYIvjf-0ZUOpGi_wab0RYW1i5s61xvKyIk_2evZ87LyS57WccbcLy88MJ26kRxPMf9rOcEetd1aZxykk73d7A_pj7zxIrvjeExHyxUrM0XFgLN79kvoEAhyhFdZ_FZItdc98yLjaToxZPORBhTn1w0nj4spz5FjshbItFfVLfGCsAxgxRI88AO2oB8389PNPMe8tA4uMPMC2PFTqK795Hek8Vos_khmzeiXwo1BQaVfwLglOeKhUBAuoVvCyh93vTjhapy14oMAt24rP1eeHnQjee5Lfb_8p3gXOMQ39yxQ0Ts32B-CfxQzbPQrRQtJls8Y6lVDr0oOFz1gMHDWRrzA5z3tqHpj0Cxe3R1luIIQ06DHrv73dswQFCY6mYUsMfumIz3WAO0sa7s8fzbGRpG4zcA5_zxQpkwOEmTbBf8n_7vCRaS3weOMVJBuNSJCiQGBHR2eESoSSbV_ESxcoPGf-Wz_Fam4chWBty66ZX9gMqaAE1zWKAGMEF9zlemaUpKjF_NQJkTSbvh94a6Rtr-WR9QhWFzNxPBPIxItxGb5yNTiGZ6Ie-tQJE2Kyd1SmcfUY5fJnCdItfpnyXL4WSAbSsob9XVg4Op0uBGG4yXL__kme-X8WI0wABAACDV6iueeDk3PptXUV0BSR3PCdB9sa2FWGoPt81rhXS1voD5ApICH0CYlLLFnsnBNNi0fB0f7ZKC8y4286yDEl0NhkKDvq2n9HkwBGA_oiFOcGotvk5QXufiP82pBzLwQOow95Fx6OM7HK_uPVjzxxdawXQgSdHoQiMJwbUK2UYbfr0iYvGr8ERELWRTOOiBcZYsSsNhYHMvwVW5ahDFqpCiW8JJOq6gjlJmZ3cvwVWD7kgLmJXMnnRqtqaYl9Uk0EBEw6CZI8R0Fprd4sn-AM5SIgL6PkVm0AsR9FkBxFO5F6x3-DMWIZnbpEFcOjgpkwAtbmPtesiKe7w_XeKXSYKPfzCM5wyVZ7sq4BZaQSMzOEOgpFp7_W4kjVZuWL4HvPBA0eaJkqCCnO9CvTPynRPisSgqY5zcysrcKLAAHSQ247c1yi8smlgYsFznlptT_2rAD8h2xfxUSv9KDaokZ9LROVtS1pGJumZfwAKuHqEis6B5GAG1uZw8SgmRDB5-_dcAQWOP6jgn5PBB08RKA4xGMxzHTTF0iQgF1HMX4ScdvPmR2tC1g2_z9NYw5VvHewjIQTVUgKhl6WkLiggz4qCItjEQ-sQaFctZo2QgTphAAhAPbVVKGmXydWSPn9-MLyRxMEFd_MFPx0xEKWUtWopZnXoAnB6cuRUlaR7Ex1bd9kSJeRT-zS9vg6SmVVeqqF10HbBydZAp2CPsaAXMzrohNXkjT1tHa5DFsGCWN8Pl96gZ4XU0hcy0-v_g66wmMXmP7XBBUEh8wlJ2tg5_32LC9uz3mUecfSbUnNnM7jzPEBx0MWh0T5W4oXWkjl0JtkiRFaawUveTNuckzEnkGqxWKC3Pfi-4_c19f14CGUzZTVXhAWYKQD15Ldl65r6xU7U87dFAQUOHcEY6KUiQ-xEZztcLU_KDfunv1hTy9IE73SiYpIvhvSeus46KY7z9D_G1Hw7nQFhHgxspVLEjejdXY5Pms0wE_YhQ-bkrCOPXpnJxE194xSi57ykPsPH5TBygVP_fwEFAdqOPwiKKQ4MV-d2G2-omn1DCyqoL0Vc-bvCee7FYytR_RFO2_xikbrBZwnj_buFvANP_K1TtKf04nY7mjKJiSbrTdpywo8PvxNB2JpBD9gkVPuA2oMFvUFHHownN0jBA9yWmiKpQTY_ZqT2TR2bmCTmwL3sZEdPVl0oaBlPiFZbDTLGgF-4fBlm_xZl1OiAhj4KxXwB7w_DqvCS0V34A0o-Su4VjZzaEqO3cTuPCBuJRfnExkN0QMMtx-OMPaumAQSyZ7-x27l3q_-q2ABDt7hOImYxGar-1FLvfxxmv_aAUPWCKHHyEk-TpdjgaLYs3EWC2FD-DNMegViiW_kEhe5hNwBo_JVCn82HCUH14yb3mZwFNe2vAp5WvSVoSdkBCgEELEZw33U_IZSQ5fm0BtguhMiFPbE86oWsZYU3cs3LiC3hW-hEBIIiqIh3zxWg7Z8AcaoK_0hQeGI2DANl22GKyVTRdHgB6Vv2Ggz-KqB3NYkLJ3AirxooP_x_mqVVoIj"}}],"authentication":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"assertionMethod":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"keyAgreement":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"capabilityInvocation":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"capabilityDelegation":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"service":[{"id":"#TrustchainID","type":"Identity","serviceEndpoint":"https://identity.foundation/ion/trustchain-root-plus-2"},{"id":"#RSSPublicKey","type":"IPFSKey","serviceEndpoint":"QmdPZgcyqHJTiPeGMcAu2AAkZZ1U4KtdQXid1gdJQtpvyU"}]},"didDocumentMetadata":{"method":{"updateCommitment":"EiB8B_LS_O3NWo2P8fSuRwS32GODaXoLREZHdqpg6x86yA","published":true,"recoveryCommitment":"EiCy4pW16uB7H-ijA6V6jO6ddWfGCwqNcDSJpdv_USzoRA"},"canonicalId":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","proof":{"type":"JsonWebSignature2020","proofValue":"eyJhbGciOiJFUzI1NksifQ.IkVpQV91YUV2QjctR0FyRTlkeERuMk1rclRUa0t0VXN4eGJPc1NESzhwQjl0ZWci.X94wTgzsovLEAXU1CG5M0Gqs6Gu9oHklr4Zn7aEbrdtOI_WCSCrWJuYomkcdeF8X5dV_ApZ6Gh08pPcV2VSClQ","id":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"}}}"##; pub(crate) const TEST_ROOT_PLUS_2_CHAIN: &str = r##"{"didChain":[{"@context":"https://w3id.org/did-resolution/v1","didDocument":{"@context":["https://www.w3.org/ns/did/v1",{"@base":"did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg"}],"id":"did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg","verificationMethod":[{"id":"#9CMTR3dvGvwm6KOyaXEEIOK8EOTtek-n7BV9SVBr2Es","type":"JsonWebSignature2020","controller":"did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg","publicKeyJwk":{"kty":"EC","crv":"secp256k1","x":"7ReQHHysGxbyuKEQmspQOjL7oQUqDTldTHuc9V3-yso","y":"kWvmS7ZOvDUhF8syO08PBzEpEk3BZMuukkvEJOKSjqE"}}],"authentication":["#9CMTR3dvGvwm6KOyaXEEIOK8EOTtek-n7BV9SVBr2Es"],"assertionMethod":["#9CMTR3dvGvwm6KOyaXEEIOK8EOTtek-n7BV9SVBr2Es"],"keyAgreement":["#9CMTR3dvGvwm6KOyaXEEIOK8EOTtek-n7BV9SVBr2Es"],"capabilityInvocation":["#9CMTR3dvGvwm6KOyaXEEIOK8EOTtek-n7BV9SVBr2Es"],"capabilityDelegation":["#9CMTR3dvGvwm6KOyaXEEIOK8EOTtek-n7BV9SVBr2Es"],"service":[{"id":"#TrustchainID","type":"Identity","serviceEndpoint":"https://identity.foundation/ion/trustchain-root"}]},"didDocumentMetadata":{"method":{"published":true,"updateCommitment":"EiDVRETvZD9iSUnou-HUAz5Ymk_F3tpyzg7FG1jdRG-ZRg","recoveryCommitment":"EiCymv17OGBAs7eLmm4BIXDCQBVhdOUAX5QdpIrN4SDE5w"},"canonicalId":"did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg"}},{"@context":"https://w3id.org/did-resolution/v1","didDocument":{"@context":["https://www.w3.org/ns/did/v1",{"@base":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A"}],"id":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A","controller":"did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg","verificationMethod":[{"id":"#kjqrr3CTkmlzJZVo0uukxNs8vrK5OEsk_OcoBO4SeMQ","type":"JsonWebSignature2020","controller":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A","publicKeyJwk":{"kty":"EC","crv":"secp256k1","x":"aApKobPO8H8wOv-oGT8K3Na-8l-B1AE3uBZrWGT6FJU","y":"dspEqltAtlTKJ7cVRP_gMMknyDPqUw-JHlpwS2mFuh0"}}],"authentication":["#kjqrr3CTkmlzJZVo0uukxNs8vrK5OEsk_OcoBO4SeMQ"],"assertionMethod":["#kjqrr3CTkmlzJZVo0uukxNs8vrK5OEsk_OcoBO4SeMQ"],"keyAgreement":["#kjqrr3CTkmlzJZVo0uukxNs8vrK5OEsk_OcoBO4SeMQ"],"capabilityInvocation":["#kjqrr3CTkmlzJZVo0uukxNs8vrK5OEsk_OcoBO4SeMQ"],"capabilityDelegation":["#kjqrr3CTkmlzJZVo0uukxNs8vrK5OEsk_OcoBO4SeMQ"],"service":[{"id":"#TrustchainID","type":"Identity","serviceEndpoint":"https://identity.foundation/ion/trustchain-root-plus-1"},{"id":"#TrustchainAttestation","type":"AttestationEndpoint","serviceEndpoint":"http://localhost:8081"}]},"didDocumentMetadata":{"proof":{"proofValue":"eyJhbGciOiJFUzI1NksifQ.IkVpRC1tZHk5UWhoR3Nzd1lNbG9FeHR0cXFNVHlEajhUbjdRT3RpTVItalc2MWci.LutefXAigkrHZSfNkz7JQadsyTAmLGU9KeT1LDtUfs4jslp_5xfz_Y153fUTs3WiQgPLUdvuXHFjQ3INP-OfbQ","id":"did:ion:test:EiCClfEdkTv_aM3UnBBhlOV89LlGhpQAbfeZLFdFxVFkEg","type":"JsonWebSignature2020"},"canonicalId":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A","method":{"published":true,"updateCommitment":"EiBCBZ5TkPXA7i0X_bgcY2AR3Q1mOYOdpG7AREos6GxZqA","recoveryCommitment":"EiClOaWycGv1m-QejUjB0L18G6DVFVeTQCZCuTRrmzCBQg"}}},{"@context":"https://w3id.org/did-resolution/v1","didDocument":{"@context":["https://www.w3.org/ns/did/v1",{"@base":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"}],"id":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","controller":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A","verificationMethod":[{"id":"#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","type":"JsonWebSignature2020","controller":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","publicKeyJwk":{"kty":"EC","crv":"secp256k1","x":"0nnR-pz2EZGfb7E1qfuHhnDR824HhBioxz4E-EBMnM4","y":"rWqDVJ3h16RT1N-Us7H7xRxvbC0UlMMQQgxmXOXd4bY"}},{"id":"#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA","type":"JsonWebSignature2020","controller":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","publicKeyJwk":{"kty":"OKP","crv":"RSSKey2023","x":"EyGvw3AkcUf2TZToBh6pddeaaocmvTuLCSLun_yYJpL7x0W3gVEzeKlj06J5Sej9Duk0W_yGhbOKCahOx16LszwTHVgnH9FjRk0nwOer4yKaKnjTZ2FlZsYI0OI__jhCGP9cbcOEd-1rfvUFu-ghsj6oHfSXDBm0Ekplkgs1IktoicuMsF-bD7I6tZRpP9tqFGqARUqvR2daQN-scwYUNsv5ap3XakBCDvOCBc_rPAwzapY_nuC3L6x60UGBAPtUBANdaMhAU0gxd-3JMjcSjFgwzAhw5Eorr7bIp1_od6OfBRYu3sIkij5Es6RDBLghUAx2Z3dznniJRh5Xlx_8zn4SYw_xhV1X04vY5U4O7-7veKMqKxzzoGOR7O137gSTtBk66ISXfE0k6LLsZK0Qkzi0B6YQ0Xo86d-COFNhRWQ_Lq3SCSiOaJ4lFP5_RVlHzgUXm6XY1X0jrkVPWdT42VxGjFvy_KX9f50dOkdPJTax8bGv1nEpDm-55UN8nrIzsRODaxMBooRL1y4OxyW1tpHaEdsoHvsZrLzM5g7FB2ah-62TCGkPcG3Yx84MPp50eRPIlj2omMFxMpnAZKBSRMGtk35A6xAZUI6KTYGfNI-IuWKdk0UOn6xL8W3EwMTxRgx1v7iklbgxKuCBoOeAK7FhoOVzL5YnUCHb1NUwAxDs9I5pNmrvaXsDDLKLIoz50hRAdnK92whifFoWoJOOJbQTb9sx43zmB1J7G_T28MG6UetI4dZljoNfWpXePl3vNwW979nNg7GU3N_V8ZE_slRmUv-rAw9jD0w9KXVCuZuwGIKoJ2Co8qjZxnhZUtmi3wFJin73V5BC684ebh40fnA9z-H1Kwa3ItX_mQSVYeMV-_1fydNULsdhlEnpwI5XNQ25LGqMNb4v-YRBXLSmN5CituV9rPXg5ZzQvy8VVE9qxWnicCxz2TzFrxFOOIhNTxf-YQT5Re5HJAvdy7Y9szo-i_PgskFdVm4UxMgH9ddrFUhDPNmVtVY8PoXlMzuU6gKR-1np9J6FBttHOIPu7LFFdO0Vd_Y3-Dl5mdBXFcP1Do1GN7ojcuRUB4rmB__upRAQQsqCApGurtGP1zgtMQm6ozF0gt_JpoXgvZEFK5kkm92vpedrSfDPBBn5NPIgmQgKSYfvmWRmADyr2J9bc6EjJr1-YD7QR1r2g_eGRBE1S6dexWceWTq-RktXQYOSJBnKLSkbqJniuoA70BMkjU4Jsj1EJB7oxE41RRMchA4BRlClSi31ga0T_bk31rNTLQNLGSrBrh0x2nlG8IZUZLB4fIKKweFD9pL1qhLMM-SQl3YR4-v2wxjlMXTrEDjz2xdwJsQhhzM5trtqhVdxfgBwB_ZBtU9KJqYvkB_3BhY3kYQSGDLhyCHbjyIVYl7saQGkTz_owGfj8tD3gU9oJlZHDyjf4p9AObfF4YXKjVBpPrPgwgNd-G4LAgUOn4DAVwGmGBjQaNWiLet4g4lRsLS3LkM1az1w_KyYCX_k9bptp4qLgwV6HqbLx1V5WkmubxLMpHlbV0tZFLzwThEaKpqNyz7M5qIyDvaSbTFtQ9feXhRHU7VN1MgH2AQmQzHiygXHs5qafdGSsKoMm6c_6R2-NXl3asM1TSUmD82yKonGYhSHHy60KvB4M2rVTKRENxR93u7gaYr_4cqFY9LlcqGUMzxmm6TadfSHz3rSj53C8c3Z3U9x9ftbKGOZeybdWhYbRGyES_HzmlXV5MFY5qHiE6INi_ao7Xxm8VRi5rdaHlVDWfBb8gJENbUHDDcsKQfae-4j_vXmvq4s_9L5It5kVLCT9f5NEf7jsxSP3mg9hqgwdY96ob73GsHO3HRoQARhPUt-2o7i1JzScqRH38AeDr9XnxC2Qu4LT6ffOmMKzA3qngyxKmkvyKmIl3_eEhDxpdTSf2ba6EGOD2GuzvGv2a_P9QHw52mvtEoCLNJAslzsxwxbLSnLIOkbJca1Ew26womAjSgnNwUvPCkz4lmSNTbyF63wvmNJJeD0UgkBTb2MxDw_39ukWvH0mOSJegpmENWzMhvKvxxMgB5Y1VY6Hq06V9mcg4iD0AdI-dM646yU8iLfMAAkB-EvwUUMXRE3KGU9Kx6dqhsSCrow4QDpzk0B4FCATLwawfGc1_rxQyumhF9nagl8jP1ITcLi-hlUyrOsKfSK_s3WKTw4j9iBoBWCzHrX1YC_2UTnq5XIdbY9tT4NajRzqwKLV3aYWRnqXLg_-l5k0H2GmwmRnm4ZqU-9YuAy8MQR5CM93H1gxE7oL_IWIyH_tCXrVH4hRhjd7GrWcA90s1AFpCHhBZs72ORxG_Rh8VcJpB5cTpbQfk1ESme0-UTXoSnuLPfNIQb6I6fwFkIvBx9YL7gxaVmjHMgk9BLR89iwuo3VsEsAs4ktbFfZ70l821y6q_xmOBPF-BxJzlVuHMq9hfyYVA-1ka8tBBeEy8NJ1PlYBMiVjHoKWMfqDKo0ONNv1Il_ThirUq-MM4pc0ENOqwCYkomNBFfFHdbS8L1Y5yIruufFxRbRPt6xC1TnDtq3K7JCpRjsTqv_1_u81WA4UIlW49NaruM-2lPlL6P7rWtBqG4axy6U9WYqom7aXBW0cbg31hY39xZb49G_SfSYewGr_pelurFdTag1R3ZL5VuDTggqErrppxKIBYHQP7M_reJ8fQf4JcXOmMkUOap1K7QJvvENxlQ_RQRj10d-t9spgDv5gki7uMDSA3fp4q4gf3HxZhYwPaImQ9J44zCCLUdo5dyhHsyd9neEeBniNZk5LDZRfX66ERlj49CO2dHmHLe-YQACZnMQDDug7LF0il3QHinPD-nedAAxpjfUus9Ay9vRx6nB3fHr-_9C76qx_NjCehMZHlsAOgZGU-yjdwY2uu8lvnb8dvmCbkIBYn4S_aWJ0qIOEjfWuADwWO9BXI5uzQZ0EhKuhALABMhOIi4pmnHqCE0Durvn9RaPiFz6ZKFhW2d85ZAkks_-ARI0phaKzggmB4E6k5EV3cLqkI63Oiiq21QY0VCvc0LuNoAVYzG8s4bx3udSSORrRJm2fOdURg3wtPlFq21m_7y8D09xKpHkXgEbuDJV3hWk52u0Rxv1MTY2V2_LkHIDF6my-MZLQQh0dQYnUjDfvQ3bTqj6UE4MZ07R6UZzl3Vjw53lM2x4gI17Trma17Ag6Yg6XiQA7QqgXKWy3jG6AuBLjuYRPeYo18lJm00D1D_Z_C--D6zMJKr5ohYrTi4ea_dh3CI82xBNwjeTAd95r6X0wzC3xodd7FSWJMCgt0MF6pz-MEL_jNi6sK9mIn05U4icLZLjBwl2lObaoiYxpyWEpnuMGy8J7dM1Z_aRpYt3J-Zw7i3Yf4JI2JV9u1Mo-ywQyXgRcRBhK3emrFT2fxH8SqkKwJCWn7frvbukOzSQiKD8RFuXA-SWK60mJ3erCRnka-xkGg3AiBxxeE8Prk8EGzLcB1UDRGQ_x1PXmMNtdBK65dtv1b0jGTM_uSHFndWXOrFALwi66JGyIca2WnCfQRQDR5EPyD2d2Naecbj_jMwFUsbYCxGTc76n46c1pI_QH1rxDBQ7j1Tj_rcQz6Bk7DMTNnlTFhJn2h7yVnoRPenlNCWZWZPRpr4vnvS6Ii30os5W2QaGHI_TqhhaXRFU8Z7K4PUUUVEv6u3KIZpvcuVxAbcx-ppLVkj-r2vM061Nx9aXEBFd2whV1Tw2rjf-6fm10N7U3ssLGC6sfHRpSVcsENk-ZjuYH7sY-zmN7Hf8zOYHIAZDUr1rjCgG2yCujbdOPFtPs4QKC_cFSzbpOjRmJ-urzi7duH_vH3_TBhMzM4jowgM70l1LoB9sjQ68wzlaAs74T04IroWMULoZOdaeIS54ugR79EhgqvukrIDLEoCekAY7jAs-iNW14YRPrtdul8zVUjLd4I_X3efx-IX7HvR4RUp-6lqMSN46IfvlScl0qBY_SBgCpdEw66SRo1OAIAuTy7VWX_mbvLtgZPPMkaVheFwYwBZnBLKQKyJHrNrKRQ5GdrSnJP89jdh-o6VEqG_whEec3cB1LwXipXb6v1vi-7jxU4kpU_BTMtEChb21tRhmfKGiQxHbOTRJbHVoQJ4NFlS14bTYAEuJm6yXnIW-GOVCLvlHShp5jeWc_9vvvBZnk4C7bDxY80GxadNmsKy_-AcEFN_QI9pt6lckDeTOQxgVz6Anz58RIkvJ1oPL8A5FZOl4iYuQGDAqTP6Yo-SdHbuVOuV3aM9K3L6RMgj5Z9z517O3oqsmthQdy5xtxhalD2bjV4fNsQrsXIGuNa4nAnFtfsi0uN4ahR1_YYVuQgfEQLOGSzJnw-bQ7m8tOxlDOP4MsXg6BFSBvo0LPwieTdNbZR_N4FueA59bt73HfANTd-xz6ycnZNRNO9DbxBRwXJnQogguwZQdLLLuZjqoglKwi3gmMHvCR-3QngZYQw46vAkTUuYfdG0OgaYuAAqtsEvJRaBVSud7q6pgMqM5UbG9eWv20h-bMQeBEpIuVG08HOEc9TeUzDOoE87PzBkfBqVu_s1tyItQQ-DqSvfCQBobT1pYeVsuyJSGXuaF5MXooxYfRpsAuysjWDKDNxAarmMCpioPCo5ebD0elYa6S1KV52RN15vaAZLPqNRiFkek3oy_M8C9Fi2nLzXG1Bjn_JlKzni0I3pofwFNE2ZJnoLSVpLwVLQUzzCB5GoS5P5C1DcPDxpjAr7e8pWb0QAyyIuz1EvSssczBargovo8iNxthV_MgoN4UGY3RtkDRyw2DPcFdji7AYXw_q3xlxXsWEZMfjTlkG0FfwSTHbhrL-BIXXw1u88y-w5SvjBBwk2wW0SjPVgm-qq8yonWXhnVfu4xRLMY7qNRltkzyB5pQ44rJ0iFr6tXtKus3rUTx2PbQOPNCYJynCWQnA8anAlOiTmIJV8G-MYkP3hH3g-VZSnWE8gQhbvXy9OY4YtyqX96TXRGuHNuZBDEHiPmNAvKkfgVdGE1xrxPnfZ5eN2RQWXAf5a8xgISY1bXxlt1prbFSiHTMLnikDpYNy95JBQnPEqdIYRhgzh29L_RQpIM2ItE6rPrJCl-NL0Mo3YZNdFepgL-5uOjFilpmO_EfAc06pm5sP-g6S3vOx8I9j4JrOnhygXvZx4Mr2D8-R_7s2F5QOYKCpcYmhKSqaPbdAX-q6oNQQ3fesRtmDJIVbBmioMmu5k3C8hh_L2RNAe6ItXT7XVCo-QFQ8fiUIOMWASrYHiy8qsbX4kKQJ98v070GnqCMpKVtB9522SHxJWv4h6Kpsmadh9WjAmzItl4tRV763mNcLeidWzlJFUcfZIVm9OrWbHinBUjKFnoeexpecTm2ncrzpUkMmJghWKv9hUzk6wGkQhsps-94GvQJT2ou4T5xLpeATQ3oenwez9tEwxQ07tB7FHEiIBpA4PFExNwdv8sxaEe2Zaoakh1iEjIbd4uBcEAd_E8eE3VSEPvB2_zT8nek2I9pcHEIHA52Q2_j979f-vAyJci99RN1Va8nvk3TyMz_g6OCknUZcqkhXK3lqigvhkUBl-IxjWqagdTwPfwGPtwV3JT71CZDfBWujVMLPGB_gT_dhsWlIN-sC_yiWL_thQrkgKFPqXPwQKCyz8r_iv4f8NnJIh3W6_hUURFsnu0NpVAlhi7iOU-B0cqk1NHN9BgNbT_zU2aVBEFBrlQetG5pyxxgyDSvrz-igEzZ9oqa7-EIgNv8P-0T0IUrlCIQSfPsiAUsbExwg5JwdgdQ_gD9HUt4U2Npk03XtaAySY1IXJCXeJLp0OIcc8hFeaiPMMv7Caif9RsIxjwnikwLFGtpNy70Ed6CkTMtxBR4uShDzbSz7Hk90gu5-jV5WGysOA9AbW24iqgfgCKjrjgfrod_MNG939PdD9KOV0x3MqbZJmBLB7jKCINC2ilgH3Ez4crHFZJEkuJ_Qq-KDXW7l7hjHUG_debtAu6qI1edYP09UkgmQtnZgLcGAWUhDxWhdf4XYOHfqXxfhiVu8tF-ly7iqWkmRCqhRGV5NmzUWuwvQ8-Jlh4kRa7nhpwb7ivyXiDubq85_tKuha0qKFzzz8gFuiefICHX_Uy3xM8m6Gy3KfYirumMAkuB5-IY7Dgr6IZK8YXGLZb3QEXmOjuwp8Rmm-bMnCXehgCJZplNtcWi7eQxsP4y0IoEUsmmC5Y1as1sAs8-R9XlxBfP3hdGWbOupZfS6FmMRiGD9HoWesUSVtRs_tgOUPPVav2HRIK2CLYBRwgI1NaeRcpnO8cOye4UgRm_UF36pi3hJPfIdCnhxGeOH5J0r9zYEnTDs18YsIQedQOJ9jvGBLvDi8dJ3NRzof0hk9riVtSPV7H2EKhkEL67E5pccehsmZnha0ewYbZdgEstjzjwQ6qkZRmFLOBdP11yCDzgs3eDmnk0Ztewl22-WhhpumCfNgux5OEtcSu6hcC_gtsXQgTm4QV09fFZJAH8tyfFildcaycx0w6zG_tT47jBYIwVyEI-Mvv08qYw3ZN6558VgacYehFWake3ahdjDxZ8bO_tBtLMrFXmjRpibEIYbWZW2OPgBv-4-Z_EPXtLrDpJxYjD8bUxNgxwyqxAlyqZe0FUQVo1RTWV9hzvj4GcOG7wC-_t9aEEv5h9hg3sQXBxwKwIulPSsJlAeW3dygypohfIMKiUdjDERwhgvPsvB_vsJIaVpN3SJVfNWvMEFAIRxl0o0b4upYbISICcxav7YjxARlPcV_nqG6Lnj9-6MtHOzvmwMWpcM0Y_FFro9TqKAj8TkAiGaEMYyJ8Z5EMAsGd32HwMhmdeJbA9TxNpC8CIpeNlU0H9JeSDR3bl76oGAPDIc7bDmfKjcCL_8rZamAaZucmCI4Fkkjaqyl_k0TOHrxrc8EcYzbICfu2Xp9j5Bl_w7GErvNIbMsbJejezsJxt6CR71oex_OaL_DyxGJE6bOaWZFwF3WqhVWMoMEuRwy4Z11DIsqZ2pbxyArURVFG3mIHnBJ7ffjxYbofuuuw9Ce3S0W9AwEvXRlquPr3-wLesE-Y09JL2x63dPrsfx88itwaKSyGuJyvqpTu8NwpAR8d0bU6nXG38O2ysH6-xwvDGoeApjhGaTD71tv5hYcJj1X2M-GeWFi74NjG-PYBkamWVPk8v2uimVuB402YMgUAe5RtZcKVUfHczIcj7IWreTJr8JCLl4N_X48ji2KDuBuuaBRBUYdjkl8ltWE-AQzatqUi3DF2ZDEjEarQrk8K6QDaHNbMAEQwqxIcKVB7rX6pwR4EA2xN2VYmCskYAReAbKYyzbFKgx-_kbylwjO1CMcDTdhKYHnfEznxeaxzjwopfWQR5JQ_y_4OExcY6gh_FHXXyMOQdyzdcNMPFOZDvKAf4PiXg6BV6VVbvlssgImhEbhyfKlwhmbHkrD90BVSZOfwp0m_zd_xOfwSYckSwo8ef1K6DILkCmiUSc9wiCBBGHF8ex_0u3nepPICWg30NqJPii7moRYlXNi2hKgTB2Cy1njuP9pNFSD-8cOxrrAoAz6SaxdS4QqxjykSaRko3FibccYcSE_fkx7_WWBSW_1GOKTqQltkzHWMqTbu3wEjBAbnQjYGEWn8aTNzsAh1pezmZurCOdi9uL-cjIVavKPn23HhHGfS88f3pRdohcdlszyc74acnD6VgT0VnArfeYPNBWcliVDnCE3qYSvter4l5Fe4rH1qDISEq2ni1-uxNRJx6Ck3-5bWSZxHAgvc_2gC2O5qc9TU-akXvNSqLmNtKmO2FGFtBltwgyLc8bVWAJrNxuWQVCUxXlfSkxaGXtN18lGJX-SvmRn5IsqfhUitHzJjEASiI_YOVY9OoGEkK1a532FFGdO00mS07BQCPV0w_gldLncCOgt8VPaB5d5SjOF0_whIcVAIY95y5MrZEJWcbES4zg_jdGb5SRLlr9PENPbne9VYK4_ju-MCFNo0uWibQJzJcpaKU2rZ9sAsT2goR_lu-aLGCdeimhRmual5ISX_tyMRikPCDidsweqUeRzPcriSIRDKLcQfzA3P9Lt_Mo0ql-l1EX7TcwLgCsISBJ39jyhHyPvNPbBAFAlrlF9uRhz_ATonpUwgZrQHSlpsy6Mzh-O8f57HKQTRT0VigvfIeC3J1TR4EzLkHUdC7QF4JNlprKFQl-HUh9VIOpwXfQ7VwhbxUw-MThAn8fnFAKqd8S-4S76Yn4Ns3B0FA0wlDWp9AvfCSlm50bQHUgj8FEtwz8279OoIhBEIMnA_rHNwA1gPMSAl8aU4RO4L9wTbhwVEs32i77O1pQS93ZeNwOwXXoquAAVFZwusOXz2C3jxzKzB6IdrA9LE7-ALHDvmxB-y9KUe-RgCfFgjh9EE7rdwftpCOMj30we1IOtQ1XyFSwpbIK-y6e6itkyx73nB8UicYQEQHDnl2UPtxm3TLUe5bx_E0sisng5ZV2ISypN4_CiyoAbUPCapdHnGLh5VJtaPPq0NGIVA88MkPxnJC_dTfsZKzNVDywA36U6dGzcSH16QoTfJ-ZcUJhHAKJHizKtLpdxpNKlSugnNW0P0XwgrRYAehBBqJAWrmDc2vll-f5KYy6AFEWfIub9SODwuu3j3yfdoVAjpi6Tvm_e_w18ZBYKjtRrAAg38eTrwQwdDDovzBO6t7xmJkqOxsCFl0tz0WB7YxhVMfhC6qv0ojnXM4XrhX482Ew0yMUB9Ql2_2d7u9-aM7VztBqRf9dtPj0Fc1WdfiMD1d72U2D5NukpfdO0k74QL4xFcEWgq0qAPT1Xd35HaQhe9KfUYx0d7KtbBb1BrpQ3zZWS_ThLtfTHOvGZRQH9bQQyFkx7r9Lnal_GmnKw_w-Y5ecOTXwxvtB_XQNOo2i02MTPLpYHXMCWCFB6kHee4fhJVL4yQnaac8WOYkNDZeHf7y15M6Ezs0ieyusNjY-nfeAuXS1kJ_lf-qI-1xCpx4wmOy-W4Y4Xbr5YWS8Pe17115uh3ZGN9n88HuWj_fzZ0BcrgsT4p5LvSm9lntyD3oQ8pX17phhk3xqItrnJYAq8MfnLgifMDl6XucGJj1rhsvVGfr_ccjSHxohBb0HWL6g16xEvKsXnQe-PHn8Djtpc9doxqWWC1QeFnjIFJ38TnZd2v6S9irKu2D-YTw_9TvgRZTHMLgHH7pdFo2P_-mrKP74-OvYkn0O4aUVAZ6-bCXKIZ4ZzFgt-aO6l6vyUUfhcVrQKcnRdrZ4_GYfiRdxlBL1rvcZAkVpH-iitAdQ4N0xFHFL3MO3MH_EepQXLXSgciWBbbc9lzJnd4GkCRT-uH1SKKtquXZIO28ERVLB5yD9xkl6-ch9qTYNnNcBDNSAJQeFBwCHB5xZoyuYfN9p5v40vfSDAoJU9A_3_kaYMyUBVaxQWnKjZrrA5hWy2fjRUnVpeX7PDyAyb6eZDt7dKlkWGQxvhDXRFeN9yjohquhDj9OSS0JlHsPLobIYEPThAwpAYAEH9aspydpQDzH5LdB8aSUzTmFvdt87KW_OjCX2bAvPUj7a8bhfrITHuCUwOl_hNSIaxUX9EuHEifvRKi_KnQRZvkTyN6Ji93jcr1wYk2FOjZEVdUfC_lI-xzuQDSVWUUl6URvL2tfzx5FxqScbNiq3xnIqLrNONk-p4hi1QvPbgiYvXevv6-KgoCOBN5b7E0KUoVcBh8GBPzCeP2EZwA6C9k8u55Ul0Y6dohgm5HS8NQfXCSTt7QQgchGBOyOP96JR_uRbyLPJ18KaFr9QTxkQrxpuks_tWBdd9QD7GN2MU26S9veV2mrWHNXBiKY7NNZjYSkfNyzvjsg3VCwvxU9kzvkozJ_hQnkOnEmlI8bu34cFvYy1Ms4X5fLwaFLMmG3SnAIwBsCz3HxzKU05NBHikuB3B79BGskfQK_Fe-rkahNqJgG2ya6xgeIBivC2iuCuVjM1xcVN3jM0VuwQOCIVwjPpyDgWwjm5rpjX7LfEzwjyXynX5OR8PVugx7bAFwv0UNcbkBNLadJmL5hZfeXHzgPM5u8M1_PEpwxRddCDLbmbY-Y1naQwfaKRQp_c6KwJtT3IzkOJlaYsUlEeoLQKfQI-OFr7Jy6N9-tP3x_0OpecilN6J7UQLOTQEIeygISrIiIkSQgL8m7YCl7cRejrq3kF9UutkU2OIJFseVIFtIKZL92vc3WSxj6A8NkX-yqQ9LCFljVw_acJ9tUT7tNyOF7mFKBQJPa92WpaOGgzq4OCV2nJs4GFYjXgw7uE2NjQ2i9_auhXryGm3uD3G29NjUQ6Lkingi5trDZLCzoFKtQ_-2tWnf6sC4HBlShllmYDfCCorSX3Qc9WvEwxLbRvNX0CgPCEoxIKHAE9UzN9sfWZLD6BCXAtERDgNqc458B3xIrpXpk-hmIe-Res9HtuS43LqebcFiHjjKKiBuUEBCSxSEYQPYdEII9QMsBsp9IoCOKL7y6m5EgCfQzA7hiWLlE_Xrppv625MGLzebKWzu8CP1mOPWTp4FYwaXl6sm0rgbAoR5XtNLcBazT83ji0Qhc39dVR0nFyvdSe9L-EFw6dbYUPPbQDh0hQVzwnXZYFi4wgX8iFfyvfj1cAGrQNfx2yekQfLm-vhGK_sIlCRVZf2bjS6rwAbVIhhPFuTsQ5EaYCc3QbvJg-slvxMGfr3gpUkMV24EE0dCemwKRyRyf9zH-oswETPMyAFTQmlx715Ao-RESnFuc1Ebl13oTofrWpye9ZaqqsGko3Cimdifa716i5Gkq2FJNQRRRrp979uFgzdwm2AL3Wa_5I1t4aHY0hFNXzKU5u7gNmtiTDyLSOIWLGfd44msxBYFSE9YqSdU-7KpEtOLQRppx3FR1TQooT35XW13oPp37k91Uv2j8wLJPAid7msh1AUWmpGiq9vhair7EUlZhnjNIEvhlTr6sIwFzsJPRl9Dy838w_UqVXhKcA2wJpTCjgRWXL8R8b6L7Qs2v0H554fmrK3qcTm1BgmPf6d0aeO9wsgj_cSO2gI6HgI4zL6PUQTsMTzhIY8pN8MW1jPWVa89yWjGjaanxKT6WyzdkCGj6NcG3Yh5UoKGeehwa_5FQwggBfzXYMIAK3swXYvK1bVz_68c3eLtW96nYc1mnOw0QmcuQ7ajBPpwPVqQwH1iLRS3nEWbxznVbgvcdHS1Sv8LcVU8htWp9JheVP2OCiGQPFFScImnsLDC5WZxJNohrxFO6HHJ_6T3py6zz491E_zWqb0B89YapQO7LKc_D3pU7_3-ug2A-BmtjReN5-I0QAaNX86gN5o-LNW8yl7DmVU8rDBHQBV7vZ4uijVQhDvpifKk5mqhztr7B82gamJD6gUucjs6nA9V8i9496A3dTMHdtEjeEIE5zkvtbLe44WyaDxa5KiwZikk137DL-hp9w5b2-ZjwrGqcNJrYwpTQAjHigL12EWMHKEnPEsSXqmYujeWGfB2M9_VDmSgf3J-XAZroxarSzyVuead1XNLHtLqQgT0Prh-PS1lDJ8jH5y4_JzNS6lN78BaEi-rBl-hyhXqi7ZEzGEyZVB-H9rkmCE1jnuQsHj_iWUkZFeE5wJRemTSNTxF_GqZrFTkTD68qxdtMg7nWns8pXHaqDxpWAFaONRj8JdfPCeJhQ3W9qIdugEHXFlYYtZLEuXAlBGkHQQlnL2XeZ5aYE7xDC2JYQRJBj8c5fYfusrnqBgsz4EIO5ewfwmX-OAJg2d9Pm0UVxGrXtTW1H277sVslv-2FcU32cZwwls4YthQ6fyoIVLzJTyMOYJUrpFW32r5tG425wn_Q8ezmTs90EKuVrvVo8w92JL6MDKA-orDvhvQ3beb9l7Sgc5yy9cb90rjD-lyQBgcDfJ0xHFnhjnz4S8t0yga42xeRI3r_mXd0NvRzTUHkedNMtRAdU-W382jaFGRBxXL_4YziKyewh_nGh6BlW9EQ83Qf0oSwb43IN4k6GmK6KKvwr_KiERaBougue7YpwtYyqCrEoMiEEMn-Sog4CeLzg6IuYx4awivB7VYGGGwU6Bwc2IkZkKUFxVhJK63cAwQX5Gcve_j_-WcRRGlUhI9W4RvFhQFpl0YfC3cLUzRQZfV_fWH2MIwrJm6y4VCHhnvx8O87qetR0kM7el6lY4Nrk5bNtCdBeoyy_C1sz--DjsmM-z9i9IR8PqMCZcX3gBry0Sn_js4Ka0cXPsKpM-GpR6L0CLxge1FdKNDSFUOacsiEzh3-LTu-rUUYglWzQShuc8_dtZrIEvVocirTKZ3gaImQ1M1EylwXITBxzCUW19Io1X1mxKiFpXKHtzK7AvEs0kdicMBNl1HsKSn8OH3jxwLSHI4DwFIGYBxCQ0vvG3NN5ZZ_c4OnSfQ-nojlgmeCjMGykcA9E__NgeddsOdWxnG3fVQFIiMzoJ1AtYnxHoPRbtVZdyWB3dX1L9AKxlFep77w6KS48z70KzKseRnKLa6OCPZwfXgP5kEKA7FcKwpwIaMPNxCOedtULYeDhclbLeDtjK8LA2q7a8elVyK6YRvseXaZ4-nnd7iLYLZNOv807ZLaYGm51X7aFt0YRTimfsQIGztdkY9aakmyH_XQkqPmlNa75aE4xf8FqLjwa3AZ9PcIS8EpwX_Vw_pFA0NJcvJxCBgY4Iz98FxssnBRC9dJ1aAn4Kd8lgWvHIXS974MFCCGhfI8RRVDl4S0QO7W6vrGTIZB1ngY6VHZQ1JG9NJOGtomR_8RNH98FwcPzVNUzy9AhGeKBS3WECJCxk_gKjcGB-rBogS4EU0BVCfxzCoTMJF51ufpG1k4eWlEiEpOqUYgUWAN_3XYWNhphToFLg-h1xmQWWUBiVS6tV-XVvEOgKCKp_b8dMJ_99civ11moW0s3XQpzbxo02gCBR9LQYl2OPBcoRr1bVQfmS3sljBMCgtj5NodsMpz-rIZtgbzdchFe-RE6QK4qaMwAUY0oldGd7nIW9V1C3hnGg0kekWG3JKlxMhIB3IbDAVQ4jRJ90_JbLVaj8v0cNmhAwT0QwIwuTJJYFDGM1fYrocL0UKFsHEdPGZQFnfGAeFoMQwUt3I6zpmXbIqWA0VpRYwiUwTTRNTSsH1_eX-LWUnbXBsOmr6X38Sf9SQD2giVwmji2KBw4GSfRjUsbae5gpgZZbTcXH2ZF4FK79B7kM3RW1yKHcMrT3jXyZKjfEee008n6CJraHTc2sBDtV85wr-TQgic1VgACOfee02nwbPgPGhlUsN1e1cBwTGCJiIthec58AQtsEGIsqpTwh0axbKUmUaOj7zuUjDTg0imRCdYb_iMh8ya-YUncdYTabPkBJYlnbHzCB7aXmq42akqBQTTTgVgUsrRy22Q9gn7CkGltOZRbiPZ4Oa6Uzu-CYOsK-0JcD1xUgtTd9icWNNbAg5DCHh8FhryzVmRa5VUkC81OQryM3CgKdyzyw4xSH3qw2HcCMu7VHbHYhvVEXOQQtSaedW6w1shQMbPRKt0Bf_n3DTiyvSsfAgZmA3lrhQhRzd710dzxxljzkbfYEl3Q3SKg2CNM4Pu8SzAcJj9M4WubFMqDirRgVIMgL4xthq9u4qvIGxTERgAu1h7xhUcA9f0IvKiPzBkfExW_QIYR8c9kewkGILCplgqOHbvNBtqK5uXJrnscBUm-Su8yfc3gTiWWlsb1KBm2qwj6uXOBWQ-u4xyatyltsx8AJlshq-YB-K5oJuvlwCXkeXkU3hqRM4SRwLng3VyhdL0Jr5HUv_M1ENVemAJCR1W_6IXWxbChAYiRUFVnGQMCf2Jx46eQo1sNMaO-1r1LdtVSJo4ZELftKu2X0BMQC-l9iQ5EfDT2VEPZvl5JszWbqWIlkr_RY4jwbY_OeQCkPaMxE0eywBeG5zjdTYzmPLm0YjmK5J-_7tjM_678RIQ8qyuFPuNRGFUClznKIZ-T7SYMtFie6XAQ6j3q12Mh4-zEomU1jIOcy2EzZzTVgrpmqVtZUB9wzPIsNtq27VtLz231dh2i2fAfAZHdvIy_7XQsY7-JWltkQ-fY41Dw9QOIhDb_KJHhFNH2xa3g3NGh1WxZIiJNfPXXH2pMA0xU_FnJF0uPEr2u0rEcTWqTsDgHk4krHglASUYsJYneG_YgBCHWWrGXWzbQNGYsZryPJeXNcY3hw0wO49CxV7gb56BbUNBvNIfgS6SogajoeoPTkPQAICjtAVhnrgXyIFnQ38zu9Cwjwqxy10jt04Gwm1Q6xAh_CNQwcLgtJ7elaM7zi9uEGFskPfZHF35EOhpMwR6wBoPSv0ESs8PX1_WKhYSakFyW7SewR86-W3aCDR6xznTr57lJB7BnDb9_fF6rjfysDLSjofLGwjD8qC43OlMNZB9m868hgZoCUKvSnTpVW0B2NcAoM8lgXDox6cxZPtDsW65C2fMFUmt8yqLg9MOB9QRvr8jQVvgQ75GPADaHTVbcDukGOlpWsE8qHc0y8sbWnBRwGu4lUVpyOe3R-q2Y9DVCPonQoeUt3r6EfyIPeid7GaY1S-jCTuj5GlZA4Ridz6yYYZmGXzju_OqZL9TpH14-DvywWaBu8ZUqvz9kVamnK9P_M-jTDn6iz2zy37xyEGtzWT5Mv82avznCG1l0kSoG7HPg2kdA2ngIutv3-sn-D4_H3_Wzni52iLO-5CdMjEHyo8IRF2gsHDwR0mkF5uGdXv8RD_b5KZtgMy91QfiU-h1B1OTDWxxhfSPDO00EtPBW3UPQhkMJY2_MdHzKiG6i28PRjUTIYDcQjc1RrUZFuBmD6S679gKEzKw25fKmSbk6MBIhBfV1Q0h9uX9RauUq8yFRB7mV2EQgMRzrSZd0LVqNtBcOCU7TdrpzJzk0pZkfmjIVGOAJ37T234ICX4_M28IgaNiluXWNYvW8j7k_nTy6-8uRVw30AJnkQRswmxllkn8sE8pfxq2ACMG6LhiwkUeRJU7QYz8GMhtn1HcppGw27GGLZDbd1fHQ-X8EyC_pEx6wcSKdLWOZJ-TOqBWCDHZAJJ44G9MQ_eYCZKj78LA5pooQ1OQJeno7YefrhaY7gsJEY9LqHaDBBrDYPefTlMYgHPkHKxgkT6QtpbAHN81lB5uiiN-o2HPIgI45ODYY8pmvk7SY5BVsu-lJ0K3KZJOhOsfQsoK9CWB37yZj73eFNgWO9Wd5qmmiRVbUyBrjWSXc_dLnbEAKxB08xoITcG4hDIO1TSbTIF1QsBKXbyH11lwKM9Gr3bGckU_ni5H49T8MeAx2Cce-oeZ26dj5jDGQwwwgRbDf_9eKjzVzH0MtA32QPr-ZDqwIPJlpSAIswVKI7W6-TVHeKdYjBufEUoVhjsJ2kZLNnwsgUPySarkA7PjTLxcS7L5eXTIzBWpcSqQfY6eII492F_RPgaAzRnqRW7FA0lvNcCblQJoRK80DLGM_oZajzqytR-ZgfJvWQXY5UAcW0ywx1hVklrP5H9hxJBM6LujBC-bfK2gatWTUNoo7ciIWk8WPKZf9jCnGd2s9YQhwqJfIoYWLYZj2obHw-WfedxSpLOl72ucoXM_UvtvSjnnX18plcNrQ5lkO4f23N0gh_oZhdwYeyeb1N-KADIKIdY3_6tj1AFOqN_vXTuFtEAilg5YpHC5akZeMvfOGunAVza3qucicsRDEYutxcXggArT_nUZa_j9X5lp9EItKRVyGjBvRa8VKDwoHe0Qq9JYaDk2zA0Gqz2BsXKjxS5eArOJ4t-el3UdlFrsrGz0IIM53LsVDnYFGo7G8sQWzxQHD3LqVKhumuL4q0I6gBmOZBhAzzAb-j3dE8MFDXLKOzpMXj4yY_f1BqaSVhA2LxC9FXh8xlYclwHgweVkA98obGvKfW4iMNKJza4tQ5A1QDFPDwcsF1biEPK0svQmSnHNvjhOBM_hRoZK1YD_RXmIYPWzJnULt_2Nq4Fus7QlP0m4I7qSxDSUe3Ly_RtLefBaV3G7dUa62RQJfXVKgbGQTy_64COJ89TVWD5LIEPW_LRrYvSjVlsMD7LPexlQnh6J4g3zq0uRHxcWa1bDQDUQYrQp4Ud_6qc7d7FoQqYbQgib1M_MIbRyJezKZJFNXN8aZWzAkSjR6Luk43uWgogzv_PLON19AnvbC-eLg3fE4aUvJAueCiTQGGFkBb1O2IW1kc4i8wN_II3s1TkjQ6KSvre1kN4YMOTk73lEcC6L3NcgOd-o0tPDO2O9E6I8FG4yCWmnFPjPO1FFmEnjAUSgwhEs4KdKbQwRphNPnZQ6dWsjKPVM5AfmEiLx8drX7C2NFidylmW1dpC6T9L7Qcvd2YbocFGnNv3j4ztPjt-9Z2Y4fZq-02HVNkkuOO5AB4TdPTftjgiGipnbMaBmgBNMwbxkzHuWZ-avaQfSifAvfuePdugEVjmjhcS0NQuh0_hZ-K8m0-41A-EqQ6kzgfYTwKuQ8JdIWawuYoM1Q0G1bJGpwQxG9DPDB8c6y-WupSOZ8c5l2pWsRVw7UJ47hHhFIsoDHFHVDBT9N85Y2SIRbttX2pcnKj3nw7aj6ZcTRwpNPN-Qvu8YMMjMUVV0QoIn1CEyhim0x7jqidBvcSHLamlTSqYvzDfI4l9fSA8m4Yar_VZSMYMxls278D2sxVIEjXt-fqUbXc397qGzvNniARzqZcqrataPpzQoOM-bNj5LEJJdYPqSsHioJGOkhFzWXu49UuMFYUvyNxOhrbUy8h1N6GKiGDMSwe9k9wN-5WhvfEf3wPAztWl5R4PFRf306CPhL-FW83zhBr4c1UxU56taoVNnJtsblxuTTDJr8HgIiS0bqCLpL1s-ZYOgARzAgymuZCRdaxTmK4fdFhlTs6coahCbrSXO9Iehq58t6uw55hGhAqMjVvaRn2TpgwtHS2jvGMCsLFBYnkVXeeCDwA8uIEvujo_WcIUiT7STSP1IHMyllhlhU9tb0sD8wadR8caAgHBe2CuuE6YeO4qet9JIzOLTd3kJRE9Ev7aChlmuuAElJ0o-ktfVIvUbwVAwiWV3X6AcMlmVR_6HzhwZvc64Phapf84hPMYXvnIxBSI5UbvA0X5nHU2lnqPeRlhQI0mKXvLk4Z60WTgGrJoz6mjUQNep_zG1WTSkLwk4zlLwupc492MMc-M3x-vYQBmA0J2OfXEZjnuqAQ6az1hF9SaaF87c_W-Dkd5wgzUEkoUA2kjAfLtSItyltjCzxTnH5gGs7KaeoN_9V3bj_EAquWTrF9Vdr0DyN3fVdwrjU7oZhp_CVfondyy_VQO2wtxzBICKDcgraDmcBS1Pw_VPEIXvNm0ia52zwDDo6h53kRiKECACeOLLwif-WO5IBh4DZ_DFsiuaX1dJyUUO_7vk56KjmN0QEHxaNwpvKMuPtRGOMWkRAwIKezgkGJ-GRLXbeAA_1qqT0hLDsqJUal65fXdZ_J-qEnJH9xThlPem3WrWpAYKXeVOLOCxuA-7wxyxO2DxHqJdxsvzd16aErXTcIq7OgGXL14QQXLcpQIKermnxygZf06I83xy3pkfwEY07BVX6MnouU0ybMlqeFQgsWFnP_yjPuYGA0RQGOqsL_Cz_aq94VrHtzL1M8NTQt3Jhpr_L908QQMXN7kK6CKJnDkh9Rzykak8Lig_xmz8E42bPY-RWpAgAvpju1nggo6H4oH41IfQYW2gVzTviJq9EC1rP3FtJouq9gmSH5xDo5IW09XFskxJatkvOUIjgtZhCNG_VxtML1VdSDLZSrYjMT46SO8JjWJcn__4tR6gEmTrzRE2OSjbLuZpOksXgFrOgRDsZuPSeBAE8VKVpLtHvRQKWimJumFONfHJ7JxCOaUSBzpvk88Wg9em4x7YAd_SAChQoT7XRtjlwkRszQ-TwYfGsyOOGiTyG9dzCGGy_fsTugpowfedGCGBHJpuApn7cf5NNyLsafquuDtEyUly0NDpCwF2i4Dhma5jQsDEbKOlHnq8uzAkJXRe96IQBj0FWieRJyLU-pNsgXz2PqRxNXs__iId_f1X7avOZHN7FyBa-vE-u8RuYGXuLsUtQnnA0eYesQ0hCvGHa71I5E3-w1DCu9dLeY725SC1yVZ_vJ2WJmwEPXJIXKhVgTfvw8GIEml1VGxRFvb5kMQtGbXChL1tz7Y35ux-SRoX4A23pTZVEVquaXb2QjNFOprmA0tuFeYlsUdqD82ls4R1WzgzLVRRF4Z1Jh9AFgfYHqV-7UHwJAY0OpYK9iu6PPknBPAxWsxnLxyIxQ_rRnrbD-AyW-uFhBZ5d38zkvKw68Fr24Czq84U_OlBAvHtTWSzQa_6pc6tu5KT43QDCeWwiyWt1gdahuyoqGpJNgqyD6gh5xjSr1U-ahTJpXgVjnbNBkfOWecj9GK6CMLgvcI21qVrX2IHwG9kMyQgNmu--z0VHXt0WUtEuUcHMM4PzFM5AOZ_oxSVtIbvoYGDXjUgEI-xM7BOr4e1B4n8X0aoorefQhCLe1-Lv2pKRSeUlX60RlVuRN9GkoD_UoFqz59zJwL3h2uakwjt7iehx7DeI2pHUthZL03BqsYtJth9Emw5gsDKfBIR9BAjIzbSFRnnC_pthG2E1WMRMeeKThVkL_JYkmFj4Cr1xjqXXCTAI9QFwcTqRI4ZkRgem_jqVB7H9-BzVDrqgbQoxuWhNRn3_w-xfyzv_JtRcP150_7bEN2-gbBJCexcaF-0PbkopUuQqUjE3-WYKc9X9vLWcdkEehB0F7eqzdIWqRPTsnEat4SQhSvbaOp7EgY6Ypkvjkheer3fkPelAHN86SGviWWtaxDTWMBwHQjM866tuDKWOEnLQhMb_IjQDFKHrUKUnz42saPlPWfvbas8_Ymk7bX-E263Wzb5_MWXqPHMt6UTMSOtw86MTE46YEW9Ww-WW10cmatGb4jfoQHXa_JxCRry14AjwF7CmmQLP6dnm8r4_jm8AylHV8iKCG6r6csAhY1jQ3I-24iLu01EDB6H-_bIX3uiZDXpf4T1aGBJh7I7INB-Ad7d_IV7At-qaorPyE1xvTWeFVQLymsE87ZHY0J157ggITtT95e_Q8_SEiFYg0vxg89qBpuXygL2M_Pbrb5eYTCA6K6N86CxlOvFAb2AJnhAmxe8c_KHIsFZPL6lReDGQmMPBuvdCjjLPV7seEZX30ZMTuHYXNuD7IytEJ7X1o0_04eCmcqbivHBCoQGOzDhQ86DSoX2Omx-hmQl3hI2KgKnGcnfym2Ukd-3CmHAyCDAv2kDHm38H-JdcsO2DNk9QsYtAln6XRVl5kFDnWEhm9bRh-fg9Lmt_mNkwHSwZ0YrdYhAOCMkNlukUp0EYKKhBSY8lsY7a_TPbt8vkTMSCmi2sPr7NnuyaxMvw6Jblb9OD885lSOUp3oPpoH8QPkkhYUJ4-HVmmMGD8orSe0L3k7lLbyHzz5l1EmMahHWCCbnoMGGfO2QnxV4v9YcsMmIA_NX_1CjMUh_LYKrVWE2tfmhj7Zdprbop3nTylHV6YNet5h2MVUtpfj3CFTz-7V0AxKhqmTkSE9fMv5_XY9-QxFKf9B785SPTdj1xBiOsQ0uz3TJ2CPFHOtikiqYkNu9w2cUgYejqlM0crBDpQCuFmFJCFNKrfMa7eue_4H3RSh8Yu9Yw1LXbkAuGoFMGYhegcBEvcxcDSHfZ9f1HFT7IgimpuFuoGHwaNhPnlNc1uI1ILsFeRrrXide0q3L78aMAdu7eFfSSXHm-RcZypE9LHU8caoGqd0cr8hMAFvmAacrXiUE6RtzQUZjswSOziVVwlqyszgPXIuDsA4m0AcaLyEYQ8fEsRZAg7RyRbTgMGrlo-_L1Me2JMPPbiuNi2EtBXz_85Ylbaz45KQ45mdka24ouxzs3YK5aPi-Bv-fYL7FhoIWM6AiJH5ETjucj9KrhL5u-mnEi7sYh6ttj6I-MtSpCzOLrIB5HZ-tJktRhN78f2m8h6N4FBL9ooQXR4Y-QC1MG4eRlAiugn97K-r3MDGQZR5fVwC8SPW4Pt6UDvfaxXZek0HmjYPEk63MIxeMBOLaipBGR2ziR6YsoTUZ3NOopXjZr-UsGukdLw0OIJsxA-nGjmOZCr6iDgY-EfaCAVwAOxAv47u05VBTOP1xoUhMrxNefZ1lt8hEziCDaHInMkDdc4lQVeYv6H4rR2KugX0IXGsFc-C8sfQVnALLdQNjEg8_AfTsEmY3NqE_ECIUhFwxaW8s8aWBgX97Pi8SxkCwX6DyksH9fjA76rP4P5kpWl7ynaOaCfytRliE4j5uDXXywFfwN64DWKIQt4u2gDGo9d12CWUMGrWZZdn3qn8IgEDmUdr_CGXIGcPNuS-wxWoh4G8eGNhvMk1V9zhyhcxgbjoIJLl1T9MOZZ8JQVpiy-cPgClLI2jgIbKSVZTTZ8B6T93aQj5oEbOw87RZxArjYP2XeIHMNh6JUUOND97h1D-tXlI6hlFtFTouMxLzyOpVJLfdrUcr2p0bkbNPAyk3qzxwdRWegSWH2nojJVRP5dopYDUvX3a6sXVGUefUr6llKEtyQ9W84oVESDWyhWRv6GiBkpimAlkoolaGYFYCD72gUISM-ptvaWmVvNmXdZhR2JCSn3Ec5K9TZMg0ArIgFvnJeksow6nIwDSYZ_EXqtEgn9hjLaOcKZSrixLgvGqWY5phJcyYWP7kBsJTxc9U7xCIDh_RCU8fjZzAOAl4r3DtGTEntqzqhScZ_-Fx4ygPgpi4Ko84FM0RvNQGw5VSrOWADroETQVP-La2KyDOjYo4dTauA5ArmYnXyLatcyfbnvgE5KofVhMHwPq-QSV7QAaN9aM3KdDRxBXV7YtnjPx5DzLQE_61NLQkdC0iWFjHwLwM58comkNfrKAUw3vtLzWDiLHT1nPG0pxYBn0zAid0cdOFJ3JRJl2F6-GuMSeUK6kCqbX4mtShWXp1gn0YErlKR2PFjCDNj1o56a5ejMOYAB_SNIjRLO_O7uGofXv_Om9Uevp9XKu3ca86Qt6uOpwQsifkwS6j78cGRTJeU0SlIAGBjzi6b4aJN--CpFIqF6JpuZAxhiLzsHAXRAKik3Lu6Pmb_24KBL5_ktbQRcQX6GQjGi0A4gccSOF3hdJ9j1any3RaFOA1_0HRAv-ExWoiQEyUnWALcqaC1FmXgDTxYx_VUMjeb-MqxAV4eHjJsR7e1q9cJS8qhubSQbHMH72GccTJKlZYdLBHmc0Oqejf-JKgaBMxgkGX30uCXhT9B8dag8jVrDBemQV-wak7QHgbAveaWX74ZsZZF6ZuZ6YU1llAllJlLWPVNr4aaPj_wMfurz6YyOJDnCcVxcKFjBCJRuTBF1ACh9Ye1aj5wDUVwjeKXnjEy-quQNoB5c4clujc-G-ep6-EHj6WgHZefu1HYolZNprU9zHY3T_OrisT2jDBUByHv2RajGe3K7nDZprR-e1SPApINTcKQ42Fh8SfDQsXg0qOfvMdKbfKJqQizEQiCtvkQu1oXhlO8fC4J5UkN3qsPcdG_h1TQ-_zlAPDJ97B_92zV5NkIF3XFM2iQht1oWwZdN6xwKeDRqKmpER-qz7bxiy9Hh1IxU5T_Ac5c8B5xIxbQzgTJal2t1M-_cRvGT0CjpEBjRxqts-KliiGxFl48wNePKySRiGEfnn4Xfqmy4enbmmZgyHCmo-h--qxLIxBEykrcQurpumcrK29z2_jGUNichMpAaaT3UlzgVTbOVb3gVN3Qsu8ltR1RtlO5DM_Sc6q3GQ2QpdHafa2S8Z5D_A90PuohDCpyqvS7tA24KNQEKYM2W_ONMBNNEoyU2p7hZezbbj5T_HLHVRPUiVLgugGFQkNwZ5cRgrgYqstoKu9VJWFE-odBF8G9GwHGFFqyCdBL2CADSx9AnfEssP0TSarXyn-ALo1n5f6vpUFmkcuY-4gFSang5orkODd3k7hSmsCxs5NVMLfQxPtjJcTTrKR04H7xAVNnt79YJYVW73UaXEUammc_qu0GAuNwgeaX3wIQv8ieBeqJvGbfOoXd-U6c8b2xS7b_9BCWtTKZ1A8azUrXAqOr5rXlKkq6I31ht1XzyQAWq3_YWEc8MJahqr7bR5GQqOxRg_adTocY65i1qhxebStP6XWRRurHWyHzDhi9duKfGK_eC1bbuUIevXsNDHdQBDNE8_w1BBBlg4eFuM8vSDZWJEKPxvB4Vl7ciLOs6-diW3bj_JDo1BZlpdDQFKCwDuk5RtRJmr9hGUaIbF6nrjbFduzQFh6laU7VkD_3XyqJ2C3dCD1vOOhslfiVG1fBWHpTJvKsgfLa0u94IUipo6YWCz8K-LCeOymEufdrfaI1A5qutL6tF0CaPl48rmLRMayxqTf4ZGCCDe49C74wOS_kGmxchhr8DKGUgKwiWJWQjIQLIk2PzaHSQ4cE8uBQebBsCMzlrzNr1YhYzvzhje-qorpNcwCluQeaXkqp1WST9LbExS1jN8gmJhLgS8yAOd_yGdJchugXdbfPXWD_R4oVf40bCAv3HBB3MxQKq8dZeXg_9xqr_bhwqY1oUraAHLEol6kUS--0eDJ9PzaLed1ZQ_6j-pHR-mu-OkQUvtM-THVLuNMKWGSYKcBnOFYw_1NpEkwoWtcYCzk-nq-aHJ5XnijDKutRPJQ5W6RLMmhB8qFoZpRp_aDS5LJiqp-Q4g2QhtSCckgUwHN5GSDTLaYvjkR5jeIDI0Df_tQZQv7BiusW4M-iXMunM3qpOcdAdfnBTmODqjdeBAk4dRnayZtb2Ib-JKl5ywa6WUDhpA_UQA_sIlBBbTjetvlH2sChS0D17boDPANxqPYQLorzUflL42ay1DQFsRRdnxTiNvzN3nMOxzFdIUYqWEiY29KQmAFyuERLmtWNxvUB7KB9WqxV21mbJ-yIhTsuUTHve3HdcJuWPzEtbZemmvTyJr1wckTGBWVfeT20e24dPMpBbRN24Mpx_tMxfsioxNsXFYqKHzqWqZ8Tp-gj0TUMr-dATGUJHHQ2Un1nVUYhOfB-G-cycBf8zmgcnA9EsKkTOlZY1LRmvBIknw6thweHCggBJ8Ke5N7lgYjdTTPs9HXMZk-YcGJ8Q-TkB4_Dw35xq9_hnncS-Dl-_aTs3FD-V3fAbAd9eYbttpwk9kwVnc3GzF_d-eoCntwtxNH_iYmdeBZIqLZAoDwzvFnGfVunFP4RiUtLYepxu1m7HLhPSCAQn6SNcLwGg1U0jQpfYIYGZTL3Ntq91XYv3J9vy5O1apgQZic9XEMxzOuoYf0zDEU41PaVOmGv-H-mdrmH-MI0AquibmsDkD1GoUssNDqsqGVBgMMp1kc3N6irmLeIpdrSjOLUsW8eq0YGWoMXXxp32wIfDr1fad4KV22Slqlrfv4RC2v15WxVI6j8Cn2l6ymNxCj95fk55ibBk8IgObZEwbu-O4F6focQnbqXcLMSHipxWVOo0PNAnxeG8ER8AuVaimP1nXVWhNo77VuX_Yat85m9l4Avt0Q8tR6Rpqruw0cxZRH-3GRk97-svz5QsXMJgNZsDquzmeRT7ydwFrr8NK2Ei9NmlZ4pziY4xgIjVIJgIhgkY2wEH9EBDPLuqmYrA9z2RC4KUg5aMAvhRRZ1Jrxd4uv6C7iq9o9x6AOVwA3AzuM-A42325s1cNlnURin7VjQvoDg03eXsB-G-iSEUw_WoiFatKsO1U8bW4GP1-XwaZMD2w9-NXF9JCCGp2PaYNl79WZXpoNqtOv7CS-USx0vOF6DLllVZebsUhgMTBHg6I7dmJShzC1VLrCV_XjFCVlxfSdC-HkHceCUwQwQvkH7CzkW3Xxqn9onVcL1vMKgt-D7ov_952u8jsS6gkzEkUZgSFKNUMJGZv8J1rhg-ZNUi_50EsohJTlxy8H3xw8RFN9JsTZ7T7_O2yJ-yB5bCdSHldOwfQWtPvCw0df7yzUQtkMqMY384QRdKraWO3CwhrqD5_j-iqM1nw3AKDnqvUZ_pL_MrJT5OwqvaQLlIJpSymmfw642aXt7P1TzzFnwOYb0Myjc0geBp6JKLB4MetCiKUxmYP8M3hiH8FSZLv00jUmVJj-CPVj2IVml-IiAPyPU45_2W_Sek_l6JDqxgviPNU2QfLqXLOgs7-30-8ZhrtlZLC1AYco0hIEyVvFBQC5CjorAuillJuZ02YU5_kNwGG-Avbqb2zLhjw3gO7ZB1Lz68cv8F5YVsUvCvMgRhgpr5Wj_5uFtw23HGXHKY2Ejm3Kjya_Tw1EbrPl7t-UYyUxZkF6lUh-ZnndeOB7RWVO9lDvW-kuu5XuYFbAM6ouYOPd0Am1Te__qnJe0cYwKBaqopwTCE_7cu9EH37OBm3YWyGrthggmOrcK9jSI-xA40URX30vYvyuvNzZ-0f8PrZIfTtss2f0w9om6vDpwxsWhXRlTyz9qc0ntEgVwX6t6xWklLasPIwXZpahtO8PAA9Vqy2D3t-nMSyeBaPMhkZi_k5x3ckiLR9RHH1OmiAyYkGafn1_aB381MKMv_8AS4YGzeAvaHBwwfNDBlPpBhdupAGXoGPKFCM6d5W1QoDhwQyIZ9uFKuvoPtxntY8MwG5x-Vwmg3GhIDiSmoybRNIpfIqXUVzg5_a9p9b0-Go59h9B1ntMB0K1Q0X1EtZq-tVRlv1MRpSjOl8LFyGFQ8rYS0aY54cZgE_tdOaozg5NuXDJPQR515WrBf6NyJ2E66D3u1Fde7hd-zUMSiASQXMKwCLOAMNn4f3MWoj6UR3vKPjtBNwF1umNrE8P1tErywv40kYGz8-Zy5Jub9dMgKEfXbz1s6XIqZJEDSXngwVYNQx2fhaO-uGxt-eahjkVAkt1KoTe3sDxtkX7CFQNAaVBlsy4JEqRM1-Mxg0GfAP6M5l6MMhbqkJoN4oC4TVUlASghOUHqkCorULtgKctw01Ea9UnPzXz-KKpA4RllrWdUryiRH2A5RPs3KH6mTKVjJmzXvs-tHHeQphSLLm3QV1smoj9Z-oAJrz0C-f_Y0LE4Rsaw8Ag_7G9OOrBOD1odrNT2PbpvyeMCv2179maxKeUB3WRIU_Mz8b4_vi76gODzX6t-K5zDm1ukMlpNLfRtD2FZOEu2S9dGFFy-Ut3gB8Vnu_b1wnzETDDqWZJ-6bo9qRxrRAkH6q3TF5VTKv_hnYKY6QzcmotJrdTNPQvwCztcqj4c45FtJyax2tdOQo4lhoqDapMA9TawQMxunVToG8YmNP1YKJljFq-ZFttAxcnIpaTYq9scd3cfS0S63cnjaMT_H_LEBW9FedIR53Ko12fyQn9cLgErigUWMWwgdTmE2rPo3ygRky06cEcrh6zUtNb5E0Xt8FnmR0n53wZbJHsX9N6ficGSVwanB9ZBGJz5TmRHdF2aE6NrALFCVLZ_9mUP0XVz9HSUH9YbauXqYM8afLJ_R8XNm1WtqX6gWkCG4HulNtWURyTWgVuQT4jiB392QSDulnwnUnaFiroMxbHD6UENVgg78icspfeRQ3I_wEKLpCmngQSDvgNlV-vzVct_920i-n6DSDav6Ez6MgxCa0cgrF5Fbzak-koA7olgU2xqiyoAFv02H76alrTcE6Ooi0zNIBABz8McKSqmJDhJ3RTpCYQCmJ71Xq3xdeT-9-WBX9QgNEGQ9BAcZNT8IHY7yUocfYNOQS3XbCogSc0HR260BC8-8ijyyx1RfZB2kErTGpUCo3FQJLg8QNYU4cThUe1rmgzC1aJSHdYD8OLKHflJCHZiGGaYW_MA-tBWfHiEISIUcIghjbVjF2dBoMZBW5hlzvYWOV5y1QXW0zvTJ1Tw4R6kJGWNTK4wePkrh9W3t4wMu2QvyJQLGGwb4ltSDWefD44MtkWdfquG7OTbXqEiPr2KreJ2j3DASXuBDBD25RvlZc4bhLHFj9BUJ-lulsAvDWKCb2Bou0i6akOancevmmSZUwphs-hQM2b3ugNTsgsUEoF82dXWCJ70gyr1RFBfBsZCYDMDWbiqMYC221y5Pw2zoHRdQ40xDVCmTzDZZxzBr3ywIcE0Y_6c9tlm4e6EgOkdHg5KaAV9sV_uMLbBeSxyihQgJuxA4dzQnCo3Q_owAGtnkvhQp4UgYlx2AeclHenpTuFb_t-BsO1-DV6LgRplzfXH7ocQedgUXsd-gZtA61tnwNR2qRk9dbmtOikjI7qf7tFv8r0pRbe_d_mNadmgformlLzAtUn87xkZLmcMx_iH0g7gW7gbEXnkKmX9syage0xeQ12qnGvGF-p6mBKFUM7d_8ZBFt3pSd0M2Wl1zLnK9HQJVPXjWWBf8r9UecYdpyhtZAnxREWSqG1APYDP8cPpQcewy_QaCnVqyYZRFkf6X6ch-O9sJAwzR4MLElaZ31KyCxHTj8565hGC5bJUdg_I91UgH2yJArG54y_Yc5Dl6ALUn9QgPzbqDFFUOJjwU5o9uD2XyEBYzEErekT-GqxtSGOgCFSStNay_o8OmjolNWZVRc1_aFeMUOgh_GJCAnBMs8AVNU8rG-2bL8Yn_08Lfn-QpqpZIZIVsTZinG9cCIy-nuGGUtwHtPdG8xntWD7d5rNUtro9BCoxdrnbFOkSAwCQ365HHDHG-D0bnxTd70UQLYZcAb6rkxFrENHGBQFl5f1sOWZnGhofb6snJCirTWsgJcst54Dzu14XaX-57i-J3gi6pI0alrVQhxukhTtV3oj42A2TUGD6Qb2P_PjwhVbwpyfkd9tNTRT4YKbB6v7FviTl7JKRh_lMFAeLiNc10auLFBnXOdq28pbt64ilr05QoEABo-2qj0w1qRgK1RfdC_x2WRHcrI7zWIyDONsyqumIklidGqrEh8EXCSg3a1PBLMIrUfkfyV8C7LvTL_lifHl18bZO1BJtoksrMcCmPiwEJhCCMn1olm_DSh1YHahgEFrP9PhmLrFpJrymDuzXlWENX0QfqD8_bsiaIC7sqi4ZCnGI-KCnePmdiATIkO1ROI0ty_1kRce2LFztuwYFLY_z1yJlFflviLtyjU2z3F8Dl5JjO2dWm4n7bBCRT8wAqp5eztDZdaiuQUZKi9vhIuEnqFpL5zQVTUlDpMWodeYlcEZT0pQQamulicCkRslA7Z-CThZgOW3QWCv3eYTvOlZ0merHzQFxYq-8S_0rfwK9BEA1xck28GdMIXUd5cqBN1kUPd06qbwbCAgVBABucXvWbmkCeokCXOyfxb2BHl7381ZWy3_U6M0AnKzxhtYBSmBjY8sQAeJg1WTQ0ZpbMT651_b8ipPHAUl57j9rwVzxrdtmtai0VoUVNv4UEF6gDR_byb09xWMXgCWHrBMbbs7KNNC307cI7lmSHDwFDiWjxXcZtGMCix71kfh6uZsRBursMcnUoIaGvd_Pqv7SKeo3c1DXs8d4yraU5VqtmvHuodSmfcmOCEkzLb4lmVfBZPrsJQcLb9xFH8wunqxWYhr2ERzOJDZoLIKNwQnPDcxoK7UX_tLfbHKAO_CcfHWRgB_NkcPVvf8jViQRTrskD_19WqQFq241yN8yW4a61C6v-9og8yJyy8BWPQdiKESA180YGsfujYRx40jXR1u0g-WgRF35S97vOzm963EAkAmfCPBpRckAFxeDcb9DfBvhihOeaQEobt9UNhiDTNaiSN_Hl66wA5DIPIptw0_HQQLoVQ6HUevZymcwe9A5p7_AdCf86KBN-Z6cu7-5OTmctbwROcfjMYjlJLXI4vSE1fY_BdaYPBvPWsGaPKTNr9kwy0RyDrYd4a3hzDBzEOAGUJm14pdaOSbjtwoIJ0m5TeQRm-e-EBqxv4dcABhod1agzhWgyKZarIrtkDhGW7dkDqSdxHzPCxphtD1a7SD2MdKfz0IK_IkPRSr5N690e9kBMO8r0MmuMg85Jf4vA3w3-ywnIbaW865qXxkW-3CYgJ8RloGuBcJewQH13Ozoz1FAlt1Gt5Q-uHiMokLpmbCmvGVk7xPXqDu_sqRhQSjlEXRBjmGzeotBxxhTwmzqZfJxRXEdmGAtrfqva6gzYGgSdXFWo-_wfN2-DjBa1Z8FAxpmT-dRPNvaKwOmknS-tI5xi2i7kzmh-oIn8n-AJ6WanEBaFc5vTC9SnQNxnjnnbTu-bRMj_KlXXpw-ryvlGEGhdMOqfcgSWzQLPBSVMJpDU9rSZMfGl77Q-S3q9mRfjPnd6TqlNfOskpiQijqlKNvhC_D2S8SerwBOrWTSZ2i0W2NKgtAvkgn1v7wHkNIp6iJ9CU0mXIobg1uDrdvReirxIxuznqXyf9xma99oqKmQvh4dWfhlQH-a8AB1Hl624CTjEs4CcoZfCm2pMpcDie4gVvQiGkHQosnTdOA12IX3REq8peIyawJpoyI50ConQxCFuWqKfZkxvaLMfVAHcpvRNrNEF-jD1lf6R1emRB8jW6iQLCKYVueF6qfUsmb6Ql-gmKcakkB71QGMSGTa91eBg--S11MB79NFQdZhQDpYYc5GAAKTR3PF9Cj-xk_33qn0Xz3Xw5jRTZqm-qVcqPMwcdxcB9p8JhtWuhGcfyGmON9hM83JHg8xKGUn-1qPOnvF1yWoRcI6wv7Xe3jfo-_RHLEwbPTbihfw2H6ycYxEl_iz9zlG40_WNJwwWDdHn-jsau08fNxdR4WC9FEvC7lRAUeQPVxUWE3ziJjlDMeZGz2jy4daSi-LY-QZCzarHtQ4_olBcW11Q8gtV0lOBrkATxbd7YRAL7_dh54Xw9T6X0O7TlpofzzAVMZzIn0iTai8k0eAzuj3DT2FiCHAh4-RbKHr7mzyrPQ0MUmJp2PomCnzG25BUbYSlClBcjtotLGm6YuDPzB5X7Lu_vH9eRjxMEh7ZqIYO6m81D0dwZO9aVZSSwa_LBb1iBFrHijTsL8rHXXcBSnp_jIaZrGLyKkxMaJDegmLd8HdgACP3rOqVCDg1n_CVE3_jRaqwwHJVpani_j77aSGBmItjp7HqbcgZr_CVMCBHX3XfzlhuXZkvBoc8ZaYYifhvgGFGEg0jHEaxIIU0QDqm2L6dHqCH6yAlkkT8zRgWeLH4Pey8nR2KTAZP55YtaaU38cUPOqVlvTmPihzfNHH18h0vLfaPPjA712C9V3hvVACSpU5SsXQU7NfnnIO7_5ZcX-iCaEuDsSFlJcAJFaSyKJh5kcXsGdRCAM5nVfyH6_NFHzGiNWaIqc-E3Yl4a4pS07bpe74bsEUrxUfdgmY9XULfNwuGPVg4qBsSoS8coVBn5SxwVR6OITKjr8Iq6b8EZZxxc6qJJe2Xd5mExe6NxAW3sClorNhS_wwcBYwj6HUH8SmXpZ0xqADYVqky8bn-pa5j6RFNSH5zz9deI4_1ioLhkVtvpbRFHOxCPzm56wjqQnEci9QQd8axmpiKgHP8HnpTzLHO2MgqjjunSox4sXOz_BEEPWghInV_VpmFb0KN0B4UH_M0f9Yar4O1unjCGwlLF_ZfLfNfwmi8JoDRMYIyFn6D1PxQgdBBPKN0oC_Z11E28WQqTORvTJqusVY4qoZ4d1FOkd5E9srOWuvs0gBGweaIzUAZHdRGr4NygezGmf27uWSos68ZHaB2qOc79z_TpsXiVeik5uT-pSbt2R-GEIeg8cwCH1J2u7UHsWLmJFyUmBW3K372QeHxoW8UKinTNg4Zy6uF5acVZmom5E8s957-83Qcs_unrHFoUTPy_KWoiqRefrQcpmCHra-JYSYwNxfwgzoCp-EHgl2ypCIZ5BpRQHgKweWJWeRhioSBwGejT7evYEl3-L_FazZFY5W6tKyXFktO2jIySP0NMGxFL8S-PWQERH9cdm7l1KN849iSIqeMI8cROEUCWjUIhdh9pXJnY8vYhQBfbEjJ2fJFjOEtT8ARZe1jBPNUFdoRph8YXVXRkHn0uw826uIzZGnacbNgRwgNdilq-j1Rj5iirOQwXSQ1s_L2Y2Gl8O7YZ_tuEek0ovZnebzesmYKtoY_XhunbD_U-4afK57BtBTsmm1Ed_AwfhZNV_vqKC5DraEE6c6J_7d1f3NJEMVK-QDm-iMLGdLHjOr3bf8TjpeXNjITXiBZ0kJBb_qf7Y6Sze1UueGWd_23NVi5Ufe8w--C9fE3YT0Hl0wnSRJ1WvOGlLQf2Hgk8KaazMuCVbkNFzjojCQ_IrmsEz2sbWOSMDB_E2y-6JJyET54mCpfMYhdHXVhtbAH0sdBNtp2KGfh9206nOJU-lKwjo71lgNm4XoWV5Ux1LXYSeN9r7BSrpirkFIqxyQkJez9Ulcbiz5ES5t8oaTwCOnIDE28Vy324HhGPSi5W2QPkCOV_PjOWCeM8yjS_6w_FnGuO_26ecaOEkCNBZung5p0pHSmD9D0SeQ55YvwYvwMhT3smiwDo9dRcFa6sigkWHHKtBLW29sYLB4r5pNWtHd6CihJCcG9DTTbaE5qP0-eOF1l4GKEhtIUKDPGJGwEzYHjq9emeIy1uacdIcWTCJylvCVOHdWmLaD1HefI1tjSyga1LuX-uZPAYEu4H3BHd_8RhEhTIIR2W1Zi4pcy___Mg6UnxiELbieUU9M-kBKnEG8wm1_VCAJVg6GulXQG20z5Zq0Zr8HsRUEpcO6ULm-_3zF1WYWSPU-JDi_ZiKxGdLOidzU4gb-zzrrLYtA2USFwdncVimCESLHhKPSvv6r2xX5Hz0eTuLmhshN4wL2du7QNz_mLVnI0aIGrHWQgs_DEy06L1P4ANm_Y-0xdzookmfICUGKChRsnNFH5Ardfg5JWwzC_jQrW1XM_t8g-3Hnv_A-UzUyJWBl3ezae1NPikowsbMsIwLuHHteDmQmqb9-93yiUdXB9FxycWFgaPksF17KxTvI8FS2PPwZKsSOTXMQNCQyFd4fJDR60nQhm19DhQImTl_QPvqibTAg_p5zlhxlEFdMKoMEdSrqovWF0mKoOLbIHlGum-tDlq2Ll96PE2-CrnW8NyHVDdew8iZSZ5dahyl3prZnh_EiRB8nNBESy8uH9ppuSH6XlQ0TJXdhwI1ZdOJvFonZ-7IBR1TVb4ynvpzRt-oWE-tNx1-6qwSJGzrsKnn1EYkDQaRj7nfztiOa9af0LGUR5ejBaZVx-bQ-75PO-xBTxd0UpI5kyaEf9T3rUM19GzASEzvIwPCPRplhpopMmPORqBqg1oFxqI9vzahfzntnYmWEBLGc2ks1NZWq1gLcSZLw947_EEGgyqw51cFGXLaB1DeA85qa6WT1jRmS4Fjj747XLPynyNH73NU8RWsx03F0y_fvUpPGS_vaXWR8AhEy-gdBW5CCYbsPv7WB1Ls0_DJMBSHylHgNQvC_5knHobolZyERyyye0rwmLca0TnAJS0QhgywEwaoateT_H3_aqypXAFQdqP9aXzDLINETQH-jPND97CG-mhA5bh_mmulEvQMxHyt1e4d2IWPOJjYUvSj1gaxoNl8C_v-h8719rmYl7e5jedHHzYQuDgq-i4B8HlQxgLycD2vQqtt9F8fadudBvjaa4qaHQNw_AZc_8aWNUQ23FdSfC2ZSwJvYASGSz5iwwZotTwF92WMyzfnNvdjFyluEZR4D2RXnYP9GUuwGcg6LvtzjZDq4GoOG8cZEqgSQpSUFWN4-NUVBrb8GLY-SDo08tW7Q42PvN8h6h6cPCpFgrKFrqEuNupBiw_GvD-Ihj6S81070U74EpW3yin5jY5dVGJO_Q-8GBVsyfe9VyPGlDCt9p2-FwvgP6aMZnWAQys5HjDo7QxHaLXAUAJEB4HJatbd3sDYsC3S3Py-_NDzA9_JuOI4iqvOjwf96mS8xfOkoDY0CyKso6cn7BWBDbtgGL5yjjAOrsgyRzALWaUehhq0p48D45hMtJh40lBfgA2QkEqXaqlFdooXKlfyn0nePdsQPYJWxg4O42Up_ha9yeggy_bdTtWJQlR1bpgphhsDFFhPq3rrrD54e-AmMPvLS_KnhRHR22d8t80bo2yhrXzT612iv6Z_2_wxWbm8AnUB1L4t1pnI0BW9MLhU0EC55f52wZCJQ8wJdRcH4lbuUsZ4ioBA8J6X-UtP7YjjBTeXITfvyCaLvkwGseuU4DCiTHh6mkqIq6ynzsg9kXqjCB7oDfO8yZm82JEuzLWaReeZSub0J4FAyCUQImgs3Ui1shcwK6IVbk57-Gjywva17R7qQhkYxqeDCbrd64y3QLFBnhiYSN4TrR5AaPiNz3eCYFYPTdMjNCWa7HMb8wgI8Bix513uKuS7HenMc_h1QwCzrD146GKiiEZ0LT2IIDDO8h_gKx3Y-7N5B9Og7wjsDps624fXnr889NYznFOBwuVhNmT4aULq_L32VNXYO7bvGEm8T__RrBnigqlftf0nHzP2U7gN3kKnuCg0VryDRRs30No9mmIxpCzEkGfEDb3g8SxDiiyOjZEuFTG-doTdRDPfe8DqiPTfJdFWRfDkBKFbpnV46-Dy1PKe1HdpoF82ggBjtwT6N3GZ4MPq1UVYQ6aiwlk-vUpetZHohzn1AD15XlDE_NfnZHhvGrHGApPPUFCMmZRmqQTkNH4IEpUDQM4_SacoAIdkrgHO7PoUAFoHYMpumQ2pow4VTR3mj0tpvG-iIBbcxvqc5XLQQZhXuhDVAEl3p8HPTDKqFgxTxiKT_Ns2pfkp7zHS9-Qp6VzlZgoa1Kt-ipc-BOpwBzzeDqg5bOYvDF4mySuTfNy7RnMfX2F0WZKN0j0Rbo99iNUgkvxQNTAsicaZGuGWaUbgiQI5OT_kltLhbL0Lwk4AQpgKHQ0OBgIYC7ONSWNWlHqRTR0CGRYRPPB5tOfzJ9iVeKQKgTnH-PTukqdsxJyrwalRgF9I_b3qBXCFeY7Ea1JyqYhi2c1OLLoI8UJ1kNsH9Jsuww0WjthK7U5KQEHkQTZSjdEyoD3M-daQhocYGcPqRLqt_kfDWpA9fQYJVlMCUL9aQuMdYVz0ZzZwV4PhAoqep2MwxErhdjEUPhqyt4mVopZW-Zyigqpw7ef5K8lrBvtfLV3rt0hFTzuxACp1wQOWVsYvY36I0Yff9iHGHaOArfsR0KgDgbNK7E7D5CtFrHyOn5XGjWcdjLaYKvCJ8wKrIItOXpWEMxBCcKsKsj3bo_jJKiKYS5hVeaznfwc7pi0J21-4BAkb9Vs4XqIcooEFbUlqFSxWMuBokQAsxBEdeZ4ZEWbD_jZdx8NxELKLxPuKiYYmaljKyW4NqhyeGPgFxeHV7PC8fZ5O1Zg2sTMkW7J_BkZte3oGa9zeENRYMYmVp90gURGZ9vex7-GM362BBH-Uq9w9XYGL_yVfylRVU2PGoCEmMoxqgxsYTt6t--noIEO67jMxWhOdX-i2bLo4xdZnTBBDiiCwDLBM4SS5FWv9Q1b5NO8GL9ePjw0PEowJy6Lhq1MEBrQSR_AiNr7tAQPoJc-ltUMtBCn0FrDKT8UZchBVaMPazNXHJyJB__MZfJLc36Pr3xI3YG7C7plb4MOzJ2UU7knbHbcGM8WqKykYOBlde91ywezS-WEo8EUTO9rVUTDPwSPH2NjnuFnu9cEAmXYicqip9J5WLcnWxKuo51O53VaSXa3KOwkRsh86PPoxbN_6boEBx2b78eQOgVrE8T52OD8SryaCcj7GmHsA-nLWXhAZ98WTCCR_O3N3JZSMDB8NNKaTdyjILTThzcZBAMHpCZteh3JxXO2kiw9Q53cCVt-PNAVFwgANiyFFW00sGKI1VxK2SqsCXupmVQqzwJ_VN_KyQfh56xgMWxEucdcbneMoOWUzDZduKIBBhM3BiiaidHeflnpuDid8poBugQVdxNZdxxi27cdV7h0ieu0WAJj5G4DjNY5XI-S3cilYnTXUNg3nE4kQb6jVsjVPKwS7sur3AvwPld2qHJD5Zo5_63axnH-FQuiA2oF7pZxoYiz4IYY94ydG8gOOYteoiwEDD4tDi9_p-Vh19qsJ8NyAaC3sO1mKZUhLpGX4W5vXI9bONL6KfiZtpGsNOS0al73DiqdLiFtAcp68geOr3ym7Miq2xtthT-mCiNOn4HugT-rogZbzPlRK3aHEY3MsLL2BBcPue8ffnazWOosLQuThIGdGwHxSHwk9crZito6H3rfhy5FQYRZELbjkp6XwSzWqwGNh5PvS3a4WxLOImjdS_SdeFFztTbz643sos675Aodwntlo8e97352Zl54dJVBWQQQXZe92VNcHdywcaHzSA2NyLRWz9kJA4R4jHUBq0Kd_y-f_4LZMgcnSJyB_kxotskTdJvy8K4VSB7NSgMxkfzv-DWokMaWuZ6i9lhG6laXjt8SzVmZnBXx2fcGgveBZ0cEEy_ZAjwSaqkircbn6rIcmwjOLxsSvcyHHaB4371u2OZzhoM1eRQ6I_wXHJP2FW4zESJYPOhSWtJ6Apz4rHoUnlDCcg1MnT3Q6PvRNDq0jB26NCCl4ixvXlWtuWTa6_bXBARoDauSXsf9YAX-vnSTK2lOz0pOWgz_QjQw0Lx7nEi4sMXdnGvQNxkSiGAmExZzqAPZwMGbdAJUnjc0jW7Fi28MG3G8cHvO6fcGMo-IHUlH1hr7vMVCViYqjcZQOJ6YgAQNQNe6mXCcsSJij3_AeMXOJvC55N2l9GkRBkByX7-NO0zWRMGZdtYxe-25RMM46v4AZi3A2mH-31HphZ34kIlBH9yb-8Vw4cdUHpY42kEhnXusSk0gx_bGxqJRVVpVgo0EAAAkhSRkWSqJiccp5iZ1yZ2EpHOgEM1vthLyCualal7K-fTHBm5jSjNqNNiZ85xJF3tbnHSjLNdQ-sYcUnhDFedPfS1bzfVZrJBfzjp9_itNRPeJnHhYGe-K9d5TQqjrBAtwrGnMkGhpegfK6Ac2Nklvcl-yCdX0Fx_OYe6peI4slr4S9XmZBj3ZpG7PX4NdyAKDu0GwufKIcSATJlFk-1L17vj-b54H5iFj5472wPjh-E9NJ2UWS5GbEC8TPpqw5wQH_Q4KnOIE03lgzCcImIKW4jK52uCSsBljKI5CXQzgTj2lR2lf7OqqEwyuFP6KEm4Gbd98fASaqrgFmR3CBqJfFkaIeuluglEt6hbkIQU4KlhVJ1kwkOq23gcjyxC4TXYEBNake_62MYh17xz5yxky34x6cl8B-e14KXqOG5qG5ug3gsoD334ICr72xkt-m3mICgkUYOSBE83pb2AA7YuW5IqwTLStyt03wQhYmDXd_q4FBM7ZO-uwue_cT49vvpDHBAL7zwG9if6P_wwVVqO85qFfri0-S37JXpakkJ6_9SUpM18Yo4g2SbEoFLE_psEgmhRAVyGZjGMCU2Yb2Nh6eQaVhuiciWgij3Hf69IJYKZ7dgNmCuuTMp_VlJ0_bDWGlAQZUvZoXemSxVUvOEMjNj0JxhAnuo6Pi9eWLcpy018a71RUAcCrdI6NLvPBNr6qYJgZL2YE6lLe5kN2xxuxtNIm0PdkyvAo9N0OGwXOkQcY8KxwwhBPI01FGQ1ULM51ICIEBERqQD5-RkIAICNR6o8zZD-6Iqah6mvg2OOhpEWzyTuIV6y3d_hOKpYtdPZ0tYpmGdXjl0CM6UZmUyAxk43Frunx0UQg3pA_Awwu5YhXCPek64_gbjQve8bn5Dxl6ZAvBAk85VngWQNtjH4JNk2GABmghnZr2ZHWhO_GX-q3KKTyOqbUjACY1il-tUhIs0TkcQqrYLRMXRrSACeDKw1VWm6iTI_6IYfcUGs_H1Y0fgyCSI3lq3495MNy-dbp-G5WiAQCZI_mqzoxTcr0EifYsDKQuzpSs4e6e4beFerRgJmLVr9Jgo9heM988Va39i0Vo0AEIPlaZqLXrAz--eT1xxSdBi6JlxKS2uzYsl800ySl66rIKPUoXdkVni_F_20mmkwEGCAQ4ZJS1g52aDOSjCYPuP4nUfCCL1868DyocogHBIwr7PCQ4-_0e7rKflnzCoPtETbNRKJj55oRaiAlFdqaTWWSMp_LjH7w0GFXxzTtnuur3GA3QaeaCO9bIPf-kiFhBArunZ4iY6SdxqV2bu3ANgoc35zfPy7r4wZDnS2BfHFn6KXRHhns5yN5U-OVjT2pIBWbLxQj8J8TOrSGYkpcTwJ526XWPKA03qIn2pOEe4wUDkW0tkxyyIgt5cCjSPWhhQQLsYYKJ8rk2ojWvIHSdHSgIof0eVI51RGCW4jcg2pJ3I25sFIfpgqI5QipxB75eTIB32XCBtzWmK2E6dPAQfnHNPYITbjLmOrH2f6zbW1_LJ3LVtMMijseSomNhA0v4KUEBy5aOriMgwBRc2doCITBcWz0OD6TCXbcrNvW7g6BDK67Ym4Vpn6bl3B4tIH19TNQB4YhX4z2kAyhlOOlvwqMcfhtdiNxuSZ7BAqQYixn5dDpswpCqiI_MjH51TMikt-YBBCHTr-RGRIXaWxk2sTl01agDUdyWGJ8wsP1f0ndpLm3fHdejNab0MOn6osZGpP3ZgZIYoX0o7CoF_5lVDdc08Dt7L_yEmzk4ccF-JQ0JtbfYdzvc4OrUBm3zQfNVsdw_AQHE0H8y3wolZFgsPzAOF39j-_9SDKkZQAHkO42MKEBuDYNRANGd41ztyybua00Dn8XEYC7OiWofp6CNgeFts0oXhYM7YU-0A8h4n_xVYrk-0Rb-zpprX3pmPsLySXIDR0EBHRdi54BjFeutO1ODlZUI0JXKinpc3TEq1Q8Umhk5Yid-CmzYfaVtt65hsdKIybzDgZkBSqOZHNlU-qgtHZsZjB7HhlsQH_hsJMfO_GDYmvUyL61zZ_6i-kzVl9kQzarBALNWbFaReiu2SG9cY4n8raKYyXQxQXE31wFUrKaibEAXJlq26xQzmZmf12t4-3ZVxMi15PRbREWLYGzqNRARqU3mHd3_FPTeaLxcWy-KfufvSTVOIYkKoAXAbHfGckSZgQMlCPqKvao0Lss7N3bdcI04kJRmOcExYhAXvepyznGreKpfwWLm2YpoPgFuWq2cbkOg_KNOxeI-SCe8WL5geA7u7S-PPZZ89jarsvO7kPAIQXxHg7a46y9wzDLclZD7UcECTva6MEKRlMP5zsg4EfRkmZ8AQcykymQikio50dvSITkyqtD5XLkLYv2eypab6-1CHu3z-YUQSHYLOw4fsU6dR8lToK4I4pl9auL2j4z2FqwZTt-wnGkTXTevikprpz7BBaY78BYmJHquSGjIEoy59aBoFNWsKLhyB7r-JFAVRXgZAspE59-JmzJVSIfyNWXThYFzabEXW2VmUNRAcb2pRUP7KYWY8xqgZTvQZ2mtXQBY4GpAoXR6jgH-fmWg988kAQBxRnDoZgb0VqOUNQK29C5BIEt8CsHE97YSouTsqqGtATh9YQUinkIpjyHMAYRfnkMiywoFYeaJdEd4DFPIvJ_MmDWtg43nh4dbJahewqSfAzmFH1B-js9WAG7bivifCkEFdHfWcyDybAKICp2iZ4clqNYH9EoSgYJuDnUoyHrBvhWbaG4CZFi6bALdp68fj_7D6MCId76bo2D47SRj-q6bzrQFHvrbfK86EdM5KbJftG9ieNvuE7PjAEAheezl1fxBBKKZDCnxPzovqnmBX3mnEy_giFlxpBfUm7g0ot-FrszjXCMAcw4PNQchogsmtV8zQ8XZOo2Rlay3YmS9-nK2Z1jEBXckY8C8y2IavccKdbWAOUidl9LsHe0wLA0tC0YcAQH5HF1yfqhXeaUXmVA1tF7vJW6tBMsm443zWLqD3MvCjC6DoUb1O6IMaeSwvS7spYGuleZPr4OvXuWcylIBgHS8TlIwoo4P1zBFAlYOYCGsulS8TBKmLxOWskPS-grktYEBBK-uDxU9pVaKCMWy_l_LV8-r3z2HRajh54V3cEsSiG5CF5_EVeFJzAzQTGd79k-AjLERnGw7kNMs4LWMhPS-00_R3nRt_OPxiVnSY_vNyT3HHpf8Lf7NQnZQQ7jM6d3BBSmIUlvlECPBpaVgP6oc1FKSkSPs-6DGL-DkJW3Xo0WlcJKwl7rIXjCrM0t6n3ioRNkxBOg3grZKqF12fnWOn-jtqr0V0Iw4Lf-3Gh007OcyCIy1-RENp6DXM8JKsg1XwQTo7OfDfyf3ZSDWOLan4L6hrHPXKBKtk0m1fJvJQ9dwEM3jzPWJBilBQDI_09Nr2MCbLzNTGi2wzGMlMt4B8u7g6B5wmRWKDZchS0pSFgP8B6maEEZ8JH-c6p7wk6YfeMEC2Ih-KN9IEUvnsh-b6jj0FwcqtpWKlHBJFWJtGnXMT8rDuYX5Mm_-lAWornFLriTA8I9uu1ZOGiej0pWVgoQVWFawXYkYuoZRW5q4OGBwpiPtZIYAyDoZeAUOu7FAqrTBA2NfYfJr9vsXJOaDiYPDHRgf9IPb4xQHM0YSgpvkCDTERAkFVgQ0lLemlf2qcUXjgmQg2MNuI1NcMCu9A9o8-g15M6Sswsu2uLf8PD13MAUsf2bSudfdKaViZvkMCJ-VgQKsy2y-9J6nybC5tzJ9S3yfnlqMyHkbrxFAUf7NnocSzZcRtuRUpuGZsx20gb8xHIA7aUuwd41zsDvsOUpovILruvtFXnA2_18wbHXFKUGmKPHYYGLsz3rhJNtjs0dZF8EDD2XVmxsow3EHn4CXSQkJ8x3D5sDdyQE74fx_9l-BybhGK0-Ww_qLjHwwArVN6GcDacya-onH823CihgmmZKN3bg_XP0Q1c37IUApEO-R6ywQpAOWGv_re4uecj_1jmbBAxwRcvCNpNSwoGTm8_KSozpV6-vadvp_RC3TDHkH7f97yLxJ7ROIt5J8cQl-9eNJBHtVvWv0H0oe8V42gg4FsXB7_Fv8Ou9YUFWaJYb7FVU3IyWGVNYJyPoT662ImG2kQQHTzoNdHPdqTT_kh421XyfaJINAHA3KzKTcOq_4uNp3hq158xepsHM8HLizQKPI_oM3qvpSMxj-BuMVfkDGTnsX-JLAe3NA8yuFiZXyziuYw6hC4rMLuV5UTNJZnGS-3EEGSXXHCfghBQslnMt4jDj1X9FYwL8cJCmPPC9sEgpCfBdPYZCJUjoxwd2i4Nd2vweECi1KOOoFCdmTcDcp6WmlQxv06XLgfCiyC50yBmqw034Ukq2IsrYFPDsITQIQG_HBAe6k-2dxanLxJGlZK6CPCx2MKGElRlIESSqa99pCuUgzdvs-_ZbG-fjr42LTHtP0hHJy_ngCjrt8IgDmUKI3xEvlXZRnxnp4jkH-7FwZoKkh01DjFYkAscw5BjAlcWFqgQFnqle20OyaUTMaYIvjf-0ZUOpGi_wab0RYW1i5s61xvKyIk_2evZ87LyS57WccbcLy88MJ26kRxPMf9rOcEetd1aZxykk73d7A_pj7zxIrvjeExHyxUrM0XFgLN79kvoEAhyhFdZ_FZItdc98yLjaToxZPORBhTn1w0nj4spz5FjshbItFfVLfGCsAxgxRI88AO2oB8389PNPMe8tA4uMPMC2PFTqK795Hek8Vos_khmzeiXwo1BQaVfwLglOeKhUBAuoVvCyh93vTjhapy14oMAt24rP1eeHnQjee5Lfb_8p3gXOMQ39yxQ0Ts32B-CfxQzbPQrRQtJls8Y6lVDr0oOFz1gMHDWRrzA5z3tqHpj0Cxe3R1luIIQ06DHrv73dswQFCY6mYUsMfumIz3WAO0sa7s8fzbGRpG4zcA5_zxQpkwOEmTbBf8n_7vCRaS3weOMVJBuNSJCiQGBHR2eESoSSbV_ESxcoPGf-Wz_Fam4chWBty66ZX9gMqaAE1zWKAGMEF9zlemaUpKjF_NQJkTSbvh94a6Rtr-WR9QhWFzNxPBPIxItxGb5yNTiGZ6Ie-tQJE2Kyd1SmcfUY5fJnCdItfpnyXL4WSAbSsob9XVg4Op0uBGG4yXL__kme-X8WI0wABAACDV6iueeDk3PptXUV0BSR3PCdB9sa2FWGoPt81rhXS1voD5ApICH0CYlLLFnsnBNNi0fB0f7ZKC8y4286yDEl0NhkKDvq2n9HkwBGA_oiFOcGotvk5QXufiP82pBzLwQOow95Fx6OM7HK_uPVjzxxdawXQgSdHoQiMJwbUK2UYbfr0iYvGr8ERELWRTOOiBcZYsSsNhYHMvwVW5ahDFqpCiW8JJOq6gjlJmZ3cvwVWD7kgLmJXMnnRqtqaYl9Uk0EBEw6CZI8R0Fprd4sn-AM5SIgL6PkVm0AsR9FkBxFO5F6x3-DMWIZnbpEFcOjgpkwAtbmPtesiKe7w_XeKXSYKPfzCM5wyVZ7sq4BZaQSMzOEOgpFp7_W4kjVZuWL4HvPBA0eaJkqCCnO9CvTPynRPisSgqY5zcysrcKLAAHSQ247c1yi8smlgYsFznlptT_2rAD8h2xfxUSv9KDaokZ9LROVtS1pGJumZfwAKuHqEis6B5GAG1uZw8SgmRDB5-_dcAQWOP6jgn5PBB08RKA4xGMxzHTTF0iQgF1HMX4ScdvPmR2tC1g2_z9NYw5VvHewjIQTVUgKhl6WkLiggz4qCItjEQ-sQaFctZo2QgTphAAhAPbVVKGmXydWSPn9-MLyRxMEFd_MFPx0xEKWUtWopZnXoAnB6cuRUlaR7Ex1bd9kSJeRT-zS9vg6SmVVeqqF10HbBydZAp2CPsaAXMzrohNXkjT1tHa5DFsGCWN8Pl96gZ4XU0hcy0-v_g66wmMXmP7XBBUEh8wlJ2tg5_32LC9uz3mUecfSbUnNnM7jzPEBx0MWh0T5W4oXWkjl0JtkiRFaawUveTNuckzEnkGqxWKC3Pfi-4_c19f14CGUzZTVXhAWYKQD15Ldl65r6xU7U87dFAQUOHcEY6KUiQ-xEZztcLU_KDfunv1hTy9IE73SiYpIvhvSeus46KY7z9D_G1Hw7nQFhHgxspVLEjejdXY5Pms0wE_YhQ-bkrCOPXpnJxE194xSi57ykPsPH5TBygVP_fwEFAdqOPwiKKQ4MV-d2G2-omn1DCyqoL0Vc-bvCee7FYytR_RFO2_xikbrBZwnj_buFvANP_K1TtKf04nY7mjKJiSbrTdpywo8PvxNB2JpBD9gkVPuA2oMFvUFHHownN0jBA9yWmiKpQTY_ZqT2TR2bmCTmwL3sZEdPVl0oaBlPiFZbDTLGgF-4fBlm_xZl1OiAhj4KxXwB7w_DqvCS0V34A0o-Su4VjZzaEqO3cTuPCBuJRfnExkN0QMMtx-OMPaumAQSyZ7-x27l3q_-q2ABDt7hOImYxGar-1FLvfxxmv_aAUPWCKHHyEk-TpdjgaLYs3EWC2FD-DNMegViiW_kEhe5hNwBo_JVCn82HCUH14yb3mZwFNe2vAp5WvSVoSdkBCgEELEZw33U_IZSQ5fm0BtguhMiFPbE86oWsZYU3cs3LiC3hW-hEBIIiqIh3zxWg7Z8AcaoK_0hQeGI2DANl22GKyVTRdHgB6Vv2Ggz-KqB3NYkLJ3AirxooP_x_mqVVoIj"}}],"authentication":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"assertionMethod":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"keyAgreement":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"capabilityInvocation":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"capabilityDelegation":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"service":[{"id":"#TrustchainID","type":"Identity","serviceEndpoint":"https://identity.foundation/ion/trustchain-root-plus-2"},{"id":"#RSSPublicKey","type":"IPFSKey","serviceEndpoint":"QmdPZgcyqHJTiPeGMcAu2AAkZZ1U4KtdQXid1gdJQtpvyU"}]},"didDocumentMetadata":{"canonicalId":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","method":{"updateCommitment":"EiB8B_LS_O3NWo2P8fSuRwS32GODaXoLREZHdqpg6x86yA","published":true,"recoveryCommitment":"EiCy4pW16uB7H-ijA6V6jO6ddWfGCwqNcDSJpdv_USzoRA"},"proof":{"id":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A","type":"JsonWebSignature2020","proofValue":"eyJhbGciOiJFUzI1NksifQ.IkVpQV91YUV2QjctR0FyRTlkeERuMk1rclRUa0t0VXN4eGJPc1NESzhwQjl0ZWci.X94wTgzsovLEAXU1CG5M0Gqs6Gu9oHklr4Zn7aEbrdtOI_WCSCrWJuYomkcdeF8X5dV_ApZ6Gh08pPcV2VSClQ"}}}]}"##; pub(crate) const TEST_ROOT_PLUS_2_BUNDLE: &str = r##"{"did_doc":{"@context":["https://www.w3.org/ns/did/v1",{"@base":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q"}],"id":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","controller":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A","verificationMethod":[{"id":"#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","type":"JsonWebSignature2020","controller":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","publicKeyJwk":{"kty":"EC","crv":"secp256k1","x":"0nnR-pz2EZGfb7E1qfuHhnDR824HhBioxz4E-EBMnM4","y":"rWqDVJ3h16RT1N-Us7H7xRxvbC0UlMMQQgxmXOXd4bY"}},{"id":"#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA","type":"JsonWebSignature2020","controller":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","publicKeyJwk":{"kty":"OKP","crv":"RSSKey2023","x":"EyGvw3AkcUf2TZToBh6pddeaaocmvTuLCSLun_yYJpL7x0W3gVEzeKlj06J5Sej9Duk0W_yGhbOKCahOx16LszwTHVgnH9FjRk0nwOer4yKaKnjTZ2FlZsYI0OI__jhCGP9cbcOEd-1rfvUFu-ghsj6oHfSXDBm0Ekplkgs1IktoicuMsF-bD7I6tZRpP9tqFGqARUqvR2daQN-scwYUNsv5ap3XakBCDvOCBc_rPAwzapY_nuC3L6x60UGBAPtUBANdaMhAU0gxd-3JMjcSjFgwzAhw5Eorr7bIp1_od6OfBRYu3sIkij5Es6RDBLghUAx2Z3dznniJRh5Xlx_8zn4SYw_xhV1X04vY5U4O7-7veKMqKxzzoGOR7O137gSTtBk66ISXfE0k6LLsZK0Qkzi0B6YQ0Xo86d-COFNhRWQ_Lq3SCSiOaJ4lFP5_RVlHzgUXm6XY1X0jrkVPWdT42VxGjFvy_KX9f50dOkdPJTax8bGv1nEpDm-55UN8nrIzsRODaxMBooRL1y4OxyW1tpHaEdsoHvsZrLzM5g7FB2ah-62TCGkPcG3Yx84MPp50eRPIlj2omMFxMpnAZKBSRMGtk35A6xAZUI6KTYGfNI-IuWKdk0UOn6xL8W3EwMTxRgx1v7iklbgxKuCBoOeAK7FhoOVzL5YnUCHb1NUwAxDs9I5pNmrvaXsDDLKLIoz50hRAdnK92whifFoWoJOOJbQTb9sx43zmB1J7G_T28MG6UetI4dZljoNfWpXePl3vNwW979nNg7GU3N_V8ZE_slRmUv-rAw9jD0w9KXVCuZuwGIKoJ2Co8qjZxnhZUtmi3wFJin73V5BC684ebh40fnA9z-H1Kwa3ItX_mQSVYeMV-_1fydNULsdhlEnpwI5XNQ25LGqMNb4v-YRBXLSmN5CituV9rPXg5ZzQvy8VVE9qxWnicCxz2TzFrxFOOIhNTxf-YQT5Re5HJAvdy7Y9szo-i_PgskFdVm4UxMgH9ddrFUhDPNmVtVY8PoXlMzuU6gKR-1np9J6FBttHOIPu7LFFdO0Vd_Y3-Dl5mdBXFcP1Do1GN7ojcuRUB4rmB__upRAQQsqCApGurtGP1zgtMQm6ozF0gt_JpoXgvZEFK5kkm92vpedrSfDPBBn5NPIgmQgKSYfvmWRmADyr2J9bc6EjJr1-YD7QR1r2g_eGRBE1S6dexWceWTq-RktXQYOSJBnKLSkbqJniuoA70BMkjU4Jsj1EJB7oxE41RRMchA4BRlClSi31ga0T_bk31rNTLQNLGSrBrh0x2nlG8IZUZLB4fIKKweFD9pL1qhLMM-SQl3YR4-v2wxjlMXTrEDjz2xdwJsQhhzM5trtqhVdxfgBwB_ZBtU9KJqYvkB_3BhY3kYQSGDLhyCHbjyIVYl7saQGkTz_owGfj8tD3gU9oJlZHDyjf4p9AObfF4YXKjVBpPrPgwgNd-G4LAgUOn4DAVwGmGBjQaNWiLet4g4lRsLS3LkM1az1w_KyYCX_k9bptp4qLgwV6HqbLx1V5WkmubxLMpHlbV0tZFLzwThEaKpqNyz7M5qIyDvaSbTFtQ9feXhRHU7VN1MgH2AQmQzHiygXHs5qafdGSsKoMm6c_6R2-NXl3asM1TSUmD82yKonGYhSHHy60KvB4M2rVTKRENxR93u7gaYr_4cqFY9LlcqGUMzxmm6TadfSHz3rSj53C8c3Z3U9x9ftbKGOZeybdWhYbRGyES_HzmlXV5MFY5qHiE6INi_ao7Xxm8VRi5rdaHlVDWfBb8gJENbUHDDcsKQfae-4j_vXmvq4s_9L5It5kVLCT9f5NEf7jsxSP3mg9hqgwdY96ob73GsHO3HRoQARhPUt-2o7i1JzScqRH38AeDr9XnxC2Qu4LT6ffOmMKzA3qngyxKmkvyKmIl3_eEhDxpdTSf2ba6EGOD2GuzvGv2a_P9QHw52mvtEoCLNJAslzsxwxbLSnLIOkbJca1Ew26womAjSgnNwUvPCkz4lmSNTbyF63wvmNJJeD0UgkBTb2MxDw_39ukWvH0mOSJegpmENWzMhvKvxxMgB5Y1VY6Hq06V9mcg4iD0AdI-dM646yU8iLfMAAkB-EvwUUMXRE3KGU9Kx6dqhsSCrow4QDpzk0B4FCATLwawfGc1_rxQyumhF9nagl8jP1ITcLi-hlUyrOsKfSK_s3WKTw4j9iBoBWCzHrX1YC_2UTnq5XIdbY9tT4NajRzqwKLV3aYWRnqXLg_-l5k0H2GmwmRnm4ZqU-9YuAy8MQR5CM93H1gxE7oL_IWIyH_tCXrVH4hRhjd7GrWcA90s1AFpCHhBZs72ORxG_Rh8VcJpB5cTpbQfk1ESme0-UTXoSnuLPfNIQb6I6fwFkIvBx9YL7gxaVmjHMgk9BLR89iwuo3VsEsAs4ktbFfZ70l821y6q_xmOBPF-BxJzlVuHMq9hfyYVA-1ka8tBBeEy8NJ1PlYBMiVjHoKWMfqDKo0ONNv1Il_ThirUq-MM4pc0ENOqwCYkomNBFfFHdbS8L1Y5yIruufFxRbRPt6xC1TnDtq3K7JCpRjsTqv_1_u81WA4UIlW49NaruM-2lPlL6P7rWtBqG4axy6U9WYqom7aXBW0cbg31hY39xZb49G_SfSYewGr_pelurFdTag1R3ZL5VuDTggqErrppxKIBYHQP7M_reJ8fQf4JcXOmMkUOap1K7QJvvENxlQ_RQRj10d-t9spgDv5gki7uMDSA3fp4q4gf3HxZhYwPaImQ9J44zCCLUdo5dyhHsyd9neEeBniNZk5LDZRfX66ERlj49CO2dHmHLe-YQACZnMQDDug7LF0il3QHinPD-nedAAxpjfUus9Ay9vRx6nB3fHr-_9C76qx_NjCehMZHlsAOgZGU-yjdwY2uu8lvnb8dvmCbkIBYn4S_aWJ0qIOEjfWuADwWO9BXI5uzQZ0EhKuhALABMhOIi4pmnHqCE0Durvn9RaPiFz6ZKFhW2d85ZAkks_-ARI0phaKzggmB4E6k5EV3cLqkI63Oiiq21QY0VCvc0LuNoAVYzG8s4bx3udSSORrRJm2fOdURg3wtPlFq21m_7y8D09xKpHkXgEbuDJV3hWk52u0Rxv1MTY2V2_LkHIDF6my-MZLQQh0dQYnUjDfvQ3bTqj6UE4MZ07R6UZzl3Vjw53lM2x4gI17Trma17Ag6Yg6XiQA7QqgXKWy3jG6AuBLjuYRPeYo18lJm00D1D_Z_C--D6zMJKr5ohYrTi4ea_dh3CI82xBNwjeTAd95r6X0wzC3xodd7FSWJMCgt0MF6pz-MEL_jNi6sK9mIn05U4icLZLjBwl2lObaoiYxpyWEpnuMGy8J7dM1Z_aRpYt3J-Zw7i3Yf4JI2JV9u1Mo-ywQyXgRcRBhK3emrFT2fxH8SqkKwJCWn7frvbukOzSQiKD8RFuXA-SWK60mJ3erCRnka-xkGg3AiBxxeE8Prk8EGzLcB1UDRGQ_x1PXmMNtdBK65dtv1b0jGTM_uSHFndWXOrFALwi66JGyIca2WnCfQRQDR5EPyD2d2Naecbj_jMwFUsbYCxGTc76n46c1pI_QH1rxDBQ7j1Tj_rcQz6Bk7DMTNnlTFhJn2h7yVnoRPenlNCWZWZPRpr4vnvS6Ii30os5W2QaGHI_TqhhaXRFU8Z7K4PUUUVEv6u3KIZpvcuVxAbcx-ppLVkj-r2vM061Nx9aXEBFd2whV1Tw2rjf-6fm10N7U3ssLGC6sfHRpSVcsENk-ZjuYH7sY-zmN7Hf8zOYHIAZDUr1rjCgG2yCujbdOPFtPs4QKC_cFSzbpOjRmJ-urzi7duH_vH3_TBhMzM4jowgM70l1LoB9sjQ68wzlaAs74T04IroWMULoZOdaeIS54ugR79EhgqvukrIDLEoCekAY7jAs-iNW14YRPrtdul8zVUjLd4I_X3efx-IX7HvR4RUp-6lqMSN46IfvlScl0qBY_SBgCpdEw66SRo1OAIAuTy7VWX_mbvLtgZPPMkaVheFwYwBZnBLKQKyJHrNrKRQ5GdrSnJP89jdh-o6VEqG_whEec3cB1LwXipXb6v1vi-7jxU4kpU_BTMtEChb21tRhmfKGiQxHbOTRJbHVoQJ4NFlS14bTYAEuJm6yXnIW-GOVCLvlHShp5jeWc_9vvvBZnk4C7bDxY80GxadNmsKy_-AcEFN_QI9pt6lckDeTOQxgVz6Anz58RIkvJ1oPL8A5FZOl4iYuQGDAqTP6Yo-SdHbuVOuV3aM9K3L6RMgj5Z9z517O3oqsmthQdy5xtxhalD2bjV4fNsQrsXIGuNa4nAnFtfsi0uN4ahR1_YYVuQgfEQLOGSzJnw-bQ7m8tOxlDOP4MsXg6BFSBvo0LPwieTdNbZR_N4FueA59bt73HfANTd-xz6ycnZNRNO9DbxBRwXJnQogguwZQdLLLuZjqoglKwi3gmMHvCR-3QngZYQw46vAkTUuYfdG0OgaYuAAqtsEvJRaBVSud7q6pgMqM5UbG9eWv20h-bMQeBEpIuVG08HOEc9TeUzDOoE87PzBkfBqVu_s1tyItQQ-DqSvfCQBobT1pYeVsuyJSGXuaF5MXooxYfRpsAuysjWDKDNxAarmMCpioPCo5ebD0elYa6S1KV52RN15vaAZLPqNRiFkek3oy_M8C9Fi2nLzXG1Bjn_JlKzni0I3pofwFNE2ZJnoLSVpLwVLQUzzCB5GoS5P5C1DcPDxpjAr7e8pWb0QAyyIuz1EvSssczBargovo8iNxthV_MgoN4UGY3RtkDRyw2DPcFdji7AYXw_q3xlxXsWEZMfjTlkG0FfwSTHbhrL-BIXXw1u88y-w5SvjBBwk2wW0SjPVgm-qq8yonWXhnVfu4xRLMY7qNRltkzyB5pQ44rJ0iFr6tXtKus3rUTx2PbQOPNCYJynCWQnA8anAlOiTmIJV8G-MYkP3hH3g-VZSnWE8gQhbvXy9OY4YtyqX96TXRGuHNuZBDEHiPmNAvKkfgVdGE1xrxPnfZ5eN2RQWXAf5a8xgISY1bXxlt1prbFSiHTMLnikDpYNy95JBQnPEqdIYRhgzh29L_RQpIM2ItE6rPrJCl-NL0Mo3YZNdFepgL-5uOjFilpmO_EfAc06pm5sP-g6S3vOx8I9j4JrOnhygXvZx4Mr2D8-R_7s2F5QOYKCpcYmhKSqaPbdAX-q6oNQQ3fesRtmDJIVbBmioMmu5k3C8hh_L2RNAe6ItXT7XVCo-QFQ8fiUIOMWASrYHiy8qsbX4kKQJ98v070GnqCMpKVtB9522SHxJWv4h6Kpsmadh9WjAmzItl4tRV763mNcLeidWzlJFUcfZIVm9OrWbHinBUjKFnoeexpecTm2ncrzpUkMmJghWKv9hUzk6wGkQhsps-94GvQJT2ou4T5xLpeATQ3oenwez9tEwxQ07tB7FHEiIBpA4PFExNwdv8sxaEe2Zaoakh1iEjIbd4uBcEAd_E8eE3VSEPvB2_zT8nek2I9pcHEIHA52Q2_j979f-vAyJci99RN1Va8nvk3TyMz_g6OCknUZcqkhXK3lqigvhkUBl-IxjWqagdTwPfwGPtwV3JT71CZDfBWujVMLPGB_gT_dhsWlIN-sC_yiWL_thQrkgKFPqXPwQKCyz8r_iv4f8NnJIh3W6_hUURFsnu0NpVAlhi7iOU-B0cqk1NHN9BgNbT_zU2aVBEFBrlQetG5pyxxgyDSvrz-igEzZ9oqa7-EIgNv8P-0T0IUrlCIQSfPsiAUsbExwg5JwdgdQ_gD9HUt4U2Npk03XtaAySY1IXJCXeJLp0OIcc8hFeaiPMMv7Caif9RsIxjwnikwLFGtpNy70Ed6CkTMtxBR4uShDzbSz7Hk90gu5-jV5WGysOA9AbW24iqgfgCKjrjgfrod_MNG939PdD9KOV0x3MqbZJmBLB7jKCINC2ilgH3Ez4crHFZJEkuJ_Qq-KDXW7l7hjHUG_debtAu6qI1edYP09UkgmQtnZgLcGAWUhDxWhdf4XYOHfqXxfhiVu8tF-ly7iqWkmRCqhRGV5NmzUWuwvQ8-Jlh4kRa7nhpwb7ivyXiDubq85_tKuha0qKFzzz8gFuiefICHX_Uy3xM8m6Gy3KfYirumMAkuB5-IY7Dgr6IZK8YXGLZb3QEXmOjuwp8Rmm-bMnCXehgCJZplNtcWi7eQxsP4y0IoEUsmmC5Y1as1sAs8-R9XlxBfP3hdGWbOupZfS6FmMRiGD9HoWesUSVtRs_tgOUPPVav2HRIK2CLYBRwgI1NaeRcpnO8cOye4UgRm_UF36pi3hJPfIdCnhxGeOH5J0r9zYEnTDs18YsIQedQOJ9jvGBLvDi8dJ3NRzof0hk9riVtSPV7H2EKhkEL67E5pccehsmZnha0ewYbZdgEstjzjwQ6qkZRmFLOBdP11yCDzgs3eDmnk0Ztewl22-WhhpumCfNgux5OEtcSu6hcC_gtsXQgTm4QV09fFZJAH8tyfFildcaycx0w6zG_tT47jBYIwVyEI-Mvv08qYw3ZN6558VgacYehFWake3ahdjDxZ8bO_tBtLMrFXmjRpibEIYbWZW2OPgBv-4-Z_EPXtLrDpJxYjD8bUxNgxwyqxAlyqZe0FUQVo1RTWV9hzvj4GcOG7wC-_t9aEEv5h9hg3sQXBxwKwIulPSsJlAeW3dygypohfIMKiUdjDERwhgvPsvB_vsJIaVpN3SJVfNWvMEFAIRxl0o0b4upYbISICcxav7YjxARlPcV_nqG6Lnj9-6MtHOzvmwMWpcM0Y_FFro9TqKAj8TkAiGaEMYyJ8Z5EMAsGd32HwMhmdeJbA9TxNpC8CIpeNlU0H9JeSDR3bl76oGAPDIc7bDmfKjcCL_8rZamAaZucmCI4Fkkjaqyl_k0TOHrxrc8EcYzbICfu2Xp9j5Bl_w7GErvNIbMsbJejezsJxt6CR71oex_OaL_DyxGJE6bOaWZFwF3WqhVWMoMEuRwy4Z11DIsqZ2pbxyArURVFG3mIHnBJ7ffjxYbofuuuw9Ce3S0W9AwEvXRlquPr3-wLesE-Y09JL2x63dPrsfx88itwaKSyGuJyvqpTu8NwpAR8d0bU6nXG38O2ysH6-xwvDGoeApjhGaTD71tv5hYcJj1X2M-GeWFi74NjG-PYBkamWVPk8v2uimVuB402YMgUAe5RtZcKVUfHczIcj7IWreTJr8JCLl4N_X48ji2KDuBuuaBRBUYdjkl8ltWE-AQzatqUi3DF2ZDEjEarQrk8K6QDaHNbMAEQwqxIcKVB7rX6pwR4EA2xN2VYmCskYAReAbKYyzbFKgx-_kbylwjO1CMcDTdhKYHnfEznxeaxzjwopfWQR5JQ_y_4OExcY6gh_FHXXyMOQdyzdcNMPFOZDvKAf4PiXg6BV6VVbvlssgImhEbhyfKlwhmbHkrD90BVSZOfwp0m_zd_xOfwSYckSwo8ef1K6DILkCmiUSc9wiCBBGHF8ex_0u3nepPICWg30NqJPii7moRYlXNi2hKgTB2Cy1njuP9pNFSD-8cOxrrAoAz6SaxdS4QqxjykSaRko3FibccYcSE_fkx7_WWBSW_1GOKTqQltkzHWMqTbu3wEjBAbnQjYGEWn8aTNzsAh1pezmZurCOdi9uL-cjIVavKPn23HhHGfS88f3pRdohcdlszyc74acnD6VgT0VnArfeYPNBWcliVDnCE3qYSvter4l5Fe4rH1qDISEq2ni1-uxNRJx6Ck3-5bWSZxHAgvc_2gC2O5qc9TU-akXvNSqLmNtKmO2FGFtBltwgyLc8bVWAJrNxuWQVCUxXlfSkxaGXtN18lGJX-SvmRn5IsqfhUitHzJjEASiI_YOVY9OoGEkK1a532FFGdO00mS07BQCPV0w_gldLncCOgt8VPaB5d5SjOF0_whIcVAIY95y5MrZEJWcbES4zg_jdGb5SRLlr9PENPbne9VYK4_ju-MCFNo0uWibQJzJcpaKU2rZ9sAsT2goR_lu-aLGCdeimhRmual5ISX_tyMRikPCDidsweqUeRzPcriSIRDKLcQfzA3P9Lt_Mo0ql-l1EX7TcwLgCsISBJ39jyhHyPvNPbBAFAlrlF9uRhz_ATonpUwgZrQHSlpsy6Mzh-O8f57HKQTRT0VigvfIeC3J1TR4EzLkHUdC7QF4JNlprKFQl-HUh9VIOpwXfQ7VwhbxUw-MThAn8fnFAKqd8S-4S76Yn4Ns3B0FA0wlDWp9AvfCSlm50bQHUgj8FEtwz8279OoIhBEIMnA_rHNwA1gPMSAl8aU4RO4L9wTbhwVEs32i77O1pQS93ZeNwOwXXoquAAVFZwusOXz2C3jxzKzB6IdrA9LE7-ALHDvmxB-y9KUe-RgCfFgjh9EE7rdwftpCOMj30we1IOtQ1XyFSwpbIK-y6e6itkyx73nB8UicYQEQHDnl2UPtxm3TLUe5bx_E0sisng5ZV2ISypN4_CiyoAbUPCapdHnGLh5VJtaPPq0NGIVA88MkPxnJC_dTfsZKzNVDywA36U6dGzcSH16QoTfJ-ZcUJhHAKJHizKtLpdxpNKlSugnNW0P0XwgrRYAehBBqJAWrmDc2vll-f5KYy6AFEWfIub9SODwuu3j3yfdoVAjpi6Tvm_e_w18ZBYKjtRrAAg38eTrwQwdDDovzBO6t7xmJkqOxsCFl0tz0WB7YxhVMfhC6qv0ojnXM4XrhX482Ew0yMUB9Ql2_2d7u9-aM7VztBqRf9dtPj0Fc1WdfiMD1d72U2D5NukpfdO0k74QL4xFcEWgq0qAPT1Xd35HaQhe9KfUYx0d7KtbBb1BrpQ3zZWS_ThLtfTHOvGZRQH9bQQyFkx7r9Lnal_GmnKw_w-Y5ecOTXwxvtB_XQNOo2i02MTPLpYHXMCWCFB6kHee4fhJVL4yQnaac8WOYkNDZeHf7y15M6Ezs0ieyusNjY-nfeAuXS1kJ_lf-qI-1xCpx4wmOy-W4Y4Xbr5YWS8Pe17115uh3ZGN9n88HuWj_fzZ0BcrgsT4p5LvSm9lntyD3oQ8pX17phhk3xqItrnJYAq8MfnLgifMDl6XucGJj1rhsvVGfr_ccjSHxohBb0HWL6g16xEvKsXnQe-PHn8Djtpc9doxqWWC1QeFnjIFJ38TnZd2v6S9irKu2D-YTw_9TvgRZTHMLgHH7pdFo2P_-mrKP74-OvYkn0O4aUVAZ6-bCXKIZ4ZzFgt-aO6l6vyUUfhcVrQKcnRdrZ4_GYfiRdxlBL1rvcZAkVpH-iitAdQ4N0xFHFL3MO3MH_EepQXLXSgciWBbbc9lzJnd4GkCRT-uH1SKKtquXZIO28ERVLB5yD9xkl6-ch9qTYNnNcBDNSAJQeFBwCHB5xZoyuYfN9p5v40vfSDAoJU9A_3_kaYMyUBVaxQWnKjZrrA5hWy2fjRUnVpeX7PDyAyb6eZDt7dKlkWGQxvhDXRFeN9yjohquhDj9OSS0JlHsPLobIYEPThAwpAYAEH9aspydpQDzH5LdB8aSUzTmFvdt87KW_OjCX2bAvPUj7a8bhfrITHuCUwOl_hNSIaxUX9EuHEifvRKi_KnQRZvkTyN6Ji93jcr1wYk2FOjZEVdUfC_lI-xzuQDSVWUUl6URvL2tfzx5FxqScbNiq3xnIqLrNONk-p4hi1QvPbgiYvXevv6-KgoCOBN5b7E0KUoVcBh8GBPzCeP2EZwA6C9k8u55Ul0Y6dohgm5HS8NQfXCSTt7QQgchGBOyOP96JR_uRbyLPJ18KaFr9QTxkQrxpuks_tWBdd9QD7GN2MU26S9veV2mrWHNXBiKY7NNZjYSkfNyzvjsg3VCwvxU9kzvkozJ_hQnkOnEmlI8bu34cFvYy1Ms4X5fLwaFLMmG3SnAIwBsCz3HxzKU05NBHikuB3B79BGskfQK_Fe-rkahNqJgG2ya6xgeIBivC2iuCuVjM1xcVN3jM0VuwQOCIVwjPpyDgWwjm5rpjX7LfEzwjyXynX5OR8PVugx7bAFwv0UNcbkBNLadJmL5hZfeXHzgPM5u8M1_PEpwxRddCDLbmbY-Y1naQwfaKRQp_c6KwJtT3IzkOJlaYsUlEeoLQKfQI-OFr7Jy6N9-tP3x_0OpecilN6J7UQLOTQEIeygISrIiIkSQgL8m7YCl7cRejrq3kF9UutkU2OIJFseVIFtIKZL92vc3WSxj6A8NkX-yqQ9LCFljVw_acJ9tUT7tNyOF7mFKBQJPa92WpaOGgzq4OCV2nJs4GFYjXgw7uE2NjQ2i9_auhXryGm3uD3G29NjUQ6Lkingi5trDZLCzoFKtQ_-2tWnf6sC4HBlShllmYDfCCorSX3Qc9WvEwxLbRvNX0CgPCEoxIKHAE9UzN9sfWZLD6BCXAtERDgNqc458B3xIrpXpk-hmIe-Res9HtuS43LqebcFiHjjKKiBuUEBCSxSEYQPYdEII9QMsBsp9IoCOKL7y6m5EgCfQzA7hiWLlE_Xrppv625MGLzebKWzu8CP1mOPWTp4FYwaXl6sm0rgbAoR5XtNLcBazT83ji0Qhc39dVR0nFyvdSe9L-EFw6dbYUPPbQDh0hQVzwnXZYFi4wgX8iFfyvfj1cAGrQNfx2yekQfLm-vhGK_sIlCRVZf2bjS6rwAbVIhhPFuTsQ5EaYCc3QbvJg-slvxMGfr3gpUkMV24EE0dCemwKRyRyf9zH-oswETPMyAFTQmlx715Ao-RESnFuc1Ebl13oTofrWpye9ZaqqsGko3Cimdifa716i5Gkq2FJNQRRRrp979uFgzdwm2AL3Wa_5I1t4aHY0hFNXzKU5u7gNmtiTDyLSOIWLGfd44msxBYFSE9YqSdU-7KpEtOLQRppx3FR1TQooT35XW13oPp37k91Uv2j8wLJPAid7msh1AUWmpGiq9vhair7EUlZhnjNIEvhlTr6sIwFzsJPRl9Dy838w_UqVXhKcA2wJpTCjgRWXL8R8b6L7Qs2v0H554fmrK3qcTm1BgmPf6d0aeO9wsgj_cSO2gI6HgI4zL6PUQTsMTzhIY8pN8MW1jPWVa89yWjGjaanxKT6WyzdkCGj6NcG3Yh5UoKGeehwa_5FQwggBfzXYMIAK3swXYvK1bVz_68c3eLtW96nYc1mnOw0QmcuQ7ajBPpwPVqQwH1iLRS3nEWbxznVbgvcdHS1Sv8LcVU8htWp9JheVP2OCiGQPFFScImnsLDC5WZxJNohrxFO6HHJ_6T3py6zz491E_zWqb0B89YapQO7LKc_D3pU7_3-ug2A-BmtjReN5-I0QAaNX86gN5o-LNW8yl7DmVU8rDBHQBV7vZ4uijVQhDvpifKk5mqhztr7B82gamJD6gUucjs6nA9V8i9496A3dTMHdtEjeEIE5zkvtbLe44WyaDxa5KiwZikk137DL-hp9w5b2-ZjwrGqcNJrYwpTQAjHigL12EWMHKEnPEsSXqmYujeWGfB2M9_VDmSgf3J-XAZroxarSzyVuead1XNLHtLqQgT0Prh-PS1lDJ8jH5y4_JzNS6lN78BaEi-rBl-hyhXqi7ZEzGEyZVB-H9rkmCE1jnuQsHj_iWUkZFeE5wJRemTSNTxF_GqZrFTkTD68qxdtMg7nWns8pXHaqDxpWAFaONRj8JdfPCeJhQ3W9qIdugEHXFlYYtZLEuXAlBGkHQQlnL2XeZ5aYE7xDC2JYQRJBj8c5fYfusrnqBgsz4EIO5ewfwmX-OAJg2d9Pm0UVxGrXtTW1H277sVslv-2FcU32cZwwls4YthQ6fyoIVLzJTyMOYJUrpFW32r5tG425wn_Q8ezmTs90EKuVrvVo8w92JL6MDKA-orDvhvQ3beb9l7Sgc5yy9cb90rjD-lyQBgcDfJ0xHFnhjnz4S8t0yga42xeRI3r_mXd0NvRzTUHkedNMtRAdU-W382jaFGRBxXL_4YziKyewh_nGh6BlW9EQ83Qf0oSwb43IN4k6GmK6KKvwr_KiERaBougue7YpwtYyqCrEoMiEEMn-Sog4CeLzg6IuYx4awivB7VYGGGwU6Bwc2IkZkKUFxVhJK63cAwQX5Gcve_j_-WcRRGlUhI9W4RvFhQFpl0YfC3cLUzRQZfV_fWH2MIwrJm6y4VCHhnvx8O87qetR0kM7el6lY4Nrk5bNtCdBeoyy_C1sz--DjsmM-z9i9IR8PqMCZcX3gBry0Sn_js4Ka0cXPsKpM-GpR6L0CLxge1FdKNDSFUOacsiEzh3-LTu-rUUYglWzQShuc8_dtZrIEvVocirTKZ3gaImQ1M1EylwXITBxzCUW19Io1X1mxKiFpXKHtzK7AvEs0kdicMBNl1HsKSn8OH3jxwLSHI4DwFIGYBxCQ0vvG3NN5ZZ_c4OnSfQ-nojlgmeCjMGykcA9E__NgeddsOdWxnG3fVQFIiMzoJ1AtYnxHoPRbtVZdyWB3dX1L9AKxlFep77w6KS48z70KzKseRnKLa6OCPZwfXgP5kEKA7FcKwpwIaMPNxCOedtULYeDhclbLeDtjK8LA2q7a8elVyK6YRvseXaZ4-nnd7iLYLZNOv807ZLaYGm51X7aFt0YRTimfsQIGztdkY9aakmyH_XQkqPmlNa75aE4xf8FqLjwa3AZ9PcIS8EpwX_Vw_pFA0NJcvJxCBgY4Iz98FxssnBRC9dJ1aAn4Kd8lgWvHIXS974MFCCGhfI8RRVDl4S0QO7W6vrGTIZB1ngY6VHZQ1JG9NJOGtomR_8RNH98FwcPzVNUzy9AhGeKBS3WECJCxk_gKjcGB-rBogS4EU0BVCfxzCoTMJF51ufpG1k4eWlEiEpOqUYgUWAN_3XYWNhphToFLg-h1xmQWWUBiVS6tV-XVvEOgKCKp_b8dMJ_99civ11moW0s3XQpzbxo02gCBR9LQYl2OPBcoRr1bVQfmS3sljBMCgtj5NodsMpz-rIZtgbzdchFe-RE6QK4qaMwAUY0oldGd7nIW9V1C3hnGg0kekWG3JKlxMhIB3IbDAVQ4jRJ90_JbLVaj8v0cNmhAwT0QwIwuTJJYFDGM1fYrocL0UKFsHEdPGZQFnfGAeFoMQwUt3I6zpmXbIqWA0VpRYwiUwTTRNTSsH1_eX-LWUnbXBsOmr6X38Sf9SQD2giVwmji2KBw4GSfRjUsbae5gpgZZbTcXH2ZF4FK79B7kM3RW1yKHcMrT3jXyZKjfEee008n6CJraHTc2sBDtV85wr-TQgic1VgACOfee02nwbPgPGhlUsN1e1cBwTGCJiIthec58AQtsEGIsqpTwh0axbKUmUaOj7zuUjDTg0imRCdYb_iMh8ya-YUncdYTabPkBJYlnbHzCB7aXmq42akqBQTTTgVgUsrRy22Q9gn7CkGltOZRbiPZ4Oa6Uzu-CYOsK-0JcD1xUgtTd9icWNNbAg5DCHh8FhryzVmRa5VUkC81OQryM3CgKdyzyw4xSH3qw2HcCMu7VHbHYhvVEXOQQtSaedW6w1shQMbPRKt0Bf_n3DTiyvSsfAgZmA3lrhQhRzd710dzxxljzkbfYEl3Q3SKg2CNM4Pu8SzAcJj9M4WubFMqDirRgVIMgL4xthq9u4qvIGxTERgAu1h7xhUcA9f0IvKiPzBkfExW_QIYR8c9kewkGILCplgqOHbvNBtqK5uXJrnscBUm-Su8yfc3gTiWWlsb1KBm2qwj6uXOBWQ-u4xyatyltsx8AJlshq-YB-K5oJuvlwCXkeXkU3hqRM4SRwLng3VyhdL0Jr5HUv_M1ENVemAJCR1W_6IXWxbChAYiRUFVnGQMCf2Jx46eQo1sNMaO-1r1LdtVSJo4ZELftKu2X0BMQC-l9iQ5EfDT2VEPZvl5JszWbqWIlkr_RY4jwbY_OeQCkPaMxE0eywBeG5zjdTYzmPLm0YjmK5J-_7tjM_678RIQ8qyuFPuNRGFUClznKIZ-T7SYMtFie6XAQ6j3q12Mh4-zEomU1jIOcy2EzZzTVgrpmqVtZUB9wzPIsNtq27VtLz231dh2i2fAfAZHdvIy_7XQsY7-JWltkQ-fY41Dw9QOIhDb_KJHhFNH2xa3g3NGh1WxZIiJNfPXXH2pMA0xU_FnJF0uPEr2u0rEcTWqTsDgHk4krHglASUYsJYneG_YgBCHWWrGXWzbQNGYsZryPJeXNcY3hw0wO49CxV7gb56BbUNBvNIfgS6SogajoeoPTkPQAICjtAVhnrgXyIFnQ38zu9Cwjwqxy10jt04Gwm1Q6xAh_CNQwcLgtJ7elaM7zi9uEGFskPfZHF35EOhpMwR6wBoPSv0ESs8PX1_WKhYSakFyW7SewR86-W3aCDR6xznTr57lJB7BnDb9_fF6rjfysDLSjofLGwjD8qC43OlMNZB9m868hgZoCUKvSnTpVW0B2NcAoM8lgXDox6cxZPtDsW65C2fMFUmt8yqLg9MOB9QRvr8jQVvgQ75GPADaHTVbcDukGOlpWsE8qHc0y8sbWnBRwGu4lUVpyOe3R-q2Y9DVCPonQoeUt3r6EfyIPeid7GaY1S-jCTuj5GlZA4Ridz6yYYZmGXzju_OqZL9TpH14-DvywWaBu8ZUqvz9kVamnK9P_M-jTDn6iz2zy37xyEGtzWT5Mv82avznCG1l0kSoG7HPg2kdA2ngIutv3-sn-D4_H3_Wzni52iLO-5CdMjEHyo8IRF2gsHDwR0mkF5uGdXv8RD_b5KZtgMy91QfiU-h1B1OTDWxxhfSPDO00EtPBW3UPQhkMJY2_MdHzKiG6i28PRjUTIYDcQjc1RrUZFuBmD6S679gKEzKw25fKmSbk6MBIhBfV1Q0h9uX9RauUq8yFRB7mV2EQgMRzrSZd0LVqNtBcOCU7TdrpzJzk0pZkfmjIVGOAJ37T234ICX4_M28IgaNiluXWNYvW8j7k_nTy6-8uRVw30AJnkQRswmxllkn8sE8pfxq2ACMG6LhiwkUeRJU7QYz8GMhtn1HcppGw27GGLZDbd1fHQ-X8EyC_pEx6wcSKdLWOZJ-TOqBWCDHZAJJ44G9MQ_eYCZKj78LA5pooQ1OQJeno7YefrhaY7gsJEY9LqHaDBBrDYPefTlMYgHPkHKxgkT6QtpbAHN81lB5uiiN-o2HPIgI45ODYY8pmvk7SY5BVsu-lJ0K3KZJOhOsfQsoK9CWB37yZj73eFNgWO9Wd5qmmiRVbUyBrjWSXc_dLnbEAKxB08xoITcG4hDIO1TSbTIF1QsBKXbyH11lwKM9Gr3bGckU_ni5H49T8MeAx2Cce-oeZ26dj5jDGQwwwgRbDf_9eKjzVzH0MtA32QPr-ZDqwIPJlpSAIswVKI7W6-TVHeKdYjBufEUoVhjsJ2kZLNnwsgUPySarkA7PjTLxcS7L5eXTIzBWpcSqQfY6eII492F_RPgaAzRnqRW7FA0lvNcCblQJoRK80DLGM_oZajzqytR-ZgfJvWQXY5UAcW0ywx1hVklrP5H9hxJBM6LujBC-bfK2gatWTUNoo7ciIWk8WPKZf9jCnGd2s9YQhwqJfIoYWLYZj2obHw-WfedxSpLOl72ucoXM_UvtvSjnnX18plcNrQ5lkO4f23N0gh_oZhdwYeyeb1N-KADIKIdY3_6tj1AFOqN_vXTuFtEAilg5YpHC5akZeMvfOGunAVza3qucicsRDEYutxcXggArT_nUZa_j9X5lp9EItKRVyGjBvRa8VKDwoHe0Qq9JYaDk2zA0Gqz2BsXKjxS5eArOJ4t-el3UdlFrsrGz0IIM53LsVDnYFGo7G8sQWzxQHD3LqVKhumuL4q0I6gBmOZBhAzzAb-j3dE8MFDXLKOzpMXj4yY_f1BqaSVhA2LxC9FXh8xlYclwHgweVkA98obGvKfW4iMNKJza4tQ5A1QDFPDwcsF1biEPK0svQmSnHNvjhOBM_hRoZK1YD_RXmIYPWzJnULt_2Nq4Fus7QlP0m4I7qSxDSUe3Ly_RtLefBaV3G7dUa62RQJfXVKgbGQTy_64COJ89TVWD5LIEPW_LRrYvSjVlsMD7LPexlQnh6J4g3zq0uRHxcWa1bDQDUQYrQp4Ud_6qc7d7FoQqYbQgib1M_MIbRyJezKZJFNXN8aZWzAkSjR6Luk43uWgogzv_PLON19AnvbC-eLg3fE4aUvJAueCiTQGGFkBb1O2IW1kc4i8wN_II3s1TkjQ6KSvre1kN4YMOTk73lEcC6L3NcgOd-o0tPDO2O9E6I8FG4yCWmnFPjPO1FFmEnjAUSgwhEs4KdKbQwRphNPnZQ6dWsjKPVM5AfmEiLx8drX7C2NFidylmW1dpC6T9L7Qcvd2YbocFGnNv3j4ztPjt-9Z2Y4fZq-02HVNkkuOO5AB4TdPTftjgiGipnbMaBmgBNMwbxkzHuWZ-avaQfSifAvfuePdugEVjmjhcS0NQuh0_hZ-K8m0-41A-EqQ6kzgfYTwKuQ8JdIWawuYoM1Q0G1bJGpwQxG9DPDB8c6y-WupSOZ8c5l2pWsRVw7UJ47hHhFIsoDHFHVDBT9N85Y2SIRbttX2pcnKj3nw7aj6ZcTRwpNPN-Qvu8YMMjMUVV0QoIn1CEyhim0x7jqidBvcSHLamlTSqYvzDfI4l9fSA8m4Yar_VZSMYMxls278D2sxVIEjXt-fqUbXc397qGzvNniARzqZcqrataPpzQoOM-bNj5LEJJdYPqSsHioJGOkhFzWXu49UuMFYUvyNxOhrbUy8h1N6GKiGDMSwe9k9wN-5WhvfEf3wPAztWl5R4PFRf306CPhL-FW83zhBr4c1UxU56taoVNnJtsblxuTTDJr8HgIiS0bqCLpL1s-ZYOgARzAgymuZCRdaxTmK4fdFhlTs6coahCbrSXO9Iehq58t6uw55hGhAqMjVvaRn2TpgwtHS2jvGMCsLFBYnkVXeeCDwA8uIEvujo_WcIUiT7STSP1IHMyllhlhU9tb0sD8wadR8caAgHBe2CuuE6YeO4qet9JIzOLTd3kJRE9Ev7aChlmuuAElJ0o-ktfVIvUbwVAwiWV3X6AcMlmVR_6HzhwZvc64Phapf84hPMYXvnIxBSI5UbvA0X5nHU2lnqPeRlhQI0mKXvLk4Z60WTgGrJoz6mjUQNep_zG1WTSkLwk4zlLwupc492MMc-M3x-vYQBmA0J2OfXEZjnuqAQ6az1hF9SaaF87c_W-Dkd5wgzUEkoUA2kjAfLtSItyltjCzxTnH5gGs7KaeoN_9V3bj_EAquWTrF9Vdr0DyN3fVdwrjU7oZhp_CVfondyy_VQO2wtxzBICKDcgraDmcBS1Pw_VPEIXvNm0ia52zwDDo6h53kRiKECACeOLLwif-WO5IBh4DZ_DFsiuaX1dJyUUO_7vk56KjmN0QEHxaNwpvKMuPtRGOMWkRAwIKezgkGJ-GRLXbeAA_1qqT0hLDsqJUal65fXdZ_J-qEnJH9xThlPem3WrWpAYKXeVOLOCxuA-7wxyxO2DxHqJdxsvzd16aErXTcIq7OgGXL14QQXLcpQIKermnxygZf06I83xy3pkfwEY07BVX6MnouU0ybMlqeFQgsWFnP_yjPuYGA0RQGOqsL_Cz_aq94VrHtzL1M8NTQt3Jhpr_L908QQMXN7kK6CKJnDkh9Rzykak8Lig_xmz8E42bPY-RWpAgAvpju1nggo6H4oH41IfQYW2gVzTviJq9EC1rP3FtJouq9gmSH5xDo5IW09XFskxJatkvOUIjgtZhCNG_VxtML1VdSDLZSrYjMT46SO8JjWJcn__4tR6gEmTrzRE2OSjbLuZpOksXgFrOgRDsZuPSeBAE8VKVpLtHvRQKWimJumFONfHJ7JxCOaUSBzpvk88Wg9em4x7YAd_SAChQoT7XRtjlwkRszQ-TwYfGsyOOGiTyG9dzCGGy_fsTugpowfedGCGBHJpuApn7cf5NNyLsafquuDtEyUly0NDpCwF2i4Dhma5jQsDEbKOlHnq8uzAkJXRe96IQBj0FWieRJyLU-pNsgXz2PqRxNXs__iId_f1X7avOZHN7FyBa-vE-u8RuYGXuLsUtQnnA0eYesQ0hCvGHa71I5E3-w1DCu9dLeY725SC1yVZ_vJ2WJmwEPXJIXKhVgTfvw8GIEml1VGxRFvb5kMQtGbXChL1tz7Y35ux-SRoX4A23pTZVEVquaXb2QjNFOprmA0tuFeYlsUdqD82ls4R1WzgzLVRRF4Z1Jh9AFgfYHqV-7UHwJAY0OpYK9iu6PPknBPAxWsxnLxyIxQ_rRnrbD-AyW-uFhBZ5d38zkvKw68Fr24Czq84U_OlBAvHtTWSzQa_6pc6tu5KT43QDCeWwiyWt1gdahuyoqGpJNgqyD6gh5xjSr1U-ahTJpXgVjnbNBkfOWecj9GK6CMLgvcI21qVrX2IHwG9kMyQgNmu--z0VHXt0WUtEuUcHMM4PzFM5AOZ_oxSVtIbvoYGDXjUgEI-xM7BOr4e1B4n8X0aoorefQhCLe1-Lv2pKRSeUlX60RlVuRN9GkoD_UoFqz59zJwL3h2uakwjt7iehx7DeI2pHUthZL03BqsYtJth9Emw5gsDKfBIR9BAjIzbSFRnnC_pthG2E1WMRMeeKThVkL_JYkmFj4Cr1xjqXXCTAI9QFwcTqRI4ZkRgem_jqVB7H9-BzVDrqgbQoxuWhNRn3_w-xfyzv_JtRcP150_7bEN2-gbBJCexcaF-0PbkopUuQqUjE3-WYKc9X9vLWcdkEehB0F7eqzdIWqRPTsnEat4SQhSvbaOp7EgY6Ypkvjkheer3fkPelAHN86SGviWWtaxDTWMBwHQjM866tuDKWOEnLQhMb_IjQDFKHrUKUnz42saPlPWfvbas8_Ymk7bX-E263Wzb5_MWXqPHMt6UTMSOtw86MTE46YEW9Ww-WW10cmatGb4jfoQHXa_JxCRry14AjwF7CmmQLP6dnm8r4_jm8AylHV8iKCG6r6csAhY1jQ3I-24iLu01EDB6H-_bIX3uiZDXpf4T1aGBJh7I7INB-Ad7d_IV7At-qaorPyE1xvTWeFVQLymsE87ZHY0J157ggITtT95e_Q8_SEiFYg0vxg89qBpuXygL2M_Pbrb5eYTCA6K6N86CxlOvFAb2AJnhAmxe8c_KHIsFZPL6lReDGQmMPBuvdCjjLPV7seEZX30ZMTuHYXNuD7IytEJ7X1o0_04eCmcqbivHBCoQGOzDhQ86DSoX2Omx-hmQl3hI2KgKnGcnfym2Ukd-3CmHAyCDAv2kDHm38H-JdcsO2DNk9QsYtAln6XRVl5kFDnWEhm9bRh-fg9Lmt_mNkwHSwZ0YrdYhAOCMkNlukUp0EYKKhBSY8lsY7a_TPbt8vkTMSCmi2sPr7NnuyaxMvw6Jblb9OD885lSOUp3oPpoH8QPkkhYUJ4-HVmmMGD8orSe0L3k7lLbyHzz5l1EmMahHWCCbnoMGGfO2QnxV4v9YcsMmIA_NX_1CjMUh_LYKrVWE2tfmhj7Zdprbop3nTylHV6YNet5h2MVUtpfj3CFTz-7V0AxKhqmTkSE9fMv5_XY9-QxFKf9B785SPTdj1xBiOsQ0uz3TJ2CPFHOtikiqYkNu9w2cUgYejqlM0crBDpQCuFmFJCFNKrfMa7eue_4H3RSh8Yu9Yw1LXbkAuGoFMGYhegcBEvcxcDSHfZ9f1HFT7IgimpuFuoGHwaNhPnlNc1uI1ILsFeRrrXide0q3L78aMAdu7eFfSSXHm-RcZypE9LHU8caoGqd0cr8hMAFvmAacrXiUE6RtzQUZjswSOziVVwlqyszgPXIuDsA4m0AcaLyEYQ8fEsRZAg7RyRbTgMGrlo-_L1Me2JMPPbiuNi2EtBXz_85Ylbaz45KQ45mdka24ouxzs3YK5aPi-Bv-fYL7FhoIWM6AiJH5ETjucj9KrhL5u-mnEi7sYh6ttj6I-MtSpCzOLrIB5HZ-tJktRhN78f2m8h6N4FBL9ooQXR4Y-QC1MG4eRlAiugn97K-r3MDGQZR5fVwC8SPW4Pt6UDvfaxXZek0HmjYPEk63MIxeMBOLaipBGR2ziR6YsoTUZ3NOopXjZr-UsGukdLw0OIJsxA-nGjmOZCr6iDgY-EfaCAVwAOxAv47u05VBTOP1xoUhMrxNefZ1lt8hEziCDaHInMkDdc4lQVeYv6H4rR2KugX0IXGsFc-C8sfQVnALLdQNjEg8_AfTsEmY3NqE_ECIUhFwxaW8s8aWBgX97Pi8SxkCwX6DyksH9fjA76rP4P5kpWl7ynaOaCfytRliE4j5uDXXywFfwN64DWKIQt4u2gDGo9d12CWUMGrWZZdn3qn8IgEDmUdr_CGXIGcPNuS-wxWoh4G8eGNhvMk1V9zhyhcxgbjoIJLl1T9MOZZ8JQVpiy-cPgClLI2jgIbKSVZTTZ8B6T93aQj5oEbOw87RZxArjYP2XeIHMNh6JUUOND97h1D-tXlI6hlFtFTouMxLzyOpVJLfdrUcr2p0bkbNPAyk3qzxwdRWegSWH2nojJVRP5dopYDUvX3a6sXVGUefUr6llKEtyQ9W84oVESDWyhWRv6GiBkpimAlkoolaGYFYCD72gUISM-ptvaWmVvNmXdZhR2JCSn3Ec5K9TZMg0ArIgFvnJeksow6nIwDSYZ_EXqtEgn9hjLaOcKZSrixLgvGqWY5phJcyYWP7kBsJTxc9U7xCIDh_RCU8fjZzAOAl4r3DtGTEntqzqhScZ_-Fx4ygPgpi4Ko84FM0RvNQGw5VSrOWADroETQVP-La2KyDOjYo4dTauA5ArmYnXyLatcyfbnvgE5KofVhMHwPq-QSV7QAaN9aM3KdDRxBXV7YtnjPx5DzLQE_61NLQkdC0iWFjHwLwM58comkNfrKAUw3vtLzWDiLHT1nPG0pxYBn0zAid0cdOFJ3JRJl2F6-GuMSeUK6kCqbX4mtShWXp1gn0YErlKR2PFjCDNj1o56a5ejMOYAB_SNIjRLO_O7uGofXv_Om9Uevp9XKu3ca86Qt6uOpwQsifkwS6j78cGRTJeU0SlIAGBjzi6b4aJN--CpFIqF6JpuZAxhiLzsHAXRAKik3Lu6Pmb_24KBL5_ktbQRcQX6GQjGi0A4gccSOF3hdJ9j1any3RaFOA1_0HRAv-ExWoiQEyUnWALcqaC1FmXgDTxYx_VUMjeb-MqxAV4eHjJsR7e1q9cJS8qhubSQbHMH72GccTJKlZYdLBHmc0Oqejf-JKgaBMxgkGX30uCXhT9B8dag8jVrDBemQV-wak7QHgbAveaWX74ZsZZF6ZuZ6YU1llAllJlLWPVNr4aaPj_wMfurz6YyOJDnCcVxcKFjBCJRuTBF1ACh9Ye1aj5wDUVwjeKXnjEy-quQNoB5c4clujc-G-ep6-EHj6WgHZefu1HYolZNprU9zHY3T_OrisT2jDBUByHv2RajGe3K7nDZprR-e1SPApINTcKQ42Fh8SfDQsXg0qOfvMdKbfKJqQizEQiCtvkQu1oXhlO8fC4J5UkN3qsPcdG_h1TQ-_zlAPDJ97B_92zV5NkIF3XFM2iQht1oWwZdN6xwKeDRqKmpER-qz7bxiy9Hh1IxU5T_Ac5c8B5xIxbQzgTJal2t1M-_cRvGT0CjpEBjRxqts-KliiGxFl48wNePKySRiGEfnn4Xfqmy4enbmmZgyHCmo-h--qxLIxBEykrcQurpumcrK29z2_jGUNichMpAaaT3UlzgVTbOVb3gVN3Qsu8ltR1RtlO5DM_Sc6q3GQ2QpdHafa2S8Z5D_A90PuohDCpyqvS7tA24KNQEKYM2W_ONMBNNEoyU2p7hZezbbj5T_HLHVRPUiVLgugGFQkNwZ5cRgrgYqstoKu9VJWFE-odBF8G9GwHGFFqyCdBL2CADSx9AnfEssP0TSarXyn-ALo1n5f6vpUFmkcuY-4gFSang5orkODd3k7hSmsCxs5NVMLfQxPtjJcTTrKR04H7xAVNnt79YJYVW73UaXEUammc_qu0GAuNwgeaX3wIQv8ieBeqJvGbfOoXd-U6c8b2xS7b_9BCWtTKZ1A8azUrXAqOr5rXlKkq6I31ht1XzyQAWq3_YWEc8MJahqr7bR5GQqOxRg_adTocY65i1qhxebStP6XWRRurHWyHzDhi9duKfGK_eC1bbuUIevXsNDHdQBDNE8_w1BBBlg4eFuM8vSDZWJEKPxvB4Vl7ciLOs6-diW3bj_JDo1BZlpdDQFKCwDuk5RtRJmr9hGUaIbF6nrjbFduzQFh6laU7VkD_3XyqJ2C3dCD1vOOhslfiVG1fBWHpTJvKsgfLa0u94IUipo6YWCz8K-LCeOymEufdrfaI1A5qutL6tF0CaPl48rmLRMayxqTf4ZGCCDe49C74wOS_kGmxchhr8DKGUgKwiWJWQjIQLIk2PzaHSQ4cE8uBQebBsCMzlrzNr1YhYzvzhje-qorpNcwCluQeaXkqp1WST9LbExS1jN8gmJhLgS8yAOd_yGdJchugXdbfPXWD_R4oVf40bCAv3HBB3MxQKq8dZeXg_9xqr_bhwqY1oUraAHLEol6kUS--0eDJ9PzaLed1ZQ_6j-pHR-mu-OkQUvtM-THVLuNMKWGSYKcBnOFYw_1NpEkwoWtcYCzk-nq-aHJ5XnijDKutRPJQ5W6RLMmhB8qFoZpRp_aDS5LJiqp-Q4g2QhtSCckgUwHN5GSDTLaYvjkR5jeIDI0Df_tQZQv7BiusW4M-iXMunM3qpOcdAdfnBTmODqjdeBAk4dRnayZtb2Ib-JKl5ywa6WUDhpA_UQA_sIlBBbTjetvlH2sChS0D17boDPANxqPYQLorzUflL42ay1DQFsRRdnxTiNvzN3nMOxzFdIUYqWEiY29KQmAFyuERLmtWNxvUB7KB9WqxV21mbJ-yIhTsuUTHve3HdcJuWPzEtbZemmvTyJr1wckTGBWVfeT20e24dPMpBbRN24Mpx_tMxfsioxNsXFYqKHzqWqZ8Tp-gj0TUMr-dATGUJHHQ2Un1nVUYhOfB-G-cycBf8zmgcnA9EsKkTOlZY1LRmvBIknw6thweHCggBJ8Ke5N7lgYjdTTPs9HXMZk-YcGJ8Q-TkB4_Dw35xq9_hnncS-Dl-_aTs3FD-V3fAbAd9eYbttpwk9kwVnc3GzF_d-eoCntwtxNH_iYmdeBZIqLZAoDwzvFnGfVunFP4RiUtLYepxu1m7HLhPSCAQn6SNcLwGg1U0jQpfYIYGZTL3Ntq91XYv3J9vy5O1apgQZic9XEMxzOuoYf0zDEU41PaVOmGv-H-mdrmH-MI0AquibmsDkD1GoUssNDqsqGVBgMMp1kc3N6irmLeIpdrSjOLUsW8eq0YGWoMXXxp32wIfDr1fad4KV22Slqlrfv4RC2v15WxVI6j8Cn2l6ymNxCj95fk55ibBk8IgObZEwbu-O4F6focQnbqXcLMSHipxWVOo0PNAnxeG8ER8AuVaimP1nXVWhNo77VuX_Yat85m9l4Avt0Q8tR6Rpqruw0cxZRH-3GRk97-svz5QsXMJgNZsDquzmeRT7ydwFrr8NK2Ei9NmlZ4pziY4xgIjVIJgIhgkY2wEH9EBDPLuqmYrA9z2RC4KUg5aMAvhRRZ1Jrxd4uv6C7iq9o9x6AOVwA3AzuM-A42325s1cNlnURin7VjQvoDg03eXsB-G-iSEUw_WoiFatKsO1U8bW4GP1-XwaZMD2w9-NXF9JCCGp2PaYNl79WZXpoNqtOv7CS-USx0vOF6DLllVZebsUhgMTBHg6I7dmJShzC1VLrCV_XjFCVlxfSdC-HkHceCUwQwQvkH7CzkW3Xxqn9onVcL1vMKgt-D7ov_952u8jsS6gkzEkUZgSFKNUMJGZv8J1rhg-ZNUi_50EsohJTlxy8H3xw8RFN9JsTZ7T7_O2yJ-yB5bCdSHldOwfQWtPvCw0df7yzUQtkMqMY384QRdKraWO3CwhrqD5_j-iqM1nw3AKDnqvUZ_pL_MrJT5OwqvaQLlIJpSymmfw642aXt7P1TzzFnwOYb0Myjc0geBp6JKLB4MetCiKUxmYP8M3hiH8FSZLv00jUmVJj-CPVj2IVml-IiAPyPU45_2W_Sek_l6JDqxgviPNU2QfLqXLOgs7-30-8ZhrtlZLC1AYco0hIEyVvFBQC5CjorAuillJuZ02YU5_kNwGG-Avbqb2zLhjw3gO7ZB1Lz68cv8F5YVsUvCvMgRhgpr5Wj_5uFtw23HGXHKY2Ejm3Kjya_Tw1EbrPl7t-UYyUxZkF6lUh-ZnndeOB7RWVO9lDvW-kuu5XuYFbAM6ouYOPd0Am1Te__qnJe0cYwKBaqopwTCE_7cu9EH37OBm3YWyGrthggmOrcK9jSI-xA40URX30vYvyuvNzZ-0f8PrZIfTtss2f0w9om6vDpwxsWhXRlTyz9qc0ntEgVwX6t6xWklLasPIwXZpahtO8PAA9Vqy2D3t-nMSyeBaPMhkZi_k5x3ckiLR9RHH1OmiAyYkGafn1_aB381MKMv_8AS4YGzeAvaHBwwfNDBlPpBhdupAGXoGPKFCM6d5W1QoDhwQyIZ9uFKuvoPtxntY8MwG5x-Vwmg3GhIDiSmoybRNIpfIqXUVzg5_a9p9b0-Go59h9B1ntMB0K1Q0X1EtZq-tVRlv1MRpSjOl8LFyGFQ8rYS0aY54cZgE_tdOaozg5NuXDJPQR515WrBf6NyJ2E66D3u1Fde7hd-zUMSiASQXMKwCLOAMNn4f3MWoj6UR3vKPjtBNwF1umNrE8P1tErywv40kYGz8-Zy5Jub9dMgKEfXbz1s6XIqZJEDSXngwVYNQx2fhaO-uGxt-eahjkVAkt1KoTe3sDxtkX7CFQNAaVBlsy4JEqRM1-Mxg0GfAP6M5l6MMhbqkJoN4oC4TVUlASghOUHqkCorULtgKctw01Ea9UnPzXz-KKpA4RllrWdUryiRH2A5RPs3KH6mTKVjJmzXvs-tHHeQphSLLm3QV1smoj9Z-oAJrz0C-f_Y0LE4Rsaw8Ag_7G9OOrBOD1odrNT2PbpvyeMCv2179maxKeUB3WRIU_Mz8b4_vi76gODzX6t-K5zDm1ukMlpNLfRtD2FZOEu2S9dGFFy-Ut3gB8Vnu_b1wnzETDDqWZJ-6bo9qRxrRAkH6q3TF5VTKv_hnYKY6QzcmotJrdTNPQvwCztcqj4c45FtJyax2tdOQo4lhoqDapMA9TawQMxunVToG8YmNP1YKJljFq-ZFttAxcnIpaTYq9scd3cfS0S63cnjaMT_H_LEBW9FedIR53Ko12fyQn9cLgErigUWMWwgdTmE2rPo3ygRky06cEcrh6zUtNb5E0Xt8FnmR0n53wZbJHsX9N6ficGSVwanB9ZBGJz5TmRHdF2aE6NrALFCVLZ_9mUP0XVz9HSUH9YbauXqYM8afLJ_R8XNm1WtqX6gWkCG4HulNtWURyTWgVuQT4jiB392QSDulnwnUnaFiroMxbHD6UENVgg78icspfeRQ3I_wEKLpCmngQSDvgNlV-vzVct_920i-n6DSDav6Ez6MgxCa0cgrF5Fbzak-koA7olgU2xqiyoAFv02H76alrTcE6Ooi0zNIBABz8McKSqmJDhJ3RTpCYQCmJ71Xq3xdeT-9-WBX9QgNEGQ9BAcZNT8IHY7yUocfYNOQS3XbCogSc0HR260BC8-8ijyyx1RfZB2kErTGpUCo3FQJLg8QNYU4cThUe1rmgzC1aJSHdYD8OLKHflJCHZiGGaYW_MA-tBWfHiEISIUcIghjbVjF2dBoMZBW5hlzvYWOV5y1QXW0zvTJ1Tw4R6kJGWNTK4wePkrh9W3t4wMu2QvyJQLGGwb4ltSDWefD44MtkWdfquG7OTbXqEiPr2KreJ2j3DASXuBDBD25RvlZc4bhLHFj9BUJ-lulsAvDWKCb2Bou0i6akOancevmmSZUwphs-hQM2b3ugNTsgsUEoF82dXWCJ70gyr1RFBfBsZCYDMDWbiqMYC221y5Pw2zoHRdQ40xDVCmTzDZZxzBr3ywIcE0Y_6c9tlm4e6EgOkdHg5KaAV9sV_uMLbBeSxyihQgJuxA4dzQnCo3Q_owAGtnkvhQp4UgYlx2AeclHenpTuFb_t-BsO1-DV6LgRplzfXH7ocQedgUXsd-gZtA61tnwNR2qRk9dbmtOikjI7qf7tFv8r0pRbe_d_mNadmgformlLzAtUn87xkZLmcMx_iH0g7gW7gbEXnkKmX9syage0xeQ12qnGvGF-p6mBKFUM7d_8ZBFt3pSd0M2Wl1zLnK9HQJVPXjWWBf8r9UecYdpyhtZAnxREWSqG1APYDP8cPpQcewy_QaCnVqyYZRFkf6X6ch-O9sJAwzR4MLElaZ31KyCxHTj8565hGC5bJUdg_I91UgH2yJArG54y_Yc5Dl6ALUn9QgPzbqDFFUOJjwU5o9uD2XyEBYzEErekT-GqxtSGOgCFSStNay_o8OmjolNWZVRc1_aFeMUOgh_GJCAnBMs8AVNU8rG-2bL8Yn_08Lfn-QpqpZIZIVsTZinG9cCIy-nuGGUtwHtPdG8xntWD7d5rNUtro9BCoxdrnbFOkSAwCQ365HHDHG-D0bnxTd70UQLYZcAb6rkxFrENHGBQFl5f1sOWZnGhofb6snJCirTWsgJcst54Dzu14XaX-57i-J3gi6pI0alrVQhxukhTtV3oj42A2TUGD6Qb2P_PjwhVbwpyfkd9tNTRT4YKbB6v7FviTl7JKRh_lMFAeLiNc10auLFBnXOdq28pbt64ilr05QoEABo-2qj0w1qRgK1RfdC_x2WRHcrI7zWIyDONsyqumIklidGqrEh8EXCSg3a1PBLMIrUfkfyV8C7LvTL_lifHl18bZO1BJtoksrMcCmPiwEJhCCMn1olm_DSh1YHahgEFrP9PhmLrFpJrymDuzXlWENX0QfqD8_bsiaIC7sqi4ZCnGI-KCnePmdiATIkO1ROI0ty_1kRce2LFztuwYFLY_z1yJlFflviLtyjU2z3F8Dl5JjO2dWm4n7bBCRT8wAqp5eztDZdaiuQUZKi9vhIuEnqFpL5zQVTUlDpMWodeYlcEZT0pQQamulicCkRslA7Z-CThZgOW3QWCv3eYTvOlZ0merHzQFxYq-8S_0rfwK9BEA1xck28GdMIXUd5cqBN1kUPd06qbwbCAgVBABucXvWbmkCeokCXOyfxb2BHl7381ZWy3_U6M0AnKzxhtYBSmBjY8sQAeJg1WTQ0ZpbMT651_b8ipPHAUl57j9rwVzxrdtmtai0VoUVNv4UEF6gDR_byb09xWMXgCWHrBMbbs7KNNC307cI7lmSHDwFDiWjxXcZtGMCix71kfh6uZsRBursMcnUoIaGvd_Pqv7SKeo3c1DXs8d4yraU5VqtmvHuodSmfcmOCEkzLb4lmVfBZPrsJQcLb9xFH8wunqxWYhr2ERzOJDZoLIKNwQnPDcxoK7UX_tLfbHKAO_CcfHWRgB_NkcPVvf8jViQRTrskD_19WqQFq241yN8yW4a61C6v-9og8yJyy8BWPQdiKESA180YGsfujYRx40jXR1u0g-WgRF35S97vOzm963EAkAmfCPBpRckAFxeDcb9DfBvhihOeaQEobt9UNhiDTNaiSN_Hl66wA5DIPIptw0_HQQLoVQ6HUevZymcwe9A5p7_AdCf86KBN-Z6cu7-5OTmctbwROcfjMYjlJLXI4vSE1fY_BdaYPBvPWsGaPKTNr9kwy0RyDrYd4a3hzDBzEOAGUJm14pdaOSbjtwoIJ0m5TeQRm-e-EBqxv4dcABhod1agzhWgyKZarIrtkDhGW7dkDqSdxHzPCxphtD1a7SD2MdKfz0IK_IkPRSr5N690e9kBMO8r0MmuMg85Jf4vA3w3-ywnIbaW865qXxkW-3CYgJ8RloGuBcJewQH13Ozoz1FAlt1Gt5Q-uHiMokLpmbCmvGVk7xPXqDu_sqRhQSjlEXRBjmGzeotBxxhTwmzqZfJxRXEdmGAtrfqva6gzYGgSdXFWo-_wfN2-DjBa1Z8FAxpmT-dRPNvaKwOmknS-tI5xi2i7kzmh-oIn8n-AJ6WanEBaFc5vTC9SnQNxnjnnbTu-bRMj_KlXXpw-ryvlGEGhdMOqfcgSWzQLPBSVMJpDU9rSZMfGl77Q-S3q9mRfjPnd6TqlNfOskpiQijqlKNvhC_D2S8SerwBOrWTSZ2i0W2NKgtAvkgn1v7wHkNIp6iJ9CU0mXIobg1uDrdvReirxIxuznqXyf9xma99oqKmQvh4dWfhlQH-a8AB1Hl624CTjEs4CcoZfCm2pMpcDie4gVvQiGkHQosnTdOA12IX3REq8peIyawJpoyI50ConQxCFuWqKfZkxvaLMfVAHcpvRNrNEF-jD1lf6R1emRB8jW6iQLCKYVueF6qfUsmb6Ql-gmKcakkB71QGMSGTa91eBg--S11MB79NFQdZhQDpYYc5GAAKTR3PF9Cj-xk_33qn0Xz3Xw5jRTZqm-qVcqPMwcdxcB9p8JhtWuhGcfyGmON9hM83JHg8xKGUn-1qPOnvF1yWoRcI6wv7Xe3jfo-_RHLEwbPTbihfw2H6ycYxEl_iz9zlG40_WNJwwWDdHn-jsau08fNxdR4WC9FEvC7lRAUeQPVxUWE3ziJjlDMeZGz2jy4daSi-LY-QZCzarHtQ4_olBcW11Q8gtV0lOBrkATxbd7YRAL7_dh54Xw9T6X0O7TlpofzzAVMZzIn0iTai8k0eAzuj3DT2FiCHAh4-RbKHr7mzyrPQ0MUmJp2PomCnzG25BUbYSlClBcjtotLGm6YuDPzB5X7Lu_vH9eRjxMEh7ZqIYO6m81D0dwZO9aVZSSwa_LBb1iBFrHijTsL8rHXXcBSnp_jIaZrGLyKkxMaJDegmLd8HdgACP3rOqVCDg1n_CVE3_jRaqwwHJVpani_j77aSGBmItjp7HqbcgZr_CVMCBHX3XfzlhuXZkvBoc8ZaYYifhvgGFGEg0jHEaxIIU0QDqm2L6dHqCH6yAlkkT8zRgWeLH4Pey8nR2KTAZP55YtaaU38cUPOqVlvTmPihzfNHH18h0vLfaPPjA712C9V3hvVACSpU5SsXQU7NfnnIO7_5ZcX-iCaEuDsSFlJcAJFaSyKJh5kcXsGdRCAM5nVfyH6_NFHzGiNWaIqc-E3Yl4a4pS07bpe74bsEUrxUfdgmY9XULfNwuGPVg4qBsSoS8coVBn5SxwVR6OITKjr8Iq6b8EZZxxc6qJJe2Xd5mExe6NxAW3sClorNhS_wwcBYwj6HUH8SmXpZ0xqADYVqky8bn-pa5j6RFNSH5zz9deI4_1ioLhkVtvpbRFHOxCPzm56wjqQnEci9QQd8axmpiKgHP8HnpTzLHO2MgqjjunSox4sXOz_BEEPWghInV_VpmFb0KN0B4UH_M0f9Yar4O1unjCGwlLF_ZfLfNfwmi8JoDRMYIyFn6D1PxQgdBBPKN0oC_Z11E28WQqTORvTJqusVY4qoZ4d1FOkd5E9srOWuvs0gBGweaIzUAZHdRGr4NygezGmf27uWSos68ZHaB2qOc79z_TpsXiVeik5uT-pSbt2R-GEIeg8cwCH1J2u7UHsWLmJFyUmBW3K372QeHxoW8UKinTNg4Zy6uF5acVZmom5E8s957-83Qcs_unrHFoUTPy_KWoiqRefrQcpmCHra-JYSYwNxfwgzoCp-EHgl2ypCIZ5BpRQHgKweWJWeRhioSBwGejT7evYEl3-L_FazZFY5W6tKyXFktO2jIySP0NMGxFL8S-PWQERH9cdm7l1KN849iSIqeMI8cROEUCWjUIhdh9pXJnY8vYhQBfbEjJ2fJFjOEtT8ARZe1jBPNUFdoRph8YXVXRkHn0uw826uIzZGnacbNgRwgNdilq-j1Rj5iirOQwXSQ1s_L2Y2Gl8O7YZ_tuEek0ovZnebzesmYKtoY_XhunbD_U-4afK57BtBTsmm1Ed_AwfhZNV_vqKC5DraEE6c6J_7d1f3NJEMVK-QDm-iMLGdLHjOr3bf8TjpeXNjITXiBZ0kJBb_qf7Y6Sze1UueGWd_23NVi5Ufe8w--C9fE3YT0Hl0wnSRJ1WvOGlLQf2Hgk8KaazMuCVbkNFzjojCQ_IrmsEz2sbWOSMDB_E2y-6JJyET54mCpfMYhdHXVhtbAH0sdBNtp2KGfh9206nOJU-lKwjo71lgNm4XoWV5Ux1LXYSeN9r7BSrpirkFIqxyQkJez9Ulcbiz5ES5t8oaTwCOnIDE28Vy324HhGPSi5W2QPkCOV_PjOWCeM8yjS_6w_FnGuO_26ecaOEkCNBZung5p0pHSmD9D0SeQ55YvwYvwMhT3smiwDo9dRcFa6sigkWHHKtBLW29sYLB4r5pNWtHd6CihJCcG9DTTbaE5qP0-eOF1l4GKEhtIUKDPGJGwEzYHjq9emeIy1uacdIcWTCJylvCVOHdWmLaD1HefI1tjSyga1LuX-uZPAYEu4H3BHd_8RhEhTIIR2W1Zi4pcy___Mg6UnxiELbieUU9M-kBKnEG8wm1_VCAJVg6GulXQG20z5Zq0Zr8HsRUEpcO6ULm-_3zF1WYWSPU-JDi_ZiKxGdLOidzU4gb-zzrrLYtA2USFwdncVimCESLHhKPSvv6r2xX5Hz0eTuLmhshN4wL2du7QNz_mLVnI0aIGrHWQgs_DEy06L1P4ANm_Y-0xdzookmfICUGKChRsnNFH5Ardfg5JWwzC_jQrW1XM_t8g-3Hnv_A-UzUyJWBl3ezae1NPikowsbMsIwLuHHteDmQmqb9-93yiUdXB9FxycWFgaPksF17KxTvI8FS2PPwZKsSOTXMQNCQyFd4fJDR60nQhm19DhQImTl_QPvqibTAg_p5zlhxlEFdMKoMEdSrqovWF0mKoOLbIHlGum-tDlq2Ll96PE2-CrnW8NyHVDdew8iZSZ5dahyl3prZnh_EiRB8nNBESy8uH9ppuSH6XlQ0TJXdhwI1ZdOJvFonZ-7IBR1TVb4ynvpzRt-oWE-tNx1-6qwSJGzrsKnn1EYkDQaRj7nfztiOa9af0LGUR5ejBaZVx-bQ-75PO-xBTxd0UpI5kyaEf9T3rUM19GzASEzvIwPCPRplhpopMmPORqBqg1oFxqI9vzahfzntnYmWEBLGc2ks1NZWq1gLcSZLw947_EEGgyqw51cFGXLaB1DeA85qa6WT1jRmS4Fjj747XLPynyNH73NU8RWsx03F0y_fvUpPGS_vaXWR8AhEy-gdBW5CCYbsPv7WB1Ls0_DJMBSHylHgNQvC_5knHobolZyERyyye0rwmLca0TnAJS0QhgywEwaoateT_H3_aqypXAFQdqP9aXzDLINETQH-jPND97CG-mhA5bh_mmulEvQMxHyt1e4d2IWPOJjYUvSj1gaxoNl8C_v-h8719rmYl7e5jedHHzYQuDgq-i4B8HlQxgLycD2vQqtt9F8fadudBvjaa4qaHQNw_AZc_8aWNUQ23FdSfC2ZSwJvYASGSz5iwwZotTwF92WMyzfnNvdjFyluEZR4D2RXnYP9GUuwGcg6LvtzjZDq4GoOG8cZEqgSQpSUFWN4-NUVBrb8GLY-SDo08tW7Q42PvN8h6h6cPCpFgrKFrqEuNupBiw_GvD-Ihj6S81070U74EpW3yin5jY5dVGJO_Q-8GBVsyfe9VyPGlDCt9p2-FwvgP6aMZnWAQys5HjDo7QxHaLXAUAJEB4HJatbd3sDYsC3S3Py-_NDzA9_JuOI4iqvOjwf96mS8xfOkoDY0CyKso6cn7BWBDbtgGL5yjjAOrsgyRzALWaUehhq0p48D45hMtJh40lBfgA2QkEqXaqlFdooXKlfyn0nePdsQPYJWxg4O42Up_ha9yeggy_bdTtWJQlR1bpgphhsDFFhPq3rrrD54e-AmMPvLS_KnhRHR22d8t80bo2yhrXzT612iv6Z_2_wxWbm8AnUB1L4t1pnI0BW9MLhU0EC55f52wZCJQ8wJdRcH4lbuUsZ4ioBA8J6X-UtP7YjjBTeXITfvyCaLvkwGseuU4DCiTHh6mkqIq6ynzsg9kXqjCB7oDfO8yZm82JEuzLWaReeZSub0J4FAyCUQImgs3Ui1shcwK6IVbk57-Gjywva17R7qQhkYxqeDCbrd64y3QLFBnhiYSN4TrR5AaPiNz3eCYFYPTdMjNCWa7HMb8wgI8Bix513uKuS7HenMc_h1QwCzrD146GKiiEZ0LT2IIDDO8h_gKx3Y-7N5B9Og7wjsDps624fXnr889NYznFOBwuVhNmT4aULq_L32VNXYO7bvGEm8T__RrBnigqlftf0nHzP2U7gN3kKnuCg0VryDRRs30No9mmIxpCzEkGfEDb3g8SxDiiyOjZEuFTG-doTdRDPfe8DqiPTfJdFWRfDkBKFbpnV46-Dy1PKe1HdpoF82ggBjtwT6N3GZ4MPq1UVYQ6aiwlk-vUpetZHohzn1AD15XlDE_NfnZHhvGrHGApPPUFCMmZRmqQTkNH4IEpUDQM4_SacoAIdkrgHO7PoUAFoHYMpumQ2pow4VTR3mj0tpvG-iIBbcxvqc5XLQQZhXuhDVAEl3p8HPTDKqFgxTxiKT_Ns2pfkp7zHS9-Qp6VzlZgoa1Kt-ipc-BOpwBzzeDqg5bOYvDF4mySuTfNy7RnMfX2F0WZKN0j0Rbo99iNUgkvxQNTAsicaZGuGWaUbgiQI5OT_kltLhbL0Lwk4AQpgKHQ0OBgIYC7ONSWNWlHqRTR0CGRYRPPB5tOfzJ9iVeKQKgTnH-PTukqdsxJyrwalRgF9I_b3qBXCFeY7Ea1JyqYhi2c1OLLoI8UJ1kNsH9Jsuww0WjthK7U5KQEHkQTZSjdEyoD3M-daQhocYGcPqRLqt_kfDWpA9fQYJVlMCUL9aQuMdYVz0ZzZwV4PhAoqep2MwxErhdjEUPhqyt4mVopZW-Zyigqpw7ef5K8lrBvtfLV3rt0hFTzuxACp1wQOWVsYvY36I0Yff9iHGHaOArfsR0KgDgbNK7E7D5CtFrHyOn5XGjWcdjLaYKvCJ8wKrIItOXpWEMxBCcKsKsj3bo_jJKiKYS5hVeaznfwc7pi0J21-4BAkb9Vs4XqIcooEFbUlqFSxWMuBokQAsxBEdeZ4ZEWbD_jZdx8NxELKLxPuKiYYmaljKyW4NqhyeGPgFxeHV7PC8fZ5O1Zg2sTMkW7J_BkZte3oGa9zeENRYMYmVp90gURGZ9vex7-GM362BBH-Uq9w9XYGL_yVfylRVU2PGoCEmMoxqgxsYTt6t--noIEO67jMxWhOdX-i2bLo4xdZnTBBDiiCwDLBM4SS5FWv9Q1b5NO8GL9ePjw0PEowJy6Lhq1MEBrQSR_AiNr7tAQPoJc-ltUMtBCn0FrDKT8UZchBVaMPazNXHJyJB__MZfJLc36Pr3xI3YG7C7plb4MOzJ2UU7knbHbcGM8WqKykYOBlde91ywezS-WEo8EUTO9rVUTDPwSPH2NjnuFnu9cEAmXYicqip9J5WLcnWxKuo51O53VaSXa3KOwkRsh86PPoxbN_6boEBx2b78eQOgVrE8T52OD8SryaCcj7GmHsA-nLWXhAZ98WTCCR_O3N3JZSMDB8NNKaTdyjILTThzcZBAMHpCZteh3JxXO2kiw9Q53cCVt-PNAVFwgANiyFFW00sGKI1VxK2SqsCXupmVQqzwJ_VN_KyQfh56xgMWxEucdcbneMoOWUzDZduKIBBhM3BiiaidHeflnpuDid8poBugQVdxNZdxxi27cdV7h0ieu0WAJj5G4DjNY5XI-S3cilYnTXUNg3nE4kQb6jVsjVPKwS7sur3AvwPld2qHJD5Zo5_63axnH-FQuiA2oF7pZxoYiz4IYY94ydG8gOOYteoiwEDD4tDi9_p-Vh19qsJ8NyAaC3sO1mKZUhLpGX4W5vXI9bONL6KfiZtpGsNOS0al73DiqdLiFtAcp68geOr3ym7Miq2xtthT-mCiNOn4HugT-rogZbzPlRK3aHEY3MsLL2BBcPue8ffnazWOosLQuThIGdGwHxSHwk9crZito6H3rfhy5FQYRZELbjkp6XwSzWqwGNh5PvS3a4WxLOImjdS_SdeFFztTbz643sos675Aodwntlo8e97352Zl54dJVBWQQQXZe92VNcHdywcaHzSA2NyLRWz9kJA4R4jHUBq0Kd_y-f_4LZMgcnSJyB_kxotskTdJvy8K4VSB7NSgMxkfzv-DWokMaWuZ6i9lhG6laXjt8SzVmZnBXx2fcGgveBZ0cEEy_ZAjwSaqkircbn6rIcmwjOLxsSvcyHHaB4371u2OZzhoM1eRQ6I_wXHJP2FW4zESJYPOhSWtJ6Apz4rHoUnlDCcg1MnT3Q6PvRNDq0jB26NCCl4ixvXlWtuWTa6_bXBARoDauSXsf9YAX-vnSTK2lOz0pOWgz_QjQw0Lx7nEi4sMXdnGvQNxkSiGAmExZzqAPZwMGbdAJUnjc0jW7Fi28MG3G8cHvO6fcGMo-IHUlH1hr7vMVCViYqjcZQOJ6YgAQNQNe6mXCcsSJij3_AeMXOJvC55N2l9GkRBkByX7-NO0zWRMGZdtYxe-25RMM46v4AZi3A2mH-31HphZ34kIlBH9yb-8Vw4cdUHpY42kEhnXusSk0gx_bGxqJRVVpVgo0EAAAkhSRkWSqJiccp5iZ1yZ2EpHOgEM1vthLyCualal7K-fTHBm5jSjNqNNiZ85xJF3tbnHSjLNdQ-sYcUnhDFedPfS1bzfVZrJBfzjp9_itNRPeJnHhYGe-K9d5TQqjrBAtwrGnMkGhpegfK6Ac2Nklvcl-yCdX0Fx_OYe6peI4slr4S9XmZBj3ZpG7PX4NdyAKDu0GwufKIcSATJlFk-1L17vj-b54H5iFj5472wPjh-E9NJ2UWS5GbEC8TPpqw5wQH_Q4KnOIE03lgzCcImIKW4jK52uCSsBljKI5CXQzgTj2lR2lf7OqqEwyuFP6KEm4Gbd98fASaqrgFmR3CBqJfFkaIeuluglEt6hbkIQU4KlhVJ1kwkOq23gcjyxC4TXYEBNake_62MYh17xz5yxky34x6cl8B-e14KXqOG5qG5ug3gsoD334ICr72xkt-m3mICgkUYOSBE83pb2AA7YuW5IqwTLStyt03wQhYmDXd_q4FBM7ZO-uwue_cT49vvpDHBAL7zwG9if6P_wwVVqO85qFfri0-S37JXpakkJ6_9SUpM18Yo4g2SbEoFLE_psEgmhRAVyGZjGMCU2Yb2Nh6eQaVhuiciWgij3Hf69IJYKZ7dgNmCuuTMp_VlJ0_bDWGlAQZUvZoXemSxVUvOEMjNj0JxhAnuo6Pi9eWLcpy018a71RUAcCrdI6NLvPBNr6qYJgZL2YE6lLe5kN2xxuxtNIm0PdkyvAo9N0OGwXOkQcY8KxwwhBPI01FGQ1ULM51ICIEBERqQD5-RkIAICNR6o8zZD-6Iqah6mvg2OOhpEWzyTuIV6y3d_hOKpYtdPZ0tYpmGdXjl0CM6UZmUyAxk43Frunx0UQg3pA_Awwu5YhXCPek64_gbjQve8bn5Dxl6ZAvBAk85VngWQNtjH4JNk2GABmghnZr2ZHWhO_GX-q3KKTyOqbUjACY1il-tUhIs0TkcQqrYLRMXRrSACeDKw1VWm6iTI_6IYfcUGs_H1Y0fgyCSI3lq3495MNy-dbp-G5WiAQCZI_mqzoxTcr0EifYsDKQuzpSs4e6e4beFerRgJmLVr9Jgo9heM988Va39i0Vo0AEIPlaZqLXrAz--eT1xxSdBi6JlxKS2uzYsl800ySl66rIKPUoXdkVni_F_20mmkwEGCAQ4ZJS1g52aDOSjCYPuP4nUfCCL1868DyocogHBIwr7PCQ4-_0e7rKflnzCoPtETbNRKJj55oRaiAlFdqaTWWSMp_LjH7w0GFXxzTtnuur3GA3QaeaCO9bIPf-kiFhBArunZ4iY6SdxqV2bu3ANgoc35zfPy7r4wZDnS2BfHFn6KXRHhns5yN5U-OVjT2pIBWbLxQj8J8TOrSGYkpcTwJ526XWPKA03qIn2pOEe4wUDkW0tkxyyIgt5cCjSPWhhQQLsYYKJ8rk2ojWvIHSdHSgIof0eVI51RGCW4jcg2pJ3I25sFIfpgqI5QipxB75eTIB32XCBtzWmK2E6dPAQfnHNPYITbjLmOrH2f6zbW1_LJ3LVtMMijseSomNhA0v4KUEBy5aOriMgwBRc2doCITBcWz0OD6TCXbcrNvW7g6BDK67Ym4Vpn6bl3B4tIH19TNQB4YhX4z2kAyhlOOlvwqMcfhtdiNxuSZ7BAqQYixn5dDpswpCqiI_MjH51TMikt-YBBCHTr-RGRIXaWxk2sTl01agDUdyWGJ8wsP1f0ndpLm3fHdejNab0MOn6osZGpP3ZgZIYoX0o7CoF_5lVDdc08Dt7L_yEmzk4ccF-JQ0JtbfYdzvc4OrUBm3zQfNVsdw_AQHE0H8y3wolZFgsPzAOF39j-_9SDKkZQAHkO42MKEBuDYNRANGd41ztyybua00Dn8XEYC7OiWofp6CNgeFts0oXhYM7YU-0A8h4n_xVYrk-0Rb-zpprX3pmPsLySXIDR0EBHRdi54BjFeutO1ODlZUI0JXKinpc3TEq1Q8Umhk5Yid-CmzYfaVtt65hsdKIybzDgZkBSqOZHNlU-qgtHZsZjB7HhlsQH_hsJMfO_GDYmvUyL61zZ_6i-kzVl9kQzarBALNWbFaReiu2SG9cY4n8raKYyXQxQXE31wFUrKaibEAXJlq26xQzmZmf12t4-3ZVxMi15PRbREWLYGzqNRARqU3mHd3_FPTeaLxcWy-KfufvSTVOIYkKoAXAbHfGckSZgQMlCPqKvao0Lss7N3bdcI04kJRmOcExYhAXvepyznGreKpfwWLm2YpoPgFuWq2cbkOg_KNOxeI-SCe8WL5geA7u7S-PPZZ89jarsvO7kPAIQXxHg7a46y9wzDLclZD7UcECTva6MEKRlMP5zsg4EfRkmZ8AQcykymQikio50dvSITkyqtD5XLkLYv2eypab6-1CHu3z-YUQSHYLOw4fsU6dR8lToK4I4pl9auL2j4z2FqwZTt-wnGkTXTevikprpz7BBaY78BYmJHquSGjIEoy59aBoFNWsKLhyB7r-JFAVRXgZAspE59-JmzJVSIfyNWXThYFzabEXW2VmUNRAcb2pRUP7KYWY8xqgZTvQZ2mtXQBY4GpAoXR6jgH-fmWg988kAQBxRnDoZgb0VqOUNQK29C5BIEt8CsHE97YSouTsqqGtATh9YQUinkIpjyHMAYRfnkMiywoFYeaJdEd4DFPIvJ_MmDWtg43nh4dbJahewqSfAzmFH1B-js9WAG7bivifCkEFdHfWcyDybAKICp2iZ4clqNYH9EoSgYJuDnUoyHrBvhWbaG4CZFi6bALdp68fj_7D6MCId76bo2D47SRj-q6bzrQFHvrbfK86EdM5KbJftG9ieNvuE7PjAEAheezl1fxBBKKZDCnxPzovqnmBX3mnEy_giFlxpBfUm7g0ot-FrszjXCMAcw4PNQchogsmtV8zQ8XZOo2Rlay3YmS9-nK2Z1jEBXckY8C8y2IavccKdbWAOUidl9LsHe0wLA0tC0YcAQH5HF1yfqhXeaUXmVA1tF7vJW6tBMsm443zWLqD3MvCjC6DoUb1O6IMaeSwvS7spYGuleZPr4OvXuWcylIBgHS8TlIwoo4P1zBFAlYOYCGsulS8TBKmLxOWskPS-grktYEBBK-uDxU9pVaKCMWy_l_LV8-r3z2HRajh54V3cEsSiG5CF5_EVeFJzAzQTGd79k-AjLERnGw7kNMs4LWMhPS-00_R3nRt_OPxiVnSY_vNyT3HHpf8Lf7NQnZQQ7jM6d3BBSmIUlvlECPBpaVgP6oc1FKSkSPs-6DGL-DkJW3Xo0WlcJKwl7rIXjCrM0t6n3ioRNkxBOg3grZKqF12fnWOn-jtqr0V0Iw4Lf-3Gh007OcyCIy1-RENp6DXM8JKsg1XwQTo7OfDfyf3ZSDWOLan4L6hrHPXKBKtk0m1fJvJQ9dwEM3jzPWJBilBQDI_09Nr2MCbLzNTGi2wzGMlMt4B8u7g6B5wmRWKDZchS0pSFgP8B6maEEZ8JH-c6p7wk6YfeMEC2Ih-KN9IEUvnsh-b6jj0FwcqtpWKlHBJFWJtGnXMT8rDuYX5Mm_-lAWornFLriTA8I9uu1ZOGiej0pWVgoQVWFawXYkYuoZRW5q4OGBwpiPtZIYAyDoZeAUOu7FAqrTBA2NfYfJr9vsXJOaDiYPDHRgf9IPb4xQHM0YSgpvkCDTERAkFVgQ0lLemlf2qcUXjgmQg2MNuI1NcMCu9A9o8-g15M6Sswsu2uLf8PD13MAUsf2bSudfdKaViZvkMCJ-VgQKsy2y-9J6nybC5tzJ9S3yfnlqMyHkbrxFAUf7NnocSzZcRtuRUpuGZsx20gb8xHIA7aUuwd41zsDvsOUpovILruvtFXnA2_18wbHXFKUGmKPHYYGLsz3rhJNtjs0dZF8EDD2XVmxsow3EHn4CXSQkJ8x3D5sDdyQE74fx_9l-BybhGK0-Ww_qLjHwwArVN6GcDacya-onH823CihgmmZKN3bg_XP0Q1c37IUApEO-R6ywQpAOWGv_re4uecj_1jmbBAxwRcvCNpNSwoGTm8_KSozpV6-vadvp_RC3TDHkH7f97yLxJ7ROIt5J8cQl-9eNJBHtVvWv0H0oe8V42gg4FsXB7_Fv8Ou9YUFWaJYb7FVU3IyWGVNYJyPoT662ImG2kQQHTzoNdHPdqTT_kh421XyfaJINAHA3KzKTcOq_4uNp3hq158xepsHM8HLizQKPI_oM3qvpSMxj-BuMVfkDGTnsX-JLAe3NA8yuFiZXyziuYw6hC4rMLuV5UTNJZnGS-3EEGSXXHCfghBQslnMt4jDj1X9FYwL8cJCmPPC9sEgpCfBdPYZCJUjoxwd2i4Nd2vweECi1KOOoFCdmTcDcp6WmlQxv06XLgfCiyC50yBmqw034Ukq2IsrYFPDsITQIQG_HBAe6k-2dxanLxJGlZK6CPCx2MKGElRlIESSqa99pCuUgzdvs-_ZbG-fjr42LTHtP0hHJy_ngCjrt8IgDmUKI3xEvlXZRnxnp4jkH-7FwZoKkh01DjFYkAscw5BjAlcWFqgQFnqle20OyaUTMaYIvjf-0ZUOpGi_wab0RYW1i5s61xvKyIk_2evZ87LyS57WccbcLy88MJ26kRxPMf9rOcEetd1aZxykk73d7A_pj7zxIrvjeExHyxUrM0XFgLN79kvoEAhyhFdZ_FZItdc98yLjaToxZPORBhTn1w0nj4spz5FjshbItFfVLfGCsAxgxRI88AO2oB8389PNPMe8tA4uMPMC2PFTqK795Hek8Vos_khmzeiXwo1BQaVfwLglOeKhUBAuoVvCyh93vTjhapy14oMAt24rP1eeHnQjee5Lfb_8p3gXOMQ39yxQ0Ts32B-CfxQzbPQrRQtJls8Y6lVDr0oOFz1gMHDWRrzA5z3tqHpj0Cxe3R1luIIQ06DHrv73dswQFCY6mYUsMfumIz3WAO0sa7s8fzbGRpG4zcA5_zxQpkwOEmTbBf8n_7vCRaS3weOMVJBuNSJCiQGBHR2eESoSSbV_ESxcoPGf-Wz_Fam4chWBty66ZX9gMqaAE1zWKAGMEF9zlemaUpKjF_NQJkTSbvh94a6Rtr-WR9QhWFzNxPBPIxItxGb5yNTiGZ6Ie-tQJE2Kyd1SmcfUY5fJnCdItfpnyXL4WSAbSsob9XVg4Op0uBGG4yXL__kme-X8WI0wABAACDV6iueeDk3PptXUV0BSR3PCdB9sa2FWGoPt81rhXS1voD5ApICH0CYlLLFnsnBNNi0fB0f7ZKC8y4286yDEl0NhkKDvq2n9HkwBGA_oiFOcGotvk5QXufiP82pBzLwQOow95Fx6OM7HK_uPVjzxxdawXQgSdHoQiMJwbUK2UYbfr0iYvGr8ERELWRTOOiBcZYsSsNhYHMvwVW5ahDFqpCiW8JJOq6gjlJmZ3cvwVWD7kgLmJXMnnRqtqaYl9Uk0EBEw6CZI8R0Fprd4sn-AM5SIgL6PkVm0AsR9FkBxFO5F6x3-DMWIZnbpEFcOjgpkwAtbmPtesiKe7w_XeKXSYKPfzCM5wyVZ7sq4BZaQSMzOEOgpFp7_W4kjVZuWL4HvPBA0eaJkqCCnO9CvTPynRPisSgqY5zcysrcKLAAHSQ247c1yi8smlgYsFznlptT_2rAD8h2xfxUSv9KDaokZ9LROVtS1pGJumZfwAKuHqEis6B5GAG1uZw8SgmRDB5-_dcAQWOP6jgn5PBB08RKA4xGMxzHTTF0iQgF1HMX4ScdvPmR2tC1g2_z9NYw5VvHewjIQTVUgKhl6WkLiggz4qCItjEQ-sQaFctZo2QgTphAAhAPbVVKGmXydWSPn9-MLyRxMEFd_MFPx0xEKWUtWopZnXoAnB6cuRUlaR7Ex1bd9kSJeRT-zS9vg6SmVVeqqF10HbBydZAp2CPsaAXMzrohNXkjT1tHa5DFsGCWN8Pl96gZ4XU0hcy0-v_g66wmMXmP7XBBUEh8wlJ2tg5_32LC9uz3mUecfSbUnNnM7jzPEBx0MWh0T5W4oXWkjl0JtkiRFaawUveTNuckzEnkGqxWKC3Pfi-4_c19f14CGUzZTVXhAWYKQD15Ldl65r6xU7U87dFAQUOHcEY6KUiQ-xEZztcLU_KDfunv1hTy9IE73SiYpIvhvSeus46KY7z9D_G1Hw7nQFhHgxspVLEjejdXY5Pms0wE_YhQ-bkrCOPXpnJxE194xSi57ykPsPH5TBygVP_fwEFAdqOPwiKKQ4MV-d2G2-omn1DCyqoL0Vc-bvCee7FYytR_RFO2_xikbrBZwnj_buFvANP_K1TtKf04nY7mjKJiSbrTdpywo8PvxNB2JpBD9gkVPuA2oMFvUFHHownN0jBA9yWmiKpQTY_ZqT2TR2bmCTmwL3sZEdPVl0oaBlPiFZbDTLGgF-4fBlm_xZl1OiAhj4KxXwB7w_DqvCS0V34A0o-Su4VjZzaEqO3cTuPCBuJRfnExkN0QMMtx-OMPaumAQSyZ7-x27l3q_-q2ABDt7hOImYxGar-1FLvfxxmv_aAUPWCKHHyEk-TpdjgaLYs3EWC2FD-DNMegViiW_kEhe5hNwBo_JVCn82HCUH14yb3mZwFNe2vAp5WvSVoSdkBCgEELEZw33U_IZSQ5fm0BtguhMiFPbE86oWsZYU3cs3LiC3hW-hEBIIiqIh3zxWg7Z8AcaoK_0hQeGI2DANl22GKyVTRdHgB6Vv2Ggz-KqB3NYkLJ3AirxooP_x_mqVVoIj"}}],"authentication":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"assertionMethod":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"keyAgreement":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"capabilityInvocation":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"capabilityDelegation":["#ePyXsaNza8buW6gNXaoGZ07LMTxgLC9K7cbaIjIizTI","#QDsGIX_7NfNEaXdEeV7PJ5e_CwoH5LlF3srsCp5dcHA"],"service":[{"id":"#TrustchainID","type":"Identity","serviceEndpoint":"https://identity.foundation/ion/trustchain-root-plus-2"},{"id":"#RSSPublicKey","type":"IPFSKey","serviceEndpoint":"QmdPZgcyqHJTiPeGMcAu2AAkZZ1U4KtdQXid1gdJQtpvyU"}]},"did_doc_meta":{"method":{"recoveryCommitment":"EiCy4pW16uB7H-ijA6V6jO6ddWfGCwqNcDSJpdv_USzoRA","updateCommitment":"EiB8B_LS_O3NWo2P8fSuRwS32GODaXoLREZHdqpg6x86yA","published":true},"canonicalId":"did:ion:test:EiAtHHKFJWAk5AsM3tgCut3OiBY4ekHTf66AAjoysXL65Q","proof":{"type":"JsonWebSignature2020","id":"did:ion:test:EiBVpjUxXeSRJpvj2TewlX9zNF3GKMCKWwGmKBZqF6pk_A","proofValue":"eyJhbGciOiJFUzI1NksifQ.IkVpQV91YUV2QjctR0FyRTlkeERuMk1rclRUa0t0VXN4eGJPc1NESzhwQjl0ZWci.X94wTgzsovLEAXU1CG5M0Gqs6Gu9oHklr4Zn7aEbrdtOI_WCSCrWJuYomkcdeF8X5dV_ApZ6Gh08pPcV2VSClQ"}},"chunk_file":[31,139,8,0,0,0,0,0,0,3,229,147,79,115,162,76,16,198,191,11,231,240,70,197,8,230,38,50,138,18,64,16,212,184,149,178,6,152,0,242,103,6,102,64,97,43,223,125,199,36,149,125,79,123,217,220,246,48,85,93,205,211,221,195,211,191,249,41,68,40,103,144,10,143,63,126,10,4,178,48,65,31,49,12,89,138,75,225,81,168,17,201,97,136,132,59,33,194,97,83,160,146,9,143,92,218,4,121,26,26,168,251,80,167,17,87,78,231,166,231,74,81,187,108,47,197,196,176,59,120,0,96,101,27,10,176,61,134,50,177,148,213,221,116,187,83,235,17,160,188,27,235,8,226,69,107,138,203,61,10,182,105,92,66,214,212,104,52,24,13,248,215,175,254,235,75,118,155,23,214,45,23,83,20,146,209,195,36,27,114,69,198,58,158,1,115,30,94,121,32,187,200,209,245,142,46,175,65,215,24,192,41,40,113,236,243,147,140,29,191,210,188,60,242,244,38,156,238,36,177,163,152,87,220,74,179,125,91,108,229,163,221,106,126,178,80,104,103,15,148,141,218,3,2,50,73,61,154,77,147,101,45,88,219,198,246,92,1,225,237,118,163,154,96,250,238,142,0,41,69,245,205,31,19,177,4,71,188,33,108,88,194,173,73,67,248,110,27,191,30,234,102,113,141,208,187,97,119,66,8,9,12,210,60,101,221,170,108,241,151,234,119,90,67,57,138,63,210,47,111,47,119,252,87,235,54,13,209,255,236,245,234,134,242,253,192,180,92,105,191,253,91,69,183,177,220,139,175,18,80,70,4,167,183,53,9,9,99,132,62,222,223,167,159,162,255,94,113,83,70,239,83,238,111,135,125,181,20,107,140,153,240,246,242,118,155,221,16,174,65,115,92,20,41,251,88,184,0,82,109,231,2,175,61,106,211,116,235,151,184,17,117,127,214,63,60,23,217,105,33,49,210,245,177,188,88,14,207,145,187,20,143,110,204,253,250,107,154,178,115,85,215,210,220,203,138,188,95,31,119,120,192,55,114,181,168,210,214,198,131,13,104,118,178,67,172,218,227,45,50,157,239,167,9,206,136,129,131,141,173,232,202,197,110,69,188,244,20,67,178,160,168,228,162,58,156,1,169,81,143,245,126,233,77,22,107,255,147,166,136,18,80,229,108,198,114,207,88,203,225,206,221,156,98,211,204,202,78,219,84,254,69,92,235,57,185,108,71,197,162,73,6,255,8,77,34,201,27,42,14,255,8,213,124,0,162,75,239,132,149,249,108,29,78,18,172,240,209,242,97,51,158,25,246,147,20,31,156,135,77,47,205,188,116,88,157,210,217,119,64,133,54,221,129,66,171,135,74,208,236,39,177,117,128,120,121,28,200,79,166,119,141,159,230,83,67,14,3,184,58,175,210,222,91,125,63,84,131,178,116,69,210,143,192,113,249,26,200,96,88,189,54,122,82,106,174,50,26,235,137,154,226,107,63,6,34,80,205,210,28,127,66,85,239,43,109,183,150,146,225,196,245,134,150,232,83,89,151,175,238,181,13,230,3,63,55,77,199,137,175,197,193,62,68,227,224,249,159,130,106,244,71,168,84,237,149,26,173,10,183,179,103,254,128,137,44,163,205,212,210,109,226,239,93,51,9,125,203,92,91,186,167,237,29,235,50,186,220,26,253,2,202,202,18,61,4,7,0,0],"provisional_index_file":[31,139,8,0,0,0,0,0,0,3,171,86,74,206,40,205,203,46,86,178,138,174,134,48,221,50,115,82,67,139,50,149,172,148,2,115,195,83,189,77,3,146,188,29,131,43,253,178,92,35,189,189,204,140,44,243,204,42,74,203,115,19,139,162,66,34,205,10,138,82,12,45,29,253,10,74,204,10,43,253,148,106,99,107,1,80,57,150,45,78,0,0,0],"core_index_file":[31,139,8,0,0,0,0,0,0,3,133,144,221,142,162,48,0,70,223,165,215,67,34,136,69,189,107,65,20,196,65,228,71,119,55,27,211,129,162,229,183,219,34,35,24,223,125,221,125,128,153,235,47,57,231,228,123,0,46,218,158,73,214,54,164,114,154,140,222,109,86,209,88,48,176,4,65,157,159,16,209,118,114,35,121,26,133,131,184,233,173,250,81,236,131,149,231,49,168,73,161,113,180,101,163,45,59,114,15,37,120,3,45,167,130,116,47,148,4,203,7,72,5,37,29,5,203,95,15,32,111,121,206,238,22,233,200,191,33,163,85,71,54,68,94,95,142,21,195,37,58,45,6,37,146,103,201,118,99,52,150,57,26,247,219,189,227,124,224,201,116,186,175,236,201,161,234,237,133,49,100,238,229,229,16,52,109,123,42,6,179,173,107,214,213,180,233,254,83,204,161,238,85,195,95,99,36,13,234,213,181,142,157,147,101,6,56,185,102,126,140,78,179,32,227,142,120,215,67,107,53,251,4,207,231,219,55,77,184,236,39,197,108,110,96,43,140,10,183,115,122,205,114,109,223,223,148,16,106,205,34,110,175,106,223,97,242,99,234,110,209,23,77,149,79,142,67,186,238,213,90,9,104,17,23,120,226,169,243,53,180,18,59,161,81,96,254,52,111,209,65,212,163,137,131,203,247,77,86,68,108,228,159,9,133,83,87,247,118,136,163,157,178,72,80,59,111,46,179,121,20,113,109,171,104,66,109,104,9,171,224,171,159,116,126,84,225,13,27,27,133,21,8,38,176,240,97,150,29,243,181,249,249,231,61,181,66,151,103,253,57,14,199,246,128,94,77,191,159,207,191,142,167,192,117,34,2,0,0],"transaction":[2,0,0,0,1,113,221,4,189,16,26,231,2,48,224,28,93,57,7,140,195,149,161,45,117,110,230,205,103,61,52,184,254,125,243,83,89,1,0,0,0,106,71,48,68,2,32,33,204,63,234,205,220,221,165,43,15,131,19,214,231,83,195,252,217,246,170,251,83,229,47,78,58,174,92,91,222,243,186,2,32,71,116,233,174,111,54,233,197,138,99,93,100,175,153,165,194,166,101,203,26,217,146,169,131,208,230,247,254,171,12,5,2,1,33,3,210,138,101,166,212,146,135,234,245,80,56,11,62,159,113,207,113,16,105,102,75,44,32,130,109,119,241,154,12,3,85,7,255,255,255,255,2,0,0,0,0,0,0,0,0,54,106,52,105,111,110,58,51,46,81,109,82,118,103,90,109,52,74,51,74,83,120,102,107,52,119,82,106,69,50,117,50,72,105,50,85,55,86,109,111,98,89,110,112,113,104,113,72,53,81,80,54,74,57,55,109,76,238,0,0,0,0,0,25,118,169,20,199,246,99,10,196,245,226,169,38,84,22,59,206,40,9,49,99,20,24,221,136,172,0,0,0,0],"merkle_block":[0,224,228,44,50,91,136,90,53,184,101,89,134,219,136,40,143,2,100,212,246,127,92,201,14,109,13,17,39,0,0,0,0,0,0,0,105,173,156,82,17,65,101,68,32,6,152,112,104,119,198,46,124,201,58,41,245,245,163,29,5,181,212,9,82,121,206,125,61,49,81,99,192,255,63,25,113,234,45,246,29,0,0,0,6,3,211,202,105,163,97,74,203,69,161,73,102,200,18,205,158,224,52,199,5,242,15,172,61,175,143,121,108,153,244,216,5,165,253,142,118,26,226,235,158,11,14,77,98,209,149,153,88,111,185,142,138,123,230,252,113,19,68,30,85,111,179,31,248,44,156,234,132,87,199,197,126,65,242,234,243,46,166,97,119,197,11,227,194,64,83,68,66,52,146,13,149,202,60,196,157,0,163,31,110,109,24,100,1,127,156,249,212,139,81,39,72,113,196,112,14,112,145,223,239,20,175,156,146,197,52,2,21,183,216,140,200,32,33,136,227,131,123,23,29,186,20,255,237,232,241,69,178,200,124,29,188,54,66,102,153,48,81,121,88,251,117,66,156,69,172,170,81,196,22,178,131,96,77,81,95,128,249,93,219,79,97,14,141,219,120,118,152,87,19,135,118,2,175,0],"block_header":[0,224,228,44,50,91,136,90,53,184,101,89,134,219,136,40,143,2,100,212,246,127,92,201,14,109,13,17,39,0,0,0,0,0,0,0,105,173,156,82,17,65,101,68,32,6,152,112,104,119,198,46,124,201,58,41,245,245,163,29,5,181,212,9,82,121,206,125,61,49,81,99,192,255,63,25,113,234,45,246]}"##; From 5623f798ca16efa8903d3848654711173ecb262b Mon Sep 17 00:00:00 2001 From: Sam Greenbury Date: Mon, 23 Sep 2024 16:53:40 +0100 Subject: [PATCH 85/86] Clippy --- trustchain-http/src/attestation_utils.rs | 7 ++++--- trustchain-http/src/requester.rs | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/trustchain-http/src/attestation_utils.rs b/trustchain-http/src/attestation_utils.rs index 40b964c5..3e1ad9d7 100644 --- a/trustchain-http/src/attestation_utils.rs +++ b/trustchain-http/src/attestation_utils.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + fmt::Display, fs::{self, File}, io::{BufWriter, Write}, path::{Path, PathBuf}, @@ -163,9 +164,9 @@ impl AsRef for Nonce { } } -impl ToString for Nonce { - fn to_string(&self) -> String { - self.0.clone() +impl Display for Nonce { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) } } diff --git a/trustchain-http/src/requester.rs b/trustchain-http/src/requester.rs index 90742874..3714e05f 100644 --- a/trustchain-http/src/requester.rs +++ b/trustchain-http/src/requester.rs @@ -202,7 +202,7 @@ pub async fn initiate_content_challenge( &signed_encrypted_challenge.to_string(), services, attestor_p_key.clone(), - &ddid.to_owned(), + ddid, ) .await?; let content_challenge = ContentCRChallenge { @@ -227,7 +227,7 @@ pub async fn content_response( challenge: &str, services: &[Service], attestor_p_key: Jwk, - ddid: &String, + ddid: &str, ) -> Result<(HashMap, String), TrustchainCRError> { // get keys let identity_initiation = IdentityCRInitiation::new() From 563a43855e93d68568603720b20d20bb3f68efbb Mon Sep 17 00:00:00 2001 From: Tim Hobson Date: Mon, 7 Oct 2024 16:18:30 +0100 Subject: [PATCH 86/86] Add init() call in issuer tests --- trustchain-http/src/issuer.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/trustchain-http/src/issuer.rs b/trustchain-http/src/issuer.rs index 226c80df..605a1fd4 100644 --- a/trustchain-http/src/issuer.rs +++ b/trustchain-http/src/issuer.rs @@ -249,6 +249,7 @@ mod tests { vc::{Credential, CredentialSubject, Issuer, URI}, }; use std::{collections::HashMap, sync::Arc}; + use trustchain_core::utils::init; use trustchain_core::{utils::canonicalize, verifier::Verifier}; use trustchain_ion::{trustchain_resolver, verifier::TrustchainVerifier}; @@ -337,6 +338,7 @@ mod tests { #[tokio::test] #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] async fn test_post_issuer_credential() { + init(); let app = TrustchainRouter::from(Arc::new(AppState::new_with_cache( TEST_HTTP_CONFIG.to_owned(), serde_json::from_str(CREDENTIALS).unwrap(), @@ -390,6 +392,7 @@ mod tests { #[tokio::test] #[ignore = "integration test requires ION, MongoDB, IPFS and Bitcoin RPC"] async fn test_post_issuer_rss_credential() { + init(); let app = TrustchainRouter::from(Arc::new(AppState::new_with_cache( TEST_HTTP_CONFIG.to_owned(), serde_json::from_str(CREDENTIALS).unwrap(),