use serde::{Deserialize, Serialize};
use crate::error::CoreError;
use crate::sha256::{hex, sha256};
use crate::wal::{chain_value, WalRecord};
pub const ANCHOR_SCHEMA: &str = "wanning-anchor-v1";
const HMAC_BLOCK: usize = 64;
pub fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
let mut block = [0u8; HMAC_BLOCK];
if key.len() > HMAC_BLOCK {
block[..32].copy_from_slice(&sha256(key));
} else {
block[..key.len()].copy_from_slice(key);
}
let mut inner = Vec::with_capacity(HMAC_BLOCK + message.len());
for byte in &block {
inner.push(byte ^ 0x36);
}
inner.extend_from_slice(message);
let inner_hash = sha256(&inner);
let mut outer = Vec::with_capacity(HMAC_BLOCK + 32);
for byte in &block {
outer.push(byte ^ 0x5c);
}
outer.extend_from_slice(&inner_hash);
sha256(&outer)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AnchorMaterial {
pub lines: u64,
pub chain_tail: u64,
pub records_sha256: [u8; 32],
}
pub fn material_from_records(records: &[(u64, WalRecord)]) -> Result<AnchorMaterial, CoreError> {
let mut chain = 0u64;
let mut content = Vec::new();
for (line_no, record) in records {
let rec_json = serde_json::to_string(record)
.map_err(|e| CoreError::AnchorInvalid(format!("记录序列化失败: {e}")))?;
chain = chain_value(chain, *line_no, &rec_json);
content.extend_from_slice(rec_json.as_bytes());
content.push(b'\n');
}
Ok(AnchorMaterial {
lines: records.len() as u64,
chain_tail: chain,
records_sha256: sha256(&content),
})
}
pub fn canonical_payload(material: &AnchorMaterial, anchored_at_unix: u64) -> String {
format!(
"WANNING-ANCHOR-v1\n\
lines={}\n\
chain_tail=0x{:016x}\n\
records_sha256={}\n\
anchored_at_unix={}",
material.lines,
material.chain_tail,
hex(&material.records_sha256),
anchored_at_unix
)
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AnchorFile {
pub schema: String,
#[serde(default = "default_anchor_version")]
#[serde(skip_serializing_if = "AnchorFile::version_is_implicit")]
pub version: u32,
pub lines: u64,
pub chain_tail_hex: String,
pub records_sha256_hex: String,
pub anchored_at_unix: u64,
pub mac_hex: String,
}
fn default_anchor_version() -> u32 {
1
}
impl AnchorFile {
fn version_is_implicit(version: &u32) -> bool {
*version == 1
}
}
pub fn sign_anchor(material: &AnchorMaterial, key: &[u8; 32], anchored_at_unix: u64) -> AnchorFile {
let payload = canonical_payload(material, anchored_at_unix);
let mac = hmac_sha256(key, payload.as_bytes());
AnchorFile {
schema: ANCHOR_SCHEMA.to_string(),
version: 1,
lines: material.lines,
chain_tail_hex: format!("0x{:016x}", material.chain_tail),
records_sha256_hex: hex(&material.records_sha256),
anchored_at_unix,
mac_hex: hex(&mac),
}
}
pub fn verify_anchor_file(file: &AnchorFile, key: &[u8; 32]) -> Result<AnchorMaterial, CoreError> {
if file.schema != ANCHOR_SCHEMA {
return Err(CoreError::AnchorInvalid(format!(
"schema {:?} 不是 {:?}(版本不符不猜,换版要换验法)",
file.schema, ANCHOR_SCHEMA
)));
}
let records_sha256 = parse_hex_32(&file.records_sha256_hex)
.map_err(|e| CoreError::AnchorInvalid(format!("records_sha256_hex 读不懂: {e}")))?;
let chain_tail = parse_chain_tail(&file.chain_tail_hex)
.map_err(|e| CoreError::AnchorInvalid(format!("chain_tail_hex 读不懂: {e}")))?;
let material = AnchorMaterial {
lines: file.lines,
chain_tail,
records_sha256,
};
let payload = canonical_payload(&material, file.anchored_at_unix);
let claimed = parse_hex_32(&file.mac_hex)
.map_err(|e| CoreError::AnchorInvalid(format!("mac_hex 读不懂: {e}")))?;
let expected = hmac_sha256(key, payload.as_bytes());
if !constant_time_eq(&expected, &claimed) {
return Err(CoreError::AnchorInvalid(
"锚点 MAC 与老板密钥对不上——锚点不是老板签的,或锚点文件被改过".to_string(),
));
}
Ok(material)
}
pub fn assert_wal_matches_anchor(
records: &[(u64, WalRecord)],
anchored: &AnchorMaterial,
) -> Result<(), CoreError> {
if (records.len() as u64) < anchored.lines {
return Err(CoreError::AnchorMismatch(format!(
"整体截尾:当前 WAL 只有 {} 行,锚点声明 {} 行——锚定之后的行不见了",
records.len(),
anchored.lines
)));
}
let actual = material_from_records(&records[..anchored.lines as usize])?;
if actual.records_sha256 != anchored.records_sha256 {
return Err(CoreError::AnchorMismatch(format!(
"前 {} 行内容与锚点不符——被锚定的部分在锚定后被改过\
(完整性链抓不住的尾行篡改/历史改写,锚点抓住了)",
anchored.lines
)));
}
if actual.chain_tail != anchored.chain_tail {
return Err(CoreError::AnchorMismatch(format!(
"链尾 0x{:016x} 与锚点声明的 0x{:016x} 不符(内容哈希一致而链尾不一致,\
属状态异常,fail-closed)",
actual.chain_tail, anchored.chain_tail
)));
}
Ok(())
}
fn constant_time_eq(a: &[u8; 32], b: &[u8; 32]) -> bool {
let mut diff = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
diff == 0
}
pub fn parse_hex_32(s: &str) -> Result<[u8; 32], String> {
let s = s.trim();
let bytes = parse_hex_bytes(s)?;
let arr: [u8; 32] = bytes
.try_into()
.map_err(|v: Vec<u8>| format!("需要 64 个十六进制字符(32 字节),实际 {} 字节", v.len()))?;
Ok(arr)
}
fn parse_chain_tail(s: &str) -> Result<u64, String> {
let s = s.trim();
let digits = s
.strip_prefix("0x")
.or_else(|| s.strip_prefix("0X"))
.ok_or_else(|| format!("缺少 0x 前缀: {s:?}"))?;
if digits.len() != 16 {
return Err(format!("需要 16 位十六进制,实际 {} 位", digits.len()));
}
u64::from_str_radix(digits, 16).map_err(|e| format!("十六进制解析失败: {e}"))
}
fn parse_hex_bytes(s: &str) -> Result<Vec<u8>, String> {
if !s.len().is_multiple_of(2) {
return Err("十六进制长度必须是偶数".to_string());
}
(0..s.len())
.step_by(2)
.map(|i| {
u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| format!("位置 {i} 不是十六进制: {e}"))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rfc4231_test_cases() {
let cases: Vec<(&[u8], &[u8], &str)> = vec![
(
&[0x0b; 20],
b"Hi There",
"b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7",
),
(
b"Jefe",
b"what do ya want for nothing?",
"5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843",
),
(
&[0xaa; 20],
&[0xdd; 50],
"773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe",
),
(
&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19],
&[0xcd; 50],
"82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b",
),
(
&[0xaa; 131],
b"Test Using Larger Than Block-Size Key - Hash Key First",
"60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54",
),
(
&[0xaa; 131],
b"This is a test using a larger than block-size key and a larger than block-size data. The key needs to be hashed before being used by the HMAC algorithm.",
"9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2",
),
];
for (idx, (key, message, expected)) in cases.iter().enumerate() {
let actual = hex(&hmac_sha256(key, message));
assert_eq!(&actual, expected, "RFC 4231 用例 {}", idx + 1);
}
}
#[test]
fn hmac_key_length_boundaries() {
assert_eq!(
hex(&hmac_sha256(b"", b"")),
"b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad",
"空密钥+空消息(.NET oracle 实算)"
);
assert_eq!(
hex(&hmac_sha256(&[0x3a; 64], b"exact block size key")),
"a59ee14066ab0f880f654a760fbc54ebe0abcd27b31743e1a4e5378797470bb3",
"恰好 64 字节密钥(.NET oracle 实算)"
);
}
fn sample_records() -> Vec<(u64, WalRecord)> {
use crate::delegation::Delegation;
use crate::intent::SpendIntent;
use crate::wal::WalDecision;
let delegation =
Delegation::new("d1", "老板", "agent-1", 10_00, 1_000, 2_000, "wanning-test");
vec![
(
1,
WalRecord::RegisterDelegation {
ts: 1_500,
delegation: delegation.clone(),
},
),
(
2,
WalRecord::Decide {
ts: 1_600,
decision: WalDecision::Allow,
delegation_id: "d1".into(),
intent: SpendIntent::new("d1", 1, 500, "jd:shop-1", "grocery", "测试意图"),
reason: None,
budget_after_cents: 500,
},
),
]
}
#[test]
fn material_is_independent_recompute() {
let material = material_from_records(&sample_records()).expect("材料");
assert_eq!(material.lines, 2);
assert_ne!(material.chain_tail, 0);
let empty = material_from_records(&[]).expect("空材料");
assert_eq!(empty.lines, 0);
assert_eq!(empty.chain_tail, 0);
assert_eq!(
hex(&empty.records_sha256),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
}
#[test]
fn content_hash_changes_on_any_record_change() {
let mut tampered = sample_records();
if let WalRecord::Decide { intent, .. } = &mut tampered[1].1 {
intent.memo = "测试意图".to_string() + "改";
}
let a = material_from_records(&sample_records()).expect("材料");
let b = material_from_records(&tampered).expect("被改材料");
assert_ne!(a.records_sha256, b.records_sha256, "改内容必须改哈希");
assert_ne!(a.chain_tail, b.chain_tail, "链值同样变");
}
#[test]
fn payload_is_stable_and_field_complete() {
let material = material_from_records(&sample_records()).expect("材料");
let payload = canonical_payload(&material, 1_700_000_000);
let expected = format!(
"WANNING-ANCHOR-v1\nlines=2\nchain_tail=0x{:016x}\nrecords_sha256={}\nanchored_at_unix=1700000000",
material.chain_tail,
hex(&material.records_sha256)
);
assert_eq!(payload, expected);
assert_eq!(payload, canonical_payload(&material, 1_700_000_000));
assert_ne!(payload, canonical_payload(&material, 1_700_000_001));
}
#[test]
fn sign_then_verify_roundtrip() {
let material = material_from_records(&sample_records()).expect("材料");
let key = [7u8; 32];
let file = sign_anchor(&material, &key, 1_700_000_000);
assert_eq!(file.schema, ANCHOR_SCHEMA);
let verified = verify_anchor_file(&file, &key).expect("同密钥验得过");
assert_eq!(verified, material);
}
#[test]
fn sign_is_deterministic() {
let material = material_from_records(&sample_records()).expect("材料");
let key = [9u8; 32];
let a = sign_anchor(&material, &key, 42);
let b = sign_anchor(&material, &key, 42);
assert_eq!(a, b, "同材料同密钥同时刻 → 同锚点");
let c = sign_anchor(&material, &[10u8; 32], 42);
assert_ne!(a, c, "换密钥锚点必须变");
}
#[test]
fn verify_rejects_wrong_key() {
let material = material_from_records(&sample_records()).expect("材料");
let file = sign_anchor(&material, &[1u8; 32], 42);
let err = verify_anchor_file(&file, &[2u8; 32]).unwrap_err();
assert!(
matches!(err, CoreError::AnchorInvalid(_)),
"错密钥 = 锚点不可信: {err}"
);
}
#[test]
fn verify_rejects_tampered_fields() {
let material = material_from_records(&sample_records()).expect("材料");
let key = [3u8; 32];
let file = sign_anchor(&material, &key, 42);
let mut lines = file.clone();
lines.lines = 3; assert!(matches!(
verify_anchor_file(&lines, &key),
Err(CoreError::AnchorInvalid(_))
));
let mut anchored_at = file.clone();
anchored_at.anchored_at_unix = 43;
assert!(matches!(
verify_anchor_file(&anchored_at, &key),
Err(CoreError::AnchorInvalid(_))
));
let mut schema = file.clone();
schema.schema = "wanning-anchor-v0".into();
assert!(matches!(
verify_anchor_file(&schema, &key),
Err(CoreError::AnchorInvalid(_))
));
let mut mac = file.clone();
mac.mac_hex = "00".repeat(32);
assert!(matches!(
verify_anchor_file(&mac, &key),
Err(CoreError::AnchorInvalid(_))
));
}
#[test]
fn match_semantics_prefix_truncation_and_tamper() {
let records = sample_records();
let anchored = material_from_records(&records).expect("锚定材料");
assert!(assert_wal_matches_anchor(&records, &anchored).is_ok());
let mut grown = records.clone();
grown.push((3, records[1].1.clone()));
assert!(
assert_wal_matches_anchor(&grown, &anchored).is_ok(),
"锚定后追加新行,前缀锚照常通过"
);
let err = assert_wal_matches_anchor(&records[..1], &anchored).unwrap_err();
assert!(
matches!(err, CoreError::AnchorMismatch(ref m) if m.contains("截尾")),
"截尾要点名截尾: {err}"
);
let mut tampered = grown.clone();
if let WalRecord::Decide { intent, .. } = &mut tampered[1].1 {
intent.amount_cents = 999;
}
let err = assert_wal_matches_anchor(&tampered, &anchored).unwrap_err();
assert!(
matches!(err, CoreError::AnchorMismatch(ref m) if m.contains("被改")),
"改前缀内容要现形: {err}"
);
}
#[test]
fn hex_parsing_is_strict() {
assert!(parse_hex_32(&"ab".repeat(32)).is_ok());
assert!(parse_hex_32(&"AB".repeat(32)).is_ok(), "大写也收");
assert!(parse_hex_32(&"ab".repeat(31)).is_err(), "长度不足拒");
assert!(parse_hex_32("zz").is_err(), "非十六进制拒");
assert_eq!(parse_chain_tail("0x0000000000000000").unwrap(), 0);
assert!(
parse_chain_tail("0000000000000000").is_err(),
"缺 0x 前缀拒"
);
assert!(parse_chain_tail("0x00").is_err(), "长度不对拒");
}
}