pq_crypto 0.5.2 → 0.6.0

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: 2274795113da1599b664af11b1393a86d68f1886f3536e0a65385b04b8ce03fe
4
- data.tar.gz: 386fe1093ba0b28a910195a40d7698e0cef91f9f57fc90b3a1aaac3eec4451a5
3
+ metadata.gz: 11bb63e90651697a30709e9621678c7a53224facc67264a331291a8c57520880
4
+ data.tar.gz: 111c87fa0809924f0c2997fc0d9c2ea41c43e88044c2802d35370c01ed5f88ca
5
5
  SHA512:
6
- metadata.gz: f9f73a9d575d9931020e1674f96adafe08816a1f7183b0b948194fcd2fb4da106afb854525ef544409b47bde34c987ac0eae6ce54440b7562f70faf5807446c3
7
- data.tar.gz: c5e54b45ab8461bd1edd2b3e4e14f0092267232bc3bf1436841488fa7a604fdf66056dfd160767ec2d47b0a4afe37cb660a46a54466677994a3c1d73269959d1
6
+ metadata.gz: c95c38ec77fce342cb92febfbdbecdee5c35495323e7f153a2e263e04b89dc6cb13e8bd2874b8abb0b5e19e5b26cde13fb7f6b9d99ef107c5d6e3e41b20d819c
7
+ data.tar.gz: 1aa62a91a03203fcd6253e3175d1166b5bf2a3590889509517b8b5eaa57ff6fac49023b253f0859b45c7c2e0f4b28b849a657a5d089890b328da8a234ef7b5c9
@@ -31,7 +31,7 @@ jobs:
31
31
  fail-fast: false
32
32
  matrix:
33
33
  os: [ubuntu-latest, macos-latest]
34
- ruby: ["3.4", "4.0"]
34
+ ruby: ["3.1", "3.4", "4.0"]
35
35
 
36
36
  steps:
37
37
  - name: Checkout
data/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.0] - 2026-05-14
4
+
5
+ ### Added
6
+
7
+ - Added seed-aware `SecretKey.from_seed` helpers for ML-KEM and ML-DSA, with PKCS#8 `:seed` / `:both` re-export when seed material is retained.
8
+ - Added one-shot ML-DSA `context:` support, ML-DSA-44/65/87 streaming coverage, encrypted PKCS#8, and `PQCrypto::Key.from_pem/from_der` auto-dispatch.
9
+
10
+ ### Security
11
+
12
+ - Tightened secret-key lifetime handling for PKCS#8 temporary buffers, key equality, and HybridKEM expanded-key wiping.
13
+
14
+ ## [0.5.3] - 2026-05-08
15
+
16
+ ### Compatibility
17
+
18
+ - Lowered the minimum supported Ruby version from `>= 3.4.0` to `>= 3.1`.
19
+ - Kept the Ruby 3.4+ optimized `rb_nogvl(..., RB_NOGVL_OFFLOAD_SAFE)` path intact.
20
+ - Added explicit native build probes for `ruby/thread.h`, `rb_thread_call_without_gvl`, and `rb_nogvl`.
21
+ - Ruby 3.1-3.3 now build the same selected `rb_nogvl` calls with a local `PQ_RB_NOGVL_OFFLOAD_SAFE` fallback of `0`, preserving ordinary no-GVL behavior without claiming scheduler offload guarantees.
22
+
23
+ ### CI
24
+
25
+ - Added Ruby 3.1-3.3 compatibility coverage as compile + smoke checks while keeping full test coverage on Ruby 3.4 and 4.0.
26
+ - Scoped the strict Async/Fiber Scheduler integration assertion to Ruby 3.4+ so compatibility runtimes do not claim `RB_NOGVL_OFFLOAD_SAFE` behavior.
27
+ - Pinned the test-only `async` dependency to the Ruby 3.1-compatible `2.21.x` line, which still contains the worker-pool support needed for the Ruby 3.4+ offload test.
28
+
29
+ ### Documentation
30
+
31
+ - Documented the Ruby 3.1+ support policy and the difference between compatibility no-GVL behavior and Ruby 3.4+ scheduler-aware offload.
32
+
3
33
  ## [0.5.2] - 2026-05-06
4
34
 
5
35
  ### Build
data/GET_STARTED.md CHANGED
@@ -115,6 +115,19 @@ public_key.verify!(message, signature)
115
115
  # returns true, or raises on mismatch
