pq_crypto 0.5.3 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,6 +46,10 @@ module PQCrypto
46
46
  SecretKey.new(algorithm, bytes)
47
47
  end
48
48
 
49
+ def secret_key_from_seed(algorithm, seed)
50
+ SecretKey.from_seed(resolve_algorithm!(algorithm), seed)
51
+ end
52
+
49
53
  def public_key_from_pqc_container_der(der, algorithm = nil)
50
54
  resolved_algorithm, bytes = Serialization.public_key_from_pqc_container_der(algorithm, der)
51
55
  resolve_algorithm!(resolved_algorithm)
@@ -82,12 +86,12 @@ module PQCrypto
82
86
  PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
83
87
  end
84
88
 
85
- def secret_key_from_pkcs8_der(der)
86
- secret_key_from_decoded_pkcs8(*PKCS8.decode_der(der))
89
+ def secret_key_from_pkcs8_der(der, passphrase: nil)
90
+ secret_key_from_decoded_pkcs8(*PKCS8.decode_der(der, passphrase: passphrase))
87
91
  end
88
92
 
89
- def secret_key_from_pkcs8_pem(pem)
90
- secret_key_from_decoded_pkcs8(*PKCS8.decode_pem(pem))
93
+ def secret_key_from_pkcs8_pem(pem, passphrase: nil)
94
+ secret_key_from_decoded_pkcs8(*PKCS8.decode_pem(pem, passphrase: passphrase))
91
95
  end
92
96
 
93
97
  def details(algorithm)
@@ -114,10 +118,10 @@ module PQCrypto
114
118
  SecretKey.new(algorithm, material)
115
119
  when :seed
116
120
  _public_key, expanded = PQCrypto.__send__(native_method_for(algorithm, :keypair_from_seed), material)
117
- SecretKey.new(algorithm, expanded)
121
+ SecretKey.new(algorithm, expanded, seed: material)
118
122
  when :both
119
123
  _seed, expanded = material
120
- SecretKey.new(algorithm, expanded)
124
+ SecretKey.new(algorithm, expanded, seed: _seed)
121
125
  else
122
126
  raise SerializationError, "Unsupported ML-DSA PKCS#8 private key format: #{format.inspect}"
123
127
  end
@@ -145,9 +149,10 @@ module PQCrypto
145
149
  context = validate_context!(context)
146
150
  validate_io!(io)
147
151
 
152
+ algorithm = secret_key.algorithm
148
153
  sk_bytes = secret_key.__send__(:bytes_for_native)
149
154
  begin
150
- tr = PQCrypto.__send__(:_native_mldsa_extract_tr, sk_bytes)
155
+ tr = PQCrypto.__send__(:_native_mldsa_extract_tr, algorithm, sk_bytes)
151
156
  rescue ArgumentError => e
152
157
  raise InvalidKeyError, e.message
153
158
  end
@@ -159,7 +164,7 @@ module PQCrypto
159
164
  _drain_io_into_builder(io, builder, chunk_size)
160
165
  mu = PQCrypto.__send__(:_native_mldsa_mu_builder_finalize, builder)
161
166
  builder_consumed = true
162
- PQCrypto.__send__(:_native_mldsa_sign_mu, mu, sk_bytes)
167
+ PQCrypto.__send__(:_native_mldsa_sign_mu, algorithm, mu, sk_bytes)
163
168
  ensure
164
169
  PQCrypto.__send__(:_native_mldsa_mu_builder_release, builder) unless builder_consumed
165
170
  PQCrypto.secure_wipe(tr) if tr && !tr.frozen?
@@ -173,9 +178,10 @@ module PQCrypto
173
178
  context = validate_context!(context)
174
179
  validate_io!(io)
175
180
 
181
+ algorithm = public_key.algorithm
176
182
  pk_bytes = public_key.__send__(:bytes_for_native)
177
183
  begin
