pq_crypto 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15356ea593b25eefd75360992b4b716df348601a5198b8085e271e866e431371
4
- data.tar.gz: b9667b1d123009b44009b8819f02c1f2855bd5117bd6142faa36cef3bcd4bf22
3
+ metadata.gz: 8cd88ab6ebe3042111e60711895a9563a1510057a321ac9dab510496634656b8
4
+ data.tar.gz: 684f469f8be7912780e00d368989418499aae00bb530d7fc32f8b6c6d5576593
5
5
  SHA512:
6
- metadata.gz: 722560e27ea481d4f9acaee6798b3a34c06796d5d365a2164cb6ca612917e1b6db0e06e36c74b7624b1f33a78ab3e91930f3e67b5e67988363c4498cba24e585
7
- data.tar.gz: 843fdee9d26cca30bfe5e6013a759713a062f9d63b08544ea5727b86b3b94e9b58dab0a201d52d0fe2e94c6aedb6ec7c770743bab65ea6d1fbf8545a71d81abc
6
+ metadata.gz: 54c1f3b0b8a2f7141d3ec4c8649c003793a3469f212fed94b0dc7ef0e2b0ff3ddba510383a0b62813d9ee98700bf266547be1900693c9ad465fb36deec91ab7b
7
+ data.tar.gz: 41c0bdea2d91bbd2a2e8884ee2b2a328c4c06d410fba8d92c9c1a9470b9d5597d0b8f5233b0ba87225535a80c9055033518ef1dd82e90101a9f5ffcd606b81a6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.2] — 2026-04-25
4
+
5
+ ### Added — streaming ML-DSA for large inputs
6
+
7
+ - Added `PQCrypto::Signature::SecretKey#sign_io(io, chunk_size: 1 << 20, context: "".b)`.
8
+ - Added `PQCrypto::Signature::PublicKey#verify_io(io, signature, chunk_size: 1 << 20, context: "".b)` and `verify_io!`.
9
+ - Implemented streaming pure ML-DSA through an internal FIPS 204 ExternalMu path. Existing one-shot `sign` / `verify` semantics are unchanged; no public `sign_mu` / `verify_mu` API is exposed.
10
+
11
+ ### Notes
12
+
13
+ - Streaming is primarily for large IO inputs and lower peak memory pressure. It is not a HashML-DSA/prehash speed mode; CPU cost is still dominated by SHAKE/Keccak.
14
+ - Default empty-context streaming signatures interoperate with the existing one-shot `verify(message, signature)` API. Non-empty `context:` must be supplied again during `verify_io`.
15
+
16
+ ## [0.3.1] — 2026-04-24
17
+
18
+ ### Fixed — X-Wing draft-10 compatibility
19
+
20
+ - Changed `:ml_kem_768_x25519_xwing` secret keys to the draft-10 32-byte
21
+ X-Wing decapsulation seed and derive ML-KEM/X25519 private material with
22
+ SHAKE256 during key generation and decapsulation.
23
+ - Corrected the X-Wing combiner transcript to
24
+ `ss = SHA3-256( ss_M || ss_X || ct_X || pk_X || XWingLabel )`.
25
+ - Updated the hybrid serialization OID to the X-Wing draft OID
26
+ `1.3.6.1.4.1.62253.25722`.
27
+ - Redacted key `inspect` output, removed public secret-key fingerprints,
28
+ improved native extension load diagnostics, switched the extension build
29
+ flag to C11, and aligned docs with the implementation.
30
+
3
31
  ## [0.3.0] — 2026-04-24
4
32
 
5
33
  **Breaking release.** Hybrid KEM keys, ciphertexts, and `pqc_container_*`
@@ -9,17 +37,14 @@ and ML-DSA-65 material is unaffected.
9
37
  ### Changed — hybrid KEM (breaking)
10
38
 
11
39
  - Replaced the 0.2.0 ad-hoc `HKDF-SHA256`-with-double-transcript combiner
