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}