178
- tr = PQCrypto.__send__(:_native_mldsa_compute_tr, pk_bytes)
184
+ tr = PQCrypto.__send__(:_native_mldsa_compute_tr, algorithm, pk_bytes)
179
185
  rescue ArgumentError => e
180
186
  raise InvalidKeyError, e.message
181
187
  end
@@ -183,12 +189,12 @@ module PQCrypto
183
189
  builder = PQCrypto.__send__(:_native_mldsa_mu_builder_new, tr, context)
184
190
  builder_consumed = false
185
191
  mu = nil
186
- sig_bytes = String(signature).b
192
+ sig_bytes = Internal.binary_string(signature)
187
193
  begin
188
194
  _drain_io_into_builder(io, builder, chunk_size)
189
195
  mu = PQCrypto.__send__(:_native_mldsa_mu_builder_finalize, builder)
190
196
  builder_consumed = true
191
- PQCrypto.__send__(:_native_mldsa_verify_mu, mu, sig_bytes, pk_bytes)
197
+ PQCrypto.__send__(:_native_mldsa_verify_mu, algorithm, mu, sig_bytes, pk_bytes)
192
198
  ensure
193
199
  PQCrypto.__send__(:_native_mldsa_mu_builder_release, builder) unless builder_consumed
194
200
 
@@ -224,7 +230,7 @@ module PQCrypto
224
230
  end
225
231
 
226
232
  def validate_context!(context)
227
- ctx = String(context).b
233
+ ctx = Internal.binary_string(context)
228
234
  if ctx.bytesize > 255
229
235
  raise ArgumentError, "context must be at most 255 bytes (FIPS 204)"
230
236
  end
@@ -232,10 +238,7 @@ module PQCrypto
232
238
  end
233
239
 
234
240
  def validate_streaming_algorithm!(algorithm)
235
- return if resolve_algorithm!(algorithm) == CANONICAL_ALGORITHM
236
-
237
- raise UnsupportedAlgorithmError,
238
- "Streaming sign_io/verify_io currently supports only #{CANONICAL_ALGORITHM.inspect}"
241
+ resolve_algorithm!(algorithm)
239
242
  end
240
243
  end
241
244
 
@@ -261,7 +264,7 @@ module PQCrypto
261
264
 
262
265
  def initialize(algorithm, bytes)
263
266
  @algorithm = algorithm
264
- @bytes = String(bytes).b
267
+ @bytes = Internal.binary_string(bytes)
265
268
  validate_length!
266
269
  end
267
270
 
@@ -285,14 +288,17 @@ module PQCrypto
285
288
  SPKI.encode_pem(@algorithm, @bytes)
286
289
  end
287
290
 
288
- def verify(message, signature)
289
- PQCrypto.__send__(Signature.send(:native_method_for, @algorithm, :verify), String(message).b, String(signature).b, @bytes)
290
- rescue ArgumentError => e
291
- raise InvalidKeyError, e.message
291
+ def verify(message, signature, context: "".b)
292
+ context = Signature.send(:validate_context!, context)
293
+ begin
294
+ PQCrypto.__send__(Signature.send(:native_method_for, @algorithm, :verify), Internal.binary_string(message), Internal.binary_string(signature), @bytes, context)
295
+ rescue ArgumentError => e
296
+ raise InvalidKeyError, e.message
297
+ end
292
298
  end
293
299
 
294
- def verify!(message, signature)
295
- raise PQCrypto::VerificationError, "Verification failed" unless verify(message, signature)
300
+ def verify!(message, signature, context: "".b)
301
+ raise PQCrypto::VerificationError, "Verification failed" unless verify(message, signature, context: context)
296
302
  true
297
303
  end
298
304
 
@@ -309,7 +315,7 @@ module PQCrypto
309
315
 
310
316
  def ==(other)
311
317
  return false unless other.is_a?(PublicKey) && other.algorithm == algorithm
