Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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
    }
}