Skip to content

fix(sphragis)!: make deterministic encapsulation private - #31

Merged
forkwright merged 3 commits into
mainfrom
fix/17-gate-deterministic-encaps
Aug 15, 2026
Merged

forkwright merged 3 commits into
mainfrom
fix/17-gate-deterministic-encaps

Conversation

@forkwright

Copy link
Copy Markdown
Owner

What

EncapsulationKey::encapsulate_deterministic accepted caller-supplied KEM
randomness and was pub (gated only by #[doc(hidden)], which hides
rustdoc output, not visibility). Any consumer of sphragis could call it
directly, 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::encapsulate
exists to guarantee.

  • Dropped pub and #[doc(hidden)]: encapsulate_deterministic is now a
    private inherent method. It stays plain-private rather than gaining a
    cfg(test) gate, because encapsulate() calls straight through to it
    with fresh OsRng bytes in every build — the method must compile
    unconditionally, and privacy alone already makes it unreachable from any
    downstream crate (a tests/ integration file compiles as a separate
    crate, same as a real consumer).
  • Relocated the X-Wing draft known-answer test beside the implementation,
    into src/hybrid.rs's own #[cfg(test)] mod tests — it's the only test
    build with visibility into the now-private method.
  • Added a compile_fail doctest on EncapsulationKey (the fleet-documented
    pattern for API-surface guarantees, basanos/standards/RUST.md § Compile-Fail
    tests) that calls encapsulate_deterministic from outside the crate and
    asserts it does not compile — an executable assertion, not a comment.

Closes #17

Why private, not hazmat feature

No real external conformance consumer exists: the only caller was this
crate's own KAT (tests/known_answer_vectors.rs), and akroasis — the sole
declared consumer of sphragis — doesn't reference encapsulate_deterministic,
or in fact anything from this crate: it names sphragis in
[workspace.dependencies] but no member crate depends on it and no .rs
file in the tree references it at all (verified by tree-wide grep). A
hazmat feature would still ship the
method 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 hazmat feature/module with a distinct
type 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.rs called
ek.encapsulate_deterministic(&eseed) directly as an external crate
(everything under tests/ compiles separately from src/, exactly like a
downstream 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 named
encapsulate_deterministic found" once the method drops pub. The new
compile_fail doctest on EncapsulationKey pins this down permanently: it
must fail to compile, and does — a regression that re-exposes the method
(e.g. someone reverting to pub) fails cargo test --doc --features preview-pq in CI (gate-attestation.yml's doctest_cmd).

cargo fmt --all -- --check
cargo check --all-targets && cargo check --all-targets --features preview-pq
cargo clippy --all-targets -- -D warnings && cargo clippy --all-targets --features preview-pq -- -D warnings
cargo test && cargo test --features preview-pq

Breaking change (pre-1.0, semver-minor)

EncapsulationKey::encapsulate_deterministic is removed from the public
API surface. sphragis is 0.1.2, pre-1.0. akroasis pins its
[workspace.dependencies.sphragis] to tag = "v0.1.1" — already two
releases behind current main — so it will not take this break on any
ordinary 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 .rs file
references sphragis at all, let alone encapsulate_deterministic.

Standard traceability

No new algorithmic choice: X-Wing (draft-connolly-cfrg-xwing-kem) and its
combiner 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.json entry [0] this crate has always gated on.

@forkwright
forkwright force-pushed the fix/17-gate-deterministic-encaps branch from 10a0851 to 65e79d1 Compare August 15, 2026 21:42
forkwright added 2 commits August 15, 2026 17:37
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
@forkwright
forkwright force-pushed the fix/17-gate-deterministic-encaps branch from 65e79d1 to 88e2c54 Compare August 15, 2026 22:40
…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
@forkwright

Copy link
Copy Markdown
Owner Author

Rebased onto current main (now carrying #24 bounded-CBOR, #27 KAT provenance lock, #32 public-API narrowing). src/hybrid.rs auto-merged textually (this branch and #32 touched non-overlapping regions); tests/known_answer_vectors.rs conflicted and was resolved by hand.

Does #32 subsume this PR? No. #32 narrowed the public API (HybridKem, SharedSecret, EncapsulationKey::encapsulate/DecapsulationKey::decapsulate, derive_wrap_key all moved behind hazmat) but left EncapsulationKey::encapsulate_deterministic exactly as this PR found it: #[doc(hidden)] pub fn, unconditionally reachable regardless of hazmat (EncapsulationKey itself is always-public, and #[doc(hidden)] hides rustdoc output, not visibility). This PR's privatization remains fully necessary.

#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 (crypto-provenance.toml: xwing-kat-0). Relocating the test into src/hybrid.rs's own #[cfg(test)] mod tests (required once encapsulate_deterministic is private) used #27's enhanced version, not this branch's original hardcoded-hex one — otherwise the relocation would have silently regressed #27's own guarantee. crypto-provenance.toml's executed_by field and review_policy.trigger prose updated to point at the new location instead of going stale.

Also completed while in this code: the SharedSecret type alias was left unconditionally pub by #32, with a doc comment explaining why — encapsulate_deterministic returned it as a pub (doc-hidden) method, so narrowing the alias would have leaked it through that signature (private_interfaces). That blocker is exactly what this PR removes. Gated SharedSecret itself behind hazmat too, matching HybridKem's existing pub(crate)/pub split, and corrected the now-stale doc comment.

CI green on the current head.

@forkwright
forkwright merged commit cf48668 into main Aug 15, 2026
12 checks passed
@forkwright
forkwright deleted the fix/17-gate-deterministic-encaps branch August 15, 2026 23:02
forkwright pushed a commit that referenced this pull request Aug 15, 2026
"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
forkwright pushed a commit that referenced this pull request Aug 16, 2026
🤖 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keep deterministic encapsulation out of the safe public API

1 participant