diff --git a/crates/common/src/hashchain.rs b/crates/common/src/hashchain.rs index 1719eb26..2b588952 100644 --- a/crates/common/src/hashchain.rs +++ b/crates/common/src/hashchain.rs @@ -9,7 +9,8 @@ use std::{ use crate::{ keys::VerifyingKey, operation::{ - CreateAccountArgs, Operation, RegisterServiceArgs, ServiceChallenge, ServiceChallengeInput, + AddDataArgs, CreateAccountArgs, KeyOperationArgs, Operation, RegisterServiceArgs, + ServiceChallenge, ServiceChallengeInput, }, tree::{Digest, Hasher}, }; @@ -130,6 +131,7 @@ impl Hashchain { Operation::RevokeKey(args) => { valid_keys.remove(&args.value); } + Operation::AddData(_) => {} } } @@ -161,14 +163,29 @@ impl Hashchain { )?; } Operation::AddKey(args) | Operation::RevokeKey(args) => { - let signing_key = self.get_key_at_index(args.signature.key_idx as usize)?; - if !valid_keys.contains(signing_key) { - bail!("Last operation signed with revoked or non-existent key"); - } - signing_key.verify_signature( - &bincode::serialize(&last_entry.operation.without_signature())?, + self.verify_signature_at_key_idx( + &last_entry.operation, &args.signature.signature, + args.signature.key_idx, + &valid_keys, + )?; + } + Operation::AddData(args) => { + self.verify_signature_at_key_idx( + &last_entry.operation, + &args.op_signature.signature, + args.op_signature.key_idx, + &valid_keys, )?; + + let Some(value_signature) = &args.value_signature else { + return Ok(()); + }; + + // If data to be added is signed, also validate its signature + value_signature + .verifying_key + .verify_signature(&args.value, &value_signature.signature)?; } } @@ -193,7 +210,7 @@ impl Hashchain { for entry in self.entries.clone() { match &entry.operation { - Operation::RegisterService(_) => {} + Operation::RegisterService(_) | Operation::AddData(_) => {} Operation::CreateAccount(args) => { valid_keys.insert(args.value.clone()); } @@ -220,6 +237,26 @@ impl Hashchain { .unwrap_or(false) } + fn verify_signature_at_key_idx( + &self, + operation: &Operation, + signature: &[u8], + idx: usize, + valid_keys: &HashSet, + ) -> Result<()> { + let verifying_key = self.get_key_at_index(idx)?; + if !valid_keys.contains(verifying_key) { + bail!( + "Key intended to verify signature {:?} is not in valid keys {:?}", + verifying_key, + valid_keys + ); + } + + let message = bincode::serialize(&operation.without_signature())?; + verifying_key.verify_signature(&message, signature) + } + pub fn iter(&self) -> std::slice::Iter<'_, HashchainEntry> { self.entries.iter() } @@ -253,7 +290,7 @@ impl Hashchain { self.push(operation) } - /// Verifies the structure and signature of a new operation without checking if the key is revoked. + /// Verifies the structure and signature of a new operation fn validate_new_operation(&self, operation: &Operation) -> Result<()> { match operation { Operation::RegisterService(_) => { @@ -262,8 +299,13 @@ impl Hashchain { } Ok(()) } - Operation::AddKey(args) | Operation::RevokeKey(args) => { - let signing_key = self.get_key_at_index(args.signature.key_idx as usize)?; + Operation::AddKey(KeyOperationArgs { signature, .. }) + | Operation::RevokeKey(KeyOperationArgs { signature, .. }) + | Operation::AddData(AddDataArgs { + op_signature: signature, + .. + }) => { + let signing_key = self.get_key_at_index(signature.key_idx)?; if self.is_key_revoked(signing_key.clone()) { bail!("The signing key is revoked"); diff --git a/crates/common/src/operation.rs b/crates/common/src/operation.rs index b5201683..232a7e64 100644 --- a/crates/common/src/operation.rs +++ b/crates/common/src/operation.rs @@ -13,9 +13,11 @@ use crate::keys::{SigningKey, VerifyingKey}; pub enum Operation { /// Creates a new account with the given id and value. CreateAccount(CreateAccountArgs), - /// Adds a value to an existing account. + /// Adds a key to an existing account. AddKey(KeyOperationArgs), - /// Revokes a value from an existing account. + /// Adds arbitrary signed data to an existing account. + AddData(AddDataArgs), + /// Revokes a key from an existing account. RevokeKey(KeyOperationArgs), /// Registers a new service with the given id. RegisterService(RegisterServiceArgs), @@ -24,22 +26,31 @@ pub enum Operation { #[derive(Clone, Serialize, Deserialize, Default, Debug, PartialEq)] /// Represents a signature bundle, which includes the index of the key /// in the user's hashchain and the associated signature. -pub struct SignatureBundle { +pub struct HashchainSignatureBundle { /// Index of the key in the hashchain - pub key_idx: u64, + pub key_idx: usize, /// The actual signature pub signature: Vec, } -impl SignatureBundle { - pub fn empty_with_idx(idx: u64) -> Self { - SignatureBundle { +impl HashchainSignatureBundle { + pub fn empty_with_idx(idx: usize) -> Self { + HashchainSignatureBundle { key_idx: idx, signature: vec![], } } } +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] +/// Represents a signature including its. +pub struct SignatureBundle { + /// The key that can be used to verify the signature + pub verifying_key: VerifyingKey, + /// The actual signature + pub signature: Vec, +} + #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] /// Input required to complete a challenge for account creation. pub enum ServiceChallengeInput { @@ -81,6 +92,19 @@ impl From for ServiceChallenge { } } +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] +/// Structure for adding data. +pub struct AddDataArgs { + /// Account ID + pub id: String, + /// Data to be added + pub value: Vec, + /// Optional external signature used to sign the data to be added + pub value_signature: Option, + /// Signature to authorize the action + pub op_signature: HashchainSignatureBundle, +} + #[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] /// Common structure for operations involving keys (adding or revoking). pub struct KeyOperationArgs { @@ -89,7 +113,7 @@ pub struct KeyOperationArgs { /// Public key being added or revoked pub value: VerifyingKey, /// Signature to authorize the action - pub signature: SignatureBundle, + pub signature: HashchainSignatureBundle, } impl Operation { @@ -130,16 +154,16 @@ impl Operation { id: String, value: VerifyingKey, signing_key: &SigningKey, - key_idx: u64, + key_idx: usize, ) -> Result { let op_to_sign = Operation::AddKey(KeyOperationArgs { id: id.clone(), value: value.clone(), - signature: SignatureBundle::empty_with_idx(key_idx), + signature: HashchainSignatureBundle::empty_with_idx(key_idx), }); let message = bincode::serialize(&op_to_sign)?; - let signature = SignatureBundle { + let signature = HashchainSignatureBundle { key_idx, signature: signing_key.sign(&message).to_vec(), }; @@ -155,16 +179,16 @@ impl Operation { id: String, value: VerifyingKey, signing_key: &SigningKey, - key_idx: u64, + key_idx: usize, ) -> Result { let op_to_sign = Operation::RevokeKey(KeyOperationArgs { id: id.clone(), value: value.clone(), - signature: SignatureBundle::empty_with_idx(key_idx), + signature: HashchainSignatureBundle::empty_with_idx(key_idx), }); let message = bincode::serialize(&op_to_sign)?; - let signature = SignatureBundle { + let signature = HashchainSignatureBundle { key_idx, signature: signing_key.sign(&message).to_vec(), }; @@ -176,10 +200,39 @@ impl Operation { })) } + pub fn new_add_signed_data( + id: String, + value: Vec, + value_signature: Option, + signing_key: &SigningKey, + key_idx: usize, + ) -> Result { + let op_to_sign = Operation::AddData(AddDataArgs { + id: id.clone(), + value: value.clone(), + value_signature: value_signature.clone(), + op_signature: HashchainSignatureBundle::empty_with_idx(key_idx), + }); + + let message = { bincode::serialize(&op_to_sign)? }; + let op_signature = HashchainSignatureBundle { + key_idx, + signature: signing_key.sign(&message).to_vec(), + }; + + Ok(Operation::AddData(AddDataArgs { + id, + value, + value_signature, + op_signature, + })) + } + pub fn id(&self) -> String { match self { Operation::CreateAccount(args) => args.id.clone(), Operation::AddKey(args) | Operation::RevokeKey(args) => args.id.clone(), + Operation::AddData(args) => args.id.clone(), Operation::RegisterService(args) => args.id.clone(), } } @@ -188,15 +241,7 @@ impl Operation { match self { Operation::RevokeKey(args) | Operation::AddKey(args) => Some(&args.value), Operation::CreateAccount(args) => Some(&args.value), - Operation::RegisterService(_) => None, - } - } - - pub fn get_signature_bundle(&self) -> Option { - match self { - Operation::AddKey(args) => Some(args.signature.clone()), - Operation::RevokeKey(args) => Some(args.signature.clone()), - Operation::RegisterService(_) | Operation::CreateAccount(_) => None, + Operation::RegisterService(_) | Operation::AddData(_) => None, } } @@ -232,7 +277,7 @@ impl Operation { Operation::AddKey(args) => Operation::AddKey(KeyOperationArgs { id: args.id.clone(), value: args.value.clone(), - signature: SignatureBundle { + signature: HashchainSignatureBundle { key_idx: args.signature.key_idx, signature: Vec::new(), }, @@ -240,11 +285,20 @@ impl Operation { Operation::RevokeKey(args) => Operation::RevokeKey(KeyOperationArgs { id: args.id.clone(), value: args.value.clone(), - signature: SignatureBundle { + signature: HashchainSignatureBundle { key_idx: args.signature.key_idx, signature: Vec::new(), }, }), + Operation::AddData(args) => Operation::AddData(AddDataArgs { + id: args.id.clone(), + value: args.value.clone(), + value_signature: args.value_signature.clone(), + op_signature: HashchainSignatureBundle { + key_idx: args.op_signature.key_idx, + signature: Vec::new(), + }, + }), Operation::CreateAccount(args) => Operation::CreateAccount(CreateAccountArgs { id: args.id.clone(), value: args.value.clone(), @@ -272,13 +326,35 @@ impl Operation { .context("User signature failed")?; pubkey.verify_signature(&message, &args.signature.signature) } + Operation::AddData(args) => { + let message = bincode::serialize(&self.without_signature()) + .context("Serializing operation failed")?; + pubkey + .verify_signature(&message, &args.op_signature.signature) + .context("Verifying operation signature failed")?; + + let Some(value_signature) = &args.value_signature else { + return Ok(()); + }; + + // If data to be added is signed, also validate its signature + value_signature + .verifying_key + .verify_signature(&args.value, &value_signature.signature) + .context("Verifying value signature failed") + } } } pub fn validate(&self) -> Result<()> { match &self { Operation::AddKey(KeyOperationArgs { id, signature, .. }) - | Operation::RevokeKey(KeyOperationArgs { id, signature, .. }) => { + | Operation::RevokeKey(KeyOperationArgs { id, signature, .. }) + | Operation::AddData(AddDataArgs { + id, + op_signature: signature, + .. + }) => { if id.is_empty() { return Err( GeneralError::MissingArgumentError("id is empty".to_string()).into(), diff --git a/crates/common/src/test_utils.rs b/crates/common/src/test_utils.rs index 513e0851..626fe722 100644 --- a/crates/common/src/test_utils.rs +++ b/crates/common/src/test_utils.rs @@ -1,7 +1,7 @@ use crate::{ hashchain::Hashchain, keys::{SigningKey, VerifyingKey}, - operation::{Operation, ServiceChallenge}, + operation::{Operation, ServiceChallenge, SignatureBundle}, tree::{ HashchainResponse::*, InsertProof, KeyDirectoryTree, Proof, SnarkableTree, UpdateProof, }, @@ -123,6 +123,48 @@ impl TestTreeState { account.hashchain.perform_operation(op).unwrap(); Ok(()) } + + pub fn add_unsigned_data_to_account( + &mut self, + data: &[u8], + account: &mut TestAccount, + ) -> Result<()> { + self.add_data_to_account(data, account, None) + } + + pub fn add_signed_data_to_account( + &mut self, + data: &[u8], + account: &mut TestAccount, + ) -> Result<()> { + let random_signing_key = create_mock_signing_key(); + self.add_data_to_account(data, account, Some(&random_signing_key)) + } + + fn add_data_to_account( + &mut self, + data: &[u8], + account: &mut TestAccount, + signing_key: Option<&SigningKey>, + ) -> Result<()> { + let signature_bundle = signing_key.map(|sk| SignatureBundle { + verifying_key: sk.verifying_key(), + signature: sk.sign(data), + }); + + let op_signing_key = self.signing_keys.get(&account.hashchain.id).unwrap(); + + let op = Operation::new_add_signed_data( + account.hashchain.id.clone(), + data.to_vec(), + signature_bundle, + op_signing_key, + 0, + )?; + + account.hashchain.perform_operation(op).unwrap(); + Ok(()) + } } impl Default for TestTreeState { diff --git a/crates/common/src/tree.rs b/crates/common/src/tree.rs index 599c19fc..e6886b43 100644 --- a/crates/common/src/tree.rs +++ b/crates/common/src/tree.rs @@ -16,8 +16,8 @@ use std::{ use crate::{ hashchain::{Hashchain, HashchainEntry}, operation::{ - CreateAccountArgs, KeyOperationArgs, Operation, RegisterServiceArgs, ServiceChallenge, - ServiceChallengeInput, + AddDataArgs, CreateAccountArgs, KeyOperationArgs, Operation, RegisterServiceArgs, + ServiceChallenge, ServiceChallengeInput, }, }; @@ -387,7 +387,8 @@ where fn process_operation(&mut self, operation: &Operation) -> Result { match operation { Operation::AddKey(KeyOperationArgs { id, .. }) - | Operation::RevokeKey(KeyOperationArgs { id, .. }) => { + | Operation::RevokeKey(KeyOperationArgs { id, .. }) + | Operation::AddData(AddDataArgs { id, .. }) => { let hashed_id = Digest::hash(id); let key_hash = KeyHash::with::(hashed_id); @@ -721,11 +722,17 @@ mod tests { tree_state.insert_account(account1.clone()).unwrap(); tree_state.insert_account(account2.clone()).unwrap(); + // Do insert and update accounts using the correct key indices tree_state.add_key_to_account(&mut account1).unwrap(); - tree_state.add_key_to_account(&mut account2).unwrap(); - - // Update accounts using the correct key indices tree_state.update_account(account1.clone()).unwrap(); + + tree_state + .add_unsigned_data_to_account(b"unsigned", &mut account2) + .unwrap(); + tree_state.update_account(account2.clone()).unwrap(); + tree_state + .add_signed_data_to_account(b"signed", &mut account2) + .unwrap(); tree_state.update_account(account2.clone()).unwrap(); let get_result1 = tree_state.tree.get(account1.key_hash); diff --git a/crates/node_types/prover/src/prover/mod.rs b/crates/node_types/prover/src/prover/mod.rs index 2c64ba83..b6bf14c9 100644 --- a/crates/node_types/prover/src/prover/mod.rs +++ b/crates/node_types/prover/src/prover/mod.rs @@ -456,7 +456,7 @@ impl Prover { match incoming_operation { Operation::RegisterService(_) => (), Operation::CreateAccount(_) => (), - Operation::AddKey(_) | Operation::RevokeKey(_) => { + Operation::AddKey(_) | Operation::RevokeKey(_) | Operation::AddData(_) => { let hc_response = self.get_hashchain(&incoming_operation.id()).await?; let Found(mut hc, _) = hc_response else { diff --git a/elf/riscv32im-succinct-zkvm-elf b/elf/riscv32im-succinct-zkvm-elf index 373dd6a2..be80f892 100755 Binary files a/elf/riscv32im-succinct-zkvm-elf and b/elf/riscv32im-succinct-zkvm-elf differ