pq_crypto 0.6.6 → 0.6.8

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: 273b83c96db2314540118b933a9f70a22c954e59a6fb38377f7c6cd1f337d698
4
- data.tar.gz: 0a6f4b88a3aefde58049f41f582548aa10f2aa939e98a8b60791ff23c8a5be0e
3
+ metadata.gz: db8e5b65d14cc5dcc4950a2da14b781efcf4c8dd17774cffc076e9d5591509ca
4
+ data.tar.gz: '09cf8c3e990a416ab0f8f77fe2a2a6067fdd8a3f4d00607a4162e4259d6044bd'
5
5
  SHA512:
6
- metadata.gz: f4905490a34b75ae7548a896cbb379b135c7615dfba9dca564add76892fb6074a6e0a12220ca3158417ea3264c2dcb2dc432b0a49fa91f8429eaebeef966d895
7
- data.tar.gz: 854941b5a331956cba2aa91ef1777148f9a93713eec6ce746fe01475b126b69f409ad0dbcf631a1db5c7c09d127d55053d59585850bcf06a8047f8b2ae14adec
6
+ metadata.gz: 81a8509efb9d280100975a8e4a9a0a6aab70061b49bbc48a396de490723c316abe63442cfb041c68dbeefd9383fba647b198318082723ffb9bace9d0ffd12c78
7
+ data.tar.gz: 1055665295f23df22a53880fbf02a2e8d0c0cb33f1b65a43b1fe2f1f040da829bcaf4895440093addc432f15fb9c21792f28bf41e2949c90b9c93bc2784e5e93
data/CHANGELOG.md CHANGED
@@ -1,5 +1,113 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.8] - 2026-10-28
4
+
5
+ ### Changed
6
+
7
+ - **Refactored Ruby serialization and streaming paths.** Split SPKI and PKCS#8
8
+ decoding into focused validation steps, shared the seed/expanded-key codecs,
9
+ and consolidated ML-DSA streaming builder cleanup. SPKI algorithm matching
10
+ now uses one shared validator. Public APIs and encoded formats are unchanged.
11
+
12
+ ## [0.6.7] - 2026-08-10
13
+
14
+ ### Fixed
15
+
16
+ - **DER parsers could raise exceptions outside the `PQCrypto::Error` hierarchy.**
17
+ `OpenSSL::ASN1.decode` raises more than `OpenSSL::ASN1::ASN1Error`, and
18
+ re-encoding a successfully decoded object can fail as well. Three shapes
19
+ escaped the previous handling in `PQCrypto::SPKI`:
20
+
21
+ - `"\x0A\x01\xFF"` (ENUMERATED) raised `OpenSSL::OpenSSLError`
22
+ - `"\x17\x00"` / `"\x18\x00"` (empty UTCTIME / GENERALIZEDTIME) raised `TypeError`
23
+ - `"\x10\x00"` (universal SEQUENCE tag without the constructed bit) decoded
24
+ cleanly but raised `TypeError` from the `to_der` round trip used for the
25
+ trailing-data check
26
+
27
+ `TypeError` does not descend from `OpenSSL::OpenSSLError`, so widening the
28
+ rescue to the OpenSSL base class alone was not sufficient. Both rescue sites
29
+ now catch `OpenSSL::OpenSSLError, TypeError`, and `SPKI.decode_der` is guarded
30
+ end to end rather than only around the decode call.
31
+
32
+ Impact: code written as `rescue PQCrypto::Error` around key loading could be
33
+ bypassed by malformed input. No memory-safety consequence. Covered by a
34
+ systematic tag sweep over 1,280 DER shapes × three entry points
35
+ (`Key.from_der`, `SPKI.decode_der`, `PKCS8.decode_der`).
36
+
37
+ ### Added
38
+
39
+ - **FIPS 203 key validation for ML-KEM (boundary diagnostics).** mlkem-native
40
+ v2 exposes `check_pk` and `check_sk`, which this gem did not use. Both are
41
+ now wired in and run when a `PQCrypto::KEM::PublicKey` or `SecretKey` is
42
+ constructed:
43
+
44
+ - public keys get the Section 7.2 modulus check (coefficients in `[0,q-1]`)
45
+ - secret keys get the Section 7.3 hash check (`H(pk)` embedded in the sk)
46
+
47
+ Scope limits (intentional, matching FIPS 203):
48
+
49
+ - these checks are **diagnostics at import**, not a substitute for the
50
+ checks already performed inside encapsulation / decapsulation
51
+ - Section 7.3 does **not** detect a corrupted IND-CPA secret (`dk`) when
52
+ the embedded `H(pk)` is still intact — `#valid?` can still be true
53
+ - X-Wing public keys validate only the ML-KEM-768 half; the X25519 half is
54
+ unrestricted. X-Wing secret keys are 32-byte seeds and are not structure-
55
+ checked
56
+
57
+ `#valid?` is exposed on both key classes. Failures raise
58
+ `PQCrypto::InvalidKeyError`. Public-key internal buffers are frozen after
59
+ construction to block post-validation mutation; secret-key buffers stay
60
+ mutable so `#wipe!` can still zero them in place.
61
+
62
+ New C API: `pq_mlkem{,512,1024}_check_public_key` / `_check_secret_key`,
63
+ plus `PQ_ERROR_INVALID_PUBLIC_KEY` (-11) and `PQ_ERROR_INVALID_SECRET_KEY`
64
+ (-12). Upstream OOM (`-2`) is mapped to `PQ_ERROR_NOMEM` rather than
65
+ “invalid key”.
66
+
67
+ - **ML-DSA secret-key structure check on import.** Expanded ML-DSA secret
68
+ keys are validated via upstream `pk_from_sk` (coefficient norms, recomputed
69
+ `t0`, and `tr = H(pk)`). This matches the check mldsa-native documents for
70
+ importers and is enforced on `Signature::SecretKey` construction with
71
+ `#valid?` exposed. Public keys remain length-checked only (FIPS 204 has no
72
+ ML-KEM-style modulus check for the public key).
73
+
74
+ The check runs on import only. Keys produced by this process's own key
75
+ generation are valid by construction, and `pk_from_sk` re-derives the public
76
+ key, so validating them would roughly double the cost of every ML-DSA
77
+ keypair. Upstream also documents `pk_from_sk` as leaking secret-key validity
78
+ through its return value and timing.
79
+
80
+ New C API: `pq_mldsa{,44,87}_check_secret_key`.
81
+
82
+ - **`PQCRYPTO_KEYGEN_PCT=1` build option.** Enables the FIPS 140-3 pairwise
83
+ consistency test in both backends (`MLK_CONFIG_KEYGEN_PCT` /
84
+ `MLD_CONFIG_KEYGEN_PCT`): after generating a keypair it is exercised to catch
85
+ a faulty RNG or memory corruption at generation time. Upstream ships this
86
+ opt-in because it makes key generation noticeably more expensive, and it stays
87
+ opt-in here for the same reason. The selected setting is reported in the
88
+ build configuration banner.
89
+
90
+ - **`require_encrypted:` for PKCS#8 loading.** Supplying a passphrase states an
91
+ expectation that the key at rest is encrypted, but an unencrypted
92
+ PrivateKeyInfo was previously accepted and the passphrase left unused. An
93
+ encrypted key file replaced with a plaintext one therefore loaded without
94
+ complaint.
95
+
96
+ `require_encrypted: true` enforces that the input is an
97
+ EncryptedPrivateKeyInfo (DER content, not PEM label — relabeling a plaintext
98
+ key as `ENCRYPTED PRIVATE KEY` does not satisfy the policy). Available on
99
+ `PKCS8.decode_der` / `decode_pem`, `Key.from_der` / `from_pem`, and
100
+ `secret_key_from_pkcs8_der` / `_pem` on both `KEM` and `Signature`.
101
+
102
+ Limits (documented so the flag is not over-read):
103
+
104
+ - default is `false`; other loaders (`from_bytes`, `pqc_container_*`) are
105
+ unchanged
106
+ - the flag checks **format**, not passphrase strength (empty passphrase and
107
+ low PBKDF iterations still count as encrypted)
108
+ - the value must be a real boolean (`true`/`false`); strings like `"false"`
109
+ raise `ArgumentError` instead of being treated as truthy
110
+
3
111
  ## [0.6.6] - 2026-08-10
