pq_crypto 0.6.0 → 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.
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "openssl"
4
-
5
3
  module PQCrypto
6
4
  module PKCS8
7
5
  PEM_LABEL = "PRIVATE KEY"
@@ -12,10 +10,6 @@ module PQCrypto
12
10
  ENCRYPTED_PEM_END = "-----END #{ENCRYPTED_PEM_LABEL}-----"
13
11
  ML_KEM_SEED_BYTES = 64
14
12
  ML_DSA_SEED_BYTES = 32
15
- PBES2_OID = "1.2.840.113549.1.5.13"
16
- PBKDF2_OID = "1.2.840.113549.1.5.12"
17
- HMAC_SHA256_OID = "1.2.840.113549.2.9"
18
- AES_256_CBC_OID = "2.16.840.1.101.3.4.1.42"
19
13
  ENCRYPTED_PKCS8_DEFAULT_ITERATIONS = 200_000
20
14
 
21
15
  @allow_ml_dsa_seed_format = false
@@ -56,506 +50,95 @@ module PQCrypto
56
50
  class << self
57
51
  attr_accessor :allow_ml_dsa_seed_format
58
52
 
59
- def encode_der(algorithm_symbol, secret_material, format:, passphrase: nil, iterations: ENCRYPTED_PKCS8_DEFAULT_ITERATIONS)
60
- entry = AlgorithmRegistry.fetch(algorithm_symbol)
61
- validate_secret_key_algorithm!(algorithm_symbol, entry)
62
- ensure_format_supported!(algorithm_symbol, format)
63
-
64
- choice_der = case format
65
- when :seed
66
- encode_seed_choice(secret_material, algorithm_symbol)
67
- when :expanded
68
- encode_expanded_key_choice(secret_material, algorithm_symbol)
69
- when :both
70
- encode_both_choice(secret_material, algorithm_symbol)
71
- else
72
- raise SerializationError, "Unsupported PKCS#8 private key format: #{format.inspect}"
73
- end
74
-
75
- der = OpenSSL::ASN1::Sequence.new([
76
- OpenSSL::ASN1::Integer.new(0),
77
- OpenSSL::ASN1::Sequence.new([
78
- OpenSSL::ASN1::ObjectId.new(AlgorithmRegistry.standard_oid(algorithm_symbol)),
79
- ]),
80
- OpenSSL::ASN1::OctetString.new(choice_der),
81
- ]).to_der.b
53
+ def encode_der(algorithm, secret_material, format:, passphrase: nil, iterations: ENCRYPTED_PKCS8_DEFAULT_ITERATIONS)
54
+ entry = AlgorithmRegistry.fetch(algorithm)
55
+ PrivateKeyChoice.validate_secret_key_algorithm!(algorithm, entry)
82
56
 
57
+ choice_der = PrivateKeyChoice.encode(algorithm, secret_material, format)
58
+ der = private_key_info_to_der(algorithm, choice_der)
83
59
  return der if passphrase.nil?
84
60
 
85
61
  encrypt_der(der, passphrase: passphrase, iterations: iterations)
86
- rescue OpenSSL::ASN1::ASN1Error => e
62
+ rescue ArgumentError => e
87
63
  raise SerializationError, e.message
88
64
  ensure
89
- safe_wipe(choice_der) if defined?(choice_der)
90
- safe_wipe(der) if passphrase && defined?(der)
65
+ Internal.safe_wipe(choice_der) if defined?(choice_der)
66
+ Internal.safe_wipe(der) if passphrase && defined?(der)
91
67
  end
92
68
 
93
- def encode_pem(algorithm_symbol, secret_material, format:, passphrase: nil, iterations: ENCRYPTED_PKCS8_DEFAULT_ITERATIONS)
94
- der = encode_der(algorithm_symbol, secret_material, format: format, passphrase: passphrase, iterations: iterations)
95
- body = encode_base64(der).scan(/.{1,64}/).join("\n")
96
- if passphrase.nil?
97
- "#{PEM_BEGIN}\n#{body}\n#{PEM_END}\n"
98
- else
99
- "#{ENCRYPTED_PEM_BEGIN}\n#{body}\n#{ENCRYPTED_PEM_END}\n"
100
- end
69
+ def encode_pem(algorithm, secret_material, format:, passphrase: nil, iterations: ENCRYPTED_PKCS8_DEFAULT_ITERATIONS)
70
+ der = encode_der(algorithm, secret_material, format: format, passphrase: passphrase, iterations: iterations)
71
+ pkcs8_native { PQCrypto.__send__(:native_pkcs8_der_to_pem, der, !passphrase.nil?) }
101
72
  end
