//! Consume the published `map` conformance pack. //! //! The harness in `main.rs` runs its suites against in-process fixtures, and CI now //! runs that harness — but nothing reads `map/conformance/v1/*.json`, the files a //! third-party implementation is told to certify against. So the published bytes //! could drift from the implementation with nothing to notice. //! //! Inputs come from the pack and recorded outputs are recomputed, not re-read: //! a cell key is rebuilt from its scalar fields and re-addressed, a directory is //! deserialized and its Merkle root recomputed, signatures are checked, and the //! sealed receipt is re-derived and compared byte for byte. //! //! Note `gen-vectors` writes to a hardcoded path with no output argument, so it //! cannot be run to diff against a scratch copy without overwriting the published //! pack. This test does that comparison in-process instead. use ed25519_dalek::SigningKey; use map_receipt::{Energy, Receipt}; use map_schema::{ AltBand, AltSchedule, CellKey, ContentId, DirEntry, Directory, Lod, Manifest, ManifestBody, Revision, RevisionBody, S2CellId, Tier, TimeBucket, canonical_cbor, verify_chain, }; use serde_json::Value; use std::path::PathBuf; /// The fixed publisher key the pack is generated under. const PUBLISHER_SEED: [u8; 32] = [7u8; 32]; fn pack(name: &str) -> Value { let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../../conformance/v1") .canonicalize() .expect("the conformance pack must exist next to the spec") .join(name); serde_json::from_slice(&std::fs::read(&p).unwrap_or_else(|e| panic!("read {name}: {e}"))) .unwrap_or_else(|e| panic!("{name} must parse: {e}")) } /// Every published cell key re-addresses to its recorded canonical CBOR and content /// id, rebuilt from the scalar fields rather than from a fixture. #[test] fn published_cell_keys_re_address_to_their_recorded_ids() { let doc = pack("cell_key_vectors.json"); let cells = doc["cells"].as_array().expect("pack lists cells"); assert!(cells.len() >= 3, "a wrong path must not look like a pass"); for (i, c) in cells.iter().enumerate() { let key = CellKey { s2: S2CellId(c["s2"].as_u64().unwrap()), alt: AltBand(c["alt"].as_i64().unwrap() as _), time: TimeBucket { start_tai_ms: c["time"]["start_tai_ms"].as_i64().unwrap() as _, width_ms: c["time"]["width_ms"].as_u64().unwrap() as _, }, lod: Lod(c["lod"].as_u64().unwrap() as u8), }; assert_eq!( hex::encode(canonical_cbor(&key).unwrap()), c["canonical_cbor_hex"].as_str().unwrap(), "cell {i}: canonical CBOR differs from the published bytes" ); assert_eq!( ContentId::of(&key).unwrap().to_hex(), c["content_id"].as_str().unwrap(), "cell {i}: content id differs from the published id" ); } } /// The tileset the pack publishes, rebuilt exactly as `gen-vectors` builds it. /// /// Rebuilding is the stronger comparison — it holds the implementation to the /// published bytes rather than merely reading them back. Reading them back is /// checked too, in `the_published_tileset_round_trips`. fn rebuilt_tileset() -> (Directory, Manifest, Revision) { let sk = SigningKey::from_bytes(&PUBLISHER_SEED); let cell = |s2: u64, lod: u8| CellKey { s2: S2CellId(s2), alt: AltBand(0), time: TimeBucket::ATEMPORAL, lod: Lod(lod), }; let directory = Directory::new(vec![ DirEntry { key: cell(0x10, 0), offset: 0, len: 128, content_id: ContentId::of_bytes(b"a") }, DirEntry { key: cell(0x20, 0), offset: 128, len: 64, content_id: ContentId::of_bytes(b"b") }, DirEntry { key: cell(0x30, 0), offset: 256, len: 32, content_id: ContentId::of_bytes(b"c") }, ]); let manifest = ManifestBody { spec_version: "0.1.0".into(), datum: "WGS84".into(), alt_schedule: AltSchedule::default(), dir_root: directory.root().unwrap(), bounds: Some((cell(0x10, 0), cell(0x30, 0))), disclaimer: None, publisher: sk.verifying_key(), } .sign(&sk) .unwrap(); let revision = RevisionBody { prev_root: manifest.root().unwrap(), new_root: ContentId::of_bytes(b"next-cycle-root"), timestamp_tai_ms: 1, publisher: sk.verifying_key(), } .sign(&sk) .unwrap(); (directory, manifest, revision) } /// The implementation still produces exactly the published tileset, its Merkle root /// recomputes, both signatures verify, and the revision chains onto the manifest — /// which is what makes the tileset updatable rather than merely signed. #[test] fn published_tileset_still_matches_and_its_signatures_verify() { let doc = pack("tileset_vectors.json"); let (directory, manifest, revision) = rebuilt_tileset(); assert_eq!(serde_json::to_value(&directory).unwrap(), doc["directory"], "directory has drifted"); assert_eq!(serde_json::to_value(&manifest).unwrap(), doc["manifest"], "manifest has drifted"); assert_eq!(serde_json::to_value(&revision).unwrap(), doc["revision"], "revision has drifted"); assert_eq!(directory.root().unwrap().to_hex(), doc["dir_root"].as_str().unwrap()); manifest.verify().expect("the published manifest must verify"); revision.verify().expect("the published revision must verify"); assert_eq!(manifest.body.dir_root, directory.root().unwrap()); assert_eq!(revision.body.prev_root, manifest.root().unwrap()); let tip = verify_chain(manifest.root().unwrap(), std::slice::from_ref(&revision)) .expect("the one-link revision chain must verify"); assert_eq!(tip, revision.body.new_root); } /// The checks above are not vacuous. /// /// Note what the root does *not* commit to: `root()` is documented as the Merkle /// root over the sorted `(key, content_id)` leaves, so editing an entry's `offset` /// or `len` leaves it unchanged by design. That is sound — the `content_id` /// authenticates whatever bytes a range fetch returns, so a moved range fails at the /// content check rather than at the root. The tamper below therefore edits a /// `content_id`, which the root does commit to. #[test] fn tampering_with_committed_fields_is_rejected() { let (mut directory, mut manifest, revision) = rebuilt_tileset(); let published_root = directory.root().unwrap(); directory.entries[0].content_id = ContentId::of_bytes(b"swapped tile"); assert_ne!(directory.root().unwrap(), published_root, "a swapped tile left the root unchanged"); manifest.body.datum = "NOT-WGS84".into(); assert!(manifest.verify().is_err(), "a rewritten manifest body still verified"); let wrong_root = ContentId::of_bytes(b"not the manifest root"); assert!( verify_chain(wrong_root, std::slice::from_ref(&revision)).is_err(), "the revision chained onto a root it does not follow" ); } /// The published tileset reads back into the reference implementation's own types — /// what a third party certifying against the pack does first — and what it reads /// back verifies, re-serializes to the published JSON, and has the rebuilt roots. /// /// `AltSchedule::default()`, the schedule a 2.5D surface-only tileset uses and so the /// common case, has `width_m = +∞`. JSON has no infinity, so the pack writes it as /// `null`, and the schema reads `null` back as `+∞`. Until it did, the manifest could /// not be read back at all (`invalid type: null, expected f64`). The signing form is /// CBOR, which carries the float, so no signature or content id moved. #[test] fn the_published_tileset_round_trips() { let doc = pack("tileset_vectors.json"); let (directory, rebuilt, _) = rebuilt_tileset(); assert!( doc["manifest"]["body"]["alt_schedule"]["width_m"].is_null(), "an unbounded band width is published as null" ); let manifest: Manifest = serde_json::from_value(doc["manifest"].clone()) .expect("the published manifest must read back"); assert_eq!(manifest.body.alt_schedule.width_m, f64::INFINITY, "null must read back as +inf"); manifest.verify().expect("the manifest read from JSON must verify: its signed form is unchanged"); assert_eq!(serde_json::to_value(&manifest).unwrap(), doc["manifest"], "it must re-serialize to the pack"); assert_eq!(manifest, rebuilt, "it must be the manifest the implementation builds"); assert_eq!(manifest.root().unwrap(), rebuilt.root().unwrap()); let read_directory: Directory = serde_json::from_value(doc["directory"].clone()) .expect("the published directory must read back"); assert_eq!(read_directory.root().unwrap(), directory.root().unwrap()); let revision: Revision = serde_json::from_value(doc["revision"].clone()) .expect("the published revision must read back"); revision.verify().expect("and verify"); assert_eq!(revision.body.prev_root, manifest.root().unwrap(), "and chain onto the manifest read back"); } /// Drift: the reference implementation still seals exactly the published receipt. /// /// This is the check that caught stale vectors in joulehook and jouledesk, whose /// published JCR-1 envelopes fell behind when the receipt payload gained a field. /// `map` pins a JCR-1 envelope the same way, so it is exposed to the same drift. #[test] fn the_published_receipt_is_still_what_the_implementation_seals() { let doc = pack("receipt_vectors.json"); let steward = doc["steward"].as_str().expect("pack names the steward"); let sk = SigningKey::from_bytes(&PUBLISHER_SEED); assert_eq!( hex::encode(sk.verifying_key().to_bytes()), doc["publisher_pubkey"].as_str().unwrap(), "the pack's publisher key is not the documented seed's" ); // Rebuilt exactly as gen-vectors builds it. let rcpt = Receipt { cell: CellKey { s2: S2CellId(0x42), alt: AltBand(0), time: TimeBucket::ATEMPORAL, lod: Lod(0), }, content_id: ContentId::of_bytes(b"tile"), tier: Tier::Photoreal, energy: Energy::estimate_tier(Tier::Photoreal), sequence: 1, nonce: [0, 0, 0, 0, 0, 0, 0, 1], }; assert_eq!( rcpt.to_jcr1_wire(steward).unwrap(), doc["wire"], "the JCR-1 wire projection differs from the published one" ); assert_eq!( hex::encode(rcpt.signed_core(steward).unwrap()), doc["signed_core_hex"].as_str().unwrap(), "the signed core differs from the published bytes" ); assert_eq!( hex::encode(rcpt.seal(&sk, steward).unwrap()), doc["cose_sign1_envelope_hex"].as_str().unwrap(), "the sealed envelope differs from the published bytes — a stale vector?" ); }