116
116
  ```
117
117
 
118
+ Use `context:` when the same key is shared across domains or protocols:
119
+
120
+ ```ruby
121
+ context = "orders-v1".b
122
+ signature = secret_key.sign(message, context: context)
123
+
124
+ public_key.verify(message, signature, context: context)
125
+ # => true
126
+
127
+ public_key.verify(message, signature, context: "other".b)
128
+ # => false
129
+ ```
130
+
118
131
  The same API shape applies to other supported ML-DSA parameter sets:
119
132
 
120
133
  ```ruby
@@ -125,7 +138,8 @@ PQCrypto::Signature.generate(:ml_dsa_87)
125
138
  ## 5. ML-DSA for large files
126
139
 
127
140
  For large inputs, use the streaming helpers so the whole message does not need
128
- to be materialized as one Ruby string.
141
+ to be materialized as one Ruby string. Streaming is available for
142
+ `:ml_dsa_44`, `:ml_dsa_65`, and `:ml_dsa_87`.
129
143
 
130
144
  ```ruby
131
145
  keypair = PQCrypto::Signature.generate(:ml_dsa_65)
@@ -246,10 +260,19 @@ der = keypair.secret_key.to_pkcs8_der
246
260
  imported = PQCrypto::KEM.secret_key_from_pkcs8_der(der)
247
261
  ```
248
262
 
249
- ML-KEM PKCS#8 supports `:seed`, `:expanded`, and `:both` formats. A generated
250
- `SecretKey` does not retain the original seed, so exporting `:seed` or `:both`
251
- from `SecretKey#to_pkcs8_*` is intentionally unavailable. If you explicitly
252
- have the seed material, use the low-level PKCS#8 encoder.
263
+ ML-KEM PKCS#8 supports `:seed`, `:expanded`, and `:both` formats. Generated
264
+ keys export `:expanded` by default. If you have 64-byte seed material, build a
265
+ seed-aware key to allow `:seed` and `:both` re-export:
266
+
267
+ ```ruby
268
+ require "securerandom"
269
+
270
+ seed = SecureRandom.random_bytes(PQCrypto::PKCS8::ML_KEM_SEED_BYTES)
271
+ secret_key = PQCrypto::KEM.secret_key_from_seed(:ml_kem_768, seed)
272
+
273
+ pem = secret_key.to_pkcs8_pem(format: :both)
274
+ imported = PQCrypto::KEM.secret_key_from_pkcs8_pem(pem)
275
+ ```
253
276
 
254
277
  ### ML-DSA PKCS#8
255
278
 
@@ -269,10 +292,48 @@ PQCrypto::PKCS8.allow_ml_dsa_seed_format = true
269
292
  imported = PQCrypto::Signature.secret_key_from_pkcs8_pem(pem)