4
112
 
5
113
  ### 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
@@ -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);
@@ -8,6 +8,8 @@
8
8
  #if defined(__clang__) || defined(__GNUC__)
9
9
  #pragma GCC diagnostic pop
10
10
  #endif
11
+ #include <stdbool.h>
12
+ #include <stdatomic.h>
11
13
  #include <stdlib.h>
12
14
  #include <string.h>
13
15
 
@@ -718,6 +720,12 @@ __attribute__((noreturn)) static void pq_raise_general_error(int err) {
718
720
  case PQ_ERROR_OPENSSL:
719
721
  rb_raise(ePQCryptoError, "OpenSSL error");
720
722
  break;
723
+ case PQ_ERROR_INVALID_PUBLIC_KEY:
724
+ rb_raise(ePQCryptoError, "Invalid public key");
725
+ break;
726
+ case PQ_ERROR_INVALID_SECRET_KEY:
727
+ rb_raise(ePQCryptoError, "Invalid secret key");
728
+ break;
721
729
  default:
722
730
  rb_raise(ePQCryptoError, "Unknown error: %d", err);
723
731
  break;
@@ -765,6 +773,28 @@ static VALUE pq_run_kem_keypair_from_seed(void *(*nogvl)(void *), VALUE seed, si
765
773
  X(ml_kem_1024, MLKEM1024_PUBLICKEYBYTES, MLKEM1024_SECRETKEYBYTES, MLKEM1024_CIPHERTEXTBYTES, \
766
774
  MLKEM1024_SHAREDSECRETBYTES)
767
775
 
776
+ static VALUE pq_run_key_check(int (*check)(const uint8_t *), VALUE key, size_t expected_len) {
777
+ uint8_t *buffer;
778
+ size_t copied_len = 0;
779
+ int result;
780
+
781
+ StringValue(key);
782
+ if ((size_t)RSTRING_LEN(key) != expected_len) {
783
+ return Qfalse;
784
+ }
785
+
786
+ buffer = pq_copy_ruby_string(key, &copied_len);
787
+ result = check(buffer);
788
+ pq_secure_wipe(buffer, copied_len);
789
+ free(buffer);
790
+
791
+ if (result == PQ_ERROR_NOMEM) {
792
+ rb_raise(rb_eNoMemError, "Memory allocation failed");
793
+ }
794
+
795
+ return result == PQ_SUCCESS ? Qtrue : Qfalse;
796
+ }
797
+
768
798
  #define PQ_DEFINE_RUBY_KEM(rb_name, pk_bytes, sk_bytes, ct_bytes, ss_bytes) \
769
799
  static VALUE pqcrypto_##rb_name##_keypair(VALUE self) { \
770
800
  (void)self; \
@@ -787,6 +817,41 @@ static VALUE pq_run_kem_keypair_from_seed(void *(*nogvl)(void *), VALUE seed, si
787
817
  secret_key, sk_bytes, ss_bytes); \
788
818
  }
789
819
 
820
+ #define PQ_DEFINE_RUBY_KEM_CHECK(rb_name, c_name) \
821
+ static VALUE pqcrypto_##rb_name##_check_public_key(VALUE self, VALUE key) { \
822
+ (void)self; \
823
+ return pq_run_key_check(pq_##c_name##_check_public_key, key, \
824
+ (size_t)c_name##_PUBLICKEYBYTES_VALUE); \
825
+ } \
826
+ static VALUE pqcrypto_##rb_name##_check_secret_key(VALUE self, VALUE key) { \
827
+ (void)self; \
828
+ return pq_run_key_check(pq_##c_name##_check_secret_key, key, \
829
+ (size_t)c_name##_SECRETKEYBYTES_VALUE); \
830
+ }
831
+
832
+ #define mlkem_PUBLICKEYBYTES_VALUE PQ_MLKEM_PUBLICKEYBYTES
833
+ #define mlkem_SECRETKEYBYTES_VALUE PQ_MLKEM_SECRETKEYBYTES
834
+ #define mlkem512_PUBLICKEYBYTES_VALUE MLKEM512_PUBLICKEYBYTES
835
+ #define mlkem512_SECRETKEYBYTES_VALUE MLKEM512_SECRETKEYBYTES
836
+ #define mlkem1024_PUBLICKEYBYTES_VALUE MLKEM1024_PUBLICKEYBYTES
837
+ #define mlkem1024_SECRETKEYBYTES_VALUE MLKEM1024_SECRETKEYBYTES
838
+
839
+ PQ_KEM_KEYPAIR_NOGVL_VARIANTS(PQ_DEFINE_RUBY_KEM_CHECK)
840
+
841
+ #undef PQ_DEFINE_RUBY_KEM_CHECK
842
+
843
+ #define PQ_DEFINE_RUBY_MLDSA_CHECK_SK(rb_name, c_name, sk_bytes) \
844
+ static VALUE pqcrypto_##rb_name##_check_secret_key(VALUE self, VALUE key) { \
845
+ (void)self; \
846
+ return pq_run_key_check(pq_##c_name##_check_secret_key, key, (size_t)(sk_bytes)); \
847
+ }
848
+
849
+ PQ_DEFINE_RUBY_MLDSA_CHECK_SK(ml_dsa, mldsa, MLDSA65_SECRETKEYBYTES)
850
+ PQ_DEFINE_RUBY_MLDSA_CHECK_SK(ml_dsa_44, mldsa44, MLDSA44_SECRETKEYBYTES)
851
+ PQ_DEFINE_RUBY_MLDSA_CHECK_SK(ml_dsa_87, mldsa87, MLDSA87_SECRETKEYBYTES)
852
+
853
+ #undef PQ_DEFINE_RUBY_MLDSA_CHECK_SK
854
+
790
855
  PQ_RUBY_KEM_VARIANTS(PQ_DEFINE_RUBY_KEM)
791
856
 
792
857
  #undef PQ_DEFINE_RUBY_KEM
@@ -1397,6 +1462,7 @@ static VALUE pqcrypto_version(VALUE self) {
1397
1462
 
1398
1463
  typedef struct {
1399
1464
  void *builder;
1465
+ atomic_bool busy;
1400
1466
  } mu_builder_wrapper_t;
1401
1467
 
1402
1468
  typedef struct {
@@ -1463,15 +1529,29 @@ static const rb_data_type_t mu_builder_data_type = {
1463
1529
  .flags = RUBY_TYPED_FREE_IMMEDIATELY,
1464
1530
  };
1465
1531
 
1466
- static mu_builder_wrapper_t *mu_builder_unwrap(VALUE obj) {
1532
+ static mu_builder_wrapper_t *mu_builder_lock(VALUE obj) {
1467
1533
  mu_builder_wrapper_t *wrapper;
1468
1534
  TypedData_Get_Struct(obj, mu_builder_wrapper_t, &mu_builder_data_type, wrapper);
1469
- if (wrapper == NULL || wrapper->builder == NULL) {
1470
- rb_raise(ePQCryptoError, "mu builder used after release");
1535
+ if (wrapper == NULL) {
1536
+ rb_raise(ePQCryptoError, "mu builder is unavailable");
1537
+ }
1538
+ if (atomic_exchange_explicit(&wrapper->busy, true, memory_order_acquire)) {
1539
+ rb_raise(ePQCryptoError, "mu builder is busy");
1471
1540
  }
1472
1541
  return wrapper;
1473
1542
  }
1474
1543
 
1544
+ static void mu_builder_unlock(mu_builder_wrapper_t *wrapper) {
1545
+ atomic_store_explicit(&wrapper->busy, false, memory_order_release);
1546
+ }
1547
+
1548
+ static void mu_builder_require_active(mu_builder_wrapper_t *wrapper) {
1549
+ if (wrapper->builder == NULL) {
1550
+ mu_builder_unlock(wrapper);
1551
+ rb_raise(ePQCryptoError, "mu builder used after release");
1552
+ }
1553
+ }
1554
+
1475
1555
  static VALUE pqcrypto__native_mldsa_extract_tr(int argc, VALUE *argv, VALUE self) {
1476
1556
  (void)self;
1477
1557
  VALUE algorithm = ID2SYM(rb_intern("ml_dsa_65"));
@@ -1551,6 +1631,7 @@ static VALUE pqcrypto__native_mldsa_mu_builder_new(VALUE self, VALUE tr, VALUE c
1551
1631
  VALUE obj =
1552
1632
  TypedData_Make_Struct(rb_cObject, mu_builder_wrapper_t, &mu_builder_data_type, wrapper);
1553
1633
  wrapper->builder = builder;
1634
+ atomic_init(&wrapper->busy, false);
1554
1635
  return obj;
1555
1636
  }
1556
1637
 
@@ -1562,24 +1643,31 @@ static void *pq_mu_absorb_nogvl(void *arg) {
1562
1643
 
1563
1644
  static VALUE pqcrypto__native_mldsa_mu_builder_update(VALUE self, VALUE builder_obj, VALUE chunk) {
1564
1645
  (void)self;
1565
- mu_builder_wrapper_t *wrapper = mu_builder_unwrap(builder_obj);
1566
1646
  StringValue(chunk);
1567
1647
 
1568
1648
  size_t chunk_len = (size_t)RSTRING_LEN(chunk);
1649
+ mu_builder_wrapper_t *wrapper = mu_builder_lock(builder_obj);
1650
+ mu_builder_require_active(wrapper);
1569
1651
  if (chunk_len == 0) {
1652
+ mu_builder_unlock(wrapper);
1570
1653
  return Qnil;
1571
1654
  }
1572
1655
 
1573
1656
  if (chunk_len < PQ_MU_ABSORB_NOGVL_MIN_BYTES) {
1574
- int rc =
1575
- pq_mu_builder_absorb(wrapper->builder, (const uint8_t *)RSTRING_PTR(chunk), chunk_len);
1657
+ int rc = pq_mu_builder_absorb(wrapper->builder,
1658
+ (const uint8_t *)RSTRING_PTR(chunk), chunk_len);
1659
+ mu_builder_unlock(wrapper);
1576
1660
  if (rc != PQ_SUCCESS) {
1577
1661
  pq_raise_general_error(rc);
1578
1662
  }
1579
1663
  return Qnil;
1580
1664
  }
1581
1665
 
1582
- uint8_t *copy = pq_alloc_buffer(chunk_len);
1666
+ uint8_t *copy = malloc(chunk_len);
1667
+ if (copy == NULL) {
1668
+ mu_builder_unlock(wrapper);
1669
+ rb_raise(rb_eNoMemError, "Memory allocation failed");
1670
+ }
1583
1671
  memcpy(copy, RSTRING_PTR(chunk), chunk_len);
1584
1672
 
1585
1673
  mu_absorb_call_t call = {0};
@@ -1588,7 +1676,9 @@ static VALUE pqcrypto__native_mldsa_mu_builder_update(VALUE self, VALUE builder_
1588
1676
  call.chunk_len = chunk_len;
1589
1677
 
1590
1678
  rb_nogvl(pq_mu_absorb_nogvl, &call, NULL, NULL, PQ_RB_NOGVL_OFFLOAD_SAFE);
1679
+ RB_GC_GUARD(builder_obj);
1591
1680
  free(copy);
1681
+ mu_builder_unlock(wrapper);
1592
1682
 
1593
1683
  if (call.result != PQ_SUCCESS) {
1594
1684
  pq_raise_general_error(call.result);
@@ -1604,7 +1694,8 @@ static void *pq_mu_finalize_nogvl(void *arg) {
1604
1694
 
1605
1695
  static VALUE pqcrypto__native_mldsa_mu_builder_finalize(VALUE self, VALUE builder_obj) {
1606
1696
  (void)self;
1607
- mu_builder_wrapper_t *wrapper = mu_builder_unwrap(builder_obj);
1697
+ mu_builder_wrapper_t *wrapper = mu_builder_lock(builder_obj);
1698
+ mu_builder_require_active(wrapper);
1608
1699
 
1609
1700
  uint8_t mu[PQ_MLDSA_MUBYTES];
1610
1701
 
@@ -1613,8 +1704,10 @@ static VALUE pqcrypto__native_mldsa_mu_builder_finalize(VALUE self, VALUE builde
1613
1704
  call.mu_out = mu;
1614
1705
 
1615
1706
  rb_nogvl(pq_mu_finalize_nogvl, &call, NULL, NULL, PQ_RB_NOGVL_OFFLOAD_SAFE);
1707
+ RB_GC_GUARD(builder_obj);
1616
1708
 
1617
1709
  wrapper->builder = NULL;
1710
+ mu_builder_unlock(wrapper);
1618
1711
 
1619
1712
  if (call.result != PQ_SUCCESS) {
1620
1713
  pq_secure_wipe(mu, sizeof(mu));
@@ -1628,12 +1721,12 @@ static VALUE pqcrypto__native_mldsa_mu_builder_finalize(VALUE self, VALUE builde
1628
1721
 
1629
1722
  static VALUE pqcrypto__native_mldsa_mu_builder_release(VALUE self, VALUE builder_obj) {
1630
1723
  (void)self;
1631
- mu_builder_wrapper_t *wrapper;
1632
- TypedData_Get_Struct(builder_obj, mu_builder_wrapper_t, &mu_builder_data_type, wrapper);
1633
- if (wrapper != NULL && wrapper->builder != NULL) {
1724
+ mu_builder_wrapper_t *wrapper = mu_builder_lock(builder_obj);
1725
+ if (wrapper->builder != NULL) {
1634
1726
  pq_mu_builder_release(wrapper->builder);
1635
1727
  wrapper->builder = NULL;
1636
1728
  }
1729
+ mu_builder_unlock(wrapper);
1637
1730
  return Qnil;
1638
1731
  }
1639
1732
 
@@ -1999,6 +2092,24 @@ void Init_pqcrypto_secure(void) {
1999
2092
  pqcrypto_ml_kem_1024_encapsulate, 1);
2000
2093
  rb_define_module_function(mPQCrypto, "ml_kem_1024_decapsulate",
2001
2094
  pqcrypto_ml_kem_1024_decapsulate, 2);
2095
+ rb_define_module_function(mPQCrypto, "ml_kem_check_public_key",
2096
+ pqcrypto_ml_kem_check_public_key, 1);
2097
+ rb_define_module_function(mPQCrypto, "ml_kem_check_secret_key",
2098
+ pqcrypto_ml_kem_check_secret_key, 1);
2099
+ rb_define_module_function(mPQCrypto, "ml_kem_512_check_public_key",
2100
+ pqcrypto_ml_kem_512_check_public_key, 1);
2101
+ rb_define_module_function(mPQCrypto, "ml_kem_512_check_secret_key",
2102
+ pqcrypto_ml_kem_512_check_secret_key, 1);
2103
+ rb_define_module_function(mPQCrypto, "ml_kem_1024_check_public_key",
2104
+ pqcrypto_ml_kem_1024_check_public_key, 1);
2105
+ rb_define_module_function(mPQCrypto, "ml_kem_1024_check_secret_key",
2106
+ pqcrypto_ml_kem_1024_check_secret_key, 1);
2107
+ rb_define_module_function(mPQCrypto, "ml_dsa_check_secret_key",
2108
+ pqcrypto_ml_dsa_check_secret_key, 1);
2109
+ rb_define_module_function(mPQCrypto, "ml_dsa_44_check_secret_key",
2110
+ pqcrypto_ml_dsa_44_check_secret_key, 1);
2111
+ rb_define_module_function(mPQCrypto, "ml_dsa_87_check_secret_key",
2112
+ pqcrypto_ml_dsa_87_check_secret_key, 1);
2002
2113
  rb_define_module_function(mPQCrypto, "hybrid_kem_keypair", pqcrypto_hybrid_kem_keypair, 0);
2003
2114
  rb_define_module_function(mPQCrypto, "hybrid_kem_encapsulate", pqcrypto_hybrid_kem_encapsulate,
2004
2115
  1);