312
- PQCrypto.__send__(:native_ct_equals, other.to_bytes, @bytes)
318
+ Internal.constant_time_equal?(other.send(:bytes_for_native), @bytes)
313
319
  end
314
320
 
315
321
  alias eql? ==
@@ -337,10 +343,20 @@ module PQCrypto
337
343
  class SecretKey
338
344
  attr_reader :algorithm
339
345
 
340
- def initialize(algorithm, bytes)
346
+ def initialize(algorithm, bytes, seed: nil)
341
347
  @algorithm = algorithm
342
- @bytes = String(bytes).b
348
+ @bytes = Internal.binary_string(bytes)
349
+ @seed = seed.nil? ? nil : Internal.binary_string(seed)
343
350
  validate_length!
351
+ validate_seed_length! if @seed
352
+ end
353
+
354
+ def self.from_seed(algorithm, seed)
355
+ seed_bytes = Internal.binary_string(seed)
356
+ _public_key, expanded = PQCrypto.__send__(Signature.send(:native_method_for, algorithm, :keypair_from_seed), seed_bytes)
357
+ new(algorithm, expanded, seed: seed_bytes)
358
+ rescue ArgumentError => e
359
+ raise InvalidKeyError, e.message
344
360
  end
345
361
 
346
362
  def to_bytes
@@ -355,34 +371,21 @@ module PQCrypto
355
371
  Serialization.secret_key_to_pqc_container_pem(@algorithm, @bytes)
356
372
  end
357
373
 
358
- def to_pkcs8_der(format: :expanded)
359
- case format
360
- when :expanded
361
- PKCS8.encode_der(@algorithm, @bytes, format: :expanded)
362
- when :seed, :both
363
- raise SerializationError,
364
- "ML-DSA seed/both PKCS#8 export requires original seed material; use PQCrypto::PKCS8.encode_der/encode_pem directly"
365
- else
366
- raise SerializationError, "Unsupported PKCS#8 private key format: #{format.inspect}"
367
- end
374
+ def to_pkcs8_der(format: :expanded, passphrase: nil, iterations: PKCS8::ENCRYPTED_PKCS8_DEFAULT_ITERATIONS)
375
+ PKCS8.encode_der(@algorithm, pkcs8_material(format), format: format, passphrase: passphrase, iterations: iterations)
368
376
  end
369
377
 
370
- def to_pkcs8_pem(format: :expanded)
371
- case format
372
- when :expanded
373
- PKCS8.encode_pem(@algorithm, @bytes, format: :expanded)
374
- when :seed, :both
375
- raise SerializationError,
376
- "ML-DSA seed/both PKCS#8 export requires original seed material; use PQCrypto::PKCS8.encode_der/encode_pem directly"
377
- else
378
- raise SerializationError, "Unsupported PKCS#8 private key format: #{format.inspect}"
379
- end
378
+ def to_pkcs8_pem(format: :expanded, passphrase: nil, iterations: PKCS8::ENCRYPTED_PKCS8_DEFAULT_ITERATIONS)
379
+ PKCS8.encode_pem(@algorithm, pkcs8_material(format), format: format, passphrase: passphrase, iterations: iterations)
380
380
  end
381
381
 
382
- def sign(message)
383
- PQCrypto.__send__(Signature.send(:native_method_for, @algorithm, :sign), String(message).b, @bytes)
384
- rescue ArgumentError => e
385
- raise InvalidKeyError, e.message
382
+ def sign(message, context: "".b)
383
+ context = Signature.send(:validate_context!, context)
384
+ begin
385
+ PQCrypto.__send__(Signature.send(:native_method_for, @algorithm, :sign), Internal.binary_string(message), @bytes, context)
386
+ rescue ArgumentError => e
387
+ raise InvalidKeyError, e.message
388
+ end
386
389
  end
387
390
 
388
391
  def sign_io(io, chunk_size: 1 << 20, context: "".b)
@@ -391,12 +394,13 @@ module PQCrypto
391
394
 
