pq_crypto 0.3.1 → 0.3.2

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: 534c420f62323e9ddb49b48acdfa7af869731e3273bda56637d6bfee753de8d7
4
- data.tar.gz: f003494b4d33702a1b718dc6ded7a9f0612b315e41f0d739a5b2fe3ebba9d1d1
3
+ metadata.gz: 8cd88ab6ebe3042111e60711895a9563a1510057a321ac9dab510496634656b8
4
+ data.tar.gz: 684f469f8be7912780e00d368989418499aae00bb530d7fc32f8b6c6d5576593
5
5
  SHA512:
6
- metadata.gz: 1a039325a13cf8c074741fb70edc1e66f4e4939727b4c1369665794bc6ed71ef1ed2ef50d9debd9064d850e4856fac166f325889a9df4bfd3d8849a2d7a918e9
7
- data.tar.gz: 304888656181149eed84eca063e24eeb6c862819f6c54022e77b287ca4030b8602e9f61eb17809ad86ca0eea84796c6275a2afa12018ffdeaafc489c15c8f24a
6
+ metadata.gz: 54c1f3b0b8a2f7141d3ec4c8649c003793a3469f212fed94b0dc7ef0e2b0ff3ddba510383a0b62813d9ee98700bf266547be1900693c9ad465fb36deec91ab7b
7
+ data.tar.gz: 41c0bdea2d91bbd2a2e8884ee2b2a328c4c06d410fba8d92c9c1a9470b9d5597d0b8f5233b0ba87225535a80c9055033518ef1dd82e90101a9f5ffcd606b81a6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.2] — 2026-04-25
4
+
5
+ ### Added — streaming ML-DSA for large inputs
6
+
7
+ - Added `PQCrypto::Signature::SecretKey#sign_io(io, chunk_size: 1 << 20, context: "".b)`.
8
+ - Added `PQCrypto::Signature::PublicKey#verify_io(io, signature, chunk_size: 1 << 20, context: "".b)` and `verify_io!`.
9
+ - Implemented streaming pure ML-DSA through an internal FIPS 204 ExternalMu path. Existing one-shot `sign` / `verify` semantics are unchanged; no public `sign_mu` / `verify_mu` API is exposed.
10
+
11
+ ### Notes
12
+
13
+ - Streaming is primarily for large IO inputs and lower peak memory pressure. It is not a HashML-DSA/prehash speed mode; CPU cost is still dominated by SHAKE/Keccak.
14
+ - Default empty-context streaming signatures interoperate with the existing one-shot `verify(message, signature)` API. Non-empty `context:` must be supplied again during `verify_io`.
15
+
3
16
  ## [0.3.1] — 2026-04-24
4
17
 
5
18
  ### Fixed — X-Wing draft-10 compatibility
data/GET_STARTED.md CHANGED
@@ -35,6 +35,29 @@ sig.public_key.verify("message", signature)
35
35
  sig.public_key.verify!("message", signature)
36
36
  ```
37
37
 
38
+ For large files, use streaming ML-DSA:
39
+
40
+ ```ruby
41
+ signature = File.open("document.bin", "rb") do |io|
42
+ sig.secret_key.sign_io(io, chunk_size: 1 << 20)
43
+ end
44
+
45
+ ok = File.open("document.bin", "rb") do |io|
46
+ sig.public_key.verify_io(io, signature, chunk_size: 1 << 20)
47
+ end
48
+ ```
49
+
50
+ With an optional context:
51
+
52
+ ```ruby
53
+ ctx = "document-v1".b
54
+ signature = File.open("document.bin", "rb") { |io| sig.secret_key.sign_io(io, context: ctx) }
55
+ ok = File.open("document.bin", "rb") { |io| sig.public_key.verify_io(io, signature, context: ctx) }
56
+ ```
57
+
58
+ `sign_io` / `verify_io` are pure ML-DSA streaming helpers, not prehash
59
+ shortcuts. `verify_io!` raises on mismatch.
60
+
38
61
  ## 6. Hybrid KEM (X-Wing)
39
62
 
40
63
  ```ruby
data/README.md CHANGED
@@ -20,6 +20,7 @@ roll-your-own where a library primitive exists.
20
20
 
21
21
  - primitive-first API only
22
22
  - no protocol/session helpers in the public surface
23
+ - streaming ML-DSA signing/verification is available for large IO inputs
23
24
  - serialization uses pq_crypto-specific `pqc_container_*` wrappers
24
25
  - not audited
25
26
  - not yet positioned as production-ready
@@ -44,6 +45,20 @@ bundle exec rake compile
44
45
  - a C toolchain with C11 support (for `_Static_assert` / `_Thread_local`)
45
46
  - OpenSSL **3.0 or later** with SHA3-256 and SHAKE256 available (default provider)
46
47
 
48
+ ### Build-time Keccak backend
49
+
50
+ The default build uses PQClean's scalar `common/fips202.c` backend:
51
+
52
+ ```bash
53
+ PQCRYPTO_KECCAK_BACKEND=clean bundle exec rake compile
54
+ ```
55
+
56
+ `PQCRYPTO_KECCAK_BACKEND=xkcp` is reserved for a separately vendored,
57
+ reviewed, `fips202.h`-compatible XKCP adapter. If requested without that
58
+ adapter, the build aborts instead of silently falling back to `clean`.
59
+ This avoids mixing OpenSSL EVP SHAKE state with PQClean SHAKE state and
60
+ keeps output-byte compatibility explicit.
61
+
47
62
  ## Async / Fiber scheduler support
48
63
 
49
64
  `pq_crypto` does not require any gem-specific Async configuration. On
@@ -100,6 +115,8 @@ shared_secret = keypair.secret_key.decapsulate(result.ciphertext)
100
115
 
101
116
  ### ML-DSA-65
102
117
 
118
+ One-shot signing keeps the existing API:
119
+
103
120
  ```ruby
104
121
  keypair = PQCrypto::Signature.generate(:ml_dsa_65)
105
122
  signature = keypair.secret_key.sign("hello")
@@ -108,6 +125,36 @@ keypair.public_key.verify("hello", signature) # => true / false
108
125
  keypair.public_key.verify!("hello", signature) # raises on mismatch
109
126
  ```
110
127
 
128
+ For large inputs, use streaming IO so the message does not need to be
129
+ materialized as one Ruby string:
130
+
131
+ ```ruby
132
+ signature = File.open("document.bin", "rb") do |io|
133
+ keypair.secret_key.sign_io(io, chunk_size: 1 << 20)
134
+ end
135
+
136
+ ok = File.open("document.bin", "rb") do |io|
137
+ keypair.public_key.verify_io(io, signature, chunk_size: 1 << 20)
138
+ end
139
+ ```
140
+
141
+ `sign_io` / `verify_io` use pure ML-DSA with an internal FIPS 204
142
+ ExternalMu flow. They are not HashML-DSA/prehash shortcuts and do not
143
+ expose public `sign_mu` / `verify_mu` APIs. With the default empty
144
+ context, streaming signatures verify with `verify(message, signature)`
145
+ and one-shot signatures verify with `verify_io(io, signature)`.
146
+
147
+ Optional context is supported and must match on verify:
148
+
149
+ ```ruby
150
+ ctx = "invoice-v1".b
151
+ signature = File.open("document.bin", "rb") { |io| keypair.secret_key.sign_io(io, context: ctx) }
152
+ ok = File.open("document.bin", "rb") { |io| keypair.public_key.verify_io(io, signature, context: ctx) }
153
+ ```
154
+
155
+ `chunk_size` must be positive. `context` is limited to 255 bytes by
156
+ FIPS 204. `verify_io!` raises `PQCrypto::VerificationError` on mismatch.
157
+
111
158
  Note: `verify` returns a plain boolean for normal outcomes. `verify!`
112
159
  raises `PQCrypto::VerificationError` when the signature does not
113
160
  match.
