Skip to main content

ReceiptSigningHandle

Struct ReceiptSigningHandle 

Source
pub struct ReceiptSigningHandle { /* private fields */ }
Expand description

A one-time, move-only handle that binds a signature to a specific evaluated artifact’s canonical content.

§Why this exists (WYSIWYS at the trust boundary)

Today the receipt signer accepts a ChioReceiptBody whose content_hash is a caller-supplied string and never recomputes it. That lets a caller render content A to a human (and to the audit surface) while handing the signer a body claiming the hash of content B – a render-A / sign-B forgery. A signature over such a body says nothing about the content the human actually saw.

ReceiptSigningHandle closes that gap. It is constructed inside the trust boundary from the exact canonical content the producer evaluated; at construction time it recomputes content_hash = sha256_hex(canonical_content) and stores both the immutable canonical bytes and the authoritative hash. The signing entrypoints that take a handle then refuse to sign unless the body’s claimed content_hash equals the handle’s recomputed hash (ContentHashMismatch, fail-closed).

§One-time semantics

The handle is move-only (no Clone): the signing entrypoints consume it by value, so a single handle corresponds to a single signing attempt over a single evaluated artifact. A producer cannot reuse one handle to back two different signatures, and cannot smuggle a handle built over content A into a sign call for content B – the recomputed hash is fixed at construction and the body’s claim is checked against it.

§Seam note (follow-up: full evaluate() -> sign() binding)

In the present change the handle is built from canonical content bytes that the producer supplies via ReceiptSigningHandle::from_content / ReceiptSigningHandle::from_canonical_bytes. The intended end state is that evaluate() returns the handle (so the only way to obtain one is to have actually run an evaluation, and the bytes are the evaluator’s own canonical output rather than anything the caller chose). Threading the handle out of chio-kernel-core::evaluate and through every adapter’s receipt-construction path is a larger change tracked as a follow-up (follow-up seam). The hash-recompute + refuse-on-mismatch guarantee in this type already closes the render-A / sign-B regression regardless of that follow-up, because the signer never trusts the caller’s content_hash.

§Debug redaction

Debug is implemented by hand (not derived) so the raw content_preimage – which for a value receipt is the exact canonical tool output and can carry user data or secrets – never reaches a log line, a debug_assert! panic, or a {:?} format. The custom impl prints the recomputed content_hash (a non-secret digest) and the preimage length in place of the bytes themselves. See the debug_redacts_preimage test.

Implementations§

Source§

impl ReceiptSigningHandle

Source

pub fn from_content<T>(content: &T) -> Result<ReceiptSigningHandle, Error>
where T: Serialize,

Build a handle by serializing an evaluated artifact to canonical JSON and recomputing its content hash.

content is the artifact the producer actually evaluated and intends to represent to the human (e.g. the tool output, the request projection). The handle owns the resulting canonical bytes and the sha256_hex(canonical) digest; both are fixed for the lifetime of the handle.

§Errors

Returns an error if content cannot be canonicalized.

Source

pub fn from_canonical_bytes( canonical_content: CanonicalBytes, ) -> ReceiptSigningHandle

Build a handle directly from already-canonicalized content bytes.

Use this when the producer has computed the canonical bytes through the witnessed CanonicalBytes path already (e.g. a shared evaluation buffer) and wants to avoid reserializing. The content hash is recomputed here regardless, so the resulting hash is always the signer’s own, never a caller-asserted value.

Source

pub fn from_content_preimage(preimage: Vec<u8>) -> ReceiptSigningHandle

Build a handle from the exact byte preimage the receipt content_hash is defined over, recomputing the hash here.

Use this on the production kernel signing path, where content_hash is not always sha256_hex(canonical_json(value)): stream receipts hash a concatenation of per-chunk digests, and the empty-output receipt hashes the literal null canonicalization. In every case the kernel already holds the exact bytes it hashed; passing them here lets the signer recompute the same digest independently and refuse to sign when the body’s claimed content_hash does not match (WYSIWYS, fail-closed).

The hash is recomputed from preimage regardless of its shape, so the resulting hash is always the signer’s own, never a caller assertion.

Source

pub fn content_hash(&self) -> &str

The authoritative content_hash recomputed by the signer over the bound canonical content. This is what the receipt body’s content_hash must equal for signing to proceed.

Source

pub fn canonical_content(&self) -> &[u8]

Borrow the immutable canonical content bytes bound to this handle.

Source

pub fn ensure_body_matches( &self, body: &ChioReceiptBody, ) -> Result<(), ContentHashMismatch>

Fail-closed check that the body’s claimed content_hash matches the hash recomputed over this handle’s canonical content.

§Errors

Returns ContentHashMismatch when the body’s content_hash differs from the handle’s recomputed hash.

Source

pub fn into_parts(self) -> (String, Vec<u8>)

Consume the handle, returning the recomputed hash and canonical bytes.

Consuming by value enforces the one-time semantics: once a handle has been turned into a signed receipt (or otherwise unwrapped) it cannot be reused to back another signature.

Trait Implementations§

Source§

impl Debug for ReceiptSigningHandle

Source§

fn fmt(&self, formatter: &mut Formatter<'_>) -> Result<(), Error>

Redacted formatter: prints the recomputed content_hash (non-secret) and the preimage byte length, but NEVER the preimage bytes. The raw preimage is the canonical tool output for value receipts and may carry user data or secrets, so it must not leak into logs or panic messages.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.