pq_crypto 0.3.0 → 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.
@@ -99,6 +99,32 @@ cleanup:
99
99
  return ret;
100
100
  }
101
101
 
102
+ static int x25519_public_from_private(uint8_t *pk, const uint8_t *sk) {
103
+ EVP_PKEY *pkey = NULL;
104
+ size_t pklen = X25519_PUBLICKEYBYTES;
105
+ int ret = PQ_ERROR_KEYPAIR;
106
+
107
+ if (!pk || !sk) {
108
+ return PQ_ERROR_BUFFER;
109
+ }
110
+
111
+ pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, sk, X25519_SECRETKEYBYTES);
112
+ if (!pkey)
113
+ goto cleanup;
114
+
115
+ if (EVP_PKEY_get_raw_public_key(pkey, pk, &pklen) <= 0)
116
+ goto cleanup;
117
+ if (pklen != X25519_PUBLICKEYBYTES)
118
+ goto cleanup;
119
+
120
+ ret = PQ_SUCCESS;
121
+
122
+ cleanup:
123
+ if (pkey)
124
+ EVP_PKEY_free(pkey);
125
+ return ret;
126
+ }
127
+
102
128
  static int x25519_shared_secret(uint8_t *shared, const uint8_t *their_pk, const uint8_t *my_sk) {
103
129
  EVP_PKEY_CTX *ctx = NULL;
104
130
  EVP_PKEY *pkey = NULL;
@@ -159,8 +185,6 @@ static int xwing_combiner(uint8_t shared_secret[HYBRID_SHAREDSECRETBYTES],
159
185
 
160
186
  if (EVP_DigestInit_ex(ctx, EVP_sha3_256(), NULL) != 1)
161
187
  goto cleanup;
162
- if (EVP_DigestUpdate(ctx, XWING_LABEL, sizeof(XWING_LABEL)) != 1)
163
- goto cleanup;
164
188
  if (EVP_DigestUpdate(ctx, ss_M, MLKEM_SHAREDSECRETBYTES) != 1)
165
189
  goto cleanup;
166
190
  if (EVP_DigestUpdate(ctx, ss_X, X25519_SHAREDSECRETBYTES) != 1)
@@ -169,6 +193,8 @@ static int xwing_combiner(uint8_t shared_secret[HYBRID_SHAREDSECRETBYTES],
169
193
  goto cleanup;
170
194
  if (EVP_DigestUpdate(ctx, pk_X, X25519_PUBLICKEYBYTES) != 1)
171
195
  goto cleanup;
196
+ if (EVP_DigestUpdate(ctx, XWING_LABEL, sizeof(XWING_LABEL)) != 1)
197
+ goto cleanup;
172
198
  if (EVP_DigestFinal_ex(ctx, shared_secret, &out_len) != 1)
173
199
  goto cleanup;
174
200
  if (out_len != HYBRID_SHAREDSECRETBYTES)
@@ -181,6 +207,55 @@ cleanup:
181
207
  return ret;
182
208
  }
183
209
 
210
+ static int xwing_expand_secret_key(hybrid_expanded_secret_key_t *expanded_key,
211
+ const uint8_t seed[HYBRID_SECRETKEYBYTES]) {
212
+ EVP_MD_CTX *ctx = NULL;
213
+ uint8_t expanded[XWING_EXPANDEDBYTES];
214
+ int ret = PQ_ERROR_OPENSSL;
215
+
216
+ if (!expanded_key || !seed) {
217
+ return PQ_ERROR_BUFFER;
218
+ }
219
+
220
+ memset(expanded_key, 0, sizeof(*expanded_key));
221
+ memset(expanded, 0, sizeof(expanded));
222
+
223
+ ctx = EVP_MD_CTX_new();
224
+ if (!ctx)
225
+ goto cleanup;
226
+
227
+ if (EVP_DigestInit_ex(ctx, EVP_shake256(), NULL) != 1)
228
+ goto cleanup;
229
+ if (EVP_DigestUpdate(ctx, seed, HYBRID_SECRETKEYBYTES) != 1)
230
+ goto cleanup;
231
+ if (EVP_DigestFinalXOF(ctx, expanded, sizeof(expanded)) != 1)
232
+ goto cleanup;
233
+
234
+ ret = PQCLEAN_MLKEM768_CLEAN_crypto_kem_keypair_derand(expanded_key->mlkem_pk,
235
+ expanded_key->mlkem_sk, expanded);
236
+ if (ret != 0) {
237
+ ret = PQ_ERROR_KEYPAIR;
238
+ goto cleanup;
239
+ }
240
+
241
+ memcpy(expanded_key->x25519_sk, expanded + 64, X25519_SECRETKEYBYTES);
242
+ ret = x25519_public_from_private(expanded_key->x25519_pk, expanded_key->x25519_sk);
243
+ if (ret != PQ_SUCCESS) {
244
+ goto cleanup;
245
+ }
246
+
247
+ ret = PQ_SUCCESS;
248
+
249
+ cleanup:
250
+ if (ctx)
251
+ EVP_MD_CTX_free(ctx);
252
+ pq_secure_wipe(expanded, sizeof(expanded));
253
+ if (ret != PQ_SUCCESS && expanded_key) {
254
+ pq_secure_wipe(expanded_key, sizeof(*expanded_key));
255
+ }
256
+ return ret;
257
+ }
258
+
184
259
  int pq_mlkem_keypair(uint8_t *pk, uint8_t *sk) {
185
260
  return PQCLEAN_MLKEM768_CLEAN_crypto_kem_keypair(pk, sk) == 0 ? PQ_SUCCESS : PQ_ERROR_KEYPAIR;
186
261
  }
@@ -278,35 +353,36 @@ int pq_testing_mldsa_sign_from_seed(uint8_t *signature, size_t *signature_len,
278
353
 
279
354
  int pq_hybrid_kem_keypair(uint8_t *public_key, uint8_t *secret_key) {
280
355
  hybrid_public_key_t pk;
281
- hybrid_secret_key_t sk;
282
- int ret;
356
+ hybrid_expanded_secret_key_t expanded;
357
+ uint8_t seed[HYBRID_SECRETKEYBYTES];
358
+ int ret = PQ_SUCCESS;
283
359
 
284
360
  if (!public_key || !secret_key) {
285
361
  return PQ_ERROR_BUFFER;
286
362
  }
287
363
 
288
364
  memset(&pk, 0, sizeof(pk));
289
- memset(&sk, 0, sizeof(sk));
365
+ memset(&expanded, 0, sizeof(expanded));
366
+ memset(seed, 0, sizeof(seed));
290
367
 
291
- ret = PQCLEAN_MLKEM768_CLEAN_crypto_kem_keypair(pk.mlkem_pk, sk.mlkem_sk) == 0
292
- ? PQ_SUCCESS
293
- : PQ_ERROR_KEYPAIR;
294
- if (ret != PQ_SUCCESS) {
368
+ if (RAND_bytes(seed, sizeof(seed)) != 1) {
369
+ ret = PQ_ERROR_RANDOM;
295
370
  goto cleanup;
296
371
  }
297
372
 
298
- ret = x25519_keypair(pk.x25519_pk, sk.x25519_sk);
373
+ ret = xwing_expand_secret_key(&expanded, seed);
299
374
  if (ret != PQ_SUCCESS) {
300
375
  goto cleanup;
301
376
  }
302
377
 
378
+ memcpy(pk.mlkem_pk, expanded.mlkem_pk, MLKEM_PUBLICKEYBYTES);
379
+ memcpy(pk.x25519_pk, expanded.x25519_pk, X25519_PUBLICKEYBYTES);
303
380
  memcpy(public_key, &pk, HYBRID_PUBLICKEYBYTES);
304
- memcpy(secret_key, &sk, HYBRID_SECRETKEYBYTES);
381
+ memcpy(secret_key, seed, HYBRID_SECRETKEYBYTES);
305
382
 
306
383
  cleanup:
307
- pq_secure_wipe(&sk, sizeof(sk));
308
-
309
- pq_secure_wipe(&pk, sizeof(pk));
384
+ pq_secure_wipe(seed, sizeof(seed));
385
+ pq_secure_wipe(&expanded, sizeof(expanded));
310
386
  return ret;
311
387
  }
312
388
 
@@ -357,20 +433,15 @@ cleanup:
357
433
  pq_secure_wipe(mlkem_ss, sizeof(mlkem_ss));
358
434
  pq_secure_wipe(x25519_ss, sizeof(x25519_ss));
359
435
  pq_secure_wipe(x25519_ephemeral_sk, sizeof(x25519_ephemeral_sk));
360
- pq_secure_wipe(&pk, sizeof(pk));
361
- pq_secure_wipe(&ct, sizeof(ct));
362
436
  return ret;
363
437
  }
364
438
 
365
439
  int pq_hybrid_kem_decapsulate(uint8_t *shared_secret, const uint8_t *ciphertext,
366
440
  const uint8_t *secret_key) {
367
441
  hybrid_ciphertext_t ct;
368
- hybrid_secret_key_t sk;
369
- uint8_t recipient_x25519_pk[X25519_PUBLICKEYBYTES];
442
+ hybrid_expanded_secret_key_t expanded;
370
443
  uint8_t mlkem_ss[MLKEM_SHAREDSECRETBYTES];
371
444
  uint8_t x25519_ss[X25519_SHAREDSECRETBYTES];
372
- EVP_PKEY *pkey = NULL;
373
- size_t pklen = X25519_PUBLICKEYBYTES;
374
445
  int ret = PQ_SUCCESS;
375
446
 
376
447
  if (!shared_secret || !ciphertext || !secret_key) {
@@ -378,44 +449,34 @@ int pq_hybrid_kem_decapsulate(uint8_t *shared_secret, const uint8_t *ciphertext,
378
449
  }
379
450
 
380
451
  memcpy(&ct, ciphertext, HYBRID_CIPHERTEXTBYTES);
381
- memcpy(&sk, secret_key, HYBRID_SECRETKEYBYTES);
382
- memset(recipient_x25519_pk, 0, sizeof(recipient_x25519_pk));
452
+ memset(&expanded, 0, sizeof(expanded));
383
453
  memset(mlkem_ss, 0, sizeof(mlkem_ss));
384
454
  memset(x25519_ss, 0, sizeof(x25519_ss));
385
455
 
386
- if (PQCLEAN_MLKEM768_CLEAN_crypto_kem_dec(mlkem_ss, ct.mlkem_ct, sk.mlkem_sk) != 0) {
387
- ret = PQ_ERROR_DECAPSULATE;
388
- goto cleanup;
389
- }
390
-
391
- ret = x25519_shared_secret(x25519_ss, ct.x25519_ephemeral, sk.x25519_sk);
456
+ ret = xwing_expand_secret_key(&expanded, secret_key);
392
457
  if (ret != PQ_SUCCESS) {
393
458
  ret = PQ_ERROR_DECAPSULATE;
394
459
  goto cleanup;
395
460
  }
396
461
 
397
- pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, sk.x25519_sk, X25519_SECRETKEYBYTES);
398
- if (!pkey) {
462
+ if (PQCLEAN_MLKEM768_CLEAN_crypto_kem_dec(mlkem_ss, ct.mlkem_ct, expanded.mlkem_sk) != 0) {
399
463
  ret = PQ_ERROR_DECAPSULATE;
400
464
  goto cleanup;
401
465
  }
402
- if (EVP_PKEY_get_raw_public_key(pkey, recipient_x25519_pk, &pklen) <= 0 ||
403
- pklen != X25519_PUBLICKEYBYTES) {
466
+
467
+ ret = x25519_shared_secret(x25519_ss, ct.x25519_ephemeral, expanded.x25519_sk);
468
+ if (ret != PQ_SUCCESS) {
404
469
  ret = PQ_ERROR_DECAPSULATE;
405
470
  goto cleanup;
406
471
  }
407
472
 
408
- ret = xwing_combiner(shared_secret, mlkem_ss, x25519_ss, ct.x25519_ephemeral,
409
- recipient_x25519_pk);
473
+ ret =
474
+ xwing_combiner(shared_secret, mlkem_ss, x25519_ss, ct.x25519_ephemeral, expanded.x25519_pk);
410
475
 
411
476
  cleanup:
412
- if (pkey)
413
- EVP_PKEY_free(pkey);
414
- pq_secure_wipe(recipient_x25519_pk, sizeof(recipient_x25519_pk));
415
477
  pq_secure_wipe(mlkem_ss, sizeof(mlkem_ss));
416
478
  pq_secure_wipe(x25519_ss, sizeof(x25519_ss));
417
- pq_secure_wipe(&ct, sizeof(ct));
418
- pq_secure_wipe(&sk, sizeof(sk));
479
+ pq_secure_wipe(&expanded, sizeof(expanded));
419
480
  return ret;
420
481
  }
421
482
 
@@ -429,8 +490,7 @@ cleanup:
429
490
 
430
491
  static const char PQC_OID_ML_KEM_768[] = "2.25.186599352125448088867056807454444238446";
431
492
 
432
- static const char PQC_OID_ML_KEM_768_X25519_XWING[] =
433
- "2.25.318532651283923671095712569430174917109";
493
+ static const char PQC_OID_ML_KEM_768_X25519_XWING[] = "1.3.6.1.4.1.62253.25722";
434
494
  static const char PQC_OID_ML_DSA_65[] = "2.25.305232938483772195555080795650659207792";
435
495
  static const char PQC_PUBLIC_KEY_PEM_LABEL[] = "PQC PUBLIC KEY CONTAINER";
436
496
  static const char PQC_PRIVATE_KEY_PEM_LABEL[] = "PQC PRIVATE KEY CONTAINER";
@@ -899,5 +959,5 @@ int pq_secret_key_from_pqc_container_pem(char **algorithm_out, uint8_t **key_out
899
959
  }
900
960
 
901
961
  const char *pq_version(void) {
902
- return "0.3.0";
962
+ return "0.3.2";
903
963
  }
@@ -23,9 +23,11 @@
23
23
  #define X25519_PUBLICKEYBYTES 32
24
24
  #define X25519_SECRETKEYBYTES 32
25
25
  #define X25519_SHAREDSECRETBYTES 32
26
+ #define XWING_SEEDBYTES 32
27
+ #define XWING_EXPANDEDBYTES 96
26
28
 
27
29
  #define HYBRID_PUBLICKEYBYTES (MLKEM_PUBLICKEYBYTES + X25519_PUBLICKEYBYTES)
28
- #define HYBRID_SECRETKEYBYTES (MLKEM_SECRETKEYBYTES + X25519_SECRETKEYBYTES)
30
+ #define HYBRID_SECRETKEYBYTES XWING_SEEDBYTES
29
31
  #define HYBRID_CIPHERTEXTBYTES (MLKEM_CIPHERTEXTBYTES + X25519_PUBLICKEYBYTES)
30
32
  #define HYBRID_SHAREDSECRETBYTES 32
31
33
 
@@ -48,10 +50,16 @@ typedef struct {
48
50
  uint8_t x25519_pk[X25519_PUBLICKEYBYTES];
49
51
  } hybrid_public_key_t;
50
52
 
53
+ typedef struct {
54
+ uint8_t seed[XWING_SEEDBYTES];
55
+ } hybrid_secret_key_t;
56
+
51
57
  typedef struct {
52
58
  uint8_t mlkem_sk[MLKEM_SECRETKEYBYTES];
53
59
  uint8_t x25519_sk[X25519_SECRETKEYBYTES];
54
- } hybrid_secret_key_t;
60
+ uint8_t mlkem_pk[MLKEM_PUBLICKEYBYTES];
61
+ uint8_t x25519_pk[X25519_PUBLICKEYBYTES];
62
+ } hybrid_expanded_secret_key_t;
55
63
 
56
64
  typedef struct {
57
65
  uint8_t mlkem_ct[MLKEM_CIPHERTEXTBYTES];
@@ -136,6 +144,22 @@ const char *pq_version(void);
136
144
  #define PQ_MLDSA_PUBLICKEYBYTES MLDSA_PUBLICKEYBYTES
137
145
  #define PQ_MLDSA_SECRETKEYBYTES MLDSA_SECRETKEYBYTES
138
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);
139
163
 
140
164
  int pq_hybrid_kem_keypair(uint8_t *public_key, uint8_t *secret_key);
141
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)
data/lib/pq_crypto/kem.rb CHANGED
@@ -184,11 +184,11 @@ module PQCrypto
184
184
  alias eql? ==
185
185
 
186
186
  def hash
187
- fingerprint.hash
187
+ object_id.hash
188
188
  end
189
189
 
190
- def fingerprint
191
- Digest::SHA256.digest(@bytes)
190
+ def inspect
191
+ "#<#{self.class}:0x#{object_id.to_s(16)} algorithm=#{algorithm.inspect}>"
192
192
  end
193
193
 
194
194
  private
@@ -206,6 +206,10 @@ module PQCrypto
206
206
  @ciphertext = String(ciphertext).b
207
207
  @shared_secret = String(shared_secret).b
208
208
  end
209
+
210
+ def inspect
211
+ "#<#{self.class}:0x#{object_id.to_s(16)} ciphertext_bytes=#{@ciphertext.bytesize} shared_secret_bytes=#{@shared_secret.bytesize}>"
212
+ end
209
213
  end
210
214
  end
211
215
  end
@@ -9,7 +9,7 @@ module PQCrypto
9
9
  }.freeze,
10
10
  ml_kem_768_x25519_xwing: {
11
11
  family: :ml_kem_hybrid,
12
- oid: "2.25.318532651283923671095712569430174917109",
12
+ oid: "1.3.6.1.4.1.62253.25722",
13
13
  }.freeze,
14
14
  ml_dsa_65: {
15
15
  family: :ml_dsa,
@@ -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
@@ -188,15 +296,19 @@ module PQCrypto
188
296
  alias eql? ==
189
297
 
190
298
  def hash
191
- fingerprint.hash
299
+ object_id.hash
192
300
  end
193
301
 
194
- def fingerprint
195
- Digest::SHA256.digest(@bytes)
302
+ def inspect
303
+ "#<#{self.class}:0x#{object_id.to_s(16)} algorithm=#{algorithm.inspect}>"
196
304
  end
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.0"
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,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pq_crypto
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Haydarov
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-04-24 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rake
@@ -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
@@ -163,14 +167,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
163
167
  requirements:
164
168
  - - ">="
165
169
  - !ruby/object:Gem::Version
166
- version: 3.4.0.a
170
+ version: 3.4.0
167
171
  required_rubygems_version: !ruby/object:Gem::Requirement
168
172
  requirements:
169
173
  - - ">="
170
174
  - !ruby/object:Gem::Version
171
175
  version: '0'
172
176
  requirements: []
173
- rubygems_version: 3.6.2
177
+ rubygems_version: 3.6.7
174
178
  specification_version: 4
175
179
  summary: Primitive-first post-quantum cryptography for Ruby
176
180
  test_files: []