chore(salvage): snapshot org-vault tail uncommitted work before worktree cleanup
org_audit.rs (B8 verified-signer test) + the two uncommitted org.rs diffs (item-CRUD B9-B13, status/audit B8) from the wf_22020aea first-run worktrees. All superseded by v0.8.0 main; also committed on the -r2 branches. Kept so nothing is lost when the stale worktrees are removed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5JvzEse4xUxLZKhofyeCD
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
//! 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 `<config_home>/relicario/devices/<name>/` 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<String> {
|
||||
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"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user