Post-quantum signatures
Sign receipts and threshold proposals with a hybrid Ed25519 and ML-DSA-65 key, and set exactly which signature algorithms the kernel admits.
On this page
Forging a hybrid-signed Chio receipt means breaking both Ed25519 and ML-DSA-65, and the operator decides exactly which signature algorithms the kernel admits. A hybrid signature commits a classical half and a post-quantum half to the same message, and a verifier accepts it only when both halves verify.
In chio-kernel the hybrid signer, the floor, and the self-quote gate are a Rust embedder API: an embedder installs the signer and sets the floor through calls on ChioKernel, and supplies its own KernelSelfQuoteVerifier. Building a hybrid signer or verifying a hybrid signature needs the pq cargo feature, which is off by default. A kernel starts with the allow_classical floor and a classical Ed25519 receipt signer.
mmessageσcclassical halfσqpost-quantum halfσhybrid signaturevverifier
Both halves verify over the original bytes
The Ed25519 half and the ML-DSA-65 half sign the same message and are paired under one algorithm-set tag. The hybrid public key accepts the signature only when both halves verify. Signing authority.
The floor is the set of algorithms the kernel admits
The kernel's signer decides which signatures it produces; the floor decides which it accepts, as a set of admitted algorithms rather than a minimum. Changing one setting without the other can produce records another component refuses, or admit an algorithm you meant to exclude.
| Floor | Admitted signatures | Signer it needs |
|---|---|---|
allow_classical | Ed25519, P-256, and P-384; hybrid refused | The default classical signer |
allow_hybrid | Classical and complete hybrid signatures | The signer you install for production |
pq_required | Complete hybrid signatures only | The gated hybrid signer, per required authority |
with_hybrid_signing_backend installs the kernel's hybrid signing backend after the verification gate and sets the capability and threshold validation floor to the configuration's floor, replacing any floor set before it. set_capability_crypto_floor changes only that validation floor. It generates no key, keeps the receipt signer and every independent authority as they were, and called after installation it replaces the floor the installation set.
The floor governs acceptance alone, so admitting hybrid signatures leaves the receipt signer as it was. Threshold-proposal signing checks its own signer against the floor, so pq_required on a kernel that kept its classical signer refuses classical capability envelopes and refuses to sign threshold proposals, while ordinary receipts are still signed with Ed25519.
A hybrid backend signs receipts and threshold proposals
Built with the pq feature, the kernel can share an Ed25519 backend or an Ed25519 plus ML-DSA-65 backend across its supported receipt and threshold-proposal paths. Required keys and the self-quote gate validate before with_hybrid_signing_backend installs the configuration, and a rejected configuration leaves the existing signer and floor unchanged.
Installing a hybrid backend configures exactly two artifact classes: threshold-proposal signing and ordinary receipt signing. Capability issuance, child receipts, session anchors, and checkpoints each keep their own independently configured authority. The receipt wire schema accepts the hybrid encoding on its public-key field and its signature field, alongside the classical and P-256 and P-384 forms.
A claim that all retained evidence follows one algorithm policy has to account for each class that crosses the application's boundary: grants, approval proposals, approval tokens, receipts, authority decisions, and key-log records. receipt_signing_public_key() and the retained envelope name the receipt signer; the separate public_key() names the classical capability issuer; the current floor and each envelope's own algorithm decide which capabilities and approval votes count; an independently pinned response authority signs responses; and activated key history with trusted time evidence says which authority was valid when.
The post-quantum key loads only after a verified self-quote
load_kernel_signing_backend_after_self_quote gates the whole non-classical path on one verifier call. Under a classical-only floor it returns the classical backend directly, because there is no post-quantum key to gate.
Under a hybrid or post-quantum-required floor, it asks the embedder's KernelSelfQuoteVerifier whether the quote binds to the classical kernel key it will pair with, before it derives any post-quantum material. A verified quote lets it build the hybrid backend; a rejected quote returns an error, and no post-quantum key is ever materialized.
with_hybrid_signing_backend routes through this gate both ways. One test drives a verified self-quote through to a working hybrid signer, and a second confirms that a rejected self-quote returns SelfQuoteRejected instead of a signer. Both tests build only with the pq feature.
The example accepts the original and refuses each changed half
The published program signs one summary with the Ed25519 and ML-DSA-65 backends, verifies it, then corrupts exactly one required component at a time. Enable chio-core-types/pq in an existing Rust host and call compare_hybrid_signatures():
let classical_signature = classical.sign_bytes(message)?;
let pq_signature = pq.sign_bytes(message)?;
let valid = Signature::from_hybrid_parts(
classical_signature.clone(),
&pq_signature,
HYBRID_ED25519_MLDSA65,
)?;VERIFY original hybrid signature; REFUSE substituted summary bytes REFUSE changed classical/PQ half and missing post-quantum component
You should see two lines. The first separates content binding from format recognition: the same complete signature verifies only its original summary. The second shows that a valid half never compensates for an invalid other half, and that a missing post-quantum component is refused while the envelope is built. The keys are disposable fixtures.
The complete program, hybrid_signature_cases.rs
use chio_core_types::crypto::HYBRID_ED25519_MLDSA65;
use chio_core_types::pq::{HybridBackend, MlDsa65Backend};
use chio_core_types::{Ed25519Backend, Keypair, Signature, SigningBackend};
/// Requires chio-core-types/pq. These are portable test keys, never production
/// custody or a kernel self-quote substitute.
pub fn compare_hybrid_signatures() -> chio_core_types::Result<()> {
let classical = Ed25519Backend::new(Keypair::generate());
let pq = MlDsa65Backend::generate()?;
let message = b"reviewed summary: dependency update approved";
let classical_signature = classical.sign_bytes(message)?;
let pq_signature = pq.sign_bytes(message)?;
let valid = Signature::from_hybrid_parts(
classical_signature.clone(),
&pq_signature,
HYBRID_ED25519_MLDSA65,
)?;
let backend = HybridBackend::new(Box::new(classical), pq)?;
let key = backend.public_key();
assert!(key.verify(message, &valid));
assert!(!key.verify(b"reviewed summary: deploy credentials", &valid));
println!("VERIFY original hybrid signature; REFUSE substituted summary bytes");
let mut changed_pq = pq_signature.clone();
changed_pq[0] ^= 1;
let bad_pq = Signature::from_hybrid_parts(
classical_signature.clone(),
&changed_pq,
HYBRID_ED25519_MLDSA65,
)?;
assert!(!key.verify(message, &bad_pq));
let other_classical = Ed25519Backend::new(Keypair::generate()).sign_bytes(message)?;
let bad_classical =
Signature::from_hybrid_parts(other_classical, &pq_signature, HYBRID_ED25519_MLDSA65)?;
assert!(!key.verify(message, &bad_classical));
// A missing post-quantum component is rejected during construction itself.
assert!(
Signature::from_hybrid_parts(classical_signature, &[], HYBRID_ED25519_MLDSA65).is_err()
);
println!("REFUSE changed classical/PQ half and missing post-quantum component");
Ok(())
}Download hybrid_signature_cases.rs. A kernel embedding installs its signer with boot-owned material and a verified quote, as Signing authority shows.
The wire format pairs one classical half with ML-DSA-65 and never nests
A hybrid wire value is bounded and non-recursive. Its encoded size is checked against a limit before parsing begins, and the grammar pairs one classical half with one post-quantum half under an explicit algorithm-set tag. ed25519, p256, and p384 each pair only with mldsa65.
enum Wire<'a> {
Classical(ClassicalWire<'a>),
Hybrid {
classical: ClassicalWire<'a>,
pq_hex: &'a str,
alg_set: &'static str,
},
}A dedicated classical-only path parses the classical half, so decoding never recurses into the hybrid branch: the parser reads a nested value's hybrid prefix and refuses it before any hex is decoded. The kernel's signer construction, kernel_signing_backend, only ever builds the Ed25519 plus ML-DSA-65 pairing; the P-256 and P-384 pairings serve other callers.
The algorithm comes from the envelope's explicit contract, never from a key's byte length or a deployment label. A capability envelope without an algorithm field is verified by its signature's own wire prefix, and when the field is present it must agree with the signature. A threshold proposal or approval token without the field is read as Ed25519, and its key and signature must then be Ed25519.
A boot refusal is an error, and a floor refusal is a signed Deny
The first two rows reach the Rust caller of with_hybrid_signing_backend as a KernelBootError. The last two reach a tool call as a signed Deny verdict; resource, prompt, and completion requests receive the same message as KernelError::GuardDenied.
| Refusal | Condition |
|---|---|
Kernel "kernel self-quote did not bind to classical kernel public key: <diagnostic>" | The embedder's self-quote verifier rejected the quote; no post-quantum key is loaded |
Kernel "policy.Kernel) | The floor requires a hybrid signer, and no ML-DSA-65 seed was supplied or chio-kernel was built without pq |
"capability verification failed: capability rejected by crypto floor: capability rejected by crypto_floor=<floor>: signature algorithm <algorithm> not permitted" | The floor does not admit the signature's algorithm: classical under pq_required, or hybrid under allow_classical |
"capability verification failed: capability rejected by crypto floor: capability algorithm envelope field <declared> disagrees with signature <actual>" | The capability's algorithm field names a different algorithm than its signature carries |
Repair the selected feature, key, or floor, or the complete envelope binding, before resubmitting.
Source reference: cryptographic refusal wrapping
The boot rows carry no CLI string wrapping. The second carries KernelSigningBackendError::HybridFloorRequiresPqKey as its source, so a Rust caller matches KernelBootError::SigningBackend(KernelSigningBackendError::HybridFloorRequiresPqKey { floor }); under a non-classical floor the verifier runs first, so this refusal follows an accepted quote. On the tool-call path a rejected floor is a successful ToolCallResponse whose verdict is Deny, carrying the composed reason strings above. On the resource, prompt, and completion path the same underlying message returns as KernelError::GuardDenied(reason), without the tool-call prefix. An embedder calling chio-kernel-core's verifier directly, bypassing the full kernel, sees CapabilityError::CryptoFloorRejected itself, with CapabilityFloorVerifyError::RejectedByCryptoFloor or AlgorithmMismatch folded in as that message's source.