1use serde::{Deserialize, Serialize};
34
35use crate::error::CoreError;
36use crate::sha256::{hex, sha256};
37use crate::wal::{chain_value, WalRecord};
38
39pub const ANCHOR_SCHEMA: &str = "wanning-anchor-v1";
41
42const HMAC_BLOCK: usize = 64;
44
45pub fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
47 let mut block = [0u8; HMAC_BLOCK];
48 if key.len() > HMAC_BLOCK {
49 block[..32].copy_from_slice(&sha256(key));
50 } else {
51 block[..key.len()].copy_from_slice(key);
52 }
53 let mut inner = Vec::with_capacity(HMAC_BLOCK + message.len());
54 for byte in &block {
55 inner.push(byte ^ 0x36);
56 }
57 inner.extend_from_slice(message);
58 let inner_hash = sha256(&inner);
59
60 let mut outer = Vec::with_capacity(HMAC_BLOCK + 32);
61 for byte in &block {
62 outer.push(byte ^ 0x5c);
63 }
64 outer.extend_from_slice(&inner_hash);
65 sha256(&outer)
66}
67
68#[derive(Clone, Debug, PartialEq, Eq)]
70pub struct AnchorMaterial {
71 pub lines: u64,
73 pub chain_tail: u64,
75 pub records_sha256: [u8; 32],
78}
79
80pub fn material_from_records(records: &[(u64, WalRecord)]) -> Result<AnchorMaterial, CoreError> {
85 let mut chain = 0u64;
86 let mut content = Vec::new();
87 for (line_no, record) in records {
88 let rec_json = serde_json::to_string(record)
89 .map_err(|e| CoreError::AnchorInvalid(format!("记录序列化失败: {e}")))?;
90 chain = chain_value(chain, *line_no, &rec_json);
91 content.extend_from_slice(rec_json.as_bytes());
92 content.push(b'\n');
93 }
94 Ok(AnchorMaterial {
95 lines: records.len() as u64,
96 chain_tail: chain,
97 records_sha256: sha256(&content),
98 })
99}
100
101pub fn canonical_payload(material: &AnchorMaterial, anchored_at_unix: u64) -> String {
105 format!(
106 "WANNING-ANCHOR-v1\n\
107 lines={}\n\
108 chain_tail=0x{:016x}\n\
109 records_sha256={}\n\
110 anchored_at_unix={}",
111 material.lines,
112 material.chain_tail,
113 hex(&material.records_sha256),
114 anchored_at_unix
115 )
116}
117
118#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
120#[serde(deny_unknown_fields)]
121pub struct AnchorFile {
122 pub schema: String,
123 #[serde(default = "default_anchor_version")]
126 #[serde(skip_serializing_if = "AnchorFile::version_is_implicit")]
127 pub version: u32,
128 pub lines: u64,
129 pub chain_tail_hex: String,
131 pub records_sha256_hex: String,
133 pub anchored_at_unix: u64,
134 pub mac_hex: String,
136}
137
138fn default_anchor_version() -> u32 {
139 1
140}
141
142impl AnchorFile {
143 fn version_is_implicit(version: &u32) -> bool {
145 *version == 1
146 }
147}
148
149pub fn sign_anchor(material: &AnchorMaterial, key: &[u8; 32], anchored_at_unix: u64) -> AnchorFile {
151 let payload = canonical_payload(material, anchored_at_unix);
152 let mac = hmac_sha256(key, payload.as_bytes());
153 AnchorFile {
154 schema: ANCHOR_SCHEMA.to_string(),
155 version: 1,
156 lines: material.lines,
157 chain_tail_hex: format!("0x{:016x}", material.chain_tail),
158 records_sha256_hex: hex(&material.records_sha256),
159 anchored_at_unix,
160 mac_hex: hex(&mac),
161 }
162}
163
164pub fn verify_anchor_file(file: &AnchorFile, key: &[u8; 32]) -> Result<AnchorMaterial, CoreError> {
167 if file.schema != ANCHOR_SCHEMA {
168 return Err(CoreError::AnchorInvalid(format!(
169 "schema {:?} 不是 {:?}(版本不符不猜,换版要换验法)",
170 file.schema, ANCHOR_SCHEMA
171 )));
172 }
173 let records_sha256 = parse_hex_32(&file.records_sha256_hex)
174 .map_err(|e| CoreError::AnchorInvalid(format!("records_sha256_hex 读不懂: {e}")))?;
175 let chain_tail = parse_chain_tail(&file.chain_tail_hex)
176 .map_err(|e| CoreError::AnchorInvalid(format!("chain_tail_hex 读不懂: {e}")))?;
177 let material = AnchorMaterial {
178 lines: file.lines,
179 chain_tail,
180 records_sha256,
181 };
182 let payload = canonical_payload(&material, file.anchored_at_unix);
183 let claimed = parse_hex_32(&file.mac_hex)
184 .map_err(|e| CoreError::AnchorInvalid(format!("mac_hex 读不懂: {e}")))?;
185 let expected = hmac_sha256(key, payload.as_bytes());
186 if !constant_time_eq(&expected, &claimed) {
187 return Err(CoreError::AnchorInvalid(
189 "锚点 MAC 与所有者密钥对不上——锚点不是所有者签的,或锚点文件被改过".to_string(),
190 ));
191 }
192 Ok(material)
193}
194
195pub fn assert_wal_matches_anchor(
202 records: &[(u64, WalRecord)],
203 anchored: &AnchorMaterial,
204) -> Result<(), CoreError> {
205 if (records.len() as u64) < anchored.lines {
206 return Err(CoreError::AnchorMismatch(format!(
207 "整体截尾:当前 WAL 只有 {} 行,锚点声明 {} 行——锚定之后的行不见了",
208 records.len(),
209 anchored.lines
210 )));
211 }
212 let actual = material_from_records(&records[..anchored.lines as usize])?;
213 if actual.records_sha256 != anchored.records_sha256 {
214 return Err(CoreError::AnchorMismatch(format!(
215 "前 {} 行内容与锚点不符——被锚定的部分在锚定后被改过\
216 (完整性链抓不住的尾行篡改/历史改写,锚点抓住了)",
217 anchored.lines
218 )));
219 }
220 if actual.chain_tail != anchored.chain_tail {
221 return Err(CoreError::AnchorMismatch(format!(
224 "链尾 0x{:016x} 与锚点声明的 0x{:016x} 不符(内容哈希一致而链尾不一致,\
225 属状态异常,fail-closed)",
226 actual.chain_tail, anchored.chain_tail
227 )));
228 }
229 Ok(())
230}
231
232fn constant_time_eq(a: &[u8; 32], b: &[u8; 32]) -> bool {
235 let mut diff = 0u8;
236 for (x, y) in a.iter().zip(b.iter()) {
237 diff |= x ^ y;
238 }
239 diff == 0
240}
241
242pub fn parse_hex_32(s: &str) -> Result<[u8; 32], String> {
245 let s = s.trim();
246 let bytes = parse_hex_bytes(s)?;
247 let arr: [u8; 32] = bytes
248 .try_into()
249 .map_err(|v: Vec<u8>| format!("需要 64 个十六进制字符(32 字节),实际 {} 字节", v.len()))?;
250 Ok(arr)
251}
252
253fn parse_chain_tail(s: &str) -> Result<u64, String> {
254 let s = s.trim();
255 let digits = s
256 .strip_prefix("0x")
257 .or_else(|| s.strip_prefix("0X"))
258 .ok_or_else(|| format!("缺少 0x 前缀: {s:?}"))?;
259 if digits.len() != 16 {
260 return Err(format!("需要 16 位十六进制,实际 {} 位", digits.len()));
261 }
262 u64::from_str_radix(digits, 16).map_err(|e| format!("十六进制解析失败: {e}"))
263}
264
265fn parse_hex_bytes(s: &str) -> Result<Vec<u8>, String> {
266 if !s.len().is_multiple_of(2) {
267 return Err("十六进制长度必须是偶数".to_string());
268 }
269 (0..s.len())
270 .step_by(2)
271 .map(|i| {
272 u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| format!("位置 {i} 不是十六进制: {e}"))
273 })
274 .collect()
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
285 fn rfc4231_test_cases() {
286 let cases: Vec<(&[u8], &[u8], &str)> = vec![
287 (
289 &[0x0b; 20],
290 b"Hi There",
291 "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7",
292 ),
293 (
295 b"Jefe",
296 b"what do ya want for nothing?",
297 "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843",
298 ),
299 (
301 &[0xaa; 20],
302 &[0xdd; 50],
303 "773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe",
304 ),
305 (
307 &[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
308 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19],
309 &[0xcd; 50],
310 "82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b",
311 ),
312 (
314 &[0xaa; 131],
315 b"Test Using Larger Than Block-Size Key - Hash Key First",
316 "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54",
317 ),
318 (
320 &[0xaa; 131],
321 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.",
322 "9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2",
323 ),
324 ];
325 for (idx, (key, message, expected)) in cases.iter().enumerate() {
326 let actual = hex(&hmac_sha256(key, message));
327 assert_eq!(&actual, expected, "RFC 4231 用例 {}", idx + 1);
328 }
329 }
330
331 #[test]
333 fn hmac_key_length_boundaries() {
334 assert_eq!(
335 hex(&hmac_sha256(b"", b"")),
336 "b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad",
337 "空密钥+空消息(.NET oracle 实算)"
338 );
339 assert_eq!(
340 hex(&hmac_sha256(&[0x3a; 64], b"exact block size key")),
341 "a59ee14066ab0f880f654a760fbc54ebe0abcd27b31743e1a4e5378797470bb3",
342 "恰好 64 字节密钥(.NET oracle 实算)"
343 );
344 }
345
346 fn sample_records() -> Vec<(u64, WalRecord)> {
347 use crate::delegation::Delegation;
348 use crate::intent::SpendIntent;
349 use crate::wal::WalDecision;
350 let delegation = Delegation::new(
351 "d1",
352 "所有者",
353 "agent-1",
354 10_00,
355 1_000,
356 2_000,
357 "wanning-test",
358 );
359 vec![
360 (
361 1,
362 WalRecord::RegisterDelegation {
363 ts: 1_500,
364 delegation: delegation.clone(),
365 },
366 ),
367 (
368 2,
369 WalRecord::Decide {
370 ts: 1_600,
371 decision: WalDecision::Allow,
372 delegation_id: "d1".into(),
373 intent: SpendIntent::new("d1", 1, 500, "jd:shop-1", "grocery", "测试意图"),
374 reason: None,
375 budget_after_cents: 500,
376 },
377 ),
378 ]
379 }
380
381 #[test]
382 fn material_is_independent_recompute() {
383 let material = material_from_records(&sample_records()).expect("材料");
385 assert_eq!(material.lines, 2);
386 assert_ne!(material.chain_tail, 0);
387 let empty = material_from_records(&[]).expect("空材料");
389 assert_eq!(empty.lines, 0);
390 assert_eq!(empty.chain_tail, 0);
391 assert_eq!(
392 hex(&empty.records_sha256),
393 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
394 );
395 }
396
397 #[test]
398 fn content_hash_changes_on_any_record_change() {
399 let mut tampered = sample_records();
401 if let WalRecord::Decide { intent, .. } = &mut tampered[1].1 {
402 intent.memo = "测试意图".to_string() + "改";
403 }
404 let a = material_from_records(&sample_records()).expect("材料");
405 let b = material_from_records(&tampered).expect("被改材料");
406 assert_ne!(a.records_sha256, b.records_sha256, "改内容必须改哈希");
407 assert_ne!(a.chain_tail, b.chain_tail, "链值同样变");
408 }
409
410 #[test]
411 fn payload_is_stable_and_field_complete() {
412 let material = material_from_records(&sample_records()).expect("材料");
413 let payload = canonical_payload(&material, 1_700_000_000);
414 let expected = format!(
416 "WANNING-ANCHOR-v1\nlines=2\nchain_tail=0x{:016x}\nrecords_sha256={}\nanchored_at_unix=1700000000",
417 material.chain_tail,
418 hex(&material.records_sha256)
419 );
420 assert_eq!(payload, expected);
421 assert_eq!(payload, canonical_payload(&material, 1_700_000_000));
423 assert_ne!(payload, canonical_payload(&material, 1_700_000_001));
424 }
425
426 #[test]
427 fn sign_then_verify_roundtrip() {
428 let material = material_from_records(&sample_records()).expect("材料");
429 let key = [7u8; 32];
430 let file = sign_anchor(&material, &key, 1_700_000_000);
431 assert_eq!(file.schema, ANCHOR_SCHEMA);
432 let verified = verify_anchor_file(&file, &key).expect("同密钥验得过");
433 assert_eq!(verified, material);
434 }
435
436 #[test]
437 fn sign_is_deterministic() {
438 let material = material_from_records(&sample_records()).expect("材料");
439 let key = [9u8; 32];
440 let a = sign_anchor(&material, &key, 42);
441 let b = sign_anchor(&material, &key, 42);
442 assert_eq!(a, b, "同材料同密钥同时刻 → 同锚点");
443 let c = sign_anchor(&material, &[10u8; 32], 42);
444 assert_ne!(a, c, "换密钥锚点必须变");
445 }
446
447 #[test]
448 fn verify_rejects_wrong_key() {
449 let material = material_from_records(&sample_records()).expect("材料");
450 let file = sign_anchor(&material, &[1u8; 32], 42);
451 let err = verify_anchor_file(&file, &[2u8; 32]).unwrap_err();
452 assert!(
453 matches!(err, CoreError::AnchorInvalid(_)),
454 "错密钥 = 锚点不可信: {err}"
455 );
456 }
457
458 #[test]
459 fn verify_rejects_tampered_fields() {
460 let material = material_from_records(&sample_records()).expect("材料");
461 let key = [3u8; 32];
462 let file = sign_anchor(&material, &key, 42);
463
464 let mut lines = file.clone();
465 lines.lines = 3; assert!(matches!(
467 verify_anchor_file(&lines, &key),
468 Err(CoreError::AnchorInvalid(_))
469 ));
470
471 let mut anchored_at = file.clone();
472 anchored_at.anchored_at_unix = 43;
473 assert!(matches!(
474 verify_anchor_file(&anchored_at, &key),
475 Err(CoreError::AnchorInvalid(_))
476 ));
477
478 let mut schema = file.clone();
479 schema.schema = "wanning-anchor-v0".into();
480 assert!(matches!(
481 verify_anchor_file(&schema, &key),
482 Err(CoreError::AnchorInvalid(_))
483 ));
484
485 let mut mac = file.clone();
486 mac.mac_hex = "00".repeat(32);
487 assert!(matches!(
488 verify_anchor_file(&mac, &key),
489 Err(CoreError::AnchorInvalid(_))
490 ));
491 }
492
493 #[test]
494 fn match_semantics_prefix_truncation_and_tamper() {
495 let records = sample_records();
496 let anchored = material_from_records(&records).expect("锚定材料");
497
498 assert!(assert_wal_matches_anchor(&records, &anchored).is_ok());
500 let mut grown = records.clone();
501 grown.push((3, records[1].1.clone()));
502 assert!(
503 assert_wal_matches_anchor(&grown, &anchored).is_ok(),
504 "锚定后追加新行,前缀锚照常通过"
505 );
506
507 let err = assert_wal_matches_anchor(&records[..1], &anchored).unwrap_err();
509 assert!(
510 matches!(err, CoreError::AnchorMismatch(ref m) if m.contains("截尾")),
511 "截尾要点名截尾: {err}"
512 );
513
514 let mut tampered = grown.clone();
516 if let WalRecord::Decide { intent, .. } = &mut tampered[1].1 {
517 intent.amount_cents = 999;
518 }
519 let err = assert_wal_matches_anchor(&tampered, &anchored).unwrap_err();
520 assert!(
521 matches!(err, CoreError::AnchorMismatch(ref m) if m.contains("被改")),
522 "改前缀内容要现形: {err}"
523 );
524 }
525
526 #[test]
527 fn hex_parsing_is_strict() {
528 assert!(parse_hex_32(&"ab".repeat(32)).is_ok());
529 assert!(parse_hex_32(&"AB".repeat(32)).is_ok(), "大写也收");
530 assert!(parse_hex_32(&"ab".repeat(31)).is_err(), "长度不足拒");
531 assert!(parse_hex_32("zz").is_err(), "非十六进制拒");
532 assert_eq!(parse_chain_tail("0x0000000000000000").unwrap(), 0);
533 assert!(
534 parse_chain_tail("0000000000000000").is_err(),
535 "缺 0x 前缀拒"
536 );
537 assert!(parse_chain_tail("0x00").is_err(), "长度不对拒");
538 }
539}