sdk_client/tests/pairing.rs
2024-11-26 22:55:31 +01:00

338 lines
16 KiB
Rust
Raw 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, get_update_proposals, pair_device, parse_cipher, reset_device, restore_device, set_process_cache, set_shared_secrets, setup, validate_state
};
use sdk_common::crypto::AnkSharedSecretHash;
use sdk_common::log::debug;
use sdk_common::pcd::{Member, Pcd};
use sdk_common::sp_client::bitcoin::hex::DisplayHex;
use sdk_common::secrets::SecretsStore;
use sdk_common::sp_client::bitcoin::OutPoint;
use serde_json::{json, 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();
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
}
]
}
},
"session_privkey": initial_session_privkey,
"session_pubkey": initial_session_pubkey,
"key_parity": true, // This allows us to use a 32 bytes array in serialization
});
let create_process_return = create_new_process(pairing_init_state.to_string(), 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);
// We send the commit_msg to the relay we got the address from
// now we create prd update for this new process
debug!("Alice sends an update prd to Bob");
let root = <Value as Pcd>::create_merkle_tree(&commit_msg.pcd_commitment).unwrap().root().unwrap();
let create_update_return = create_update_message(updated_process.commitment_tx.to_string(), root.to_lower_hex_string()).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();
bob_process_cache.insert(updated_process.commitment_tx, updated_process.current_process);
let prd_confirm_cipher = bob_parsed_return.ciphers_to_send.iter().next().unwrap();
debug!("Bob sends a Confirm Prd to Alice");
// 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();
debug!("Alice receives the Confirm Prd");
let alice_parsed_confirm = parse_cipher(prd_confirm_cipher.clone()).unwrap();
debug!(
"Alice parsed Bob's Confirm Prd: {:#?}",
alice_parsed_confirm
);
// Alice simply shoots back the return value in the ws
let bob_received_pcd = alice_parsed_confirm.ciphers_to_send[0].clone();
let commit_msg = alice_parsed_confirm.commit_to_send.unwrap();
// Take the relevant state out of the process
let relevant_process = alice_process_cache.get(&OutPoint::from_str(&commit_msg.init_tx).unwrap()).unwrap();
let concurrent_states = relevant_process.get_latest_concurrent_states().unwrap();
let relevant_state = concurrent_states.into_iter().find(|s| s.pcd_commitment == commit_msg.pcd_commitment).unwrap();
let root = <Value as Pcd>::create_merkle_tree(&relevant_state.pcd_commitment).unwrap().root().unwrap();
// Alice can also sign her response and send it to Bob
let validate_state_return = validate_state(commit_msg.init_tx, root.to_lower_hex_string()).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(), root.to_lower_hex_string()).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();
debug!("Bob parses Alice's pcd");
let bob_parsed_pcd_return = parse_cipher(bob_received_pcd).unwrap();
let updated_process = bob_parsed_pcd_return.updated_process.unwrap();
// Here we would update our database
bob_process_cache.insert(
updated_process.commitment_tx,
updated_process.current_process
);
// At this point, user must validate the pairing proposal received from Alice
// We decrypt the content of the pcd so that we can display to user what matters
let alice_proposal = get_update_proposals(updated_process.commitment_tx.to_string()).unwrap().decrypted_pcds;
debug!("Alice proposal: {:#?}", alice_proposal);
let (pcd_commitment_root, proposal) = alice_proposal.iter().next().unwrap();
// debug!("proposal: {:#?}", proposal);
// get the roles from the proposal
let roles = proposal.extract_roles().unwrap();
// we check that the proposal contains only one member
assert!(roles.len() == 1);
assert!(roles["owner"].members.len() == 1);
// we get all the addresses of the members of the proposal
let proposal_members = roles
.iter()
.flat_map(|(_, members)| members.members.iter().flat_map(|m| m.get_addresses()))
.collect::<Vec<String>>();
// we can automatically check that a pairing member contains local device address + the one that sent the proposal
assert!(proposal_members.contains(&alice_address));
assert!(proposal_members.contains(&bob_address));
assert!(proposal_members.len() == 2); // no free riders
// We remove the local address, but maybe that's the responsibility of the Member type
let proposal_members = proposal_members
.into_iter()
.filter(|m| m != &bob_address)
.collect::<Vec<String>>();
debug!("proposal_members: {:?}", proposal_members);
// we can now show all the addresses to the user on device to prompt confirmation
debug!("Pop-up: User confirmation");
// 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(), pcd_commitment_root.to_string()).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(), pcd_commitment_root.to_string()).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");
pair_device(updated_process.commitment_tx.to_string(), proposal_members).unwrap();
// We can also check alice response
let parsed_alice_response = parse_cipher(alice_response.ciphers_to_send[0].clone()).unwrap();
debug!("parsed_alice_response: {:#?}", parsed_alice_response.updated_process.unwrap());
// Since we have enough validation we can send it directly to relay for commitment
// login();
}