320 lines
15 KiB
Rust
320 lines
15 KiB
Rust
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 sdk_common::sp_client::bitcoin::hex::FromHex;
|
||
use sdk_common::sp_client::bitcoin::OutPoint;
|
||
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 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(),
|
||
])
|
||
.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().new_state_merkle_root;
|
||
|
||
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_commitments_root(&new_state_id).unwrap();
|
||
|
||
let hash2values: Map<String, Value> = bob_diff_cache.iter()
|
||
.filter(|diff| diff.new_state_merkle_root == *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();
|
||
}
|