open-standards/jcp/jcp-rs/crates/jcp-logseal/tests/note.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

111 lines
6.6 KiB
Rust

//! A sealed log as a signed-note checkpoint (log-seal §5.6), held to the
//! signed-note format's published vectors.
use ed25519_dalek::SigningKey;
use jcp_logseal::note::{
checkpoint, cosign_checkpoint, cosignatures, cosigned_message, key_id, open_note, read_checkpoint, sha256_root, sign_note,
verify_checkpoint, NoteError, NoteSigner, NoteVerifier, TYPE_ED25519,
};
use jcp_schema::ContentId;
const TEXT: &str = "If you think cryptography is the answer to your problem,\nthen you don't know what your problem is.\n";
const PETER_SKEY: &str = "PRIVATE+KEY+PeterNeumann+c74f20a3+AYEKFALVFGyNhPJEMzD1QIDr+Y7hfZx09iUvxdXHKDFz";
const PETER_VKEY: &str = "PeterNeumann+c74f20a3+ARpc2QcUPDhMQegwxbzhKqiBfsVkmqq/LDE4izWy10TW";
const ENOCH_VKEY: &str = "EnochRoot+af0cfe78+ATtqJ7zOtqQtYqOo0CpvDXNlMhV3HeJDpjrASKGLWdop";
const PETER_SIG: &str = "\u{2014} PeterNeumann x08go/ZJkuBS9UG/SffcvIAQxVBtiFupLLr8pAcElZInNIuGUgYN1FFYC2pZSNXgKvqfqdngotpRZb6KE6RyyBwJnAM=\n";
const ENOCH_SIG: &str = "\u{2014} EnochRoot rwz+eBzmZa0SO3NbfRGzPCpDckykFXSdeX+MNtCOXm2/5n2tiOHp+vAF1aGrQ5ovTG01oOTGwnWLox33WWd1RvMc+QQ=\n";
/// The format's own vectors: a signer key gives its verifier key, signing
/// gives the published note byte for byte, and a note with two signatures
/// opens under either key.
#[test]
fn the_published_note_vectors_hold() {
let peter = NoteSigner::from_skey(PETER_SKEY).unwrap();
assert_eq!(peter.vkey(), PETER_VKEY);
assert_eq!(sign_note(TEXT, &[&peter]).unwrap(), format!("{TEXT}\n{PETER_SIG}"));
let both = format!("{TEXT}\n{PETER_SIG}{ENOCH_SIG}");
let keys = [NoteVerifier::from_vkey(PETER_VKEY).unwrap(), NoteVerifier::from_vkey(ENOCH_VKEY).unwrap()];
let (text, names) = open_note(&both, &keys).unwrap();
assert_eq!((text, names), (TEXT, vec!["PeterNeumann".to_owned(), "EnochRoot".to_owned()]));
// a key known by name only, or not at all, is passed over
assert_eq!(open_note(&both, &keys[1..]).unwrap().1, vec!["EnochRoot".to_owned()]);
// the published bad keys: the wrong length, and an unknown algorithm
assert!(NoteVerifier::from_vkey("PeterNeumann+cc469956+ARpc2QcUPDhMQegwxbzhKqiBfsVkmqq/LDE4izWy10TWBADKEY==").is_err());
assert!(NoteVerifier::from_vkey("PeterNeumann+173116ae+ZRpc2QcUPDhMQegwxbzhKqiBfsVkmqq/LDE4izWy10TW").is_err());
// the example key of the format's own document
let example = NoteVerifier::from_vkey("example.com/foo+530d903a+AekyeRrm56hApGFkyQR4ZCbV54Id2LKaANYcrnKv3U2k").unwrap();
assert_eq!(key_id(&example.name, TYPE_ED25519, &example.key), 0x530d_903a);
}
#[test]
fn a_known_key_that_does_not_verify_refuses_the_note() {
let bent = format!("{}\n{PETER_SIG}", TEXT.replace("answer", "question"));
let keys = [NoteVerifier::from_vkey(PETER_VKEY).unwrap()];
assert_eq!(open_note(&bent, &keys), Err(NoteError::BadSignature("PeterNeumann".into())));
assert_eq!(open_note(&format!("{TEXT}\n{ENOCH_SIG}"), &keys), Err(NoteError::Unverified));
assert!(matches!(sign_note("no newline", &[]), Err(NoteError::BadText(_))));
}
fn ids(n: u32) -> Vec<Vec<u8>> {
(0..n).map(|i| format!("receipt {i}").into_bytes()).collect()
}
/// The checkpoint's root is RFC 6962's: a lone leaf is `SHA-256(0x00 ‖ id)`.
#[test]
fn a_checkpoint_states_the_rfc_6962_root_of_the_receipts() {
use sha2::{Digest, Sha256};
let one = ContentId::of_bytes(b"receipt 0");
let want: [u8; 32] = Sha256::new().chain_update([0u8]).chain_update(one.0).finalize().into();
assert_eq!(sha256_root(&[one]), want);
let two = [one, ContentId::of_bytes(b"receipt 1")];
let leaf = |c: &ContentId| -> [u8; 32] { Sha256::new().chain_update([0u8]).chain_update(c.0).finalize().into() };
let node: [u8; 32] = Sha256::new().chain_update([1u8]).chain_update(leaf(&two[0])).chain_update(leaf(&two[1])).finalize().into();
assert_eq!(sha256_root(&two), node);
let log = ids(13);
let steward = NoteSigner::new("jcp.example/steward", SigningKey::from_bytes(&[7; 32])).unwrap();
let text = checkpoint("jcp.example/steward", &log, 13).unwrap();
let (origin, count, _) = read_checkpoint(&text).unwrap();
assert_eq!((origin.as_str(), count), ("jcp.example/steward", 13));
let note = sign_note(&text, &[&steward]).unwrap();
let vk = NoteVerifier::from_vkey(&steward.vkey()).unwrap();
assert_eq!(verify_checkpoint(&note, &vk, "jcp.example/steward", &log), Ok(13));
// another history of the same length does not reach the stated root
let other: Vec<Vec<u8>> = (0..13).map(|i| format!("forged {i}").into_bytes()).collect();
assert_eq!(verify_checkpoint(&note, &vk, "jcp.example/steward", &other), Err(NoteError::BadCheckpoint("root")));
assert!(verify_checkpoint(&note, &vk, "jcp.example/other", &log).is_err());
assert!(checkpoint("has space", &log, 1).is_err());
assert!(read_checkpoint("o\n013\nAAAA\n").is_err(), "a count with a leading zero");
}
/// A witness's timestamped cosignature (type 0x04) over the checkpoint.
#[test]
fn a_witness_cosigns_a_checkpoint_with_its_time() {
let log = ids(5);
let steward = NoteSigner::new("jcp.example/steward", SigningKey::from_bytes(&[7; 32])).unwrap();
let witness = NoteSigner::cosigner("witness.example/one", SigningKey::from_bytes(&[8; 32])).unwrap();
let text = checkpoint("jcp.example/steward", &log, 5).unwrap();
let mut note = sign_note(&text, &[&steward]).unwrap();
note.push_str(&cosign_checkpoint(&text, &witness, 1_679_315_147).unwrap());
let wk = NoteVerifier::from_vkey(&witness.vkey()).unwrap();
assert_eq!(cosignatures(&note, std::slice::from_ref(&wk)).unwrap(), vec![("witness.example/one".to_owned(), 1_679_315_147)]);
// the steward's signature still opens the note beside it
assert!(verify_checkpoint(&note, &NoteVerifier::from_vkey(&steward.vkey()).unwrap(), "jcp.example/steward", &log).is_ok());
// a cosignature moved to another checkpoint does not verify
let other = sign_note(&checkpoint("jcp.example/steward", &log, 4).unwrap(), &[&steward]).unwrap();
let moved = format!("{other}{}", cosign_checkpoint(&text, &witness, 1_679_315_147).unwrap());
assert!(matches!(cosignatures(&moved, std::slice::from_ref(&wk)), Err(NoteError::BadSignature(_))));
// a note signer is not a cosigner
assert!(cosign_checkpoint(&text, &steward, 1).is_err());
}
/// The cosignature format's own worked example of what is signed.
#[test]
fn the_cosigned_message_is_the_formats_example() {
let checkpoint = "example.com/behind-the-sofa\n20852163\nCsUYapGGPo4dkMgIAUqom/Xajj7h2fB2MPA3j2jxq2I=\n";
assert_eq!(
cosigned_message(checkpoint, 1_679_315_147),
"cosignature/v1\ntime 1679315147\nexample.com/behind-the-sofa\n20852163\nCsUYapGGPo4dkMgIAUqom/Xajj7h2fB2MPA3j2jxq2I=\n"
);
}