use ed25519_dalek::{Signature, VerifyingKey};
use serde_json::{Map, Value};
use crate::audit_canonical::{canonical_audit_bytes, payload_hash_hex};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeySource {
Pinned,
Event,
}
#[derive(Debug, Clone)]
pub struct AuditVerifyResult {
pub valid: bool,
pub reason: Option<String>,
pub payload_hash: String,
pub key_source: KeySource,
}
fn ed25519_verify(message: &[u8], signature: &[u8], pubkey: &[u8]) -> bool {
let key_bytes: [u8; 32] = match pubkey.try_into() {
Ok(b) => b,
Err(_) => return false,
};
let vk = match VerifyingKey::from_bytes(&key_bytes) {
Ok(k) => k,
Err(_) => return false,
};
let sig = match Signature::from_slice(signature) {
Ok(s) => s,
Err(_) => return false,
};
vk.verify_strict(message, &sig).is_ok()
}
pub fn verify_audit_event(event: &Value, public_key: Option<&str>) -> AuditVerifyResult {
let key_source = if public_key.is_some() {
KeySource::Pinned
} else {
KeySource::Event
};
let obj = match event.as_object() {
Some(o) => o,
None => {
return AuditVerifyResult {
valid: false,
reason: Some("canonicalization_failed".into()),
payload_hash: String::new(),
key_source,
}
}
};
let mut signed = Map::new();
let copy = |m: &mut Map<String, Value>, key: &str, val: Option<&Value>| {
m.insert(key.to_string(), val.cloned().unwrap_or(Value::Null));
};
copy(&mut signed, "org_id", obj.get("org_id"));
let event_id = obj
.get("id")
.filter(|v| !v.is_null())
.or_else(|| obj.get("event_id"));
signed.insert(
"event_id".to_string(),
event_id.cloned().unwrap_or(Value::Null),
);
copy(&mut signed, "seq", obj.get("seq"));
copy(&mut signed, "ingested_at", obj.get("ingested_at"));
copy(&mut signed, "action", obj.get("action"));
copy(&mut signed, "occurred_at", obj.get("occurred_at"));
copy(&mut signed, "actor", obj.get("actor"));
copy(&mut signed, "targets", obj.get("targets"));
if let Some(ctx) = obj.get("context").filter(|v| !v.is_null()) {
signed.insert("context".to_string(), ctx.clone());
}
if let Some(md) = obj.get("metadata").filter(|v| !v.is_null()) {
signed.insert("metadata".to_string(), md.clone());
}
let canonical = match canonical_audit_bytes(&Value::Object(signed)) {
Ok(c) => c,
Err(_) => {
return AuditVerifyResult {
valid: false,
reason: Some("canonicalization_failed".into()),
payload_hash: String::new(),
key_source,
}
}
};
let recomputed = payload_hash_hex(&canonical);
if let Some(ph) = obj.get("payload_hash").and_then(Value::as_str) {
if ph != recomputed {
return AuditVerifyResult {
valid: false,
reason: Some("payload_hash_mismatch".into()),
payload_hash: recomputed,
key_source,
};
}
}
let key_hex = public_key.map(str::to_string).or_else(|| {
obj.get("signing_public_key")
.and_then(Value::as_str)
.map(str::to_string)
});
let key_hex = match key_hex {
Some(k) if !k.is_empty() => k,
_ => {
return AuditVerifyResult {
valid: false,
reason: Some("no_public_key".into()),
payload_hash: recomputed,
key_source,
}
}
};
let sig_hex = obj.get("signature").and_then(Value::as_str).unwrap_or("");
if sig_hex.is_empty() {
return AuditVerifyResult {
valid: false,
reason: Some("no_signature".into()),
payload_hash: recomputed,
key_source,
};
}
let sig_bytes = match hex::decode(sig_hex) {
Ok(b) => b,
Err(_) => {
return AuditVerifyResult {
valid: false,
reason: Some("signature_invalid".into()),
payload_hash: recomputed,
key_source,
}
}
};
let key_bytes = match hex::decode(&key_hex) {
Ok(b) => b,
Err(_) => {
return AuditVerifyResult {
valid: false,
reason: Some("signature_invalid".into()),
payload_hash: recomputed,
key_source,
}
}
};
if ed25519_verify(&canonical, &sig_bytes, &key_bytes) {
AuditVerifyResult {
valid: true,
reason: None,
payload_hash: recomputed,
key_source,
}
} else {
AuditVerifyResult {
valid: false,
reason: Some("signature_invalid".into()),
payload_hash: recomputed,
key_source,
}
}
}