//! 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> { (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(¬e, &vk, "jcp.example/steward", &log), Ok(13)); // another history of the same length does not reach the stated root let other: Vec> = (0..13).map(|i| format!("forged {i}").into_bytes()).collect(); assert_eq!(verify_checkpoint(¬e, &vk, "jcp.example/steward", &other), Err(NoteError::BadCheckpoint("root"))); assert!(verify_checkpoint(¬e, &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(¬e, 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(¬e, &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" ); }