270
293
  ```
271
294
 
272
- Seed/both export from an existing ML-DSA `SecretKey` is intentionally not
273
- available because the object does not retain the original seed material. When
274
- you explicitly have seed material, call `PQCrypto::PKCS8.encode_der` /
275
- `encode_pem` directly.
295
+ If you have 32-byte seed material, build a seed-aware key to allow `:seed` and
296
+ `:both` re-export:
297
+
298
+ ```ruby
299
+ require "securerandom"
300
+
301
+ PQCrypto::PKCS8.allow_ml_dsa_seed_format = true
302
+
303
+ seed = SecureRandom.random_bytes(PQCrypto::PKCS8::ML_DSA_SEED_BYTES)
304
+ secret_key = PQCrypto::Signature.secret_key_from_seed(:ml_dsa_65, seed)
305
+
306
+ pem = secret_key.to_pkcs8_pem(format: :both)
307
+ imported = PQCrypto::Signature.secret_key_from_pkcs8_pem(pem)
308
+ ```
309
+
310
+ ### Encrypted PKCS#8
311
+
312
+ Pass `passphrase:` to export encrypted private keys:
313
+
314
+ ```ruby
315
+ keypair = PQCrypto::Signature.generate(:ml_dsa_65)
316
+
317
+ pem = keypair.secret_key.to_pkcs8_pem(passphrase: "correct horse")
318
+ imported = PQCrypto::Signature.secret_key_from_pkcs8_pem(
319
+ pem,
320
+ passphrase: "correct horse",
321
+ )
322
+ ```
323
+
324
+ The same option is available for DER and ML-KEM secret keys.
325
+
326
+ ### Auto-dispatch key loading
327
+
328
+ Use `PQCrypto::Key` when you want the gem to detect SPKI vs PKCS#8 and the
329
+ algorithm family from the encoded key:
330
+
331
+ ```ruby
332
+ key = PQCrypto::Key.from_pem(pem, passphrase: "correct horse")
333
+ key = PQCrypto::Key.from_der(der)
334
+
335
+ keypair = PQCrypto::Key.generate(:ml_kem_768)
336
+ ```
276
337
 
277
338
  ## 9. pq_crypto-local container serialization
278
339
 
@@ -379,11 +440,15 @@ PQCRYPTO_NATIVE_ASM=1 bundle exec rake compile
379
440
 
380
441
  ## 14. Async / Fiber scheduler behavior
381
442
 
382
- On Ruby 3.4, signing and verification use Ruby's scheduler-aware
443
+ On Ruby 3.4 and later, signing and verification keep Ruby's scheduler-aware
383
444
  `rb_nogvl(..., RB_NOGVL_OFFLOAD_SAFE)` path automatically. With a scheduler
384
445
  that implements `blocking_operation_wait`, blocking native work can be moved
385
446
  off the event loop.
386
447
 
448
+ Ruby 3.1-3.3 are supported as a compatibility path: native operations still
449
+ release the GVL, but `RB_NOGVL_OFFLOAD_SAFE` is not available there, so the gem
450
+ does not claim Fiber Scheduler offload guarantees on those runtimes.
451
+
387
452
  ## 15. Test-only deterministic helpers
388
453
 
389
454
  `PQCrypto::Testing` exposes deterministic helpers for regression tests:
data/README.md CHANGED
@@ -35,10 +35,10 @@ bundle exec rake test
35
35
 
36
36
  | Area | Capabilities |
37
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. |
38
+ | ML-KEM | Key generation, seed-aware secret keys, encapsulation, decapsulation, raw key import/export, SPKI public keys, PKCS#8 private keys. |
39
+ | ML-DSA | Key generation, seed-aware secret keys, signing/verification with optional FIPS 204 context, streaming signing/verification for large inputs, raw key import/export, SPKI public keys, PKCS#8 private keys. |
40
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. |
41
+ | Serialization | Standard SPKI / PKCS#8 for NIST PQC keys, encrypted PKCS#8 private keys, auto-dispatch key loading, plus frozen `pqc_container_*` compatibility formats for the original algorithms. |
42
42
  | Safety helpers | Best-effort secret wiping and constant-time equality for key comparisons. |
43
43
  | Introspection | Supported algorithm lists, algorithm metadata, backend/version helpers. |
44
44
 
@@ -63,7 +63,9 @@ original algorithms:
63
63
 
64
64
  ## Requirements
65
65
 
66
- - Ruby 3.4 or later
66
+ - Ruby 3.1 or later
67
+ - Ruby 3.4+ keeps the optimized Fiber Scheduler offload path via `RB_NOGVL_OFFLOAD_SAFE`
68
+ - Ruby 3.1-3.3 use the compatibility no-GVL path without scheduler offload guarantees
67
69
  - a C toolchain with C11 support
68
70
  - OpenSSL 3.0 or later with SHA3-256 and SHAKE256 available
69
71
  - vendored minimal PQ Code Package native snapshot in `ext/pqcrypto/vendor`
@@ -109,8 +111,13 @@ PQCrypto.supported_signatures
109
111
  PQCrypto.supported_hybrid_kems
110
112
 
111
113
  PQCrypto::KEM.generate(:ml_kem_768)
114
+ PQCrypto::KEM.secret_key_from_seed(:ml_kem_768, seed_64_bytes)
115
+
112
116
  PQCrypto::Signature.generate(:ml_dsa_65)
117
+ PQCrypto::Signature.secret_key_from_seed(:ml_dsa_65, seed_32_bytes)
118
+
113
119
  PQCrypto::HybridKEM.generate(:ml_kem_768_x25519_xwing)
120
+ PQCrypto::Key.from_pem(pem, passphrase: passphrase)
114
121
  ```
115
122
 
116
123
  ## More examples
@@ -119,9 +126,9 @@ Detailed usage examples live in [`GET_STARTED.md`](GET_STARTED.md):
119
126
 