@@ -11,6 +11,9 @@ $LDFLAGS << " -Wl,-no_warn_duplicate_libraries" if RbConfig::CONFIG["host_os"] =
11
11
 
12
12
  USE_SYSTEM = arg_config("--use-system-libraries") || ENV["PQCRYPTO_USE_SYSTEM_LIBRARIES"]
13
13
 
14
+ KECCAK_BACKEND = (ENV["PQCRYPTO_KECCAK_BACKEND"] || "clean").strip.downcase
15
+ SUPPORTED_KECCAK_BACKENDS = %w[clean xkcp].freeze
16
+
14
17
  SANITIZE = ENV["PQCRYPTO_SANITIZE"]
15
18
 
16
19
  if SANITIZE && !SANITIZE.strip.empty?
@@ -85,6 +88,41 @@ def configure_openssl!
85
88
  $CFLAGS << " -DHAVE_OPENSSL_EVP_H -DHAVE_OPENSSL_RAND_H"
86
89
  end
87
90
 
91
+ def configure_keccak_backend(vendor_dir, common_dir)
92
+ abort "Unsupported PQCRYPTO_KECCAK_BACKEND=#{KECCAK_BACKEND.inspect}. Supported: #{SUPPORTED_KECCAK_BACKENDS.join(", ")}" unless SUPPORTED_KECCAK_BACKENDS.include?(KECCAK_BACKEND)
93
+
94
+ case KECCAK_BACKEND
95
+ when "clean"
96
+ {
97
+ name: "clean",
98
+ include_dirs: [],
99
+ source_group: ["pqclean_common", [File.join(common_dir, "fips202.c")]]
100
+ }
101
+ when "xkcp"
102
+ # The optimized backend must provide the same fips202.h-compatible API as
103
+ # PQClean's common/fips202.c. Do not substitute OpenSSL EVP SHAKE here: the
104
+ # PQClean SHAKE state layout is part of the ML-KEM/ML-DSA call graph.
105
+ xkcp_dir = File.join(vendor_dir, "xkcp")
106
+ adapter_source = File.join(xkcp_dir, "pqclean_fips202_xkcp.c")
107
+
108
+ abort <<~MSG unless File.exist?(adapter_source)
109
+ PQCRYPTO_KECCAK_BACKEND=xkcp was requested, but no reviewed XKCP adapter was found.
110
+
111
+ Expected:
112
+ #{adapter_source}
113
+
114
+ Refusing to fall back silently to the clean backend. Vendor a fips202.h-compatible
115
+ XKCP adapter first, then run the full SHAKE-dependent KAT/regression test matrix.
116
+ MSG
117
+
118
+ {
119
+ name: "xkcp",
120
+ include_dirs: [xkcp_dir],
121
+ source_group: ["xkcp_keccak", [adapter_source]]
122
+ }
123
+ end
124
+ end
125
+
88
126
  def configure_pqclean(vendor_dir)
89
127
  return nil unless vendor_dir
90
128
 
@@ -95,17 +133,20 @@ def configure_pqclean(vendor_dir)
95
133
  mldsa_dir = File.join(pqclean_dir, "crypto_sign", "ml-dsa-65", "clean")
96
134
  common_dir = File.join(pqclean_dir, "common")
97
135
 
98
- include_dirs = [mlkem_dir, mldsa_dir, common_dir]
136
+ keccak_config = configure_keccak_backend(vendor_dir, common_dir)
137
+
138
+ include_dirs = [mlkem_dir, mldsa_dir, common_dir, *keccak_config[:include_dirs]]
99
139
  return nil unless include_dirs.all? { |dir| Dir.exist?(dir) }
100
140
 
101
141
  mlkem_sources = Dir.glob(File.join(mlkem_dir, "*.c")).sort
102
142
  mldsa_sources = Dir.glob(File.join(mldsa_dir, "*.c")).sort
103
- common_sources = %w[fips202.c sha2.c sp800-185.c].map { |name| File.join(common_dir, name) }
143
+ common_sources = %w[sha2.c sp800-185.c].map { |name| File.join(common_dir, name) }
104
144
 
105
145
  source_groups = [
106
146
  ["pqclean_mlkem", mlkem_sources],
107
147
  ["pqclean_mldsa", mldsa_sources],
108
- ["pqclean_common", common_sources]
148
+ ["pqclean_common", common_sources],
149
+ keccak_config[:source_group]
109
150
  ]
110
151
 
111
152
  return nil unless source_groups.all? { |_, sources| sources.all? { |path| File.exist?(path) } }
@@ -115,6 +156,7 @@ def configure_pqclean(vendor_dir)
115
156
 
116
157
  {
117
158
  include_dirs: include_dirs,
159
+ keccak_backend: keccak_config[:name],
118
160
  source_groups: source_groups
119
161
  }
120
162
  end
@@ -165,6 +207,7 @@ pqclean_config = configure_pqclean(vendor_dir)
165
207
  puts "OpenSSL: system"
166
208
  abort "PQClean vendored sources are required. Run: bundle exec rake vendor" unless pqclean_config
167
209
  puts "PQClean: vendored (randombytes overridden by pq_randombytes.c)"
210
+ puts "Keccak backend: #{pqclean_config[:keccak_backend]}"
168
211
  puts "Output: pqcrypto/pqcrypto_secure"
169
212
  puts "===================================="
170
213
 