392
395
  def wipe!
393
396
  PQCrypto.secure_wipe(@bytes)
397
+ PQCrypto.secure_wipe(@seed) if @seed
394
398
  self
395
399
  end
396
400
 
397
401
  def ==(other)
398
402
  return false unless other.is_a?(SecretKey) && other.algorithm == algorithm
399
- PQCrypto.__send__(:native_ct_equals, other.to_bytes, @bytes)
403
+ Internal.constant_time_equal?(other.send(:bytes_for_native), @bytes)
400
404
  end
401
405
 
402
406
  alias eql? ==
@@ -419,6 +423,32 @@ module PQCrypto
419
423
  expected = Signature.details(@algorithm).fetch(:secret_key_bytes)
420
424
  raise InvalidKeyError, "Invalid signature secret key length" unless @bytes.bytesize == expected
421
425
  end
426
+
427
+ def pkcs8_material(format)
428
+ case format
429
+ when :expanded
430
+ @bytes
431
+ when :seed
432
+ ensure_seed_available!(format)
433
+ @seed
434
+ when :both
435
+ ensure_seed_available!(format)
436
+ [@seed, @bytes]
437
+ else
438
+ raise SerializationError, "Unsupported PKCS#8 private key format: #{format.inspect}"
439
+ end
440
+ end
441
+
442
+ def validate_seed_length!
443
+ expected = PKCS8::PrivateKeyChoice.seed_bytes(@algorithm)
444
+ raise InvalidKeyError, "Invalid signature seed length" unless @seed.bytesize == expected
445
+ end
446
+
447
+ def ensure_seed_available!(format)
448
+ return if @seed
449
+
450
+ raise SerializationError, "ML-DSA #{format.inspect} PKCS#8 export requires original seed material"
451
+ end
422
452
  end
423
453
  end
424
454
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PQCrypto
4
- VERSION = "0.5.3"
4
+ VERSION = "0.6.1"
5
5
  end
data/lib/pq_crypto.rb CHANGED
@@ -30,13 +30,17 @@ end
30
30
 
31
31
  require_relative "pq_crypto/errors"
32
32
  require_relative "pq_crypto/version"
33
+ require_relative "pq_crypto/internal"
33
34
  require_relative "pq_crypto/algorithm_registry"
34
35
  require_relative "pq_crypto/serialization"
35
36
  require_relative "pq_crypto/spki"
37
+ require_relative "pq_crypto/pkcs8/der"
38
+ require_relative "pq_crypto/pkcs8/private_key_choice"
36
39
  require_relative "pq_crypto/pkcs8"
37
40
  require_relative "pq_crypto/kem"
38
41
  require_relative "pq_crypto/signature"
39
42
  require_relative "pq_crypto/hybrid_kem"
43
+ require_relative "pq_crypto/key"
40
44
 
41
45
  module PQCrypto