120
127
  - generating keys
121
128
  - ML-KEM encapsulation / decapsulation
122
- - ML-DSA signing / verification
129
+ - ML-DSA signing / verification with optional FIPS 204 context
123
130
  - streaming ML-DSA for large files
124
- - SPKI and PKCS#8 serialization
131
+ - SPKI, PKCS#8, encrypted PKCS#8, and auto-dispatch key loading
125
132
  - `pqc_container_*` compatibility serialization
126
133
  - native backend / vendoring notes
127
- - secure wiping and practical safety notes
134
+ - seed-aware keys, secure wiping, and practical safety notes
@@ -187,6 +187,12 @@ def find_vendor_dir
187
187
  candidates.find { |path| native_vendor_ready?(path) }
188
188
  end
189
189
 
190
+ def configure_ruby_c_api!
191
+ abort "ruby/thread.h is required" unless have_header("ruby/thread.h")
192
+ abort "rb_thread_call_without_gvl is required" unless have_func("rb_thread_call_without_gvl", "ruby/thread.h")
193
+ abort "rb_nogvl is required" unless have_func("rb_nogvl", "ruby/thread.h")
194
+ end
195
+
190
196
  def configure_openssl!
191
197
  configure_compiler_environment
192
198
 
@@ -349,6 +355,7 @@ vendor_dir = find_vendor_dir
349
355
 
350
356
  puts
351
357
  puts "=== PQCrypto build configuration ==="
358
+ configure_ruby_c_api!
352
359
  configure_openssl!
353
360
  native_config = native_vendor_config(vendor_dir)
354
361
  puts "OpenSSL: system"
@@ -34,57 +34,62 @@ cleanup:
34
34
  return ret;
35
35
  }
36
36
 
