use pamoja_core::{Error, Result};
use pamoja_security::{DeviceIdentity, PublicIdentity};
use crate::entry::{digest, Entry};
const GENESIS: [u8; 32] = [0u8; 32];
pub struct AuditLog {
identity: DeviceIdentity,
head: [u8; 32],
next_index: u64,
}
impl AuditLog {
pub fn new(identity: DeviceIdentity) -> Self {
Self {
identity,
head: GENESIS,
next_index: 0,
}
}
pub fn resume(identity: DeviceIdentity, last: &Entry) -> Self {
Self {
identity,
head: last.digest(),
next_index: last.index() + 1,
}
}
pub fn append(&mut self, payload: &[u8]) -> Entry {
let index = self.next_index;
let prev = self.head;
let digest = digest(index, &prev, payload);
let signature = self.identity.sign(&digest);
self.head = digest;
self.next_index += 1;
Entry::new(index, prev, signature, payload.to_vec())
}
}
pub struct Verifier {
public: PublicIdentity,
expected_index: u64,
expected_prev: [u8; 32],
}
impl Verifier {
pub fn new(public: PublicIdentity) -> Self {
Self {
public,
expected_index: 0,
expected_prev: GENESIS,
}
}
pub fn check(&mut self, entry: &Entry) -> Result<()> {
if entry.index() != self.expected_index {
return Err(Error::Auth("audit entry is out of sequence".into()));
}
if entry.previous() != self.expected_prev {
return Err(Error::Auth("audit chain is broken".into()));
}
let digest = entry.digest();
self.public.verify(&digest, entry.signature())?;
self.expected_index += 1;
self.expected_prev = digest;
Ok(())
}
}
pub fn verify_chain(public: &PublicIdentity, entries: &[Entry]) -> Result<()> {
let mut verifier = Verifier::new(*public);
for entry in entries {
verifier.check(entry)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn device() -> DeviceIdentity {
DeviceIdentity::from_seed(&[1u8; 32])
}
fn sample_log() -> (alloc::vec::Vec<Entry>, PublicIdentity) {
let signer = device();
let public = signer.public();
let mut log = AuditLog::new(signer);
let entries = alloc::vec![log.append(b"r0"), log.append(b"r1"), log.append(b"r2")];
(entries, public)
}
#[test]
fn a_genuine_chain_verifies() {
let (entries, public) = sample_log();
assert!(verify_chain(&public, &entries).is_ok());
}
#[test]
fn a_tampered_payload_is_detected() {
let (mut entries, public) = sample_log();
let mut bytes = entries[1].to_bytes();
*bytes.last_mut().expect("non-empty entry") ^= 0xff;
entries[1] = Entry::from_bytes(&bytes).expect("parse");
assert!(matches!(
verify_chain(&public, &entries),
Err(Error::Auth(_))
));
}
#[test]
fn a_reordered_chain_is_detected() {
let (mut entries, public) = sample_log();
entries.swap(1, 2);
assert!(matches!(
verify_chain(&public, &entries),
Err(Error::Auth(_))
));
}
#[test]
fn a_dropped_entry_is_detected() {
let (entries, public) = sample_log();
let gap = alloc::vec![entries[0].clone(), entries[2].clone()];
assert!(matches!(verify_chain(&public, &gap), Err(Error::Auth(_))));
}
#[test]
fn another_signer_does_not_verify() {
let (entries, _) = sample_log();
let stranger = DeviceIdentity::from_seed(&[2u8; 32]).public();
assert!(verify_chain(&stranger, &entries).is_err());
}
#[test]
fn an_empty_chain_is_trivially_valid() {
let public = device().public();
assert!(verify_chain(&public, &[]).is_ok());
}
#[test]
fn a_chain_that_does_not_start_at_the_beginning_is_rejected() {
let (entries, public) = sample_log();
assert!(matches!(
verify_chain(&public, &entries[1..]),
Err(Error::Auth(_))
));
}
#[test]
fn resume_continues_the_chain() {
let public = device().public();
let mut log = AuditLog::new(device());
let e0 = log.append(b"r0");
let e1 = log.append(b"r1");
let mut resumed = AuditLog::resume(device(), &e1);
let e2 = resumed.append(b"r2");
assert!(verify_chain(&public, &[e0, e1, e2]).is_ok());
}
}