102
73
 
103
74
  def decode_der(der, passphrase: nil)
104
- input = String(der).b
105
- outer = decode_asn1(input)
106
- raise SerializationError, "PKCS#8 DER contains trailing data" unless outer.to_der.b == input
107
- raise SerializationError, "PKCS#8 must be an ASN.1 SEQUENCE" unless outer.is_a?(OpenSSL::ASN1::Sequence)
108
-
109
- if encrypted_private_key_info?(outer)
110
- raise SerializationError, "Encrypted PKCS#8 requires passphrase" if passphrase.nil?
111
-
112
- plain_der = decrypt_der(outer, passphrase: passphrase)
113
- begin
114
- return decode_der(plain_der)
115
- ensure
116
- safe_wipe(plain_der)
117
- end
118
- end
75
+ input = Internal.binary_string(der)
76
+ return decode_encrypted_der(input, passphrase: passphrase) if encrypted_der?(input)
119
77
 
120
- raise SerializationError, "PKCS#8 OneAsymmetricKey must contain exactly 3 elements" unless outer.value.size == 3
78
+ oid, choice_der = private_key_info_from_der(input)
79
+ algorithm = AlgorithmRegistry.by_standard_oid(oid)
80
+ raise SerializationError, "Unsupported PKCS#8 algorithm OID: #{oid}" if algorithm.nil?
121
81
 
122
- version, algorithm_identifier, private_key = outer.value
123
- decode_version(version)
124
- algorithm = decode_algorithm_identifier(algorithm_identifier)
125
82
  entry = AlgorithmRegistry.fetch(algorithm)
126
- validate_secret_key_algorithm!(algorithm, entry)
127
-
128
- unless private_key.is_a?(OpenSSL::ASN1::OctetString)
129
- raise SerializationError, "PKCS#8 privateKey must be an OCTET STRING"
130
- end
131
-
132
- decode_private_key_choice(algorithm, String(private_key.value).b)
133
- end
134
-
135
- def decode_pem(pem, passphrase: nil)
136
- der = der_from_pem(pem)
137
- decode_der(der, passphrase: passphrase)
138
- end
139
-
140
- private
141
-
142
- def decode_asn1(der)
143
- OpenSSL::ASN1.decode(der)
144
- rescue OpenSSL::ASN1::ASN1Error => e
145
- raise SerializationError, e.message
146
- end
147
-
148
- def safe_wipe(value)
149
- return unless value.is_a?(String) && !value.frozen?
150
-
151
- PQCrypto.secure_wipe(value)
152
- rescue ArgumentError
153
- nil
154
- end
155
-
156
- def encrypt_der(der, passphrase:, iterations:)
157
- passphrase = String(passphrase)
158
- iterations = Integer(iterations)
159
- raise SerializationError, "Encrypted PKCS#8 iterations must be positive" unless iterations.positive?
160
-
161
- salt = OpenSSL::Random.random_bytes(16)
162
- cipher = OpenSSL::Cipher.new("AES-256-CBC")
163
- cipher.encrypt
164
- iv = cipher.random_iv
165
- key = OpenSSL::PKCS5.pbkdf2_hmac(passphrase, salt, iterations, 32, OpenSSL::Digest::SHA256.new)
166
- cipher.key = key
167
- encrypted = cipher.update(String(der).b) + cipher.final
168
- PQCrypto.secure_wipe(key) if key && !key.frozen?
169
-
170
- encrypted_private_key_info_der(salt, iterations, iv, encrypted)
171
- rescue ArgumentError, OpenSSL::Cipher::CipherError => e
83
+ PrivateKeyChoice.validate_secret_key_algorithm!(algorithm, entry)
84
+ PrivateKeyChoice.decode(algorithm, Internal.binary_string(choice_der))
85
+ rescue ArgumentError => e
172
86
  raise SerializationError, e.message
173
- end
174
-
175
- def decrypt_der(outer, passphrase:)
176
- salt, iterations, iv, encrypted = decode_encrypted_private_key_info(outer)
177
- key = OpenSSL::PKCS5.pbkdf2_hmac(String(passphrase), salt, iterations, 32, OpenSSL::Digest::SHA256.new)
178
- cipher = OpenSSL::Cipher.new("AES-256-CBC")
179
- cipher.decrypt
180
- cipher.key = key
181
- cipher.iv = iv
182
- (cipher.update(encrypted) + cipher.final).b
183
- rescue ArgumentError, OpenSSL::Cipher::CipherError => e
184
- raise SerializationError, "Failed to decrypt PKCS#8 private key: #{e.message}"
185
87
  ensure
