pq_crypto 0.5.3 → 0.6.1
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/CHANGELOG.md +18 -0
- data/GET_STARTED.md +70 -9
- data/README.md +11 -6
- data/ext/pqcrypto/extconf.rb +2 -0
- data/ext/pqcrypto/pq_externalmu.c +23 -18
- data/ext/pqcrypto/pqcrypto_native_api.h +10 -0
- data/ext/pqcrypto/pqcrypto_ruby_secure.c +351 -48
- data/ext/pqcrypto/pqcrypto_secure.c +615 -84
- data/ext/pqcrypto/pqcrypto_secure.h +35 -10
- data/ext/pqcrypto/pqcrypto_version.h +1 -1
- data/lib/pq_crypto/hybrid_kem.rb +2 -1
- data/lib/pq_crypto/internal.rb +23 -0
- data/lib/pq_crypto/kem.rb +79 -44
- data/lib/pq_crypto/key.rb +90 -0
- data/lib/pq_crypto/pkcs8/der.rb +68 -0
- data/lib/pq_crypto/pkcs8/private_key_choice.rb +186 -0
- data/lib/pq_crypto/pkcs8.rb +61 -304
- data/lib/pq_crypto/serialization.rb +19 -29
- data/lib/pq_crypto/signature.rb +81 -51
- data/lib/pq_crypto/version.rb +1 -1
- data/lib/pq_crypto.rb +16 -4
- metadata +10 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 78b11d409c22bfd37c2afcf11cfdeb8bc7e825a1e4ebf8b0a94c655afbd59f78
|
|
4
|
+
data.tar.gz: 0206a99c5b364c812176c3d31629e5eb1490dcf694a2c8902b9537fd1ab16ebc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d79069163427ae03428e5ff8d01af39315cef796d498f59ca8e021c92f6a987b6a1a6d04c8e6826529a85df1af428f80086dc1f56cda74b0866e16aca27b8901
|
|
7
|
+
data.tar.gz: 552eb1abce00c25fc1313690c5df3a4a341b158caa3aa3ec2e37b604499c43c514fd2e0b82b245961acc4e73895dcbc1b7637112958660a91e0a32b8e3e91134
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.1] - 2026-05-14
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
|
|
7
|
+
- Moved PKCS#8 PEM handling, PrivateKeyInfo wrapping/unwrapping, and encrypted PKCS#8 encryption/decryption to native C/OpenSSL helpers.
|
|
8
|
+
- Removed Ruby `OpenSSL::ASN1` parsing/building from the PKCS#8 path while preserving the existing Ruby API.
|
|
9
|
+
|
|
10
|
+
## [0.6.0] - 2026-05-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- 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.
|
|
15
|
+
- 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.
|
|
16
|
+
|
|
17
|
+
### Security
|
|
18
|
+
|
|
19
|
+
- Tightened secret-key lifetime handling for PKCS#8 temporary buffers, key equality, and HybridKEM expanded-key wiping.
|
|
20
|
+
|
|
3
21
|
## [0.5.3] - 2026-05-08
|
|
4
22
|
|
|
5
23
|
### 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.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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,
|
|
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
|
|
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
|
data/ext/pqcrypto/extconf.rb
CHANGED
|
@@ -201,6 +201,8 @@ def configure_openssl!
|
|
|
201
201
|
abort "openssl/evp.h is required" unless have_header("openssl/evp.h")
|
|
202
202
|
abort "openssl/rand.h is required" unless have_header("openssl/rand.h")
|
|
203
203
|
abort "openssl/crypto.h is required" unless have_header("openssl/crypto.h")
|
|
204
|
+
abort "openssl/x509.h is required" unless have_header("openssl/x509.h")
|
|
205
|
+
abort "openssl/pkcs12.h is required" unless have_header("openssl/pkcs12.h")
|
|
204
206
|
|
|
205
207
|
version_check = <<~SRC
|
|
206
208
|
#include <openssl/opensslv.h>
|
|
@@ -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
|
-
|
|
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 =
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
72
|
-
|
|
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
|
-
|
|
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 !=
|
|
87
|
+
if (signature_len != expected_signature_len) {
|
|
82
88
|
return PQ_ERROR_VERIFY;
|
|
83
89
|
}
|
|
84
90
|
|
|
85
|
-
return
|
|
86
|
-
|
|
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
|