Skip to main content

chio_kernel/
compliance_certificate.rs

1//! Session compliance certificate issuance with hybrid signing.
2//!
3//! A `SessionComplianceCertificate` is a kernel-signed envelope that
4//! attests a session's receipt log was free of policy violations against
5//! a specific `crypto_floor` and `policy_hash`. It is the artifact that
6//! external auditors consume when they need a compact, signed summary of
7//! a session without re-walking the underlying receipt store.
8//!
9//! ## Why this module exists
10//!
11//! `chio-acp-proxy` ships a Sigstore-only compliance certificate that
12//! consumes a bare `Keypair` for Ed25519 signing. The kernel needs the
13//! same envelope shape signed through an arbitrary `&dyn SigningBackend`
14//! so a `HybridBackend` can produce `Signature::Hybrid` envelopes under
15//! `crypto_floor=allow_hybrid` or `pq_required`. This module is the
16//! kernel-side hybrid path; it wraps a body identical in shape to the
17//! acp-proxy form but signs through the `chio-core-types` backend
18//! abstraction.
19//!
20//! Spec reference: `spec/COMPLIANCE-CERTIFICATE.md`.
21//!
22//! Threat model row `pq_signature_downgrade` is the surface this guards.
23
24use chio_core::canonical::canonical_json_bytes;
25use chio_core::crypto::{
26    sign_canonical_with_backend, PublicKey, Signature, SigningAlgorithm, SigningBackend,
27};
28use serde::{Deserialize, Serialize};
29
30use crate::receipt_support::KernelCryptoFloor;
31
32/// Body of a session compliance certificate.
33///
34/// Mirrors the fields the auditor consumes (`session_id`, `policy_hash`,
35/// receipt counts, `all_signatures_valid`) and adds a `crypto_floor` field
36/// so the verifier can reproduce the floor under which the certificate
37/// was issued. Canonical JSON of this body is the byte input to
38/// `SigningBackend::sign_bytes`; downstream verifiers reproduce the
39/// canonical bytes and call `PublicKey::verify` on the resulting
40/// signature.
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
42pub struct SessionComplianceCertificateBody {
43    /// Session this certificate covers.
44    pub session_id: String,
45    /// SHA-256 hash of the policy applied to every receipt in the
46    /// session.
47    pub policy_hash: String,
48    /// Number of receipts walked while issuing this certificate.
49    pub receipt_count: u64,
50    /// Whether every receipt's signature re-verified during issuance.
51    pub all_signatures_valid: bool,
52    /// Crypto floor under which the certificate was issued. The verifier
53    /// reproduces this floor when re-verifying receipt-side signatures so
54    /// a downgrade attack on receipts is caught at certificate-load time.
55    pub crypto_floor: String,
56    /// Unix timestamp (seconds) when the certificate was issued.
57    pub issued_at: u64,
58}
59
60/// A signed session compliance certificate.
61///
62/// Wraps a [`SessionComplianceCertificateBody`] with the issuing kernel's
63/// public key and a signature over canonical JSON of the body. Under
64/// `crypto_floor=allow_hybrid` or `pq_required` the signature is
65/// [`Signature::Hybrid`] and the public key is [`PublicKey::Hybrid`];
66/// otherwise both fields carry classical material (Ed25519 / P-256 /
67/// P-384) and remain byte-identical to the classical envelope.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct SessionComplianceCertificate {
70    /// Body the signature covers.
71    pub body: SessionComplianceCertificateBody,
72    /// Public key of the issuing kernel.
73    pub signer_key: PublicKey,
74    /// Algorithm used for `signature`. Carried so consumers can branch
75    /// without parsing the self-describing prefix; informational only
76    /// (verification dispatches off the signature material).
77    pub algorithm: SigningAlgorithm,
78    /// Signature over canonical JSON of `body`.
79    pub signature: Signature,
80}
81
82/// Errors raised during compliance certificate issuance and verification.
83#[derive(Debug, thiserror::Error)]
84pub enum ComplianceCertificateError {
85    /// The body's `crypto_floor` field disagrees with the floor the
86    /// caller threaded into issuance. Fail-closed: a mis-stamped floor
87    /// is a downgrade signal.
88    #[error(
89        "compliance certificate crypto_floor field {body_floor} disagrees with issuance floor {issuance_floor}"
90    )]
91    CryptoFloorMismatch {
92        /// The floor recorded in the body.
93        body_floor: String,
94        /// The floor the caller threaded into issuance.
95        issuance_floor: String,
96    },
97
98    /// Canonical JSON serialization or signing failed.
99    #[error("compliance certificate signing failed: {0}")]
100    SigningFailed(String),
101
102    /// Signature verification rejected the certificate.
103    #[error("compliance certificate signature verification failed")]
104    SignatureVerificationFailed,
105
106    /// The declared algorithm field was tampered or drifted from the
107    /// self-describing signature envelope.
108    #[error(
109        "compliance certificate algorithm field {declared} disagrees with signature algorithm {actual}"
110    )]
111    AlgorithmMismatch {
112        /// The informational algorithm field carried by the envelope.
113        declared: &'static str,
114        /// The algorithm parsed from the signature material.
115        actual: &'static str,
116    },
117
118    /// The certificate's signature algorithm violates the configured
119    /// `crypto_floor`. Threat model row `pq_signature_downgrade`.
120    #[error(
121        "compliance certificate rejected by crypto_floor={floor}: signature algorithm {actual} not permitted"
122    )]
123    RejectedByCryptoFloor {
124        /// The configured floor that rejected the certificate.
125        floor: &'static str,
126        /// The signature algorithm carried by the certificate.
127        actual: &'static str,
128    },
129}
130
131/// Issue a session compliance certificate signed through `backend`.
132///
133/// The caller threads the `crypto_floor` separately so the body field is
134/// authoritative and tamper-evident: a body with a different floor than
135/// the issuance call rejects fail-closed at issuance time, not at first
136/// audit. Under `KernelCryptoFloor::AllowClassical` this produces a
137/// classical-only envelope byte-identical to the signed form (when
138/// the body's `crypto_floor` field agrees with the configured floor).
139///
140/// # Errors
141///
142/// Returns [`ComplianceCertificateError::CryptoFloorMismatch`] when the
143/// body's `crypto_floor` field disagrees with `floor`.
144/// [`ComplianceCertificateError::SigningFailed`] when canonical JSON or
145/// signing fails. Threat model row `pq_signature_downgrade` is the
146/// surface this guards.
147pub fn issue_session_compliance_certificate(
148    body: SessionComplianceCertificateBody,
149    floor: KernelCryptoFloor,
150    backend: &dyn SigningBackend,
151) -> Result<SessionComplianceCertificate, ComplianceCertificateError> {
152    if body.crypto_floor != floor.as_str() {
153        return Err(ComplianceCertificateError::CryptoFloorMismatch {
154            body_floor: body.crypto_floor.clone(),
155            issuance_floor: floor.as_str().to_string(),
156        });
157    }
158
159    let (signature, _bytes) = sign_canonical_with_backend(backend, &body)
160        .map_err(|error| ComplianceCertificateError::SigningFailed(error.to_string()))?;
161
162    Ok(SessionComplianceCertificate {
163        body,
164        signer_key: backend.public_key(),
165        algorithm: backend.algorithm(),
166        signature,
167    })
168}
169
170/// Verify a session compliance certificate against the configured
171/// `crypto_floor`.
172///
173/// Performs the same dispatch table as
174/// `CapabilityToken::verify_signature_with_floor`: floor rejection BEFORE
175/// cryptographic verification, fail-closed on a mismatch between the
176/// body's `crypto_floor` field and the verification floor, fail-closed on
177/// signature failures.
178///
179/// # Errors
180///
181/// Returns [`ComplianceCertificateError::RejectedByCryptoFloor`] when the
182/// certificate's signature algorithm violates the configured floor.
183/// [`ComplianceCertificateError::CryptoFloorMismatch`] when the body's
184/// floor field disagrees with the verification floor.
185/// [`ComplianceCertificateError::SignatureVerificationFailed`] when the
186/// cryptographic check fails.
187pub fn verify_session_compliance_certificate(
188    cert: &SessionComplianceCertificate,
189    floor: KernelCryptoFloor,
190) -> Result<(), ComplianceCertificateError> {
191    // Step 1: floor enforcement. Reject any algorithm the floor does not
192    // permit BEFORE running the cryptographic check.
193    let signature_algorithm = cert.signature.algorithm();
194    if cert.algorithm != signature_algorithm {
195        return Err(ComplianceCertificateError::AlgorithmMismatch {
196            declared: signing_algorithm_label(cert.algorithm),
197            actual: signing_algorithm_label(signature_algorithm),
198        });
199    }
200    let is_hybrid = matches!(signature_algorithm, SigningAlgorithm::Hybrid);
201    let allowed = if is_hybrid {
202        floor.allows_hybrid()
203    } else {
204        floor.allows_classical_only()
205    };
206    if !allowed {
207        return Err(ComplianceCertificateError::RejectedByCryptoFloor {
208            floor: floor.as_str(),
209            actual: signing_algorithm_label(signature_algorithm),
210        });
211    }
212
213    // Step 2: body floor field consistency.
214    if cert.body.crypto_floor != floor.as_str() {
215        return Err(ComplianceCertificateError::CryptoFloorMismatch {
216            body_floor: cert.body.crypto_floor.clone(),
217            issuance_floor: floor.as_str().to_string(),
218        });
219    }
220
221    // Step 3: cryptographic verification.
222    let bytes = canonical_json_bytes(&cert.body)
223        .map_err(|error| ComplianceCertificateError::SigningFailed(error.to_string()))?;
224    if !cert.signer_key.verify(&bytes, &cert.signature) {
225        return Err(ComplianceCertificateError::SignatureVerificationFailed);
226    }
227
228    Ok(())
229}
230
231fn signing_algorithm_label(alg: SigningAlgorithm) -> &'static str {
232    match alg {
233        SigningAlgorithm::Ed25519 => "ed25519",
234        SigningAlgorithm::P256 => "p256",
235        SigningAlgorithm::P384 => "p384",
236        SigningAlgorithm::Hybrid => "hybrid",
237    }
238}