Files
2025-12-26 23:21:39 +00:00

178 lines
5.9 KiB
Rust

use base64::Engine as _;
use ed25519_dalek::{Signer, SigningKey};
use ledger_core::attestation::{
checkpoint_attestation_signing_message_v0, checkpoint_attestation_signing_message_v1,
};
use ledger_core::{
CHECKPOINT_ATTESTATION_V0_FORMAT, CHECKPOINT_ATTESTATION_V1_FORMAT, CheckpointAttestation,
CheckpointAttestationV0, CheckpointAttestationV1, EntryUnsigned, RECEIPT_V0_FORMAT,
ReadProofV0, ReceiptV0, verify_receipt_v0,
};
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
#[test]
fn receipt_v0_verifies_with_attestation() {
let author_key = SigningKey::from_bytes(&[7u8; 32]);
let author_pubkey = author_key.verifying_key().to_bytes();
let unsigned1 = EntryUnsigned {
prev_hash: [0u8; 32],
ts_ms: 1,
namespace: "law".to_string(),
payload_cbor: vec![0x61, 0x41], // "A"
author_pubkey,
};
let sig1 = author_key.sign(&unsigned1.signing_message()).to_bytes();
let entry1 = unsigned1.to_entry(sig1);
let h1 = entry1.hash();
let unsigned2 = EntryUnsigned {
prev_hash: h1,
ts_ms: 2,
namespace: "law".to_string(),
payload_cbor: vec![0x61, 0x42], // "B"
author_pubkey,
};
let sig2 = author_key.sign(&unsigned2.signing_message()).to_bytes();
let entry2 = unsigned2.to_entry(sig2);
let h2 = entry2.hash();
let hashes = vec![h1, h2];
let root = ledger_core::merkle::merkle_root(&hashes);
let head = h2;
let proof = ReadProofV0::from_tree(&hashes, 1).expect("build proof");
let witness_key = SigningKey::from_bytes(&[0x44u8; 32]);
let witness_pubkey = witness_key.verifying_key().to_bytes();
let ts_seen_ms = 123_456u64;
let msg = checkpoint_attestation_signing_message_v0(&h1, 2, &root, &head, ts_seen_ms);
let witness_sig = witness_key.sign(&msg).to_bytes();
let att = CheckpointAttestationV0 {
format: CHECKPOINT_ATTESTATION_V0_FORMAT.to_string(),
ledger_genesis_hash_hex: h1,
checkpoint_entry_count: 2,
checkpoint_merkle_root_hex: root,
checkpoint_head_hash_hex: head,
ts_seen_ms,
witness_pubkey_hex: witness_pubkey,
witness_sig_hex: witness_sig,
};
let entry_cbor = serde_cbor::to_vec(&entry2).expect("encode entry");
let receipt = ReceiptV0 {
format: RECEIPT_V0_FORMAT.to_string(),
entry_cbor_b64: base64::engine::general_purpose::STANDARD_NO_PAD.encode(entry_cbor),
entry_hash_hex: hex(&h2),
read_proof: proof,
attestations: vec![CheckpointAttestation::V0(att)],
};
verify_receipt_v0(&receipt, true).expect("receipt verifies");
}
#[test]
fn receipt_v0_verifies_with_attestation_v1() {
let author_key = SigningKey::from_bytes(&[7u8; 32]);
let author_pubkey = author_key.verifying_key().to_bytes();
let unsigned1 = EntryUnsigned {
prev_hash: [0u8; 32],
ts_ms: 1,
namespace: "law".to_string(),
payload_cbor: vec![0x61, 0x41], // "A"
author_pubkey,
};
let sig1 = author_key.sign(&unsigned1.signing_message()).to_bytes();
let entry1 = unsigned1.to_entry(sig1);
let h1 = entry1.hash();
let unsigned2 = EntryUnsigned {
prev_hash: h1,
ts_ms: 2,
namespace: "law".to_string(),
payload_cbor: vec![0x61, 0x42], // "B"
author_pubkey,
};
let sig2 = author_key.sign(&unsigned2.signing_message()).to_bytes();
let entry2 = unsigned2.to_entry(sig2);
let h2 = entry2.hash();
let hashes = vec![h1, h2];
let root = ledger_core::merkle::merkle_root(&hashes);
let head = h2;
let proof = ReadProofV0::from_tree(&hashes, 1).expect("build proof");
let witness_key = SigningKey::from_bytes(&[0x44u8; 32]);
let witness_pubkey = witness_key.verifying_key().to_bytes();
let checkpoint_ts_ms = 100u64;
let ts_seen_ms = 123_456u64;
let msg = checkpoint_attestation_signing_message_v1(
&h1,
2,
&root,
&head,
checkpoint_ts_ms,
ts_seen_ms,
);
let witness_sig = witness_key.sign(&msg).to_bytes();
let att = CheckpointAttestationV1 {
format: CHECKPOINT_ATTESTATION_V1_FORMAT.to_string(),
ledger_genesis_hash_hex: h1,
checkpoint_entry_count: 2,
checkpoint_merkle_root_hex: root,
checkpoint_head_hash_hex: head,
checkpoint_ts_ms,
ts_seen_ms,
witness_pubkey_hex: witness_pubkey,
witness_sig_hex: witness_sig,
};
let entry_cbor = serde_cbor::to_vec(&entry2).expect("encode entry");
let receipt = ReceiptV0 {
format: RECEIPT_V0_FORMAT.to_string(),
entry_cbor_b64: base64::engine::general_purpose::STANDARD_NO_PAD.encode(entry_cbor),
entry_hash_hex: hex(&h2),
read_proof: proof,
attestations: vec![CheckpointAttestation::V1(att)],
};
verify_receipt_v0(&receipt, true).expect("receipt verifies");
}
#[test]
fn receipt_v0_rejects_tampered_hash() {
let author_key = SigningKey::from_bytes(&[7u8; 32]);
let author_pubkey = author_key.verifying_key().to_bytes();
let unsigned = EntryUnsigned {
prev_hash: [0u8; 32],
ts_ms: 1,
namespace: "law".to_string(),
payload_cbor: vec![0x61, 0x41], // "A"
author_pubkey,
};
let sig = author_key.sign(&unsigned.signing_message()).to_bytes();
let entry = unsigned.to_entry(sig);
let h = entry.hash();
let hashes = vec![h];
let proof = ReadProofV0::from_tree(&hashes, 0).expect("build proof");
let entry_cbor = serde_cbor::to_vec(&entry).expect("encode entry");
let mut receipt = ReceiptV0 {
format: RECEIPT_V0_FORMAT.to_string(),
entry_cbor_b64: base64::engine::general_purpose::STANDARD_NO_PAD.encode(entry_cbor),
entry_hash_hex: "00".repeat(32),
read_proof: proof,
attestations: vec![],
};
assert!(verify_receipt_v0(&receipt, false).is_err());
receipt.entry_hash_hex = hex(&h);
assert!(verify_receipt_v0(&receipt, false).is_ok());
}