crates/gunnar-policy/src/gates/signed.rs
//! Gate 3. Every commit this push adds must carry a verifying ssh signature.
//!
//! The cryptography lives in [`crate::signature`]; this is the policy wrapper
//! around it. What the wrapper decides is *which commits* and *whose keys*, and
//! both answers are ones a blind implementation gets to skip entirely. A gate
//! that checks no commits is indistinguishable from a gate that checks them all
//! successfully.
//!
//! So the commit set comes from a walk bounded by the pack's own new-object set
//! ([`crate::ancestry::introduced_commits`]) rather than from a caller-supplied
//! list, and an empty key set rejects rather than vacuously passing.
use std::collections::HashSet;
use crate::config::SignaturePolicy;
use crate::context::PushContext;
use crate::error::Result;
use crate::{Gate, Verdict};
/// Requires ssh signatures on newly added commits.
#[derive(Clone, Debug)]
pub struct SignedCommitsGate {
policy: SignaturePolicy,
}
impl SignedCommitsGate {
/// A gate over `policy`.
pub fn new(policy: SignaturePolicy) -> Self {
SignedCommitsGate { policy }
}
/// The policy this gate enforces.
pub fn policy(&self) -> &SignaturePolicy {
&self.policy
}
}
impl Gate for SignedCommitsGate {
fn name(&self) -> &str {
"signed-commits"
}
fn check(&self, ctx: &PushContext<'_>) -> Result<Verdict> {
if !self.policy.required {
return Ok(Verdict::Allow);
}
let mut checked: HashSet<gix_hash::ObjectId> = HashSet::new();
for update in ctx.updates {
let verdict = super::verify_update_signatures(
ctx,
update,
self.policy.scope,
&self.policy.namespace,
self.policy.budget,
&mut checked,
self.name(),
)?;
if verdict.is_reject() {
return Ok(verdict);
}
}
Ok(Verdict::Allow)
}
}