use crate::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum KernelCryptoFloor {
#[default]
AllowClassical,
AllowHybrid,
PqRequired,
}
impl KernelCryptoFloor {
#[must_use]
pub fn allows_hybrid(&self) -> bool {
matches!(self, Self::AllowHybrid | Self::PqRequired)
}
#[must_use]
pub fn requires_pq(&self) -> bool {
matches!(self, Self::PqRequired)
}
#[must_use]
pub fn allows_classical_only(&self) -> bool {
matches!(self, Self::AllowClassical | Self::AllowHybrid)
}
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::AllowClassical => "allow_classical",
Self::AllowHybrid => "allow_hybrid",
Self::PqRequired => "pq_required",
}
}
}
pub fn sign_receipt_body_with_backend(
body: ChioReceiptBody,
backend: &dyn chio_core::crypto::SigningBackend,
canonical_content: &[u8],
) -> Result<ChioReceipt, KernelError> {
chio_kernel_core::sign_receipt(body, backend, canonical_content).map_err(|error| {
use chio_kernel_core::ReceiptSigningError;
let message = match error {
ReceiptSigningError::KernelKeyMismatch => {
"kernel signing key does not match receipt body kernel_key".to_string()
}
ReceiptSigningError::ContentHashMismatch {
recomputed,
claimed,
} => format!(
"receipt content_hash mismatch: body claimed {claimed} but signer \
recomputed {recomputed} over the canonical content (WYSIWYS refused)"
),
ReceiptSigningError::SigningFailed(reason) => reason,
};
KernelError::ReceiptSigningFailed(message)
})
}
#[derive(Clone, Debug)]
pub struct SignedHybridReceipt {
pub receipt: ChioReceipt,
pub canonical: chio_core::crypto::SharedCanonicalBytes,
}
pub fn sign_receipt_body_hybrid_canonical(
body: ChioReceiptBody,
backend: &dyn chio_core::crypto::SigningBackend,
canonical_content: &[u8],
) -> Result<SignedHybridReceipt, KernelError> {
use chio_core::crypto::{
canonical_json_shared_bytes, sign_shared_canonical_with_backend, PublicKey,
};
use chio_core::receipt::{
body::chio_receipt_id, signing::bind_receipt_signing_nonce, signing::ChioReceiptSigningBody,
};
let recomputed = chio_core::crypto::sha256_hex(canonical_content);
if recomputed != body.content_hash {
return Err(KernelError::ReceiptSigningFailed(format!(
"receipt content_hash mismatch: body claimed {} but signer recomputed {} \
over the canonical content (WYSIWYS refused)",
body.content_hash, recomputed
)));
}
let backend_pk: PublicKey = backend.public_key();
if body.kernel_key.algorithm() != backend_pk.algorithm() || body.kernel_key != backend_pk {
return Err(KernelError::ReceiptSigningFailed(
"kernel signing key does not match receipt body kernel_key".to_string(),
));
}
let mut body = body;
body.validate_signable_semantics().map_err(|error| {
KernelError::ReceiptSigningFailed(format!(
"receipt body failed semantic validation: {error}"
))
})?;
bind_receipt_signing_nonce(&mut body);
body.id = chio_receipt_id(&body).map_err(|error| {
KernelError::ReceiptSigningFailed(format!(
"canonical JSON encoding of receipt id input failed: {error}"
))
})?;
let signing_body = ChioReceiptSigningBody::from(&body);
let canonical = canonical_json_shared_bytes(&signing_body).map_err(|error| {
KernelError::ReceiptSigningFailed(format!(
"canonical JSON encoding of receipt signing body failed: {error}"
))
})?;
let signed =
sign_shared_canonical_with_backend(backend, canonical.clone()).map_err(|error| {
KernelError::ReceiptSigningFailed(format!("hybrid signing failed: {error}"))
})?;
let (signature, signed_canonical) = signed.into_parts();
debug_assert_eq!(
canonical.as_bytes(),
signed_canonical.as_bytes(),
"byte-identity drift: shared canonical bytes were re-encoded"
);
let receipt = ChioReceipt {
id: body.id,
timestamp: body.timestamp,
capability_id: body.capability_id,
tool_server: body.tool_server,
tool_name: body.tool_name,
action: body.action,
decision: body.decision,
receipt_kind: body.receipt_kind,
boundary_class: body.boundary_class,
observation_outcome: body.observation_outcome,
tool_origin: body.tool_origin,
redaction_mode: body.redaction_mode,
actor_chain: body.actor_chain,
content_hash: body.content_hash,
policy_hash: body.policy_hash,
evidence: body.evidence,
metadata: body.metadata,
trust_level: body.trust_level,
tenant_id: body.tenant_id,
bbs_projection_version: None,
kernel_key: body.kernel_key,
bbs_signature: None,
algorithm: Some(backend.algorithm()),
signature,
};
Ok(SignedHybridReceipt {
receipt,
canonical: signed_canonical,
})
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum KernelSigningBackendError {
#[error(
"policy.crypto_floor={floor} requires a post-quantum (ML-DSA-65) signing key but \
none was provisioned at kernel boot"
)]
HybridFloorRequiresPqKey {
floor: &'static str,
},
#[error("post-quantum signing key import failed: {reason}")]
PqKeyImportFailed {
reason: String,
},
}
#[cfg(feature = "pq")]
pub fn kernel_signing_backend(
crypto_floor: KernelCryptoFloor,
classical_keypair: Keypair,
pq_seed: Option<&[u8; 32]>,
) -> Result<Box<dyn chio_core::crypto::SigningBackend>, KernelSigningBackendError> {
use chio_core::crypto::{Ed25519Backend, HybridBackend, MlDsa65Backend};
let classical = Ed25519Backend::new(classical_keypair);
if !crypto_floor.allows_hybrid() {
return Ok(Box::new(classical));
}
let seed = pq_seed.ok_or(KernelSigningBackendError::HybridFloorRequiresPqKey {
floor: crypto_floor.as_str(),
})?;
let pq = MlDsa65Backend::from_seed(seed);
let hybrid = HybridBackend::new(Box::new(classical), pq).map_err(|error| {
KernelSigningBackendError::PqKeyImportFailed {
reason: error.to_string(),
}
})?;
Ok(Box::new(hybrid))
}
#[cfg(not(feature = "pq"))]
pub fn kernel_signing_backend(
crypto_floor: KernelCryptoFloor,
classical_keypair: Keypair,
_pq_seed: Option<&[u8; 32]>,
) -> Result<Box<dyn chio_core::crypto::SigningBackend>, KernelSigningBackendError> {
use chio_core::crypto::Ed25519Backend;
if crypto_floor.allows_hybrid() {
return Err(KernelSigningBackendError::HybridFloorRequiresPqKey {
floor: crypto_floor.as_str(),
});
}
Ok(Box::new(Ed25519Backend::new(classical_keypair)))
}
#[cfg(test)]
mod borrowed_preimage_tests {
use chio_core::crypto::{Ed25519Backend, Keypair};
use chio_core::receipt::body::ChioReceiptBody;
use chio_core::receipt::decision::{Decision, ToolCallAction};
use chio_core::receipt::kinds::TrustLevel;
use super::sign_receipt_body_hybrid_canonical;
use crate::KernelError;
const PREIMAGE: &[u8] = br#"{"q":"borrowed-preimage"}"#;
fn seed() -> [u8; 32] {
let raw = b"chio-borrowed-preimage-test-seed";
let mut out = [0u8; 32];
out.copy_from_slice(raw);
out
}
fn body_for(kernel_key: chio_core::crypto::PublicKey, content_hash: String) -> ChioReceiptBody {
ChioReceiptBody {
id: "rcpt-borrowed-preimage".to_string(),
timestamp: 1_700_000_010,
capability_id: "cap-borrowed".to_string(),
tool_server: "audit-server".to_string(),
tool_name: "report".to_string(),
action: ToolCallAction::from_parameters(serde_json::json!({"q": "borrowed-preimage"}))
.expect("action canonicalises"),
decision: Some(Decision::Allow),
receipt_kind: Default::default(),
boundary_class: Default::default(),
observation_outcome: None,
tool_origin: Default::default(),
redaction_mode: Default::default(),
actor_chain: Vec::new(),
content_hash,
policy_hash: "policy-borrowed".to_string(),
evidence: Vec::new(),
metadata: None,
trust_level: TrustLevel::Mediated,
tenant_id: None,
kernel_key,
bbs_projection_version: None,
}
}
#[test]
fn borrowed_hash_path_signs_matching_body() {
let kp = Keypair::from_seed(&seed());
let backend = Ed25519Backend::new(kp.clone());
let body = body_for(kp.public_key(), chio_core::crypto::sha256_hex(PREIMAGE));
let signed = sign_receipt_body_hybrid_canonical(body, &backend, PREIMAGE)
.expect("matching body must sign through the borrowed-slice recompute path");
assert!(
signed
.receipt
.kernel_key
.verify(signed.canonical.as_bytes(), &signed.receipt.signature),
"produced signature must verify over the exact signed bytes"
);
}
#[test]
fn borrowed_hash_path_refuses_render_a_sign_b() {
let kp = Keypair::from_seed(&seed());
let backend = Ed25519Backend::new(kp.clone());
let body = body_for(kp.public_key(), chio_core::crypto::sha256_hex(PREIMAGE));
let attacker_preimage = br#"{"q":"a-different-thing"}"#;
let err = sign_receipt_body_hybrid_canonical(body, &backend, attacker_preimage)
.expect_err("render-A / sign-B must fail closed on the borrowed-slice path");
match err {
KernelError::ReceiptSigningFailed(message) => {
assert!(
message.contains("content_hash mismatch")
&& message.contains("WYSIWYS refused"),
"expected WYSIWYS content-hash mismatch diagnostic, got {message}"
);
}
other => panic!("expected ReceiptSigningFailed, got {other:?}"),
}
}
}