186
- PQCrypto.secure_wipe(key) if defined?(key) && key && !key.frozen?
187
- end
188
-
189
- def encrypted_private_key_info_der(salt, iterations, iv, encrypted)
190
- OpenSSL::ASN1::Sequence.new([
191
- OpenSSL::ASN1::Sequence.new([
192
- OpenSSL::ASN1::ObjectId.new(PBES2_OID),
193
- OpenSSL::ASN1::Sequence.new([
194
- OpenSSL::ASN1::Sequence.new([
195
- OpenSSL::ASN1::ObjectId.new(PBKDF2_OID),
196
- OpenSSL::ASN1::Sequence.new([
197
- OpenSSL::ASN1::OctetString.new(salt),
198
- OpenSSL::ASN1::Integer.new(iterations),
199
- OpenSSL::ASN1::Integer.new(32),
200
- OpenSSL::ASN1::Sequence.new([
201
- OpenSSL::ASN1::ObjectId.new(HMAC_SHA256_OID),
202
- OpenSSL::ASN1::Null.new(nil),
203
- ]),
204
- ]),
205
- ]),
206
- OpenSSL::ASN1::Sequence.new([
207
- OpenSSL::ASN1::ObjectId.new(AES_256_CBC_OID),
208
- OpenSSL::ASN1::OctetString.new(iv),
209
- ]),
210
- ]),
211
- ]),
212
- OpenSSL::ASN1::OctetString.new(encrypted),
213
- ]).to_der.b
214
- rescue OpenSSL::ASN1::ASN1Error => e
215
- raise SerializationError, e.message
216
- end
217
-
218
- def encrypted_private_key_info?(outer)
219
- outer.value.size == 2 &&
220
- outer.value.first.is_a?(OpenSSL::ASN1::Sequence) &&
221
- outer.value.first.value.first.is_a?(OpenSSL::ASN1::ObjectId) &&
222
- outer.value.first.value.first.oid == PBES2_OID
223
- end
224
-
225
- def decode_encrypted_private_key_info(outer)
226
- raise SerializationError, "Encrypted PKCS#8 must contain exactly 2 elements" unless outer.value.size == 2
227
-
228
- algorithm_identifier, encrypted_data = outer.value
229
- raise SerializationError, "Encrypted PKCS#8 encryptedData must be an OCTET STRING" unless encrypted_data.is_a?(OpenSSL::ASN1::OctetString)
230
- raise SerializationError, "Encrypted PKCS#8 algorithm must be an AlgorithmIdentifier SEQUENCE" unless algorithm_identifier.is_a?(OpenSSL::ASN1::Sequence)
231
- raise SerializationError, "Encrypted PKCS#8 AlgorithmIdentifier must contain PBES2 parameters" unless algorithm_identifier.value.size == 2
232
-
233
- pbes2_oid, pbes2_params = algorithm_identifier.value
234
- raise SerializationError, "Encrypted PKCS#8 algorithm must be PBES2" unless pbes2_oid.is_a?(OpenSSL::ASN1::ObjectId) && pbes2_oid.oid == PBES2_OID
235
- raise SerializationError, "PBES2 parameters must be a SEQUENCE" unless pbes2_params.is_a?(OpenSSL::ASN1::Sequence)
236
- raise SerializationError, "PBES2 parameters must contain KDF and encryption scheme" unless pbes2_params.value.size == 2
237
-
238
- salt, iterations = decode_pbkdf2_params(pbes2_params.value[0])
239
- iv = decode_aes256_cbc_params(pbes2_params.value[1])
240
- [salt, iterations, iv, String(encrypted_data.value).b]
241
- end
242
-
243
- def decode_pbkdf2_params(kdf_alg)
244
- raise SerializationError, "PBES2 KDF must be an AlgorithmIdentifier SEQUENCE" unless kdf_alg.is_a?(OpenSSL::ASN1::Sequence)
245
- raise SerializationError, "PBES2 KDF AlgorithmIdentifier must contain parameters" unless kdf_alg.value.size == 2
246
-
247
- oid, params = kdf_alg.value
248
- raise SerializationError, "PBES2 KDF must be PBKDF2" unless oid.is_a?(OpenSSL::ASN1::ObjectId) && oid.oid == PBKDF2_OID
249
- raise SerializationError, "PBKDF2 params must be a SEQUENCE" unless params.is_a?(OpenSSL::ASN1::Sequence)
250
- raise SerializationError, "PBKDF2 params must contain salt, iteration count, key length, and PRF" unless params.value.size == 4
251
-
252
- salt_asn1, iterations_asn1, key_length_asn1, prf_alg = params.value
253
- raise SerializationError, "PBKDF2 salt must be an OCTET STRING" unless salt_asn1.is_a?(OpenSSL::ASN1::OctetString)
254
- raise SerializationError, "PBKDF2 iterations must be an INTEGER" unless iterations_asn1.is_a?(OpenSSL::ASN1::Integer)
255
- raise SerializationError, "PBKDF2 key length must be an INTEGER" unless key_length_asn1.is_a?(OpenSSL::ASN1::Integer)
256
-
257
- iterations = iterations_asn1.value.to_i
258
- key_length = key_length_asn1.value.to_i
259
- raise SerializationError, "PBKDF2 iterations must be positive" unless iterations.positive?
260
- raise SerializationError, "PBKDF2 key length must be 32 bytes" unless key_length == 32
261
- validate_hmac_sha256_prf!(prf_alg)
262
-
263
- [String(salt_asn1.value).b, iterations]
264
- end
265
-
266
- def validate_hmac_sha256_prf!(prf_alg)
267
- raise SerializationError, "PBKDF2 PRF must be an AlgorithmIdentifier SEQUENCE" unless prf_alg.is_a?(OpenSSL::ASN1::Sequence)
268
- raise SerializationError, "PBKDF2 PRF AlgorithmIdentifier must contain OID and NULL" unless prf_alg.value.size == 2
269
-
270
- oid, null = prf_alg.value
271
- raise SerializationError, "PBKDF2 PRF must be HMAC-SHA256" unless oid.is_a?(OpenSSL::ASN1::ObjectId) && oid.oid == HMAC_SHA256_OID
272
- raise SerializationError, "PBKDF2 PRF parameters must be NULL" unless null.is_a?(OpenSSL::ASN1::Null)
273
- end
274
-
275
- def decode_aes256_cbc_params(encryption_scheme)
276
- raise SerializationError, "PBES2 encryption scheme must be an AlgorithmIdentifier SEQUENCE" unless encryption_scheme.is_a?(OpenSSL::ASN1::Sequence)
277
- raise SerializationError, "PBES2 encryption scheme must contain OID and IV" unless encryption_scheme.value.size == 2
278
-
279
- oid, iv = encryption_scheme.value
280
- raise SerializationError, "PBES2 encryption scheme must be AES-256-CBC" unless oid.is_a?(OpenSSL::ASN1::ObjectId) && oid.oid == AES_256_CBC_OID
281
- raise SerializationError, "AES-256-CBC IV must be an OCTET STRING" unless iv.is_a?(OpenSSL::ASN1::OctetString)
282
-
283
- iv_bytes = String(iv.value).b
284
- raise SerializationError, "AES-256-CBC IV must be 16 bytes" unless iv_bytes.bytesize == 16
285
- iv_bytes
286
- end
287
-
288
- def decode_version(value)
289
- raise SerializationError, "PKCS#8 version must be an INTEGER" unless value.is_a?(OpenSSL::ASN1::Integer)
290
-
291
- version = value.value.respond_to?(:to_i) ? value.value.to_i : value.value
292
- raise SerializationError, "PKCS#8 version must be 0" unless version == 0
293
- end
294
-
295
- def decode_algorithm_identifier(value)
296
- unless value.is_a?(OpenSSL::ASN1::Sequence)
297
- raise SerializationError, "PKCS#8 algorithm must be an AlgorithmIdentifier SEQUENCE"
298
- end
299
- unless value.value.size == 1
300
- raise SerializationError, "PKCS#8 AlgorithmIdentifier parameters must be absent"
301
- end
302
-
303
- oid = value.value.first
304
- raise SerializationError, "PKCS#8 AlgorithmIdentifier must contain an OBJECT IDENTIFIER" unless oid.is_a?(OpenSSL::ASN1::ObjectId)
305
-
306
- algorithm = AlgorithmRegistry.by_standard_oid(oid.oid)
307
- raise SerializationError, "Unsupported PKCS#8 algorithm OID: #{oid.oid}" if algorithm.nil?
308
-
309
- algorithm
310
- end
311
-
312
- def decode_private_key_choice(algorithm, choice_der)
313
- tag = choice_der.getbyte(0)
314
- raise SerializationError, "PKCS#8 privateKey CHOICE is empty" if tag.nil?
315
-
316
- case tag
317
- when 0x80
318
- ensure_format_supported!(algorithm, :seed)
319
- decode_seed_choice(algorithm, choice_der)
320
- when 0x04
321
- ensure_format_supported!(algorithm, :expanded)
322
- decode_expanded_key(algorithm, choice_der)
323
- when 0x30
324
- ensure_format_supported!(algorithm, :both)
325
- decode_both_choice(algorithm, choice_der)
326
- else
327
- raise SerializationError,
328
- "Unsupported PKCS#8 #{algorithm.inspect} private key CHOICE tag: 0x#{tag.to_s(16).rjust(2, '0')}"
329
- end
330
- end
331
-
332
- def decode_seed_choice(algorithm, choice_der)
333
- seed = decode_tlv_value(choice_der, expected_tag: 0x80, label: "seed")
334
- validate_seed_length!(algorithm, seed)
335
-
336
- [algorithm, :seed, seed]
337
- end
338
-
339
- def decode_expanded_key(algorithm, choice_der)
340
- expanded = decode_asn1(choice_der)
341
- unless expanded.to_der.b == choice_der
342
- raise SerializationError, "PKCS#8 expandedKey contains trailing data"
343
- end
344
- unless expanded.is_a?(OpenSSL::ASN1::OctetString)
345
- raise SerializationError, "PKCS#8 expandedKey must be an OCTET STRING"
346
- end
347
-
348
- bytes = String(expanded.value).b
349
- validate_expanded_key_length!(algorithm, bytes)
350
-
351
- [algorithm, :expanded, bytes]
88
+ Internal.safe_wipe(choice_der) if defined?(choice_der)
352
89
  end
