Skip to content

Commit cfa3246

Browse files
committed
Add subtle companion crate
1 parent 40fff5b commit cfa3246

19 files changed

Lines changed: 301 additions & 24 deletions

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
- Wiped the small unpadded in-place decode tail buffer before return, expanded
3232
test-only AArch64 NEON prototype cleanup to all vector registers, and
3333
documented the wrapped slice encoder's temporary in-buffer staging behavior.
34+
- Added `base64-ng-subtle` as an optional companion crate for projects that
35+
already admit `subtle` and want reviewed `ConstantTimeEq` comparisons for
36+
`base64-ng` buffers.
3437
- Added a real non-dispatchable AVX2 fixed-block encode prototype for Standard
3538
and URL-safe alphabets. The prototype remains test-only and active runtime
3639
backend selection remains scalar-only.

Cargo.lock

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ members = [
3838
"crates/base64-ng-derive",
3939
"crates/base64-ng-sanitization",
4040
"crates/base64-ng-serde",
41+
"crates/base64-ng-subtle",
4142
"crates/base64-ng-tokio",
4243
]
4344
default-members = ["."]

README.md

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,9 @@ Implemented now:
7474
clear-on-drop secret containers.
7575
- Optional `base64-ng-derive` companion crate for fixed-size byte newtypes that
7676
need narrow Base64 parsing and encoding helpers.
77-
- Optional `base64-ng-serde`, `base64-ng-bytes`, and `base64-ng-tokio`
78-
companion crates for projects that explicitly admit those ecosystem
79-
dependencies.
77+
- Optional `base64-ng-serde`, `base64-ng-bytes`, `base64-ng-subtle`, and
78+
`base64-ng-tokio` companion crates for projects that explicitly admit those
79+
ecosystem dependencies.
8080
- Local check scripts, release gate, dependency policy, audit config, CI, SBOM script, reproducible build check, and a 500-line production-source budget guard.
8181

8282
Planned behind admission evidence:
@@ -162,9 +162,10 @@ The core `base64-ng` crate keeps its zero-runtime-dependency policy. Optional
162162
ecosystem integrations live as separate crates so applications can opt into
163163
their own approved dependency set without changing the base package.
164164

165-
The `1.1.0` release changes only the core crate. Optional companion crates
166-
remain on their previous published versions, and Cargo's normal compatible
167-
dependency ranges allow those companions to resolve with `base64-ng` `1.1.0`.
165+
The `1.1.0` release changes the core crate and adds the optional
166+
`base64-ng-subtle` companion crate. Other optional companion crates remain on
167+
their previous published versions, and Cargo's normal compatible dependency
168+
ranges allow those companions to resolve with `base64-ng` `1.1.0`.
168169

169170
| Crate | Purpose |
170171
| --- | --- |
@@ -173,6 +174,7 @@ dependency ranges allow those companions to resolve with `base64-ng` `1.1.0`.
173174
| `base64-ng-derive` | Dependency-free `Base64Secret` derive for fixed-size secret newtypes. |
174175
| `base64-ng-serde` | Optional `serde` wrappers for projects that already admit `serde`. |
175176
| `base64-ng-bytes` | Optional `bytes` helpers for `Bytes`, `Buf`, and `BufMut` users. |
177+
| `base64-ng-subtle` | Optional `subtle::ConstantTimeEq` helpers for token/MAC comparison boundaries. |
176178
| `base64-ng-tokio` | Optional bounded Tokio async helpers for services that already admit Tokio. |
177179

