use sha2::{Digest, Sha256};
pub const CHAIN_HASH_LEN: usize = 32;
pub const GENESIS_HASH: [u8; CHAIN_HASH_LEN] = [0u8; CHAIN_HASH_LEN];
const HMAC_BLOCK_SIZE: usize = 64;
fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; CHAIN_HASH_LEN] {
let mut key_block = [0u8; HMAC_BLOCK_SIZE];
let normalized: Vec<u8> = if key.len() > HMAC_BLOCK_SIZE {
Sha256::digest(key).to_vec()
} else {
key.to_vec()
};
key_block[..normalized.len()].copy_from_slice(&normalized);
let mut inner = Sha256::new();
let mut outer = Sha256::new();
for byte in &key_block {
inner.update([byte ^ 0x36]);
outer.update([byte ^ 0x5c]);
}
inner.update(message);
outer.update(inner.finalize());
let out = outer.finalize();
let mut bytes = [0u8; CHAIN_HASH_LEN];
bytes.copy_from_slice(&out);
bytes
}
fn hex_encode(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
out.push_str(&format!("{b:02x}"));
}
out
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct PermissionChangeRecord {
pub role: String,
pub action: String,
pub actor: String,
pub at: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
}
impl PermissionChangeRecord {
pub fn new(
role: impl Into<String>,
action: impl Into<String>,
actor: impl Into<String>,
) -> Self {
let at = time::OffsetDateTime::now_utc()
.format(&time::format_description::well_known::Rfc3339)
.unwrap_or_default();
Self {
role: role.into(),
action: action.into(),
actor: actor.into(),
at,
detail: None,
}
}
pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
self.detail = Some(detail.into());
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ChainEntry {
pub seq: u64,
pub prev_hash: String,
pub event_json: String,
pub hmac: String,
}
impl ChainEntry {
pub fn compute_hmac(&self, key: &[u8]) -> String {
let prev = hex_decode(&self.prev_hash).unwrap_or_default();
let msg = chain_message(&prev, self.event_json.as_bytes());
hex_encode(&hmac_sha256(key, &msg))
}
}
fn chain_message(prev_hash: &[u8], canonical_event: &[u8]) -> Vec<u8> {
let mut msg = Vec::with_capacity(prev_hash.len() + canonical_event.len());
msg.extend_from_slice(prev_hash);
msg.extend_from_slice(canonical_event);
msg
}
fn hex_decode(s: &str) -> Option<Vec<u8>> {
if s.len() % 2 != 0 {
return None;
}
(0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
.collect()
}
pub struct PermissionAuditChain {
key: Vec<u8>,
prev_hash: [u8; CHAIN_HASH_LEN],
seq: u64,
entries: Vec<ChainEntry>,
}
impl PermissionAuditChain {
pub fn new(key: &[u8]) -> Self {
Self {
key: key.to_vec(),
prev_hash: GENESIS_HASH,
seq: 0,
entries: Vec::new(),
}
}
pub fn append(&mut self, record: &PermissionChangeRecord) -> ChainEntry {
let event_json = serde_json::to_string(record).expect("record serializes");
let entry = self.append_event_json(event_json);
self.entries.push(entry.clone());
entry
}
pub fn append_event_json(&mut self, event_json: String) -> ChainEntry {
let msg = chain_message(&self.prev_hash, event_json.as_bytes());
let hmac = hmac_sha256(&self.key, &msg);
let entry = ChainEntry {
seq: self.seq,
prev_hash: hex_encode(&self.prev_hash),
event_json,
hmac: hex_encode(&hmac),
};
self.prev_hash = hmac;
self.seq += 1;
entry
}
pub fn entries(&self) -> &[ChainEntry] {
&self.entries
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn last_hash(&self) -> String {
hex_encode(&self.prev_hash)
}
pub fn verify(&self) -> bool {
verify_permission_chain(&self.entries, &self.key)
}
}
pub fn verify_permission_chain(entries: &[ChainEntry], key: &[u8]) -> bool {
let mut prev_hash = GENESIS_HASH;
for (expected_seq, entry) in entries.iter().enumerate() {
let seq = entry.seq as usize;
if seq != expected_seq {
return false;
}
let Some(prev) = hex_decode(&entry.prev_hash) else {
return false;
};
if prev != prev_hash {
return false;
}
let recomputed_msg = chain_message(&prev, entry.event_json.as_bytes());
let recomputed = hmac_sha256(key, &recomputed_msg);
let Some(hmac) = hex_decode(&entry.hmac) else {
return false;
};
if hmac.len() != CHAIN_HASH_LEN || hmac != recomputed {
return false;
}
prev_hash = hmac.try_into().unwrap_or(GENESIS_HASH);
}
true
}
#[cfg(test)]
mod tests {
use super::*;
fn record(role: &str, action: &str) -> PermissionChangeRecord {
PermissionChangeRecord::new(role, action, "operator")
}
#[test]
fn hmac_matches_rfc4231_case_1() {
let expected = "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7";
let mac = hmac_sha256(&[0x0b; 20], b"Hi There");
assert_eq!(hex_encode(&mac), expected);
}
#[test]
fn hmac_matches_rfc4231_case_2() {
let expected = "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843";
let mac = hmac_sha256(b"Jefe", b"what do ya want for nothing?");
assert_eq!(hex_encode(&mac), expected);
}
#[test]
fn hmac_long_key_normalized() {
let key = [0xaa; 131];
let expected = "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54";
let mac = hmac_sha256(
&key,
b"Test Using Larger Than Block-Size Key - Hash Key First",
);
assert_eq!(hex_encode(&mac), expected);
}
#[test]
fn test_chain_append_links_entries() {
let mut chain = PermissionAuditChain::new(b"key");
let e0 = chain.append(&PermissionChangeRecord::new("admin", "role_added", "op"));
assert_eq!(e0.seq, 0);
assert_eq!(e0.prev_hash, hex_encode(&GENESIS_HASH));
let e1 = chain.append(&PermissionChangeRecord::new(
"analyst",
"policy_updated",
"op",
));
assert_eq!(e1.seq, 1);
assert_eq!(e1.prev_hash, e0.hmac, "第二条 prev_hash 应为第一条 hmac");
assert_eq!(chain.len(), 2);
assert!(chain.verify(), "完好链条应通过校验");
}
#[test]
fn test_verify_detects_tampered_event() {
let mut chain = PermissionAuditChain::new(b"key");
chain.append(&PermissionChangeRecord::new("admin", "role_added", "op"));
let mut entries = chain.entries().to_vec();
entries[0].event_json = entries[0].event_json.replace("role_added", "role_removed");
assert!(
!verify_permission_chain(&entries, b"key"),
"篡改事件应被检出"
);
let mut entries2 = chain.entries().to_vec();
entries2[0].hmac = "00".repeat(CHAIN_HASH_LEN);
assert!(!verify_permission_chain(&entries2, b"key"));
}
#[test]
fn test_verify_detects_deletion_and_reordering() {
let mut chain = PermissionAuditChain::new(b"key");
chain.append(&PermissionChangeRecord::new("admin", "role_added", "op"));
chain.append(&PermissionChangeRecord::new(
"analyst",
"policy_updated",
"op",
));
chain.append(&PermissionChangeRecord::new("guest", "role_removed", "op"));
let entries = chain.entries().to_vec();
let deleted: Vec<ChainEntry> = entries
.iter()
.enumerate()
.filter(|(i, _)| *i != 1)
.map(|(_, e)| e.clone())
.collect();
assert!(
!verify_permission_chain(&deleted, b"key"),
"删除条目应被检出"
);
let mut reordered = entries.clone();
reordered.swap(0, 1);
assert!(!verify_permission_chain(&reordered, b"key"), "重排应被检出");
}
#[test]
fn test_verify_wrong_key_and_empty_chain() {
let mut chain = PermissionAuditChain::new(b"key");
chain.append(&PermissionChangeRecord::new("admin", "role_added", "op"));
assert!(!verify_permission_chain(chain.entries(), b"other-key"));
let empty = PermissionAuditChain::new(b"key");
assert!(verify_permission_chain(&[], b"key"));
assert!(empty.verify());
}
#[test]
fn test_hex_decode_invalid() {
assert!(hex_decode("zz").is_none());
assert!(hex_decode("abc").is_none());
assert_eq!(hex_decode(""), Some(Vec::new()));
}
#[test]
fn test_record_serde_roundtrip() {
let record = PermissionChangeRecord::new("admin", "role_added", "op")
.with_detail(r#"{"tables":["*"]}"#);
let text = serde_json::to_string(&record).unwrap();
let parsed: PermissionChangeRecord = serde_json::from_str(&text).unwrap();
assert_eq!(parsed.role, "admin");
assert_eq!(parsed.detail.as_deref(), Some(r#"{"tables":["*"]}"#));
}
}