353
90
 
354
- def decode_both_choice(algorithm, choice_der)
355
- both = decode_asn1(choice_der)
356
- raise SerializationError, "PKCS#8 both contains trailing data" unless both.to_der.b == choice_der
357
- raise SerializationError, "PKCS#8 both must be a SEQUENCE" unless both.is_a?(OpenSSL::ASN1::Sequence)
358
- raise SerializationError, "PKCS#8 both must contain exactly 2 elements" unless both.value.size == 2
359
-
360
- seed, expanded = both.value
361
- raise SerializationError, "PKCS#8 both seed must be an OCTET STRING" unless seed.is_a?(OpenSSL::ASN1::OctetString)
362
- unless expanded.is_a?(OpenSSL::ASN1::OctetString)
363
- raise SerializationError, "PKCS#8 both expandedKey must be an OCTET STRING"
364
- end
365
-
366
- seed_bytes = String(seed.value).b
367
- expanded_bytes = String(expanded.value).b
368
- validate_seed_length!(algorithm, seed_bytes)
369
- validate_expanded_key_length!(algorithm, expanded_bytes)
370
- verify_both_consistency!(algorithm, seed_bytes, expanded_bytes)
371
-
372
- [algorithm, :both, [seed_bytes, expanded_bytes]]
373
- end
374
-
375
- def encode_seed_choice(secret_material, algorithm)
376
- seed = String(secret_material).b
377
- validate_seed_length!(algorithm, seed)
378
-
379
- encode_tlv(0x80, seed)
380
- end
381
-
382
- def encode_expanded_key_choice(secret_material, algorithm)
383
- bytes = String(secret_material).b
384
- validate_expanded_key_length!(algorithm, bytes)
385
-
386
- OpenSSL::ASN1::OctetString.new(bytes).to_der.b
387
- end
388
-
389
- def encode_both_choice(secret_material, algorithm)
390
- unless secret_material.is_a?(Array) && secret_material.size == 2
391
- raise SerializationError, "PKCS#8 both format requires [seed, expandedKey]"
91
+ def decode_pem(pem, passphrase: nil)
92
+ _encrypted, der = pkcs8_native { PQCrypto.__send__(:native_pkcs8_pem_to_der, String(pem)) }
93
+ begin
94
+ decode_der(der, passphrase: passphrase)
95
+ ensure
96
+ Internal.safe_wipe(der)
392
97
  end
