-
Notifications
You must be signed in to change notification settings - Fork 3
test(core): golden wire-format fixtures pin VERSION_V3 in-repo #243
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| Golden VERSION_V3 wire-format fixtures (EPIC issue #211): tiny ciphertexts in | ||
| both containers (streaming + in-memory), sealed by the published | ||
| `@e4a/pg-wasm@0.6.1`, committed with the verifying key and user secret key | ||
| needed to unseal them forever. `pg-core/tests/wire_format.rs` asserts they | ||
| still parse and decrypt — so a change to `consts.rs` (PRELUDE/VERSION_V3), | ||
| `bincode_compat`, or the header layout fails `cargo test -p pg-core` | ||
| immediately, in this repo, before it can reach clients. | ||
|
|
||
| Generated by the e2e harness (`postguard-e2e` fixtures generator); the same | ||
| sets are verified there against published clients. To regenerate after a | ||
| DELIBERATE format change, see postguard-e2e `fixtures/README.md` — and keep | ||
| the old set, adding a new one: old ciphertexts don't disappear from inboxes. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| { | ||
| "con": [ | ||
| { | ||
| "t": "irma-demo.RU.studentCard.level", | ||
| "v": "PhD" | ||
| } | ||
| ], | ||
| "files": { | ||
| "mem.bin": "in-memory container (seal / pg-js toBytes)", | ||
| "stream.bin": "streaming container (sealStream / cryptify / pg-js upload)" | ||
| }, | ||
| "generatedAt": "2026-07-16T13:03:47Z", | ||
| "plaintextB64": "cG9zdGd1YXJkLWUyZSBjcnlwdG8tY29tcGF0IGZpeHR1cmU6IG9sZCBjaXBoZXJ0ZXh0cyBtdXN0IGRlY3J5cHQgZm9yZXZlcg==", | ||
| "recipientId": "phd-holder", | ||
| "sealedBy": "@e4a/pg-wasm@0.6.1", | ||
| "senderEmail": "sender@golden.test", | ||
| "ts": 1784207026 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {"status":"DONE","proofStatus":"VALID","key":"j/xqfDoVQlHcMjeejZv2k6WG/YbOqthksquli6oe9AYZqa4EslnYva1AYilab3RJFODq62ecYjCIXVdN+jqaAvsj27jxuh63OlAP93QWXV6KD78hGO6O0Piomnd4v/d5iCbI0xmu1ItIOJ0VgYOmI2UPfzz4npFM4Ts7LK5ZMzsBaj5VDMLMlXXml5fbsiQ7FcPiZciD0RjcprZwvX+WqGd0OQF0j0OGaW5yaXCqlwGoFagHhpVb7gX/aGcl0kBrt255f+eOAkh3ANdW4HiziaIz+Grpih5ovSKIpilc6WVof/xTbzMdleKO3WBL58OPF5ONSbZhVA1+70ldpYWW7QNKtoOJKzAkgk++nfUO8GNWHh4DmI1tCaLPYIIRLlbBitIeFcRJKZ8uGsRn8nNJOHKal/Lsh/23UtklHWwfIfAf9nu6pLyq/VitpncRXaeZE+/hJTenpkocs8Y/on8P1IrLci1B78myDWamW82yzlc67+rLfwgHu4xt2rA103WKrS2w0JdpP3Z3WDIOKQcyToyYr7C6HFr7BGRTRk5PlEhwoPd9B/n4Z3w7KhjePdI+D7STsy9ECYtDZ7+tbexe1p7LedJOSlXZY87mUk9dR67WiwQNZoywTWjsgUK8Dq/MsWeiBXsfuqMNhbffQAMdGvdodHFMcm2TehTXyaRL7VvOA9RD169pL4oo00Pah4egFX8Sx/wcsQckEXDVbbhB9AyHsWJWXZpUkz0QfS1GTsFhRpib6Twq2aEwLNw7uNvv"} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {"formatVersion":0,"publicKey":"rNxi2LLU7MyaEBcR60cE+aNGgEg1juZnDnSGaWZrMl4="} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| //! Wire-format tripwire (EPIC issue #211). | ||
| //! | ||
| //! Pins the VERSION_V3 byte format against golden fixtures committed in | ||
| //! `testdata/wire-format-v3/` (sealed once by the published `@e4a/pg-wasm`, | ||
| //! with the key material to unseal them). If a change to `consts.rs`, the | ||
| //! bincode framing, or the header layout breaks these tests, every ciphertext | ||
| //! already in the wild — old encrypted emails and files — breaks identically. | ||
| //! That must be a deliberate, versioned decision: regenerate fixtures for the | ||
| //! NEW version (see the testdata README) and keep the old set decodable. | ||
| //! | ||
| //! Gated on the default `rust` feature so a plain `cargo test -p pg-core` | ||
| //! runs the tripwires; only the streaming-container test additionally needs | ||
| //! the `stream` feature (CI runs with `test,rust,stream`, covering all four). | ||
| #![cfg(feature = "rust")] | ||
|
|
||
| use std::fs; | ||
| use std::path::PathBuf; | ||
|
|
||
| use pg_core::api::Parameters; | ||
| use pg_core::artifacts::{UserSecretKey, VerifyingKey}; | ||
| use pg_core::client::rust::UnsealerMemoryConfig; | ||
| use pg_core::client::Unsealer; | ||
| use pg_core::consts::{PREAMBLE_SIZE, PRELUDE, PRELUDE_SIZE, VERSION_SIZE, VERSION_V3}; | ||
| use pg_core::kem::cgw_kv::CGWKV; | ||
|
|
||
| use serde::Deserialize; | ||
|
|
||
| fn testdata(name: &str) -> Vec<u8> { | ||
| let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) | ||
| .join("testdata/wire-format-v3") | ||
| .join(name); | ||
| fs::read(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display())) | ||
| } | ||
|
|
||
| #[derive(Deserialize)] | ||
| struct Meta { | ||
| #[serde(rename = "recipientId")] | ||
| recipient_id: String, | ||
| #[serde(rename = "plaintextB64")] | ||
| plaintext_b64: String, | ||
| } | ||
|
|
||
| struct Fixture { | ||
| meta: Meta, | ||
| vk: VerifyingKey, | ||
| usk: UserSecretKey<CGWKV>, | ||
| want: Vec<u8>, | ||
| } | ||
|
|
||
| fn fixture() -> Fixture { | ||
| #[derive(Deserialize)] | ||
| struct UskResponse { | ||
| key: UserSecretKey<CGWKV>, | ||
| } | ||
|
|
||
| let meta: Meta = serde_json::from_slice(&testdata("meta.json")).expect("parse meta.json"); | ||
| let vk: Parameters<VerifyingKey> = | ||
| serde_json::from_slice(&testdata("vk.json")).expect("parse vk.json"); | ||
| let usk: UskResponse = serde_json::from_slice(&testdata("usk.json")).expect("parse usk.json"); | ||
| let want = b64(&meta.plaintext_b64); | ||
|
|
||
| Fixture { | ||
| meta, | ||
| vk: vk.public_key, | ||
| usk: usk.key, | ||
| want, | ||
| } | ||
| } | ||
|
|
||
| /// The exact byte values of the wire constants. These are literals on | ||
| /// purpose: if someone edits `consts.rs`, this must fail — the constants ARE | ||
| /// the compatibility contract with every ciphertext ever produced. | ||
| #[test] | ||
| fn wire_constants_are_pinned() { | ||
| assert_eq!(PRELUDE, [0x14, 0x8A, 0x8E, 0xA7], "PRELUDE bytes changed"); | ||
| assert_eq!(VERSION_V3, 2, "VERSION_V3 changed"); | ||
| assert_eq!(PREAMBLE_SIZE, 10, "preamble layout changed"); | ||
| } | ||
|
|
||
| /// Both golden containers begin with the documented 10-byte preamble: | ||
| /// PRELUDE (4) || VERSION_V3 big-endian (2) || header length (4). | ||
| #[test] | ||
| fn golden_fixtures_carry_the_pinned_preamble() { | ||
| for name in ["stream.bin", "mem.bin"] { | ||
| let ct = testdata(name); | ||
| assert!(ct.len() > PREAMBLE_SIZE, "{name}: too short"); | ||
| assert_eq!(&ct[..PRELUDE_SIZE], &PRELUDE, "{name}: prelude mismatch"); | ||
| let version = u16::from_be_bytes( | ||
| ct[PRELUDE_SIZE..PRELUDE_SIZE + VERSION_SIZE] | ||
| .try_into() | ||
| .unwrap(), | ||
| ); | ||
| assert_eq!(version, VERSION_V3, "{name}: version mismatch"); | ||
| } | ||
| } | ||
|
|
||
| /// The in-memory golden container must keep parsing (bincode header framing) | ||
| /// and decrypting (KEM + AEAD + signature verification) forever. | ||
| #[test] | ||
| fn golden_mem_fixture_unseals() { | ||
| let f = fixture(); | ||
| let unsealer = Unsealer::<Vec<u8>, UnsealerMemoryConfig>::new(testdata("mem.bin"), &f.vk) | ||
| .expect("parse the golden in-memory container"); | ||
| let (plain, _verified) = unsealer | ||
| .unseal(&f.meta.recipient_id, &f.usk) | ||
| .expect("unseal the golden in-memory container"); | ||
| assert_eq!(plain, f.want, "decrypted plaintext changed"); | ||
| } | ||
|
|
||
| /// The streaming golden container (what cryptify stores and clients upload) | ||
| /// must keep parsing and decrypting forever. | ||
| #[test] | ||
| #[cfg(feature = "stream")] | ||
| fn golden_stream_fixture_unseals() { | ||
| use pg_core::client::rust::stream::UnsealerStreamConfig; | ||
|
|
||
| let f = fixture(); | ||
| let ct = testdata("stream.bin"); | ||
|
|
||
| let mut plain = Vec::new(); | ||
| futures::executor::block_on(async { | ||
| let mut reader = futures::io::Cursor::new(&ct); | ||
| let unsealer = Unsealer::<_, UnsealerStreamConfig>::new(&mut reader, &f.vk) | ||
| .await | ||
| .expect("parse the golden streaming container"); | ||
| unsealer | ||
| .unseal(&f.meta.recipient_id, &f.usk, &mut plain) | ||
| .await | ||
| .expect("unseal the golden streaming container"); | ||
| }); | ||
| assert_eq!(plain, f.want, "decrypted plaintext changed"); | ||
| } | ||
|
|
||
| /// Minimal std-only base64 decoder (standard alphabet, padded) — avoids a | ||
| /// dev-dependency for one meta field. | ||
| fn b64(s: &str) -> Vec<u8> { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Small accuracy nit, non-blocking: That said, the swap is not entirely free: |
||
| const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; | ||
| let mut out = Vec::with_capacity(s.len() / 4 * 3); | ||
| let mut buf: u32 = 0; | ||
| let mut bits = 0; | ||
| for &c in s.as_bytes() { | ||
| if c == b'=' { | ||
| break; | ||
| } | ||
| let v = ALPHABET | ||
| .iter() | ||
| .position(|&a| a == c) | ||
| .unwrap_or_else(|| panic!("invalid base64 byte {c}")) as u32; | ||
| buf = (buf << 6) | v; | ||
| bits += 6; | ||
| if bits >= 8 { | ||
| bits -= 8; | ||
| out.push((buf >> bits) as u8); | ||
| } | ||
| } | ||
| out | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The file-level gate requires the
streamfeature, but onlygolden_stream_fixture_unsealsneeds it —wire_constants_are_pinnedneeds no feature andgolden_mem_fixture_unsealsneeds onlyrust(the default). Sincestreamis not a default feature (default = ["rust"]), plaincargo test -p pg-corerunsrunning 0 testshere, so the README/module-doc claim that a format change "failscargo test -p pg-coreimmediately" is only true with the stream feature.Two ways to make the claim hold: gate the file on
#![cfg(feature = "rust")]and put#[cfg(feature = "stream")]on just the stream test — or, if the stream-feature requirement is intended, tweak the README/doc wording to say the tripwire needs--features stream. Non-blocking.