Skip to content

Commit f132a69

Browse files
committed
Clarify high-assurance audit caveats
1 parent 1555825 commit f132a69

7 files changed

Lines changed: 46 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@
5151
- Hardened the workspace publish helper so real crates.io publishing requires
5252
`HEAD` to match a verified signed `v<version>` tag, and documented the
5353
`git tag -v` release check.
54+
- Tightened high-assurance documentation after follow-up audit review: clarified
55+
conservative tail wiping, `DecodedBuffer` clone duplication, public-length
56+
`subtle` comparisons, strict decode error logging, AArch64/RISC-V deployment
57+
policy checks, and wrapped in-place decode retention behavior.
5458

5559
## 1.1.0 - 2026-06-20
5660

crates/base64-ng-subtle/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
//!
1313
//! Length is treated as public. Mismatched lengths return
1414
//! [`subtle::Choice::from(0)`] immediately. Use fixed-size protocol tokens when
15-
//! length must not vary.
15+
//! length must not vary. When the length itself is secret, compare fixed-size
16+
//! arrays or fixed-width protocol buffers directly with
17+
//! [`subtle::ConstantTimeEq`] instead of this public-length helper.
1618
1719
use base64_ng::{DecodedBuffer, EncodedBuffer};
1820
use subtle::{Choice, ConstantTimeEq};
@@ -77,6 +79,9 @@ impl SubtleEqExt for SecretBuffer {
7779
///
7880
/// Equal-length comparisons are delegated to [`subtle::ConstantTimeEq`].
7981
/// Mismatched lengths return `Choice::from(0)` immediately.
82+
///
83+
/// Use [`subtle::ConstantTimeEq`] directly on fixed-size arrays or fixed-width
84+
/// protocol buffers when token length must not be observable.
8085
#[must_use = "use Choice or convert it deliberately with bool::from(choice)"]
8186
pub fn subtle_ct_eq_public_len(left: &[u8], right: &[u8]) -> Choice {
8287
if left.len() == right.len() {

docs/SECURITY_CONTROLS.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ certification claim.
8787
On AArch64, failure is expected unless the operator has attested CSDB
8888
effectiveness with `--cfg base64_ng_aarch64_csdb_attested`; do not use that
8989
cfg without processor, BSP, or certification evidence.
90+
Deployment CI for a certified AArch64 target should run the application or
91+
integration test suite with the same `RUSTFLAGS` and assert that this startup
92+
policy passes. Generic project CI must not set that cfg because it would turn
93+
an operator attestation into an unreviewed build default. RISC-V and 32-bit
94+
ARM targets should expect this policy to fail unless the platform provides
95+
external Spectre-v1 mitigation evidence.
9096
- Decode tokens, wrapped keys, MAC-adjacent payloads, and equivalent
9197
secret-bearing Base64 through `base64_ng::ct`. Use
9298
`ct::CtEngine::decode_slice_staged_clear_tail` when the caller-owned output
@@ -130,6 +136,13 @@ secret-adjacent tokens, or attacker-probed fragments of those values. Use
130136
`DecodeError::kind()` for redacted logging, or use the `ct` module for opaque
131137
malformed-content errors.
132138

139+
```rust
140+
let err = base64_ng::STANDARD
141+
.decode_buffer::<32>(input)
142+
.unwrap_err();
143+
tracing::warn!(error_kind = %err.kind(), "base64 decode failed");
144+
```
145+
133146
Legacy whitespace handling is opt-in through explicitly named APIs. Wrapped
134147
profiles are strict about the configured line ending and non-final line width.
135148

src/buffers/decoded.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ use crate::{DecodeError, STANDARD, constant_time_eq_public_len, wipe_bytes, wipe
1010
/// The backing array is cleared when the value is dropped. This is best-effort
1111
/// data-retention reduction and is not a formal zeroization guarantee.
1212
///
13+
/// # Security: cloning
14+
///
15+
/// This type implements [`Clone`] for no-alloc ergonomics, but cloning
16+
/// duplicates decoded bytes. The compiler may also create temporary
17+
/// intermediates during the copy that are outside this crate's cleanup
18+
/// boundary. For heap-owned key material, prefer the alloc-gated
19+
/// `SecretBuffer`, which does not implement `Clone`.
20+
///
1321
/// On `wasm32` targets, the wipe barrier uses only a compiler fence. The wasm
1422
/// runtime JIT may still optimize or retain cleared bytes in ways this crate
1523
/// cannot control. `wasm32` builds fail closed by default; enable

src/cleanup.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ fn wipe_barrier(ptr: *mut u8, len: usize) {
9595
pub(crate) fn wipe_tail(bytes: &mut [u8], start: usize) {
9696
debug_assert!(start <= bytes.len(), "wipe_tail start exceeds slice length");
9797
if start > bytes.len() {
98+
// A caller that asks to wipe past the end has violated the helper's
99+
// invariant. In release builds, fail closed by wiping everything
100+
// instead of silently retaining bytes because of a bad offset.
98101
wipe_bytes(bytes);
99102
return;
100103
}

src/engine/decode_in_place.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ where
2626
/// Use
2727
/// [`Self::decode_in_place_wrapped_clear_tail`] when the buffer may be
2828
/// reused or freed without a caller-managed wipe; treat that clear-tail
29-
/// variant as the default for secret-bearing wrapped payloads.
29+
/// variant as the default for secret-bearing wrapped payloads. If the
30+
/// original encoded input must be preserved for audit logging or retry,
31+
/// copy it before calling any in-place decode method or use a slice-output
32+
/// decode API instead.
3033
///
3134
/// # Examples
3235
///

src/errors.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ impl std::error::Error for EncodeError {}
6363
/// intentionally prints those diagnostics for developer-facing debugging. Do
6464
/// not log or return full [`DecodeError`] values for secret-bearing input; log
6565
/// [`Self::kind`] instead.
66+
///
67+
/// ```
68+
/// use base64_ng::STANDARD;
69+
///
70+
/// let err = STANDARD.decode_buffer::<8>(b"!!!!").unwrap_err();
71+
/// // Production logs should use the redacted class, not `{err}`.
72+
/// assert_eq!(err.kind().as_str(), "invalid-byte");
73+
/// ```
6674
#[derive(Clone, Copy, Eq, PartialEq)]
6775
pub enum DecodeError {
6876
/// The encoded input is malformed, but the decoder intentionally does not

0 commit comments

Comments
 (0)