393
-
394
- seed, expanded = secret_material
395
- seed_bytes = String(seed).b
396
- expanded_bytes = String(expanded).b
397
- validate_seed_length!(algorithm, seed_bytes)
398
- validate_expanded_key_length!(algorithm, expanded_bytes)
399
-
400
- OpenSSL::ASN1::Sequence.new([
401
- OpenSSL::ASN1::OctetString.new(seed_bytes),
402
- OpenSSL::ASN1::OctetString.new(expanded_bytes),
403
- ]).to_der.b
404
98
  end
405
99
 
406
- def verify_both_consistency!(algorithm, seed, expanded)
407
- native_method = {
408
- ml_kem_512: :native_ml_kem_512_keypair_from_seed,
409
- ml_kem_768: :native_ml_kem_keypair_from_seed,
410
- ml_kem_1024: :native_ml_kem_1024_keypair_from_seed,
411
- ml_dsa_44: :native_ml_dsa_44_keypair_from_seed,
412
- ml_dsa_65: :native_ml_dsa_keypair_from_seed,
413
- ml_dsa_87: :native_ml_dsa_87_keypair_from_seed,
414
- }[algorithm]
415
- return if native_method.nil?
416
-
417
- _public_key, expected_expanded = PQCrypto.__send__(native_method, seed)
418
- return if PQCrypto.__send__(:native_ct_equals, expected_expanded, expanded)
419
-
420
- message = if ml_dsa_algorithm?(algorithm)
421
- "seed/expandedKey inconsistency in ML-DSA PKCS#8 'both' encoding (RFC 9881 §6)"
422
- else
423
- "seed/expandedKey inconsistency in PKCS#8 'both' encoding (RFC 9935 §8)"
424
- end
425
- raise SerializationError, message
426
- ensure
427
- safe_wipe(expected_expanded) if defined?(expected_expanded)
428
- end
429
-
430
- def validate_seed_length!(algorithm, seed)
431
- expected = choice_profile(algorithm).fetch(:seed_bytes)
432
- return if seed.bytesize == expected
433
-
434
- raise SerializationError,
435
- "Invalid #{algorithm.inspect} seed private key length: expected #{expected}, got #{seed.bytesize}"
436
- end
437
-
438
- def validate_expanded_key_length!(algorithm, expanded)
439
- expected = choice_profile(algorithm).fetch(:expanded_bytes)
440
- return if expanded.bytesize == expected
441
-
442
- raise SerializationError,
443
- "Invalid #{algorithm.inspect} expanded private key length: expected #{expected}, got #{expanded.bytesize}"
444
- end
445
-
446
- def validate_secret_key_algorithm!(algorithm_symbol, entry)
447
- return if PRIVATE_KEY_CHOICES.key?(algorithm_symbol) && %i[ml_kem ml_dsa].include?(entry.fetch(:family))
448
-
449
- raise SerializationError, "PKCS#8 private key codec is not supported for #{algorithm_symbol.inspect}"
450
- end
451
-
452
- def choice_profile(algorithm)
453
- PRIVATE_KEY_CHOICES.fetch(algorithm) do
454
- raise SerializationError, "PKCS#8 private key codec is not supported for #{algorithm.inspect}"
455
- end
456
- end
100
+ private
457
101
 
