Skip to content

Production-grade rewrite: hybrid PQ crypto v0.1.0 → v0.3.0 - #1

Merged
ilyk merged 20 commits into
mainfrom
claude/crate-analysis-2gpwdm
Jul 4, 2026
Merged

Production-grade rewrite: hybrid PQ crypto v0.1.0 → v0.3.0#1
ilyk merged 20 commits into
mainfrom
claude/crate-analysis-2gpwdm

Conversation

@ilyk

@ilyk ilyk commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Description

Rewrites quantum-shield from a broken v0.1.0 demo into a production-grade hybrid post-quantum library, staged across three releases (0.2.x → 0.3.0). v0.1.0 was not actually quantum-resistant: it OR-combined the RSA and Kyber key wraps (breaking either recovered the key) and made the Dilithium signature optional (verify passed on RSA alone). It also shipped security-theater code and a test suite that didn't compile.

Type of Change

  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • New feature (non-breaking change which adds functionality)
  • Security enhancement
  • Documentation update
  • Performance improvement

Changes Made

  • New cryptographic core (0.2.0): X25519 + ML-KEM-1024 hybrid KEM through a SHA3-256 combiner (both secrets must break) with AES-256-GCM; Ed25519 + ML-DSA-87 signatures, both mandatory (no signature stripping). Real FIPS 203/204 algorithms via pure-Rust RustCrypto crates; RSA and deprecated round-3 pqcrypto removed. Versioned binary wire format; v0.1.x artifacts rejected by design.
  • Assurance (0.2.1): criterion benchmarks, in-crate KATs (RFC 7748/8032 + ML-KEM/ML-DSA), six cargo-fuzz targets, and CI for semver-checks, coverage, and fuzzing.
  • Hardening (0.2.2): #[no_std] + alloc (bare-metal CI gate on thumbv7em-none-eabi), a dudect constant-time regression harness, and per-component PKCS#8/SPKI/PEM public-key interop.
  • Features (0.3.0): multi-recipient envelopes (QSM2), streaming AEAD (QST2), and key-rotation attestations (QSR2), each with adversarial tests, fuzz targets, a normative spec, and golden vectors.
  • Validation pass: three independent adversarial reviews; fixed multi-recipient sender equivocation (CEK commitment), rotation replay/rollback (signed monotonic epoch), AES-GCM key-schedule zeroization, and serde/stream allocation-and-nonce edge cases.
  • Honest docs throughout: removed fabricated benchmarks and unfounded FIPS/"battle-tested" claims; added design spec, threat model with explicit non-goals, and a v1→v2 migration guide.

Code Quality

  • Code follows the project's style guidelines
  • Self-review of the code has been performed
  • Code has been commented, particularly in hard-to-understand areas
  • No new warnings or errors are introduced

Testing

  • Tests have been added/updated for the changes
  • All existing tests pass
  • New tests cover the changes made
  • Tests have been run locally and pass

CI is green on the head commit: tests on ubuntu-latest and macOS arm64 (Apple Silicon), no_std bare-metal check, MSRV 1.85, clippy -D warnings, docs, semver-checks, coverage, cargo-deny, and a fuzz smoke run across all 10 targets. Adversarial suites cover tampering, downgrade/stripping, multi-recipient add/remove/reorder/duplicate, stream reorder/truncate/duplicate, and rotation forgery/rollback.

Security Considerations

  • This change affects security validation
  • This change has been tested against known attack vectors
  • Security implications have been documented

Remaining out-of-scope items (external, non-code): an independent security audit and supply-chain attestation (signed releases / SLSA / SBOM). The crate and its ml-kem/ml-dsa dependencies are not independently audited, and the library implements FIPS 203/204 algorithms but is not itself FIPS-validated — stated plainly in the README and SECURITY.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QpNCMZbsbN43rinwmifi2u

claude added 20 commits July 4, 2026 16:38
Delete src/security.rs: its sleep-based timing jitter, XOR 'blinding' that
discards the factor, entropy counter, and hard-coded audit provided no real
protection while adding latency. Delete tests/security_tests.rs, which did
not compile (missing Arc import) and tested only the removed APIs.

cargo test now compiles and passes (previously one deterministic failure
plus the compile error).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpNCMZbsbN43rinwmifi2u
…ures

Replace the v1 design, which was not quantum-resistant despite its claims:
the same AES key was wrapped independently by RSA and Kyber (breaking either
layer recovered it), and the Dilithium signature was optional so verification
passed with it stripped.