42
46
  SUITES = {
@@ -65,6 +69,7 @@ module PQCrypto
65
69
  hybrid_kem_encapsulate
66
70
  hybrid_kem_expand_secret_key
67
71
  hybrid_kem_expand_secret_key_object
72
+ hybrid_kem_expanded_secret_key_wipe
68
73
  hybrid_kem_decapsulate
69
74
  hybrid_kem_decapsulate_expanded
70
75
  hybrid_kem_decapsulate_expanded_object
@@ -91,6 +96,13 @@ module PQCrypto
91
96
  public_key_from_pqc_container_pem
92
97
  secret_key_from_pqc_container_der
93
98
  secret_key_from_pqc_container_pem
99
+ pkcs8_private_key_info_to_der
100
+ pkcs8_private_key_info_from_der
101
+ pkcs8_encrypt_der
102
+ pkcs8_decrypt_der
103
+ pkcs8_encrypted_der?
104
+ pkcs8_der_to_pem
105
+ pkcs8_pem_to_der
94
106
  __test_ml_kem_keypair_from_seed
95
107
  __test_ml_kem_encapsulate_from_seed
96
108
  __test_ml_kem_512_encapsulate_from_seed
@@ -164,25 +176,25 @@ module PQCrypto
164
176
  KEM_KEYPAIR_METHODS = {
165
177
  ml_kem_512: :native_ml_kem_512_keypair_from_seed,
166
178
  ml_kem_768: :native_ml_kem_keypair_from_seed,
167
- ml_kem_1024: :native_ml_kem_1024_keypair_from_seed,
179
+ ml_kem_1024: :native_ml_kem_1024_keypair_from_seed
168
180
  }.freeze
169
181
 
170
182
  KEM_ENCAPSULATE_METHODS = {
171
183
  ml_kem_512: :native_test_ml_kem_512_encapsulate_from_seed,
172
184
  ml_kem_768: :native_test_ml_kem_encapsulate_from_seed,
173
- ml_kem_1024: :native_test_ml_kem_1024_encapsulate_from_seed,
185
+ ml_kem_1024: :native_test_ml_kem_1024_encapsulate_from_seed
174
186
  }.freeze
175
187
 
176
188
  MLDSA_KEYPAIR_METHODS = {
177
189
  ml_dsa_44: :native_test_ml_dsa_44_keypair_from_seed,
178
190
  ml_dsa_65: :native_test_sign_keypair_from_seed,
179
- ml_dsa_87: :native_test_ml_dsa_87_keypair_from_seed,
191
+ ml_dsa_87: :native_test_ml_dsa_87_keypair_from_seed
180
192
  }.freeze
181
193
 
182
194
  MLDSA_SIGN_METHODS = {
183
195
  ml_dsa_44: :native_test_ml_dsa_44_sign_from_seed,
184
196
  ml_dsa_65: :native_test_sign_from_seed,
185
- ml_dsa_87: :native_test_ml_dsa_87_sign_from_seed,
197
+ ml_dsa_87: :native_test_ml_dsa_87_sign_from_seed
186
198
  }.freeze
187
199
 
188
200
  def self.ml_kem_keypair_from_seed(seed, algorithm: :ml_kem_768)
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pq_crypto
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.3
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Haydarov
8
+ autorequire:
8
9
  bindir: exe
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-05-14 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: rake
@@ -313,8 +314,12 @@ files:
313
314
  - lib/pq_crypto/algorithm_registry.rb
314
315
  - lib/pq_crypto/errors.rb
315
316
  - lib/pq_crypto/hybrid_kem.rb
317
+ - lib/pq_crypto/internal.rb
316
318
  - lib/pq_crypto/kem.rb
319
+ - lib/pq_crypto/key.rb
317
320
  - lib/pq_crypto/pkcs8.rb
321
+ - lib/pq_crypto/pkcs8/der.rb
322
+ - lib/pq_crypto/pkcs8/private_key_choice.rb
318
323
  - lib/pq_crypto/serialization.rb
319
324
  - lib/pq_crypto/signature.rb
320
325
  - lib/pq_crypto/spki.rb
@@ -328,6 +333,7 @@ metadata:
328
333
  homepage_uri: https://github.com/roman-haidarov/pq_crypto
329
334
  source_code_uri: https://github.com/roman-haidarov/pq_crypto/tree/main
330
335
  changelog_uri: https://github.com/roman-haidarov/pq_crypto/blob/main/CHANGELOG.md
336
+ post_install_message:
331
337
  rdoc_options: []
332
338
  require_paths:
333
339
  - lib
@@ -342,7 +348,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
342
348
  - !ruby/object:Gem::Version
343
349
  version: '0'
344
350
  requirements: []
345
- rubygems_version: 3.6.7
351
+ rubygems_version: 3.3.27
352
+ signing_key:
346
353
  specification_version: 4
347
354
  summary: Primitive-first post-quantum cryptography for Ruby
348
355
  test_files: []