@@ -0,0 +1,297 @@
1
+ #include "pqcrypto_secure.h"
2
+
3
+ #include <stdint.h>
4
+ #include <stddef.h>
5
+ #include <string.h>
6
+
7
+ #include "vendor/pqclean/crypto_sign/ml-dsa-65/clean/params.h"
8
+ #include "vendor/pqclean/crypto_sign/ml-dsa-65/clean/packing.h"
9
+ #include "vendor/pqclean/crypto_sign/ml-dsa-65/clean/polyvec.h"
10
+ #include "vendor/pqclean/crypto_sign/ml-dsa-65/clean/poly.h"
11
+ #include "vendor/pqclean/crypto_sign/ml-dsa-65/clean/symmetric.h"
12
+ #include "fips202.h"
13
+ #include "randombytes.h"
14
+
15
+ #if CRHBYTES != PQ_MLDSA_MUBYTES
16
+ #error "PQ_MLDSA_MUBYTES must match PQClean's CRHBYTES"
17
+ #endif
18
+ #if TRBYTES != PQ_MLDSA_TRBYTES
19
+ #error "PQ_MLDSA_TRBYTES must match PQClean's TRBYTES"
20
+ #endif
21
+
22
+ int pq_mldsa_extract_tr_from_secret_key(uint8_t *tr_out, const uint8_t *secret_key) {
23
+ if (tr_out == NULL || secret_key == NULL) {
24
+ return PQ_ERROR_BUFFER;
25
+ }
26
+
27
+ uint8_t rho[SEEDBYTES];
28
+ uint8_t key[SEEDBYTES];
29
+ polyveck t0;
30
+ polyvecl s1;
31
+ polyveck s2;
32
+
33
+ PQCLEAN_MLDSA65_CLEAN_unpack_sk(rho, tr_out, key, &t0, &s1, &s2, secret_key);
34
+
35
+ pq_secure_wipe(rho, sizeof(rho));
36
+ pq_secure_wipe(key, sizeof(key));
37
+ pq_secure_wipe(&t0, sizeof(t0));
38
+ pq_secure_wipe(&s1, sizeof(s1));
39
+ pq_secure_wipe(&s2, sizeof(s2));
40
+
41
+ return PQ_SUCCESS;
42
+ }
43
+
44
+ int pq_mldsa_compute_tr_from_public_key(uint8_t *tr_out, const uint8_t *public_key) {
45
+ if (tr_out == NULL || public_key == NULL) {
46
+ return PQ_ERROR_BUFFER;
47
+ }
48
+
49
+ shake256(tr_out, TRBYTES, public_key, PQCLEAN_MLDSA65_CLEAN_CRYPTO_PUBLICKEYBYTES);
50
+ return PQ_SUCCESS;
51
+ }
52
+
53
+ int pq_sign_mu(uint8_t *signature, size_t *signature_len, const uint8_t *mu,
54
+ const uint8_t *secret_key) {
55
+ if (signature == NULL || signature_len == NULL || mu == NULL || secret_key == NULL) {
56
+ return PQ_ERROR_BUFFER;
57
+ }
58
+
59
+ unsigned int n;
60
+ uint8_t rho[SEEDBYTES];
61
+ uint8_t tr_unused[TRBYTES];
62
+ uint8_t key[SEEDBYTES];
63
+ uint8_t rnd[RNDBYTES];
64
+ uint8_t mu_local[CRHBYTES];
65
+ uint8_t rhoprime[CRHBYTES];
66
+ uint16_t nonce = 0;
67
+ polyvecl mat[K], s1, y, z;
68
+ polyveck t0, s2, w1, w0, h;
69
+ poly cp;
70
+ shake256incctx state;
71
+
72
+ PQCLEAN_MLDSA65_CLEAN_unpack_sk(rho, tr_unused, key, &t0, &s1, &s2, secret_key);
73
+ pq_secure_wipe(tr_unused, sizeof(tr_unused));
74
+
75
+ memcpy(mu_local, mu, CRHBYTES);
76
+
77
+ randombytes(rnd, RNDBYTES);
78
+
79
+ {
80
+ uint8_t kr[SEEDBYTES + RNDBYTES + CRHBYTES];
81
+ memcpy(kr, key, SEEDBYTES);
82
+ memcpy(kr + SEEDBYTES, rnd, RNDBYTES);
83
+ memcpy(kr + SEEDBYTES + RNDBYTES, mu_local, CRHBYTES);
84
+ shake256(rhoprime, CRHBYTES, kr, sizeof(kr));
85
+ pq_secure_wipe(kr, sizeof(kr));
86
+ }
87
+
88
+ PQCLEAN_MLDSA65_CLEAN_polyvec_matrix_expand(mat, rho);
89
+ PQCLEAN_MLDSA65_CLEAN_polyvecl_ntt(&s1);
90
+ PQCLEAN_MLDSA65_CLEAN_polyveck_ntt(&s2);
91
+ PQCLEAN_MLDSA65_CLEAN_polyveck_ntt(&t0);
92
+
93
+ rej:
94
+ PQCLEAN_MLDSA65_CLEAN_polyvecl_uniform_gamma1(&y, rhoprime, nonce++);
95
+
96
+ z = y;
97
+ PQCLEAN_MLDSA65_CLEAN_polyvecl_ntt(&z);
98
+ PQCLEAN_MLDSA65_CLEAN_polyvec_matrix_pointwise_montgomery(&w1, mat, &z);
99
+ PQCLEAN_MLDSA65_CLEAN_polyveck_reduce(&w1);
100
+ PQCLEAN_MLDSA65_CLEAN_polyveck_invntt_tomont(&w1);
101
+
102
+ PQCLEAN_MLDSA65_CLEAN_polyveck_caddq(&w1);
103
+ PQCLEAN_MLDSA65_CLEAN_polyveck_decompose(&w1, &w0, &w1);
104
+ PQCLEAN_MLDSA65_CLEAN_polyveck_pack_w1(signature, &w1);
105
+
106
+ shake256_inc_init(&state);
107
+ shake256_inc_absorb(&state, mu_local, CRHBYTES);
108
+ shake256_inc_absorb(&state, signature, K * POLYW1_PACKEDBYTES);
109
+ shake256_inc_finalize(&state);
110
+ shake256_inc_squeeze(signature, CTILDEBYTES, &state);
111
+ shake256_inc_ctx_release(&state);
112
+
113
+ PQCLEAN_MLDSA65_CLEAN_poly_challenge(&cp, signature);
114
+ PQCLEAN_MLDSA65_CLEAN_poly_ntt(&cp);
115
+
116
+ PQCLEAN_MLDSA65_CLEAN_polyvecl_pointwise_poly_montgomery(&z, &cp, &s1);
117
+ PQCLEAN_MLDSA65_CLEAN_polyvecl_invntt_tomont(&z);
118
+ PQCLEAN_MLDSA65_CLEAN_polyvecl_add(&z, &z, &y);
119
+ PQCLEAN_MLDSA65_CLEAN_polyvecl_reduce(&z);
120
+ if (PQCLEAN_MLDSA65_CLEAN_polyvecl_chknorm(&z, GAMMA1 - BETA)) {
121
+ goto rej;
122
+ }
123
+
124
+ PQCLEAN_MLDSA65_CLEAN_polyveck_pointwise_poly_montgomery(&h, &cp, &s2);
125
+ PQCLEAN_MLDSA65_CLEAN_polyveck_invntt_tomont(&h);
126
+ PQCLEAN_MLDSA65_CLEAN_polyveck_sub(&w0, &w0, &h);
127
+ PQCLEAN_MLDSA65_CLEAN_polyveck_reduce(&w0);
128
+ if (PQCLEAN_MLDSA65_CLEAN_polyveck_chknorm(&w0, GAMMA2 - BETA)) {
129
+ goto rej;
130
+ }
131
+
132
+ PQCLEAN_MLDSA65_CLEAN_polyveck_pointwise_poly_montgomery(&h, &cp, &t0);
133
+ PQCLEAN_MLDSA65_CLEAN_polyveck_invntt_tomont(&h);
134
+ PQCLEAN_MLDSA65_CLEAN_polyveck_reduce(&h);
135
+ if (PQCLEAN_MLDSA65_CLEAN_polyveck_chknorm(&h, GAMMA2)) {
136
+ goto rej;
137
+ }
138
+
139
+ PQCLEAN_MLDSA65_CLEAN_polyveck_add(&w0, &w0, &h);
140
+ n = PQCLEAN_MLDSA65_CLEAN_polyveck_make_hint(&h, &w0, &w1);
141
+ if (n > OMEGA) {
142
+ goto rej;
143
+ }
144
+
145
+ PQCLEAN_MLDSA65_CLEAN_pack_sig(signature, signature, &z, &h);
146
+ *signature_len = PQCLEAN_MLDSA65_CLEAN_CRYPTO_BYTES;
147
+
148
+ pq_secure_wipe(rho, sizeof(rho));
149
+ pq_secure_wipe(key, sizeof(key));
150
+ pq_secure_wipe(rnd, sizeof(rnd));
151
+ pq_secure_wipe(mu_local, sizeof(mu_local));
152
+ pq_secure_wipe(rhoprime, sizeof(rhoprime));
153
+ pq_secure_wipe(&s1, sizeof(s1));
154
+ pq_secure_wipe(&s2, sizeof(s2));
155
+ pq_secure_wipe(&t0, sizeof(t0));
156
+ pq_secure_wipe(&y, sizeof(y));
157
+ pq_secure_wipe(&z, sizeof(z));
158
+ pq_secure_wipe(&w0, sizeof(w0));
159
+ pq_secure_wipe(&cp, sizeof(cp));
160
+
161
+ return PQ_SUCCESS;
162
+ }
163
+
164
+ int pq_verify_mu(const uint8_t *signature, size_t signature_len, const uint8_t *mu,
165
+ const uint8_t *public_key) {
166
+ if (signature == NULL || mu == NULL || public_key == NULL) {
167
+ return PQ_ERROR_BUFFER;
168
+ }
169
+ if (signature_len != PQCLEAN_MLDSA65_CLEAN_CRYPTO_BYTES) {
170
+ return PQ_ERROR_VERIFY;
171
+ }
172
+
173
+ unsigned int i;
174
+ uint8_t buf[K * POLYW1_PACKEDBYTES];
175
+ uint8_t rho[SEEDBYTES];
176
+ uint8_t c[CTILDEBYTES];
177
+ uint8_t c2[CTILDEBYTES];
178
+ poly cp;
179
+ polyvecl mat[K], z;
180
+ polyveck t1, w1, h;
181
+ shake256incctx state;
182
+
183
+ PQCLEAN_MLDSA65_CLEAN_unpack_pk(rho, &t1, public_key);
184
+ if (PQCLEAN_MLDSA65_CLEAN_unpack_sig(c, &z, &h, signature)) {
185
+ return PQ_ERROR_VERIFY;
186
+ }
187
+ if (PQCLEAN_MLDSA65_CLEAN_polyvecl_chknorm(&z, GAMMA1 - BETA)) {
188
+ return PQ_ERROR_VERIFY;
189
+ }
190
+
191
+ PQCLEAN_MLDSA65_CLEAN_poly_challenge(&cp, c);
192
+ PQCLEAN_MLDSA65_CLEAN_polyvec_matrix_expand(mat, rho);
193
+
194
+ PQCLEAN_MLDSA65_CLEAN_polyvecl_ntt(&z);
195
+ PQCLEAN_MLDSA65_CLEAN_polyvec_matrix_pointwise_montgomery(&w1, mat, &z);
196
+
197
+ PQCLEAN_MLDSA65_CLEAN_poly_ntt(&cp);
198
+ PQCLEAN_MLDSA65_CLEAN_polyveck_shiftl(&t1);
199
+ PQCLEAN_MLDSA65_CLEAN_polyveck_ntt(&t1);
200
+ PQCLEAN_MLDSA65_CLEAN_polyveck_pointwise_poly_montgomery(&t1, &cp, &t1);
201
+
202
+ PQCLEAN_MLDSA65_CLEAN_polyveck_sub(&w1, &w1, &t1);
203
+ PQCLEAN_MLDSA65_CLEAN_polyveck_reduce(&w1);
204
+ PQCLEAN_MLDSA65_CLEAN_polyveck_invntt_tomont(&w1);
205
+
206
+ PQCLEAN_MLDSA65_CLEAN_polyveck_caddq(&w1);
207
+ PQCLEAN_MLDSA65_CLEAN_polyveck_use_hint(&w1, &w1, &h);
208
+ PQCLEAN_MLDSA65_CLEAN_polyveck_pack_w1(buf, &w1);
209
+
210
+ shake256_inc_init(&state);
211
+ shake256_inc_absorb(&state, mu, CRHBYTES);
212
+ shake256_inc_absorb(&state, buf, K * POLYW1_PACKEDBYTES);
213
+ shake256_inc_finalize(&state);
214
+ shake256_inc_squeeze(c2, CTILDEBYTES, &state);
215
+ shake256_inc_ctx_release(&state);
216
+
217
+ for (i = 0; i < CTILDEBYTES; ++i) {
218
+ if (c[i] != c2[i]) {
219
+ return PQ_ERROR_VERIFY;
220
+ }
221
+ }
222
+
223
+ return PQ_SUCCESS;
224
+ }
225
+
226
+ void *pq_mu_builder_new(void) {
227
+ shake256incctx *state = (shake256incctx *)malloc(sizeof(shake256incctx));
228
+ if (state == NULL) {
229
+ return NULL;
230
+ }
231
+
232
+ shake256_inc_init(state);
233
+ return state;
234
+ }
235
+
236
+ int pq_mu_builder_init(void *state_ptr, const uint8_t *tr, const uint8_t *ctx, size_t ctxlen) {
237
+ if (state_ptr == NULL || tr == NULL) {
238
+ return PQ_ERROR_BUFFER;
239
+ }
240
+ if (ctxlen > 255) {
241
+ return PQ_ERROR_BUFFER;
242
+ }
243
+ if (ctxlen > 0 && ctx == NULL) {
244
+ return PQ_ERROR_BUFFER;
245
+ }
246
+
247
+ shake256incctx *state = (shake256incctx *)state_ptr;
248
+
249
+ uint8_t prefix[2];
250
+ prefix[0] = 0x00;
251
+ prefix[1] = (uint8_t)ctxlen;
252
+
253
+ shake256_inc_absorb(state, tr, TRBYTES);
254
+ shake256_inc_absorb(state, prefix, sizeof(prefix));
255
+ if (ctxlen > 0) {
256
+ shake256_inc_absorb(state, ctx, ctxlen);
257
+ }
258
+ return PQ_SUCCESS;
259
+ }
260
+
261
+ int pq_mu_builder_absorb(void *state_ptr, const uint8_t *chunk, size_t chunk_len) {
262
+ if (state_ptr == NULL) {
263
+ return PQ_ERROR_BUFFER;
264
+ }
265
+ if (chunk_len == 0) {
266
+ return PQ_SUCCESS;
267
+ }
268
+ if (chunk == NULL) {
269
+ return PQ_ERROR_BUFFER;
270
+ }
271
+
272
+ shake256incctx *state = (shake256incctx *)state_ptr;
273
+ shake256_inc_absorb(state, chunk, chunk_len);
274
+ return PQ_SUCCESS;
275
+ }
276
+
277
+ int pq_mu_builder_finalize(void *state_ptr, uint8_t *mu_out) {
278
+ if (state_ptr == NULL || mu_out == NULL) {
279
+ return PQ_ERROR_BUFFER;
280
+ }
281
+
282
+ shake256incctx *state = (shake256incctx *)state_ptr;
283
+ shake256_inc_finalize(state);
284
+ shake256_inc_squeeze(mu_out, CRHBYTES, state);
285
+ shake256_inc_ctx_release(state);
286
+ free(state);
287
+ return PQ_SUCCESS;
288
+ }
289
+
290
+ void pq_mu_builder_release(void *state_ptr) {
291
+ if (state_ptr == NULL) {
292
+ return;
293
+ }
294
+ shake256incctx *state = (shake256incctx *)state_ptr;
295
+ shake256_inc_ctx_release(state);
296
+ free(state);
297
+ }
@@ -223,6 +223,10 @@ static void pq_wipe_and_free(uint8_t *buffer, size_t len) {
223
223
  }
