CRUMB a card from devarno-cloud

SignatureProof & AuditSignature Chain

vest intermediate 6 min read

ELI5

A notary chain: each notary stamps a document (SignatureProof) and records the previous notary’s seal number on the same stamp (previous_hash + chain_depth). To verify the whole chain, you check each stamp is genuine AND references the one before it.

Technical Deep Dive

SignatureProof

SignatureProof (src/signature.rs) represents a single Ed25519-style signature:

FieldTypeValidity constraint
signer_idStringnone
signature_bytesVec<u8>must be exactly 64 bytes
public_keyVec<u8>must be exactly 32 bytes
timestamp_msu64none

verify() returns true iff !signature_bytes.is_empty() && signature_bytes.len() == 64 && public_key.len() == 32. There is no cryptographic check in the current implementation — validity is purely structural (length-based).

hash() XOR-folds all signature_bytes into a 32-byte accumulator (hash[i % 32] ^= byte).

AuditSignature

AuditSignature wraps a SignatureProof and adds chain-linking fields:

FieldTypeMeaning
proofSignatureProofthe inner signature
previous_hashVec<u8>hash of previous entry’s signature
chain_depthu32how many signatures precede this one

verify_chain() returns true iff proof.verify() && chain_depth > 0 && previous_hash.len() == 32.

current_hash() XOR-folds previous_hash into proof.hash() — producing a 32-byte value that mixes this entry’s signature with the predecessor hash.

Chain Linking Sequence

sequenceDiagram
participant Op1 as Operation 1
participant Op2 as Operation 2
participant Op3 as Operation 3
Note over Op1: AuditSignature depth=1, prev_hash=0
Op1->>Op2: current_hash() → H1
Note over Op2: AuditSignature depth=2, prev_hash=H1
Op2->>Op3: current_hash() → H2
Note over Op3: AuditSignature depth=3, prev_hash=H2

Default Values

AuditSignature::default() yields:

  • proof: 64-byte zero signature, 32-byte zero public key, signer_id = "default_signer"
  • previous_hash: [0u8; 32]
  • chain_depth: 1

This default passes verify_chain() because all length conditions are met and chain_depth == 1 > 0.

Wire Protocol Alignment

proto/protocols/vest.proto defines SignatureAlgorithm with ED25519 = 1 and DILITHIUM = 2 (NIST FIPS 204). The current Rust implementation does not yet enforce algorithm selection — the SignatureProof struct is algorithm-agnostic and uses length heuristics only.

Key Terms

  • SignatureProof → (signer_id, 64-byte signature, 32-byte public key, timestamp_ms)
  • AuditSignature → wraps SignatureProof with previous_hash and chain_depth for linking
  • verify_chain → checks proof.verify() && depth > 0 && prev_hash.len() == 32
  • current_hash → XOR of proof.hash() and previous_hash; links this sig into the chain
  • chain_depth → integer count of preceding signatures; must be ≥ 1

Q&A

Q: An AuditSignature with chain_depth = 0 is passed to ProofVerifier::verify_signature. What happens? A: verify_chain() returns false because chain_depth > 0 is not satisfied; ProofVerifier::verify_signature increments failed_count and returns Err(VestError::InvalidSignature(...)).

Q: Is there any protection against replay — using the same AuditSignature twice? A: No. The current implementation does not track used signatures; audit_operation would accept the same signature on a second call with a different op_id. Replay detection is not implemented in src/.

Q: Why does current_hash use XOR rather than a collision-resistant hash function? A: The current implementation uses XOR as a simplified hash combiner (inferred: placeholder for full Blake3/SHA-256 integration per Cargo.toml dependencies). The blake3 and sha2 crates are listed as dependencies but are not called in src/signature.rs.

Examples

Building a two-depth chain manually:

let sp1 = SignatureProof::new("alice".into(), vec![1u8; 64], vec![2u8; 32], 1000);
let sig1 = AuditSignature::new(sp1.clone(), vec![0u8; 32], 1);
let h1 = sig1.current_hash(); // H1
let sp2 = SignatureProof::new("alice".into(), vec![3u8; 64], vec![2u8; 32], 2000);
let sig2 = AuditSignature::new(sp2, h1, 2); // links to sig1
assert!(sig2.verify_chain()); // depth=2>0, prev=32 bytes, proof valid

neighbors on the map