Skip to content

Commit cce5260

Browse files
committed
Harden exposed arrays and CT posture reporting
1 parent 2b9433b commit cce5260

7 files changed

Lines changed: 294 additions & 51 deletions

File tree

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,9 @@ or `ct::CtEngine::decode_buffer`. The non-clear-tail CT slice API was removed
432432
before the `1.0` stable boundary because it can leave real decoded plaintext
433433
from valid leading quanta in `output` when later malformed input is rejected
434434
after the fixed-shape decode pass.
435+
For shared-memory, HSM-adjacent, sandboxed, or other multi-principal threat
436+
models where even transient writes to caller-owned output are unacceptable, use
437+
`ct::CtEngine::decode_slice_staged_clear_tail` with a private staging buffer.
435438

436439
For short values, `encode_buffer` returns a stack-backed `EncodedBuffer`
437440
and `decode_buffer` returns a stack-backed `DecodedBuffer` without requiring
@@ -478,9 +481,11 @@ bearer-token, password-hash, or authentication-secret comparison primitive in
478481
high-assurance systems.
479482

480483
`into_exposed_array` is the explicit no-alloc ownership escape hatch for both
481-
stack-backed buffers. It returns the backing array and visible length, so
482-
redacted formatting and drop-time cleanup no longer apply to the returned
483-
array.
484+
stack-backed buffers. It returns `ExposedEncodedArray` or
485+
`ExposedDecodedArray`, keeping redacted formatting and best-effort drop-time
486+
cleanup after ownership transfer. If a bare array is unavoidable, call
487+
`into_exposed_unprotected_array_caller_must_zeroize`; cleanup then becomes the
488+
caller responsibility.
484489

485490
Stack-backed buffers clear their backing arrays when dropped, but they cannot
486491
clear historical stack-frame copies made by the compiler, caller code, panic

docs/API_AUDIT.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,11 @@ Decision rationale:
133133
Base64 text through explicit display/text APIs.
134134
- Drop-time cleanup is best-effort and scoped to the buffer's current backing
135135
array, not historical stack-frame copies.
136-
- `into_exposed_array` is intentionally named as an ownership escape hatch
137-
where redaction and drop-time cleanup stop applying to the returned array.
136+
- `into_exposed_array` is intentionally named as an ownership escape hatch and
137+
returns `ExposedEncodedArray` or `ExposedDecodedArray`, preserving redaction
138+
and drop-time cleanup after ownership transfer. The raw-array escape hatch is
139+
deliberately loud:
140+
`into_exposed_unprotected_array_caller_must_zeroize`.
138141
- Equality is intentionally not exposed through `PartialEq`/`==`. Callers must
139142
opt into the explicit `constant_time_eq_public_len` helper, whose equal-length
140143
scan is best-effort and whose length mismatch remains public.

docs/CONSTANT_TIME.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -263,14 +263,14 @@ partial plaintext before the final wipe.
263263
Before reporting the opaque malformed-input result, the ct decoder passes the
264264
accumulated error mask through a non-inlined `ct_error_gate_barrier` that uses
265265
`core::hint::black_box`, a compiler fence, and architecture-specific hardware
266-
speculation barriers where available (`lfence` on x86/x86_64, `isb sy` on ARM,
267-
and `isb sy; hint #20` on AArch64). On RISCV the gate uses `fence rw, rw`,
268-
which is an ordering fence rather than a canonical speculation barrier in the
269-
base ISA. The runtime backend report exposes this separately through
270-
`ct_gate_posture`. This is defense in depth around the final public
271-
success/failure gate; it does not make the ct decoder a formally verified
272-
hardware side-channel resistant primitive and does not change the transient
273-
output window described above.
266+
speculation barriers where available (`lfence` on x86/x86_64 and
267+
`isb sy; hint #20` on AArch64). On 32-bit ARM the gate uses `isb sy`, and on
268+
RISCV it uses `fence rw, rw`; both are reported as `ordering-fence` because the
269+
base ISA path is not a canonical Spectre-v1 data-flow speculation barrier. The
270+
runtime backend report exposes this separately through `ct_gate_posture`. This
271+
is defense in depth around the final public success/failure gate; it does not
272+
make the ct decoder a formally verified hardware side-channel resistant
273+
primitive and does not change the transient output window described above.
274274

275275
For shared-memory or in-process sandbox threat models where even that transient
276276
output window is unacceptable, use
@@ -316,6 +316,10 @@ CT gate barrier used by malformed-input reporting. The helper is marked
316316
`constant_time_eq_public_len` remains visible as a separate text symbol in the
317317
LTO artifact.
318318

