From 655e0d51f8860ba10b904fcb9b6109880a0b9594 Mon Sep 17 00:00:00 2001 From: Grant Date: Fri, 8 May 2026 10:43:36 -0500 Subject: [PATCH] Daemon-side wire-format crosscheck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Loads tests/crosscheck/vector.json (the same file the TS, Python, and Rust SDKs each test against independently) and verifies the daemon's crypto::parse_key produces field-by-field identical values. What was missing: the SDKs each ran their crosscheck against the shared vectors, but the **daemon itself** never did. The daemon shares no parser code with the SDKs (separate trees, separate implementations of the same byte layout), so drift in the daemon's parser could ship undetected until an SDK on the wire couldn't validate a daemon-issued key. Four tests, one per fixture in vector.json (v1 legacy fingerprint- bound, v2 trial with entitlements, v2 perpetual unbound), plus a sanity check that publicKeyPem is present. Each fixture asserts: version, product_id UUID, license_id UUID, issued_at, expires_at, flags + derived `is_fingerprint_bound`/ `is_trial` getters, entitlements (order-sensitive), and the 32-byte fingerprint_hash bytes hex-encoded. When `fingerprintRaw` is provided and binding is active, hashes the raw fingerprint with crypto::hash_fingerprint and asserts the result matches the wire bytes — pinning the SHA-256 contract the SDKs depend on. Signature verification is intentionally out of scope: the unit tests in src/crypto/mod.rs already prove daemon's sign/verify roundtrip works, and the SDKs prove the same key verifies in three independent crypto implementations. The parser-to-fields contract is what hadn't been pinned from the daemon's side, and what this file enforces. Test count: 30 (9 unit + 4 migration + 10 API + 3 worker + 4 crosscheck), up from 26. --- licensing-service/tests/crosscheck.rs | 159 ++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 licensing-service/tests/crosscheck.rs diff --git a/licensing-service/tests/crosscheck.rs b/licensing-service/tests/crosscheck.rs new file mode 100644 index 0000000..ad9b54b --- /dev/null +++ b/licensing-service/tests/crosscheck.rs @@ -0,0 +1,159 @@ +//! Daemon-side wire-format crosscheck against the shared vector at +//! `tests/crosscheck/vector.json` (at the repo root, two levels up +//! from this crate). +//! +//! Each fixture in vector.json was generated by `reference_signer.py` +//! using independent Python crypto, then is consumed by: +//! - the TypeScript SDK (via tests/crosscheck/run_ts.mjs) +//! - the Python SDK (via licensing-client-python/tests/test_crosscheck.py) +//! - the Rust SDK (via its own unit tests against the same byte layouts) +//! +//! What was missing: the **daemon itself** never ran against the same +//! vectors. Drift in the daemon's parser (which uses the same byte +//! layouts but shares no code with the SDKs) could ship undetected +//! until an SDK on the wire couldn't validate a daemon-issued key. +//! +//! This file closes the loop. For every fixture we call +//! `crypto::parse_key` and assert that every parsed field matches +//! the `expected` block byte-for-byte. If a future change to the +//! daemon's parser drifts away from the wire format the SDKs +//! enforce, this fails before the build even lands in a .s9pk. +//! +//! Signature verification is intentionally NOT included here — the +//! crypto unit tests in `src/crypto/mod.rs` already prove the +//! daemon's sign+verify roundtrip works, and the SDKs prove the +//! same key verifies in three independent crypto implementations. +//! What hadn't been pinned was the parser-to-fields contract, which +//! is what this file enforces. + +use keysat::crypto; +use serde_json::Value; +use uuid::Uuid; + +const VECTOR_PATH: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../tests/crosscheck/vector.json" +); + +fn load_vector() -> Value { + let raw = std::fs::read_to_string(VECTOR_PATH).unwrap_or_else(|e| { + panic!("crosscheck vector not at {VECTOR_PATH}: {e}\n\ + If you cloned without the parent repo, make sure the \ + tests/crosscheck/ directory is present.") + }); + serde_json::from_str(&raw).expect("vector.json should be valid JSON") +} + +/// Run one fixture. Asserts every `expected` field matches the +/// daemon's `parse_key` output. The vector's field names mirror +/// what the SDKs use, which is camelCase; the daemon uses snake_case +/// for the same things internally. +fn check_fixture(name: &str, fixture: &Value) { + let key = fixture["licenseKey"] + .as_str() + .unwrap_or_else(|| panic!("{name}: missing licenseKey")); + let expected = &fixture["expected"]; + + let (payload, _signature, _signed_bytes) = crypto::parse_key(key) + .unwrap_or_else(|e| panic!("{name}: parse_key failed: {e}")); + + // Version + let want_version = expected["version"].as_u64().unwrap(); + assert_eq!( + payload.version as u64, want_version, + "{name}: version mismatch" + ); + + // UUIDs + let want_product = Uuid::parse_str(expected["productUuid"].as_str().unwrap()).unwrap(); + assert_eq!(payload.product_id, want_product, "{name}: product_id"); + + let want_license = Uuid::parse_str(expected["licenseUuid"].as_str().unwrap()).unwrap(); + assert_eq!(payload.license_id, want_license, "{name}: license_id"); + + // Times (Unix seconds) + let want_issued = expected["issuedAt"].as_i64().unwrap(); + assert_eq!(payload.issued_at, want_issued, "{name}: issued_at"); + + let want_expires = expected["expiresAt"].as_i64().unwrap(); + assert_eq!(payload.expires_at, want_expires, "{name}: expires_at"); + + // Flags + derived bits + let want_flags = expected["flags"].as_u64().unwrap(); + assert_eq!(payload.flags as u64, want_flags, "{name}: flags"); + + let want_fp_bound = expected["isFingerprintBound"].as_bool().unwrap(); + assert_eq!( + payload.is_fingerprint_bound(), + want_fp_bound, + "{name}: is_fingerprint_bound" + ); + + let want_trial = expected["isTrial"].as_bool().unwrap(); + assert_eq!(payload.is_trial(), want_trial, "{name}: is_trial"); + + // Entitlements (order-sensitive — wire format preserves order) + let want_ents: Vec = expected["entitlements"] + .as_array() + .unwrap() + .iter() + .map(|v| v.as_str().unwrap().to_string()) + .collect(); + assert_eq!(payload.entitlements, want_ents, "{name}: entitlements"); + + // Fingerprint hash. The `expected.fingerprintHashHex` is what the + // wire format must contain; we compare against payload's bytes. + let want_fp_hex = expected["fingerprintHashHex"].as_str().unwrap(); + let got_fp_hex = hex::encode(payload.fingerprint_hash); + assert_eq!(got_fp_hex, want_fp_hex, "{name}: fingerprint_hash"); + + // Cross-check: if `fingerprintRaw` is provided and binding is + // active, hashing the raw fingerprint with the daemon's helper + // should produce the same hex. This locks in the + // `hash_fingerprint` SHA-256 contract that SDKs rely on. + if want_fp_bound { + if let Some(raw) = expected["fingerprintRaw"].as_str() { + let computed = crypto::hash_fingerprint(raw); + let computed_hex = hex::encode(computed); + assert_eq!( + computed_hex, want_fp_hex, + "{name}: hash_fingerprint(raw) does not match wire bytes" + ); + } + } +} + +#[test] +fn vector_v1_legacy_roundtrip() { + let vector = load_vector(); + check_fixture("v1", &vector["v1"]); +} + +#[test] +fn vector_v2_trial_with_entitlements() { + let vector = load_vector(); + check_fixture("v2", &vector["v2"]); +} + +#[test] +fn vector_v2_perpetual_unbound() { + let vector = load_vector(); + check_fixture("v2_perpetual_unbound", &vector["v2_perpetual_unbound"]); +} + +/// The vector file's publicKeyPem is the master crosscheck key — +/// every fixture's signature was made against that PEM. This test +/// just locks in that the file has it (so a future regeneration of +/// the vectors keeps the public-key surface accessible to all +/// tooling that reads vector.json). +#[test] +fn vector_has_public_key_pem() { + let vector = load_vector(); + let pem = vector["publicKeyPem"] + .as_str() + .expect("vector.json must include publicKeyPem"); + assert!( + pem.contains("BEGIN PUBLIC KEY"), + "publicKeyPem should be a PEM block: {pem}" + ); +}