37
- int pq_mldsa_extract_tr_from_secret_key(uint8_t *tr_out, const uint8_t *secret_key) {
38
- uint8_t public_key[MLDSA_PUBLICKEYBYTES];
37
+ int pq_mldsa_extract_tr_from_secret_key(uint8_t *tr_out, const uint8_t *secret_key,
38
+ size_t public_key_len,
39
+ int (*pk_from_sk)(uint8_t *, const uint8_t *)) {
40
+ uint8_t public_key[MLDSA87_PUBLICKEYBYTES];
39
41
  int rc;
40
42
 
41
- if (tr_out == NULL || secret_key == NULL) {
43
+ if (tr_out == NULL || secret_key == NULL || pk_from_sk == NULL || public_key_len == 0 ||
44
+ public_key_len > sizeof(public_key)) {
42
45
  return PQ_ERROR_BUFFER;
43
46
  }
44
47
 
45
48
  memset(public_key, 0, sizeof(public_key));
46
- rc = pqcr_mldsa65_pk_from_sk(public_key, secret_key);
49
+ rc = pk_from_sk(public_key, secret_key);
47
50
  if (rc != 0) {
48
51
  pq_secure_wipe(public_key, sizeof(public_key));
49
52
  return PQ_ERROR_KEYPAIR;
50
53
  }
51
54
 
52
- rc = pq_shake256(tr_out, PQ_MLDSA_TRBYTES, public_key, sizeof(public_key));
55
+ rc = pq_shake256(tr_out, PQ_MLDSA_TRBYTES, public_key, public_key_len);
53
56
  pq_secure_wipe(public_key, sizeof(public_key));
54
57
  return rc;
55
58
  }
56
59
 
57
- int pq_mldsa_compute_tr_from_public_key(uint8_t *tr_out, const uint8_t *public_key) {
60
+ int pq_mldsa_compute_tr_from_public_key(uint8_t *tr_out, const uint8_t *public_key,
61
+ size_t public_key_len) {
58
62
  if (tr_out == NULL || public_key == NULL) {
59
63
  return PQ_ERROR_BUFFER;
60
64
  }
61
65
 
62
- return pq_shake256(tr_out, PQ_MLDSA_TRBYTES, public_key, MLDSA_PUBLICKEYBYTES);
66
+ return pq_shake256(tr_out, PQ_MLDSA_TRBYTES, public_key, public_key_len);
63
67
  }
64
68
 
65
69
  int pq_sign_mu(uint8_t *signature, size_t *signature_len, const uint8_t *mu,
66
- const uint8_t *secret_key) {
67
- if (signature == NULL || signature_len == NULL || mu == NULL || secret_key == NULL) {
70
+ const uint8_t *secret_key,
71
+ int (*signature_extmu)(uint8_t *, size_t *, const uint8_t *, const uint8_t *)) {
72
+ if (signature == NULL || signature_len == NULL || mu == NULL || secret_key == NULL ||
73
+ signature_extmu == NULL) {
68
74
  return PQ_ERROR_BUFFER;
69
75
  }
70
76
 
71
- return pqcr_mldsa65_signature_extmu(signature, signature_len, mu, secret_key) == 0
72
- ? PQ_SUCCESS
73
- : PQ_ERROR_SIGN;
77
+ return signature_extmu(signature, signature_len, mu, secret_key) == 0 ? PQ_SUCCESS
78
+ : PQ_ERROR_SIGN;
74
79
  }
75
80
 
76
81
  int pq_verify_mu(const uint8_t *signature, size_t signature_len, const uint8_t *mu,
77
- const uint8_t *public_key) {
78
- if (signature == NULL || mu == NULL || public_key == NULL) {
82
+ const uint8_t *public_key, size_t expected_signature_len,
83
+ int (*verify_extmu)(const uint8_t *, size_t, const uint8_t *, const uint8_t *)) {
84
+ if (signature == NULL || mu == NULL || public_key == NULL || verify_extmu == NULL) {
79
85
  return PQ_ERROR_BUFFER;
80
86
  }
81
- if (signature_len != MLDSA_BYTES) {
87
+ if (signature_len != expected_signature_len) {
82
88
  return PQ_ERROR_VERIFY;
83
89
  }
84
90
 
85
- return pqcr_mldsa65_verify_extmu(signature, signature_len, mu, public_key) == 0
86
- ? PQ_SUCCESS
87
- : PQ_ERROR_VERIFY;
91
+ return verify_extmu(signature, signature_len, mu, public_key) == 0 ? PQ_SUCCESS
92
+ : PQ_ERROR_VERIFY;
88
93
  }
89
94
 
90
95
  void *pq_mu_builder_new(void) {
@@ -95,6 +95,11 @@ int pqcr_mldsa44_verify(const uint8_t *sig, size_t siglen, const uint8_t *m, siz
95
95
  size_t pqcr_mldsa44_prepare_domain_separation_prefix(
96
96
  uint8_t prefix[MLDSA_DOMAIN_SEPARATION_MAX_BYTES], const uint8_t *ph, size_t phlen,
97
97
  const uint8_t *ctx, size_t ctxlen, int hashalg);
98
+ int pqcr_mldsa44_signature_extmu(uint8_t *sig, size_t *siglen, const uint8_t mu[MLDSA_CRHBYTES],
99
+ const uint8_t *sk);
100
+ int pqcr_mldsa44_verify_extmu(const uint8_t *sig, size_t siglen, const uint8_t mu[MLDSA_CRHBYTES],
101
+ const uint8_t *pk);
102
+ int pqcr_mldsa44_pk_from_sk(uint8_t *pk, const uint8_t *sk);
98
103
 
99
104
  int pqcr_mldsa65_keypair(uint8_t *pk, uint8_t *sk);
100
105
  int pqcr_mldsa65_keypair_internal(uint8_t *pk, uint8_t *sk, const uint8_t seed[MLDSA_SEEDBYTES]);
@@ -128,5 +133,10 @@ int pqcr_mldsa87_verify(const uint8_t *sig, size_t siglen, const uint8_t *m, siz
128
133
  size_t pqcr_mldsa87_prepare_domain_separation_prefix(
129
134
  uint8_t prefix[MLDSA_DOMAIN_SEPARATION_MAX_BYTES], const uint8_t *ph, size_t phlen,
130
135
  const uint8_t *ctx, size_t ctxlen, int hashalg);
136
+ int pqcr_mldsa87_signature_extmu(uint8_t *sig, size_t *siglen, const uint8_t mu[MLDSA_CRHBYTES],
137
+ const uint8_t *sk);
138
+ int pqcr_mldsa87_verify_extmu(const uint8_t *sig, size_t siglen, const uint8_t mu[MLDSA_CRHBYTES],
139
+ const uint8_t *pk);
140
+ int pqcr_mldsa87_pk_from_sk(uint8_t *pk, const uint8_t *sk);
131
141
 
132
142
  #endif