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;
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:?}"
);
}
#[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}"
);
}