use assert_cmd::prelude::*;
use std::path::Path;
use std::process::Command;
const PSK_A: &str = "4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a";
const PSK_B: &str = "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b";
const ROOT: &str = "1111111111111111111111111111111111111111111111111111111111111111";
const ANCHOR_KEY: &str = "2222222222222222222222222222222222222222222222222222222222222222";
fn issuer_identity() -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("issuer.toml");
Command::cargo_bin("net-mesh")
.unwrap()
.args(["identity", "generate", "--out"])
.arg(&path)
.assert()
.code(0);
(dir, path)
}
fn mint(extra: &[&str]) -> std::process::Output {
let (_keep, issuer) = issuer_identity();
let mut cmd = Command::cargo_bin("net-mesh").unwrap();
cmd.arg("--output").arg("json");
cmd.args(["anchor", "credential", "mint", "--issuer-identity"]);
cmd.arg(&issuer);
cmd.args([
"--root",
ROOT,
"--anchor-noise-pubkey",
ANCHOR_KEY,
"--psk-hex",
PSK_A,
"--url",
"https://anchor.example.com",
]);
cmd.args(extra);
cmd.output().unwrap()
}
fn json(out: &std::process::Output) -> serde_json::Value {
serde_json::from_slice(&out.stdout).unwrap_or_else(|e| {
panic!(
"stdout was not JSON ({e}): {}",
String::from_utf8_lossy(&out.stdout)
)
})
}
#[test]
fn a_minted_credential_parses_back_and_states_both_lifetimes() {
let out = mint(&["--invite-ttl-secs", "600", "--psk-ttl-secs", "86400"]);
assert!(out.status.success(), "{:?}", out);
let v = json(&out);
let credential = v["credential"].as_str().expect("credential string");
assert!(credential.starts_with("net-bootstrap:"));
let nonce = v["nonce_expires_at"].as_u64().unwrap();
let psk = v["psk_expires_at"].as_u64().unwrap();
assert!(
psk > nonce + 80_000,
"the standing half must outlive the single-use half (nonce {nonce}, psk {psk})"
);
let parsed =
net_sdk::bootstrap_credential::BrowserBootstrapCredential::decode(credential).unwrap();
assert_eq!(parsed.bootstrap_url, "https://anchor.example.com");
assert_eq!(parsed.nonce_expires_at(), nonce);
assert_eq!(parsed.psk_expires_at(), psk);
}
#[test]
fn inspect_prints_the_trust_domain_and_never_the_psk() {
let minted = mint(&[]);
let credential = json(&minted)["credential"].as_str().unwrap().to_string();
let out = Command::cargo_bin("net-mesh")
.unwrap()
.args([
"--output",
"json",
"anchor",
"credential",
"inspect",
"--credential",
&credential,
])
.output()
.unwrap();
assert!(out.status.success(), "{:?}", out);
let stdout = String::from_utf8(out.stdout).unwrap();
assert!(
!stdout.contains(PSK_A),
"inspect must never print the PSK: {stdout}"
);
let v: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert!(v.get("psk").is_none(), "no psk field exists on the report");
assert_eq!(v["status"], "ok");
assert_eq!(v["anchor_noise_pubkey"], ANCHOR_KEY);
assert!(v["trust_domain"].as_str().unwrap().len() == 32);
}
#[test]
fn inspect_reports_a_foreign_trust_domain_rather_than_accepting_it() {
let credential = json(&mint(&[]))["credential"].as_str().unwrap().to_string();
let check = |psk: &str| -> serde_json::Value {
let out = Command::cargo_bin("net-mesh")
.unwrap()
.args([
"--output",
"json",
"anchor",
"credential",
"inspect",
"--credential",
&credential,
"--psk-hex",
psk,
])
.output()
.unwrap();
assert!(out.status.success());
serde_json::from_slice(&out.stdout).unwrap()
};
assert_eq!(check(PSK_A)["trust_domain_matches"], true);
assert_eq!(check(PSK_B)["trust_domain_matches"], false);
}
#[test]
fn an_expired_credential_inspects_and_says_which_half_expired() {
let credential = json(&mint(&[
"--invite-ttl-secs",
"1",
"--psk-ttl-secs",
"86400",
]))["credential"]
.as_str()
.unwrap()
.to_string();
std::thread::sleep(std::time::Duration::from_millis(1100));
let out = Command::cargo_bin("net-mesh")
.unwrap()
.args([
"--output",
"json",
"anchor",
"credential",
"inspect",
"--credential",
&credential,
])
.output()
.unwrap();
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_slice(&out.stdout).unwrap();
let status = v["status"].as_str().unwrap();
assert!(
status.contains("nonce expired"),
"the status must name the single-use half: {status}"
);
assert_eq!(v["bootstrap_url"], "https://anchor.example.com");
}
#[test]
fn writing_a_credential_file_refuses_to_overwrite_without_force() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("browser.cred");
let first = mint(&["--out", path.to_str().unwrap()]);
assert!(first.status.success(), "{:?}", first);
let written = std::fs::read_to_string(&path).unwrap();
assert!(written.starts_with("net-bootstrap:"));
assert_eq!(json(&first)["credential"].as_str().unwrap(), written);
let second = mint(&["--out", path.to_str().unwrap()]);
assert!(
!second.status.success(),
"a second mint must not clobber a credential file"
);
assert_eq!(std::fs::read_to_string(&path).unwrap(), written);
let forced = mint(&["--out", path.to_str().unwrap(), "--force"]);
assert!(forced.status.success(), "{:?}", forced);
assert_ne!(
std::fs::read_to_string(&path).unwrap(),
written,
"--force mints a fresh nonce, so the file must actually change"
);
assert!(!has_stage_temp(dir.path()), "no staging temp left behind");
}
#[test]
fn a_url_no_browser_could_fetch_is_refused_at_mint_time() {
let (_keep, issuer) = issuer_identity();
let out = Command::cargo_bin("net-mesh")
.unwrap()
.args(["anchor", "credential", "mint", "--issuer-identity"])
.arg(&issuer)
.args([
"--root",
ROOT,
"--anchor-noise-pubkey",
ANCHOR_KEY,
"--psk-hex",
PSK_A,
"--url",
"http://anchor.example.com",
])
.output()
.unwrap();
assert!(
!out.status.success(),
"plain http off localhost is not a secure context; minting it defers the \
failure to the browser"
);
}
#[test]
fn minting_without_a_psk_is_refused() {
let (_keep, issuer) = issuer_identity();
let out = Command::cargo_bin("net-mesh")
.unwrap()
.args(["anchor", "credential", "mint", "--issuer-identity"])
.arg(&issuer)
.args([
"--root",
ROOT,
"--anchor-noise-pubkey",
ANCHOR_KEY,
"--url",
"https://anchor.example.com",
])
.output()
.unwrap();
assert!(!out.status.success());
let stderr = String::from_utf8_lossy(&out.stderr).to_lowercase();
assert!(
stderr.contains("psk"),
"the error must name the PSK: {stderr}"
);
}
#[test]
fn the_psk_may_come_from_a_file_instead_of_argv() {
let (_keep, issuer) = issuer_identity();
let dir = tempfile::tempdir().unwrap();
let psk_path = dir.path().join("domain.psk");
std::fs::write(&psk_path, format!("{PSK_A}\n")).unwrap();
let out = Command::cargo_bin("net-mesh")
.unwrap()
.args([
"--output",
"json",
"anchor",
"credential",
"mint",
"--issuer-identity",
])
.arg(&issuer)
.args([
"--root",
ROOT,
"--anchor-noise-pubkey",
ANCHOR_KEY,
"--psk-file",
psk_path.to_str().unwrap(),
"--url",
"https://anchor.example.com",
])
.output()
.unwrap();
assert!(out.status.success(), "{:?}", out);
let from_file = json(&out)["trust_domain"].as_str().unwrap().to_string();
let from_argv = json(&mint(&[]))["trust_domain"]
.as_str()
.unwrap()
.to_string();
assert_eq!(from_file, from_argv);
}
fn has_stage_temp(dir: &Path) -> bool {
std::fs::read_dir(dir)
.unwrap()
.filter_map(Result::ok)
.any(|e| e.file_name().to_string_lossy().contains(".stage."))
}