base64-ng 2.0.0

no_std-first Base64 encoding and decoding with strict APIs and a security-heavy release process
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
# Constant-Time Decode Design

`base64-ng` does not currently claim a formally verified cryptographic
constant-time API. The scalar encoder and decoder avoid obvious timing pitfalls,
and the `ct` module now provides an initial constant-time-oriented scalar decode
path. The stable API still prioritizes strict correctness, small size, and
ordinary performance.

This document defines the bar for strengthening the `ct` module into a
cryptographic constant-time API claim.

The exact 2.0 evidence boundary, including the fixed-work pre-result-gate
scope, success-only post-gate release copy, target classification, and cleanup
revision binding, is recorded in
[`2.0_TIMING_AND_CODEGEN.md`](2.0_TIMING_AND_CODEGEN.md).

## Goal

Provide a clearly named API for callers that handle secret-bearing Base64
payloads:

```rust
use base64_ng::ct;

let mut staging = [0u8; 32];
let mut output = [0u8; 32];
let written = ct::STANDARD
    .decode_slice_staged_clear_tail(b"...", &mut output, &mut staging)?;
```

The API should be separate from the default strict decoder so users can choose
the tradeoff explicitly.

## Default Decoder Timing

The default `Engine` decode APIs are strict scalar decoders, not constant-time
decoders. They intentionally preserve exact error indexes and fast rejection for
malformed padding, invalid bytes, undersized outputs, and invalid lengths. That
means default methods such as `decode_slice`, `decode_in_place`,
`validate_result`, profile decoders, and stream adapters may branch or return
early based on malformed input content.

Treat the named default engines and profiles, including `STANDARD`,
`STANDARD_NO_PAD`, `URL_SAFE`, `URL_SAFE_NO_PAD`, `MIME`, `PEM`, `BCRYPT`, and
`CRYPT`, as strict interoperability APIs rather than token-comparison or
key-material decode APIs. For sensitive payloads, use the matching `ct`
constant such as `ct::STANDARD` or `ct::URL_SAFE_NO_PAD`, or promote an engine
with `Engine::ct_decoder()`.

Use the `base64_ng::ct` module for secret-bearing payloads where timing posture
matters more than localized malformed-input diagnostics. The `ct` module still
documents public length, output length, and final success/failure as public
values; callers with stricter protocol requirements must continue processing
with dummy data at the application layer.

High-assurance deployments that use the `ct` module should also consider
enforcing `runtime::BackendPolicy::HighAssuranceScalarOnly` at startup. That
keeps execution on the audited scalar backend and avoids future SIMD-induced
timing variation unless an accelerated backend has been admitted with its own
side-channel evidence.

For sensitive deployments, make that check a startup gate rather than an audit
log hint:

```rust
use base64_ng::runtime::{require_backend_policy, BackendPolicy};

require_backend_policy(BackendPolicy::HighAssuranceScalarOnly)
    .expect("base64-ng posture check failed: CT gate not attested on this core");
```

Deployments that want a compile-time fail-closed eligibility guard can build
with `base64_ng_require_high_assurance`. It requires `secrets` and rejects
unsupported or unattested speculation-barrier targets:

```sh
RUSTFLAGS="--cfg base64_ng_require_high_assurance" \
  cargo build --no-default-features --features secrets
```

This is a custom cfg rather than a Cargo feature so normal `--all-features`
release evidence and docs.rs builds can continue to exercise every public
feature. Treat the cfg as a deployment policy assertion and keep the runtime
`require_backend_policy(BackendPolicy::HighAssuranceScalarOnly)` startup gate.
This eligibility check does not attest protected storage. Commit 22 requires a
runtime assurance token and allocation-specific `ProtectedSecret` before the
2.0 API authorizes an assured operation. `secrets + simd` can coexist for
ordinary accelerated APIs; the assured secret path remains scalar. Use the
legacy `HighAssuranceScalarOnly` runtime policy when the complete process, not
only assured secret operations, must reject ordinary SIMD.

`HighAssuranceScalarOnly` is still a build and target posture assertion. On
AArch64, the crate emits `isb sy` plus the CSDB hint for the CT result gate.
By default this reports `CtGatePosture::HardwareSpeculationBarrierUnattested`
so the built-in `HighAssuranceScalarOnly` policy does not pass without
deployment-side evidence. Operators that have verified CSDB effectiveness
through processor documentation, BSP notes, or platform certification may build
with `--cfg base64_ng_aarch64_csdb_attested`; that cfg is an operator
attestation, not an automatic runtime CPU probe, and is intentionally not a
Cargo feature. With that cfg, runtime reports use
`hardware-speculation-barrier-build-asserted` so audit logs distinguish
operator attestation from a native target guarantee. On RISC-V, the crate reports
`CtGatePosture::OrderingFence`; the base ISA provides memory ordering, not a
canonical Spectre-v1 speculation barrier. RISC-V deployments with speculative
execution threat models need platform-level mitigations outside this crate.
In deployment checklists for RISC-V systems with a Spectre-v1 threat model,
assert the policy at startup and treat a failure as expected until the
platform provides an approved mitigation:

