pq_crypto 0.5.3 → 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: '0911b99fec80d68a6dff827829bbd77a28951d70c4a85f4fc68bc8c03bdffdda'
4
- data.tar.gz: fb7f9e92387316b6641b191dc1d1f5c469f5297d866301a21d44d4eb789185e6
3
+ metadata.gz: 11bb63e90651697a30709e9621678c7a53224facc67264a331291a8c57520880
4
+ data.tar.gz: 111c87fa0809924f0c2997fc0d9c2ea41c43e88044c2802d35370c01ed5f88ca
5
5
  SHA512:
6
- metadata.gz: 0a614e2b3a645332a3543062efd754421a4a002f08b7b2b71463ee2817f5008a14e65745c66d0c24b5b11a1ecb020f4fe9d479ff59676ddf770186d8d286e26a
7
- data.tar.gz: ef7bd96e717b4a99b74346dfd2c98c4787bc3b09b3a1199acfe01b0a22ea2f26cfd594499f3948241abf207dab9d5602f21aaa0a21c66742710637ce53f1a07f
6
+ metadata.gz: c95c38ec77fce342cb92febfbdbecdee5c35495323e7f153a2e263e04b89dc6cb13e8bd2874b8abb0b5e19e5b26cde13fb7f6b9d99ef107c5d6e3e41b20d819c
7
+ data.tar.gz: 1aa62a91a03203fcd6253e3175d1166b5bf2a3590889509517b8b5eaa57ff6fac49023b253f0859b45c7c2e0f4b28b849a657a5d089890b328da8a234ef7b5c9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
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
+
3
14
  ## [0.5.3] - 2026-05-08
4
15
 
5
16
  ### Compatibility
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
 
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
 
@@ -111,8 +111,13 @@ PQCrypto.supported_signatures
111
111
  PQCrypto.supported_hybrid_kems
112
112
 
113
113
  PQCrypto::KEM.generate(:ml_kem_768)
114
+ PQCrypto::KEM.secret_key_from_seed(:ml_kem_768, seed_64_bytes)
115
+
114
116
  PQCrypto::Signature.generate(:ml_dsa_65)
117
+ PQCrypto::Signature.secret_key_from_seed(:ml_dsa_65, seed_32_bytes)
118
+
115
119
  PQCrypto::HybridKEM.generate(:ml_kem_768_x25519_xwing)
120
+ PQCrypto::Key.from_pem(pem, passphrase: passphrase)
116
121
  ```
117
122
 
118
123
  ## More examples
@@ -121,9 +126,9 @@ Detailed usage examples live in [`GET_STARTED.md`](GET_STARTED.md):
121
126
 
122
127
  - generating keys
123
128
  - ML-KEM encapsulation / decapsulation
124
- - ML-DSA signing / verification
129
+ - ML-DSA signing / verification with optional FIPS 204 context
125
130
  - streaming ML-DSA for large files
126
- - SPKI and PKCS#8 serialization
131
+ - SPKI, PKCS#8, encrypted PKCS#8, and auto-dispatch key loading
127
132
  - `pqc_container_*` compatibility serialization
128
133
  - native backend / vendoring notes
129
- - secure wiping and practical safety notes
134
+ - seed-aware keys, secure wiping, and practical safety notes
@@ -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