use crate::input::sha256_hex;
use crate::WaiverAuthority;
use serde::{Deserialize, Serialize};
pub const HUMAN_SIGNER: &str = "human";
pub fn floor_allows(
signer_kind: Option<&str>,
rule_id: &str,
human_only: bool,
authority: WaiverAuthority,
) -> Result<(), String> {
if rule_id == "instrumentation_error" || rule_id == "unsupported_input" {
return Err(format!(
"waiver on fail-closed state {rule_id} is never valid"
));
}
if signer_kind == Some(HUMAN_SIGNER) {
return Ok(());
}
let shown = signer_kind.unwrap_or("<absent>");
if human_only || rule_id == "SLOP-J001" {
return Err(format!(
"waiver on {rule_id} requires a human signer; signer_kind {shown} \
cannot clear the authority floor"
));
}
if authority != WaiverAuthority::OrchestratorAgent {
return Err(format!(
"agent-signed waiver for {rule_id} rejected: deployment authority is human"
));
}
Ok(())
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct WaiverSpan {
pub start: usize,
pub end: usize,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Waiver {
pub rule_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub span: Option<WaiverSpan>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signer_kind: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Approval {
pub document_sha256: String,
pub policy_digest: String,
pub profile: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approved_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approver: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approver_kind: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub demote: Vec<String>,
#[serde(default)]
pub waivers: Vec<Waiver>,
}
#[derive(Debug, PartialEq, Eq)]
pub enum VerifyOutcome {
Verified,
Mismatch(Vec<String>),
}
pub fn parse_rfc3339(s: &str) -> Option<i64> {
let b = s.as_bytes();
if b.len() < 19 {
return None;
}
let num = |r: std::ops::Range<usize>| -> Option<i64> {
let t = s.get(r)?;
if t.bytes().all(|c| c.is_ascii_digit()) {
t.parse().ok()
} else {
None
}
};
let year = num(0..4)?;
let month = num(5..7)?;
let day = num(8..10)?;
let hour = num(11..13)?;
let minute = num(14..16)?;
let second = num(17..19)?;
if b[4] != b'-' || b[7] != b'-' || (b[10] != b'T' && b[10] != b't' && b[10] != b' ') {
return None;
}
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return None;
}
let mut rest = &s[19..];
if rest.starts_with('.') {
let frac_end = rest[1..]
.find(|c: char| !c.is_ascii_digit())
.map(|i| i + 1)
.unwrap_or(rest.len());
rest = &rest[frac_end..];
}
let offset_secs: i64 = if rest.is_empty() || rest == "Z" || rest == "z" {
0
} else {
let sign = match rest.as_bytes()[0] {
b'+' => 1,
b'-' => -1,
_ => return None,
};
if rest.len() < 6 || rest.as_bytes()[3] != b':' {
return None;
}
let oh: i64 = rest.get(1..3)?.parse().ok()?;
let om: i64 = rest.get(4..6)?.parse().ok()?;
sign * (oh * 3600 + om * 60)
};
let y = if month <= 2 { year - 1 } else { year };
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = y - era * 400;
let mp = (month + 9) % 12;
let doy = (153 * mp + 2) / 5 + day - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
let days = era * 146_097 + doe - 719_468;
Some(days * 86_400 + hour * 3600 + minute * 60 + second - offset_secs)
}
pub fn verify(input: &[u8], approval: &Approval, now_unix: i64) -> VerifyOutcome {
let mut problems = Vec::new();
let got = sha256_hex(input);
let want = approval
.document_sha256
.strip_prefix("sha256:")
.unwrap_or(&approval.document_sha256);
if got != want {
problems.push(format!(
"document hash mismatch: payload is {got}, approval says {want}"
));
}
let digest = crate::policy::compute_digest();
if approval.policy_digest != digest {
problems.push(format!(
"policy digest mismatch: embedded policy is {digest}, approval says {}",
approval.policy_digest
));
}
let profile = crate::Profile::from_str(&approval.profile);
if profile.is_none() {
problems.push(format!("unknown profile {}", approval.profile));
}
let human_only = |rule_id: &str| -> bool {
crate::engine::compiled()
.ok()
.and_then(|cp| cp.pkg.rule_by_id(rule_id))
.map(|r| r.human_only_waiver)
.unwrap_or(false)
};
for w in &approval.waivers {
if let Some(expires) = &w.expires {
match parse_rfc3339(expires) {
Some(t) if t < now_unix => {
problems.push(format!("waiver for {} expired at {}", w.rule_id, expires));
}
None => {
problems.push(format!(
"waiver for {} has unreadable expiry {}",
w.rule_id, expires
));
}
_ => {}
}
}
if let Err(reason) = floor_allows(
w.signer_kind.as_deref(),
&w.rule_id,
human_only(&w.rule_id),
WaiverAuthority::OrchestratorAgent,
) {
problems.push(reason);
}
}
if let Some(profile) = profile {
let mut config = crate::Config::new(profile);
config.waivers = approval.waivers.clone();
config.now_unix = Some(now_unix);
config.deployment.waiver_authority = Some(WaiverAuthority::OrchestratorAgent);
config.deployment.demote = approval.demote.clone();
match crate::analyze(input, &config) {
Ok(report) => {
let mut blocking: Vec<String> = report
.findings
.iter()
.filter(|f| {
!f.waived
&& f.lifecycle == "blocking"
&& (f.state == "violation" || f.state == "candidate")
})
.map(|f| f.rule_id.clone())
.collect();
blocking.sort();
blocking.dedup();
if !blocking.is_empty() {
problems.push(format!(
"unwaived blocking findings remain under profile {}: {}",
profile.as_str(),
blocking.join(", ")
));
}
}
Err(e) => {
problems.push(format!("re-analysis under the approval failed closed: {e}"));
}
}
}
if problems.is_empty() {
VerifyOutcome::Verified
} else {
VerifyOutcome::Mismatch(problems)
}
}