Skip to content

Commit

Permalink
feat(common) Allow adding signed data (#141)
Browse files Browse the repository at this point in the history
Introduces a new operation type, AddData, allowing users to add arbitrary data to accounts. The added data can optionally be signed as well.

* feat: Add operation to add signed data

* feat: Process signed data op in hashchain

* test: Method to add signed data to a TestAccount

* feat: tree delegates new op to hashchain

* refac: use usize for key indices

* feat: Added data op is always signed with key from chain

When solely relying on an external signature, this leaves prism vulnerable to replay attacks. As a result, ops to add data are always signed with a valid chain key and can optionally have an additional signature for the data to be added.

* feat: Process data op in prover

* chore: Incorporate new op code in zkvm elf
  • Loading branch information
jns-ps authored Oct 22, 2024
1 parent 4a03e7b commit fa6fd7b
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 45 deletions.
64 changes: 53 additions & 11 deletions crates/common/src/hashchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
};
Expand Down Expand Up @@ -130,6 +131,7 @@ impl Hashchain {
Operation::RevokeKey(args) => {
valid_keys.remove(&args.value);
}
Operation::AddData(_) => {}
}
}

Expand Down Expand Up @@ -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)?;
}
}

Expand All @@ -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());
}
Expand All @@ -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<VerifyingKey>,
) -> 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()
}
Expand Down Expand Up @@ -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(_) => {
Expand All @@ -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");
Expand Down
128 changes: 102 additions & 26 deletions crates/common/src/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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<u8>,
}

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<u8>,
}

#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
/// Input required to complete a challenge for account creation.
pub enum ServiceChallengeInput {
Expand Down Expand Up @@ -81,6 +92,19 @@ impl From<SigningKey> 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<u8>,
/// Optional external signature used to sign the data to be added
pub value_signature: Option<SignatureBundle>,
/// 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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -130,16 +154,16 @@ impl Operation {
id: String,
value: VerifyingKey,
signing_key: &SigningKey,
key_idx: u64,
key_idx: usize,
) -> Result<Self> {
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(),
};
Expand All @@ -155,16 +179,16 @@ impl Operation {
id: String,
value: VerifyingKey,
signing_key: &SigningKey,
key_idx: u64,
key_idx: usize,
) -> Result<Self> {
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(),
};
Expand All @@ -176,10 +200,39 @@ impl Operation {
}))
}

pub fn new_add_signed_data(
id: String,
value: Vec<u8>,
value_signature: Option<SignatureBundle>,
signing_key: &SigningKey,
key_idx: usize,
) -> Result<Self> {
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(),
}
}
Expand All @@ -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<SignatureBundle> {
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,
}
}

Expand Down Expand Up @@ -232,19 +277,28 @@ 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(),
},
}),
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(),
Expand Down Expand Up @@ -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(),
Expand Down
Loading

0 comments on commit fa6fd7b

Please sign in to comment.