```rust
base64_ng::runtime::require_backend_policy(
    base64_ng::runtime::BackendPolicy::HighAssuranceScalarOnly,
)
.expect("RISC-V builds need platform-level speculation mitigation");
```

The dependency-free equality helpers are also best-effort review aids, not
FIPS-validated comparison primitives. High-assurance authentication boundaries
should use a deployment-approved constant-time comparison primitive for MACs,
bearer tokens, password hashes, and equivalent protocol decisions.

For the final 2.0 secret owners and views, the optional `base64-ng-subtle`
companion provides the sealed `SubtleSecretEq` trait. Its only method is
`subtle_ct_eq_public_len`, and it returns `subtle::Choice` without a boolean
convenience method. The final secret types do not implement `PartialEq` and do
not expose the dependency-free 1.x equality sugar. See
`2.0_SUBTLE_EQUALITY.md` for exact scope and evidence.

## Non-Goals

- Do not describe Base64 itself as cryptography.
- Do not claim whole-program constant-time behavior.
- Do not make SIMD the first constant-time target.
- Do not hide the performance tradeoff behind the default APIs.
- Do not promise guarantees that are not backed by tests and generated-code
  review.

## Proposed Guarantee

The scalar constant-time decoder should aim to document this narrow guarantee
once the verification requirements below are complete:

> For a fixed input length and selected alphabet, the scalar constant-time
> decoder performs no secret-dependent branches and no secret-indexed table
> lookups while mapping Base64 bytes to decoded output.

The guarantee should explicitly exclude:

- public input length
- selected engine/alphabet
- final success or failure result
- total protocol work performed after the public `Result` is returned
- invalid length and output-buffer capacity errors
- output length
- allocator behavior
- memory cleanup and zeroization behavior
- OS scheduling, interrupts, and unrelated system noise

## API Shape

The initial API prefers caller-owned buffers:

```rust
pub mod ct {
    pub const STANDARD: CtEngine<Standard, true>;
    pub const STANDARD_NO_PAD: CtEngine<Standard, false>;
    pub const URL_SAFE: CtEngine<UrlSafe, true>;
    pub const URL_SAFE_NO_PAD: CtEngine<UrlSafe, false>;

    impl<A, const PAD: bool> CtEngine<A, PAD> {
        pub fn validate_result(&self, input: &[u8]) -> Result<(), DecodeError>;

        pub fn validate(&self, input: &[u8]) -> bool;

        pub fn decode_slice_clear_tail(
            &self,
            input: &[u8],
            output: &mut [u8],
        ) -> Result<usize, DecodeError>;

        pub fn decode_buffer<const CAP: usize>(
            &self,
            input: &[u8],
        ) -> Result<DecodedBuffer<CAP>, DecodeError>;

        pub fn decode_in_place_clear_tail<'a>(
            &self,
            buffer: &'a mut [u8],
        ) -> Result<&'a mut [u8], DecodeError>;
    }
}
```

The stack-backed `decode_buffer` helper avoids allocator behavior while keeping
the same cleanup and redacted formatting posture as `DecodedBuffer`.

The normal `Engine::decode_slice`, `Profile::decode_slice`,
`decode_slice_legacy`, and `decode_slice_wrapped` methods are documented with a
`# Security` section and a `#[must_use]` attribute. Those methods remain the
right APIs for strict diagnostics and ordinary throughput, but they may branch
or return early on malformed input. Secret-bearing payloads should use the
`ct` module, preferably `decode_slice_clear_tail` or `decode_buffer`.

## Implementation Rules

- Accumulate validity into masks instead of returning early on input-dependent
  byte classes.
- Avoid lookup tables indexed by input bytes or decoded 6-bit values.
- Decode symbols with a fixed scan over the selected alphabet so standard,
  URL-safe, bcrypt-style, crypt-style, and custom alphabets share the same
  generic mapping rule.
- Decode all complete quanta for the public input length before reporting
  malformed input.
- Keep padding validation explicit and documented; padding length and final
  output length are public.
- Return one opaque, non-localized malformed-content error from the
  constant-time-oriented path. Use the normal strict decoder when exact error
  indexes or malformed-input categories are required.
