|
| 1 | +//! Chain-verified clone (Phase B, SECURITY.md residual #6). |
| 2 | +//! |
| 3 | +//! `clone`/`pull` now verify the served root equals the store singleton's |
| 4 | +//! CURRENT on-chain root (read via the launcher id embedded in the module's |
| 5 | +//! `ChainState`), failing closed on mismatch or an unreachable chain. These |
| 6 | +//! tests exercise that gate entirely offline through the `DIGSTORE_ANCHOR_MOCK` |
| 7 | +//! seam (always set by `common::dig`) plus the per-command chain-root knobs: |
| 8 | +//! - `DIGSTORE_ANCHOR_MOCK_CHAIN_ROOT=<hex>` — the mocked on-chain root. |
| 9 | +//! - `DIGSTORE_ANCHOR_MOCK_CHAIN_UNREACHABLE=1` — fail closed (chain unreachable). |
| 10 | +//! - neither set — skip the comparison (so legacy remote tests stay green). |
| 11 | +
|
| 12 | +mod common; |
| 13 | +use common::*; |
| 14 | +use predicates::prelude::*; |
| 15 | + |
| 16 | +/// Read the source store's host public key (48 bytes) from trusted_keys.json — |
| 17 | +/// the exact mechanism used by `cli_remote_clone_push_pull.rs`. |
| 18 | +fn host_pubkey(dir: &tempfile::TempDir) -> [u8; 48] { |
| 19 | + let text = std::fs::read_to_string(store_dir(dir).join("trusted_keys.json")).unwrap(); |
| 20 | + let v: serde_json::Value = serde_json::from_str(&text).unwrap(); |
| 21 | + let hex = v[0]["public_key"].as_str().unwrap(); |
| 22 | + let bytes = hex::decode(hex).unwrap(); |
| 23 | + bytes.try_into().unwrap() |
| 24 | +} |
| 25 | + |
| 26 | +/// Publish a committed store to a `TestServer`; return (server, store_id, root). |
| 27 | +/// The served module embeds a `ChainState` whose launcher id == the store id |
| 28 | +/// (Phase A `commit` behavior), so the chain-root gate is active for a clone. |
| 29 | +fn published_store() -> (TestServer, String, String) { |
| 30 | + let dir = tmp_dig(); |
| 31 | + dig(&dir).arg("init").assert().success(); |
| 32 | + std::fs::write(dir.path().join("a.txt"), b"hello").unwrap(); |
| 33 | + dig(&dir).args(["add", "a.txt"]).assert().success(); |
| 34 | + dig(&dir).args(["commit", "-m", "x"]).assert().success(); |
| 35 | + |
| 36 | + let (store_id, root) = store_id_and_root(&dir); |
| 37 | + let module_path = store_dir(&dir) |
| 38 | + .join("modules") |
| 39 | + .join(format!("{store_id}-{root}.dig")); |
| 40 | + let module = std::fs::read(&module_path).unwrap(); |
| 41 | + let pubkey = host_pubkey(&dir); |
| 42 | + let sig = genesis_push_sig(&dir, &store_id, &root); |
| 43 | + let server = TestServer::start_with_module(&store_id, &root, pubkey, &module, sig); |
| 44 | + (server, store_id, root) |
| 45 | +} |
| 46 | + |
| 47 | +#[test] |
| 48 | +fn clone_passes_when_onchain_root_matches() { |
| 49 | + let (server, store_id, root) = published_store(); |
| 50 | + let dest = tmp_dig(); |
| 51 | + let url = format!("{}/stores/{}", server.base_url(), store_id); |
| 52 | + dig(&dest) |
| 53 | + .args(["clone", &url]) |
| 54 | + .env("DIGSTORE_ANCHOR_MOCK_CHAIN_ROOT", &root) |
| 55 | + .assert() |
| 56 | + .success(); |
| 57 | +} |
| 58 | + |
| 59 | +#[test] |
| 60 | +fn clone_fails_closed_when_onchain_root_differs() { |
| 61 | + let (server, store_id, _root) = published_store(); |
| 62 | + let dest = tmp_dig(); |
| 63 | + let url = format!("{}/stores/{}", server.base_url(), store_id); |
| 64 | + dig(&dest) |
| 65 | + .args(["clone", &url]) |
| 66 | + .env("DIGSTORE_ANCHOR_MOCK_CHAIN_ROOT", "11".repeat(32)) |
| 67 | + .assert() |
| 68 | + .failure() |
| 69 | + .code(5) |
| 70 | + .stderr(predicate::str::contains("on-chain root")); |
| 71 | +} |
| 72 | + |
| 73 | +#[test] |
| 74 | +fn clone_fails_closed_when_chain_unreachable() { |
| 75 | + let (server, store_id, _root) = published_store(); |
| 76 | + let dest = tmp_dig(); |
| 77 | + let url = format!("{}/stores/{}", server.base_url(), store_id); |
| 78 | + dig(&dest) |
| 79 | + .args(["clone", &url]) |
| 80 | + .env("DIGSTORE_ANCHOR_MOCK_CHAIN_UNREACHABLE", "1") |
| 81 | + .assert() |
| 82 | + .failure() |
| 83 | + .code(5) |
| 84 | + .stderr(predicate::str::contains("unreachable")); |
| 85 | +} |
0 commit comments