use assert_cmd::Command;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use chrono::Utc;
use ed25519_dalek::SigningKey;
use rand::rngs::OsRng;
use std::fs;
use std::path::{Path, PathBuf};
use tempfile::tempdir;
use tsafe_core::run_evidence::{
blake3_hash, ContractRef, DeniedSensitiveEnvEvidence, EnforcementResult, EnvironmentEvidence,
InjectedSecretEvidence, MachineEvidence, ProcessEvidence, RiskDelta, RunEvidence,
RUN_EVIDENCE_VERSION, RUN_SCHEMA,
};
use tsafe_core::sign::sign_evidence;
const VERIFY_EXIT_SIGNATURE_ABSENT: i32 = 5;
const VERIFY_EXIT_SIGNATURE_FAILED: i32 = 6;
const VERIFY_EXIT_NOT_PINNED: i32 = 7;
fn tsafe() -> Command {
Command::cargo_bin("tsafe").expect("tsafe binary built by cargo")
}
fn well_formed() -> RunEvidence {
let now = Utc::now();
RunEvidence {
schema: RUN_SCHEMA.to_string(),
tsafe_attest_version: RUN_EVIDENCE_VERSION.to_string(),
started_at: now,
finished_at: now,
repo_path: "/tmp/phase5-cli-test".to_string(),
repo_commit: None,
command: vec!["true".to_string()],
contract: ContractRef {
path: "tsafe.contract.json".to_string(),
hash: blake3_hash(b"contract"),
},
environment: EnvironmentEvidence {
parent_env_count: 2,
child_env_count: 1,
removed_env_count: 1,
safe_baseline_injected: vec!["PATH".to_string()],
secrets_injected: vec![InjectedSecretEvidence {
name: "DATABASE_URL".to_string(),
source: "literal://demo/DATABASE_URL".to_string(),
hash: blake3_hash(b"db"),
redacted_value: "p***".to_string(),
required: true,
}],
sensitive_env_denied: vec![DeniedSensitiveEnvEvidence {
name: "AWS_SECRET_ACCESS_KEY".to_string(),
hash: blake3_hash(b"aws"),
reason: "test".to_string(),
}],
},
process: ProcessEvidence {
pid: 1234,
exit_code: 0,
duration_ms: 42,
cwd: "/tmp/phase5-cli-test".to_string(),
},
machine: MachineEvidence {
hostname_hash: blake3_hash(b"host"),
username_hash: blake3_hash(b"user"),
os: "linux".to_string(),
arch: "x86_64".to_string(),
},
result: EnforcementResult {
contract_enforced: true,
violations: Vec::new(),
risk_delta: RiskDelta {
before_score: 10,
after_score: 0,
},
},
signature: None,
}
}
fn write_evidence(path: &Path, evidence: &RunEvidence) {
fs::write(path, serde_json::to_vec_pretty(evidence).unwrap())
.expect("write evidence to tempdir");
}
fn signed_artifact_path(dir: &Path) -> (PathBuf, SigningKey) {
let key = SigningKey::generate(&mut OsRng);
let signed = sign_evidence(&well_formed(), &key).expect("sign");
let path = dir.join("signed-evidence.json");
write_evidence(&path, &signed.evidence);
(path, key)
}
fn unsigned_artifact_path(dir: &Path) -> PathBuf {
let evidence = well_formed();
assert!(evidence.signature.is_none());
let path = dir.join("unsigned-evidence.json");
write_evidence(&path, &evidence);
path
}
#[test]
fn verify_succeeds_with_a_correctly_signed_artifact_tofu_path() {
let dir = tempdir().expect("tempdir");
let (path, _key) = signed_artifact_path(dir.path());
tsafe()
.args(["attest", "verify"])
.arg(&path)
.assert()
.success();
}
#[test]
fn verify_succeeds_with_explicit_operator_pubkey() {
let dir = tempdir().expect("tempdir");
let (path, key) = signed_artifact_path(dir.path());
let pubkey = URL_SAFE_NO_PAD.encode(key.verifying_key().as_bytes());
tsafe()
.args(["attest", "verify"])
.arg(&path)
.args(["--pubkey", &pubkey])
.assert()
.success();
}
#[test]
fn verify_returns_exit_code_5_when_signature_is_absent() {
let dir = tempdir().expect("tempdir");
let path = unsigned_artifact_path(dir.path());
let result = tsafe()
.args(["attest", "verify"])
.arg(&path)
.assert()
.failure();
let code = result.get_output().status.code();
assert_eq!(
code,
Some(VERIFY_EXIT_SIGNATURE_ABSENT),
"expected exit code 5 for unsigned artifact, got {code:?}"
);
}
#[test]
fn verify_returns_exit_code_6_when_artifact_is_tampered() {
let dir = tempdir().expect("tempdir");
let (path, _key) = signed_artifact_path(dir.path());
let bytes = fs::read(&path).unwrap();
let mut evidence: RunEvidence = serde_json::from_slice(&bytes).unwrap();
evidence.process.exit_code = 99;
write_evidence(&path, &evidence);
let result = tsafe()
.args(["attest", "verify"])
.arg(&path)
.assert()
.failure();
let code = result.get_output().status.code();
assert_eq!(
code,
Some(VERIFY_EXIT_SIGNATURE_FAILED),
"expected exit code 6 for tampered artifact, got {code:?}"
);
}
#[test]
fn verify_returns_exit_code_6_when_operator_pubkey_is_wrong() {
let dir = tempdir().expect("tempdir");
let (path, _key) = signed_artifact_path(dir.path());
let wrong_key = SigningKey::generate(&mut OsRng);
let wrong_pubkey = URL_SAFE_NO_PAD.encode(wrong_key.verifying_key().as_bytes());
let result = tsafe()
.args(["attest", "verify"])
.arg(&path)
.args(["--pubkey", &wrong_pubkey])
.assert()
.failure();
let code = result.get_output().status.code();
assert_eq!(
code,
Some(VERIFY_EXIT_SIGNATURE_FAILED),
"expected exit code 6 for wrong operator pubkey, got {code:?}"
);
}
fn isolated_vault_dir(dir: &Path) -> PathBuf {
dir.join("v")
}
fn pubkey_of(key: &SigningKey) -> String {
URL_SAFE_NO_PAD.encode(key.verifying_key().as_bytes())
}
#[test]
fn require_pinned_fails_closed_exit_7_when_signer_is_not_pinned() {
let dir = tempdir().expect("tempdir");
let (path, _key) = signed_artifact_path(dir.path());
let vault = isolated_vault_dir(dir.path());
let result = tsafe()
.env("TSAFE_VAULT_DIR", &vault)
.args(["attest", "verify"])
.arg(&path)
.arg("--require-pinned")
.assert()
.failure();
let code = result.get_output().status.code();
assert_eq!(
code,
Some(VERIFY_EXIT_NOT_PINNED),
"valid-but-unpinned signer must exit 7 under --require-pinned, got {code:?}"
);
}
#[test]
fn require_pinned_succeeds_when_signer_is_pinned() {
let dir = tempdir().expect("tempdir");
let (path, key) = signed_artifact_path(dir.path());
let vault = isolated_vault_dir(dir.path());
let pubkey = pubkey_of(&key);
tsafe()
.env("TSAFE_VAULT_DIR", &vault)
.args(["attest", "trust", "add", "ci-prod", &pubkey])
.assert()
.success();
tsafe()
.env("TSAFE_VAULT_DIR", &vault)
.args(["attest", "verify"])
.arg(&path)
.arg("--require-pinned")
.assert()
.success();
}
#[test]
fn require_pinned_still_rejects_tampered_artifact_with_exit_6_not_7() {
let dir = tempdir().expect("tempdir");
let (path, key) = signed_artifact_path(dir.path());
let vault = isolated_vault_dir(dir.path());
let pubkey = pubkey_of(&key);
tsafe()
.env("TSAFE_VAULT_DIR", &vault)
.args(["attest", "trust", "add", "ci-prod", &pubkey])
.assert()
.success();
let bytes = fs::read(&path).unwrap();
let mut evidence: RunEvidence = serde_json::from_slice(&bytes).unwrap();
evidence.process.exit_code = 77;
write_evidence(&path, &evidence);
let result = tsafe()
.env("TSAFE_VAULT_DIR", &vault)
.args(["attest", "verify"])
.arg(&path)
.arg("--require-pinned")
.assert()
.failure();
let code = result.get_output().status.code();
assert_eq!(
code,
Some(VERIFY_EXIT_SIGNATURE_FAILED),
"tampered artifact must fail crypto first (exit 6), not the pin gate (7), got {code:?}"
);
}
#[test]
fn trust_add_list_remove_round_trip() {
let dir = tempdir().expect("tempdir");
let vault = isolated_vault_dir(dir.path());
let key = SigningKey::generate(&mut OsRng);
let pubkey = pubkey_of(&key);
tsafe()
.env("TSAFE_VAULT_DIR", &vault)
.args(["attest", "trust", "add", "ci-prod", &pubkey])
.assert()
.success();
let listed = tsafe()
.env("TSAFE_VAULT_DIR", &vault)
.args(["attest", "trust", "list"])
.assert()
.success();
let stdout = String::from_utf8(listed.get_output().stdout.clone()).unwrap();
assert!(
stdout.contains("ci-prod") && stdout.contains(&pubkey),
"trust list must show the pinned identity: {stdout}"
);
tsafe()
.env("TSAFE_VAULT_DIR", &vault)
.args(["attest", "trust", "remove", "ci-prod"])
.assert()
.success();
let (path, signer) = signed_artifact_path(dir.path());
let _ = signer;
let result = tsafe()
.env("TSAFE_VAULT_DIR", &vault)
.args(["attest", "verify"])
.arg(&path)
.arg("--require-pinned")
.assert()
.failure();
assert_eq!(
result.get_output().status.code(),
Some(VERIFY_EXIT_NOT_PINNED)
);
}
#[test]
fn trust_add_rejects_malformed_pubkey() {
let dir = tempdir().expect("tempdir");
let vault = isolated_vault_dir(dir.path());
tsafe()
.env("TSAFE_VAULT_DIR", &vault)
.args(["attest", "trust", "add", "bad", "this-is-not-a-key"])
.assert()
.failure();
}
#[test]
fn verify_help_documents_phase5_disclosure() {
let assert = tsafe()
.args(["attest", "verify", "--help"])
.assert()
.success();
let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
assert!(
stdout.contains("TOFU") || stdout.contains("out of band"),
"tsafe attest verify --help must include the Phase 5 honest-disclosure copy: {stdout}"
);
}