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.
Files changed (114) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +56 -0
  3. data/CHANGELOG.md +37 -0
  4. data/GET_STARTED.md +361 -40
  5. data/README.md +58 -241
  6. data/SECURITY.md +101 -82
  7. data/ext/pqcrypto/extconf.rb +40 -7
  8. data/ext/pqcrypto/mldsa_api.h +71 -1
  9. data/ext/pqcrypto/mlkem_api.h +24 -0
  10. data/ext/pqcrypto/pq_externalmu.c +14 -1
  11. data/ext/pqcrypto/pqcrypto_ruby_secure.c +484 -81
  12. data/ext/pqcrypto/pqcrypto_secure.c +179 -72
  13. data/ext/pqcrypto/pqcrypto_secure.h +87 -7
  14. data/ext/pqcrypto/pqcrypto_version.h +7 -0
  15. data/ext/pqcrypto/vendor/.vendored +1 -1
  16. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/LICENSE +5 -0
  17. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/Makefile +19 -0
  18. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/Makefile.Microsoft_nmake +23 -0
  19. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/api.h +18 -0
  20. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/cbd.c +83 -0
  21. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/cbd.h +11 -0
  22. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/indcpa.c +327 -0
  23. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/indcpa.h +22 -0
  24. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/kem.c +164 -0
  25. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/kem.h +23 -0
  26. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/ntt.c +146 -0
  27. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/ntt.h +14 -0
  28. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/params.h +36 -0
  29. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/poly.c +311 -0
  30. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/poly.h +37 -0
  31. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/polyvec.c +198 -0
  32. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/polyvec.h +26 -0
  33. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/reduce.c +41 -0
  34. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/reduce.h +13 -0
  35. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/symmetric-shake.c +71 -0
  36. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/symmetric.h +30 -0
  37. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/verify.c +67 -0
  38. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-1024/clean/verify.h +13 -0
  39. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/LICENSE +5 -0
  40. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/Makefile +19 -0
  41. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/Makefile.Microsoft_nmake +23 -0
  42. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/api.h +18 -0
  43. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/cbd.c +108 -0
  44. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/cbd.h +11 -0
  45. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/indcpa.c +327 -0
  46. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/indcpa.h +22 -0
  47. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/kem.c +164 -0
  48. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/kem.h +23 -0
  49. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/ntt.c +146 -0
  50. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/ntt.h +14 -0
  51. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/params.h +36 -0
  52. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/poly.c +299 -0
  53. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/poly.h +37 -0
  54. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/polyvec.c +188 -0
  55. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/polyvec.h +26 -0
  56. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/reduce.c +41 -0
  57. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/reduce.h +13 -0
  58. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/symmetric-shake.c +71 -0
  59. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/symmetric.h +30 -0
  60. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/verify.c +67 -0
  61. data/ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-512/clean/verify.h +13 -0
  62. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/LICENSE +5 -0
  63. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/Makefile +19 -0
  64. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/Makefile.Microsoft_nmake +23 -0
  65. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/api.h +50 -0
  66. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/ntt.c +98 -0
  67. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/ntt.h +10 -0
  68. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/packing.c +261 -0
  69. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/packing.h +31 -0
  70. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/params.h +44 -0
  71. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/poly.c +848 -0
  72. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/poly.h +52 -0
  73. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/polyvec.c +415 -0
  74. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/polyvec.h +65 -0
  75. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/reduce.c +69 -0
  76. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/reduce.h +17 -0
  77. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/rounding.c +98 -0
  78. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/rounding.h +14 -0
  79. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/sign.c +407 -0
  80. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/sign.h +47 -0
  81. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/symmetric-shake.c +26 -0
  82. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-44/clean/symmetric.h +34 -0
  83. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/LICENSE +5 -0
  84. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/Makefile +19 -0
  85. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/Makefile.Microsoft_nmake +23 -0
  86. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/api.h +50 -0
  87. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/ntt.c +98 -0
  88. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/ntt.h +10 -0
  89. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/packing.c +261 -0
  90. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/packing.h +31 -0
  91. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/params.h +44 -0
  92. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/poly.c +823 -0
  93. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/poly.h +52 -0
  94. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/polyvec.c +415 -0
  95. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/polyvec.h +65 -0
  96. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/reduce.c +69 -0
  97. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/reduce.h +17 -0
  98. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/rounding.c +92 -0
  99. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/rounding.h +14 -0
  100. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/sign.c +407 -0
  101. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/sign.h +47 -0
  102. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/symmetric-shake.c +26 -0
  103. data/ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-87/clean/symmetric.h +34 -0
  104. data/lib/pq_crypto/algorithm_registry.rb +200 -0
  105. data/lib/pq_crypto/hybrid_kem.rb +1 -12
  106. data/lib/pq_crypto/kem.rb +104 -13
  107. data/lib/pq_crypto/pkcs8.rb +387 -0
  108. data/lib/pq_crypto/serialization.rb +1 -14
  109. data/lib/pq_crypto/signature.rb +123 -17
  110. data/lib/pq_crypto/spki.rb +131 -0
  111. data/lib/pq_crypto/version.rb +1 -1
  112. data/lib/pq_crypto.rb +78 -19
  113. data/script/vendor_libs.rb +4 -0
  114. 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
