update-bundle/src/approval.rs
//! Signed approval statements. Each approval is a detached ed25519 signature over
//! the **exact stored bytes** of an approval statement that binds the candidate
//! digest. Authorization of the claimed role is decided by the signer certificate
//! fingerprint against the trust policy — never by the role string a signer writes
//! into the statement.
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
/// An approval decision. Only `Approve` authorizes; anything else is non-authorizing.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Decision {
Approve,
Reject,
}
/// A signed approval statement (stored as `approvals/<role>.<fingerprint>.approval.json`).
///
/// The bytes of this struct (serialized once) are the exact message the detached
/// signature covers. `optional_comment` is metadata only and is **never** part of
/// authorization logic.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApprovalStatement {
/// SHA-256 of the exact `core.json` bytes this approval is for.
pub candidate_digest: String,
/// The role the signer claims to exercise (authorized by fingerprint, not this string).
pub approval_role: String,
/// SHA-256 of the signer certificate DER.
pub signer_certificate_fingerprint: String,
/// The signer certificate subject.
pub signer_subject: String,
pub approved_at: DateTime<Utc>,
pub decision: Decision,
#[serde(default)]
pub optional_comment: Option<String>,
/// Algorithm identifier, always "ed25519" for this MVP.
pub signature_algorithm: String,
/// The bundle-relative path of the detached signature member.
pub signature_path: String,
}
impl ApprovalStatement {
/// The exact bytes signed / verified for this approval statement.
pub fn to_json_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
serde_json::to_vec_pretty(self)
}
pub fn from_json_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
serde_json::from_slice(bytes)
}
/// True if this is an authorizing (approve) decision.
pub fn is_approve(&self) -> bool {
self.decision == Decision::Approve
}
}