open-standards/jcp/jcp-rs/crates/jcp-logseal/tests/cose.rs
Transaction Science da872c36f5
All checks were successful
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 8m42s
Log seals: projections to a signed-note checkpoint with witness cosignatures, and to COSE Receipts
Tools built for other transparency logs could not read a sealed log. Two
projections now let them (log-seal 5.6); neither is a seal or replaces one.

- The shared tree: RFC 6962 hashed with SHA-256 over the same leaves as a
  seal's tree, each receipt's content id, so a reader holding the log
  recomputes both roots.
- jcp_logseal::note: the checkpoint text (origin, count, base64 root),
  signed notes with Ed25519 (type 0x01), signer and verifier keys, opening
  a note under known keys, verifying a checkpoint against its log, and
  timestamped Ed25519 witness cosignatures (type 0x04).
- jcp_logseal::cose: a COSE Receipt (RFC 9942) for one receipt: COSE_Sign1
  over the detached root, alg and vds RFC9162_SHA256 protected, the
  inclusion proof under vdp; the RFC 9162 inclusion algorithm to verify it.
  A COSE Receipt and a checkpoint of one count state one root.

Held to the signed-note format's published vectors (a signer key gives the
published verifier key, signing gives the published note byte for byte, a
two-signature note opens under either key, the published bad keys and the
format document's example key id), the cosignature format's worked example
of what is signed, RFC 6962's one- and two-leaf roots, and every leaf of
every tree up to forty. Seven seeded faults, each caught.

Neither projection carries the seal's ML-DSA-65 half; a reader that must
outlast Ed25519 reads the seal.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-11 18:55:01 -05:00

58 lines
3.3 KiB
Rust

//! One receipt proved as a COSE Receipt (RFC 9942, log-seal §5.6): the
//! RFC 9162 inclusion algorithm over every leaf of every small tree, and a
//! receipt that proves against the root a checkpoint of the same size states.
use ed25519_dalek::SigningKey;
use jcp_logseal::cose::{cose_receipt, sha256_path, sha256_root_from_path, verify_cose_receipt, CoseError};
use jcp_logseal::note::{checkpoint, read_checkpoint, sha256_root};
use jcp_schema::ContentId;
#[test]
fn every_leaf_of_every_small_tree_proves_and_nothing_else_does() {
let ids: Vec<ContentId> = (0..40u32).map(|i| ContentId::of_bytes(&i.to_le_bytes())).collect();
for n in 1..=ids.len() {
let root = sha256_root(&ids[..n]);
for i in 0..n {
let path = sha256_path(&ids[..n], i);
assert_eq!(sha256_root_from_path(&ids[i], i as u64, n as u64, &path), Some(root), "{i} of {n}");
if n > 1 {
assert_ne!(sha256_root_from_path(&ids[(i + 1) % n], i as u64, n as u64, &path), Some(root), "another leaf at {i} of {n}");
let mut bent = path.clone();
bent[0][0] ^= 1;
assert_ne!(sha256_root_from_path(&ids[i], i as u64, n as u64, &bent), Some(root));
assert_eq!(sha256_root_from_path(&ids[i], i as u64, n as u64, &path[..path.len() - 1]), None, "a path cut short");
}
}
assert_eq!(sha256_root_from_path(&ids[0], n as u64, n as u64, &[]), None, "an index past the tree");
}
}
#[test]
fn a_cose_receipt_proves_one_receipt_against_the_checkpoints_root() {
let entries: Vec<Vec<u8>> = (0..9).map(|i| format!("receipt {i}").into_bytes()).collect();
let key = SigningKey::from_bytes(&[7; 32]);
let receipt = cose_receipt(&entries, 9, 4, &key, b"jcp.example/steward").unwrap();
let entry = ContentId::of_bytes(&entries[4]);
let proved = verify_cose_receipt(&receipt, &entry, &key.verifying_key()).unwrap();
assert_eq!((proved[0].tree_size, proved[0].leaf_index), (9, 4));
let (_, _, checkpoint_root) = read_checkpoint(&checkpoint("jcp.example/steward", &entries, 9).unwrap()).unwrap();
assert_eq!(proved[0].root, checkpoint_root, "one root, two projections");
// another receipt, another key, a changed signature: none verifies
let other = ContentId::of_bytes(&entries[5]);
assert_eq!(verify_cose_receipt(&receipt, &other, &key.verifying_key()), Err(CoseError::BadSignature));
assert_eq!(verify_cose_receipt(&receipt, &entry, &SigningKey::from_bytes(&[8; 32]).verifying_key()), Err(CoseError::BadSignature));
let mut bent = receipt.clone();
let last = bent.len() - 1;
bent[last] ^= 1;
assert_eq!(verify_cose_receipt(&bent, &entry, &key.verifying_key()), Err(CoseError::BadSignature));
// another verifiable data structure than RFC9162_SHA256
let at = receipt.windows(4).position(|w| w == [0x19, 0x01, 0x8b, 0x01]).expect("vds in the protected header");
let mut other_vds = receipt.clone();
other_vds[at + 3] = 0x02;
assert_eq!(verify_cose_receipt(&other_vds, &entry, &key.verifying_key()), Err(CoseError::Unsupported("vds")));
let mut trailing = receipt.clone();
trailing.push(0);
assert!(matches!(verify_cose_receipt(&trailing, &entry, &key.verifying_key()), Err(CoseError::Malformed(_))));
assert_eq!(cose_receipt(&entries, 9, 9, &key, b"k"), Err(CoseError::BadProof));
}