//! B8 `org audit` verified-signer attribution — integration coverage. //! //! The audit logic (`resolve_signer`, `parse_audit_log`, `run_org_audit`) lives //! in the bin crate's private `commands::org` module and the CLI dispatch is not //! wired until B14, so we cannot drive `org audit` through the binary yet. What //! we CAN do is build a real signed org vault via `org init` and assert that the //! exact verification mechanism `resolve_signer` uses — a temp `allowed_signers` //! prefixed `relicario `, injected via `GIT_CONFIG_*`, then //! `git verify-commit --raw`, then the `key (SHA256:...)` regex over stderr — //! resolves the genesis commit's signature to the seeded member's fingerprint. //! //! This pins the security-critical half of B8 (attribute to the VERIFIED signer, //! mirroring the pre-receive hook) against a genuine SSH signature rather than //! the synthetic-log unit tests, which only cover the "no member matched -> //! TAMPERED" fallback. use std::fs; use std::io::Write; use std::path::Path; use std::process::Command; use tempfile::{NamedTempFile, TempDir}; fn relicario_with_git_identity(config_home: &Path, args: &[&str]) -> std::process::Output { Command::new(env!("CARGO_BIN_EXE_relicario")) .env("XDG_CONFIG_HOME", config_home) .env("HOME", config_home) .env("GIT_AUTHOR_NAME", "Test Device") .env("GIT_AUTHOR_EMAIL", "test@relicario.test") .env("GIT_COMMITTER_NAME", "Test Device") .env("GIT_COMMITTER_EMAIL", "test@relicario.test") .args(args) .output() .expect("run relicario") } /// Lay out a device keypair under `/relicario/devices//` and /// mark it current. Mirrors `org_init_signing::seed_device`. Returns the OpenSSH /// public key string. fn seed_device(config_home: &Path, name: &str) -> String { let (priv_openssh, pub_openssh) = relicario_core::device::generate_keypair().expect("generate_keypair"); let dev_dir = config_home.join("relicario").join("devices").join(name); fs::create_dir_all(&dev_dir).expect("create device dir"); let signing_key_path = dev_dir.join("signing.key"); fs::write(&signing_key_path, priv_openssh.as_str()).expect("write signing.key"); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; fs::set_permissions(&signing_key_path, fs::Permissions::from_mode(0o600)) .expect("chmod signing.key"); } fs::write(dev_dir.join("signing.pub"), &pub_openssh).expect("write signing.pub"); fs::write(dev_dir.join("deploy.key"), "").expect("write stub deploy.key"); fs::write(dev_dir.join("deploy.pub"), "").expect("write stub deploy.pub"); let devices_dir = config_home.join("relicario").join("devices"); fs::write(devices_dir.join("current"), format!("{name}\n")).expect("write current"); pub_openssh } /// Replicate `commands::org::resolve_signer`'s verification: build an /// allowed_signers file from the given pubkeys (prefixed `relicario `), inject it /// via GIT_CONFIG_*, run `git verify-commit --raw`, and parse the SHA256 key /// fingerprint from stderr. fn resolve_signer_fp(org_root: &Path, commit: &str, pubkeys: &[&str]) -> Option { let mut tmp = NamedTempFile::new().ok()?; for pk in pubkeys { writeln!(tmp, "relicario {}", pk.trim()).ok()?; } let allowed_path = tmp.path(); let output = Command::new("git") .current_dir(org_root) .args(["verify-commit", "--raw", commit]) .env("GIT_CONFIG_COUNT", "1") .env("GIT_CONFIG_KEY_0", "gpg.ssh.allowedSignersFile") .env("GIT_CONFIG_VALUE_0", allowed_path.as_os_str()) .output() .ok()?; // The clean exit IS the gate (matches the hook): a non-member signature fails. if !output.status.success() { return None; } let stderr = String::from_utf8_lossy(&output.stderr); let re = regex::Regex::new(r"key (SHA256:[A-Za-z0-9+/]+)").ok()?; Some(re.captures(&stderr)?.get(1)?.as_str().to_string()) } #[test] fn audit_resolves_genesis_commit_to_the_signing_member() { let cfg = TempDir::new().unwrap(); let org = TempDir::new().unwrap(); let pub_openssh = seed_device(cfg.path(), "test-dev"); let init = relicario_with_git_identity( cfg.path(), &["org", "init", "--dir", org.path().to_str().unwrap(), "--name", "Acme"], ); assert!( init.status.success(), "org init failed:\nstdout: {}\nstderr: {}", String::from_utf8_lossy(&init.stdout), String::from_utf8_lossy(&init.stderr) ); // The signing member's pubkey is recorded in members.json. resolve_signer // builds allowed_signers from exactly that set. let members_json = fs::read_to_string(org.path().join("members.json")).expect("read members.json"); let members: relicario_core::OrgMembers = serde_json::from_str(&members_json).expect("parse members.json"); assert_eq!(members.members.len(), 1, "init seeds exactly one owner member"); let owner = &members.members[0]; // The genesis commit must resolve to the owner's fingerprint. let signing_fp = resolve_signer_fp(org.path(), "HEAD", &[owner.ed25519_pubkey.as_str()]) .expect("genesis commit signature must verify against the member set"); let expected = relicario_core::fingerprint(&owner.ed25519_pubkey).expect("fingerprint owner"); assert_eq!( signing_fp, expected, "verified signer fingerprint must equal the owner member's fingerprint" ); // The seeded pubkey and the members.json pubkey are the same key. assert_eq!(owner.ed25519_pubkey.trim(), pub_openssh.trim()); } #[test] fn audit_rejects_signature_from_a_non_member_key() { // A commit signed by the owner must NOT resolve when the allowed_signers set // contains only some OTHER (non-member) key — this is the TAMPERED path: // "signer is not a current member". let cfg = TempDir::new().unwrap(); let org = TempDir::new().unwrap(); let _owner_pub = seed_device(cfg.path(), "test-dev"); let init = relicario_with_git_identity( cfg.path(), &["org", "init", "--dir", org.path().to_str().unwrap(), "--name", "Acme"], ); assert!(init.status.success(), "org init failed"); // A stranger keypair that never signed anything in this repo. let (_stranger_priv, stranger_pub) = relicario_core::device::generate_keypair().expect("generate stranger keypair"); let resolved = resolve_signer_fp(org.path(), "HEAD", &[stranger_pub.as_str()]); assert!( resolved.is_none(), "a commit signed by the owner must not verify against a stranger-only signer set" ); }