fix(sphragis)!: make deterministic encapsulation private - #31
Conversation
10a0851 to
65e79d1
Compare
encapsulate_deterministic accepted caller-supplied KEM randomness and was pub (gated only by #[doc(hidden)], which hides rustdoc output, not visibility) -- reachable from any downstream crate despite its own doc comment warning "never call with non-uniform or reused randomness." Reused or predictable coins deterministically collapse the ephemeral X25519 secret, the ML-KEM message, the ciphertext, and the shared secret -- the exact guarantee EncapsulationKey::encapsulate exists to provide. Drop pub and #[doc(hidden)]: the method stays a private inherent method, not cfg(test), because encapsulate() calls straight through to it with fresh OsRng bytes in every build, so it must compile unconditionally. Privacy alone already makes it unreachable outside this crate -- no downstream crate can name it, and no external conformance consumer exists today (akroasis, the only consumer of sphragis, never references it). Relocate the X-Wing draft known-answer test beside the implementation, in hybrid.rs's own #[cfg(test)] mod tests -- the integration test in tests/ compiles as a separate crate and can no longer see the now-private method. Add a compile_fail doctest on EncapsulationKey proving the API-surface assertion directly (basanos/standards/RUST.md Compile-Fail tests convention): calling encapsulate_deterministic from outside the crate must fail to build. BREAKING CHANGE: EncapsulationKey::encapsulate_deterministic is no longer part of the public API. No known external caller. Part of #17
…hing HybridKem #32 (sphragis#23) narrowed the public API but left the `SharedSecret` type alias unconditionally `pub`, reachable via the `sphragis::hybrid::SharedSecret` module path even without `hazmat` -- unlike `HybridKem`, which #32 gated at the type-definition level (`pub(crate)` without `hazmat`, `pub` with it). The doc comment on the alias explained why: `EncapsulationKey:: encapsulate_deterministic` returned a `SharedSecret` unconditionally as a `pub` (doc-hidden) method, so narrowing the alias's visibility would have left it leaking through that method's signature (`private_interfaces`, denied under `-D warnings`). `encapsulate_deterministic` is now a private method (this branch, #17), so that blocker is gone. Complete the gating `HybridKem` already has: split the alias into `pub(crate)` (without `hazmat`) / `pub` (with `hazmat`) variants, and correct the doc comment, which cited the now-superseded public-method reasoning. Part of #17
65e79d1 to
88e2c54
Compare
…vector index Relocating the X-Wing KAT into src/hybrid.rs's own #[cfg(test)] mod tests (this branch) moved it out of tests/known_answer_vectors.rs, which carries a file-level #![expect(clippy::indexing_slicing, ...)] covering its own `&doc[0]` vector-array access. A library test module has no such blanket exemption, so clippy::indexing_slicing (denied under -D warnings) fired on this test's own `&doc[0]` under --features preview-pq (no hazmat). Part of #17
|
Rebased onto current Does #32 subsume this PR? No. #32 narrowed the public API ( #27's guarantee, carried through the relocation. #27 upgraded the X-Wing KAT from a shared-secret-only check against hardcoded hex literals to a full sk/pk/ct/ss check reading from the vendored, hash-locked vector file ( Also completed while in this code: the CI green on the current head. |
"matching HybridKem's gating above" was wrong: HybridKem's pub(crate)/pub split is declared after SharedSecret's in this file, not before it. Reworded to avoid a positional claim that silently goes stale if either declaration moves again. Part of #16
🤖 I have created a release *beep* *boop* --- ## [0.2.0](v0.1.2...v0.2.0) (2026-08-16) ### ⚠ BREAKING CHANGES * **sphragis:** make deterministic encapsulation private ([#31](#31)) ### Features * **sphragis:** define revocation as key rotation, not re-wrapping ([#34](#34)) ([37ba8e3](37ba8e3)) * **sphragis:** narrow the public API to the envelope profile, define the adapter seam ([#32](#32)) ([d0a0bb8](d0a0bb8)) ### Bug Fixes * **sphragis:** bind the KAT gate to a machine-readable crypto provenance lock ([#27](#27)) ([65e3433](65e3433)) * **sphragis:** bound and fully consume untrusted CBOR before accepting a wrapped key ([#24](#24)) ([d165e5b](d165e5b)) * **sphragis:** make deterministic encapsulation private ([#31](#31)) ([cf48668](cf48668)) * **sphragis:** redact SharedSecret's Debug and retrofit SealError location ([#33](#33)) ([11216eb](11216eb)) * **sphragis:** return typed entropy failures instead of panicking ([#28](#28)) ([2ff3a08](2ff3a08)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
What
EncapsulationKey::encapsulate_deterministicaccepted caller-supplied KEMrandomness and was
pub(gated only by#[doc(hidden)], which hidesrustdoc output, not visibility). Any consumer of
sphragiscould call itdirectly, despite its own doc comment warning "never call with non-uniform
or reused randomness." Reused or predictable input deterministically
collapses the ephemeral X25519 secret, the ML-KEM coins, the ciphertext,
and the shared secret — the exact property
EncapsulationKey::encapsulateexists to guarantee.
puband#[doc(hidden)]:encapsulate_deterministicis now aprivate inherent method. It stays plain-private rather than gaining a
cfg(test)gate, becauseencapsulate()calls straight through to itwith fresh
OsRngbytes in every build — the method must compileunconditionally, and privacy alone already makes it unreachable from any
downstream crate (a
tests/integration file compiles as a separatecrate, same as a real consumer).
into
src/hybrid.rs's own#[cfg(test)] mod tests— it's the only testbuild with visibility into the now-private method.
compile_faildoctest onEncapsulationKey(the fleet-documentedpattern for API-surface guarantees,
basanos/standards/RUST.md§ Compile-Failtests) that calls
encapsulate_deterministicfrom outside the crate andasserts it does not compile — an executable assertion, not a comment.
Closes #17
Why
private, nothazmatfeatureNo real external conformance consumer exists: the only caller was this
crate's own KAT (
tests/known_answer_vectors.rs), and akroasis — the soledeclared consumer of
sphragis— doesn't referenceencapsulate_deterministic,or in fact anything from this crate: it names
sphragisin[workspace.dependencies]but no member crate depends on it and no.rsfile in the tree references it at all (verified by tree-wide grep). A
hazmatfeature would still ship themethod in the published crate, reachable by any consumer that flips one
flag; plain privacy makes it unreachable by construction. If a real external
KAT/conformance use case appears later, the issue's own fallback path
applies: an explicitly unstable
hazmatfeature/module with a distincttype and a misuse-resistant contract — not a flag on the production key
type.
Verification (fails before, passes after)
Before this change,
tests/known_answer_vectors.rscalledek.encapsulate_deterministic(&eseed)directly as an external crate(everything under
tests/compiles separately fromsrc/, exactly like adownstream consumer) — and it compiled and passed, which is the concrete
proof of the reachability this issue reports. That call is now gone from
tests/; left in place, it fails to compile with "no method namedencapsulate_deterministicfound" once the method dropspub. The newcompile_faildoctest onEncapsulationKeypins this down permanently: itmust fail to compile, and does — a regression that re-exposes the method
(e.g. someone reverting to
pub) failscargo test --doc --features preview-pqin CI (gate-attestation.yml'sdoctest_cmd).Breaking change (pre-1.0, semver-minor)
EncapsulationKey::encapsulate_deterministicis removed from the publicAPI surface. sphragis is
0.1.2, pre-1.0. akroasis pins its[workspace.dependencies.sphragis]totag = "v0.1.1"— already tworeleases behind current
main— so it will not take this break on anyordinary resolve; picking it up requires someone to deliberately move the
pin. Named and verified blast radius today: zero. akroasis declares the
dependency but no crate in its workspace depends on it and no
.rsfilereferences
sphragisat all, let aloneencapsulate_deterministic.Standard traceability
No new algorithmic choice: X-Wing (
draft-connolly-cfrg-xwing-kem) and itscombiner are unchanged; the deterministic-encapsulation code path itself is
untouched, only its reachability. The known-answer vector moved, not
edited — same seed, eseed, and expected shared secret as the RustCrypto
x-wing
test-vectors.jsonentry[0]this crate has always gated on.