12
- with the **X-Wing** construction from
13
- [draft-connolly-cfrg-xwing-kem](https://datatracker.ietf.org/doc/draft-connolly-cfrg-xwing-kem/):
14
- `ss = SHA3-256( XWingLabel || ss_M || ss_X || ct_X || pk_X )`, where
15
- `XWingLabel` is the 6-byte ASCII string `\.//^\`.
40
+ with a SHA3-256 X-Wing-inspired combiner. This was later corrected in
41
+ `0.3.1` to match draft-10 transcript order and 32-byte secret keys.
16
42
  - Renamed the hybrid algorithm symbol
17
43
  `:ml_kem_768_x25519_hkdf_sha256` → `:ml_kem_768_x25519_xwing`.
18
44
  - Retired the 0.2.0 project-local hybrid OID
19
- (`2.25.260242945110721168101139140490528778800`). The new OID
20
- (`2.25.318532651283923671095712569430174917109`) identifies the X-Wing
21
- combiner. `pqc_container_*` blobs carry the new OID; decoding a 0.2.0
22
- hybrid container now fails fast with `SerializationError`.
45
+ (`2.25.260242945110721168101139140490528778800`). 0.3.0 used
46
+ `2.25.318532651283923671095712569430174917109`; this was later replaced
47
+ in `0.3.1` by the X-Wing draft OID.
23
48
 
24
49
  ### Changed — native code hygiene
25
50
 
@@ -39,9 +64,8 @@ and ML-DSA-65 material is unaffected.
39
64
  `hybrid_public_key_t`, `hybrid_secret_key_t`, and
40
65
  `hybrid_ciphertext_t` so any future change that introduces padding
41
66
  fails at compile time rather than silently shifting byte offsets.
42
- - Migrated PEM codec from `EVP_EncodeBlock` / `EVP_DecodeBlock` to the
43
- streaming `EVP_EncodeUpdate` / `EVP_DecodeUpdate` API, which rejects
44
- invalid base64 characters rather than treating them as zeros.
67
+ - Migrated PEM codec to OpenSSL `BIO_f_base64` with stricter PEM
68
+ header/footer framing and trailing-garbage checks.
45
69
  - Deleted the entire internal HKDF and SHA-256 helper paths that 0.2.0
46
70
  used for its combiner; the X-Wing combiner is a single SHA3-256
47
71
  invocation through `EVP_DigestUpdate`.
@@ -61,9 +85,9 @@ and ML-DSA-65 material is unaffected.
61
85
  `CRYPTO_memcmp` through a new `PQCrypto.ct_equals` native helper, so
62
86
  key equality checks no longer leak timing information about a
63
87
  prefix-match.
64
- - `SecretKey#hash` (and `PublicKey#hash` for symmetry) now hash a
65
- SHA-256 fingerprint of the bytes instead of the raw bytes, and a
66
- public `#fingerprint` method is exposed.
88
+ - `PublicKey#hash` and `SecretKey#hash` now hash a SHA-256 fingerprint
89
+ of the bytes instead of the raw bytes. The public secret-key fingerprint
90
+ method is removed in `0.3.1` to reduce accidental logging risk.
67
91
  - Native entrypoints and their `native_*` aliases are installed once via
68
92
  the new `PQCrypto::NativeBindings` module instead of the ad-hoc
69
93
  `unless method_defined?` guards on the singleton.
@@ -72,7 +96,8 @@ and ML-DSA-65 material is unaffected.
72
96
 
73
97
  ### Changed — packaging
74
98
 
75
- - `required_ruby_version` from `">= 3.4.0.a"` to `">= 3.4"`.
99
+ - Intended to change `required_ruby_version` from `">= 3.4.0.a"` to
100
+ `">= 3.4"`; the gemspec is aligned in `0.3.1`.
76
101
  - Version bumped to `0.3.0`.
77
102
  - `VerificationError` class is still defined (and still raised by
78
103
  `verify!`) for backward compatibility, but the native `verify`
data/GET_STARTED.md CHANGED
@@ -35,6 +35,29 @@ sig.public_key.verify("message", signature)
35
35
  sig.public_key.verify!("message", signature)
36
36
  ```
37
37
 
38
+ For large files, use streaming ML-DSA:
39
+
40
+ ```ruby
41
+ signature = File.open("document.bin", "rb") do |io|
42
+ sig.secret_key.sign_io(io, chunk_size: 1 << 20)
43
+ end
44
+
45
+ ok = File.open("document.bin", "rb") do |io|
46
+ sig.public_key.verify_io(io, signature, chunk_size: 1 << 20)
47
+ end
48
+ ```
49
+
50
+ With an optional context:
51
+
52
+ ```ruby
53
+ ctx = "document-v1".b
54
+ signature = File.open("document.bin", "rb") { |io| sig.secret_key.sign_io(io, context: ctx) }
55
+ ok = File.open("document.bin", "rb") { |io| sig.public_key.verify_io(io, signature, context: ctx) }
56
+ ```
57
+
58
+ `sign_io` / `verify_io` are pure ML-DSA streaming helpers, not prehash
59
+ shortcuts. `verify_io!` raises on mismatch.
60
+
38
61
  ## 6. Hybrid KEM (X-Wing)
39
62
 
40
63
  ```ruby
@@ -43,6 +66,9 @@ result = hybrid.public_key.encapsulate
43
66
  shared_secret = hybrid.secret_key.decapsulate(result.ciphertext)
44
67
  ```
45
68
 
69
+ The raw X-Wing secret key exported by this API is the draft-10 32-byte
70
+ decapsulation seed, not the expanded ML-KEM/X25519 private material.
71
+
46
72
  The hybrid mode follows `draft-connolly-cfrg-xwing-kem`. See
47
73
  `SECURITY.md` for audit status.
48
74
 
data/README.md CHANGED
@@ -13,13 +13,14 @@ It exposes three public building blocks:
13
13
  The gem is backed by vendored `PQClean` sources for `ML-KEM-768` /
14
14
  `ML-DSA-65` and by OpenSSL for `X25519` and `SHA3-256`. Every piece of
15
15
  conventional-crypto functionality goes through standard library calls
16
- (`EVP_*`, `RAND_bytes`, `CRYPTO_memcmp`, streaming `EVP_Encode*` /
17
- `EVP_Decode*`) — nothing roll-your-own where a library primitive exists.
16
+ (`EVP_*`, `RAND_bytes`, `CRYPTO_memcmp`, `BIO_f_base64`) — nothing
17
+ roll-your-own where a library primitive exists.
18
18
 
19
19
  ## Status
20
20
 
21
21
  - primitive-first API only
22
22
  - no protocol/session helpers in the public surface
23
+ - streaming ML-DSA signing/verification is available for large IO inputs
23
24
  - serialization uses pq_crypto-specific `pqc_container_*` wrappers
24
25
  - not audited
25
26
  - not yet positioned as production-ready
@@ -42,7 +43,21 @@ bundle exec rake compile
42
43
 
43
44
  - Ruby 3.4.x
44
45
  - a C toolchain with C11 support (for `_Static_assert` / `_Thread_local`)
45
- - OpenSSL **3.0 or later** with SHA3-256 available (default provider)
46
+ - OpenSSL **3.0 or later** with SHA3-256 and SHAKE256 available (default provider)
47
+
48
+ ### Build-time Keccak backend
49
+
50
+ The default build uses PQClean's scalar `common/fips202.c` backend:
51
+
52
+ ```bash
53
+ PQCRYPTO_KECCAK_BACKEND=clean bundle exec rake compile
54
+ ```
55
+
56
+ `PQCRYPTO_KECCAK_BACKEND=xkcp` is reserved for a separately vendored,
57
+ reviewed, `fips202.h`-compatible XKCP adapter. If requested without that
58
+ adapter, the build aborts instead of silently falling back to `clean`.
59
+ This avoids mixing OpenSSL EVP SHAKE state with PQClean SHAKE state and
60
+ keeps output-byte compatibility explicit.
46
61
 
47
62
  ## Async / Fiber scheduler support
48
63
 
@@ -100,6 +115,8 @@ shared_secret = keypair.secret_key.decapsulate(result.ciphertext)
100
115
 
101
116
  ### ML-DSA-65
102
117
 
118
+ One-shot signing keeps the existing API:
119
+
103
120
  ```ruby
104
121
  keypair = PQCrypto::Signature.generate(:ml_dsa_65)
105
122
  signature = keypair.secret_key.sign("hello")
@@ -108,6 +125,36 @@ keypair.public_key.verify("hello", signature) # => true / false
108
125
  keypair.public_key.verify!("hello", signature) # raises on mismatch
109
126
  ```
110
127
 
128
+ For large inputs, use streaming IO so the message does not need to be
129
+ materialized as one Ruby string:
130
+
131
+ ```ruby
132
+ signature = File.open("document.bin", "rb") do |io|
133
+ keypair.secret_key.sign_io(io, chunk_size: 1 << 20)
134
+ end
135
+
136
+ ok = File.open("document.bin", "rb") do |io|
137
+ keypair.public_key.verify_io(io, signature, chunk_size: 1 << 20)
138
+ end
139
+ ```
140
+
141
+ `sign_io` / `verify_io` use pure ML-DSA with an internal FIPS 204
142
+ ExternalMu flow. They are not HashML-DSA/prehash shortcuts and do not
143
+ expose public `sign_mu` / `verify_mu` APIs. With the default empty
144
+ context, streaming signatures verify with `verify(message, signature)`
145
+ and one-shot signatures verify with `verify_io(io, signature)`.
146
+
147
+ Optional context is supported and must match on verify:
148
+
149
+ ```ruby
150
+ ctx = "invoice-v1".b
151
+ signature = File.open("document.bin", "rb") { |io| keypair.secret_key.sign_io(io, context: ctx) }
152
+ ok = File.open("document.bin", "rb") { |io| keypair.public_key.verify_io(io, signature, context: ctx) }
153
+ ```
154
+
155
+ `chunk_size` must be positive. `context` is limited to 255 bytes by
156
+ FIPS 204. `verify_io!` raises `PQCrypto::VerificationError` on mismatch.
157
+
111
158
  Note: `verify` returns a plain boolean for normal outcomes. `verify!`
112
159
  raises `PQCrypto::VerificationError` when the signature does not
113
160
  match.
@@ -120,14 +167,16 @@ result = keypair.public_key.encapsulate
120
167
  shared_secret = keypair.secret_key.decapsulate(result.ciphertext)
121
168
  ```
122
169
 
123
- The combiner is exactly:
170
+ The implementation follows draft-10 key expansion: the X-Wing secret
171
+ decapsulation key is a 32-byte seed expanded with SHAKE256 into ML-KEM
172
+ and X25519 private material. The combiner is exactly:
124
173
 
125
174
  ```
126
- ss = SHA3-256( "\.//^\" || ss_M || ss_X || ct_X || pk_X )
175
+ ss = SHA3-256( ss_M || ss_X || ct_X || pk_X || "\.//^\" )
127
176
  ```
128
177
 
129
- as specified by `draft-connolly-cfrg-xwing-kem`. See `SECURITY.md` for
130
- audit status and interoperability caveats.
178
+ as specified by `draft-connolly-cfrg-xwing-kem-10`. See `SECURITY.md`
179
+ for audit status and interoperability caveats.
131
180
 
132
181
  ## Serialization
133
182
 
@@ -172,6 +221,12 @@ key.wipe! # scrub the key's internal copy
172
221
  `CRYPTO_memcmp` through a `PQCrypto.ct_equals` helper so comparisons
173
222
  do not leak timing information about a prefix match.
174
223
 
224
+ Secret key `inspect` output is intentionally redacted and secret key
225
+ objects do not expose a public `fingerprint` method. `wipe!` remains
226
+ best-effort only: it clears the current Ruby string buffer owned by the
227
+ key object, not every possible copy made by Ruby, OpenSSL, serialization,
228
+ logging, or the garbage collector.
229
+
175
230
  ## Introspection
176
231
 
177
232
  ```ruby
data/SECURITY.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Scope of the public API
4
4
 
5
- `pq_crypto` 0.3.0 exposes a primitive-first public surface:
5
+ `pq_crypto` exposes a primitive-first public surface:
6
6
 
7
7
  - `PQCrypto::KEM` (`ML-KEM-768`)
8
8
  - `PQCrypto::Signature` (`ML-DSA-65`)
@@ -29,23 +29,25 @@ gem.
29
29
  ### HybridKEM
30
30
 
31
31
  `PQCrypto::HybridKEM` implements the **X-Wing** construction from
32
- [`draft-connolly-cfrg-xwing-kem`](https://datatracker.ietf.org/doc/draft-connolly-cfrg-xwing-kem/):
32
+ [`draft-connolly-cfrg-xwing-kem-10`](https://datatracker.ietf.org/doc/draft-connolly-cfrg-xwing-kem/).
33
33
 
34
- ss = SHA3-256( XWingLabel || ss_M || ss_X || ct_X || pk_X )
34
+ The X-Wing secret decapsulation key is a 32-byte seed. It is expanded
35
+ with SHAKE256 into the ML-KEM-768 and X25519 private material used
36
+ internally for decapsulation. The public key and ciphertext are the
37
+ fixed-length concatenations specified by the draft.
35
38
 
36
- where `XWingLabel = "\.//^\"` (6 ASCII bytes). The hybrid public key,
37
- secret key, and ciphertext are the byte concatenations of the ML-KEM
38
- and X25519 halves exactly as specified by X-Wing.
39
+ ss = SHA3-256( ss_M || ss_X || ct_X || pk_X || XWingLabel )
40
+
41
+ where `XWingLabel = "\.//^\"` (6 ASCII bytes).
39
42
 
40
43
  X-Wing as specified has a proof of classical IND-CCA security under
41
44
  the strong Diffie-Hellman assumption for X25519 (in the ROM), and
42
45
  post-quantum IND-CCA security in the standard model assuming ML-KEM-768
43
46
  is IND-CCA secure and SHA3-256 behaves as a PRF.
44
47
 
45
- This gem is **not** yet part of a cross-language X-Wing interop test
46
- suite; wire-format claims for this release are limited to "follows the
47
- X-Wing draft as of version 10." External interoperability should be
48
- verified against the reference implementation before relying on it.
48
+ This gem is intended to match the X-Wing draft as of version 10. External
49
+ interoperability should still be verified against the reference
50
+ implementation before relying on it.
49
51
 
50
52
  ### Deterministic test hooks
51
53
 
@@ -68,16 +70,16 @@ They are:
68
70
  - not real PKCS#8
69
71
  - not advertised as interoperable with OpenSSL, Go, Java, or PKI tooling
70
72
 
71
- The OIDs embedded in these containers are project-local UUID-derived
72
- OIDs under `2.25.*`. They are part of pq_crypto's own serialized
73
- container schema, not external interoperability identifiers.
73
+ The `pqc_container_*` envelope itself is project-specific. ML-KEM and
74
+ ML-DSA currently use project-local UUID-derived OIDs under `2.25.*`.
75
+ Hybrid X-Wing uses the draft X-Wing OID `1.3.6.1.4.1.62253.25722`.
74
76
 
75
77
  The hybrid OID used by 0.2.0
76
- (`2.25.260242945110721168101139140490528778800`) is retired in 0.3.0
77
- because the combiner semantics changed (0.2.0 used an ad-hoc
78
- HKDF-SHA256 combiner; 0.3.0 uses X-Wing / SHA3-256). The new hybrid
79
- OID is `2.25.318532651283923671095712569430174917109`. A 0.2.0 hybrid
80
- container is rejected at decode time in 0.3.0.
78
+ (`2.25.260242945110721168101139140490528778800`) is retired. The
79
+ intermediate 0.3.0 project-local hybrid OID
80
+ (`2.25.318532651283923671095712569430174917109`) is also retired in
81
+ favor of the draft X-Wing OID. Older hybrid containers are rejected at
82
+ decode time.
81
83
 
82
84
  ## Memory wiping
83
85
 
@@ -98,9 +100,19 @@ OpenSSL is used for:
98
100
  - `SHA3-256` (X-Wing combiner, via `EVP_sha3_256`)
99
101
  - `RAND_bytes` (production entropy source for `randombytes()`)
100
102
  - `CRYPTO_memcmp` (constant-time comparison used by `PQCrypto.ct_equals`)
101
- - Base64 encode/decode for PEM via the streaming `EVP_Encode*` /
102
- `EVP_Decode*` API, which rejects invalid base64 rather than treating
103
- it as zeros.
103
+ - Base64 encode/decode for PEM via OpenSSL `BIO_f_base64`, with strict
104
+ header/footer framing and trailing-garbage checks.
105
+
106
+ ## Secret key display and wiping
107
+
108
+ Secret key objects redact `inspect` output and intentionally do not expose
109
+ a public `fingerprint` method. This avoids accidental logging of raw secret
110
+ bytes or stable secret-derived identifiers.
111
+
112
+ `wipe!` is best-effort only. It wipes the current Ruby string buffer held
113
+ by the key object; it cannot guarantee erasure of copies made by Ruby,
114
+ OpenSSL, native wrapper buffers, serialization, logging, crash dumps, or
115
+ the garbage collector.
104
116
 
105
117
  ## Threading
106
118
 
@@ -3,7 +3,7 @@
3
3
 
4
4
  require "mkmf"
5
5
 
6
- $CFLAGS << " -std=c99 -Wall -Wextra -O2"
6
+ $CFLAGS << " -std=c11 -Wall -Wextra -O2"
7
7
  $CFLAGS << " -fstack-protector-strong -D_FORTIFY_SOURCE=2"
8
8
  VENDOR_ONLY_CFLAGS = "-Wno-unused-parameter -Wno-unused-function -Wno-strict-prototypes -Wno-pedantic -Wno-c23-extensions -Wno-undef"
9
9
 
@@ -11,10 +11,14 @@ $LDFLAGS << " -Wl,-no_warn_duplicate_libraries" if RbConfig::CONFIG["host_os"] =
11
11
 
12
12
  USE_SYSTEM = arg_config("--use-system-libraries") || ENV["PQCRYPTO_USE_SYSTEM_LIBRARIES"]
13
13
 
14
+ KECCAK_BACKEND = (ENV["PQCRYPTO_KECCAK_BACKEND"] || "clean").strip.downcase
15
+ SUPPORTED_KECCAK_BACKENDS = %w[clean xkcp].freeze
16
+
14
17
  SANITIZE = ENV["PQCRYPTO_SANITIZE"]
15
18
 
16
19
  if SANITIZE && !SANITIZE.strip.empty?
17
20
  sanitize = SANITIZE.strip
21
+ $CFLAGS.gsub!(/\s-D_FORTIFY_SOURCE=\d+/, "")
18
22
  $CFLAGS << " -O1 -g -fno-omit-frame-pointer -fsanitize=#{sanitize}"
19
23
  $LDFLAGS << " -fsanitize=#{sanitize}"
20
24
  end
@@ -72,9 +76,53 @@ def configure_openssl!
72
76
  SRC
73
77
  abort "OpenSSL SHA3-256 is required (X-Wing combiner)" unless try_compile(sha3_check)
74
78
 
79
+ shake_check = <<~SRC
80
+ #include <openssl/evp.h>
81
+ int main(void) {
82
+ const EVP_MD *md = EVP_shake256();
83
+ return md == NULL ? 1 : 0;
84
+ }
85
+ SRC
86
+ abort "OpenSSL SHAKE256 is required (X-Wing key expansion)" unless try_compile(shake_check)
87
+
75
88
  $CFLAGS << " -DHAVE_OPENSSL_EVP_H -DHAVE_OPENSSL_RAND_H"
76
89
  end
77
90
 
91
+ def configure_keccak_backend(vendor_dir, common_dir)
92
+ abort "Unsupported PQCRYPTO_KECCAK_BACKEND=#{KECCAK_BACKEND.inspect}. Supported: #{SUPPORTED_KECCAK_BACKENDS.join(", ")}" unless SUPPORTED_KECCAK_BACKENDS.include?(KECCAK_BACKEND)
93
+
94
+ case KECCAK_BACKEND
95
+ when "clean"
96
+ {
97
+ name: "clean",
98
+ include_dirs: [],
99
+ source_group: ["pqclean_common", [File.join(common_dir, "fips202.c")]]
100
+ }
101
+ when "xkcp"
102
+ # The optimized backend must provide the same fips202.h-compatible API as
103
+ # PQClean's common/fips202.c. Do not substitute OpenSSL EVP SHAKE here: the
104
+ # PQClean SHAKE state layout is part of the ML-KEM/ML-DSA call graph.
105
+ xkcp_dir = File.join(vendor_dir, "xkcp")
106
+ adapter_source = File.join(xkcp_dir, "pqclean_fips202_xkcp.c")
107
+
108
+ abort <<~MSG unless File.exist?(adapter_source)
109
+ PQCRYPTO_KECCAK_BACKEND=xkcp was requested, but no reviewed XKCP adapter was found.
110
+
111
+ Expected:
112
+ #{adapter_source}
113
+
114
+ Refusing to fall back silently to the clean backend. Vendor a fips202.h-compatible
115
+ XKCP adapter first, then run the full SHAKE-dependent KAT/regression test matrix.
116
+ MSG
117
+
118
+ {
119
+ name: "xkcp",
120
+ include_dirs: [xkcp_dir],
121
+ source_group: ["xkcp_keccak", [adapter_source]]
122
+ }
123
+ end
124
+ end
125
+
78
126
  def configure_pqclean(vendor_dir)
79
127
  return nil unless vendor_dir
80
128
 
@@ -85,17 +133,20 @@ def configure_pqclean(vendor_dir)
85
133
  mldsa_dir = File.join(pqclean_dir, "crypto_sign", "ml-dsa-65", "clean")
86
134
  common_dir = File.join(pqclean_dir, "common")
87
135
 
88
- include_dirs = [mlkem_dir, mldsa_dir, common_dir]
136
+ keccak_config = configure_keccak_backend(vendor_dir, common_dir)
137
+
138
+ include_dirs = [mlkem_dir, mldsa_dir, common_dir, *keccak_config[:include_dirs]]
89
139
  return nil unless include_dirs.all? { |dir| Dir.exist?(dir) }
90
140
 
91
141
  mlkem_sources = Dir.glob(File.join(mlkem_dir, "*.c")).sort
92
142
  mldsa_sources = Dir.glob(File.join(mldsa_dir, "*.c")).sort
93
- common_sources = %w[fips202.c sha2.c sp800-185.c].map { |name| File.join(common_dir, name) }
143
+ common_sources = %w[sha2.c sp800-185.c].map { |name| File.join(common_dir, name) }
94
144
 
95
145
  source_groups = [
96
146
  ["pqclean_mlkem", mlkem_sources],
97
147
  ["pqclean_mldsa", mldsa_sources],
98
- ["pqclean_common", common_sources]
148
+ ["pqclean_common", common_sources],
149
+ keccak_config[:source_group]
99
150
  ]
100
151
 
101
152
  return nil unless source_groups.all? { |_, sources| sources.all? { |path| File.exist?(path) } }
@@ -105,6 +156,7 @@ def configure_pqclean(vendor_dir)
105
156
 
106
157
  {
107
158
  include_dirs: include_dirs,
159
+ keccak_backend: keccak_config[:name],
108
160
  source_groups: source_groups
109
161
  }
110
162
  end
@@ -155,6 +207,7 @@ pqclean_config = configure_pqclean(vendor_dir)
155
207
  puts "OpenSSL: system"
156
208
  abort "PQClean vendored sources are required. Run: bundle exec rake vendor" unless pqclean_config
157
209
  puts "PQClean: vendored (randombytes overridden by pq_randombytes.c)"
210
+ puts "Keccak backend: #{pqclean_config[:keccak_backend]}"
158
211
  puts "Output: pqcrypto/pqcrypto_secure"
159
212
  puts "===================================="
160
213