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()); }