224
224
  }
225
225
 
226
+ static void pq_free_buffer(uint8_t *buffer) {
227
+ free(buffer);
228
+ }
229
+
226
230
  static void pq_validate_bytes_argument(VALUE value, size_t expected_len, const char *what) {
227
231
  StringValue(value);
228
232
  if ((size_t)RSTRING_LEN(value) != expected_len) {
@@ -628,17 +632,17 @@ static VALUE pqcrypto__test_sign_from_seed(VALUE self, VALUE message, VALUE secr
628
632
 
629
633
  rb_thread_call_without_gvl(pq_testing_sign_nogvl, &call, NULL, NULL);
630
634
 
631
- pq_wipe_and_free(call.message, call.message_len);
635
+ pq_free_buffer(call.message);
632
636
  pq_wipe_and_free((uint8_t *)call.secret_key, secret_key_len);
633
637
  pq_wipe_and_free((uint8_t *)call.seed, call.seed_len);
634
638
 
635
639
  if (call.result != PQ_SUCCESS) {
636
- pq_wipe_and_free(call.signature, PQ_MLDSA_BYTES);
640
+ pq_free_buffer(call.signature);
637
641
  pq_raise_general_error(call.result);
638
642
  }
639
643
 
640
644
  VALUE result = pq_string_from_buffer(call.signature, call.signature_len);
641
- pq_wipe_and_free(call.signature, PQ_MLDSA_BYTES);
645
+ pq_free_buffer(call.signature);
642
646
  return result;
643
647
  }
644
648
 
@@ -661,16 +665,16 @@ static VALUE pqcrypto_sign(VALUE self, VALUE message, VALUE secret_key) {
661
665
 
662
666
  rb_nogvl(pq_sign_nogvl, &call, NULL, NULL, RB_NOGVL_OFFLOAD_SAFE);
663
667
 
664
- pq_wipe_and_free(call.message, call.message_len);
668
+ pq_free_buffer(call.message);
665
669
  pq_wipe_and_free((uint8_t *)call.secret_key, secret_key_len);
666
670
 
667
671
  if (call.result != PQ_SUCCESS) {
668
- pq_wipe_and_free(call.signature, PQ_MLDSA_BYTES);
672
+ pq_free_buffer(call.signature);
669
673
  pq_raise_general_error(call.result);
670
674
  }
671
675
 
672
676
  VALUE result = pq_string_from_buffer(call.signature, call.signature_len);
673
- free(call.signature);
677
+ pq_free_buffer(call.signature);
674
678
  return result;
675
679
  }
676
680
 
@@ -689,9 +693,9 @@ static VALUE pqcrypto_verify(VALUE self, VALUE message, VALUE signature, VALUE p
689
693
 
690
694
  rb_nogvl(pq_verify_nogvl, &call, NULL, NULL, RB_NOGVL_OFFLOAD_SAFE);
691
695
 
692
- pq_wipe_and_free(call.message, call.message_len);
693
- pq_wipe_and_free((uint8_t *)call.public_key, public_key_len);
694
- pq_wipe_and_free((uint8_t *)call.signature, signature_len);
696
+ pq_free_buffer(call.message);
697
+ pq_free_buffer((uint8_t *)call.public_key);
698
+ pq_free_buffer((uint8_t *)call.signature);
695
699
 
696
700
  if (call.result == PQ_SUCCESS) {
697
701
  return Qtrue;
@@ -731,6 +735,282 @@ static VALUE pqcrypto_version(VALUE self) {
731
735
  return rb_str_new_cstr(pq_version());
732
736
  }
733
737
 
738
+ typedef struct {
739
+ void *builder;
740
+ } mu_builder_wrapper_t;
741
+
742
+ typedef struct {
743
+ int result;
744
+ void *builder;
745
+ const uint8_t *chunk;
746
+ size_t chunk_len;
747
+ } mu_absorb_call_t;
748
+
749
+ typedef struct {
750
+ int result;
751
+ void *builder;
752
+ uint8_t *mu_out;
753
+ } mu_finalize_call_t;
754
+
755
+ typedef struct {
756
+ int result;
757
+ uint8_t *signature;
758
+ size_t signature_len;
759
+ const uint8_t *mu;
760
+ const uint8_t *secret_key;
761
+ } sign_mu_call_t;
762
+
763
+ typedef struct {
764
+ int result;
765
+ const uint8_t *signature;
766
+ size_t signature_len;
767
+ const uint8_t *mu;
768
+ const uint8_t *public_key;
769
+ } verify_mu_call_t;
770
+
771
+ static void mu_builder_wrapper_free(void *ptr) {
772
+ mu_builder_wrapper_t *wrapper = (mu_builder_wrapper_t *)ptr;
773
+ if (wrapper == NULL) {
774
+ return;
775
+ }
776
+ if (wrapper->builder != NULL) {
777
+ pq_mu_builder_release(wrapper->builder);
778
+ wrapper->builder = NULL;
779
+ }
780
+ xfree(wrapper);
781
+ }
782
+
783
+ static size_t mu_builder_wrapper_size(const void *ptr) {
784
+ (void)ptr;
785
+ return sizeof(mu_builder_wrapper_t);
786
+ }
787
+
788
+ static const rb_data_type_t mu_builder_data_type = {
789
+ "PQCrypto::MLDSA::MuBuilder",
790
+ {NULL, mu_builder_wrapper_free, mu_builder_wrapper_size},
791
+ NULL,
792
+ NULL,
793
+ RUBY_TYPED_FREE_IMMEDIATELY};
794
+
795
+ static mu_builder_wrapper_t *mu_builder_unwrap(VALUE obj) {
796
+ mu_builder_wrapper_t *wrapper;
797
+ TypedData_Get_Struct(obj, mu_builder_wrapper_t, &mu_builder_data_type, wrapper);
798
+ if (wrapper == NULL || wrapper->builder == NULL) {
799
+ rb_raise(ePQCryptoError, "mu builder used after release");
800
+ }
801
+ return wrapper;
802
+ }
803
+
804
+ static VALUE pqcrypto__native_mldsa_extract_tr(VALUE self, VALUE secret_key) {
805
+ (void)self;
806
+ pq_validate_bytes_argument(secret_key, PQ_MLDSA_SECRETKEYBYTES, "secret key");
807
+
808
+ uint8_t tr[PQ_MLDSA_TRBYTES];
809
+ int rc = pq_mldsa_extract_tr_from_secret_key(tr, (const uint8_t *)RSTRING_PTR(secret_key));
810
+ if (rc != PQ_SUCCESS) {
811
+ pq_secure_wipe(tr, sizeof(tr));
812
+ pq_raise_general_error(rc);
813
+ }
814
+ VALUE result = pq_string_from_buffer(tr, sizeof(tr));
815
+ pq_secure_wipe(tr, sizeof(tr));
816
+ return result;
817
+ }
818
+
819
+ static VALUE pqcrypto__native_mldsa_compute_tr(VALUE self, VALUE public_key) {
820
+ (void)self;
821
+ pq_validate_bytes_argument(public_key, PQ_MLDSA_PUBLICKEYBYTES, "public key");
822
+
823
+ uint8_t tr[PQ_MLDSA_TRBYTES];
824
+ int rc = pq_mldsa_compute_tr_from_public_key(tr, (const uint8_t *)RSTRING_PTR(public_key));
825
+ if (rc != PQ_SUCCESS) {
826
+ pq_raise_general_error(rc);
827
+ }
828
+ return pq_string_from_buffer(tr, sizeof(tr));
829
+ }
830
+
831
+ static VALUE pqcrypto__native_mldsa_mu_builder_new(VALUE self, VALUE tr, VALUE ctx) {
832
+ (void)self;
833
+ pq_validate_bytes_argument(tr, PQ_MLDSA_TRBYTES, "tr");
834
+ StringValue(ctx);
835
+
836
+ size_t ctxlen = (size_t)RSTRING_LEN(ctx);
837
+ if (ctxlen > 255) {
838
+ rb_raise(rb_eArgError, "ML-DSA context length must be <= 255 bytes");
839
+ }
840
+
841
+ void *builder = pq_mu_builder_new();
842
+ if (builder == NULL) {
843
+ rb_raise(rb_eNoMemError, "Memory allocation failed (mu builder)");
844
+ }
845
+
846
+ int rc = pq_mu_builder_init(builder, (const uint8_t *)RSTRING_PTR(tr),
847
+ (const uint8_t *)RSTRING_PTR(ctx), ctxlen);
848
+ if (rc != PQ_SUCCESS) {
849
+ pq_mu_builder_release(builder);
850
+ pq_raise_general_error(rc);
851
+ }
852
+
853
+ mu_builder_wrapper_t *wrapper;
854
+ VALUE obj =
855
+ TypedData_Make_Struct(rb_cObject, mu_builder_wrapper_t, &mu_builder_data_type, wrapper);
856
+ wrapper->builder = builder;
857
+ return obj;
858
+ }
859
+
860
+ static void *pq_mu_absorb_nogvl(void *arg) {
861
+ mu_absorb_call_t *call = (mu_absorb_call_t *)arg;
862
+ call->result = pq_mu_builder_absorb(call->builder, call->chunk, call->chunk_len);
863
+ return NULL;
864
+ }
865
+
866
+ static VALUE pqcrypto__native_mldsa_mu_builder_update(VALUE self, VALUE builder_obj, VALUE chunk) {
867
+ (void)self;
868
+ mu_builder_wrapper_t *wrapper = mu_builder_unwrap(builder_obj);
869
+ StringValue(chunk);
870
+
871
+ size_t chunk_len = (size_t)RSTRING_LEN(chunk);
872
+ if (chunk_len == 0) {
873
+ return Qnil;
874
+ }
875
+
876
+ uint8_t *copy = pq_alloc_buffer(chunk_len);
877
+ memcpy(copy, RSTRING_PTR(chunk), chunk_len);
878
+
879
+ mu_absorb_call_t call = {0};
880
+ call.builder = wrapper->builder;
881
+ call.chunk = copy;
882
+ call.chunk_len = chunk_len;
883
+
884
+ rb_nogvl(pq_mu_absorb_nogvl, &call, NULL, NULL, RB_NOGVL_OFFLOAD_SAFE);
885
+ free(copy);
886
+
887
+ if (call.result != PQ_SUCCESS) {
888
+ pq_raise_general_error(call.result);
889
+ }
890
+ return Qnil;
891
+ }
892
+
893
+ static void *pq_mu_finalize_nogvl(void *arg) {
894
+ mu_finalize_call_t *call = (mu_finalize_call_t *)arg;
895
+ call->result = pq_mu_builder_finalize(call->builder, call->mu_out);
896
+ return NULL;
897
+ }
898
+
899
+ static VALUE pqcrypto__native_mldsa_mu_builder_finalize(VALUE self, VALUE builder_obj) {
900
+ (void)self;
901
+ mu_builder_wrapper_t *wrapper = mu_builder_unwrap(builder_obj);
902
+
903
+ uint8_t mu[PQ_MLDSA_MUBYTES];
904
+
905
+ mu_finalize_call_t call = {0};
906
+ call.builder = wrapper->builder;
907
+ call.mu_out = mu;
908
+
909
+ rb_nogvl(pq_mu_finalize_nogvl, &call, NULL, NULL, RB_NOGVL_OFFLOAD_SAFE);
910
+
911
+ if (call.result != PQ_SUCCESS) {
912
+ pq_mu_builder_release(wrapper->builder);
913
+ }
914
+ wrapper->builder = NULL;
915
+
916
+ if (call.result != PQ_SUCCESS) {
917
+ pq_secure_wipe(mu, sizeof(mu));
918
+ pq_raise_general_error(call.result);
919
+ }
920
+
921
+ VALUE result = pq_string_from_buffer(mu, sizeof(mu));
922
+ pq_secure_wipe(mu, sizeof(mu));
923
+ return result;
924
+ }
925
+
926
+ static VALUE pqcrypto__native_mldsa_mu_builder_release(VALUE self, VALUE builder_obj) {
927
+ (void)self;
928
+ mu_builder_wrapper_t *wrapper;
929
+ TypedData_Get_Struct(builder_obj, mu_builder_wrapper_t, &mu_builder_data_type, wrapper);
930
+ if (wrapper != NULL && wrapper->builder != NULL) {
931
+ pq_mu_builder_release(wrapper->builder);
932
+ wrapper->builder = NULL;
933
+ }
934
+ return Qnil;
935
+ }
936
+
937
+ static void *pq_sign_mu_nogvl(void *arg) {
938
+ sign_mu_call_t *call = (sign_mu_call_t *)arg;
939
+ call->result = pq_sign_mu(call->signature, &call->signature_len, call->mu, call->secret_key);
940
+ return NULL;
941
+ }
942
+
943
+ static VALUE pqcrypto__native_mldsa_sign_mu(VALUE self, VALUE mu, VALUE secret_key) {
944
+ (void)self;
945
+ pq_validate_bytes_argument(mu, PQ_MLDSA_MUBYTES, "mu");
946
+ pq_validate_bytes_argument(secret_key, PQ_MLDSA_SECRETKEYBYTES, "secret key");
947
+
948
+ sign_mu_call_t call = {0};
949
+ size_t secret_key_len = 0;
950
+ size_t mu_len = 0;
951
+ uint8_t *mu_copy = pq_copy_ruby_string(mu, &mu_len);
952
+ uint8_t *sk_copy = pq_copy_ruby_string(secret_key, &secret_key_len);
953
+
954
+ call.mu = mu_copy;
955
+ call.secret_key = sk_copy;
956
+ call.signature_len = PQ_MLDSA_BYTES;
957
+ call.signature = pq_alloc_buffer(PQ_MLDSA_BYTES);
958
+
959
+ rb_nogvl(pq_sign_mu_nogvl, &call, NULL, NULL, RB_NOGVL_OFFLOAD_SAFE);
960
+
961
+ pq_wipe_and_free(mu_copy, mu_len);
962
+ pq_wipe_and_free(sk_copy, secret_key_len);
963
+
964
+ if (call.result != PQ_SUCCESS) {
965
+ pq_free_buffer(call.signature);
966
+ pq_raise_general_error(call.result);
967
+ }
968
+
969
+ VALUE result = pq_string_from_buffer(call.signature, call.signature_len);
970
+ pq_free_buffer(call.signature);
971
+ return result;
972
+ }
973
+
974
+ static void *pq_verify_mu_nogvl(void *arg) {
975
+ verify_mu_call_t *call = (verify_mu_call_t *)arg;
976
+ call->result = pq_verify_mu(call->signature, call->signature_len, call->mu, call->public_key);
977
+ return NULL;
978
+ }
979
+
980
+ static VALUE pqcrypto__native_mldsa_verify_mu(VALUE self, VALUE mu, VALUE signature,
981
+ VALUE public_key) {
982
+ (void)self;
983
+ StringValue(signature);
984
+ pq_validate_bytes_argument(mu, PQ_MLDSA_MUBYTES, "mu");
985
+ pq_validate_bytes_argument(public_key, PQ_MLDSA_PUBLICKEYBYTES, "public key");
986
+
987
+ verify_mu_call_t call = {0};
988
+ size_t public_key_len = 0;
989
+ size_t signature_len = 0;
990
+ size_t mu_len = 0;
991
+ uint8_t *mu_copy = pq_copy_ruby_string(mu, &mu_len);
992
+ uint8_t *pk_copy = pq_copy_ruby_string(public_key, &public_key_len);
993
+ uint8_t *sig_copy = pq_copy_ruby_string(signature, &signature_len);
994
+
995
+ call.mu = mu_copy;
996
+ call.public_key = pk_copy;
997
+ call.signature = sig_copy;
998
+ call.signature_len = signature_len;
999
+
1000
+ rb_nogvl(pq_verify_mu_nogvl, &call, NULL, NULL, RB_NOGVL_OFFLOAD_SAFE);
1001
+ pq_wipe_and_free(mu_copy, mu_len);
1002
+ pq_free_buffer(pk_copy);
1003
+ pq_free_buffer(sig_copy);
1004
+
1005
+ if (call.result == PQ_SUCCESS) {
1006
+ return Qtrue;
1007
+ }
1008
+ if (call.result == PQ_ERROR_VERIFY) {
1009
+ return Qfalse;
1010
+ }
1011
+ pq_raise_general_error(call.result);
1012
+ }
1013
+
734
1014
  static void define_constants(void) {
735
1015
  rb_define_const(mPQCrypto, "ML_KEM_PUBLIC_KEY_BYTES", INT2NUM(PQ_MLKEM_PUBLICKEYBYTES));
736
1016
  rb_define_const(mPQCrypto, "ML_KEM_SECRET_KEY_BYTES", INT2NUM(PQ_MLKEM_SECRETKEYBYTES));
@@ -834,6 +1114,22 @@ void Init_pqcrypto_secure(void) {
834
1114
  pqcrypto_secret_key_from_pqc_container_der, 1);
835
1115
  rb_define_module_function(mPQCrypto, "secret_key_from_pqc_container_pem",
836
1116
  pqcrypto_secret_key_from_pqc_container_pem, 1);
1117
+ rb_define_module_function(mPQCrypto, "_native_mldsa_extract_tr",
1118
+ pqcrypto__native_mldsa_extract_tr, 1);
1119
+ rb_define_module_function(mPQCrypto, "_native_mldsa_compute_tr",
1120
+ pqcrypto__native_mldsa_compute_tr, 1);
1121
+ rb_define_module_function(mPQCrypto, "_native_mldsa_mu_builder_new",
1122
+ pqcrypto__native_mldsa_mu_builder_new, 2);
1123
+ rb_define_module_function(mPQCrypto, "_native_mldsa_mu_builder_update",
1124
+ pqcrypto__native_mldsa_mu_builder_update, 2);
1125
+ rb_define_module_function(mPQCrypto, "_native_mldsa_mu_builder_finalize",
1126
+ pqcrypto__native_mldsa_mu_builder_finalize, 1);
1127
+ rb_define_module_function(mPQCrypto, "_native_mldsa_mu_builder_release",
1128
+ pqcrypto__native_mldsa_mu_builder_release, 1);
1129
+ rb_define_module_function(mPQCrypto, "_native_mldsa_sign_mu", pqcrypto__native_mldsa_sign_mu,
1130
+ 2);
1131
+ rb_define_module_function(mPQCrypto, "_native_mldsa_verify_mu",
1132
+ pqcrypto__native_mldsa_verify_mu, 3);
837
1133
 
838
1134
  define_constants();
839
1135
  }
@@ -959,5 +959,5 @@ int pq_secret_key_from_pqc_container_pem(char **algorithm_out, uint8_t **key_out
959
959
  }
960
960
 
961
961
  const char *pq_version(void) {
962
- return "0.3.0";
962
+ return "0.3.2";
963
963
  }
@@ -144,6 +144,22 @@ const char *pq_version(void);
144
144
  #define PQ_MLDSA_PUBLICKEYBYTES MLDSA_PUBLICKEYBYTES
145
145
  #define PQ_MLDSA_SECRETKEYBYTES MLDSA_SECRETKEYBYTES
146
146
  #define PQ_MLDSA_BYTES MLDSA_BYTES
147
+ #define PQ_MLDSA_MUBYTES 64
148
+ #define PQ_MLDSA_TRBYTES 64
149
+
150
+ int pq_mldsa_extract_tr_from_secret_key(uint8_t *tr_out, const uint8_t *secret_key);
151
+ int pq_mldsa_compute_tr_from_public_key(uint8_t *tr_out, const uint8_t *public_key);
152
+
153
+ int pq_sign_mu(uint8_t *signature, size_t *signature_len,
154
+ const uint8_t *mu, const uint8_t *secret_key);
155
+ int pq_verify_mu(const uint8_t *signature, size_t signature_len,
156
+ const uint8_t *mu, const uint8_t *public_key);
157
+ void *pq_mu_builder_new(void);
158
+ int pq_mu_builder_init(void *state, const uint8_t *tr,
159
+ const uint8_t *ctx, size_t ctxlen);
160
+ int pq_mu_builder_absorb(void *state, const uint8_t *chunk, size_t chunk_len);
161
+ int pq_mu_builder_finalize(void *state, uint8_t *mu_out);
162
+ void pq_mu_builder_release(void *state);
147
163
 
148
164
  int pq_hybrid_kem_keypair(uint8_t *public_key, uint8_t *secret_key);
149
165
  int pq_hybrid_kem_encapsulate(uint8_t *ciphertext, uint8_t *shared_secret,
@@ -0,0 +1,8 @@
1
+ KeccakP-1600-times4-SIMD256.o: KeccakP-1600-times4-SIMD256.c \
2
+ align.h brg_endian.h KeccakP-1600-times4-SnP.h \
3
+ KeccakP-1600-unrolling.macros SIMD256-config.h
4
+ $(CC) -O3 -mavx2 -c $< -o $@
5
+
6
+ .PHONY: clean
7
+ clean:
8
+ $(RM) KeccakP-1600-times4-SIMD256.o
@@ -0,0 +1,19 @@
1
+ # This Makefile can be used with GNU Make or BSD Make
2
+
3
+ LIB=libml-kem-768_clean.a
4
+ HEADERS=api.h cbd.h indcpa.h kem.h ntt.h params.h poly.h polyvec.h reduce.h symmetric.h verify.h
5
+ OBJECTS=cbd.o indcpa.o kem.o ntt.o poly.o polyvec.o reduce.o symmetric-shake.o verify.o
6
+
7
+ CFLAGS=-O3 -Wall -Wextra -Wpedantic -Werror -Wmissing-prototypes -Wredundant-decls -std=c99 -I../../../common $(EXTRAFLAGS)
8
+
9
+ all: $(LIB)
10
+
11
+ %.o: %.c $(HEADERS)
12
+ $(CC) $(CFLAGS) -c -o $@ $<
13
+
14
+ $(LIB): $(OBJECTS)
15
+ $(AR) -r $@ $(OBJECTS)
16
+
17
+ clean:
18
+ $(RM) $(OBJECTS)
19
+ $(RM) $(LIB)
@@ -0,0 +1,19 @@
1
+ # This Makefile can be used with GNU Make or BSD Make
2
+
3
+ LIB=libml-dsa-65_clean.a
4
+ HEADERS=api.h ntt.h packing.h params.h poly.h polyvec.h reduce.h rounding.h sign.h symmetric.h
5
+ OBJECTS=ntt.o packing.o poly.o polyvec.o reduce.o rounding.o sign.o symmetric-shake.o
6
+
7
+ CFLAGS=-O3 -Wall -Wextra -Wpedantic -Werror -Wmissing-prototypes -Wredundant-decls -std=c99 -I../../../common $(EXTRAFLAGS)
8
+
9
+ all: $(LIB)
10
+
11
+ %.o: %.c $(HEADERS)
12
+ $(CC) $(CFLAGS) -c -o $@ $<
13
+
14
+ $(LIB): $(OBJECTS)
15
+ $(AR) -r $@ $(OBJECTS)
16
+
17
+ clean:
18
+ $(RM) $(OBJECTS)
19
+ $(RM) $(LIB)
@@ -74,6 +74,95 @@ module PQCrypto
74
74
 
75
75
  raise UnsupportedAlgorithmError, "Unsupported signature algorithm: #{algorithm.inspect}"
76
76
  end
77
+
78
+ def _streaming_sign(secret_key, io, chunk_size, context)
79
+ validate_chunk_size!(chunk_size)
80
+ validate_context!(context)
81
+ validate_io!(io)
82
+
83
+ sk_bytes = secret_key.__send__(:bytes_for_native)
84
+ begin
85
+ tr = PQCrypto.__send__(:_native_mldsa_extract_tr, sk_bytes)
86
+ rescue ArgumentError => e
87
+ raise InvalidKeyError, e.message
88
+ end
89
+
90
+ builder = PQCrypto.__send__(:_native_mldsa_mu_builder_new, tr, context.b)
91
+ builder_consumed = false
92
+ mu = nil
93
+ begin
94
+ _drain_io_into_builder(io, builder, chunk_size)
95
+ mu = PQCrypto.__send__(:_native_mldsa_mu_builder_finalize, builder)
96
+ builder_consumed = true
97
+ PQCrypto.__send__(:_native_mldsa_sign_mu, mu, sk_bytes)
98
+ ensure
99
+ PQCrypto.__send__(:_native_mldsa_mu_builder_release, builder) unless builder_consumed
100
+ PQCrypto.secure_wipe(tr) if tr && !tr.frozen?
101
+ PQCrypto.secure_wipe(mu) if mu && !mu.frozen?
102
+ end
103
+ end
104
+
105
+ def _streaming_verify(public_key, io, signature, chunk_size, context)
106
+ validate_chunk_size!(chunk_size)
107
+ validate_context!(context)
108
+ validate_io!(io)
109
+
110
+ pk_bytes = public_key.__send__(:bytes_for_native)
111
+ begin
112
+ tr = PQCrypto.__send__(:_native_mldsa_compute_tr, pk_bytes)
113
+ rescue ArgumentError => e
114
+ raise InvalidKeyError, e.message
115
+ end
116
+
117
+ builder = PQCrypto.__send__(:_native_mldsa_mu_builder_new, tr, context.b)
118
+ builder_consumed = false
119
+ mu = nil
120
+ sig_bytes = String(signature).b
121
+ begin
122
+ _drain_io_into_builder(io, builder, chunk_size)
123
+ mu = PQCrypto.__send__(:_native_mldsa_mu_builder_finalize, builder)
124
+ builder_consumed = true
125
+ PQCrypto.__send__(:_native_mldsa_verify_mu, mu, sig_bytes, pk_bytes)
126
+ ensure
127
+ PQCrypto.__send__(:_native_mldsa_mu_builder_release, builder) unless builder_consumed
128
+
129
+ PQCrypto.secure_wipe(tr) if tr && !tr.frozen?
130
+ PQCrypto.secure_wipe(mu) if mu && !mu.frozen?
131
+ end
132
+ end
133
+
134
+ def _drain_io_into_builder(io, builder, chunk_size)
135
+ buffer = String.new(capacity: chunk_size).b
136
+ loop do
137
+ result = io.read(chunk_size, buffer)
138
+ break if result.nil?
139
+
140
+ chunk = result.equal?(buffer) ? buffer : result
141
+ chunk_bytes = chunk.encoding == Encoding::BINARY ? chunk : chunk.b
142
+ break if chunk_bytes.bytesize.zero?
143
+
144
+ PQCrypto.__send__(:_native_mldsa_mu_builder_update, builder, chunk_bytes)
145
+ end
146
+ end
147
+
148
+ def validate_io!(io)
149
+ unless io.respond_to?(:read)
150
+ raise ArgumentError, "io must respond to #read"
151
+ end
152
+ end
153
+
154
+ def validate_chunk_size!(chunk_size)
155
+ unless chunk_size.is_a?(Integer) && chunk_size > 0
156
+ raise ArgumentError, "chunk_size must be a positive Integer"
157
+ end
158
+ end
159
+
160
+ def validate_context!(context)
161
+ ctx = String(context).b
162
+ if ctx.bytesize > 255
163
+ raise ArgumentError, "context must be at most 255 bytes (FIPS 204)"
164
+ end
165
+ end
77
166
  end
78
167
 
79
168
  class Keypair
@@ -125,6 +214,17 @@ module PQCrypto
125
214
  true
126
215
  end
127
216
 
217
+ def verify_io(io, signature, chunk_size: 1 << 20, context: "".b)
218
+ Signature.send(:_streaming_verify, self, io, signature, chunk_size, context)
219
+ end
220
+
221
+ def verify_io!(io, signature, chunk_size: 1 << 20, context: "".b)
222
+ unless verify_io(io, signature, chunk_size: chunk_size, context: context)
223
+ raise PQCrypto::VerificationError, "Verification failed"
224
+ end
225
+ true
226
+ end
227
+
128
228
  def ==(other)
129
229
  return false unless other.is_a?(PublicKey) && other.algorithm == algorithm
130
230
  PQCrypto.__send__(:native_ct_equals, other.to_bytes, @bytes)
@@ -142,6 +242,10 @@ module PQCrypto
142
242
 
143
243
  private
144
244
 
245
+ def bytes_for_native
246
+ @bytes
247
+ end
248
+
145
249
  def validate_length!
146
250
  expected = Signature.details(@algorithm).fetch(:public_key_bytes)
147
251
  raise InvalidKeyError, "Invalid signature public key length" unless @bytes.bytesize == expected
@@ -175,6 +279,10 @@ module PQCrypto
175
279
  raise InvalidKeyError, e.message
176
280
  end
177
281
 
282
+ def sign_io(io, chunk_size: 1 << 20, context: "".b)
283
+ Signature.send(:_streaming_sign, self, io, chunk_size, context)
284
+ end
285
+
178
286
  def wipe!
179
287
  PQCrypto.secure_wipe(@bytes)
180
288
  self
@@ -197,6 +305,10 @@ module PQCrypto
197
305
 
198
306
  private
199
307
 
308
+ def bytes_for_native
309
+ @bytes
310
+ end
311
+
200
312
  def validate_length!
201
313
  expected = Signature.details(@algorithm).fetch(:secret_key_bytes)
202
314
  raise InvalidKeyError, "Invalid signature secret key length" unless @bytes.bytesize == expected
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PQCrypto
4
- VERSION = "0.3.1"
4
+ VERSION = "0.3.2"
5
5
  end
data/lib/pq_crypto.rb CHANGED
@@ -69,6 +69,17 @@ module PQCrypto
69
69
  __test_sign_from_seed
70
70
  ].freeze
71
71
 
72
+ EXTERNAL_MU_METHODS = %i[
73
+ _native_mldsa_extract_tr
74
+ _native_mldsa_compute_tr
75
+ _native_mldsa_mu_builder_new
76
+ _native_mldsa_mu_builder_update
77
+ _native_mldsa_mu_builder_finalize
78
+ _native_mldsa_mu_builder_release
79
+ _native_mldsa_sign_mu
80
+ _native_mldsa_verify_mu
81
+ ].freeze
82
+
72
83
  class << PQCrypto
73
84
  NativeBindings::NATIVE_METHODS.each do |name|
74
85
  alias_name = :"native_#{name.to_s.sub(/\A__/, '')}"
@@ -78,6 +89,7 @@ module PQCrypto
78
89
 
79
90
  private(*NativeBindings::NATIVE_METHODS)
80
91
  private(*NativeBindings::NATIVE_METHODS.map { |n| :"native_#{n.to_s.sub(/\A__/, '')}" })
92
+ private(*NativeBindings::EXTERNAL_MU_METHODS)
81
93
  end
82
94
  end
83
95
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pq_crypto
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Haydarov
@@ -69,6 +69,7 @@ files:
69
69
  - ext/pqcrypto/extconf.rb
70
70
  - ext/pqcrypto/mldsa_api.h
71
71
  - ext/pqcrypto/mlkem_api.h
72
+ - ext/pqcrypto/pq_externalmu.c
72
73
  - ext/pqcrypto/pq_randombytes.c
73
74
  - ext/pqcrypto/pqcrypto_ruby_secure.c
74
75
  - ext/pqcrypto/pqcrypto_secure.c
@@ -86,6 +87,7 @@ files:
86
87
  - ext/pqcrypto/vendor/pqclean/common/keccak4x/KeccakP-1600-times4-SIMD256.c
87
88
  - ext/pqcrypto/vendor/pqclean/common/keccak4x/KeccakP-1600-times4-SnP.h
88
89
  - ext/pqcrypto/vendor/pqclean/common/keccak4x/KeccakP-1600-unrolling.macros
90
+ - ext/pqcrypto/vendor/pqclean/common/keccak4x/Makefile
89
91
  - ext/pqcrypto/vendor/pqclean/common/keccak4x/Makefile.Microsoft_nmake
90
92
  - ext/pqcrypto/vendor/pqclean/common/keccak4x/SIMD256-config.h
91
93
  - ext/pqcrypto/vendor/pqclean/common/keccak4x/align.h
@@ -99,6 +101,7 @@ files:
99
101
  - ext/pqcrypto/vendor/pqclean/common/sp800-185.c
100
102
  - ext/pqcrypto/vendor/pqclean/common/sp800-185.h
101
103
  - ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/LICENSE
104
+ - ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/Makefile
102
105
  - ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/Makefile.Microsoft_nmake
103
106
  - ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/api.h
104
107
  - ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/cbd.c
@@ -121,6 +124,7 @@ files:
121
124
  - ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/verify.c
122
125
  - ext/pqcrypto/vendor/pqclean/crypto_kem/ml-kem-768/clean/verify.h
123
126
  - ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/LICENSE
127
+ - ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/Makefile
124
128
  - ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/Makefile.Microsoft_nmake
125
129
  - ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/api.h
126
130
  - ext/pqcrypto/vendor/pqclean/crypto_sign/ml-dsa-65/clean/ntt.c