Production-grade rewrite: hybrid PQ crypto v0.1.0 → v0.3.0 - #1
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Changes Made
#[no_std]+alloc(bare-metal CI gate onthumbv7em-none-eabi), a dudect constant-time regression harness, and per-component PKCS#8/SPKI/PEM public-key interop.Code Quality
Testing
CI is green on the head commit: tests on ubuntu-latest and macOS arm64 (Apple Silicon),
no_stdbare-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
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-dsadependencies 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