use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::authorize::{Decision, DenyReason};
use crate::capability::{Ops, Scope};
use crate::identity::{verify_sec1, Did, Identity};
pub type AuditResult<T> = Result<T, AuditError>;
#[derive(Debug, Error)]
pub enum AuditError {
#[error("audit canonicalization: {0}")]
Canonical(String),
#[error("audit chain broken at seq {0}")]
BrokenChain(u64),
#[error("audit entry hash mismatch at seq {0}")]
HashMismatch(u64),
#[error("audit node signature invalid at seq {0}")]
BadSignature(u64),
#[error("audit sequence mismatch at index {0}")]
SeqMismatch(u64),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum AuditDecision {
Allowed,
Denied(DenyReason),
}
impl AuditDecision {
pub fn of(decision: &Decision) -> Self {
match decision {
Decision::Allow => AuditDecision::Allowed,
Decision::Deny(reason) => AuditDecision::Denied(reason.clone()),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuditEntry {
pub seq: u64,
pub prev_hash: [u8; 32],
pub at_unix: u64,
pub bearer: Did,
pub scope: Scope,
pub op: Ops,
pub capability_id: Option<[u8; 16]>,
pub decision: AuditDecision,
pub entry_hash: [u8; 32],
pub node_signature: Vec<u8>,
}
fn content_bytes(
seq: u64,
prev_hash: &[u8; 32],
at_unix: u64,
bearer: &Did,
scope: &Scope,
op: Ops,
capability_id: &Option<[u8; 16]>,
decision: &AuditDecision,
) -> AuditResult<Vec<u8>> {
postcard::to_allocvec(&(seq, prev_hash, at_unix, bearer, scope, op, capability_id, decision))
.map_err(|e| AuditError::Canonical(e.to_string()))
}
fn hash(bytes: &[u8]) -> [u8; 32] {
*blake3::hash(bytes).as_bytes()
}
#[derive(Clone, Debug, Default)]
pub struct AuditLog {
entries: Vec<AuditEntry>,
}
impl AuditLog {
pub fn new() -> Self {
Self::default()
}
pub fn from_entries(entries: Vec<AuditEntry>) -> Self {
Self { entries }
}
pub fn entries(&self) -> &[AuditEntry] {
&self.entries
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
#[allow(clippy::too_many_arguments)]
pub fn record(
&mut self,
node: &Identity,
at_unix: u64,
bearer: &Did,
scope: &Scope,
op: Ops,
capability_id: Option<[u8; 16]>,
decision: AuditDecision,
) -> AuditResult<()> {
let seq = self.entries.len() as u64;
let prev_hash = self
.entries
.last()
.map(|e| e.entry_hash)
.unwrap_or([0u8; 32]);
let canonical = content_bytes(
seq,
&prev_hash,
at_unix,
bearer,
scope,
op,
&capability_id,
&decision,
)?;
let entry_hash = hash(&canonical);
let node_signature = node.sign(&canonical);
self.entries.push(AuditEntry {
seq,
prev_hash,
at_unix,
bearer: bearer.clone(),
scope: scope.clone(),
op,
capability_id,
decision,
entry_hash,
node_signature,
});
Ok(())
}
pub fn verify(&self, node_public_key: &[u8]) -> AuditResult<()> {
let mut expected_prev = [0u8; 32];
for (index, entry) in self.entries.iter().enumerate() {
if entry.seq != index as u64 {
return Err(AuditError::SeqMismatch(index as u64));
}
if entry.prev_hash != expected_prev {
return Err(AuditError::BrokenChain(entry.seq));
}
let canonical = content_bytes(
entry.seq,
&entry.prev_hash,
entry.at_unix,
&entry.bearer,
&entry.scope,
entry.op,
&entry.capability_id,
&entry.decision,
)?;
if hash(&canonical) != entry.entry_hash {
return Err(AuditError::HashMismatch(entry.seq));
}
if !verify_sec1(node_public_key, &canonical, &entry.node_signature) {
return Err(AuditError::BadSignature(entry.seq));
}
expected_prev = entry.entry_hash;
}
Ok(())
}
}