458
- def ensure_format_supported!(algorithm, format)
459
- if ml_dsa_algorithm?(algorithm) && %i[seed both].include?(format) && !allow_ml_dsa_seed_format
460
- raise SerializationError,
461
- "ML-DSA seed-format PKCS#8 is opt-in; set PQCrypto::PKCS8.allow_ml_dsa_seed_format = true to enable (see SECURITY.md for caveats)"
102
+ def private_key_info_to_der(algorithm, choice_der)
103
+ pkcs8_native do
104
+ PQCrypto.__send__(:native_pkcs8_private_key_info_to_der,
105
+ AlgorithmRegistry.standard_oid(algorithm), choice_der)
462
106
  end
463
-
464
- profile = choice_profile(algorithm)
465
- return if profile.fetch(:supported_formats).include?(format)
466
-
467
- raise SerializationError, "Unsupported PKCS#8 private key format for #{algorithm.inspect}: #{format.inspect}"
468
107
  end
469
108
 
470
- def ml_dsa_algorithm?(algorithm)
471
- %i[ml_dsa_44 ml_dsa_65 ml_dsa_87].include?(algorithm)
109
+ def private_key_info_from_der(der)
110
+ pkcs8_native { PQCrypto.__send__(:native_pkcs8_private_key_info_from_der, der) }
472
111
  end
473
112
 
474
- def encode_tlv(tag, value)
475
- tag.chr.b + encode_der_length(value.bytesize) + value
113
+ def encrypted_der?(der)
114
+ pkcs8_native { PQCrypto.__send__(:native_pkcs8_encrypted_der?, der) }
476
115
  end
477
116
 
