From d02505b9106256775184828b19871ff90f84bd9b Mon Sep 17 00:00:00 2001 From: zoz <97761083+0xzoz@users.noreply.github.com> Date: Tue, 12 Mar 2024 14:37:18 -0700 Subject: [PATCH] [tools] support key rotation offer capability in libra-txs (#208) Co-authored-by: coin1111 --- .gitignore | 5 +- Cargo.lock | 1 + framework/releases/head.mrb | Bin 608917 -> 608938 bytes private-keys.yaml | 6 + public-keys.yaml | 7 + tools/txs/Cargo.toml | 1 + tools/txs/src/txs_cli_user.rs | 178 ++++++++++++++++-- tools/txs/tests/key_rotation.rs | 296 ++++++++++++++++++++++++++++++ validator-full-node-identity.yaml | 3 + validator-identity.yaml | 5 + 10 files changed, 488 insertions(+), 14 deletions(-) create mode 100644 private-keys.yaml create mode 100644 public-keys.yaml create mode 100644 tools/txs/tests/key_rotation.rs create mode 100644 validator-full-node-identity.yaml create mode 100644 validator-identity.yaml diff --git a/.gitignore b/.gitignore index 4201828b0..1718a13a0 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,7 @@ sccache.log .idea # formal verification files -*.bpl \ No newline at end of file +*.bpl + +# exclude diem dependency +diem/ diff --git a/Cargo.lock b/Cargo.lock index f93a905a7..6866a84d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5797,6 +5797,7 @@ dependencies = [ "libra-smoke-tests", "libra-types", "libra-wallet", + "serde 1.0.163", "serde_json", "serde_yaml 0.8.26", "smoke-test", diff --git a/framework/releases/head.mrb b/framework/releases/head.mrb index b90b21b17173d3128c3d6b67cf4ae711a335a675..123bb05f976994e95dfcbee688d3535fa107bac4 100644 GIT binary patch delta 144 zcmbO_Rdv-=)rJF7M2#)7Pc1l7LF~P&pj3G^fU5vQ}vVcGxH4f3ySiyQj<%H iAza;@%%q~k>5N{Sa*7TFm05amDkwS;R95W8sRRHAe=&go delta 123 zcmZ2ARdwoA)rJF7M2#)7Pc1l7LF~P&poBg^)vEwQ}wIztMqd+lZp~`(~1&v fQ_J& { + match offer_rotation_capability.run(sender).await { + Ok(_) => println!("SUCCESS: offered rotation capability"), + Err(e) => { + println!("ERROR: could not offer rotation capability, message: {}", e); + } + } + } } Ok(()) @@ -61,7 +72,12 @@ impl SetSlowTx { pub struct RotateKeyTx { #[clap(short, long)] /// The new authkey to be used - new_private_key: Option, // Dev NOTE: account address has the same bytes as AuthKey + pub new_private_key: Option, // Dev NOTE: account address has the same bytes as AuthKey + #[clap(short, long)] + /// Account address for which rotation is done. It + /// can be different from caller's address if rotation capability has been granted + /// to the caller. Do not specify this if you want to rotate your own key. + pub account_address: Option, } impl RotateKeyTx { @@ -76,13 +92,30 @@ impl RotateKeyTx { }; let seq = sender.client().get_sequence_number(user_account).await?; - let payload = rotate_key( - user_account, - sender.local_account.private_key().to_owned(), - sender.local_account.authentication_key(), - seq, - new_private_key, - )?; + let payload = if let Some(account_address) = &self.account_address { + let target_account_address = AccountAddress::from_str(account_address)?; + let target_account = sender + .client() + .get_account(target_account_address) + .await? + .into_inner(); + // rotate key for account_address + rotate_key_delegated( + seq, + &target_account_address, // account for which rotation is carried + &target_account.authentication_key, // auth key for an account for which rotation is carried + &new_private_key, + ) + } else { + // rotate key for self + rotate_key( + user_account, + sender.local_account.private_key().to_owned(), + sender.local_account.authentication_key(), + seq, + new_private_key, + ) + }?; sender.sign_submit_wait(payload).await?; Ok(()) @@ -97,7 +130,7 @@ pub fn rotate_key( sequence_number: u64, new_private_key: Ed25519PrivateKey, ) -> anyhow::Result { - // form a rotation proof challence. See account.move + // form a rotation proof challenge. See account.move let rotation_proof = RotationProofChallenge { account_address: CORE_CODE_ADDRESS, module_name: "account".to_string(), @@ -132,3 +165,122 @@ pub fn rotate_key( Ok(payload) } + +/// Create the TransactionPayload for a delegated key transaction using rotation capability +pub fn rotate_key_delegated( + sequence_number: u64, + target_account_address: &AccountAddress, // account for which rotation is carried + target_auth_key: &AuthenticationKey, // auth key for an account for which rotation is carried + new_private_key: &Ed25519PrivateKey, +) -> anyhow::Result { + let new_public_key = Ed25519PublicKey::from(new_private_key); + let rotation_proof = RotationProofChallenge { + account_address: CORE_CODE_ADDRESS, + module_name: String::from("account"), + struct_name: String::from("RotationProofChallenge"), + sequence_number, + originator: *target_account_address, + current_auth_key: AccountAddress::from_bytes(target_auth_key)?, + new_public_key: new_public_key.to_bytes().to_vec(), + }; + + let rotation_msg = bcs::to_bytes(&rotation_proof)?; + + // Signs the struct using the next private key + let rotation_proof_signed_by_new_private_key = + new_private_key.sign_arbitrary_message(&rotation_msg); + + let payload = libra_stdlib::account_rotate_authentication_key_with_rotation_capability( + *target_account_address, + 0, + new_public_key.to_bytes().to_vec(), + rotation_proof_signed_by_new_private_key.to_bytes().to_vec(), + ); + + Ok(payload) +} + +#[derive(Serialize, Deserialize)] +pub struct RotationCapabilityOfferProofChallengeV2 { + account_address: AccountAddress, + module_name: String, + struct_name: String, + chain_id: u8, + sequence_number: u64, + source_address: AccountAddress, + recipient_address: AccountAddress, +} + +/// Offer rotation capability to a delegate address. +/// A delegate address now can rotate a key for this account owner +#[derive(clap::Args)] +pub struct RotationCapabilityTx { + #[clap(short, long)] + pub action: String, + + #[clap(short, long)] + pub delegate_address: String, +} +impl RotationCapabilityTx { + pub async fn run(&self, sender: &mut Sender) -> anyhow::Result<()> { + let is_offer = match self.action.to_lowercase().as_str() { + "offer" => true, + "revoke" => false, + _ => return Err(anyhow::anyhow!("Invalid action, allowed: offer, revoke")), + }; + let user_account: AccountAddress = sender.local_account.address(); + let index_response = sender.client().get_index().await?; + let chain_id = index_response.into_inner().chain_id; + + let recipient_address = AccountAddress::from_str(&self.delegate_address)?; + let seq = sender.client().get_sequence_number(user_account).await?; + let payload = if is_offer { + offer_rotation_capability_v2(&sender.local_account, recipient_address, chain_id, seq) + } else { + revoke_rotation_capability(recipient_address) + }?; + + sender.sign_submit_wait(payload).await?; + Ok(()) + } +} + +pub fn offer_rotation_capability_v2( + offerer_account: &LocalAccount, + delegate_account: AccountAddress, + chain_id: u8, + sequence_number: u64, +) -> anyhow::Result { + let rotation_capability_offer_proof = RotationCapabilityOfferProofChallengeV2 { + account_address: CORE_CODE_ADDRESS, + module_name: String::from("account"), + struct_name: String::from("RotationCapabilityOfferProofChallengeV2"), + chain_id, + sequence_number, + source_address: offerer_account.address(), + recipient_address: delegate_account, + }; + + let rotation_capability_proof_msg = bcs::to_bytes(&rotation_capability_offer_proof); + let rotation_proof_signed = offerer_account + .private_key() + .clone() + .sign_arbitrary_message(&rotation_capability_proof_msg.unwrap()); + + let payload = libra_stdlib::account_offer_rotation_capability( + rotation_proof_signed.to_bytes().to_vec(), + 0, + offerer_account.public_key().to_bytes().to_vec(), + delegate_account, + ); + + Ok(payload) +} + +pub fn revoke_rotation_capability( + delegate_account: AccountAddress, +) -> anyhow::Result { + let payload = libra_stdlib::account_revoke_rotation_capability(delegate_account); + + Ok(payload) +} diff --git a/tools/txs/tests/key_rotation.rs b/tools/txs/tests/key_rotation.rs new file mode 100644 index 000000000..1028431e1 --- /dev/null +++ b/tools/txs/tests/key_rotation.rs @@ -0,0 +1,296 @@ +use diem_sdk::crypto::ed25519::Ed25519PrivateKey; +use diem_sdk::crypto::{Uniform, ValidCryptoMaterialStringExt}; +use libra_smoke_tests::libra_smoke::LibraSmoke; +use libra_txs::submit_transaction::Sender; +use libra_txs::txs_cli_user::{RotateKeyTx, RotationCapabilityTx}; +use libra_types::legacy_types::app_cfg::Profile; +use libra_wallet::account_keys; + +// Scenario: We have an initial validator, Val 0 with a random address +// create an account for Alice (with a known address and mnemonic) +// rotate key for Alice's account using cli + +/// Test key rotation +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn rotate_key() -> anyhow::Result<()> { + // create libra swarm and get app config for the first validator + let mut ls = LibraSmoke::new(Some(1)) + .await + .expect("could not start libra smoke"); + let mut val_app_cfg = ls.first_account_app_cfg()?; + + // get an appcfg struct from Alice's mnemonic + let alice = account_keys::get_keys_from_mnem("talent sunset lizard pill fame nuclear spy noodle basket okay critic grow sleep legend hurry pitch blanket clerk impose rough degree sock insane purse".to_owned())?; + let alice_acct = &alice.child_0_owner.account; + + // create an account for alice by transferring funds + let mut s = Sender::from_app_cfg(&val_app_cfg, None).await?; + let res = s + .transfer(alice.child_0_owner.account, 100.0, false) + .await? + .unwrap(); + assert!(res.info.status().is_success()); + println!( + "alice: {:?} auth: {:?} pri: {:?}", + alice.child_0_owner.account, + alice.child_0_owner.auth_key.to_string(), + alice.child_0_owner.pri_key.to_string(), + ); + + let mut p = Profile::new(alice.child_0_owner.auth_key, alice.child_0_owner.account); + assert!(alice_acct == &p.account); + + p.set_private_key(&alice.child_0_owner.pri_key); + + val_app_cfg.maybe_add_profile(p)?; + + // also checking we can get a Sender type with a second profile + let mut alice_sender = + Sender::from_app_cfg(&val_app_cfg, Some(alice.child_0_owner.account.to_string())).await?; + + assert_eq!(alice_acct, &alice_sender.local_account.address()); + + let original_auth_key = alice.child_0_owner.auth_key.to_string(); + println!("original_auth_key: {:?}", original_auth_key); + + // check auth key from chain + let account_query = ls.client().get_account(alice.child_0_owner.account).await; + assert!(account_query.is_ok()); + let account_from_chain = account_query.unwrap().into_inner(); + let auth_key_from_chain = account_from_chain.authentication_key.to_string(); + assert_eq!(auth_key_from_chain, original_auth_key); + + // generate new private key from which auth key will be generated + let generated_private_key = Ed25519PrivateKey::generate_for_testing(); + let generated_private_key_encoded = + Ed25519PrivateKey::to_encoded_string(&generated_private_key); + assert!(generated_private_key_encoded.is_ok()); + + let cli = RotateKeyTx { + new_private_key: Some(generated_private_key_encoded.unwrap()), + account_address: None, + }; + + let res = cli.run(&mut alice_sender).await; + assert!(res.is_ok()); + + // check new auth key + let updated_account_query = ls.client().get_account(alice.child_0_owner.account).await; + assert!(updated_account_query.is_ok()); + let account_updated = updated_account_query.unwrap().into_inner(); + let new_auth_key = account_updated.authentication_key.to_string(); + println!("new_auth_key: {:?}", new_auth_key); + + assert_ne!(new_auth_key, original_auth_key); + + Ok(()) +} + +/// Test rotation capability offer and delegated key rotation +/// see rotate_auth_key_with_rotation_capability_e2e() as an example +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn offer_rotation_capability() -> anyhow::Result<()> { + // create libra swarm and get app config for the first validator + let mut ls = LibraSmoke::new(Some(1)) + .await + .expect("could not start libra smoke"); + let mut val_app_cfg = ls.first_account_app_cfg()?; + + // get an appcfg struct from Alice's mnemonic + let alice = account_keys::get_keys_from_mnem("talent sunset lizard pill fame nuclear spy noodle basket okay critic grow sleep legend hurry pitch blanket clerk impose rough degree sock insane purse".to_owned())?; + let alice_acct = &alice.child_0_owner.account; + + // create an account for alice by transferring funds + let mut s = Sender::from_app_cfg(&val_app_cfg, None).await?; + let res = s + .transfer(alice.child_0_owner.account, 100.0, false) + .await? + .unwrap(); + assert!(res.info.status().is_success()); + println!( + "alice: {:?} auth: {:?} pri: {:?}", + alice.child_0_owner.account, + alice.child_0_owner.auth_key.to_string(), + alice.child_0_owner.pri_key.to_string(), + ); + + let mut p = Profile::new(alice.child_0_owner.auth_key, alice.child_0_owner.account); + assert_eq!(alice_acct, &p.account); + + p.set_private_key(&alice.child_0_owner.pri_key); + + val_app_cfg.maybe_add_profile(p)?; + + // also checking we can get a Sender type with a second profile + let mut alice_sender = + Sender::from_app_cfg(&val_app_cfg, Some(alice.child_0_owner.account.to_string())).await?; + + assert_eq!(alice_acct, &alice_sender.local_account.address()); + + let original_auth_key = alice.child_0_owner.auth_key.to_string(); + println!("original_auth_key: {:?}", original_auth_key); + + // create a new account by transferring funds + let bob_account = ls.marlon_rando(); + let res_bob = s + .transfer(bob_account.address(), 100.0, false) + .await? + .unwrap(); + assert!(res_bob.info.status().is_success()); + + let mut bob_sender = + Sender::from_app_cfg(&val_app_cfg, Some(bob_account.address().to_string())).await?; + + // allow bob to rotate keys for alice + let cli = RotationCapabilityTx { + delegate_address: bob_sender.local_account.address().to_string(), + action: "offer".to_string(), + }; + + let res = cli.run(&mut alice_sender).await; + match res.as_ref() { + Ok(_) => {} + Err(err) => { + println!("got error={:?}", err) + } + } + assert!(res.is_ok()); + + // now bob can rotate alice's key + let generated_private_key = Ed25519PrivateKey::generate_for_testing(); + let generated_private_key_encoded = + Ed25519PrivateKey::to_encoded_string(&generated_private_key); + assert!(generated_private_key_encoded.is_ok()); + + let cli = RotateKeyTx { + new_private_key: Some(generated_private_key_encoded.unwrap()), + account_address: Some(alice_acct.to_string()), + }; + + let res_rotation = cli.run(&mut bob_sender).await; + match res_rotation.as_ref() { + Ok(_) => {} + Err(err) => { + println!("got error={:?}", err) + } + } + assert!(res_rotation.is_ok()); + + // alice got new auth key + let updated_account_query = ls.client().get_account(alice.child_0_owner.account).await; + assert!(updated_account_query.is_ok()); + let account_updated = updated_account_query.unwrap().into_inner(); + let new_auth_key = account_updated.authentication_key.to_string(); + println!("new_auth_key: {:?}", new_auth_key); + + assert_ne!(new_auth_key, original_auth_key); + + Ok(()) +} + +/// Test rotation capability revoke and failed delegated key rotation +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn revoke_rotation_capability() -> anyhow::Result<()> { + // create libra swarm and get app config for the first validator + let mut ls = LibraSmoke::new(Some(1)) + .await + .expect("could not start libra smoke"); + let mut val_app_cfg = ls.first_account_app_cfg()?; + + // get an appcfg struct from Alice's mnemonic + let alice = account_keys::get_keys_from_mnem("talent sunset lizard pill fame nuclear spy noodle basket okay critic grow sleep legend hurry pitch blanket clerk impose rough degree sock insane purse".to_owned())?; + let alice_acct = &alice.child_0_owner.account; + + // create an account for alice by transferring funds + let mut s = Sender::from_app_cfg(&val_app_cfg, None).await?; + let res = s + .transfer(alice.child_0_owner.account, 100.0, false) + .await? + .unwrap(); + assert!(res.info.status().is_success()); + println!( + "alice: {:?} auth: {:?} pri: {:?}", + alice.child_0_owner.account, + alice.child_0_owner.auth_key.to_string(), + alice.child_0_owner.pri_key.to_string(), + ); + + let mut p = Profile::new(alice.child_0_owner.auth_key, alice.child_0_owner.account); + assert_eq!(alice_acct, &p.account); + + p.set_private_key(&alice.child_0_owner.pri_key); + + val_app_cfg.maybe_add_profile(p)?; + + // also checking we can get a Sender type with a second profile + let mut alice_sender = + Sender::from_app_cfg(&val_app_cfg, Some(alice.child_0_owner.account.to_string())).await?; + + assert_eq!(alice_acct, &alice_sender.local_account.address()); + + let original_auth_key = alice.child_0_owner.auth_key.to_string(); + println!("original_auth_key: {:?}", original_auth_key); + + // create a new account by transferring funds + let bob_account = ls.marlon_rando(); + let res_bob = s + .transfer(bob_account.address(), 100.0, false) + .await? + .unwrap(); + assert!(res_bob.info.status().is_success()); + + let mut bob_sender = + Sender::from_app_cfg(&val_app_cfg, Some(bob_account.address().to_string())).await?; + + // allow bob to rotate keys for alice + let cli = RotationCapabilityTx { + delegate_address: bob_sender.local_account.address().to_string(), + action: "offer".to_string(), + }; + + let res = cli.run(&mut alice_sender).await; + match res.as_ref() { + Ok(_) => {} + Err(err) => { + println!("got error={:?}", err) + } + } + assert!(res.is_ok()); + + // revoke rotation capability from bob + let cli = RotationCapabilityTx { + delegate_address: bob_sender.local_account.address().to_string(), + action: "revoke".to_string(), + }; + + let res = cli.run(&mut alice_sender).await; + match res.as_ref() { + Ok(_) => {} + Err(err) => { + println!("got error={:?}", err) + } + } + assert!(res.is_ok()); + + // now bob cannot rotate alice's key + let generated_private_key = Ed25519PrivateKey::generate_for_testing(); + let generated_private_key_encoded = + Ed25519PrivateKey::to_encoded_string(&generated_private_key); + assert!(generated_private_key_encoded.is_ok()); + + let cli = RotateKeyTx { + new_private_key: Some(generated_private_key_encoded.unwrap()), + account_address: Some(alice_acct.to_string()), + }; + + let res_rotation = cli.run(&mut bob_sender).await; + match res_rotation.as_ref() { + Ok(_) => {} + Err(err) => { + println!("got error={:?}", err) + } + } + assert!(res_rotation.is_err()); + + Ok(()) +} diff --git a/validator-full-node-identity.yaml b/validator-full-node-identity.yaml new file mode 100644 index 000000000..1a6fa8ac2 --- /dev/null +++ b/validator-full-node-identity.yaml @@ -0,0 +1,3 @@ +--- +account_address: e8f084ba20264e3baea8578467f15bc3f534144709adff8adf02d9726eb5ae69 +network_private_key: "0x90bc05b0f7155e4ee4994acc4edc415ea80ebf0389ec2cd0eaf956aa6560b176" diff --git a/validator-identity.yaml b/validator-identity.yaml new file mode 100644 index 000000000..22db3133c --- /dev/null +++ b/validator-identity.yaml @@ -0,0 +1,5 @@ +--- +account_address: 058e4b7d6cf1b9cb2b865f76b17073d7da8c6a1b78ea51f33e451420232831d4 +account_private_key: "0xbf86f7c31ea8959a9969d8f6c29da1ac3253da2434d3aeb08bd1819ad3509b1d" +consensus_private_key: "0x4c80411f31b8cc4429a1a713e488e28a43667e19a25dbca5acf288e3fbfa31b4" +network_private_key: "0x2885971d054a05d74947efe3716c7b36ee16b77988445358464e96d1b167394d"