use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::error::{Error, Result};
use crate::identity::{AgentIdentity, SpiffeId};
pub const AUDIT_EVENT_VERSION: u8 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditEvent {
pub version: u8,
pub timestamp: DateTime<Utc>,
pub agent_id: SpiffeId,
pub action: String,
pub details_json: String,
pub chain_hash: String,
}
impl AuditEvent {
pub fn details(&self) -> serde_json::Value {
serde_json::from_str(&self.details_json).unwrap_or(serde_json::Value::Null)
}
}
impl AuditEvent {
pub fn new(
agent_id: SpiffeId,
action: impl Into<String>,
details: serde_json::Value,
previous_event_hash: Option<String>,
) -> Self {
let details_json = serde_json::to_string(&details).unwrap_or_else(|_| "null".to_string());
AuditEvent {
version: AUDIT_EVENT_VERSION,
timestamp: Utc::now(),
agent_id,
action: action.into(),
details_json,
chain_hash: previous_event_hash.unwrap_or_default(),
}
}
pub fn sign(self, identity: &AgentIdentity) -> Result<SignedAuditEvent> {
let event_bytes = bincode::serialize(&self)
.map_err(|e| Error::Serialization(format!("audit event serialize: {e}")))?;
let signature = identity.sign(&event_bytes)?;
Ok(SignedAuditEvent {
event: self,
signature,
})
}
pub fn hash_hex(&self) -> Result<String> {
let bytes = bincode::serialize(self)
.map_err(|e| Error::Serialization(format!("audit event hash serialize: {e}")))?;
Ok(hex::encode(Sha256::digest(&bytes)))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedAuditEvent {
pub event: AuditEvent,
pub signature: Vec<u8>,
}
impl SignedAuditEvent {
pub fn verify(&self, verifying_key_bytes: &[u8]) -> Result<bool> {
let event_bytes = bincode::serialize(&self.event)
.map_err(|e| Error::Serialization(format!("audit event serialize: {e}")))?;
let vk = lupine::sign::HybridVerifyingKey65::from_bytes(verifying_key_bytes)?;
match lupine::easy::verify(&vk, &event_bytes, &self.signature) {
Ok(valid) => Ok(valid),
Err(_) => Ok(false),
}
}
pub fn hash_hex(&self) -> Result<String> {
let bytes = bincode::serialize(self)
.map_err(|e| Error::Serialization(format!("signed event hash serialize: {e}")))?;
Ok(hex::encode(Sha256::digest(&bytes)))
}
pub fn to_bytes(&self) -> Result<Vec<u8>> {
bincode::serialize(self)
.map_err(|e| Error::Serialization(format!("signed event serialize: {e}")))
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
bincode::deserialize(bytes)
.map_err(|e| Error::Serialization(format!("signed event deserialize: {e}")))
}
}
pub fn verify_audit_chain(
events: &[SignedAuditEvent],
verifying_keys: &[Vec<u8>],
) -> Result<()> {
if events.len() != verifying_keys.len() {
return Err(Error::AuditError(
"events and verifying_keys slices must have the same length".to_string(),
));
}
let mut prev_hash: Option<String> = None;
for (i, (event, vk_bytes)) in events.iter().zip(verifying_keys.iter()).enumerate() {
let valid = event.verify(vk_bytes)?;
if !valid {
return Err(Error::AuditError(format!(
"signature invalid for event at index {i}"
)));
}
match &prev_hash {
None => {
if !event.event.chain_hash.is_empty() {
return Err(Error::AuditError(
"first event must have empty chain_hash".to_string(),
));
}
}
Some(expected) => {
if &event.event.chain_hash != expected {
return Err(Error::AuditError(format!(
"chain_hash mismatch at index {i}"
)));
}
}
}
prev_hash = Some(event.hash_hex()?);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::identity::AgentIdentity;
use serde_json::json;
fn with_large_stack<F: FnOnce() + Send + 'static>(f: F) {
std::thread::Builder::new()
.stack_size(32 * 1024 * 1024)
.spawn(f)
.expect("thread spawn failed")
.join()
.expect("thread panicked");
}
#[test]
fn audit_event_new_fields() {
with_large_stack(|| {
let id = SpiffeId::new("example.com", "agent/test").unwrap();
let ev = AuditEvent::new(
id.clone(),
"delegation.issued",
json!({"target": "worker/1"}),
None,
);
assert_eq!(ev.version, AUDIT_EVENT_VERSION);
assert_eq!(ev.action, "delegation.issued");
assert_eq!(ev.chain_hash, "");
assert_eq!(ev.agent_id, id);
});
}
#[test]
fn audit_event_with_chain_hash() {
with_large_stack(|| {
let id = SpiffeId::new("example.com", "agent/test").unwrap();
let ev = AuditEvent::new(
id,
"key.rotated",
json!({}),
Some("deadbeef".to_string()),
);
assert_eq!(ev.chain_hash, "deadbeef");
});
}
#[test]
fn audit_event_sign_and_verify() {
with_large_stack(|| {
let identity = AgentIdentity::new("example.com", "agent/test").unwrap();
let vk_bytes = identity.credential().verifying_key_bytes.clone();
let ev = AuditEvent::new(
identity.spiffe_id().clone(),
"test.action",
json!({"key": "value"}),
None,
);
let signed = ev.sign(&identity).unwrap();
assert!(signed.verify(&vk_bytes).unwrap());
});
}
#[test]
fn audit_event_verify_wrong_key_fails() {
with_large_stack(|| {
let signer = AgentIdentity::new("example.com", "agent/signer").unwrap();
let other = AgentIdentity::new("example.com", "agent/other").unwrap();
let vk_bytes = other.credential().verifying_key_bytes.clone();
let ev = AuditEvent::new(
signer.spiffe_id().clone(),
"test.action",
json!({}),
None,
);
let signed = ev.sign(&signer).unwrap();
assert!(!signed.verify(&vk_bytes).unwrap());
});
}
#[test]
fn audit_event_verify_tampered_signature_fails() {
with_large_stack(|| {
let identity = AgentIdentity::new("example.com", "agent/test").unwrap();
let vk_bytes = identity.credential().verifying_key_bytes.clone();
let ev = AuditEvent::new(
identity.spiffe_id().clone(),
"test.action",
json!({}),
None,
);
let mut signed = ev.sign(&identity).unwrap();
signed.signature[0] ^= 0xFF;
assert!(!signed.verify(&vk_bytes).unwrap());
});
}
#[test]
fn signed_event_serialize_roundtrip() {
with_large_stack(|| {
let identity = AgentIdentity::new("example.com", "agent/test").unwrap();
let vk_bytes = identity.credential().verifying_key_bytes.clone();
let ev = AuditEvent::new(
identity.spiffe_id().clone(),
"test.action",
json!({"round": "trip"}),
None,
);
let signed = ev.sign(&identity).unwrap();
let bytes = signed.to_bytes().unwrap();
let signed2 = SignedAuditEvent::from_bytes(&bytes).unwrap();
assert_eq!(signed.event.action, signed2.event.action);
assert!(signed2.verify(&vk_bytes).unwrap());
});
}
#[test]
fn audit_event_hash_hex_is_64_chars() {
with_large_stack(|| {
let id = SpiffeId::new("example.com", "agent/test").unwrap();
let ev = AuditEvent::new(id, "test.action", json!({}), None);
let h = ev.hash_hex().unwrap();
assert_eq!(h.len(), 64); });
}
#[test]
fn signed_event_hash_hex_is_64_chars() {
with_large_stack(|| {
let identity = AgentIdentity::new("example.com", "agent/test").unwrap();
let ev = AuditEvent::new(
identity.spiffe_id().clone(),
"test.action",
json!({}),
None,
);
let signed = ev.sign(&identity).unwrap();
let h = signed.hash_hex().unwrap();
assert_eq!(h.len(), 64);
});
}
#[test]
fn audit_chain_verify_valid_chain() {
with_large_stack(|| {
let identity = AgentIdentity::new("example.com", "agent/test").unwrap();
let vk_bytes = identity.credential().verifying_key_bytes.clone();
let ev1 = AuditEvent::new(
identity.spiffe_id().clone(),
"action.one",
json!({}),
None,
);
let signed1 = ev1.sign(&identity).unwrap();
let hash1 = signed1.hash_hex().unwrap();
let ev2 = AuditEvent::new(
identity.spiffe_id().clone(),
"action.two",
json!({}),
Some(hash1),
);
let signed2 = ev2.sign(&identity).unwrap();
verify_audit_chain(
&[signed1, signed2],
&[vk_bytes.clone(), vk_bytes],
)
.unwrap();
});
}
#[test]
fn audit_chain_verify_tampered_chain_hash_rejected() {
with_large_stack(|| {
let identity = AgentIdentity::new("example.com", "agent/test").unwrap();
let vk_bytes = identity.credential().verifying_key_bytes.clone();
let ev1 = AuditEvent::new(
identity.spiffe_id().clone(),
"action.one",
json!({}),
None,
);
let signed1 = ev1.sign(&identity).unwrap();
let ev2 = AuditEvent::new(
identity.spiffe_id().clone(),
"action.two",
json!({}),
Some("0000000000000000000000000000000000000000000000000000000000000000"
.to_string()),
);
let signed2 = ev2.sign(&identity).unwrap();
let result = verify_audit_chain(
&[signed1, signed2],
&[vk_bytes.clone(), vk_bytes],
);
assert!(matches!(result, Err(Error::AuditError(_))));
});
}
#[test]
fn audit_chain_first_event_nonempty_hash_rejected() {
with_large_stack(|| {
let identity = AgentIdentity::new("example.com", "agent/test").unwrap();
let vk_bytes = identity.credential().verifying_key_bytes.clone();
let ev = AuditEvent::new(
identity.spiffe_id().clone(),
"action.one",
json!({}),
Some("notempty".to_string()),
);
let signed = ev.sign(&identity).unwrap();
let result = verify_audit_chain(&[signed], &[vk_bytes]);
assert!(matches!(result, Err(Error::AuditError(_))));
});
}
#[test]
fn audit_chain_mismatched_lengths_rejected() {
with_large_stack(|| {
let identity = AgentIdentity::new("example.com", "agent/test").unwrap();
let vk_bytes = identity.credential().verifying_key_bytes.clone();
let ev = AuditEvent::new(
identity.spiffe_id().clone(),
"action.one",
json!({}),
None,
);
let signed = ev.sign(&identity).unwrap();
let result = verify_audit_chain(&[signed], &[vk_bytes.clone(), vk_bytes]);
assert!(matches!(result, Err(Error::AuditError(_))));
});
}
#[test]
fn audit_event_details_arbitrary_json() {
with_large_stack(|| {
let identity = AgentIdentity::new("example.com", "agent/test").unwrap();
let vk_bytes = identity.credential().verifying_key_bytes.clone();
let details = json!({
"nested": {"key": "value"},
"array": [1, 2, 3],
"null_field": null
});
let ev = AuditEvent::new(
identity.spiffe_id().clone(),
"complex.event",
details.clone(),
None,
);
let signed = ev.sign(&identity).unwrap();
assert!(signed.verify(&vk_bytes).unwrap());
assert_eq!(signed.event.details(), details);
});
}
}