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, validate_state }; use sdk_common::crypto::AnkSharedSecretHash; use sdk_common::log::debug; use sdk_common::pcd::{Member, Pcd, Roles}; use sdk_common::serialization::OutPointMemberMap; use serde_wasm_bindgen; use sdk_common::secrets::SecretsStore; use serde_json::{json}; #[allow(dead_code)] use wasm_bindgen_test::*; mod utils; use utils::*; // Exécution Node par défaut (ne pas forcer navigateur) /// # 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 Bob’s 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(), ]); 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"); // Construire Pcd et Roles à partir du JSON let private_data: Pcd = TryInto::::try_into(pairing_init_state.clone()).unwrap(); let roles_value = pairing_init_state.get("roles").unwrap().clone(); let roles_map: Roles = serde_json::from_value(roles_value).unwrap(); let public_data: Pcd = Default::default(); let create_process_return = create_new_process(private_data, roles_map.clone(), public_data, RELAY_ADDRESS.to_owned(), 1, OutPointMemberMap(std::collections::HashMap::new())).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.process_id, updated_process.current_process.clone()); // Alice keeps track of the change she needs to validate let create_process_diffs = updated_process.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.current_process.clone(), new_state_id.clone(), OutPointMemberMap(std::collections::HashMap::new())).unwrap(); let updated_process = create_update_return.updated_process.unwrap(); alice_process_cache.insert(updated_process.process_id, updated_process.current_process.clone()); debug!("Alice pairs her device"); pair_device(updated_process.process_id.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(), OutPointMemberMap(std::collections::HashMap::new())).unwrap(); let updated_process = bob_parsed_return.updated_process.unwrap(); let parsed_prd_diffs = updated_process.diffs; bob_process_cache.insert(updated_process.process_id, updated_process.current_process.clone()); // 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.process_id).unwrap(); // Mise à jour factice sans nouveaux attributs (alignement API) let update_process_res = update_process(process.clone(), Pcd::default(), roles_map.clone(), Pcd::default(), OutPointMemberMap(std::collections::HashMap::new())).unwrap(); let updated_process = update_process_res.updated_process.unwrap(); let parsed_prd_diffs = updated_process.diffs; bob_process_cache.insert(updated_process.process_id, updated_process.current_process.clone()); bob_diff_cache.extend(parsed_prd_diffs); // We can also prune the old diffs from the cache // Prune step removed (structure diff ne porte pas la valeur claire) // this is only for testing, as we're playing both parts let bob_device = dump_device().unwrap(); // ======================= Alice reset_device().unwrap(); restore_device(serde_wasm_bindgen::to_value(&alice_device).unwrap()).unwrap(); set_process_cache(serde_wasm_bindgen::to_value(&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(relevant_process.clone(), new_state_id.clone(), OutPointMemberMap(std::collections::HashMap::new())).unwrap(); let updated_process = validate_state_return.updated_process.unwrap(); alice_process_cache.insert(updated_process.process_id, updated_process.current_process.clone()); let alice_response = create_response_prd(updated_process.current_process.clone(), new_state_id.clone(), OutPointMemberMap(std::collections::HashMap::new())).unwrap(); // ======================= Bob reset_device().unwrap(); restore_device(serde_wasm_bindgen::to_value(&bob_device).unwrap()).unwrap(); set_process_cache(serde_wasm_bindgen::to_value(&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.current_process.clone(), new_state_id.clone(), OutPointMemberMap(std::collections::HashMap::new())).unwrap(); let updated_process = bob_validated_process.updated_process.unwrap(); bob_process_cache.insert(updated_process.process_id, updated_process.current_process.clone()); let bob_response = create_response_prd(updated_process.current_process.clone(), new_state_id.clone(), OutPointMemberMap(std::collections::HashMap::new())).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.process_id.to_string(), vec![helper_get_bob_address()]).unwrap(); // We can also check alice response let _parsed_alice_response = parse_cipher(alice_response.ciphers_to_send[0].clone(), OutPointMemberMap(std::collections::HashMap::new())).unwrap(); }