478
- def decode_tlv_value(der, expected_tag:, label:)
479
- tag = der.getbyte(0)
480
- unless tag == expected_tag
481
- raise SerializationError, "PKCS#8 #{label} has unexpected tag: 0x#{tag.to_s(16).rjust(2, '0')}"
482
- end
483
-
484
- length, length_bytes = decode_der_length(der, 1)
485
- value_offset = 1 + length_bytes
486
- value_end = value_offset + length
487
- raise SerializationError, "PKCS#8 #{label} length exceeds available data" if value_end > der.bytesize
488
- raise SerializationError, "PKCS#8 #{label} contains trailing data" unless value_end == der.bytesize
489
-
490
- der.byteslice(value_offset, length).b
491
- end
492
-
493
- def encode_der_length(length)
494
- raise SerializationError, "Invalid DER length" if length.negative?
495
- return length.chr.b if length < 0x80
496
-
497
- encoded = []
498
- remaining = length
499
- until remaining.zero?
500
- encoded.unshift(remaining & 0xff)
501
- remaining >>= 8
502
- end
117
+ def encrypt_der(der, passphrase:, iterations:)
118
+ iterations = Integer(iterations)
119
+ raise SerializationError, "Encrypted PKCS#8 iterations must be positive" unless iterations.positive?
503
120
 
504
- (0x80 | encoded.length).chr.b + encoded.pack("C*").b
121
+ pkcs8_native { PQCrypto.__send__(:native_pkcs8_encrypt_der, der, String(passphrase), iterations) }
505
122
  end
506
123
 
507
- def decode_der_length(der, offset)
508
- first = der.getbyte(offset)
509
- raise SerializationError, "PKCS#8 DER length is missing" if first.nil?
510
-
511
- return [first, 1] if first < 0x80
512
-
513
- length_octets = first & 0x7f
514
- raise SerializationError, "PKCS#8 DER indefinite length is not allowed" if length_octets.zero?
515
- raise SerializationError, "PKCS#8 DER length is too large" if length_octets > 4
516
- if offset + 1 + length_octets > der.bytesize
517
- raise SerializationError, "PKCS#8 DER length exceeds available data"
518
- end
124
+ def decode_encrypted_der(der, passphrase:)
125
+ raise SerializationError, "Encrypted PKCS#8 requires passphrase" if passphrase.nil?
519
126
 
520
- length = 0
521
- length_octets.times do |i|
522
- byte = der.getbyte(offset + 1 + i)
523
- length = (length << 8) | byte
127
+ plain_der = pkcs8_native { PQCrypto.__send__(:native_pkcs8_decrypt_der, der, String(passphrase)) }
128
+ begin
129
+ decode_der(plain_der)
130
+ ensure
131
+ Internal.safe_wipe(plain_der)
524
132
  end
525
-
526
- if length < 0x80 || (length_octets > 1 && der.getbyte(offset + 1).zero?)
527
- raise SerializationError, "PKCS#8 DER length is not minimally encoded"
528
- end
529
-
530
- [length, 1 + length_octets]
531
133
  end
532
134
 