- Generate byte masks with integer arithmetic helpers instead of a generic
  `bool`-to-mask conversion. Generated-code review is still required before a
  formal constant-time claim.
- Keep the Base64 symbol-mapping and decode logic scalar and `unsafe`-free.
- Clear-tail cleanup uses the audited volatile wipe helpers documented in
  `docs/UNSAFE.md`.
- Keep the module independent from future SIMD dispatch.

## Verification Requirements

Before documenting the guarantee as formally supported:

- Unit tests for all RFC 4648 vectors.
- Exhaustive short-input tests for all byte combinations practical under the
  test budget.
- Differential tests against the strict scalar decoder for canonical inputs.
- Malformed-input tests covering invalid bytes, mixed alphabets, padding, and
  non-canonical trailing bits.
- Miri coverage for the constant-time module.
- dudect-style fixed-vs-random timing evidence for the supported release
  binaries covered by the claim.
- Generated-code review for supported release targets.
- A release note that states the exact guarantee and exclusions.

Until this evidence exists, README and SECURITY must continue to say that the
`ct` module is constant-time-oriented and does not claim a formally verified
cryptographic constant-time API.

## LTO Caveat

The CT decoder's `#[inline(never)]`, `core::hint::black_box`, volatile reads,
and speculation barriers are best-effort source and generated-code controls.
Link-Time Optimization (`lto = true` or `lto = "thin"`) can change code shape
across crate boundaries and may weaken assumptions made by per-crate review.
High-assurance deployments must treat dudect and generated-assembly evidence as
toolchain-, target-, and profile-specific. Re-run the evidence scripts for the
exact release profile, and disable cross-crate LTO for code containing CT decode
unless the generated artifacts for that exact build have been reviewed.

## Generated-Code Review

Before changing the documentation from "constant-time-oriented" to a formal
cryptographic constant-time claim, maintainers must inspect generated code for
every supported release target and feature mode covered by the claim.

Minimum local commands:

```sh
scripts/generate_ct_asm_evidence.sh
```

Normal CI and the release gate run this script. It writes release assembly
artifacts and a checksum manifest under `target/release-evidence/asm/` for
no-default-features, all-features, and all-features LTO builds. It wraps these
raw compiler invocations:

```sh
cargo rustc --release --lib --no-default-features -- --emit=asm
cargo rustc --release --lib --all-features -- --emit=asm
RUSTFLAGS="-C lto=fat -C embed-bitcode=yes" cargo rustc --release --lib --all-features -- --emit=asm
```

Target-specific reviews must also include the targets named in the release
claim. For example:

```sh
cargo rustc --release --lib --no-default-features --target x86_64-unknown-linux-gnu -- --emit=asm
cargo rustc --release --lib --no-default-features --target aarch64-unknown-linux-gnu -- --emit=asm
```

The review must check the scalar `ct` decode mapping and padding/error
tracking code for:

- no secret-indexed loads from alphabet or decode tables
- no branches whose condition is derived from secret input byte classes
- no early returns after malformed content is discovered inside fixed-length
  decode loops
- no optimizer-introduced control flow that invalidates the documented mask
  arithmetic assumptions
- no accidental dispatch into future SIMD code
- high-assurance deployments that require scalar-only timing posture also
  enforce `runtime::BackendPolicy::HighAssuranceScalarOnly`

Generated assembly and reviewer notes should be archived with release evidence
if a formal claim is made. Without that evidence, public documentation must keep
the current non-claim wording.
The reviewer checklist and current release position live in
[CT_ASM_REVIEW.md](CT_ASM_REVIEW.md).

This policy is release-gated by:

```sh
scripts/validate-constant-time-policy.sh
```

## dudect-Style Timing Evidence

`dudect/` contains an isolated, dependency-free timing harness for the bounded
2.0 secret decoder and encoder. It separates decode valid contents, malformed
positions, malformed classes, and the pre-gate core, then compares fixed and
randomized encode contents for built-in and custom alphabet mapping. Normal CI and the release gate
compile the harness and check its dependency policy. Local timing runs are
opt-in because virtualized CI runners and busy developer machines can produce
noisy measurements:

```sh
BASE64_NG_RUN_DUDECT=1 scripts/check_dudect.sh
```

See [DUDECT.md](DUDECT.md) for the exact command contract and evidence rules.

## Memory Cleanup

The `ct` module provides clear-tail decode variants for caller-owned buffers.
They clear unused bytes after the decoded prefix on success and clear the whole
caller-owned buffer on error. This reduces ordinary caller-buffer retention but
does not provide a verified zeroization guarantee.