Encryption (v2): fresh X25519 + ML-KEM-1024 encapsulation per message; both
shared secrets and the full public transcript feed a SHA3-256 combiner
(X-Wing construction ported to ML-KEM-1024, hardened Chempat-style) whose
output keys AES-256-GCM directly. The whole envelope header is bound as AEAD
associated data. Security is now the AND of both layers.

Signatures (v2): Ed25519 + ML-DSA-87 over a domain-separated, length-framed
(context, message) encoding; both components are fixed-size wire fields and
both must verify.

Wire format: versioned binary (QSE2/QSS2/QSP2/QSK2 magics), suite pinning,
no algorithm negotiation; v0.1.x JSON artifacts rejected with a dedicated
error. Private keys are stored and exported as seeds only, zeroized on drop.

Dependencies: RustCrypto ml-kem/ml-dsa (FIPS 203/204, pure Rust),
x25519-dalek, ed25519-dalek, aes-gcm 0.11; rsa and the deprecated round-3
pqcrypto crates removed, along with unused anyhow/tokio/rand/base64.

Tests: tamper (per-region byte flips, cross-envelope splicing), downgrade
(v1 rejection, unknown version/suite, signature stripping), roundtrip,
proptest corruption sweeps, and pinned known-answer vectors including a
stored golden envelope. 61 tests, clippy clean with -D warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpNCMZbsbN43rinwmifi2u
The macos-15 job runs on GitHub's arm64 (Apple Silicon) runners and executes
the full test suite natively on aarch64-apple-darwin. Both platforms run
fmt, clippy with -D warnings, tests across feature combinations, and docs.
cargo-deny enforces advisories, license policy, and registry sources.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpNCMZbsbN43rinwmifi2u
Replace the fabricated benchmarks, unfounded FIPS-compliance/'battle-tested'
claims, and RSA/Kyber/Dilithium references with honest documentation:

- README: accurate algorithm list, AND-composition explanation, not-audited
  and not-FIPS-validated warnings, wire sizes, current API examples.
- docs/design.md: normative v2 wire-format and combiner specification.
- docs/security-model.md: threat model with explicit non-goals (side
  channels, replay, forward secrecy for static recipient keys, sender auth).
- docs/migration-v1-to-v2.md: why v1 artifacts are rejected and how to
  migrate; API mapping table.
- CHANGELOG/SECURITY/CONTRIBUTING updated to match; deleted the stale docs
  pages (performance, api-reference, use-cases, etc.).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpNCMZbsbN43rinwmifi2u
