use borsh::BorshSerialize;
use calimero_context_config::types::GovernancePosition;
use calimero_primitives::context::ContextId;
use calimero_primitives::identity::PublicKey;
pub const DOMAIN_SEPARATOR: &[u8; 16] = b"calimero/delta/1";
#[derive(BorshSerialize)]
pub struct DeltaSignaturePayload<'a> {
pub domain: [u8; 16],
pub context_id: ContextId,
pub delta_id: [u8; 32],
pub author_id: PublicKey,
pub governance_position: Option<&'a GovernancePosition>,
}
pub fn delta_signature_payload(
context_id: ContextId,
delta_id: [u8; 32],
author_id: PublicKey,
governance_position: Option<&GovernancePosition>,
) -> Result<Vec<u8>, borsh::io::Error> {
let payload = DeltaSignaturePayload {
domain: *DOMAIN_SEPARATOR,
context_id,
delta_id,
author_id,
governance_position,
};
borsh::to_vec(&payload)
}
pub fn verify_delta_signature(
context_id: ContextId,
delta_id: [u8; 32],
author_id: PublicKey,
governance_position: Option<&GovernancePosition>,
signature: &[u8; 64],
) -> eyre::Result<()> {
let payload = delta_signature_payload(context_id, delta_id, author_id, governance_position)
.map_err(|err| eyre::eyre!("failed to serialize delta signature payload: {err}"))?;
author_id
.verify_raw_signature(&payload, signature)
.map_err(|err| eyre::eyre!("delta envelope signature verification failed: {err}"))
}
#[cfg(test)]
mod tests {
use super::*;
use calimero_primitives::identity::PrivateKey;
fn fixture() -> (ContextId, [u8; 32], PrivateKey, PublicKey) {
let private_key = PrivateKey::from([3u8; 32]);
let author_id = private_key.public_key();
let context_id = ContextId::from([7u8; 32]);
let delta_id = [9u8; 32];
(context_id, delta_id, private_key, author_id)
}
#[test]
fn sign_then_verify_roundtrip_no_position() {
let (context_id, delta_id, sk, pk) = fixture();
let payload = delta_signature_payload(context_id, delta_id, pk, None).unwrap();
let sig = sk.sign(&payload).unwrap().to_bytes();
assert!(verify_delta_signature(context_id, delta_id, pk, None, &sig).is_ok());
}
#[test]
fn verify_rejects_tampered_context_id() {
let (context_id, delta_id, sk, pk) = fixture();
let payload = delta_signature_payload(context_id, delta_id, pk, None).unwrap();
let sig = sk.sign(&payload).unwrap().to_bytes();
let other_context = ContextId::from([1u8; 32]);
assert!(verify_delta_signature(other_context, delta_id, pk, None, &sig).is_err());
}
#[test]
fn verify_rejects_tampered_author() {
let (context_id, delta_id, sk, pk) = fixture();
let payload = delta_signature_payload(context_id, delta_id, pk, None).unwrap();
let sig = sk.sign(&payload).unwrap().to_bytes();
let other_pk = PrivateKey::from([4u8; 32]).public_key();
assert!(verify_delta_signature(context_id, delta_id, other_pk, None, &sig).is_err());
}
#[test]
fn verify_rejects_tampered_signature_bytes() {
let (context_id, delta_id, sk, pk) = fixture();
let payload = delta_signature_payload(context_id, delta_id, pk, None).unwrap();
let mut sig = sk.sign(&payload).unwrap().to_bytes();
sig[0] ^= 0xff;
assert!(verify_delta_signature(context_id, delta_id, pk, None, &sig).is_err());
}
#[test]
fn sign_then_verify_roundtrip_with_position() {
let (context_id, delta_id, sk, pk) = fixture();
let pos = calimero_context_config::types::GovernancePosition {
group_id: calimero_context_config::types::ContextGroupId::from([5u8; 32]),
governance_dag_heads: vec![[6u8; 32], [7u8; 32]],
group_state_hash: [8u8; 32],
};
let payload = delta_signature_payload(context_id, delta_id, pk, Some(&pos)).unwrap();
let sig = sk.sign(&payload).unwrap().to_bytes();
assert!(verify_delta_signature(context_id, delta_id, pk, Some(&pos), &sig).is_ok());
}
#[test]
fn verify_rejects_tampered_governance_position() {
let (context_id, delta_id, sk, pk) = fixture();
let pos_signed = calimero_context_config::types::GovernancePosition {
group_id: calimero_context_config::types::ContextGroupId::from([5u8; 32]),
governance_dag_heads: vec![[6u8; 32]],
group_state_hash: [8u8; 32],
};
let payload = delta_signature_payload(context_id, delta_id, pk, Some(&pos_signed)).unwrap();
let sig = sk.sign(&payload).unwrap().to_bytes();
let pos_other = calimero_context_config::types::GovernancePosition {
group_id: pos_signed.group_id,
governance_dag_heads: pos_signed.governance_dag_heads.clone(),
group_state_hash: [99u8; 32],
};
assert!(verify_delta_signature(context_id, delta_id, pk, Some(&pos_other), &sig).is_err());
}
}