sdk_client/tests/pairing.rs

318 lines
15 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use std::collections::HashMap;
use std::str::FromStr;
use sdk_client::api::{
create_device_from_sp_wallet, create_new_process, create_response_prd, create_update_message, dump_device, get_address, pair_device, parse_cipher, reset_device, restore_device, set_process_cache, set_shared_secrets, setup, update_process_state, validate_state
};
use sdk_common::crypto::AnkSharedSecretHash;
use sdk_common::log::debug;
use sdk_common::pcd::{Member, Pcd, RoleDefinition};
use sdk_common::secrets::SecretsStore;
use serde_json::{json, Map, Value};
use wasm_bindgen_test::*;
mod utils;
use utils::*;
wasm_bindgen_test_configure!(run_in_browser);
/// # Pairing Process Documentation between Alice and Bob
///
/// This test describes the secure pairing process between two devices, Alice and Bob.
///
/// ## What's pairing?
/// Pairing is a process, and abide by the same rules than any other process. The goal of pairing
/// is to define an identity on the network as a set of devices (defined by their sp_address).
/// Being a process it is public and can be audited by anyone, and be used as one's proof of identity.
/// It also contains a session keypair that is updated as necessary. Since all devices are needed to
/// update the key in the process it can then be used to sign a proof that someone was indeed in control
/// of all the devices for some amount of time in a MFA setup.
/// It contains the following mandatory fields:
/// * `roles`: multiple devices represented as sp adresses linked together in the same member. It is recommended
/// to have one `owner` role with one member which is the actual identity and whose signatures are all
/// needed to modify anything in the process.
/// * `session_privkey`: a private key visible by all devices of the member defined in the process, but
/// not by other members. It *must* be changed at every update of the process. This key will be used
/// to sign documents and validate actions for other processes. It's valid as soon as the commitment
/// transaction for the process udpate is seen and it stays valid for _n_ blocks after the update being mined.
/// * `session_pubkey`: the x-only public key derived from the session private key. It's visible by everyone and
/// used for validation by any third party. Obviously it changes with the private key at any update.
/// * `parity`: the parity of the session_pubkey. We could use 33 bytes compressed public key format
/// but using 32 bytes publick key + parity allows for more standard serialization.
///
/// ## Detailed protocol
/// (Here Alice and Bob are used as a convention, but keep in mind they're not 2 different users, but
/// 2 devices belonging to the same user)
/// ## Step 0 - Preliminary step
/// 1. **Establishing a Shared Secret**: A shared secret is established to secure
/// communication between Alice and Bob (see `connect.rs`).
/// ## Step 1 - Pairing Preparation by Alice
/// 1. **Pairing Status Check**: Alice verifies that it's not already paired.
/// 2. **Adding Bob's Address**: Alice adds Bobs address to her own, setting the base for creating
/// a new `Member` object.
/// 3. **Creation of the pairing process**: Alice initializes pairing by creating a prd update that contains
/// both its address and Bob's, and send it to Bob.
///
/// ## Step 2 - Receiving and Confirming the `prd` by Bob
/// 1. **Receiving and Verifying**: Bob receives and decrypts the update `prd` message sent by Alice.
/// 2. **Updating Process State**: Bob identifies the new process and store it, but it doesn't have access
/// to the actual data for now.
/// 3. **Creating and Sending `Prd Confirm`**: Bob creates a confirmation `prd`, which he then
/// sends to Alice to get the pcd containing the state for this new process.
///
/// ## Step 3 - Alice gets confirmation and answers with a pcd
/// 1. **Receiving and Verifying**: Alice receives the `Prd Confirm` sent by Bob.
/// 2. **Sending PCD**: Alice having confirmation that Bob got the update proposal,
/// it now sends the actual data in a pcd.
/// 3. **User confirmation**: At this step we must get the approval of the user. If user confirms
/// the pairing we create a prd response with a valid signature from Alice spend key and send
/// it to Bob.
///
/// ## Step 4 - Finalizing Pairing by Bob
/// 1. **Receiving and Verifying `pcd`**: Bob received the `pcd` and only now can tell what's the
/// process was about.
/// 2. **Validating Pairing State**: Bob retrieves the latest process state and the state change
/// request, in this case, the pairing. User is prompted for validation, and if confirmed a prd response
/// is created and sent(see the **User confirmation** step for Alice).
///
/// ## Commiting the process state
/// 1. **Creating the `commit_msg`**: The first device that got both validations creates the commit_msg that
/// contains a transaction paying a relay to generate the first outpoint to commit the state of the process,
/// the hash of the encrypted state of the process (relay must have access to roles though, either it is clear
/// all along or it was provided with the encryption keys) and the proofs that all devices validated this state.
/// 2. **Actual commitment**: As soon as the relay validated the proofs it spends the outpoint and puts the hash of
/// the whole prd response (including pcd hash and all the proofs) in an OP_RETURN output. The process is now
/// public and can be used to prove identity for other processes.
#[wasm_bindgen_test]
fn test_pairing() {
const RELAY_ADDRESS: &str = "tsp1qqvfm6wvd55r68ltysdhmagg7qavxrzlmm9a7tujsp8qqy6x2vr0muqajt5p2jdxfw450wyeygevypxte29sxlxzgprmh2gwnutnt09slrcqqy5h4";
setup();
let mut alice_process_cache = HashMap::new();
let mut bob_process_cache = HashMap::new();
let mut alice_secrets_store = SecretsStore::new();
let mut bob_secrets_store = SecretsStore::new();
let mut alice_diff_cache = Vec::new();
let mut bob_diff_cache = Vec::new();
debug!("==============================================\nStarting test_pairing\n==============================================");
// ========================= Alice
reset_device().unwrap();
create_device_from_sp_wallet(ALICE_LOGIN_WALLET.to_owned()).unwrap();
// we get our own address
let alice_address = get_address().unwrap();
debug!("alice address: {}", alice_address);
// we scan the qr code or get the address by any other means
let bob_address = helper_get_bob_address();
debug!("bob_address: {}", bob_address);
// we add some shared_secret in both secrets_store
let shared_secret = AnkSharedSecretHash::from_str("c3f1a64e15d2e8d50f852c20b7f0b47cbe002d9ef80bc79582d09d6f38612d45").unwrap();
alice_secrets_store.confirm_secret_for_address(shared_secret, bob_address.as_str().try_into().unwrap());
bob_secrets_store.confirm_secret_for_address(shared_secret, alice_address.as_str().try_into().unwrap());
set_shared_secrets(serde_json::to_string(&alice_secrets_store).unwrap()).unwrap();
// Alice creates the new member with Bob address
let new_member = Member::new(vec![
alice_address.as_str().try_into().unwrap(),
bob_address.as_str().try_into().unwrap(),
])
.unwrap();
let initial_session_privkey = [0u8; 32]; // In reality we would generate a random new key here
let initial_session_pubkey = [0u8; 32];
let pairing_init_state = json!({
"html": "",
"js": "",
"style": "",
"zones": [],
"description": "AliceBob",
"roles": {
"owner": {
"members":
[
new_member
],
"validation_rules":
[
{
"quorum": 1.0,
"fields": [
"description",
"roles",
"session_privkey",
"session_pubkey",
"key_parity"
],
"min_sig_member": 1.0
}
],
"storages": []
}
},
"session_privkey": initial_session_privkey,
"session_pubkey": initial_session_pubkey,
"key_parity": true, // This allows us to use a 32 bytes array in serialization
});
debug!("Alice creates the pairing process");
let create_process_return = create_new_process(pairing_init_state.to_string(), None, RELAY_ADDRESS.to_owned(), 1).unwrap();
let commit_msg = create_process_return.commit_to_send.unwrap();
let secrets_update = create_process_return.secrets.unwrap();
let unconfirmed_secrets = secrets_update.get_all_unconfirmed_secrets();
if !unconfirmed_secrets.is_empty() {
for secret in unconfirmed_secrets {
alice_secrets_store.add_unconfirmed_secret(secret);
}
}
let updated_confirmed_secrets = secrets_update.get_all_confirmed_secrets();
if !updated_confirmed_secrets.is_empty() {
for (address, secret) in updated_confirmed_secrets {
alice_secrets_store.confirm_secret_for_address(secret, address);
}
}
let updated_process = create_process_return.updated_process.unwrap();
alice_process_cache.insert(updated_process.commitment_tx, updated_process.current_process);
// Alice keeps track of the change she needs to validate
let create_process_diffs = updated_process.new_diffs;
let new_state_id = &create_process_diffs.get(0).unwrap().state_id;
alice_diff_cache.extend(create_process_diffs.iter());
// We send the commit_msg to the relay we got the address from
// now we create prd update for this new process
debug!("Alice creates an update prd to Bob");
let create_update_return = create_update_message(updated_process.commitment_tx.to_string(), new_state_id.clone()).unwrap();
let updated_process = create_update_return.updated_process.unwrap();
alice_process_cache.insert(updated_process.commitment_tx, updated_process.current_process);
debug!("Alice pairs her device");
pair_device(updated_process.commitment_tx.to_string(), vec![helper_get_bob_address()]).unwrap();
let alice_to_bob_cipher = &create_update_return.ciphers_to_send[0];
// this is only for testing, as we're playing both parts
let alice_device = dump_device().unwrap();
// ======================= Bob
reset_device().unwrap();
create_device_from_sp_wallet(BOB_LOGIN_WALLET.to_owned()).unwrap();
set_shared_secrets(serde_json::to_string(&bob_secrets_store).unwrap()).unwrap();
debug!("Bob receives the update prd");
let bob_parsed_return = parse_cipher(alice_to_bob_cipher.to_owned()).unwrap();
let updated_process = bob_parsed_return.updated_process.unwrap();
let parsed_prd_diffs = updated_process.new_diffs;
// debug!("Bob creates process {} with state {}", updated_process.commitment_tx, new_state_id);
bob_process_cache.insert(updated_process.commitment_tx, updated_process.current_process);
// Bob also keeps track of changes
bob_diff_cache.extend(parsed_prd_diffs.into_iter());
debug!("Bob can now fetch the data from storage using the hashes");
// We have to cheat here and let Bob access Alice process cache
let process = alice_process_cache.get(&updated_process.commitment_tx).unwrap();
let state = process.get_state_for_id(&new_state_id).unwrap();
let hash2values: Map<String, Value> = bob_diff_cache.iter()
.filter(|diff| diff.state_id == *new_state_id)
.map(|diff| {
let encrypted_value = state.encrypted_pcd.as_object().unwrap().get(&diff.field).unwrap();
(diff.value_commitment.clone(), encrypted_value.clone())
})
.collect();
let update_process_res = update_process_state(updated_process.commitment_tx.to_string(), new_state_id.clone(), serde_json::to_string(&Value::Object(hash2values)).unwrap()).unwrap();
let updated_process = update_process_res.updated_process.unwrap();
let parsed_prd_diffs = updated_process.new_diffs;
bob_process_cache.insert(updated_process.commitment_tx, updated_process.current_process);
bob_diff_cache.extend(parsed_prd_diffs);
// We can also prune the old diffs from the cache
bob_diff_cache.retain(|diff| diff.new_value != Value::Null);
// this is only for testing, as we're playing both parts
let bob_device = dump_device().unwrap();
// ======================= Alice
reset_device().unwrap();
restore_device(alice_device).unwrap();
set_process_cache(serde_json::to_string(&alice_process_cache).unwrap()).unwrap();
set_shared_secrets(serde_json::to_string(&alice_secrets_store).unwrap()).unwrap();
let commitment_outpoint = alice_process_cache.keys().next().unwrap();
debug!("Alice can validate the new state of the process");
let relevant_process = alice_process_cache.get(&commitment_outpoint).unwrap();
for diff in alice_diff_cache {
debug!("User validate diff: {:#?}", diff);
}
// Alice can also sign her response and send it to Bob
let validate_state_return = validate_state(commitment_outpoint.to_string(), new_state_id.clone()).unwrap();
let updated_process = validate_state_return.updated_process.unwrap();
alice_process_cache.insert(updated_process.commitment_tx, updated_process.current_process);
let alice_response = create_response_prd(updated_process.commitment_tx.to_string(), new_state_id.clone()).unwrap();
// ======================= Bob
reset_device().unwrap();
restore_device(bob_device).unwrap();
set_process_cache(serde_json::to_string(&bob_process_cache).unwrap()).unwrap();
set_shared_secrets(serde_json::to_string(&bob_secrets_store).unwrap()).unwrap();
for diff in &bob_diff_cache {
if diff.need_validation {
debug!("Pop-up: User confirmation");
debug!("{:#?}", diff);
}
}
// If user is ok, we can add our own validation token
// Get the whole commitment from the process
let bob_validated_process = validate_state(updated_process.commitment_tx.to_string(), new_state_id.clone()).unwrap();
let updated_process = bob_validated_process.updated_process.unwrap();
bob_process_cache.insert(updated_process.commitment_tx, updated_process.current_process);
let bob_response = create_response_prd(updated_process.commitment_tx.to_string(), new_state_id.clone()).unwrap();
let ciphers = bob_response.ciphers_to_send; // We would send it to Alice to let her know we agree
debug!("Bob pairs device with Alice");
let roles: HashMap<String, RoleDefinition> = serde_json::from_value(bob_diff_cache.iter().find(|diff| diff.field == "roles").unwrap().new_value.clone()).unwrap();
let owner = roles.get("owner").unwrap();
let members_to_pair: Vec<String> = owner.members.iter().flat_map(|m| m.get_addresses()).collect();
pair_device(updated_process.commitment_tx.to_string(), members_to_pair).unwrap();
// We can also check alice response
let parsed_alice_response = parse_cipher(alice_response.ciphers_to_send[0].clone()).unwrap();
}