pq_crypto 0.6.6 → 0.6.7
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 +99 -0
- data/README.md +11 -0
- data/SECURITY.md +46 -0
- data/ext/pqcrypto/extconf.rb +4 -0
- data/ext/pqcrypto/pqcrypto_native_api.h +6 -0
- data/ext/pqcrypto/pqcrypto_ruby_secure.c +81 -0
- data/ext/pqcrypto/pqcrypto_secure.c +52 -0
- data/ext/pqcrypto/pqcrypto_secure.h +14 -1
- data/ext/pqcrypto/pqcrypto_version.h +1 -1
- data/lib/pq_crypto/internal.rb +10 -0
- data/lib/pq_crypto/kem.rb +47 -5
- data/lib/pq_crypto/key.rb +14 -8
- data/lib/pq_crypto/pkcs8.rb +13 -5
- data/lib/pq_crypto/signature.rb +34 -9
- data/lib/pq_crypto/spki.rb +4 -2
- data/lib/pq_crypto/version.rb +1 -1
- data/lib/pq_crypto.rb +9 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf8d3b17b9579ef551ef337aab16a9eb356846a36ff492577e2d728ec0e285e2
|
|
4
|
+
data.tar.gz: 26c3c3646d066617e37cb2cb1219e7ee5ca640488c84b480b6259461999b48e5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bd58150566a566af1c3083ef2c72538acafb6b15c5ebe954d0d7af35f96cdf41a5f1fe17c8d3c956e29b3c51bd89b29ec24245749efbca82389b1d2dc3c91ede
|
|
7
|
+
data.tar.gz: 5d974df3a99f9284da5c5cdfff25ba1808dc083063c12f97db6b803a34aa15687a0cda9565c8617edd95015c8d23139181d56d69d2483a3ac17b2239bec3cba3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,104 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.7] - 2026-08-10
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **DER parsers could raise exceptions outside the `PQCrypto::Error` hierarchy.**
|
|
8
|
+
`OpenSSL::ASN1.decode` raises more than `OpenSSL::ASN1::ASN1Error`, and
|
|
9
|
+
re-encoding a successfully decoded object can fail as well. Three shapes
|
|
10
|
+
escaped the previous handling in `PQCrypto::SPKI`:
|
|
11
|
+
|
|
12
|
+
- `"\x0A\x01\xFF"` (ENUMERATED) raised `OpenSSL::OpenSSLError`
|
|
13
|
+
- `"\x17\x00"` / `"\x18\x00"` (empty UTCTIME / GENERALIZEDTIME) raised `TypeError`
|
|
14
|
+
- `"\x10\x00"` (universal SEQUENCE tag without the constructed bit) decoded
|
|
15
|
+
cleanly but raised `TypeError` from the `to_der` round trip used for the
|
|
16
|
+
trailing-data check
|
|
17
|
+
|
|
18
|
+
`TypeError` does not descend from `OpenSSL::OpenSSLError`, so widening the
|
|
19
|
+
rescue to the OpenSSL base class alone was not sufficient. Both rescue sites
|
|
20
|
+
now catch `OpenSSL::OpenSSLError, TypeError`, and `SPKI.decode_der` is guarded
|
|
21
|
+
end to end rather than only around the decode call.
|
|
22
|
+
|
|
23
|
+
Impact: code written as `rescue PQCrypto::Error` around key loading could be
|
|
24
|
+
bypassed by malformed input. No memory-safety consequence. Covered by a
|
|
25
|
+
systematic tag sweep over 1,280 DER shapes × three entry points
|
|
26
|
+
(`Key.from_der`, `SPKI.decode_der`, `PKCS8.decode_der`).
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **FIPS 203 key validation for ML-KEM (boundary diagnostics).** mlkem-native
|
|
31
|
+
v2 exposes `check_pk` and `check_sk`, which this gem did not use. Both are
|
|
32
|
+
now wired in and run when a `PQCrypto::KEM::PublicKey` or `SecretKey` is
|
|
33
|
+
constructed:
|
|
34
|
+
|
|
35
|
+
- public keys get the Section 7.2 modulus check (coefficients in `[0,q-1]`)
|
|
36
|
+
- secret keys get the Section 7.3 hash check (`H(pk)` embedded in the sk)
|
|
37
|
+
|
|
38
|
+
Scope limits (intentional, matching FIPS 203):
|
|
39
|
+
|
|
40
|
+
- these checks are **diagnostics at import**, not a substitute for the
|
|
41
|
+
checks already performed inside encapsulation / decapsulation
|
|
42
|
+
- Section 7.3 does **not** detect a corrupted IND-CPA secret (`dk`) when
|
|
43
|
+
the embedded `H(pk)` is still intact — `#valid?` can still be true
|
|
44
|
+
- X-Wing public keys validate only the ML-KEM-768 half; the X25519 half is
|
|
45
|
+
unrestricted. X-Wing secret keys are 32-byte seeds and are not structure-
|
|
46
|
+
checked
|
|
47
|
+
|
|
48
|
+
`#valid?` is exposed on both key classes. Failures raise
|
|
49
|
+
`PQCrypto::InvalidKeyError`. Public-key internal buffers are frozen after
|
|
50
|
+
construction to block post-validation mutation; secret-key buffers stay
|
|
51
|
+
mutable so `#wipe!` can still zero them in place.
|
|
52
|
+
|
|
53
|
+
New C API: `pq_mlkem{,512,1024}_check_public_key` / `_check_secret_key`,
|
|
54
|
+
plus `PQ_ERROR_INVALID_PUBLIC_KEY` (-11) and `PQ_ERROR_INVALID_SECRET_KEY`
|
|
55
|
+
(-12). Upstream OOM (`-2`) is mapped to `PQ_ERROR_NOMEM` rather than
|
|
56
|
+
“invalid key”.
|
|
57
|
+
|
|
58
|
+
- **ML-DSA secret-key structure check on import.** Expanded ML-DSA secret
|
|
59
|
+
keys are validated via upstream `pk_from_sk` (coefficient norms, recomputed
|
|
60
|
+
`t0`, and `tr = H(pk)`). This matches the check mldsa-native documents for
|
|
61
|
+
importers and is enforced on `Signature::SecretKey` construction with
|
|
62
|
+
`#valid?` exposed. Public keys remain length-checked only (FIPS 204 has no
|
|
63
|
+
ML-KEM-style modulus check for the public key).
|
|
64
|
+
|
|
65
|
+
The check runs on import only. Keys produced by this process's own key
|
|
66
|
+
generation are valid by construction, and `pk_from_sk` re-derives the public
|
|
67
|
+
key, so validating them would roughly double the cost of every ML-DSA
|
|
68
|
+
keypair. Upstream also documents `pk_from_sk` as leaking secret-key validity
|
|
69
|
+
through its return value and timing.
|
|
70
|
+
|
|
71
|
+
New C API: `pq_mldsa{,44,87}_check_secret_key`.
|
|
72
|
+
|
|
73
|
+
- **`PQCRYPTO_KEYGEN_PCT=1` build option.** Enables the FIPS 140-3 pairwise
|
|
74
|
+
consistency test in both backends (`MLK_CONFIG_KEYGEN_PCT` /
|
|
75
|
+
`MLD_CONFIG_KEYGEN_PCT`): after generating a keypair it is exercised to catch
|
|
76
|
+
a faulty RNG or memory corruption at generation time. Upstream ships this
|
|
77
|
+
opt-in because it makes key generation noticeably more expensive, and it stays
|
|
78
|
+
opt-in here for the same reason. The selected setting is reported in the
|
|
79
|
+
build configuration banner.
|
|
80
|
+
|
|
81
|
+
- **`require_encrypted:` for PKCS#8 loading.** Supplying a passphrase states an
|
|
82
|
+
expectation that the key at rest is encrypted, but an unencrypted
|
|
83
|
+
PrivateKeyInfo was previously accepted and the passphrase left unused. An
|
|
84
|
+
encrypted key file replaced with a plaintext one therefore loaded without
|
|
85
|
+
complaint.
|
|
86
|
+
|
|
87
|
+
`require_encrypted: true` enforces that the input is an
|
|
88
|
+
EncryptedPrivateKeyInfo (DER content, not PEM label — relabeling a plaintext
|
|
89
|
+
key as `ENCRYPTED PRIVATE KEY` does not satisfy the policy). Available on
|
|
90
|
+
`PKCS8.decode_der` / `decode_pem`, `Key.from_der` / `from_pem`, and
|
|
91
|
+
`secret_key_from_pkcs8_der` / `_pem` on both `KEM` and `Signature`.
|
|
92
|
+
|
|
93
|
+
Limits (documented so the flag is not over-read):
|
|
94
|
+
|
|
95
|
+
- default is `false`; other loaders (`from_bytes`, `pqc_container_*`) are
|
|
96
|
+
unchanged
|
|
97
|
+
- the flag checks **format**, not passphrase strength (empty passphrase and
|
|
98
|
+
low PBKDF iterations still count as encrypted)
|
|
99
|
+
- the value must be a real boolean (`true`/`false`); strings like `"false"`
|
|
100
|
+
raise `ArgumentError` instead of being treated as truthy
|
|
101
|
+
|
|
3
102
|
## [0.6.6] - 2026-08-10
|
|
4
103
|
|
|
5
104
|
### Changed
|
data/README.md
CHANGED
|
@@ -101,6 +101,13 @@ can be tested explicitly with:
|
|
|
101
101
|
PQCRYPTO_NATIVE_ASM=1 bundle exec rake compile
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
+
The FIPS 140-3 pairwise consistency test is available but off by default,
|
|
105
|
+
because it makes key generation noticeably more expensive:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
PQCRYPTO_KEYGEN_PCT=1 bundle exec rake compile
|
|
109
|
+
```
|
|
110
|
+
|
|
104
111
|
## Security status
|
|
105
112
|
|
|
106
113
|
`pq_crypto` is experimental and not audited. Treat it as a low-level primitive
|
|
@@ -125,8 +132,12 @@ PQCrypto::Signature.secret_key_from_seed(:ml_dsa_65, seed_32_bytes)
|
|
|
125
132
|
|
|
126
133
|
PQCrypto::HybridKEM.generate(:ml_kem_768_x25519_xwing)
|
|
127
134
|
PQCrypto::Key.from_pem(pem, passphrase: passphrase)
|
|
135
|
+
PQCrypto::Key.from_pem(pem, passphrase: passphrase, require_encrypted: true)
|
|
136
|
+
keypair.public_key.valid? # ML-KEM structure check; ML-DSA public keys are length-only
|
|
137
|
+
keypair.secret_key.valid?
|
|
128
138
|
```
|
|
129
139
|
|
|
140
|
+
|
|
130
141
|
## More examples
|
|
131
142
|
|
|
132
143
|
Detailed usage examples live in [`GET_STARTED.md`](GET_STARTED.md):
|
data/SECURITY.md
CHANGED
|
@@ -127,6 +127,52 @@ into `mldsa-native` `signature_internal`; for an empty context this prefix is
|
|
|
127
127
|
Outside of test-only deterministic calls, production randomness delegates
|
|
128
128
|
directly to OpenSSL `RAND_bytes`.
|
|
129
129
|
|
|
130
|
+
## Key import validation (0.6.7+)
|
|
131
|
+
|
|
132
|
+
ML-KEM public/secret keys and ML-DSA secret keys are structure-checked when
|
|
133
|
+
the corresponding Ruby key objects are constructed:
|
|
134
|
+
|
|
135
|
+
| Object | Check | Source |
|
|
136
|
+
| --- | --- | --- |
|
|
137
|
+
| `KEM::PublicKey` | FIPS 203 §7.2 modulus check | `check_pk` |
|
|
138
|
+
| `KEM::SecretKey` | FIPS 203 §7.3 `H(pk)` check | `check_sk` |
|
|
139
|
+
| `Signature::SecretKey` | norms / `t0` / `tr` consistency | `pk_from_sk` |
|
|
140
|
+
| `Signature::PublicKey` | length only | — |
|
|
141
|
+
| X-Wing public key | ML-KEM-768 half only | `check_pk` |
|
|
142
|
+
| X-Wing secret key | none (32-byte seed) | — |
|
|
143
|
+
|
|
144
|
+
These are **import diagnostics**. Encapsulation, decapsulation, signing, and
|
|
145
|
+
verification still apply their own backend checks. A true `#valid?` result
|
|
146
|
+
does not prove full cryptographic integrity of every field (for example an
|
|
147
|
+
ML-KEM secret key with a corrupted IND-CPA `dk` but intact `H(pk)` still
|
|
148
|
+
passes §7.3).
|
|
149
|
+
|
|
150
|
+
The checks run on import. Keys produced by this process's own key generation
|
|
151
|
+
are valid by construction and are not re-checked, because the ML-DSA check
|
|
152
|
+
re-derives the public key and would roughly double the cost of every keypair.
|
|
153
|
+
|
|
154
|
+
The ML-DSA check (`pk_from_sk`) is documented upstream as leaking whether the
|
|
155
|
+
secret key is valid, through both its return value and its timing.
|
|
156
|
+
|
|
157
|
+
`require_encrypted:` applies only to the PKCS#8 branch. `PQCrypto::Key.from_der`
|
|
158
|
+
and `.from_pem` also accept SPKI public keys, and for those inputs the flag has
|
|
159
|
+
no effect and is silently ignored.
|
|
160
|
+
|
|
161
|
+
Public-key internal buffers are frozen after construction. Secret-key buffers
|
|
162
|
+
remain mutable so `#wipe!` can zero them.
|
|
163
|
+
|
|
164
|
+
### `require_encrypted:`
|
|
165
|
+
|
|
166
|
+
`require_encrypted: true` on PKCS#8 loaders rejects plaintext PrivateKeyInfo
|
|
167
|
+
and requires EncryptedPrivateKeyInfo (classified from DER, not PEM labels).
|
|
168
|
+
It does **not**:
|
|
169
|
+
|
|
170
|
+
- apply to `from_bytes` / `pqc_container_*` loaders
|
|
171
|
+
- enforce passphrase quality or PBKDF iteration count
|
|
172
|
+
- default on (default remains `false`)
|
|
173
|
+
|
|
174
|
+
The flag must be a boolean; non-boolean values raise `ArgumentError`.
|
|
175
|
+
|
|
130
176
|
## Memory wiping
|
|
131
177
|
|
|
132
178
|
`PQCrypto.secure_wipe` clears mutable Ruby strings in place. Ruby key objects
|
data/ext/pqcrypto/extconf.rb
CHANGED
|
@@ -84,6 +84,8 @@ def env_bool(name, default)
|
|
|
84
84
|
end
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
KEYGEN_PCT = env_bool("PQCRYPTO_KEYGEN_PCT", false)
|
|
88
|
+
|
|
87
89
|
NATIVE_ASM = env_bool("PQCRYPTO_NATIVE_ASM", native_asm_supported_by_default?)
|
|
88
90
|
NATIVE_ARITH = env_bool("PQCRYPTO_NATIVE_ARITH", NATIVE_ASM)
|
|
89
91
|
NATIVE_FIPS202 = env_bool("PQCRYPTO_NATIVE_FIPS202", NATIVE_ASM)
|
|
@@ -529,6 +531,7 @@ def native_flags(kind, level, shared:)
|
|
|
529
531
|
# Upstream has no guard against the stale define, so passing it would be a
|
|
530
532
|
# silently dead -D rather than a build error. Do not reintroduce it.
|
|
531
533
|
flags << (shared ? "-D#{prefix}_CONFIG_MULTILEVEL_WITH_SHARED" : "-D#{prefix}_CONFIG_MULTILEVEL_NO_SHARED")
|
|
534
|
+
flags << "-D#{prefix}_CONFIG_KEYGEN_PCT" if KEYGEN_PCT
|
|
532
535
|
flags << "-D#{prefix}_CONFIG_USE_NATIVE_BACKEND_ARITH" if NATIVE_ARITH
|
|
533
536
|
flags << "-D#{prefix}_CONFIG_USE_NATIVE_BACKEND_FIPS202" if NATIVE_FIPS202
|
|
534
537
|
flags.join(" ")
|
|
@@ -615,6 +618,7 @@ puts "Vendor ASM arch flags: #{VENDOR_ASM_ARCH_FLAGS.empty? ? '(none)' : VENDOR_
|
|
|
615
618
|
if x86_64_host? && (NATIVE_ARITH || NATIVE_FIPS202)
|
|
616
619
|
puts "x86_64 native backend: AVX2 build flags enabled"
|
|
617
620
|
end
|
|
621
|
+
puts "Keygen PCT (FIPS 140-3): #{KEYGEN_PCT ? 'enabled' : 'disabled'}"
|
|
618
622
|
puts "PQClean fallback: removed"
|
|
619
623
|
puts "Output: pqcrypto/pqcrypto_secure"
|
|
620
624
|
puts "===================================="
|
|
@@ -78,18 +78,24 @@ int pqcr_mlkem512_keypair_derand(uint8_t *pk, uint8_t *sk, const uint8_t *coins)
|
|
|
78
78
|
int pqcr_mlkem512_enc(uint8_t *ct, uint8_t *ss, const uint8_t *pk);
|
|
79
79
|
int pqcr_mlkem512_enc_derand(uint8_t *ct, uint8_t *ss, const uint8_t *pk, const uint8_t *coins);
|
|
80
80
|
int pqcr_mlkem512_dec(uint8_t *ss, const uint8_t *ct, const uint8_t *sk);
|
|
81
|
+
int pqcr_mlkem512_check_pk(const uint8_t *pk);
|
|
82
|
+
int pqcr_mlkem512_check_sk(const uint8_t *sk);
|
|
81
83
|
|
|
82
84
|
int pqcr_mlkem768_keypair(uint8_t *pk, uint8_t *sk);
|
|
83
85
|
int pqcr_mlkem768_keypair_derand(uint8_t *pk, uint8_t *sk, const uint8_t *coins);
|
|
84
86
|
int pqcr_mlkem768_enc(uint8_t *ct, uint8_t *ss, const uint8_t *pk);
|
|
85
87
|
int pqcr_mlkem768_enc_derand(uint8_t *ct, uint8_t *ss, const uint8_t *pk, const uint8_t *coins);
|
|
86
88
|
int pqcr_mlkem768_dec(uint8_t *ss, const uint8_t *ct, const uint8_t *sk);
|
|
89
|
+
int pqcr_mlkem768_check_pk(const uint8_t *pk);
|
|
90
|
+
int pqcr_mlkem768_check_sk(const uint8_t *sk);
|
|
87
91
|
|
|
88
92
|
int pqcr_mlkem1024_keypair(uint8_t *pk, uint8_t *sk);
|
|
89
93
|
int pqcr_mlkem1024_keypair_derand(uint8_t *pk, uint8_t *sk, const uint8_t *coins);
|
|
90
94
|
int pqcr_mlkem1024_enc(uint8_t *ct, uint8_t *ss, const uint8_t *pk);
|
|
91
95
|
int pqcr_mlkem1024_enc_derand(uint8_t *ct, uint8_t *ss, const uint8_t *pk, const uint8_t *coins);
|
|
92
96
|
int pqcr_mlkem1024_dec(uint8_t *ss, const uint8_t *ct, const uint8_t *sk);
|
|
97
|
+
int pqcr_mlkem1024_check_pk(const uint8_t *pk);
|
|
98
|
+
int pqcr_mlkem1024_check_sk(const uint8_t *sk);
|
|
93
99
|
|
|
94
100
|
void pqcr_mlkem_shake256(uint8_t *output, size_t outlen, const uint8_t *input, size_t inlen);
|
|
95
101
|
void pqcr_mlkem_sha3_256(uint8_t *output, const uint8_t *input, size_t inlen);
|
|
@@ -718,6 +718,12 @@ __attribute__((noreturn)) static void pq_raise_general_error(int err) {
|
|
|
718
718
|
case PQ_ERROR_OPENSSL:
|
|
719
719
|
rb_raise(ePQCryptoError, "OpenSSL error");
|
|
720
720
|
break;
|
|
721
|
+
case PQ_ERROR_INVALID_PUBLIC_KEY:
|
|
722
|
+
rb_raise(ePQCryptoError, "Invalid public key");
|
|
723
|
+
break;
|
|
724
|
+
case PQ_ERROR_INVALID_SECRET_KEY:
|
|
725
|
+
rb_raise(ePQCryptoError, "Invalid secret key");
|
|
726
|
+
break;
|
|
721
727
|
default:
|
|
722
728
|
rb_raise(ePQCryptoError, "Unknown error: %d", err);
|
|
723
729
|
break;
|
|
@@ -765,6 +771,28 @@ static VALUE pq_run_kem_keypair_from_seed(void *(*nogvl)(void *), VALUE seed, si
|
|
|
765
771
|
X(ml_kem_1024, MLKEM1024_PUBLICKEYBYTES, MLKEM1024_SECRETKEYBYTES, MLKEM1024_CIPHERTEXTBYTES, \
|
|
766
772
|
MLKEM1024_SHAREDSECRETBYTES)
|
|
767
773
|
|
|
774
|
+
static VALUE pq_run_key_check(int (*check)(const uint8_t *), VALUE key, size_t expected_len) {
|
|
775
|
+
uint8_t *buffer;
|
|
776
|
+
size_t copied_len = 0;
|
|
777
|
+
int result;
|
|
778
|
+
|
|
779
|
+
StringValue(key);
|
|
780
|
+
if ((size_t)RSTRING_LEN(key) != expected_len) {
|
|
781
|
+
return Qfalse;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
buffer = pq_copy_ruby_string(key, &copied_len);
|
|
785
|
+
result = check(buffer);
|
|
786
|
+
pq_secure_wipe(buffer, copied_len);
|
|
787
|
+
free(buffer);
|
|
788
|
+
|
|
789
|
+
if (result == PQ_ERROR_NOMEM) {
|
|
790
|
+
rb_raise(rb_eNoMemError, "Memory allocation failed");
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
return result == PQ_SUCCESS ? Qtrue : Qfalse;
|
|
794
|
+
}
|
|
795
|
+
|
|
768
796
|
#define PQ_DEFINE_RUBY_KEM(rb_name, pk_bytes, sk_bytes, ct_bytes, ss_bytes) \
|
|
769
797
|
static VALUE pqcrypto_##rb_name##_keypair(VALUE self) { \
|
|
770
798
|
(void)self; \
|
|
@@ -787,6 +815,41 @@ static VALUE pq_run_kem_keypair_from_seed(void *(*nogvl)(void *), VALUE seed, si
|
|
|
787
815
|
secret_key, sk_bytes, ss_bytes); \
|
|
788
816
|
}
|
|
789
817
|
|
|
818
|
+
#define PQ_DEFINE_RUBY_KEM_CHECK(rb_name, c_name) \
|
|
819
|
+
static VALUE pqcrypto_##rb_name##_check_public_key(VALUE self, VALUE key) { \
|
|
820
|
+
(void)self; \
|
|
821
|
+
return pq_run_key_check(pq_##c_name##_check_public_key, key, \
|
|
822
|
+
(size_t)c_name##_PUBLICKEYBYTES_VALUE); \
|
|
823
|
+
} \
|
|
824
|
+
static VALUE pqcrypto_##rb_name##_check_secret_key(VALUE self, VALUE key) { \
|
|
825
|
+
(void)self; \
|
|
826
|
+
return pq_run_key_check(pq_##c_name##_check_secret_key, key, \
|
|
827
|
+
(size_t)c_name##_SECRETKEYBYTES_VALUE); \
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
#define mlkem_PUBLICKEYBYTES_VALUE PQ_MLKEM_PUBLICKEYBYTES
|
|
831
|
+
#define mlkem_SECRETKEYBYTES_VALUE PQ_MLKEM_SECRETKEYBYTES
|
|
832
|
+
#define mlkem512_PUBLICKEYBYTES_VALUE MLKEM512_PUBLICKEYBYTES
|
|
833
|
+
#define mlkem512_SECRETKEYBYTES_VALUE MLKEM512_SECRETKEYBYTES
|
|
834
|
+
#define mlkem1024_PUBLICKEYBYTES_VALUE MLKEM1024_PUBLICKEYBYTES
|
|
835
|
+
#define mlkem1024_SECRETKEYBYTES_VALUE MLKEM1024_SECRETKEYBYTES
|
|
836
|
+
|
|
837
|
+
PQ_KEM_KEYPAIR_NOGVL_VARIANTS(PQ_DEFINE_RUBY_KEM_CHECK)
|
|
838
|
+
|
|
839
|
+
#undef PQ_DEFINE_RUBY_KEM_CHECK
|
|
840
|
+
|
|
841
|
+
#define PQ_DEFINE_RUBY_MLDSA_CHECK_SK(rb_name, c_name, sk_bytes) \
|
|
842
|
+
static VALUE pqcrypto_##rb_name##_check_secret_key(VALUE self, VALUE key) { \
|
|
843
|
+
(void)self; \
|
|
844
|
+
return pq_run_key_check(pq_##c_name##_check_secret_key, key, (size_t)(sk_bytes)); \
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
PQ_DEFINE_RUBY_MLDSA_CHECK_SK(ml_dsa, mldsa, MLDSA65_SECRETKEYBYTES)
|
|
848
|
+
PQ_DEFINE_RUBY_MLDSA_CHECK_SK(ml_dsa_44, mldsa44, MLDSA44_SECRETKEYBYTES)
|
|
849
|
+
PQ_DEFINE_RUBY_MLDSA_CHECK_SK(ml_dsa_87, mldsa87, MLDSA87_SECRETKEYBYTES)
|
|
850
|
+
|
|
851
|
+
#undef PQ_DEFINE_RUBY_MLDSA_CHECK_SK
|
|
852
|
+
|
|
790
853
|
PQ_RUBY_KEM_VARIANTS(PQ_DEFINE_RUBY_KEM)
|
|
791
854
|
|
|
792
855
|
#undef PQ_DEFINE_RUBY_KEM
|
|
@@ -1999,6 +2062,24 @@ void Init_pqcrypto_secure(void) {
|
|
|
1999
2062
|
pqcrypto_ml_kem_1024_encapsulate, 1);
|
|
2000
2063
|
rb_define_module_function(mPQCrypto, "ml_kem_1024_decapsulate",
|
|
2001
2064
|
pqcrypto_ml_kem_1024_decapsulate, 2);
|
|
2065
|
+
rb_define_module_function(mPQCrypto, "ml_kem_check_public_key",
|
|
2066
|
+
pqcrypto_ml_kem_check_public_key, 1);
|
|
2067
|
+
rb_define_module_function(mPQCrypto, "ml_kem_check_secret_key",
|
|
2068
|
+
pqcrypto_ml_kem_check_secret_key, 1);
|
|
2069
|
+
rb_define_module_function(mPQCrypto, "ml_kem_512_check_public_key",
|
|
2070
|
+
pqcrypto_ml_kem_512_check_public_key, 1);
|
|
2071
|
+
rb_define_module_function(mPQCrypto, "ml_kem_512_check_secret_key",
|
|
2072
|
+
pqcrypto_ml_kem_512_check_secret_key, 1);
|
|
2073
|
+
rb_define_module_function(mPQCrypto, "ml_kem_1024_check_public_key",
|
|
2074
|
+
pqcrypto_ml_kem_1024_check_public_key, 1);
|
|
2075
|
+
rb_define_module_function(mPQCrypto, "ml_kem_1024_check_secret_key",
|
|
2076
|
+
pqcrypto_ml_kem_1024_check_secret_key, 1);
|
|
2077
|
+
rb_define_module_function(mPQCrypto, "ml_dsa_check_secret_key",
|
|
2078
|
+
pqcrypto_ml_dsa_check_secret_key, 1);
|
|
2079
|
+
rb_define_module_function(mPQCrypto, "ml_dsa_44_check_secret_key",
|
|
2080
|
+
pqcrypto_ml_dsa_44_check_secret_key, 1);
|
|
2081
|
+
rb_define_module_function(mPQCrypto, "ml_dsa_87_check_secret_key",
|
|
2082
|
+
pqcrypto_ml_dsa_87_check_secret_key, 1);
|
|
2002
2083
|
rb_define_module_function(mPQCrypto, "hybrid_kem_keypair", pqcrypto_hybrid_kem_keypair, 0);
|
|
2003
2084
|
rb_define_module_function(mPQCrypto, "hybrid_kem_encapsulate", pqcrypto_hybrid_kem_encapsulate,
|
|
2004
2085
|
1);
|
|
@@ -265,12 +265,64 @@ cleanup:
|
|
|
265
265
|
return PQ_ERROR_BUFFER; \
|
|
266
266
|
} \
|
|
267
267
|
return native##_dec(ss, ct, sk) == 0 ? PQ_SUCCESS : PQ_ERROR_DECAPSULATE; \
|
|
268
|
+
} \
|
|
269
|
+
int pq_##prefix##_check_public_key(const uint8_t *pk) { \
|
|
270
|
+
int rc; \
|
|
271
|
+
if (!pk) { \
|
|
272
|
+
return PQ_ERROR_BUFFER; \
|
|
273
|
+
} \
|
|
274
|
+
rc = native##_check_pk(pk); \
|
|
275
|
+
if (rc == 0) { \
|
|
276
|
+
return PQ_SUCCESS; \
|
|
277
|
+
} \
|
|
278
|
+
if (rc == -2) { \
|
|
279
|
+
return PQ_ERROR_NOMEM; \
|
|
280
|
+
} \
|
|
281
|
+
return PQ_ERROR_INVALID_PUBLIC_KEY; \
|
|
282
|
+
} \
|
|
283
|
+
int pq_##prefix##_check_secret_key(const uint8_t *sk) { \
|
|
284
|
+
int rc; \
|
|
285
|
+
if (!sk) { \
|
|
286
|
+
return PQ_ERROR_BUFFER; \
|
|
287
|
+
} \
|
|
288
|
+
rc = native##_check_sk(sk); \
|
|
289
|
+
if (rc == 0) { \
|
|
290
|
+
return PQ_SUCCESS; \
|
|
291
|
+
} \
|
|
292
|
+
if (rc == -2) { \
|
|
293
|
+
return PQ_ERROR_NOMEM; \
|
|
294
|
+
} \
|
|
295
|
+
return PQ_ERROR_INVALID_SECRET_KEY; \
|
|
268
296
|
}
|
|
269
297
|
|
|
270
298
|
PQ_MLKEM_VARIANTS(PQ_DEFINE_MLKEM_SHIMS)
|
|
271
299
|
|
|
272
300
|
#undef PQ_DEFINE_MLKEM_SHIMS
|
|
273
301
|
|
|
302
|
+
#define PQ_DEFINE_MLDSA_CHECK_SK(prefix, native, pk_bytes) \
|
|
303
|
+
int pq_##prefix##_check_secret_key(const uint8_t *sk) { \
|
|
304
|
+
uint8_t pk[(pk_bytes)]; \
|
|
305
|
+
int rc; \
|
|
306
|
+
if (!sk) { \
|
|
307
|
+
return PQ_ERROR_BUFFER; \
|
|
308
|
+
} \
|
|
309
|
+
rc = native##_pk_from_sk(pk, sk); \
|
|
310
|
+
pq_secure_wipe(pk, sizeof(pk)); \
|
|
311
|
+
if (rc == 0) { \
|
|
312
|
+
return PQ_SUCCESS; \
|
|
313
|
+
} \
|
|
314
|
+
if (rc == -2) { \
|
|
315
|
+
return PQ_ERROR_NOMEM; \
|
|
316
|
+
} \
|
|
317
|
+
return PQ_ERROR_INVALID_SECRET_KEY; \
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
PQ_DEFINE_MLDSA_CHECK_SK(mldsa, pqcr_mldsa65, MLDSA65_PUBLICKEYBYTES)
|
|
321
|
+
PQ_DEFINE_MLDSA_CHECK_SK(mldsa44, pqcr_mldsa44, MLDSA44_PUBLICKEYBYTES)
|
|
322
|
+
PQ_DEFINE_MLDSA_CHECK_SK(mldsa87, pqcr_mldsa87, MLDSA87_PUBLICKEYBYTES)
|
|
323
|
+
|
|
324
|
+
#undef PQ_DEFINE_MLDSA_CHECK_SK
|
|
325
|
+
|
|
274
326
|
static int pq_testing_mlkem_keypair_from_seed_with(uint8_t *public_key, uint8_t *secret_key,
|
|
275
327
|
const uint8_t *seed, size_t seed_len,
|
|
276
328
|
int (*keypair_derand)(uint8_t *, uint8_t *,
|
|
@@ -29,7 +29,9 @@ typedef enum {
|
|
|
29
29
|
PQ_ERROR_RANDOM = -7,
|
|
30
30
|
PQ_ERROR_BUFFER = -8,
|
|
31
31
|
PQ_ERROR_NOMEM = -9,
|
|
32
|
-
PQ_ERROR_OPENSSL = -10
|
|
32
|
+
PQ_ERROR_OPENSSL = -10,
|
|
33
|
+
PQ_ERROR_INVALID_PUBLIC_KEY = -11,
|
|
34
|
+
PQ_ERROR_INVALID_SECRET_KEY = -12
|
|
33
35
|
} pq_error_t;
|
|
34
36
|
|
|
35
37
|
typedef struct {
|
|
@@ -83,6 +85,17 @@ int pq_mlkem512_decapsulate(uint8_t *shared_secret, const uint8_t *ciphertext,
|
|
|
83
85
|
int pq_mlkem1024_decapsulate(uint8_t *shared_secret, const uint8_t *ciphertext,
|
|
84
86
|
const uint8_t *secret_key);
|
|
85
87
|
|
|
88
|
+
int pq_mlkem_check_public_key(const uint8_t *public_key);
|
|
89
|
+
int pq_mlkem_check_secret_key(const uint8_t *secret_key);
|
|
90
|
+
int pq_mlkem512_check_public_key(const uint8_t *public_key);
|
|
91
|
+
int pq_mlkem512_check_secret_key(const uint8_t *secret_key);
|
|
92
|
+
int pq_mlkem1024_check_public_key(const uint8_t *public_key);
|
|
93
|
+
int pq_mlkem1024_check_secret_key(const uint8_t *secret_key);
|
|
94
|
+
|
|
95
|
+
int pq_mldsa_check_secret_key(const uint8_t *secret_key);
|
|
96
|
+
int pq_mldsa44_check_secret_key(const uint8_t *secret_key);
|
|
97
|
+
int pq_mldsa87_check_secret_key(const uint8_t *secret_key);
|
|
98
|
+
|
|
86
99
|
int pq_sign_keypair(uint8_t *public_key, uint8_t *secret_key);
|
|
87
100
|
int pq_mldsa44_sign_keypair(uint8_t *public_key, uint8_t *secret_key);
|
|
88
101
|
int pq_mldsa87_sign_keypair(uint8_t *public_key, uint8_t *secret_key);
|
data/lib/pq_crypto/internal.rb
CHANGED
|
@@ -8,6 +8,16 @@ module PQCrypto
|
|
|
8
8
|
String(value).b
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
def frozen_binary_string(value)
|
|
12
|
+
binary_string(value).freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def strict_boolean!(value, name:)
|
|
16
|
+
return value if value.equal?(true) || value.equal?(false)
|
|
17
|
+
|
|
18
|
+
raise ArgumentError, "#{name} must be true or false (got #{value.inspect})"
|
|
19
|
+
end
|
|
20
|
+
|
|
11
21
|
def safe_wipe(value)
|
|
12
22
|
return unless value.is_a?(String) && !value.frozen?
|
|
13
23
|
|
data/lib/pq_crypto/kem.rb
CHANGED
|
@@ -14,18 +14,24 @@ module PQCrypto
|
|
|
14
14
|
keypair_from_seed: :native_ml_kem_512_keypair_from_seed,
|
|
15
15
|
encapsulate: :native_ml_kem_512_encapsulate,
|
|
16
16
|
decapsulate: :native_ml_kem_512_decapsulate,
|
|
17
|
+
check_public_key: :native_ml_kem_512_check_public_key,
|
|
18
|
+
check_secret_key: :native_ml_kem_512_check_secret_key,
|
|
17
19
|
}.freeze,
|
|
18
20
|
ml_kem_768: {
|
|
19
21
|
keypair: :native_ml_kem_keypair,
|
|
20
22
|
keypair_from_seed: :native_ml_kem_keypair_from_seed,
|
|
21
23
|
encapsulate: :native_ml_kem_encapsulate,
|
|
22
24
|
decapsulate: :native_ml_kem_decapsulate,
|
|
25
|
+
check_public_key: :native_ml_kem_check_public_key,
|
|
26
|
+
check_secret_key: :native_ml_kem_check_secret_key,
|
|
23
27
|
}.freeze,
|
|
24
28
|
ml_kem_1024: {
|
|
25
29
|
keypair: :native_ml_kem_1024_keypair,
|
|
26
30
|
keypair_from_seed: :native_ml_kem_1024_keypair_from_seed,
|
|
27
31
|
encapsulate: :native_ml_kem_1024_encapsulate,
|
|
28
32
|
decapsulate: :native_ml_kem_1024_decapsulate,
|
|
33
|
+
check_public_key: :native_ml_kem_1024_check_public_key,
|
|
34
|
+
check_secret_key: :native_ml_kem_1024_check_secret_key,
|
|
29
35
|
}.freeze,
|
|
30
36
|
}.freeze
|
|
31
37
|
|
|
@@ -68,12 +74,18 @@ module PQCrypto
|
|
|
68
74
|
SecretKey.new(resolve_algorithm!(resolved_algorithm), bytes)
|
|
69
75
|
end
|
|
70
76
|
|
|
71
|
-
def secret_key_from_pkcs8_der(der, passphrase: nil)
|
|
72
|
-
|
|
77
|
+
def secret_key_from_pkcs8_der(der, passphrase: nil, require_encrypted: false)
|
|
78
|
+
require_encrypted = Internal.strict_boolean!(require_encrypted, name: "require_encrypted")
|
|
79
|
+
secret_key_from_decoded_pkcs8(
|
|
80
|
+
*PKCS8.decode_der(der, passphrase: passphrase, require_encrypted: require_encrypted)
|
|
81
|
+
)
|
|
73
82
|
end
|
|
74
83
|
|
|
75
|
-
def secret_key_from_pkcs8_pem(pem, passphrase: nil)
|
|
76
|
-
|
|
84
|
+
def secret_key_from_pkcs8_pem(pem, passphrase: nil, require_encrypted: false)
|
|
85
|
+
require_encrypted = Internal.strict_boolean!(require_encrypted, name: "require_encrypted")
|
|
86
|
+
secret_key_from_decoded_pkcs8(
|
|
87
|
+
*PKCS8.decode_pem(pem, passphrase: passphrase, require_encrypted: require_encrypted)
|
|
88
|
+
)
|
|
77
89
|
end
|
|
78
90
|
|
|
79
91
|
def public_key_from_spki_der(der, algorithm: nil)
|
|
@@ -121,6 +133,10 @@ module PQCrypto
|
|
|
121
133
|
end
|
|
122
134
|
end
|
|
123
135
|
|
|
136
|
+
def checkable?(algorithm)
|
|
137
|
+
NATIVE_DISPATCH.key?(algorithm) && NATIVE_DISPATCH.fetch(algorithm).key?(:check_public_key)
|
|
138
|
+
end
|
|
139
|
+
|
|
124
140
|
def native_method_for(algorithm, operation)
|
|
125
141
|
NATIVE_DISPATCH.fetch(resolve_algorithm!(algorithm)).fetch(operation)
|
|
126
142
|
end
|
|
@@ -158,8 +174,19 @@ module PQCrypto
|
|
|
158
174
|
|
|
159
175
|
def initialize(algorithm, bytes)
|
|
160
176
|
@algorithm = algorithm
|
|
161
|
-
@bytes = Internal.
|
|
177
|
+
@bytes = Internal.frozen_binary_string(bytes)
|
|
162
178
|
validate_length!
|
|
179
|
+
validate_structure!
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def valid?
|
|
183
|
+
if @algorithm == :ml_kem_768_x25519_xwing
|
|
184
|
+
mlkem_half = @bytes.byteslice(0, KEM.details(:ml_kem_768).fetch(:public_key_bytes))
|
|
185
|
+
return PQCrypto.__send__(:native_ml_kem_check_public_key, mlkem_half)
|
|
186
|
+
end
|
|
187
|
+
return true unless KEM.send(:checkable?, @algorithm)
|
|
188
|
+
|
|
189
|
+
PQCrypto.__send__(KEM.send(:native_method_for, @algorithm, :check_public_key), @bytes)
|
|
163
190
|
end
|
|
164
191
|
|
|
165
192
|
def to_bytes
|
|
@@ -219,6 +246,10 @@ module PQCrypto
|
|
|
219
246
|
expected = KEM.details(@algorithm).fetch(:public_key_bytes)
|
|
220
247
|
raise InvalidKeyError, "Invalid KEM public key length" unless @bytes.bytesize == expected
|
|
221
248
|
end
|
|
249
|
+
|
|
250
|
+
def validate_structure!
|
|
251
|
+
raise InvalidKeyError, "Invalid #{@algorithm} public key: FIPS 203 modulus check failed" unless valid?
|
|
252
|
+
end
|
|
222
253
|
end
|
|
223
254
|
|
|
224
255
|
class SecretKey
|
|
@@ -230,6 +261,13 @@ module PQCrypto
|
|
|
230
261
|
@seed = seed.nil? ? nil : Internal.binary_string(seed)
|
|
231
262
|
validate_length!
|
|
232
263
|
validate_seed_length! if @seed
|
|
264
|
+
validate_structure!
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def valid?
|
|
268
|
+
return true unless KEM.send(:checkable?, @algorithm)
|
|
269
|
+
|
|
270
|
+
PQCrypto.__send__(KEM.send(:native_method_for, @algorithm, :check_secret_key), @bytes)
|
|
233
271
|
end
|
|
234
272
|
|
|
235
273
|
def self.from_seed(algorithm, seed)
|
|
@@ -298,6 +336,10 @@ module PQCrypto
|
|
|
298
336
|
raise InvalidKeyError, "Invalid KEM secret key length" unless @bytes.bytesize == expected
|
|
299
337
|
end
|
|
300
338
|
|
|
339
|
+
def validate_structure!
|
|
340
|
+
raise InvalidKeyError, "Invalid #{@algorithm} secret key: FIPS 203 hash check failed" unless valid?
|
|
341
|
+
end
|
|
342
|
+
|
|
301
343
|
def pkcs8_material(format)
|
|
302
344
|
case format
|
|
303
345
|
when :expanded
|
data/lib/pq_crypto/key.rb
CHANGED
|
@@ -17,22 +17,24 @@ module PQCrypto
|
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def from_pem(pem, passphrase: nil)
|
|
20
|
+
def from_pem(pem, passphrase: nil, require_encrypted: false)
|
|
21
|
+
require_encrypted = Internal.strict_boolean!(require_encrypted, name: "require_encrypted")
|
|
21
22
|
text = String(pem)
|
|
22
23
|
if text.include?(SPKI::PEM_BEGIN)
|
|
23
24
|
public_key_from_spki_pem(text)
|
|
24
25
|
elsif text.include?(PKCS8::PEM_BEGIN) || text.include?(PKCS8::ENCRYPTED_PEM_BEGIN)
|
|
25
|
-
secret_key_from_pkcs8_pem(text, passphrase: passphrase)
|
|
26
|
+
secret_key_from_pkcs8_pem(text, passphrase: passphrase, require_encrypted: require_encrypted)
|
|
26
27
|
else
|
|
27
28
|
raise SerializationError, "Unsupported PEM label for PQCrypto::Key.from_pem"
|
|
28
29
|
end
|
|
29
30
|
end
|
|
30
31
|
|
|
31
|
-
def from_der(der, passphrase: nil)
|
|
32
|
+
def from_der(der, passphrase: nil, require_encrypted: false)
|
|
33
|
+
require_encrypted = Internal.strict_boolean!(require_encrypted, name: "require_encrypted")
|
|
32
34
|
public_key_from_spki_der(der)
|
|
33
35
|
rescue SerializationError => spki_error
|
|
34
36
|
begin
|
|
35
|
-
secret_key_from_pkcs8_der(der, passphrase: passphrase)
|
|
37
|
+
secret_key_from_pkcs8_der(der, passphrase: passphrase, require_encrypted: require_encrypted)
|
|
36
38
|
rescue SerializationError => pkcs8_error
|
|
37
39
|
raise SerializationError,
|
|
38
40
|
"Unable to decode DER as SPKI or PKCS#8 (SPKI: #{spki_error.message}; PKCS#8: #{pkcs8_error.message})"
|
|
@@ -56,12 +58,16 @@ module PQCrypto
|
|
|
56
58
|
public_key_from_algorithm_and_bytes(algorithm, bytes)
|
|
57
59
|
end
|
|
58
60
|
|
|
59
|
-
def secret_key_from_pkcs8_pem(pem, passphrase: nil)
|
|
60
|
-
secret_key_from_decoded_pkcs8(
|
|
61
|
+
def secret_key_from_pkcs8_pem(pem, passphrase: nil, require_encrypted: false)
|
|
62
|
+
secret_key_from_decoded_pkcs8(
|
|
63
|
+
*PKCS8.decode_pem(pem, passphrase: passphrase, require_encrypted: require_encrypted)
|
|
64
|
+
)
|
|
61
65
|
end
|
|
62
66
|
|
|
63
|
-
def secret_key_from_pkcs8_der(der, passphrase: nil)
|
|
64
|
-
secret_key_from_decoded_pkcs8(
|
|
67
|
+
def secret_key_from_pkcs8_der(der, passphrase: nil, require_encrypted: false)
|
|
68
|
+
secret_key_from_decoded_pkcs8(
|
|
69
|
+
*PKCS8.decode_der(der, passphrase: passphrase, require_encrypted: require_encrypted)
|
|
70
|
+
)
|
|
65
71
|
end
|
|
66
72
|
|
|
67
73
|
def secret_key_from_decoded_pkcs8(algorithm, format, material)
|
data/lib/pq_crypto/pkcs8.rb
CHANGED
|
@@ -70,10 +70,16 @@ module PQCrypto
|
|
|
70
70
|
der = encode_der(algorithm, secret_material, format: format, passphrase: passphrase, iterations: iterations)
|
|
71
71
|
pkcs8_native { PQCrypto.__send__(:native_pkcs8_der_to_pem, der, !passphrase.nil?) }
|
|
72
72
|
end
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
def decode_der(der, passphrase: nil, require_encrypted: false)
|
|
74
|
+
require_encrypted = Internal.strict_boolean!(require_encrypted, name: "require_encrypted")
|
|
75
75
|
input = Internal.binary_string(der)
|
|
76
|
-
|
|
76
|
+
encrypted = encrypted_der?(input)
|
|
77
|
+
|
|
78
|
+
if require_encrypted && !encrypted
|
|
79
|
+
raise SerializationError, "PKCS#8 input is not encrypted, but require_encrypted: true was requested"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
return decode_encrypted_der(input, passphrase: passphrase) if encrypted
|
|
77
83
|
|
|
78
84
|
oid, choice_der = private_key_info_from_der(input)
|
|
79
85
|
algorithm = AlgorithmRegistry.by_standard_oid(oid)
|
|
@@ -83,15 +89,17 @@ module PQCrypto
|
|
|
83
89
|
PrivateKeyChoice.validate_secret_key_algorithm!(algorithm, entry)
|
|
84
90
|
PrivateKeyChoice.decode(algorithm, Internal.binary_string(choice_der))
|
|
85
91
|
rescue ArgumentError => e
|
|
92
|
+
raise if e.message.start_with?("require_encrypted")
|
|
86
93
|
raise SerializationError, e.message
|
|
87
94
|
ensure
|
|
88
95
|
Internal.safe_wipe(choice_der) if defined?(choice_der)
|
|
89
96
|
end
|
|
90
97
|
|
|
91
|
-
def decode_pem(pem, passphrase: nil)
|
|
98
|
+
def decode_pem(pem, passphrase: nil, require_encrypted: false)
|
|
99
|
+
require_encrypted = Internal.strict_boolean!(require_encrypted, name: "require_encrypted")
|
|
92
100
|
_encrypted, der = pkcs8_native { PQCrypto.__send__(:native_pkcs8_pem_to_der, String(pem)) }
|
|
93
101
|
begin
|
|
94
|
-
decode_der(der, passphrase: passphrase)
|
|
102
|
+
decode_der(der, passphrase: passphrase, require_encrypted: require_encrypted)
|
|
95
103
|
ensure
|
|
96
104
|
Internal.safe_wipe(der)
|
|
97
105
|
end
|
data/lib/pq_crypto/signature.rb
CHANGED
|
@@ -14,18 +14,21 @@ module PQCrypto
|
|
|
14
14
|
sign: :native_ml_dsa_44_sign,
|
|
15
15
|
verify: :native_ml_dsa_44_verify,
|
|
16
16
|
keypair_from_seed: :native_ml_dsa_44_keypair_from_seed,
|
|
17
|
+
check_secret_key: :native_ml_dsa_44_check_secret_key,
|
|
17
18
|
}.freeze,
|
|
18
19
|
ml_dsa_65: {
|
|
19
20
|
keypair: :native_sign_keypair,
|
|
20
21
|
sign: :native_sign,
|
|
21
22
|
verify: :native_verify,
|
|
22
23
|
keypair_from_seed: :native_ml_dsa_keypair_from_seed,
|
|
24
|
+
check_secret_key: :native_ml_dsa_check_secret_key,
|
|
23
25
|
}.freeze,
|
|
24
26
|
ml_dsa_87: {
|
|
25
27
|
keypair: :native_ml_dsa_87_keypair,
|
|
26
28
|
sign: :native_ml_dsa_87_sign,
|
|
27
29
|
verify: :native_ml_dsa_87_verify,
|
|
28
30
|
keypair_from_seed: :native_ml_dsa_87_keypair_from_seed,
|
|
31
|
+
check_secret_key: :native_ml_dsa_87_check_secret_key,
|
|
29
32
|
}.freeze,
|
|
30
33
|
}.freeze
|
|
31
34
|
|
|
@@ -33,7 +36,8 @@ module PQCrypto
|
|
|
33
36
|
def generate(algorithm = CANONICAL_ALGORITHM)
|
|
34
37
|
algorithm = resolve_algorithm!(algorithm)
|
|
35
38
|
public_key, secret_key = PQCrypto.__send__(native_method_for(algorithm, :keypair))
|
|
36
|
-
Keypair.new(PublicKey.new(algorithm, public_key),
|
|
39
|
+
Keypair.new(PublicKey.new(algorithm, public_key),
|
|
40
|
+
SecretKey.new(algorithm, secret_key, freshly_generated: true))
|
|
37
41
|
end
|
|
38
42
|
|
|
39
43
|
def public_key_from_bytes(algorithm, bytes)
|
|
@@ -86,12 +90,18 @@ module PQCrypto
|
|
|
86
90
|
PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
|
|
87
91
|
end
|
|
88
92
|
|
|
89
|
-
def secret_key_from_pkcs8_der(der, passphrase: nil)
|
|
90
|
-
|
|
93
|
+
def secret_key_from_pkcs8_der(der, passphrase: nil, require_encrypted: false)
|
|
94
|
+
require_encrypted = Internal.strict_boolean!(require_encrypted, name: "require_encrypted")
|
|
95
|
+
secret_key_from_decoded_pkcs8(
|
|
96
|
+
*PKCS8.decode_der(der, passphrase: passphrase, require_encrypted: require_encrypted)
|
|
97
|
+
)
|
|
91
98
|
end
|
|
92
99
|
|
|
93
|
-
def secret_key_from_pkcs8_pem(pem, passphrase: nil)
|
|
94
|
-
|
|
100
|
+
def secret_key_from_pkcs8_pem(pem, passphrase: nil, require_encrypted: false)
|
|
101
|
+
require_encrypted = Internal.strict_boolean!(require_encrypted, name: "require_encrypted")
|
|
102
|
+
secret_key_from_decoded_pkcs8(
|
|
103
|
+
*PKCS8.decode_pem(pem, passphrase: passphrase, require_encrypted: require_encrypted)
|
|
104
|
+
)
|
|
95
105
|
end
|
|
96
106
|
|
|
97
107
|
def details(algorithm)
|
|
@@ -118,7 +128,7 @@ module PQCrypto
|
|
|
118
128
|
SecretKey.new(algorithm, material)
|
|
119
129
|
when :seed
|
|
120
130
|
_public_key, expanded = PQCrypto.__send__(native_method_for(algorithm, :keypair_from_seed), material)
|
|
121
|
-
SecretKey.new(algorithm, expanded, seed: material)
|
|
131
|
+
SecretKey.new(algorithm, expanded, seed: material, freshly_generated: true)
|
|
122
132
|
when :both
|
|
123
133
|
_seed, expanded = material
|
|
124
134
|
SecretKey.new(algorithm, expanded, seed: _seed)
|
|
@@ -133,6 +143,10 @@ module PQCrypto
|
|
|
133
143
|
NATIVE_DISPATCH.fetch(resolve_algorithm!(algorithm)).fetch(operation)
|
|
134
144
|
end
|
|
135
145
|
|
|
146
|
+
def secret_key_checkable?(algorithm)
|
|
147
|
+
NATIVE_DISPATCH.key?(algorithm) && NATIVE_DISPATCH.fetch(algorithm).key?(:check_secret_key)
|
|
148
|
+
end
|
|
149
|
+
|
|
136
150
|
def validate_algorithm_match!(expected_algorithm, actual_algorithm)
|
|
137
151
|
expected = resolve_algorithm!(expected_algorithm)
|
|
138
152
|
return if expected == actual_algorithm
|
|
@@ -264,7 +278,7 @@ module PQCrypto
|
|
|
264
278
|
|
|
265
279
|
def initialize(algorithm, bytes)
|
|
266
280
|
@algorithm = algorithm
|
|
267
|
-
@bytes = Internal.
|
|
281
|
+
@bytes = Internal.frozen_binary_string(bytes)
|
|
268
282
|
validate_length!
|
|
269
283
|
end
|
|
270
284
|
|
|
@@ -343,18 +357,25 @@ module PQCrypto
|
|
|
343
357
|
class SecretKey
|
|
344
358
|
attr_reader :algorithm
|
|
345
359
|
|
|
346
|
-
def initialize(algorithm, bytes, seed: nil)
|
|
360
|
+
def initialize(algorithm, bytes, seed: nil, freshly_generated: false)
|
|
347
361
|
@algorithm = algorithm
|
|
348
362
|
@bytes = Internal.binary_string(bytes)
|
|
349
363
|
@seed = seed.nil? ? nil : Internal.binary_string(seed)
|
|
350
364
|
validate_length!
|
|
351
365
|
validate_seed_length! if @seed
|
|
366
|
+
validate_structure! unless freshly_generated
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def valid?
|
|
370
|
+
return true unless Signature.send(:secret_key_checkable?, @algorithm)
|
|
371
|
+
|
|
372
|
+
PQCrypto.__send__(Signature.send(:native_method_for, @algorithm, :check_secret_key), @bytes)
|
|
352
373
|
end
|
|
353
374
|
|
|
354
375
|
def self.from_seed(algorithm, seed)
|
|
355
376
|
seed_bytes = Internal.binary_string(seed)
|
|
356
377
|
_public_key, expanded = PQCrypto.__send__(Signature.send(:native_method_for, algorithm, :keypair_from_seed), seed_bytes)
|
|
357
|
-
new(algorithm, expanded, seed: seed_bytes)
|
|
378
|
+
new(algorithm, expanded, seed: seed_bytes, freshly_generated: true)
|
|
358
379
|
rescue ArgumentError => e
|
|
359
380
|
raise InvalidKeyError, e.message
|
|
360
381
|
end
|
|
@@ -424,6 +445,10 @@ module PQCrypto
|
|
|
424
445
|
raise InvalidKeyError, "Invalid signature secret key length" unless @bytes.bytesize == expected
|
|
425
446
|
end
|
|
426
447
|
|
|
448
|
+
def validate_structure!
|
|
449
|
+
raise InvalidKeyError, "Invalid #{@algorithm} secret key: ML-DSA structure check failed" unless valid?
|
|
450
|
+
end
|
|
451
|
+
|
|
427
452
|
def pkcs8_material(format)
|
|
428
453
|
case format
|
|
429
454
|
when :expanded
|
data/lib/pq_crypto/spki.rb
CHANGED
|
@@ -26,7 +26,7 @@ module PQCrypto
|
|
|
26
26
|
]),
|
|
27
27
|
OpenSSL::ASN1::BitString.new(bytes),
|
|
28
28
|
]).to_der.b
|
|
29
|
-
rescue OpenSSL::
|
|
29
|
+
rescue OpenSSL::OpenSSLError, TypeError => e
|
|
30
30
|
raise SerializationError, e.message
|
|
31
31
|
end
|
|
32
32
|
|
|
@@ -63,6 +63,8 @@ module PQCrypto
|
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
[algorithm, bytes]
|
|
66
|
+
rescue OpenSSL::OpenSSLError, TypeError => e
|
|
67
|
+
raise SerializationError, e.message
|
|
66
68
|
end
|
|
67
69
|
|
|
68
70
|
def decode_pem(pem)
|
|
@@ -74,7 +76,7 @@ module PQCrypto
|
|
|
74
76
|
|
|
75
77
|
def decode_asn1(der)
|
|
76
78
|
OpenSSL::ASN1.decode(der)
|
|
77
|
-
rescue OpenSSL::
|
|
79
|
+
rescue OpenSSL::OpenSSLError, TypeError => e
|
|
78
80
|
raise SerializationError, e.message
|
|
79
81
|
end
|
|
80
82
|
|
data/lib/pq_crypto/version.rb
CHANGED
data/lib/pq_crypto.rb
CHANGED
|
@@ -65,6 +65,15 @@ module PQCrypto
|
|
|
65
65
|
ml_kem_1024_keypair_from_seed
|
|
66
66
|
ml_kem_1024_encapsulate
|
|
67
67
|
ml_kem_1024_decapsulate
|
|
68
|
+
ml_kem_check_public_key
|
|
69
|
+
ml_kem_check_secret_key
|
|
70
|
+
ml_kem_512_check_public_key
|
|
71
|
+
ml_kem_512_check_secret_key
|
|
72
|
+
ml_kem_1024_check_public_key
|
|
73
|
+
ml_kem_1024_check_secret_key
|
|
74
|
+
ml_dsa_check_secret_key
|
|
75
|
+
ml_dsa_44_check_secret_key
|
|
76
|
+
ml_dsa_87_check_secret_key
|
|
68
77
|
hybrid_kem_keypair
|
|
69
78
|
hybrid_kem_encapsulate
|
|
70
79
|
hybrid_kem_expand_secret_key
|