use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use crate::git;
pub const FORMAT: &str = "amont-attest-v2";
pub const NOTES_REF: &str = "amont-attest";
pub const NAMESPACE: &str = "amont-attest";
pub fn platform() -> String {
format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS)
}
pub struct Verdict {
pub gates: Option<Vec<String>>,
pub trail: Vec<String>,
}
impl Verdict {
fn nothing(reason: impl Into<String>) -> Self {
Verdict {
gates: None,
trail: vec![reason.into()],
}
}
}
fn push_reason(trail: &mut Vec<String>, reason: String) {
if trail.last() != Some(&reason) {
trail.push(reason);
}
}
pub fn verify(payload: &str, sig: &str, allowed_signers: &Path, principal: &str) -> bool {
use std::io::Write;
let sig_file = std::env::temp_dir().join(format!(
"attest-verify-{}-{:p}.sig",
std::process::id(),
&sig
));
if std::fs::write(&sig_file, format!("{sig}\n")).is_err() {
return false;
}
let ok = (|| {
let mut child = Command::new("ssh-keygen")
.args(["-Y", "verify", "-n", NAMESPACE, "-I", principal, "-f"])
.arg(allowed_signers)
.arg("-s")
.arg(&sig_file)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.ok()?;
child.stdin.take()?.write_all(payload.as_bytes()).ok()?;
child.wait().ok().map(|s| s.success())
})()
.unwrap_or(false);
let _ = std::fs::remove_file(&sig_file);
ok
}
pub fn default_signers() -> Option<PathBuf> {
let root = git::stdout(&["rev-parse", "--show-toplevel"]).map(PathBuf::from);
[".forgejo/allowed_signers", ".github/allowed_signers"]
.into_iter()
.map(|rel| match &root {
Some(root) => root.join(rel),
None => PathBuf::from(rel),
})
.find(|p| p.exists())
}
pub fn first_principal(signers: &Path) -> Option<String> {
let body = std::fs::read_to_string(signers).ok()?;
body.lines()
.map(str::trim)
.find(|l| !l.is_empty() && !l.starts_with('#'))
.and_then(|l| l.split_whitespace().next())
.map(str::to_string)
}
pub fn split_note(body: &str) -> Option<(String, String)> {
let (payload, sig) = body.split_once("\n\n")?;
if !sig.starts_with("-----BEGIN SSH SIGNATURE-----") {
return None;
}
Some((format!("{payload}\n"), sig.to_string()))
}
fn field<'a>(payload: &'a str, name: &str) -> Option<&'a str> {
payload
.lines()
.find_map(|l| l.strip_prefix(name).and_then(|r| r.strip_prefix(' ')))
.map(str::trim)
}
pub fn evaluate(signers: &Path, principal: &str, require_platform: Option<&str>) -> Verdict {
let mut trail = Vec::new();
let refspec = format!("+refs/notes/{NOTES_REF}:refs/notes/{NOTES_REF}");
let _ = git::succeeds(&["fetch", "origin", &refspec]);
let Some(head_tree) = git::stdout(&["rev-parse", "HEAD^{tree}"]) else {
return Verdict::nothing("cannot resolve HEAD^{tree} — not a git repository?");
};
for candidate in [head_tree.as_str(), "HEAD", "HEAD^2"] {
let Some(object) = git::stdout(&["rev-parse", "--verify", candidate]) else {
continue;
};
let Some(body) = git::stdout(&["notes", "--ref", NOTES_REF, "show", &object]) else {
continue;
};
let Some((payload, sig)) = split_note(&body) else {
push_reason(
&mut trail,
format!("note on {candidate} carries no signature block"),
);
continue;
};
if payload.lines().next() != Some(FORMAT) {
push_reason(
&mut trail,
format!("note on {candidate} is not {FORMAT} — a newer producer wrote it"),
);
continue;
}
let (Some(tree), Some(gates), Some(ran_on)) = (
field(&payload, "tree"),
field(&payload, "gates"),
field(&payload, "platform"),
) else {
push_reason(
&mut trail,
format!("note on {candidate} is missing a required field"),
);
continue;
};
if tree != head_tree {
push_reason(
&mut trail,
format!("attested tree {tree} is not the checked-out tree {head_tree}"),
);
continue;
}
if gates.is_empty() {
push_reason(
&mut trail,
"attestation lists no gates — a signed way of saying nothing".into(),
);
continue;
}
if let Some(want) = require_platform {
if want != ran_on {
push_reason(
&mut trail,
format!("attested on {ran_on}, this leg is {want}"),
);
continue;
}
}
if verify(&payload, &sig, signers, principal) {
push_reason(&mut trail, format!("signature verifies as {principal}"));
return Verdict {
gates: Some(gates.split_whitespace().map(str::to_string).collect()),
trail,
};
}
push_reason(
&mut trail,
format!(
"signature on {candidate} does not verify as {principal} against {}",
signers.display()
),
);
}
if trail.is_empty() {
push_reason(
&mut trail,
format!("no attestation found for tree {head_tree}"),
);
}
Verdict { gates: None, trail }
}