319+
When the value length itself should not be modeled as a runtime branch, use
320+
`constant_time_eq_fixed_width` with fixed-size arrays. Its length is a
321+
compile-time type fact and it scans exactly that width before returning.
322+
319323
This remains a best-effort API and does not upgrade `base64-ng` to a formally
320324
verified cryptographic constant-time comparison crate. Do not use this helper
321325
as the sole MAC, bearer-token, password-hash, or authentication-secret

docs/SECURITY_CONTROLS.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ The caller still owns:
3838
- choosing strict, legacy, wrapped, MIME, PEM, bcrypt-style, or custom profiles
3939
intentionally
4040
- avoiding accidental copies of secrets before wrapping them in `SecretBuffer`
41-
- treating stack-buffer `into_exposed_array` calls as boundaries where redacted
42-
formatting and drop-time cleanup intentionally stop for the returned arrays
41+
- treating
42+
`into_exposed_unprotected_array_caller_must_zeroize` calls as boundaries
43+
where redacted formatting and crate-owned drop-time cleanup intentionally
44+
stop for returned bare arrays
4345
- understanding that stack-backed buffers can clear their own backing arrays
4446
but cannot clear historical stack-frame copies made by compiler spills,
4547
caller code, panic machinery, crash handlers, or operating system capture
@@ -104,9 +106,12 @@ for secret regions, and the deployment's approved zeroization primitive at the
104106
ownership boundary.
105107
For constant-time-oriented decode, use `ct::CtEngine::decode_slice_clear_tail`
106108
or `ct::CtEngine::decode_buffer` when a caller-owned output buffer may be
107-
reused after a rejected input. The non-clear-tail CT slice API was removed
108-
before the `1.0` stable boundary because it could leave decoded plaintext from
109-
valid leading quanta in the buffer on error.
109+
reused after a rejected input. For shared-memory, HSM-adjacent, sandboxed, or
110+
multi-principal environments where even transient writes to caller output are
111+
unacceptable, use `ct::CtEngine::decode_slice_staged_clear_tail` with a private
112+
staging buffer. The non-clear-tail CT slice API was removed before the `1.0`
113+
stable boundary because it could leave decoded plaintext from valid leading
114+
quanta in the buffer on error.
110115
The internal constant-time-oriented decode loop writes decoded bytes to the
111116
caller-owned output buffer before final malformed-input reporting, then
112117
`decode_slice_clear_tail` wipes the buffer before returning an error. This

docs/UNSAFE.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ Purpose:
151151

152152
- Keep the accumulated constant-time decoder malformed-input mask visible
153153
across a non-inlined boundary before the public success/failure branch.
154-
- Emit an architecture-specific speculation barrier where stable Rust supports
155-
one locally.
154+
- Emit an architecture-specific speculation or ordering barrier where stable
155+
Rust supports one locally.
156156

157157
Preconditions:
158158

@@ -161,7 +161,7 @@ Preconditions:
161161
Unsafe operation:
162162

163163
- `core::arch::asm!` emits `lfence` on non-Miri `x86`/`x86_64`, `isb sy` on
164-
non-Miri `arm`, `isb sy; hint #20` on non-Miri `aarch64`, and
164+
non-Miri 32-bit `arm`, `isb sy; hint #20` on non-Miri `aarch64`, and
165165
`fence rw, rw` on non-Miri `riscv32`/`riscv64`.
166166

167167
Safety argument:
@@ -177,9 +177,12 @@ Limitations:
177177
- This is defense in depth against speculation around the final public
178178
malformed-input result. It does not make the ct decoder a formally verified
179179
hardware side-channel resistant primitive.
180-
- RISC-V base ISA has no canonical speculation barrier. The crate reports its
181-
CT gate posture as `ordering-fence` on RISCV rather than
182-
`hardware-speculation-barrier`.
180+
- 32-bit ARM uses `isb sy` without CSDB, and RISC-V base ISA has no canonical
181+
speculation barrier. The crate reports both CT gate postures as
182+
`ordering-fence` rather than `hardware-speculation-barrier`.
183+
- On AArch64, the CSDB hint may be treated as a no-op on older cores. The
184+
runtime posture reports the emitted barrier sequence, not a formal
185+
microarchitecture certification.
183186
- Unsupported architectures fall back to the compiler fence only.
184187

185188
### `wipe_vec_spare_capacity`

0 commit comments

Comments
 (0)