- It exposes three public building blocks:
6
-
7
- - `PQCrypto::KEM` — pure `ML-KEM-768` (FIPS 203)
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 project and compile the extension:
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
- ### Native dependencies
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
- 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.
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
- `chunk_size` must be positive. `context` is limited to 255 bytes by
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
- 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:
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
- as specified by `draft-connolly-cfrg-xwing-kem-10`. See `SECURITY.md`
179
- for audit status and interoperability caveats.
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
- ## Serialization
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
- Key import/export is available through pq_crypto-specific containers:
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
- - `to_pqc_container_der`
186
- - `to_pqc_container_pem`
187
- - `*_from_pqc_container_der`
188
- - `*_from_pqc_container_pem`
60
+ - `:ml_kem_768`
61
+ - `:ml_dsa_65`
62
+ - `:ml_kem_768_x25519_xwing`
189
63
 
190
- Example:
64
+ ## Requirements
191
65
 
192
- ```ruby
193
- keypair = PQCrypto::KEM.generate(:ml_kem_768)
194
- der = keypair.public_key.to_pqc_container_der
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
- These containers are **not real ASN.1 SPKI or PKCS#8**. They are
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
- ## Secure wiping
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
- `PQCrypto.secure_wipe(str)` zeros the bytes of a mutable Ruby string
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::KEM.details(:ml_kem_768)
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
- ```bash
263
- bundle exec rake test
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
- Refresh vendored PQClean sources manually only when you intentionally
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
- ```bash
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
- ```bash
279
- PQCLEAN_VERSION=<full-git-commit> \
280
- PQCLEAN_URL=https://github.com/PQClean/PQClean/archive/<full-git-commit>.tar.gz \
281
- PQCLEAN_SHA256=<archive-sha256> \
282
- PQCLEAN_STRIP=PQClean-<full-git-commit> \
283
- bundle exec ruby script/vendor_libs.rb
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` (`ML-KEM-768`)
8
- - `PQCrypto::Signature` (`ML-DSA-65`)
9
- - `PQCrypto::HybridKEM` (`ML-KEM-768 + X25519` via the X-Wing combiner)
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` (constant-time byte-string comparison)
11
+ - `PQCrypto.ct_equals`
12
12
 
13
- The gem does **not** publish protocol/session helpers as part of the
14
- supported public API.
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-768 / ML-DSA-65
28
+ ### ML-KEM / ML-DSA
23
29
 
24
- The post-quantum primitives are backed by vendored `PQClean` sources
25
- and called through PQClean's public `crypto_kem_*` and `crypto_sign_*`
26
- entrypoints only. Internal PQClean symbols are not called from this
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 **X-Wing** construction from
32
- [`draft-connolly-cfrg-xwing-kem-10`](https://datatracker.ietf.org/doc/draft-connolly-cfrg-xwing-kem/).
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
- ss = SHA3-256( ss_M || ss_X || ct_X || pk_X || XWingLabel )
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
- where `XWingLabel = "\.//^\"` (6 ASCII bytes).
44
+ ```text
45
+ ss = SHA3-256( ss_M || ss_X || ct_X || pk_X || XWingLabel )
46
+ ```
42
47
 
43
- X-Wing as specified has a proof of classical IND-CCA security under
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
- 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
+ External interoperability should be verified against the reference
50
51
  implementation before relying on it.
51
52
 
52
- ### Deterministic test hooks
53
+ ## Serialization formats
53
54
 
54
- `PQCrypto::Testing` deterministic helpers drive the stock PQClean
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
- ## Serialization
57
+ `pqc_container_*` DER/PEM wrappers are pq_crypto-specific containers. They are:
64
58
 
65
- `pqc_container_*` DER/PEM wrappers are pq_crypto-specific containers.
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
- They are:
64
+ This format is frozen for backward compatibility and remains limited to the
65
+ original three algorithms:
68
66
 
69
- - not real SPKI
70
- - not real PKCS#8
71
- - not advertised as interoperable with OpenSSL, Go, Java, or PKI tooling
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
- 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`.
90
+ ## ML-DSA seed-format imports
76
91
 
77
- The hybrid OID used by 0.2.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.
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
- objects (`PublicKey`, `SecretKey`) take a copy of the bytes passed into
88
- their constructor and expose `#wipe!` to zero only that internal copy
89
- any prior Ruby string the caller still holds is untouched. Ruby
90
- garbage collection and prior derived copies may still leave sensitive
91
- material elsewhere in process memory.
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 **3.0 or later**.
130
+ `pq_crypto` requires OpenSSL 3.0 or later.
96
131
 
97
132
  OpenSSL is used for:
98
133
 
99
- - `X25519` key generation and key agreement (`EVP_PKEY_*`)
100
- - `SHA3-256` (X-Wing combiner, via `EVP_sha3_256`)
101
- - `RAND_bytes` (production entropy source for `randombytes()`)
102
- - `CRYPTO_memcmp` (constant-time comparison used by `PQCrypto.ct_equals`)
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.
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
- `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.
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
- Native calls copy Ruby string inputs before releasing the GVL, so
121
- normal concurrent use does not rely on Ruby string storage remaining
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.
@@ -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
- mlkem_dir = File.join(pqclean_dir, "crypto_kem", "ml-kem-768", "clean")
133
- mldsa_dir = File.join(pqclean_dir, "crypto_sign", "ml-dsa-65", "clean")
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 = [mlkem_dir, mldsa_dir, common_dir, *keccak_config[: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
- mlkem_sources = Dir.glob(File.join(mlkem_dir, "*.c")).sort
142
- mldsa_sources = Dir.glob(File.join(mldsa_dir, "*.c")).sort
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
- ["pqclean_mlkem", mlkem_sources],
147
- ["pqclean_mldsa", mldsa_sources],
179
+ *mlkem_source_groups,
180
+ *mldsa_source_groups,
148
181
  ["pqclean_common", common_sources],
149
182
  keccak_config[:source_group]
150
183
  ]