init: cryptographic append-only ledger
This commit is contained in:
147
crates/ledger-core/tests/attestation_vectors.rs
Normal file
147
crates/ledger-core/tests/attestation_vectors.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
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, CheckpointAttestationV0,
|
||||
CheckpointAttestationV1, verify_checkpoint_attestation_v0, verify_checkpoint_attestation_v1,
|
||||
};
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|b| format!("{:02x}", b)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checkpoint_attestation_v0_vectors() {
|
||||
let signing_key = SigningKey::from_bytes(&[0x44u8; 32]);
|
||||
let witness_pubkey = signing_key.verifying_key().to_bytes();
|
||||
|
||||
let genesis = [0x11u8; 32];
|
||||
let entry_count = 7u64;
|
||||
let merkle_root = [0x22u8; 32];
|
||||
let head_hash = [0x33u8; 32];
|
||||
let ts_seen_ms = 123_456_789u64;
|
||||
|
||||
let msg = checkpoint_attestation_signing_message_v0(
|
||||
&genesis,
|
||||
entry_count,
|
||||
&merkle_root,
|
||||
&head_hash,
|
||||
ts_seen_ms,
|
||||
);
|
||||
let sig = signing_key.sign(&msg).to_bytes();
|
||||
|
||||
let att = CheckpointAttestationV0 {
|
||||
format: CHECKPOINT_ATTESTATION_V0_FORMAT.to_string(),
|
||||
ledger_genesis_hash_hex: genesis,
|
||||
checkpoint_entry_count: entry_count,
|
||||
checkpoint_merkle_root_hex: merkle_root,
|
||||
checkpoint_head_hash_hex: head_hash,
|
||||
ts_seen_ms,
|
||||
witness_pubkey_hex: witness_pubkey,
|
||||
witness_sig_hex: sig,
|
||||
};
|
||||
|
||||
verify_checkpoint_attestation_v0(&att).expect("attestation verifies");
|
||||
|
||||
assert_eq!(
|
||||
hex(&witness_pubkey),
|
||||
"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48"
|
||||
);
|
||||
assert_eq!(
|
||||
hex(&msg),
|
||||
"4349565f4c45444745525f434845434b504f494e545f4154544553545f5630111111111111111111111111111111111111111111111111111111111111111107000000000000002222222222222222222222222222222222222222222222222222222222222222333333333333333333333333333333333333333333333333333333333333333315cd5b0700000000"
|
||||
);
|
||||
assert_eq!(
|
||||
hex(&sig),
|
||||
"ebc1627f5f82d848375ca819cad7a95bfd1da7c4eb4ef09478a10129aee5af2ad1096530965efc163742b4b6de11663e9c260f0015053bb6b04e6e00d306b40d"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checkpoint_attestation_v1_vectors() {
|
||||
let signing_key = SigningKey::from_bytes(&[0x44u8; 32]);
|
||||
let witness_pubkey = signing_key.verifying_key().to_bytes();
|
||||
|
||||
let genesis = [0x11u8; 32];
|
||||
let entry_count = 7u64;
|
||||
let merkle_root = [0x22u8; 32];
|
||||
let head_hash = [0x33u8; 32];
|
||||
let checkpoint_ts_ms = 111_222_333u64;
|
||||
let ts_seen_ms = 123_456_789u64;
|
||||
|
||||
let msg = checkpoint_attestation_signing_message_v1(
|
||||
&genesis,
|
||||
entry_count,
|
||||
&merkle_root,
|
||||
&head_hash,
|
||||
checkpoint_ts_ms,
|
||||
ts_seen_ms,
|
||||
);
|
||||
let sig = signing_key.sign(&msg).to_bytes();
|
||||
|
||||
let att = CheckpointAttestationV1 {
|
||||
format: CHECKPOINT_ATTESTATION_V1_FORMAT.to_string(),
|
||||
ledger_genesis_hash_hex: genesis,
|
||||
checkpoint_entry_count: entry_count,
|
||||
checkpoint_merkle_root_hex: merkle_root,
|
||||
checkpoint_head_hash_hex: head_hash,
|
||||
checkpoint_ts_ms,
|
||||
ts_seen_ms,
|
||||
witness_pubkey_hex: witness_pubkey,
|
||||
witness_sig_hex: sig,
|
||||
};
|
||||
|
||||
verify_checkpoint_attestation_v1(&att).expect("attestation verifies");
|
||||
|
||||
assert_eq!(
|
||||
hex(&witness_pubkey),
|
||||
"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48"
|
||||
);
|
||||
assert_eq!(
|
||||
hex(&msg),
|
||||
"4349565f4c45444745525f434845434b504f494e545f4154544553545f563111111111111111111111111111111111111111111111111111111111111111110700000000000000222222222222222222222222222222222222222222222222222222222222222233333333333333333333333333333333333333333333333333333333333333333d1ea1060000000015cd5b0700000000"
|
||||
);
|
||||
assert_eq!(
|
||||
hex(&sig),
|
||||
"c682053071a6bcf666eb9561dbdb48a9b6e886e4f2e86a540793fbf8b38b165031e61dbbd8788ee75ec56148148a396ea310e6f765d492f060c68d4c68875e09"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checkpoint_attestation_v1_rejects_seen_before_checkpoint() {
|
||||
let signing_key = SigningKey::from_bytes(&[0x44u8; 32]);
|
||||
let witness_pubkey = signing_key.verifying_key().to_bytes();
|
||||
|
||||
let genesis = [0x11u8; 32];
|
||||
let entry_count = 7u64;
|
||||
let merkle_root = [0x22u8; 32];
|
||||
let head_hash = [0x33u8; 32];
|
||||
let checkpoint_ts_ms = 200u64;
|
||||
let ts_seen_ms = 100u64;
|
||||
|
||||
let msg = checkpoint_attestation_signing_message_v1(
|
||||
&genesis,
|
||||
entry_count,
|
||||
&merkle_root,
|
||||
&head_hash,
|
||||
checkpoint_ts_ms,
|
||||
ts_seen_ms,
|
||||
);
|
||||
let sig = signing_key.sign(&msg).to_bytes();
|
||||
|
||||
let att = CheckpointAttestationV1 {
|
||||
format: CHECKPOINT_ATTESTATION_V1_FORMAT.to_string(),
|
||||
ledger_genesis_hash_hex: genesis,
|
||||
checkpoint_entry_count: entry_count,
|
||||
checkpoint_merkle_root_hex: merkle_root,
|
||||
checkpoint_head_hash_hex: head_hash,
|
||||
checkpoint_ts_ms,
|
||||
ts_seen_ms,
|
||||
witness_pubkey_hex: witness_pubkey,
|
||||
witness_sig_hex: sig,
|
||||
};
|
||||
|
||||
assert!(verify_checkpoint_attestation_v1(&att).is_err());
|
||||
}
|
||||
28
crates/ledger-core/tests/entry_cbor_roundtrip.rs
Normal file
28
crates/ledger-core/tests/entry_cbor_roundtrip.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
||||
use ledger_core::EntryUnsigned;
|
||||
|
||||
#[test]
|
||||
fn entry_cbor_roundtrips_with_sig_bytes() {
|
||||
let signing_key = SigningKey::from_bytes(&[7u8; 32]);
|
||||
let author_pubkey = signing_key.verifying_key().to_bytes();
|
||||
|
||||
let unsigned = EntryUnsigned {
|
||||
prev_hash: [0u8; 32],
|
||||
ts_ms: 42,
|
||||
namespace: "law".to_string(),
|
||||
payload_cbor: vec![0x65, b'h', b'e', b'l', b'l', b'o'],
|
||||
author_pubkey,
|
||||
};
|
||||
let sig = signing_key.sign(&unsigned.signing_message()).to_bytes();
|
||||
let entry = unsigned.to_entry(sig);
|
||||
|
||||
let bytes = serde_cbor::to_vec(&entry).expect("serialize entry");
|
||||
let decoded: ledger_core::Entry = serde_cbor::from_slice(&bytes).expect("deserialize entry");
|
||||
|
||||
assert_eq!(decoded.hash(), entry.hash());
|
||||
assert_eq!(decoded.sig, entry.sig);
|
||||
assert_eq!(decoded.namespace, entry.namespace);
|
||||
assert_eq!(decoded.payload_cbor, entry.payload_cbor);
|
||||
assert_eq!(decoded.author_pubkey, entry.author_pubkey);
|
||||
}
|
||||
47
crates/ledger-core/tests/golden_vectors.rs
Normal file
47
crates/ledger-core/tests/golden_vectors.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
||||
use ledger_core::EntryUnsigned;
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|b| format!("{:02x}", b)).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn golden_vectors_v0_signing_message_and_hashes() {
|
||||
let seed = [0u8; 32];
|
||||
let signing_key = SigningKey::from_bytes(&seed);
|
||||
let author_pubkey = signing_key.verifying_key().to_bytes();
|
||||
|
||||
let unsigned = EntryUnsigned {
|
||||
prev_hash: [0u8; 32],
|
||||
ts_ms: 1,
|
||||
namespace: "law".to_string(),
|
||||
payload_cbor: vec![0x65, b'h', b'e', b'l', b'l', b'o'],
|
||||
author_pubkey,
|
||||
};
|
||||
|
||||
let msg = unsigned.signing_message();
|
||||
let sig = signing_key.sign(&msg).to_bytes();
|
||||
let entry = unsigned.to_entry(sig);
|
||||
|
||||
assert_eq!(
|
||||
hex(&author_pubkey),
|
||||
"3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29"
|
||||
);
|
||||
assert_eq!(
|
||||
hex(&entry.payload_hash()),
|
||||
"90eeb71f0d4b768a5d449e30035beb7ffccd75d228e5b38e8e9cbfaa01ddfae9"
|
||||
);
|
||||
assert_eq!(
|
||||
hex(&msg),
|
||||
"434c763000000000000000000000000000000000000000000000000000000000000000000100000000000000030000006c617790eeb71f0d4b768a5d449e30035beb7ffccd75d228e5b38e8e9cbfaa01ddfae93b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29"
|
||||
);
|
||||
assert_eq!(
|
||||
hex(&entry.sig),
|
||||
"03c91ecb5ab44a4329c12c1e789d22d685b81a0d16483d869d88fa24dd2c20a8b786066d12c8a238c88cea0b969f1c707df305b1edc56238d5d6166ecf986e05"
|
||||
);
|
||||
assert_eq!(
|
||||
hex(&entry.hash()),
|
||||
"ce13e5272d6705d9cbf2e261d2b9862abb187f737bea6fe3542e8680d689cc8e"
|
||||
);
|
||||
}
|
||||
29
crates/ledger-core/tests/read_proof.rs
Normal file
29
crates/ledger-core/tests/read_proof.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use ledger_core::{ReadProofV0, verify_read_proof_v0};
|
||||
|
||||
fn leaf(i: u64) -> [u8; 32] {
|
||||
blake3::hash(format!("leaf-{}", i).as_bytes()).into()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_proof_verifies_for_all_leaves() {
|
||||
for n in 1..=33usize {
|
||||
let leaves: Vec<[u8; 32]> = (0..n as u64).map(leaf).collect();
|
||||
for idx in 0..n {
|
||||
let proof = ReadProofV0::from_tree(&leaves, idx).expect("build proof");
|
||||
verify_read_proof_v0(&proof).expect("verify proof");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_proof_rejects_tampering() {
|
||||
let leaves: Vec<[u8; 32]> = (0..8u64).map(leaf).collect();
|
||||
let mut proof = ReadProofV0::from_tree(&leaves, 3).expect("build proof");
|
||||
assert!(verify_read_proof_v0(&proof).is_ok());
|
||||
|
||||
// Flip one nibble in the first sibling hash.
|
||||
let mut s = proof.path[0].sibling_hash_hex.clone();
|
||||
s.replace_range(0..1, if &s[0..1] == "0" { "1" } else { "0" });
|
||||
proof.path[0].sibling_hash_hex = s;
|
||||
assert!(verify_read_proof_v0(&proof).is_err());
|
||||
}
|
||||
177
crates/ledger-core/tests/receipt.rs
Normal file
177
crates/ledger-core/tests/receipt.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
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());
|
||||
}
|
||||
Reference in New Issue
Block a user