533
- def encode_base64(bytes)
534
- [String(bytes).b].pack("m0")
535
- end
536
-
537
- def decode_base64(body)
538
- compact = body.gsub(/[\r\n]/, "")
539
- unless compact.match?(/\A(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?\z/)
540
- raise SerializationError, "Invalid PKCS#8 PEM: invalid base64"
541
- end
542
-
543
- compact.unpack1("m0").b
544
- rescue ArgumentError => e
135
+ def pkcs8_native
136
+ yield
137
+ rescue SerializationError
138
+ raise
139
+ rescue PQCrypto::Error => e
545
140
  raise SerializationError, e.message
546
141
  end
547
-
548
- def der_from_pem(pem)
549
- text = String(pem)
550
- match = text.match(/\A#{Regexp.escape(PEM_BEGIN)}\r?\n(?<body>[A-Za-z0-9+\/=\r\n]+)\r?\n#{Regexp.escape(PEM_END)}[ \t\r\n]*\z/) ||
551
- text.match(/\A#{Regexp.escape(ENCRYPTED_PEM_BEGIN)}\r?\n(?<body>[A-Za-z0-9+\/=\r\n]+)\r?\n#{Regexp.escape(ENCRYPTED_PEM_END)}[ \t\r\n]*\z/)
552
- raise SerializationError, "Invalid PKCS#8 PEM: expected #{PEM_LABEL.inspect} or #{ENCRYPTED_PEM_LABEL.inspect} label" unless match
553
-
554
- body = match[:body]
555
- raise SerializationError, "Invalid PKCS#8 PEM: embedded NUL in body" if body.include?("\0")
556
-
557
- decode_base64(body)
558
- end
559
142
  end
560
143
  end
561
144
  end
@@ -21,63 +21,53 @@ module PQCrypto
21
21
  end
22
22
 
23
23
  def public_key_to_pqc_container_der(algorithm, bytes)
24
- PQCrypto.__send__(:native_public_key_to_pqc_container_der, String(algorithm), String(bytes).b)
25
- rescue ArgumentError, PQCrypto::Error => e
26
- raise SerializationError, e.message
24
+ dump(:native_public_key_to_pqc_container_der, algorithm, bytes)
27
25
  end
28
26
 
29
27
  def public_key_to_pqc_container_pem(algorithm, bytes)
30
- PQCrypto.__send__(:native_public_key_to_pqc_container_pem, String(algorithm), String(bytes).b)
31
- rescue ArgumentError, PQCrypto::Error => e
32
- raise SerializationError, e.message
28
+ dump(:native_public_key_to_pqc_container_pem, algorithm, bytes)
33
29
  end
34
30
 
35
31
  def secret_key_to_pqc_container_der(algorithm, bytes)
36
- PQCrypto.__send__(:native_secret_key_to_pqc_container_der, String(algorithm), String(bytes).b)
37
- rescue ArgumentError, PQCrypto::Error => e
38
- raise SerializationError, e.message
32
+ dump(:native_secret_key_to_pqc_container_der, algorithm, bytes)
39
33
  end
40
34
 
41
35
  def secret_key_to_pqc_container_pem(algorithm, bytes)
42
- PQCrypto.__send__(:native_secret_key_to_pqc_container_pem, String(algorithm), String(bytes).b)
43
- rescue ArgumentError, PQCrypto::Error => e
44
- raise SerializationError, e.message
36
+ dump(:native_secret_key_to_pqc_container_pem, algorithm, bytes)
45
37
  end
46
38
 
47
39
  def public_key_from_pqc_container_der(expected_algorithm, der)
48
- algorithm, bytes = PQCrypto.__send__(:native_public_key_from_pqc_container_der, String(der).b)
49
- validate_algorithm_expectation!(expected_algorithm, algorithm)
50
- [algorithm, bytes]
51
- rescue ArgumentError, PQCrypto::Error => e
52
- raise SerializationError, e.message
40
+ load(:native_public_key_from_pqc_container_der, expected_algorithm, der)
53
41
  end
54
42
 
55
43
  def public_key_from_pqc_container_pem(expected_algorithm, pem)
56
- algorithm, bytes = PQCrypto.__send__(:native_public_key_from_pqc_container_pem, String(pem).b)
57
- validate_algorithm_expectation!(expected_algorithm, algorithm)
58
- [algorithm, bytes]
59
- rescue ArgumentError, PQCrypto::Error => e
60
- raise SerializationError, e.message
44
+ load(:native_public_key_from_pqc_container_pem, expected_algorithm, pem)
61
45
  end
62
46
 
63
47
  def secret_key_from_pqc_container_der(expected_algorithm, der)
64
- algorithm, bytes = PQCrypto.__send__(:native_secret_key_from_pqc_container_der, String(der).b)
65
- validate_algorithm_expectation!(expected_algorithm, algorithm)
66
- [algorithm, bytes]
48
+ load(:native_secret_key_from_pqc_container_der, expected_algorithm, der)
49
+ end
50
+
51
+ def secret_key_from_pqc_container_pem(expected_algorithm, pem)
52
+ load(:native_secret_key_from_pqc_container_pem, expected_algorithm, pem)
53
+ end
54
+
55
+ private
56
+
57
+ def dump(native_method, algorithm, bytes)
58
+ PQCrypto.__send__(native_method, String(algorithm), Internal.binary_string(bytes))
67
59
  rescue ArgumentError, PQCrypto::Error => e
68
60
  raise SerializationError, e.message
69
61
  end
70
62
 
71
- def secret_key_from_pqc_container_pem(expected_algorithm, pem)
72
- algorithm, bytes = PQCrypto.__send__(:native_secret_key_from_pqc_container_pem, String(pem).b)
63
+ def load(native_method, expected_algorithm, source)
64
+ algorithm, bytes = PQCrypto.__send__(native_method, Internal.binary_string(source))
73
65
  validate_algorithm_expectation!(expected_algorithm, algorithm)
74
66
  [algorithm, bytes]
75
67
  rescue ArgumentError, PQCrypto::Error => e
76
68
  raise SerializationError, e.message
77
69
  end
78
70
 
79
- private
80
-
81
71
  def validate_algorithm_expectation!(expected_algorithm, actual_algorithm)
82
72
  return if expected_algorithm.nil? || expected_algorithm == actual_algorithm
83
73