178180
Subcrates are documented so crate pages are readable, but they belong to the
@@ -257,6 +259,23 @@ let encoded = STANDARD.encode_bytes(b"hello").unwrap();
257259
assert_eq!(&encoded[..], b"aGVsbG8=");
258260
```
259261

262+
`base64-ng-subtle` provides explicit `subtle::ConstantTimeEq` integration for
263+
projects that already admit `subtle`:
264+
265+
```toml
266+
[dependencies]
267+
base64-ng = "1.1.0"
268+
base64-ng-subtle = "1.1.0"
269+
```
270+
271+
```rust
272+
use base64_ng::ct;
273+
use base64_ng_subtle::SubtleEqExt;
274+
275+
let decoded = ct::STANDARD.decode_secret(b"aGVsbG8=").unwrap();
276+
assert!(decoded.subtle_verify(b"hello"));
277+
```
278+
260279
`base64-ng-tokio` provides bounded async helpers for applications that already
261280
use Tokio:
262281

crates/base64-ng-subtle/Cargo.toml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
[package]
2+
name = "base64-ng-subtle"
3+
version = "1.1.0"
4+
edition = "2024"
5+
rust-version = "1.90"
6+
license = "MIT OR Apache-2.0"
7+
description = "Optional subtle::ConstantTimeEq helpers for base64-ng buffers"
8+
repository = "https://github.com/valkyoth/base64-ng"
9+
homepage = "https://github.com/valkyoth/base64-ng"
10+
documentation = "https://docs.rs/base64-ng-subtle"
11+
readme = "README.md"
12+
keywords = ["base64", "constant-time", "crypto", "encoding"]
13+
categories = ["cryptography", "encoding", "no-std"]
14+
include = [
15+
"src/**",
16+
"tests/**",
17+
"Cargo.toml",
18+
"README.md",
19+
]
20+
21+
[features]
22+
default = ["alloc"]
23+
alloc = ["base64-ng/alloc"]
24+
std = ["alloc", "base64-ng/std", "subtle/std"]
25+
26+
[dependencies]
27+
base64-ng = { version = "1.1.0", path = "../..", default-features = false }
28+
subtle = { version = "2.6.1", default-features = false }
29+
30+
[lints.rust]
31+
unsafe_code = "deny"
32+
missing_docs = "deny"
33+
34+
[lints.clippy]
35+
all = "deny"
36+
pedantic = "deny"
37+
38+
[package.metadata.docs.rs]
39+
all-features = true

crates/base64-ng-subtle/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# base64-ng-subtle
2+
3+
Optional `subtle::ConstantTimeEq` integration for `base64-ng`.
4+
5+
The core `base64-ng` crate stays zero-runtime-dependency. This companion crate
6+
is for applications that already admit the `subtle` crate and want explicit
7+
comparison helpers for decoded or encoded Base64 material.
8+
9+
```toml
10+
[dependencies]
11+
base64-ng = "1.1.0"
12+
base64-ng-subtle = "1.1.0"
13+
```
14+
15+
```rust
16+
use base64_ng::ct;
17+
use base64_ng_subtle::SubtleEqExt;
18+
19+
let decoded = ct::STANDARD.decode_secret(b"aGVsbG8=").unwrap();
20+
assert!(decoded.subtle_verify(b"hello"));
21+
```
22+
23+
Length is public: mismatched lengths return `Choice::from(0)` immediately.
24+
Use fixed-size protocol tokens when length must not vary.

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

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#![cfg_attr(not(feature = "std"), no_std)]
2+
#![deny(unsafe_code)]
3+
#![deny(missing_docs)]
4+
#![deny(clippy::all)]
5+
#![deny(clippy::pedantic)]
6+
7+
//! Optional `subtle::ConstantTimeEq` integration for `base64-ng`.
8+
//!
9+
//! The core `base64-ng` package stays zero-runtime-dependency. This companion
10+
//! crate exists for applications that already admit `subtle` and want a
11+
//! reviewed comparison primitive at the protocol boundary.
12+
//!
13+
//! Length is treated as public. Mismatched lengths return
14+
//! [`subtle::Choice::from(0)`] immediately. Use fixed-size protocol tokens when
15+
//! length must not vary.
16+
17+
use base64_ng::{DecodedBuffer, EncodedBuffer};
18+
use subtle::{Choice, ConstantTimeEq};
19+
20+
#[cfg(feature = "alloc")]
21+
use base64_ng::SecretBuffer;
22+
23+
/// Extension trait for comparing `base64-ng` buffers with `subtle`.
24+
///
25+
/// The comparison delegates equal-length byte comparisons to
26+
/// [`subtle::ConstantTimeEq`]. Length mismatch remains public and returns
27+
/// `Choice::from(0)`.
28+
pub trait SubtleEqExt {
29+
/// Compares `self` with `expected` using `subtle` for equal-length inputs.
30+
///
31+
/// Length is public. If lengths differ, this returns `Choice::from(0)`.
32+
#[must_use = "use Choice or convert it deliberately with bool::from(choice)"]
33+
fn subtle_ct_eq(&self, expected: &[u8]) -> Choice;
34+
35+
/// Convenience boolean wrapper around [`Self::subtle_ct_eq`].
36+
///
37+
/// Prefer [`Self::subtle_ct_eq`] when composing with other `subtle`
38+
/// decisions.
39+
#[must_use]
40+
fn subtle_verify(&self, expected: &[u8]) -> bool {
41+
bool::from(self.subtle_ct_eq(expected))
42+
}
43+
}
44+
45+
impl SubtleEqExt for [u8] {
46+
fn subtle_ct_eq(&self, expected: &[u8]) -> Choice {
47+
subtle_ct_eq_public_len(self, expected)
48+
}
49+
}
50+
51+
impl SubtleEqExt for &[u8] {
52+
fn subtle_ct_eq(&self, expected: &[u8]) -> Choice {
53+
subtle_ct_eq_public_len(self, expected)
54+
}
55+
}
56+
57+
impl<const CAP: usize> SubtleEqExt for DecodedBuffer<CAP> {
58+
fn subtle_ct_eq(&self, expected: &[u8]) -> Choice {
59+
subtle_ct_eq_public_len(self.as_bytes(), expected)
60+
}
61+
}
62+
63+
impl<const CAP: usize> SubtleEqExt for EncodedBuffer<CAP> {
64+
fn subtle_ct_eq(&self, expected: &[u8]) -> Choice {
65+
subtle_ct_eq_public_len(self.as_bytes(), expected)
66+
}
67+
}
68+
69+
#[cfg(feature = "alloc")]
70+
impl SubtleEqExt for SecretBuffer {
71+
fn subtle_ct_eq(&self, expected: &[u8]) -> Choice {
72+
subtle_ct_eq_public_len(self.expose_secret(), expected)
73+
}
74+
}
75+
76+
/// Compares two byte slices with public length.
77+
///
78+
/// Equal-length comparisons are delegated to [`subtle::ConstantTimeEq`].
79+
/// Mismatched lengths return `Choice::from(0)` immediately.
80+
#[must_use = "use Choice or convert it deliberately with bool::from(choice)"]
81+
pub fn subtle_ct_eq_public_len(left: &[u8], right: &[u8]) -> Choice {
82+
if left.len() == right.len() {
83+
left.ct_eq(right)
84+
} else {
85+
Choice::from(0)
86+
}
87+
}
88+
89+
#[cfg(test)]
90+
mod tests {
91+
use super::{SubtleEqExt, subtle_ct_eq_public_len};
92+
use base64_ng::STANDARD;
93+
94+
#[cfg(feature = "alloc")]
95+
use base64_ng::ct;
96+
97+
#[test]
98+
fn compares_raw_slices_with_public_length() {
99+
assert!(bool::from(subtle_ct_eq_public_len(b"hello", b"hello")));
100+
assert!(!bool::from(subtle_ct_eq_public_len(b"hello", b"world")));
101+
assert!(!bool::from(subtle_ct_eq_public_len(b"hello", b"hello!")));
102+
}
103+
104+
#[test]
105+
fn compares_stack_backed_buffers() {
106+
let decoded = STANDARD.decode_buffer::<5>(b"aGVsbG8=").unwrap();
107+
assert!(decoded.subtle_verify(b"hello"));
108+
assert!(!decoded.subtle_verify(b"world"));
109+
110+
let encoded = STANDARD.encode_buffer::<8>(b"hello").unwrap();
111+
assert!(encoded.subtle_verify(b"aGVsbG8="));
112+
assert!(!encoded.subtle_verify(b"aGVsbG8h"));
113+
}
114+
115+
#[cfg(feature = "alloc")]
116+
#[test]
117+
fn compares_secret_buffer() {
118+
let decoded = ct::STANDARD.decode_secret(b"aGVsbG8=").unwrap();
119+
assert!(decoded.subtle_verify(b"hello"));
120+
assert!(!decoded.subtle_verify(b"world"));
121+
}
122+
}

docs/CRATE_VERSION_MATRIX.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ sets.
1414
| `base64-ng-derive` | `1.0.9` | no | <https://crates.io/crates/base64-ng-derive> |
1515
| `base64-ng-serde` | `1.0.10` | no | <https://crates.io/crates/base64-ng-serde> |
1616
| `base64-ng-bytes` | `1.0.9` | no | <https://crates.io/crates/base64-ng-bytes> |
17+
| `base64-ng-subtle` | `1.1.0` | yes | <https://crates.io/crates/base64-ng-subtle> |
1718
| `base64-ng-tokio` | `1.0.9` | no | <https://crates.io/crates/base64-ng-tokio> |
1819

1920
## Release Policy

docs/DEPENDENCIES.md

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ new dependency expands the audit, license, advisory, and supply-chain surface.
3232
- `base64-ng-derive` is an optional companion package for fixed-size byte
3333
newtypes. It is dependency-free and does not add proc-macro machinery to the
3434
core `base64-ng` package.
35-
- `base64-ng-serde`, `base64-ng-bytes`, and `base64-ng-tokio` are optional
36-
companion packages for applications that already admit `serde`, `bytes`, or
37-
`tokio`; they are not dependencies of the core `base64-ng` package.
35+
- `base64-ng-serde`, `base64-ng-bytes`, `base64-ng-subtle`, and
36+
`base64-ng-tokio` are optional companion packages for applications that
37+
already admit `serde`, `bytes`, `subtle`, or `tokio`; they are not
38+
dependencies of the core `base64-ng` package.
3839
- Fuzz, performance, and dudect-style timing harness dependencies are isolated
3940
under `fuzz/`, `perf/`, and `dudect/`; the standard local gate checks them
4041
separately from the published crate dependency graph.
@@ -64,15 +65,21 @@ Current decisions:
6465
- `base64-ng-bytes` is admitted as a companion crate because services that
6566
already use `bytes` can opt into `Bytes`, `Buf`, and `BufMut` helpers without
6667
adding `bytes` to the core package.
68+
- `base64-ng-subtle` is admitted as a companion crate because authentication,
69+
MAC, password-hash, and token verification boundaries can opt into a reviewed
70+
`subtle::ConstantTimeEq` primitive without adding `subtle` to the core
71+
package.
6772
- `base64-ng-tokio` is admitted as a companion crate for bounded async
6873
read-all/write-all helpers. Full streaming state machines remain deferred
6974
until cancellation-safety and drop-cleanup evidence is complete.
7075
- The core `tokio` feature remains reserved and inert until async
7176
cancellation, drop cleanup, chunk-boundary, dependency, and release-evidence
7277
requirements are satisfied.
73-
- `zeroize` and `subtle` remain deferred; applications can combine their own
74-
approved dependencies with caller-owned buffers while `base64-ng` keeps its
75-
audited local best-effort helpers dependency-free.
78+
- `zeroize` remains deferred for the core crate; applications can combine their
79+
own approved dependencies with caller-owned buffers while `base64-ng` keeps
80+
its audited local best-effort helpers dependency-free.
81+
- `subtle` is admitted only through `base64-ng-subtle`, not through the core
82+
crate.
7683
- Property-testing and benchmark frameworks remain isolated or deferred; fuzz,
7784
dudect-style timing, and performance harnesses stay outside the published
7885
crate package.
@@ -131,9 +138,11 @@ core crate today:
131138
needed. The core crate does not admit `serde`.
132139
- `bytes`: use `base64-ng-bytes` when `Bytes`, `Buf`, or `BufMut` integration
133140
is needed. The core crate does not admit `bytes`.
134-
- `zeroize` or `subtle`: deferred unless a review proves that the dependency
135-
materially improves the documented best-effort cleanup or
136-
constant-time-oriented posture beyond the current audited local helpers.
141+
- `zeroize`: deferred unless a review proves that the dependency materially
142+
improves the documented best-effort cleanup posture beyond the current
143+
audited local helpers.
144+
- `subtle`: use `base64-ng-subtle` when protocol code needs a reviewed
145+
constant-time equality primitive for decoded or encoded buffers.
137146
- Criterion or other benchmark frameworks: keep benchmark evidence isolated
138147
unless the added dependency graph clearly improves release evidence quality.
139148

@@ -160,10 +169,10 @@ workspaces only when they are not packaged with the published crate:
160169
- `perf/` dependencies are reviewed by `scripts/check_perf.sh`.
161170
- `dudect/` dependencies are reviewed by `scripts/check_dudect.sh`.
162171
- `crates/base64-ng-sanitization/`, `crates/base64-ng-derive/`,
163-
`crates/base64-ng-serde/`, `crates/base64-ng-bytes/`, and
164-
`crates/base64-ng-tokio/` are optional companion crates, not dependencies of
165-
the core `base64-ng` package. They are reviewed separately by
166-
`scripts/check_companion_crates.sh` so the root package keeps its
172+
`crates/base64-ng-serde/`, `crates/base64-ng-bytes/`,
173+
`crates/base64-ng-subtle/`, and `crates/base64-ng-tokio/` are optional
174+
companion crates, not dependencies of the core `base64-ng` package. They are
175+
reviewed separately by `scripts/check_companion_crates.sh` so the root package keeps its
167176
zero-runtime-dependency guarantee.
168177

169178
`scripts/checks.sh` runs those isolated harness checks so ordinary local

docs/PLAN.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,10 @@ Recommended `1.0.x` source-layout sequence:
624624
wipe path without emitting unsafe code into downstream crates. Add
625625
`base64-ng-serde`, `base64-ng-bytes`, and `base64-ng-tokio` as explicit
626626
opt-in ecosystem companion crates. Keep the core crate dependency-free.
627+
- `1.1.x`: add `base64-ng-subtle` as an explicit companion crate for
628+
applications that already admit `subtle` and want reviewed
629+
`ConstantTimeEq` comparison helpers without adding `subtle` to the core
630+
crate.
627631
- `1.0.10`: source-layout maintenance release. Split oversized production
628632
modules into focused internal files, move Kani proofs and unit tests out of
629633
the root module, and add a 500-line production-source budget guard to the

0 commit comments

Comments
 (0)