The non-clear-tail `ct::CtEngine::decode_slice` and
`ct::CtEngine::decode_in_place` APIs were removed before the `1.0` stable
boundary. They could leave decoded plaintext in caller-owned buffers after a
malformed input error. Use `decode_slice_clear_tail`, `decode_buffer`, or
`decode_in_place_clear_tail` for constant-time-oriented decoding.

The clear-tail slice decoder still writes decoded bytes to caller-owned output
during the fixed-shape decode loop before it reports a malformed-input error.
On error it wipes the output before returning, but this is not a synchronization
or process-isolation boundary. A same-process observer with concurrent or
unsafe access to the output buffer during the call could observe transient
partial plaintext before the final wipe.

Before reporting the opaque malformed-input result, the ct decoder passes the
accumulated error mask through a non-inlined `ct_error_gate_barrier` that uses
`core::hint::black_box`, a compiler fence, and architecture-specific hardware
speculation barriers where available (`lfence` on x86/x86_64 and
`isb sy; hint #20` on AArch64). AArch64 reports
`hardware-speculation-barrier-unattested` because the crate cannot prove the
deployed core treats CSDB as effective. On 32-bit ARM the gate uses `isb sy`,
and on RISCV it uses `fence rw, rw`; both are reported as `ordering-fence`
because the base ISA path is not a canonical Spectre-v1 data-flow speculation
barrier. The runtime backend report exposes this separately through
`ct_gate_posture`. This is defense in depth around the final public
success/failure gate; it does not make the ct decoder a formally verified
hardware side-channel resistant primitive and does not change the transient
output window described above.

The 2.0 `SecretArrayFrame`, `SecretFrame`, and `SecretVecFrame` paths use a
separate scalar `SecretDecoderState`, the same accumulator and result-gate
barrier, and byte-disjoint private staging. Commit 19 covers fixed symbol work
before the gate. Final validity and the success-only copy are public. The
bounded frame contract and retained evidence are documented in
`2.0_SECRET_DECODING.md`; this still does not constitute a formally verified
hardware constant-time claim.

For shared-memory or in-process sandbox threat models where even that transient
output window is unacceptable, use
`CtEngine::decode_slice_staged_clear_tail` with a private staging buffer. That
API writes speculative decoded bytes into staging and copies into the caller's
output only after validation succeeds.

The clear-tail APIs do not try to hide success, failure, or output length:
those values are visible through the returned `Result` and decoded length. Any
future cryptographic profile must document memory cleanup separately from timing
behavior.

Applications that must hide success/failure timing at the protocol level should
continue with fixed-shape downstream work after decode failure. A common pattern
is to decode into caller-owned storage, substitute a same-length dummy buffer on
failure, and perform the same comparison, authentication, accounting, and
cleanup steps before returning a protocol decision.

## Buffer Comparisons

`SecretBuffer::constant_time_eq_public_len`,
`EncodedBuffer::constant_time_eq_public_len`, and
`DecodedBuffer::constant_time_eq_public_len` provide dependency-free,
constant-time-oriented comparison for equal-length buffers. These redacted
buffer types intentionally do not implement `PartialEq`/`==`: the explicit
method name is part of the security contract because this helper is best-effort
and not a formal cryptographic comparison primitive.

The old `constant_time_eq` method name was removed before `1.0.0`. Use
`constant_time_eq_public_len` so the public-length contract remains
visible at the call site.

Length mismatch returns immediately. Treat buffer length, the selected buffer
type, and the final equality result as public. The helper scans every byte for
equal-length inputs before returning. The per-byte difference is passed through
`core::hint::black_box`; the accumulator is also passed through `black_box`
after each OR reduction and then read through a volatile stack-local read to
reduce the risk of release-mode optimizer rewrites into early-exit equality
checks. Before returning the public equality result, the helper also passes the
accumulated difference through the same non-inlined CT gate barrier used by
malformed-input reporting. The helper is marked `#[inline(never)]` and the
release evidence script checks that
`constant_time_eq_public_len` remains visible as a separate text symbol in the
LTO artifact.

When the value length itself should not be modeled as a runtime branch, use
`constant_time_eq_fixed_width` with fixed-size arrays. Its length is a
compile-time type fact and it scans exactly that width before returning.

This remains a best-effort API and does not upgrade `base64-ng` to a formally
verified cryptographic constant-time comparison crate. Do not use this helper
as the sole MAC, bearer-token, password-hash, or authentication-secret
comparison primitive in high-assurance systems. Applications that require a
formally audited token, MAC, or password-hash comparison should admit that
dependency at the application boundary, for example by comparing exposed bytes
with `subtle`.