Assess v0.2.0 against a production-grade baseline across six dimensions
(crypto core, side channels, API/interop, testing, CI/supply chain, docs/
assurance), marking each capability done/partial/missing/out-of-scope with
the specific remaining gap. Flags the macOS arm64 CI gate and constant-time
guarantees as unverified rather than complete, and lists the highest-value
gaps to close next (parser fuzzing, proving the arm64 gate, in-crate RFC/
ACVP vectors, benchmarks, cargo-semver-checks, independent audit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpNCMZbsbN43rinwmifi2u
benches/crypto.rs covers keypair generation, seal/open across 1 KiB-16 MiB,
and sign/verify; benches/codec.rs covers to_bytes/from_bytes for every wire
type. Replaces the fabricated benchmark numbers deleted in the v2 rewrite
with a harness that produces measured ones.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpNCMZbsbN43rinwmifi2u
RFC 7748 (X25519) and RFC 8032 (Ed25519) official test vectors, plus
deterministic stability KATs for ML-KEM-1024 and ML-DSA-87 that pin the
parameter set, FIPS sizes, and seed-to-key-to-op derivation against the
locked crate versions. These catch a feature/version wiring mistake that
upstream ACVP tests would not surface in this crate; full FIPS conformance
remains covered upstream. Writing them caught (and fixed) a transcription
slip in the Ed25519 test-1 secret key.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpNCMZbsbN43rinwmifi2u
Six libFuzzer targets: from_bytes for Envelope/HybridSignature/
PublicKeyBundle/secret bundle (never panic on arbitrary input; parsed
objects round-trip), plus seal→open and sign→verify roundtrip properties.
The fuzz crate sits outside the workspace so its nightly/sanitizer build is
isolated. Smoke-run locally: all six execute crash-free (envelope 5.5M runs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpNCMZbsbN43rinwmifi2u
- semver-checks guards the API/wire format against the v0.2.0 baseline tag
- coverage via cargo-llvm-cov → Codecov (non-gating)
- nightly fuzz smoke run over all six targets
- CI now also runs on claude/** branches so the macOS arm64 gate exercises
  this work before merge

0.2.1 is API- and wire-compatible with 0.2.0 (assurance/tooling only).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpNCMZbsbN43rinwmifi2u
Mark the assurance/tooling gaps closed (fuzzing, RFC/ML-KEM/ML-DSA vectors,
benchmarks, coverage, semver-checks) and note the remaining 0.2.2/0.3.0 work.
The semver-checks CI baseline uses the 0.2.0 commit SHA because this
environment's git remote rejects tag pushes. Verified locally: 196 checks
pass, no semver break from 0.2.0 to 0.2.1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpNCMZbsbN43rinwmifi2u
The crate is now #![no_std] via cfg_attr; std stays a default feature, so
existing users are unaffected. All dependencies are reconfigured with
default-features=false + alloc; ml-dsa drops its getrandom default (signing
is deterministic) and ed25519-dalek swaps std for alloc. A ci/no_std_check
helper crate compiles the full public API for thumbv7em-none-eabi with a
custom getrandom backend — the honest bare-metal gate, added as a CI job.
The 'no default features' CI step becomes a lib build (tests need std).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpNCMZbsbN43rinwmifi2u
examples/dudect.rs probes the one secret-dependent operation an attacker can
influence: open (ML-KEM decapsulate + AES-GCM). Two dudect tests — valid vs
wrong-recipient and valid vs tampered-ciphertext (implicit-rejection path) —
both measure low t-values locally (~2-6, under the ~10 concern threshold),
confirming decryption-failure timing does not leak. sign/verify are excluded
on purpose: verification runs on public data, so a validity-timing difference
there (ML-DSA fast-reject) is not a secret leak and would be a false tripwire.

Runs as a dudect-bencher example (not a criterion bench, which mishandles
cargo's --bench arg); wired as a non-gating workflow_dispatch/schedule CI job.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpNCMZbsbN43rinwmifi2u
PublicKeyBundle::to_pem/from_pem emit a concatenated multi-PEM document:
standard SubjectPublicKeyInfo blocks for ML-KEM-1024, ML-DSA-87, and Ed25519
via each crate's native encoders, and a raw block for X25519 (which has no
upstream PKCS#8 support). from_pem reassembles the canonical QSP2 bytes and
reuses PublicKeyBundle::from_bytes, so PEM import enforces identical
validation. QSP2/QSK2 remain the primary formats. A fuzz target covers the
new parser. Feature is off by default and builds without std.

Also finalizes the 0.2.2 hardening milestone (no_std, dudect, PEM) in the
CHANGELOG, README, and gap matrix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpNCMZbsbN43rinwmifi2u
seal_multi encrypts a payload once under a random CEK and wraps that CEK for
each recipient with a full hybrid KEM (X25519 + ML-KEM-1024) — not the v1
OR-flaw, since each wrap is itself hybrid and the CEK is random. Each wrap's
AEAD binds header+count; the payload AEAD binds the entire prefix including
all wraps, so adding/removing/reordering/duplicating any wrap fails as a
uniform DecryptionFailed. open_multi trial-decrypts every wrap with no
recipient identifier on the wire (envelopes reveal nothing about who the
recipients are); MAX_RECIPIENTS=1024 bounds the cost at seal and parse.

11 tests including the full tamper suite (reorder/drop/duplicate/count
mismatch/zero/over-limit). Single-recipient Envelope (QSE2) is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpNCMZbsbN43rinwmifi2u
One hybrid KEM run derives an AES-256-GCM key; the payload is encrypted in
64 KiB chunks with the STREAM construction. The per-chunk 12-byte nonce is
prefix(7)||u32 index||last-flag, and each chunk's AAD is the one-time header
plus its index and last-flag, so reordering, duplicating, dropping, or
splicing chunks fails as DecryptionFailed. A stream with no final chunk is
StreamTruncated at finish(); sealing after the last chunk is StreamFinished.

StreamSealer::new / seal_chunk and StreamOpener (via HybridCrypto::
stream_opener) / open_chunk / finish. 9 tests including boundary sizes,
a >64 MiB roundtrip that exceeds MAX_PLAINTEXT_LEN, and the full tamper suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpNCMZbsbN43rinwmifi2u
key_id() = SHA3-256(QSP2)[..16], a short stable identifier. A
RotationAttestation is the old keypair's hybrid signature over
old_key_id || new_public under a ROTATION_CONTEXT domain, giving verifiers a
cryptographic old->new link. The attestation is itself Ed25519 + ML-DSA, so
forging it requires breaking both. The old bundle is verifier-supplied (an
explicit trust anchor), not on the wire.

HybridCrypto::attest_rotation + free verify_rotation. 7 tests: valid verify,
wire roundtrip, forged-by-other-key, wrong trust anchor, tampered successor,
key_id stability/distinctness, truncation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpNCMZbsbN43rinwmifi2u
- Fuzz targets for the QSM2/QST2/QSR2 parsers (all smoke-run crash-free:
  multi 12M runs, rotation 11.8M, stream 3.6k)
- Normative docs/design.md sections for multi-recipient, streaming, and
  rotation wire formats
- Golden vectors: key_id and a deterministic rotation attestation digest
- README, CHANGELOG, and gap matrix updated; bump to 0.3.0

The code-level gaps from the production-readiness matrix are now closed;
what remains is external (independent audit, supply-chain attestation).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpNCMZbsbN43rinwmifi2u
…, DoS caps

Adversarial review (three independent passes) found no trust *bypass* but
several real issues from smell to authority-override. Fixes:

- Multi-recipient equivocation (medium): AES-GCM is not key-committing, so a
  malicious sender could wrap different CEKs to different recipients and craft
  one payload decrypting to different plaintexts each. QSM2 now carries
  SHA3-256(CEK), bound into the payload AAD and checked constant-time on open.
- Rotation rollback / authority-override: attestations never expired, so a
  replayed superseded old->new1 could roll a verifier back. Bind a
  caller-supplied monotonic epoch into the signature, exposed via epoch();
  attest_rotation now takes an epoch. Verifiers must reject non-advancing epochs.
- AES-GCM key schedule left in memory: enable aes-gcm 'zeroize' feature.
- serde size_hint allocation DoS: cap pre-allocation at 64 KiB.
- Streaming counter-overflow: move the guard before encrypt (removes an
  internal nonce-reuse edge at 2^32 chunks) and bound per-chunk size to u32.
- CI: constant-time job's workflow_dispatch/schedule triggers now exist;
  fuzz job runs all 10 targets (was missing multi/rotation/stream/pem).
- Docs: honest ML-DSA-vk validation note, streaming finish() must-call warning,
  multi DoS + rotation rollback in the security model; design.md updated for the
  new QSM2 (commitment) and QSR2 (epoch) layouts; golden vector regenerated.

Full suite green (14 test files), clippy/doc clean, no_std + bare-metal build,
changed parsers re-fuzzed crash-free, semver + publish dry-run OK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpNCMZbsbN43rinwmifi2u
- fuzz job: the 'taiki-e/install-action@cargo-fuzz' shorthand did not install
  the binary (cargo fuzz -> 'no such command'); use the canonical
  'taiki-e/install-action@v2' with 'tool: cargo-fuzz'.
- cargo-deny: ignore RUSTSEC-2024-0375 (atty) and RUSTSEC-2021-0139
  (ansi_term) — both are *unmaintained* notices (not vulnerabilities) on
  transitive dev-dependencies of dudect-bencher (clap 2), used only by the
  manual constant-time harness; neither ships in the library. Verified locally:
  cargo deny check advisories bans licenses sources -> all ok.

The macOS arm64 (Apple Silicon) gate passed on the prior run — the last
unproven acceptance gate is now green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpNCMZbsbN43rinwmifi2u
The taiki-e prebuilt cargo-fuzz is musl-linked, so it defaulted its build
--target to x86_64-unknown-linux-musl, whose std isn't installed on the runner
(E0463: can't find crate for core). Pass --target x86_64-unknown-linux-gnu
explicitly. cargo-deny now passes (verified on the prior run).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpNCMZbsbN43rinwmifi2u
@ilyk ilyk changed the title Remove ineffective security-theater module and broken test file Production-grade rewrite: hybrid PQ crypto v0.1.0 → v0.3.0 Jul 4, 2026
@ilyk
ilyk merged commit f65c4a2 into main Jul 4, 2026
18 checks passed
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.

2 participants