use std::fs;
use std::path::Path;
use std::process::{Command, Output};
fn mkit_bin() -> &'static str {
env!("CARGO_BIN_EXE_mkit")
}
fn mkit_sign_file_bin() -> std::path::PathBuf {
let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.ancestors()
.nth(3)
.expect("repo root above crates/mkit-cli");
let signers = repo_root.join("contrib").join("signers");
let candidate = signers.join("target").join("debug").join(if cfg!(windows) {
"mkit-sign-file.exe"
} else {
"mkit-sign-file"
});
if !candidate.exists() {
let status = Command::new(env!("CARGO"))
.args(["build", "-p", "mkit-sign-file"])
.arg("--manifest-path")
.arg(signers.join("Cargo.toml"))
.env_remove("RUSTFLAGS")
.env_remove("CARGO_ENCODED_RUSTFLAGS")
.env_remove("LLVM_PROFILE_FILE")
.status()
.expect("spawn cargo");
assert!(
status.success(),
"cargo build -p mkit-sign-file (contrib/signers workspace) failed"
);
}
candidate
}
fn run_in(cwd: &Path, args: &[&str]) -> Output {
run_in_with_user_config(cwd, args, None)
}
fn run_in_with_user_config(cwd: &Path, args: &[&str], user_cfg: Option<&str>) -> Output {
let xdg_root = tempfile::tempdir().expect("xdg tempdir");
if let Some(text) = user_cfg {
let cfg_dir = xdg_root.path().join("mkit");
fs::create_dir_all(&cfg_dir).unwrap();
fs::write(cfg_dir.join("config"), text).unwrap();
}
let out = Command::new(mkit_bin())
.args(args)
.current_dir(cwd)
.env("XDG_CONFIG_HOME", xdg_root.path())
.output()
.expect("spawn mkit");
drop(xdg_root);
out
}
fn init_repo_with_commit(cwd: &Path) {
assert!(run_in(cwd, &["init"]).status.success());
let kg = run_in(cwd, &["keygen"]);
assert!(
kg.status.success(),
"keygen failed: {}",
String::from_utf8_lossy(&kg.stderr)
);
fs::write(cwd.join("README.md"), b"hello\n").unwrap();
assert!(run_in(cwd, &["add", "README.md"]).status.success());
let c = run_in(cwd, &["commit", "-m", "init"]);
assert!(
c.status.success(),
"commit failed: {}",
String::from_utf8_lossy(&c.stderr)
);
}
fn write_key(path: &Path, bytes: &[u8; 32]) {
if let Some(p) = path.parent() {
fs::create_dir_all(p).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut dir_perm = fs::metadata(p).unwrap().permissions();
dir_perm.set_mode(0o700);
fs::set_permissions(p, dir_perm).unwrap();
}
}
fs::write(path, bytes).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perm = fs::metadata(path).unwrap().permissions();
perm.set_mode(0o600);
fs::set_permissions(path, perm).unwrap();
}
}
fn hex_lower(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push(HEX[(b >> 4) as usize] as char);
s.push(HEX[(b & 0x0F) as usize] as char);
}
s
}
fn head_commit_hex(cwd: &Path) -> String {
fs::read_to_string(cwd.join(".mkit/refs/heads/main"))
.expect("read main ref")
.trim()
.to_owned()
}
fn head_commit_hash(cwd: &Path) -> mkit_core::hash::Hash {
mkit_core::hash::from_hex(&head_commit_hex(cwd)).expect("HEAD commit hash is valid hex")
}
fn commit_file(cwd: &Path, path: &str, body: &[u8], message: &str) {
fs::write(cwd.join(path), body).unwrap();
assert!(run_in(cwd, &["add", path]).status.success());
let out = run_in(cwd, &["commit", "-m", message]);
assert!(
out.status.success(),
"commit failed: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
}
fn repo_key_public_key(cwd: &Path) -> Vec<u8> {
let secret_bytes = fs::read(cwd.join(".mkit/keys/default.key")).unwrap();
let mut secret = [0u8; 32];
secret.copy_from_slice(&secret_bytes);
ed25519_pubkey(&secret)
}
fn write_repo_key_trust_roots(cwd: &Path) {
let pk = repo_key_public_key(cwd);
let toml = format!(
"[[trust_root]]\n\
keyid = \"{}\"\n\
kind = \"ed25519\"\n\
pubkey_hex = \"{}\"\n",
blake3_keyid(&pk),
hex_lower(&pk)
);
fs::write(cwd.join(".mkit/attest-trust-roots.toml"), toml).unwrap();
}
fn save_test_envelope_payload(cwd: &Path, payload: &[u8]) {
let env = mkit_attest::Envelope {
payload_type: mkit_attest::PAYLOAD_TYPE_IN_TOTO.to_owned(),
payload: payload.to_vec(),
signatures: vec![mkit_attest::Sig {
keyid: "opaque:test".to_owned(),
sig: vec![0u8; 64],
}],
};
let bytes = env.encode().expect("test envelope encodes");
mkit_attest::store::save(&cwd.join(".mkit"), &head_commit_hash(cwd), bytes.as_bytes())
.expect("save test envelope");
}
fn ed25519_pubkey(secret: &[u8; 32]) -> Vec<u8> {
use ed25519_dalek::SigningKey;
let sk = SigningKey::from_bytes(secret);
sk.verifying_key().to_bytes().to_vec()
}
#[test]
fn verify_attest_rejects_envelope_replayed_under_another_commit() {
let td = tempfile::tempdir().unwrap();
let root = td.path();
init_repo_with_commit(root);
let commit_a = head_commit_hex(root);
let attest = run_in(
root,
&["attest", "--algorithm", "ed25519", "--signer", "repo-key"],
);
assert!(
attest.status.success(),
"attest failed: stdout={} stderr={}",
String::from_utf8_lossy(&attest.stdout),
String::from_utf8_lossy(&attest.stderr)
);
write_repo_key_trust_roots(root);
let verify_a = run_in(
root,
&[
"verify-attest",
"--commit",
&commit_a,
"--trust-roots",
".mkit/attest-trust-roots.toml",
],
);
assert!(
verify_a.status.success(),
"verify A failed: stdout={} stderr={}",
String::from_utf8_lossy(&verify_a.stdout),
String::from_utf8_lossy(&verify_a.stderr)
);
commit_file(root, "README.md", b"hello again\n", "second");
let commit_b = head_commit_hex(root);
let src_dir = root.join(".mkit/attestations").join(&commit_a);
let dst_dir = root.join(".mkit/attestations").join(&commit_b);
fs::create_dir_all(&dst_dir).unwrap();
for entry in fs::read_dir(&src_dir).unwrap() {
let entry = entry.unwrap();
fs::copy(entry.path(), dst_dir.join(entry.file_name())).unwrap();
}
let verify_b = run_in(
root,
&[
"verify-attest",
"--commit",
&commit_b,
"--trust-roots",
".mkit/attest-trust-roots.toml",
],
);
assert!(!verify_b.status.success(), "replayed attestation verified");
let stderr = String::from_utf8(verify_b.stderr).unwrap();
assert!(
stderr.contains("subject mismatch"),
"expected subject mismatch, got: {stderr}"
);
}
#[test]
fn verify_attest_rejects_malformed_statement_payload() {
let td = tempfile::tempdir().unwrap();
let root = td.path();
init_repo_with_commit(root);
write_repo_key_trust_roots(root);
save_test_envelope_payload(root, b"not valid json");
let out = run_in(
root,
&[
"verify-attest",
"--trust-roots",
".mkit/attest-trust-roots.toml",
],
);
assert!(!out.status.success(), "malformed statement verified");
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(
stderr.contains("subject error") || stderr.contains("malformed in-toto"),
"expected subject error, got: {stderr}"
);
}
#[test]
fn verify_attest_rejects_statement_subject_errors() {
let invalid_hex = "g".repeat(64);
let cases = vec![
(
br#"{"_type":"https://in-toto.io/Statement/v1","predicate":{}}"#.to_vec(),
"Statement has no subject entries",
),
(
br#"{"subject":[{"name":"commit","digest":{"sha256":"00"}}]}"#.to_vec(),
"Statement subject has no `blake3` digest",
),
(
br#"{"subject":[{"name":"commit","digest":{"blake3":"abc"}}]}"#.to_vec(),
"Statement subject digest is not 64 hex characters",
),
(
format!(r#"{{"subject":[{{"name":"commit","digest":{{"blake3":"{invalid_hex}"}}}}]}}"#)
.into_bytes(),
"Statement subject digest is not lowercase hex",
),
];
for (payload, expected) in cases {
let td = tempfile::tempdir().unwrap();
let root = td.path();
init_repo_with_commit(root);
write_repo_key_trust_roots(root);
save_test_envelope_payload(root, &payload);
let out = run_in(
root,
&[
"verify-attest",
"--trust-roots",
".mkit/attest-trust-roots.toml",
],
);
assert!(!out.status.success(), "invalid subject payload verified");
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(
stderr.contains("subject error") && stderr.contains(expected),
"expected subject error containing {expected:?}, got: {stderr}"
);
}
}
#[test]
fn attest_and_verify_ed25519_roundtrip() {
let td = tempfile::tempdir().unwrap();
let root = td.path();
init_repo_with_commit(root);
let key_path = root.join(".mkit/keys/default.key");
let secret_bytes = fs::read(&key_path).unwrap();
let mut secret = [0u8; 32];
secret.copy_from_slice(&secret_bytes);
let pk = ed25519_pubkey(&secret);
let pk_hash = {
let h = mkit_core::hash::hash(&pk);
mkit_core::hash::to_hex(&h)
};
let out = run_in(
root,
&["attest", "--algorithm", "ed25519", "--signer", "repo-key"],
);
assert!(
out.status.success(),
"attest failed: status={:?} stderr={}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
let toml = format!(
"[[trust_root]]\n\
keyid = \"blake3:{}\"\n\
kind = \"ed25519\"\n\
pubkey_hex = \"{}\"\n",
pk_hash,
hex_lower(&pk)
);
let trust_path = root.join(".mkit/attest-trust-roots.toml");
fs::write(&trust_path, toml).unwrap();
let out = run_in(
root,
&[
"verify-attest",
"--trust-roots",
".mkit/attest-trust-roots.toml",
],
);
assert!(
out.status.success(),
"verify-attest failed: status={:?} stdout={} stderr={}",
out.status,
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(
stderr.contains("verified") || stderr.contains("Ok"),
"verify-attest did not report verified signature: {stderr}"
);
}
#[test]
fn attest_and_verify_secp256k1_roundtrip() {
use k256::ecdsa::SigningKey;
let td = tempfile::tempdir().unwrap();
let root = td.path();
init_repo_with_commit(root);
let mut secret = [0u8; 32];
secret[31] = 42;
let pk = {
let sk = SigningKey::from_bytes((&secret).into()).unwrap();
sk.verifying_key()
.to_encoded_point(true)
.as_bytes()
.to_vec()
};
let keyid = format!("secp256k1:{}", hex_lower(&pk));
let key_path = root.join(".mkit/keys/secp256k1.key");
write_key(&key_path, &secret);
fs::create_dir_all(root.join(".mkit")).unwrap();
fs::write(
root.join(".mkit/config"),
b"attest.secp256k1_key_path = .mkit/keys/secp256k1.key\n",
)
.unwrap();
let out = run_in(
root,
&["attest", "--algorithm", "secp256k1", "--signer", "repo-key"],
);
assert!(
out.status.success(),
"attest failed: status={:?} stdout={} stderr={}",
out.status,
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let toml = format!(
"[[trust_root]]\n\
keyid = \"{}\"\n\
kind = \"secp256k1\"\n\
pubkey_hex = \"{}\"\n",
keyid,
hex_lower(&pk)
);
fs::write(root.join(".mkit/attest-trust-roots.toml"), toml).unwrap();
let out = run_in(
root,
&[
"verify-attest",
"--trust-roots",
".mkit/attest-trust-roots.toml",
],
);
assert!(
out.status.success(),
"verify-attest failed: status={:?} stdout={} stderr={}",
out.status,
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(
stderr.contains("verified") || stderr.contains("Ok"),
"verify-attest did not report verified signature: {stderr}"
);
}
#[test]
fn attest_and_verify_p256_roundtrip() {
use p256::ecdsa::SigningKey;
let td = tempfile::tempdir().unwrap();
let root = td.path();
init_repo_with_commit(root);
let mut secret = [0u8; 32];
secret[31] = 7;
let pk = {
let sk = SigningKey::from_bytes(&secret.into()).unwrap();
sk.verifying_key()
.to_encoded_point(true)
.as_bytes()
.to_vec()
};
let keyid = format!("p256:{}", hex_lower(&pk));
let key_path = root.join(".mkit/keys/p256.key");
write_key(&key_path, &secret);
fs::create_dir_all(root.join(".mkit")).unwrap();
fs::write(
root.join(".mkit/config"),
b"attest.p256_key_path = .mkit/keys/p256.key\n",
)
.unwrap();
let out = run_in(
root,
&["attest", "--algorithm", "p256", "--signer", "repo-key"],
);
assert!(
out.status.success(),
"attest failed: status={:?} stdout={} stderr={}",
out.status,
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let toml = format!(
"[[trust_root]]\n\
keyid = \"{}\"\n\
kind = \"p256-sec1\"\n\
pubkey_hex = \"{}\"\n",
keyid,
hex_lower(&pk)
);
fs::write(root.join(".mkit/attest-trust-roots.toml"), toml).unwrap();
let out = run_in(
root,
&[
"verify-attest",
"--trust-roots",
".mkit/attest-trust-roots.toml",
],
);
assert!(
out.status.success(),
"verify-attest failed: status={:?} stdout={} stderr={}",
out.status,
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(
stderr.contains("verified") || stderr.contains("Ok"),
"verify-attest did not report verified signature: {stderr}"
);
}
#[test]
fn attest_missing_keyfile_errors_cleanly() {
let td = tempfile::tempdir().unwrap();
let root = td.path();
init_repo_with_commit(root);
fs::write(
root.join(".mkit/config"),
b"attest.secp256k1_key_path = .mkit/keys/does-not-exist.key\n",
)
.unwrap();
let out = run_in(
root,
&["attest", "--algorithm", "secp256k1", "--signer", "repo-key"],
);
assert!(!out.status.success(), "attest should have failed");
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(
stderr.contains("keygen") || stderr.contains("secp256k1"),
"error message should mention keygen/secp256k1: {stderr}"
);
}
#[test]
fn attest_unknown_algorithm_errors() {
let td = tempfile::tempdir().unwrap();
let root = td.path();
init_repo_with_commit(root);
let out = run_in(root, &["attest", "--algorithm", "rsa"]);
assert!(!out.status.success());
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(
stderr.contains("rsa") || stderr.contains("algorithm"),
"error did not explain unknown algorithm: {stderr}"
);
}
#[test]
fn attest_malformed_predicate_file_errors() {
let td = tempfile::tempdir().unwrap();
let root = td.path();
init_repo_with_commit(root);
let bad = root.join("bad-predicate.json");
fs::write(&bad, b"not valid json").unwrap();
let out = run_in(
root,
&[
"attest",
"--algorithm",
"ed25519",
"--predicate-file",
bad.to_str().unwrap(),
],
);
assert!(!out.status.success());
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(
stderr.contains("predicate") || stderr.contains("JSON") || stderr.contains("json"),
"error did not mention predicate: {stderr}"
);
}
fn write_ed25519_key(path: &Path, secret: &[u8; 32]) -> Vec<u8> {
write_key(path, secret);
ed25519_pubkey(secret)
}
fn fixture_for_external_ed25519() -> (tempfile::TempDir, std::path::PathBuf, Vec<u8>) {
let td = tempfile::tempdir().unwrap();
let root = td.path().to_path_buf();
init_repo_with_commit(&root);
let key_path = root.join("external-signer.key");
let mut secret = [0u8; 32];
secret[31] = 9;
let pk = write_ed25519_key(&key_path, &secret);
(td, key_path, pk)
}
fn blake3_keyid(pk: &[u8]) -> String {
let h = mkit_core::hash::hash(pk);
format!("blake3:{}", mkit_core::hash::to_hex(&h))
}
#[test]
fn attest_external_cli_flag_passes_argv() {
let (td, key_path, pk) = fixture_for_external_ed25519();
let root = td.path();
let signer_bin = mkit_sign_file_bin();
let user_cfg = format!(
"attest.external_signer_path = {}\n",
signer_bin.to_str().unwrap()
);
let out = run_in_with_user_config(
root,
&[
"attest",
"--algorithm",
"ed25519",
"--signer",
"external",
"--external-signer-arg",
"--key",
"--external-signer-arg",
key_path.to_str().unwrap(),
],
Some(&user_cfg),
);
assert!(
out.status.success(),
"attest failed: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let toml = format!(
"[[trust_root]]\n\
keyid = \"{}\"\n\
kind = \"ed25519\"\n\
pubkey_hex = \"{}\"\n",
blake3_keyid(&pk),
hex_lower(&pk)
);
fs::write(root.join(".mkit/attest-trust-roots.toml"), toml).unwrap();
let out = run_in(
root,
&[
"verify-attest",
"--trust-roots",
".mkit/attest-trust-roots.toml",
],
);
assert!(
out.status.success(),
"verify failed: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn attest_external_config_args_pass_through() {
let (td, key_path, pk) = fixture_for_external_ed25519();
let root = td.path();
let signer_bin = mkit_sign_file_bin();
let user_cfg = format!(
"attest.external_signer_path = {}\n\
attest.external_signer_args = --key|{}\n",
signer_bin.to_str().unwrap(),
key_path.to_str().unwrap()
);
let out = run_in_with_user_config(
root,
&["attest", "--algorithm", "ed25519", "--signer", "external"],
Some(&user_cfg),
);
assert!(
out.status.success(),
"attest failed: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let toml = format!(
"[[trust_root]]\n\
keyid = \"{}\"\n\
kind = \"ed25519\"\n\
pubkey_hex = \"{}\"\n",
blake3_keyid(&pk),
hex_lower(&pk)
);
fs::write(root.join(".mkit/attest-trust-roots.toml"), toml).unwrap();
let out = run_in(
root,
&[
"verify-attest",
"--trust-roots",
".mkit/attest-trust-roots.toml",
],
);
assert!(out.status.success());
}
#[test]
fn attest_additional_signer_args_clause_pass_through() {
let (td, key_path, pk_ext) = fixture_for_external_ed25519();
let root = td.path();
let signer_bin = mkit_sign_file_bin();
let primary_secret = fs::read(root.join(".mkit/keys/default.key")).unwrap();
let mut primary = [0u8; 32];
primary.copy_from_slice(&primary_secret);
let pk_primary = ed25519_pubkey(&primary);
let spec = format!(
"algorithm=ed25519,signer=external,path={},args=--key|{}",
signer_bin.to_str().unwrap(),
key_path.to_str().unwrap()
);
let out = run_in(
root,
&[
"attest",
"--algorithm",
"ed25519",
"--signer",
"repo-key",
"--additional-signer",
&spec,
],
);
assert!(
out.status.success(),
"attest failed: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(
stderr.contains("2 signature(s)"),
"expected envelope to carry two signatures; got: {stderr}"
);
let toml = format!(
"[[trust_root]]\n\
keyid = \"{}\"\n\
kind = \"ed25519\"\n\
pubkey_hex = \"{}\"\n\
\n\
[[trust_root]]\n\
keyid = \"{}\"\n\
kind = \"ed25519\"\n\
pubkey_hex = \"{}\"\n",
blake3_keyid(&pk_primary),
hex_lower(&pk_primary),
blake3_keyid(&pk_ext),
hex_lower(&pk_ext),
);
fs::write(root.join(".mkit/attest-trust-roots.toml"), toml).unwrap();
let out = run_in(
root,
&[
"verify-attest",
"--trust-roots",
".mkit/attest-trust-roots.toml",
],
);
assert!(
out.status.success(),
"verify failed: stdout={} stderr={}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
}