pq_crypto 0.3.2 → 0.4.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 +4 -4
- data/.github/workflows/ci.yml +56 -0
- data/CHANGELOG.md +37 -0
- data/GET_STARTED.md +361 -40
- data/README.md +58 -241
- data/SECURITY.md +101 -82
- data/ext/pqcrypto/extconf.rb +40 -7
- data/ext/pqcrypto/mldsa_api.h +71 -1
- data/ext/pqcrypto/mlkem_api.h +24 -0
- data/ext/pqcrypto/pq_externalmu.c +14 -1
- data/ext/pqcrypto/pqcrypto_ruby_secure.c +484 -81
- data/ext/pqcrypto/pqcrypto_secure.c +179 -72
- data/ext/pqcrypto/pqcrypto_secure.h +87 -7
- data/ext/pqcrypto/pqcrypto_version.h +7 -0
- data/ext/pqcrypto/vendor/.vendored +1 -1
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/LICENSE +5 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/Makefile +19 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/Makefile.Microsoft_nmake +23 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/api.h +18 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/cbd.c +83 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/cbd.h +11 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/indcpa.c +327 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/indcpa.h +22 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/kem.c +164 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/kem.h +23 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/ntt.c +146 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/ntt.h +14 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/params.h +36 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/poly.c +311 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/poly.h +37 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/polyvec.c +198 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/polyvec.h +26 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/reduce.c +41 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/reduce.h +13 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/symmetric-shake.c +71 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/symmetric.h +30 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/verify.c +67 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/verify.h +13 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/LICENSE +5 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/Makefile +19 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/Makefile.Microsoft_nmake +23 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/api.h +18 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/cbd.c +108 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/cbd.h +11 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/indcpa.c +327 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/indcpa.h +22 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/kem.c +164 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/kem.h +23 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/ntt.c +146 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/ntt.h +14 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/params.h +36 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/poly.c +299 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/poly.h +37 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/polyvec.c +188 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/polyvec.h +26 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/reduce.c +41 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/reduce.h +13 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/symmetric-shake.c +71 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/symmetric.h +30 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/verify.c +67 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/verify.h +13 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/LICENSE +5 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/Makefile +19 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/Makefile.Microsoft_nmake +23 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/api.h +50 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/ntt.c +98 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/ntt.h +10 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/packing.c +261 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/packing.h +31 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/params.h +44 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/poly.c +848 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/poly.h +52 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/polyvec.c +415 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/polyvec.h +65 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/reduce.c +69 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/reduce.h +17 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/rounding.c +98 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/rounding.h +14 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/sign.c +407 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/sign.h +47 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/symmetric-shake.c +26 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/symmetric.h +34 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/LICENSE +5 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/Makefile +19 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/Makefile.Microsoft_nmake +23 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/api.h +50 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/ntt.c +98 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/ntt.h +10 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/packing.c +261 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/packing.h +31 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/params.h +44 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/poly.c +823 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/poly.h +52 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/polyvec.c +415 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/polyvec.h +65 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/reduce.c +69 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/reduce.h +17 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/rounding.c +92 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/rounding.h +14 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/sign.c +407 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/sign.h +47 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/symmetric-shake.c +26 -0
- data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/symmetric.h +34 -0
- data/lib/pq_crypto/algorithm_registry.rb +200 -0
- data/lib/pq_crypto/hybrid_kem.rb +1 -12
- data/lib/pq_crypto/kem.rb +104 -13
- data/lib/pq_crypto/pkcs8.rb +387 -0
- data/lib/pq_crypto/serialization.rb +1 -14
- data/lib/pq_crypto/signature.rb +123 -17
- data/lib/pq_crypto/spki.rb +131 -0
- data/lib/pq_crypto/version.rb +1 -1
- data/lib/pq_crypto.rb +78 -19
- data/script/vendor_libs.rb +4 -0
- metadata +95 -3
data/README.md
CHANGED
|
@@ -1,284 +1,101 @@
|
|
|
1
1
|
# pq_crypto
|
|
2
2
|
|
|
3
3
|
`pq_crypto` is a primitive-first Ruby gem for post-quantum cryptography.
|
|
4
|
+
It provides small Ruby APIs for ML-KEM, ML-DSA, and one hybrid X-Wing KEM,
|
|
5
|
+
with standard key serialization where available.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
- `PQCrypto::Signature` — `ML-DSA-65` (FIPS 204)
|
|
9
|
-
- `PQCrypto::HybridKEM` — `ML-KEM-768 + X25519` combined via the
|
|
10
|
-
[X-Wing](https://datatracker.ietf.org/doc/draft-connolly-cfrg-xwing-kem/)
|
|
11
|
-
SHA3-256 combiner
|
|
12
|
-
|
|
13
|
-
The gem is backed by vendored `PQClean` sources for `ML-KEM-768` /
|
|
14
|
-
`ML-DSA-65` and by OpenSSL for `X25519` and `SHA3-256`. Every piece of
|
|
15
|
-
conventional-crypto functionality goes through standard library calls
|
|
16
|
-
(`EVP_*`, `RAND_bytes`, `CRYPTO_memcmp`, `BIO_f_base64`) — nothing
|
|
17
|
-
roll-your-own where a library primitive exists.
|
|
18
|
-
|
|
19
|
-
## Status
|
|
20
|
-
|
|
21
|
-
- primitive-first API only
|
|
22
|
-
- no protocol/session helpers in the public surface
|
|
23
|
-
- streaming ML-DSA signing/verification is available for large IO inputs
|
|
24
|
-
- serialization uses pq_crypto-specific `pqc_container_*` wrappers
|
|
25
|
-
- not audited
|
|
26
|
-
- not yet positioned as production-ready
|
|
7
|
+
The gem intentionally stays close to cryptographic primitives. It does not
|
|
8
|
+
provide protocol, session, transport, certificate-chain, or application-level
|
|
9
|
+
handshake helpers.
|
|
27
10
|
|
|
28
11
|
## Installation
|
|
29
12
|
|
|
30
|
-
Add the gem to your
|
|
13
|
+
Add the gem to your `Gemfile`:
|
|
31
14
|
|
|
32
15
|
```ruby
|
|
33
16
|
# Gemfile
|
|
34
17
|
gem "pq_crypto"
|
|
35
18
|
```
|
|
36
19
|
|
|
20
|
+
Then install it:
|
|
21
|
+
|
|
37
22
|
```bash
|
|
38
23
|
bundle install
|
|
39
|
-
bundle exec rake compile
|
|
40
24
|
```
|
|
41
25
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- Ruby 3.4.x
|
|
45
|
-
- a C toolchain with C11 support (for `_Static_assert` / `_Thread_local`)
|
|
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:
|
|
26
|
+
When working from a source checkout, compile the native extension before
|
|
27
|
+
running tests or examples:
|
|
51
28
|
|
|
52
29
|
```bash
|
|
53
|
-
|
|
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.
|
|
61
|
-
|
|
62
|
-
## Async / Fiber scheduler support
|
|
63
|
-
|
|
64
|
-
`pq_crypto` does not require any gem-specific Async configuration. On
|
|
65
|
-
Ruby 3.4, `sign` and `verify` use Ruby's scheduler-aware
|
|
66
|
-
`rb_nogvl(..., RB_NOGVL_OFFLOAD_SAFE)` path automatically.
|
|
67
|
-
|
|
68
|
-
That means:
|
|
69
|
-
|
|
70
|
-
- without a Fiber scheduler, these methods fall back to the ordinary
|
|
71
|
-
no-GVL behavior;
|
|
72
|
-
- with a scheduler that implements `blocking_operation_wait` (for
|
|
73
|
-
example `Async` with a worker pool), the blocking native work can
|
|
74
|
-
be moved off the event loop.
|
|
75
|
-
|
|
76
|
-
This integration is intentionally limited to `sign` and `verify`; the
|
|
77
|
-
faster primitive operations keep the lower-overhead path.
|
|
78
|
-
|
|
79
|
-
Example with `Async`:
|
|
80
|
-
|
|
81
|
-
```ruby
|
|
82
|
-
require "async"
|
|
83
|
-
require "pq_crypto"
|
|
84
|
-
|
|
85
|
-
keypair = PQCrypto::Signature.generate(:ml_dsa_65)
|
|
86
|
-
message = "hello" * 100_000
|
|
87
|
-
|
|
88
|
-
reactor = Async::Reactor.new(worker_pool: true)
|
|
89
|
-
root = reactor.async do |task|
|
|
90
|
-
task.async do
|
|
91
|
-
signature = keypair.secret_key.sign(message)
|
|
92
|
-
keypair.public_key.verify(message, signature)
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
task.async do
|
|
96
|
-
sleep 0.01
|
|
97
|
-
puts "event loop stayed responsive"
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
reactor.run
|
|
102
|
-
root.wait
|
|
103
|
-
reactor.close
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
## Primitive API
|
|
107
|
-
|
|
108
|
-
### ML-KEM-768
|
|
109
|
-
|
|
110
|
-
```ruby
|
|
111
|
-
keypair = PQCrypto::KEM.generate(:ml_kem_768)
|
|
112
|
-
result = keypair.public_key.encapsulate
|
|
113
|
-
shared_secret = keypair.secret_key.decapsulate(result.ciphertext)
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
### ML-DSA-65
|
|
117
|
-
|
|
118
|
-
One-shot signing keeps the existing API:
|
|
119
|
-
|
|
120
|
-
```ruby
|
|
121
|
-
keypair = PQCrypto::Signature.generate(:ml_dsa_65)
|
|
122
|
-
signature = keypair.secret_key.sign("hello")
|
|
123
|
-
|
|
124
|
-
keypair.public_key.verify("hello", signature) # => true / false
|
|
125
|
-
keypair.public_key.verify!("hello", signature) # raises on mismatch
|
|
126
|
-
```
|
|
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) }
|
|
30
|
+
bundle exec rake compile
|
|
31
|
+
bundle exec rake test
|
|
153
32
|
```
|
|
154
33
|
|
|
155
|
-
|
|
156
|
-
FIPS 204. `verify_io!` raises `PQCrypto::VerificationError` on mismatch.
|
|
157
|
-
|
|
158
|
-
Note: `verify` returns a plain boolean for normal outcomes. `verify!`
|
|
159
|
-
raises `PQCrypto::VerificationError` when the signature does not
|
|
160
|
-
match.
|
|
161
|
-
|
|
162
|
-
### Hybrid ML-KEM-768 + X25519 (X-Wing)
|
|
163
|
-
|
|
164
|
-
```ruby
|
|
165
|
-
keypair = PQCrypto::HybridKEM.generate(:ml_kem_768_x25519_xwing)
|
|
166
|
-
result = keypair.public_key.encapsulate
|
|
167
|
-
shared_secret = keypair.secret_key.decapsulate(result.ciphertext)
|
|
168
|
-
```
|
|
34
|
+
## What this gem provides
|
|
169
35
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
36
|
+
| Area | Capabilities |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| ML-KEM | Key generation, encapsulation, decapsulation, raw key import/export, SPKI public keys, PKCS#8 private keys. |
|
|
39
|
+
| ML-DSA | Key generation, signing, verification, streaming signing/verification for large inputs, raw key import/export, SPKI public keys, PKCS#8 private keys. |
|
|
40
|
+
| Hybrid KEM | ML-KEM-768 + X25519 using the X-Wing combiner. |
|
|
41
|
+
| Serialization | Standard SPKI / PKCS#8 for NIST PQC keys, plus frozen `pqc_container_*` compatibility formats for the original algorithms. |
|
|
42
|
+
| Safety helpers | Best-effort secret wiping and constant-time equality for key comparisons. |
|
|
43
|
+
| Introspection | Supported algorithm lists, algorithm metadata, backend/version helpers. |
|
|
173
44
|
|
|
174
|
-
|
|
175
|
-
ss = SHA3-256( ss_M || ss_X || ct_X || pk_X || "\.//^\" )
|
|
176
|
-
```
|
|
45
|
+
## Supported algorithms
|
|
177
46
|
|
|
178
|
-
|
|
179
|
-
|
|
47
|
+
| Family | Algorithms | Notes |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| KEM | `:ml_kem_512`, `:ml_kem_768`, `:ml_kem_1024` | FIPS 203 ML-KEM. Standard SPKI public keys and PKCS#8 private keys. |
|
|
50
|
+
| Signature | `:ml_dsa_44`, `:ml_dsa_65`, `:ml_dsa_87` | FIPS 204 ML-DSA. Standard SPKI public keys and PKCS#8 private keys. |
|
|
51
|
+
| Hybrid KEM | `:ml_kem_768_x25519_xwing` | ML-KEM-768 + X25519 hybrid KEM using the X-Wing construction. |
|
|
180
52
|
|
|
181
|
-
|
|
53
|
+
Standard encodings use RFC 9935 OIDs for ML-KEM and RFC 9881 OIDs for
|
|
54
|
+
ML-DSA. `AlgorithmIdentifier.parameters` are omitted, not encoded as `NULL`.
|
|
182
55
|
|
|
183
|
-
|
|
56
|
+
The `pqc_container_*` format is project-local and kept only for backward
|
|
57
|
+
compatibility. It is not ASN.1, SPKI, or PKCS#8. It remains limited to the
|
|
58
|
+
original algorithms:
|
|
184
59
|
|
|
185
|
-
- `
|
|
186
|
-
- `
|
|
187
|
-
-
|
|
188
|
-
- `*_from_pqc_container_pem`
|
|
60
|
+
- `:ml_kem_768`
|
|
61
|
+
- `:ml_dsa_65`
|
|
62
|
+
- `:ml_kem_768_x25519_xwing`
|
|
189
63
|
|
|
190
|
-
|
|
64
|
+
## Requirements
|
|
191
65
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
imported = PQCrypto::KEM.public_key_from_pqc_container_der(der)
|
|
196
|
-
```
|
|
66
|
+
- Ruby 3.4 or later
|
|
67
|
+
- a C toolchain with C11 support
|
|
68
|
+
- OpenSSL 3.0 or later with SHA3-256 and SHAKE256 available
|
|
197
69
|
|
|
198
|
-
|
|
199
|
-
intended for stable import/export inside `pq_crypto` itself and are
|
|
200
|
-
not advertised as interoperable with external PKI tooling.
|
|
70
|
+
## Security status
|
|
201
71
|
|
|
202
|
-
|
|
72
|
+
`pq_crypto` is experimental and not audited. Treat it as a low-level primitive
|
|
73
|
+
library, not a complete security protocol. See [`SECURITY.md`](SECURITY.md) for
|
|
74
|
+
audit status, serialization caveats, hybrid-KEM notes, and interoperability
|
|
75
|
+
warnings.
|
|
203
76
|
|
|
204
|
-
|
|
205
|
-
in place. Key objects hold a private copy of their bytes, so `wipe!`
|
|
206
|
-
on a `SecretKey` zeroes **only** that internal copy — any prior Ruby
|
|
207
|
-
string the caller holds is untouched. If you need to wipe the
|
|
208
|
-
caller-side buffer, do so explicitly:
|
|
209
|
-
|
|
210
|
-
```ruby
|
|
211
|
-
raw = File.binread(path)
|
|
212
|
-
key = PQCrypto::KEM.secret_key_from_bytes(:ml_kem_768, raw)
|
|
213
|
-
PQCrypto.secure_wipe(raw) # scrub the original input
|
|
214
|
-
# ... use key ...
|
|
215
|
-
key.wipe! # scrub the key's internal copy
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
## Constant-time comparison
|
|
219
|
-
|
|
220
|
-
`==` on `PublicKey` / `SecretKey` instances uses OpenSSL
|
|
221
|
-
`CRYPTO_memcmp` through a `PQCrypto.ct_equals` helper so comparisons
|
|
222
|
-
do not leak timing information about a prefix match.
|
|
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
|
-
|
|
230
|
-
## Introspection
|
|
77
|
+
## Useful entry points
|
|
231
78
|
|
|
232
79
|
```ruby
|
|
233
80
|
PQCrypto.version
|
|
234
81
|
PQCrypto.backend
|
|
235
82
|
PQCrypto.supported_kems
|
|
236
|
-
PQCrypto.supported_hybrid_kems
|
|
237
83
|
PQCrypto.supported_signatures
|
|
238
|
-
PQCrypto
|
|
239
|
-
PQCrypto::HybridKEM.details(:ml_kem_768_x25519_xwing)
|
|
240
|
-
PQCrypto::Signature.details(:ml_dsa_65)
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
## Testing helpers
|
|
244
|
-
|
|
245
|
-
Deterministic test hooks are exposed under `PQCrypto::Testing` for
|
|
246
|
-
regression coverage:
|
|
247
|
-
|
|
248
|
-
- `ml_kem_keypair_from_seed` — requires a 64-byte `d||z` seed (FIPS 203)
|
|
249
|
-
- `ml_kem_encapsulate_from_seed` — requires a 32-byte seed
|
|
250
|
-
- `ml_dsa_keypair_from_seed` — requires a 32-byte seed
|
|
251
|
-
- `ml_dsa_sign_from_seed` — requires a 32-byte seed
|
|
252
|
-
|
|
253
|
-
These helpers are intended for tests only. They work by installing a
|
|
254
|
-
thread-local seed-replay mode inside the gem's `randombytes()` for
|
|
255
|
-
the duration of the call, then call the stock PQClean entrypoints.
|
|
256
|
-
No internal PQClean algorithm logic is reimplemented in this gem.
|
|
257
|
-
|
|
258
|
-
## Development
|
|
259
|
-
|
|
260
|
-
Run the test suite with:
|
|
84
|
+
PQCrypto.supported_hybrid_kems
|
|
261
85
|
|
|
262
|
-
|
|
263
|
-
|
|
86
|
+
PQCrypto::KEM.generate(:ml_kem_768)
|
|
87
|
+
PQCrypto::Signature.generate(:ml_dsa_65)
|
|
88
|
+
PQCrypto::HybridKEM.generate(:ml_kem_768_x25519_xwing)
|
|
264
89
|
```
|
|
265
90
|
|
|
266
|
-
|
|
267
|
-
update the vendor snapshot. The refresh script has a safe pinned
|
|
268
|
-
default and records the exact vendored snapshot in
|
|
269
|
-
`ext/pqcrypto/vendor/.vendored`:
|
|
91
|
+
## More examples
|
|
270
92
|
|
|
271
|
-
|
|
272
|
-
bundle exec ruby script/vendor_libs.rb
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
To intentionally change the upstream snapshot, override all four
|
|
276
|
-
pinning inputs together:
|
|
93
|
+
Detailed usage examples live in [`GET_STARTED.md`](GET_STARTED.md):
|
|
277
94
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
95
|
+
- generating keys
|
|
96
|
+
- ML-KEM encapsulation / decapsulation
|
|
97
|
+
- ML-DSA signing / verification
|
|
98
|
+
- streaming ML-DSA for large files
|
|
99
|
+
- SPKI and PKCS#8 serialization
|
|
100
|
+
- `pqc_container_*` compatibility serialization
|
|
101
|
+
- secure wiping and practical safety notes
|
data/SECURITY.md
CHANGED
|
@@ -4,125 +4,144 @@
|
|
|
4
4
|
|
|
5
5
|
`pq_crypto` exposes a primitive-first public surface:
|
|
6
6
|
|
|
7
|
-
- `PQCrypto::KEM`
|
|
8
|
-
- `PQCrypto::Signature`
|
|
9
|
-
- `PQCrypto::HybridKEM`
|
|
7
|
+
- `PQCrypto::KEM` — ML-KEM-512, ML-KEM-768, ML-KEM-1024
|
|
8
|
+
- `PQCrypto::Signature` — ML-DSA-44, ML-DSA-65, ML-DSA-87
|
|
9
|
+
- `PQCrypto::HybridKEM` — ML-KEM-768 + X25519 via the X-Wing combiner
|
|
10
10
|
- `PQCrypto.secure_wipe`
|
|
11
|
-
- `PQCrypto.ct_equals`
|
|
11
|
+
- `PQCrypto.ct_equals`
|
|
12
12
|
|
|
13
|
-
The gem does
|
|
14
|
-
|
|
13
|
+
The gem does not publish protocol/session helpers as part of the supported
|
|
14
|
+
public API.
|
|
15
15
|
|
|
16
16
|
## Audit status
|
|
17
17
|
|
|
18
18
|
This project has not been audited. Treat it as experimental software.
|
|
19
19
|
|
|
20
|
+
The test surface includes deterministic regression tests, NIST ACVP KAT test
|
|
21
|
+
infrastructure, and OpenSSL 3.5+ interoperability tests for standard SPKI /
|
|
22
|
+
PKCS#8 encodings where the linked OpenSSL exposes the corresponding ML-KEM /
|
|
23
|
+
ML-DSA EVP support. These tests improve compatibility coverage but are not a
|
|
24
|
+
substitute for a security audit.
|
|
25
|
+
|
|
20
26
|
## Algorithm notes
|
|
21
27
|
|
|
22
|
-
### ML-KEM
|
|
28
|
+
### ML-KEM / ML-DSA
|
|
23
29
|
|
|
24
|
-
The post-quantum primitives are backed by vendored `PQClean` sources
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
gem.
|
|
30
|
+
The post-quantum primitives are backed by vendored `PQClean` sources and called
|
|
31
|
+
through PQClean's public `crypto_kem_*` and `crypto_sign_*` entrypoints. The gem
|
|
32
|
+
does not reimplement ML-KEM, ML-DSA, SHAKE, or Keccak.
|
|
28
33
|
|
|
29
34
|
### HybridKEM
|
|
30
35
|
|
|
31
|
-
`PQCrypto::HybridKEM` implements the
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
36
|
+
`PQCrypto::HybridKEM` implements the X-Wing construction from
|
|
37
|
+
`draft-connolly-cfrg-xwing-kem-10`.
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
The X-Wing secret decapsulation key is a 32-byte seed. It is expanded with
|
|
40
|
+
SHAKE256 into the ML-KEM-768 and X25519 private material used internally for
|
|
41
|
+
decapsulation. The public key and ciphertext are the fixed-length
|
|
42
|
+
concatenations specified by the draft.
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
```text
|
|
45
|
+
ss = SHA3-256( ss_M || ss_X || ct_X || pk_X || XWingLabel )
|
|
46
|
+
```
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
the strong Diffie-Hellman assumption for X25519 (in the ROM), and
|
|
45
|
-
post-quantum IND-CCA security in the standard model assuming ML-KEM-768
|
|
46
|
-
is IND-CCA secure and SHA3-256 behaves as a PRF.
|
|
48
|
+
where `XWingLabel = "\.//^\"`.
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
interoperability should still be verified against the reference
|
|
50
|
+
External interoperability should be verified against the reference
|
|
50
51
|
implementation before relying on it.
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
## Serialization formats
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
`crypto_sign_keypair` / `crypto_sign_signature` (for ML-DSA) and
|
|
56
|
-
`crypto_kem_keypair_derand` / `crypto_kem_enc_derand` (for ML-KEM)
|
|
57
|
-
against a caller-supplied seed. For ML-DSA, which has no derand API
|
|
58
|
-
upstream, the gem installs a thread-local seed-replay buffer inside
|
|
59
|
-
its `randombytes()` implementation; outside of a test call the same
|
|
60
|
-
`randombytes()` entry delegates directly to OpenSSL `RAND_bytes`. No
|
|
61
|
-
internal PQClean algorithm logic is reimplemented in this gem.
|
|
55
|
+
### pq_crypto-local `pqc_container_*`
|
|
62
56
|
|
|
63
|
-
|
|
57
|
+
`pqc_container_*` DER/PEM wrappers are pq_crypto-specific containers. They are:
|
|
64
58
|
|
|
65
|
-
|
|
59
|
+
- not ASN.1
|
|
60
|
+
- not SPKI
|
|
61
|
+
- not PKCS#8
|
|
62
|
+
- not advertised as interoperable with OpenSSL, Go, Java, or PKI tooling
|
|
66
63
|
|
|
67
|
-
|
|
64
|
+
This format is frozen for backward compatibility and remains limited to the
|
|
65
|
+
original three algorithms:
|
|
68
66
|
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
-
|
|
67
|
+
- `:ml_kem_768`
|
|
68
|
+
- `:ml_dsa_65`
|
|
69
|
+
- `:ml_kem_768_x25519_xwing`
|
|
70
|
+
|
|
71
|
+
### Standard SPKI / PKCS#8
|
|
72
|
+
|
|
73
|
+
ML-KEM and ML-DSA use standard SPKI public-key and PKCS#8 private-key encodings
|
|
74
|
+
for the NIST parameter sets. AlgorithmIdentifier parameters are absent, not
|
|
75
|
+
encoded as `NULL`.
|
|
76
|
+
|
|
77
|
+
| Algorithm | Standard OID | Reference |
|
|
78
|
+
| --- | --- | --- |
|
|
79
|
+
| ML-KEM-512 | `2.16.840.1.101.3.4.4.1` | RFC 9935 |
|
|
80
|
+
| ML-KEM-768 | `2.16.840.1.101.3.4.4.2` | RFC 9935 |
|
|
81
|
+
| ML-KEM-1024 | `2.16.840.1.101.3.4.4.3` | RFC 9935 |
|
|
82
|
+
| ML-DSA-44 | `2.16.840.1.101.3.4.3.17` | RFC 9881 |
|
|
83
|
+
| ML-DSA-65 | `2.16.840.1.101.3.4.3.18` | RFC 9881 |
|
|
84
|
+
| ML-DSA-87 | `2.16.840.1.101.3.4.3.19` | RFC 9881 |
|
|
85
|
+
|
|
86
|
+
`PQCrypto::KEM.details` / `PQCrypto::Signature.details` keep `:oid` as the
|
|
87
|
+
legacy `pqc_container_*` OID for backward compatibility. Use
|
|
88
|
+
`PQCrypto::AlgorithmRegistry.standard_oid` for the standard OID.
|
|
72
89
|
|
|
73
|
-
|
|
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`.
|
|
90
|
+
## ML-DSA seed-format imports
|
|
76
91
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
92
|
+
ML-DSA seed and both-form PKCS#8 imports are disabled by default. To import
|
|
93
|
+
these encodings, callers must explicitly set:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
PQCrypto::PKCS8.allow_ml_dsa_seed_format = true
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
This opt-in exists because PQClean exposes no public ML-DSA
|
|
100
|
+
`crypto_sign_keypair_derand` entrypoint. The implementation therefore reuses the
|
|
101
|
+
same thread-local seed-replay `randombytes()` path introduced for KAT tests to
|
|
102
|
+
expand the RFC 9881 seed into an expanded private key. The replay buffer is
|
|
103
|
+
thread-local, cleared immediately after expansion, and remains inactive for all
|
|
104
|
+
normal production randomness paths.
|
|
105
|
+
|
|
106
|
+
For `both` encodings, the decoder expands the seed and rejects the key if the
|
|
107
|
+
expandedKey half does not match the seed-derived key.
|
|
108
|
+
|
|
109
|
+
## Deterministic test hooks
|
|
110
|
+
|
|
111
|
+
`PQCrypto::Testing` deterministic helpers drive the stock PQClean entrypoints
|
|
112
|
+
against caller-supplied seeds. For ML-DSA, which has no derand API upstream, the
|
|
113
|
+
gem installs a thread-local seed-replay buffer inside its `randombytes()`
|
|
114
|
+
implementation; outside of a test call the same `randombytes()` entry delegates
|
|
115
|
+
directly to OpenSSL `RAND_bytes`.
|
|
83
116
|
|
|
84
117
|
## Memory wiping
|
|
85
118
|
|
|
86
|
-
`PQCrypto.secure_wipe` clears mutable Ruby strings in place. Ruby key
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
119
|
+
`PQCrypto.secure_wipe` clears mutable Ruby strings in place. Ruby key objects
|
|
120
|
+
take a copy of the bytes passed into their constructor and expose `#wipe!` to
|
|
121
|
+
zero only that internal copy. Ruby garbage collection and prior derived copies
|
|
122
|
+
may still leave sensitive material elsewhere in process memory.
|
|
123
|
+
|
|
124
|
+
Secret key objects redact `inspect` output and intentionally do not expose a
|
|
125
|
+
public `fingerprint` method. This avoids accidental logging of raw secret bytes
|
|
126
|
+
or stable secret-derived identifiers.
|
|
92
127
|
|
|
93
128
|
## OpenSSL baseline
|
|
94
129
|
|
|
95
|
-
`pq_crypto` requires OpenSSL
|
|
130
|
+
`pq_crypto` requires OpenSSL 3.0 or later.
|
|
96
131
|
|
|
97
132
|
OpenSSL is used for:
|
|
98
133
|
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
- Base64 encode/decode for PEM via OpenSSL
|
|
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.
|
|
134
|
+
- X25519 key generation and key agreement
|
|
135
|
+
- SHA3-256 for the X-Wing combiner
|
|
136
|
+
- RAND_bytes as the production entropy source for `randombytes()`
|
|
137
|
+
- CRYPTO_memcmp for constant-time comparison
|
|
138
|
+
- Base64 encode/decode for PEM via OpenSSL BIOs
|
|
111
139
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
OpenSSL, native wrapper buffers, serialization, logging, crash dumps, or
|
|
115
|
-
the garbage collector.
|
|
140
|
+
OpenSSL 3.5+ is additionally used in interop tests when ML-KEM / ML-DSA EVP
|
|
141
|
+
support is available.
|
|
116
142
|
|
|
117
143
|
## Threading
|
|
118
144
|
|
|
119
145
|
Concurrent read-only operations on primitive key objects are supported.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
pinned in place.
|
|
123
|
-
|
|
124
|
-
The deterministic test hooks use a thread-local seed-replay mode
|
|
125
|
-
around `randombytes()`, so a test running on one thread does not
|
|
126
|
-
affect production callers on other threads. The deterministic helpers
|
|
127
|
-
remain test-only utilities and should not be relied on as a general
|
|
128
|
-
multi-threading contract.
|
|
146
|
+
Mutating operations such as `wipe!` must not race with other uses of the same
|
|
147
|
+
object.
|
data/ext/pqcrypto/extconf.rb
CHANGED
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "mkmf"
|
|
5
|
+
require_relative "../../lib/pq_crypto/version"
|
|
6
|
+
|
|
7
|
+
def generate_version_header!
|
|
8
|
+
version = PQCrypto::VERSION
|
|
9
|
+
unless version.match?(/\A[0-9A-Za-z][0-9A-Za-z._+-]*\z/)
|
|
10
|
+
abort "Invalid PQCrypto::VERSION for C header: #{version.inspect}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
header = File.join(__dir__, "pqcrypto_version.h")
|
|
14
|
+
File.write(header, <<~C)
|
|
15
|
+
/* Generated by extconf.rb from lib/pq_crypto/version.rb. Do not edit. */
|
|
16
|
+
#ifndef PQCRYPTO_VERSION_H
|
|
17
|
+
#define PQCRYPTO_VERSION_H
|
|
18
|
+
|
|
19
|
+
#define PQCRYPTO_VERSION #{version.dump}
|
|
20
|
+
|
|
21
|
+
#endif
|
|
22
|
+
C
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
generate_version_header!
|
|
5
26
|
|
|
6
27
|
$CFLAGS << " -std=c11 -Wall -Wextra -O2"
|
|
7
28
|
$CFLAGS << " -fstack-protector-strong -D_FORTIFY_SOURCE=2"
|
|
@@ -129,22 +150,34 @@ def configure_pqclean(vendor_dir)
|
|
|
129
150
|
pqclean_dir = File.join(vendor_dir, "pqclean")
|
|
130
151
|
return nil unless Dir.exist?(pqclean_dir)
|
|
131
152
|
|
|
132
|
-
|
|
133
|
-
|
|
153
|
+
mlkem_dirs = {
|
|
154
|
+
"pqclean_mlkem512" => File.join(pqclean_dir, "crypto_kem", "ml-kem-512", "clean"),
|
|
155
|
+
"pqclean_mlkem768" => File.join(pqclean_dir, "crypto_kem", "ml-kem-768", "clean"),
|
|
156
|
+
"pqclean_mlkem1024" => File.join(pqclean_dir, "crypto_kem", "ml-kem-1024", "clean")
|
|
157
|
+
}
|
|
158
|
+
mldsa_dirs = {
|
|
159
|
+
"pqclean_mldsa44" => File.join(pqclean_dir, "crypto_sign", "ml-dsa-44", "clean"),
|
|
160
|
+
"pqclean_mldsa65" => File.join(pqclean_dir, "crypto_sign", "ml-dsa-65", "clean"),
|
|
161
|
+
"pqclean_mldsa87" => File.join(pqclean_dir, "crypto_sign", "ml-dsa-87", "clean")
|
|
162
|
+
}
|
|
134
163
|
common_dir = File.join(pqclean_dir, "common")
|
|
135
164
|
|
|
136
165
|
keccak_config = configure_keccak_backend(vendor_dir, common_dir)
|
|
137
166
|
|
|
138
|
-
include_dirs = [
|
|
167
|
+
include_dirs = [*mlkem_dirs.values, *mldsa_dirs.values, common_dir, *keccak_config[:include_dirs]]
|
|
139
168
|
return nil unless include_dirs.all? { |dir| Dir.exist?(dir) }
|
|
140
169
|
|
|
141
|
-
|
|
142
|
-
|
|
170
|
+
mlkem_source_groups = mlkem_dirs.map do |prefix, dir|
|
|
171
|
+
[prefix, Dir.glob(File.join(dir, "*.c")).sort]
|
|
172
|
+
end
|
|
173
|
+
mldsa_source_groups = mldsa_dirs.map do |prefix, dir|
|
|
174
|
+
[prefix, Dir.glob(File.join(dir, "*.c")).sort]
|
|
175
|
+
end
|
|
143
176
|
common_sources = %w[sha2.c sp800-185.c].map { |name| File.join(common_dir, name) }
|
|
144
177
|
|
|
145
178
|
source_groups = [
|
|
146
|
-
|
|
147
|
-
|
|
179
|
+
*mlkem_source_groups,
|
|
180
|
+
*mldsa_source_groups,
|
|
148
181
|
["pqclean_common", common_sources],
|
|
149
182
|
keccak_config[:source_group]
|
|
150
183
|
]
|