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.
@@ -95,17 +95,17 @@ int pq_mldsa_keypair_from_seed(uint8_t *public_key, uint8_t *secret_key,
95
95
  int pq_mldsa87_keypair_from_seed(uint8_t *public_key, uint8_t *secret_key,
96
96
  const uint8_t *seed32);
97
97
  int pq_sign(uint8_t *signature, size_t *signature_len, const uint8_t *message, size_t message_len,
98
- const uint8_t *secret_key);
98
+ const uint8_t *ctx, size_t ctx_len, const uint8_t *secret_key);
99
99
  int pq_mldsa44_sign(uint8_t *signature, size_t *signature_len, const uint8_t *message, size_t message_len,
100
- const uint8_t *secret_key);
100
+ const uint8_t *ctx, size_t ctx_len, const uint8_t *secret_key);
101
101
  int pq_mldsa87_sign(uint8_t *signature, size_t *signature_len, const uint8_t *message, size_t message_len,
102
- const uint8_t *secret_key);
102
+ const uint8_t *ctx, size_t ctx_len, const uint8_t *secret_key);
103
103
  int pq_verify(const uint8_t *signature, size_t signature_len, const uint8_t *message,
104
- size_t message_len, const uint8_t *public_key);
104
+ size_t message_len, const uint8_t *ctx, size_t ctx_len, const uint8_t *public_key);
105
105
  int pq_mldsa44_verify(const uint8_t *signature, size_t signature_len, const uint8_t *message,
106
- size_t message_len, const uint8_t *public_key);
106
+ size_t message_len, const uint8_t *ctx, size_t ctx_len, const uint8_t *public_key);
107
107
  int pq_mldsa87_verify(const uint8_t *signature, size_t signature_len, const uint8_t *message,
108
- size_t message_len, const uint8_t *public_key);
108
+ size_t message_len, const uint8_t *ctx, size_t ctx_len, const uint8_t *public_key);
109
109
 
110
110
  int pq_public_key_to_pqc_container_der(uint8_t **output, size_t *output_len,
111
111
  const uint8_t *public_key,
@@ -132,6 +132,26 @@ int pq_secret_key_from_pqc_container_pem(char **algorithm_out, uint8_t **key_out
132
132
  size_t *key_len_out, const char *input,
133
133
  size_t input_len);
134
134
 
135
+ int pq_pkcs8_private_key_info_to_der(uint8_t **output, size_t *output_len,
136
+ const char *oid_text, const uint8_t *private_key,
137
+ size_t private_key_len);
138
+ int pq_pkcs8_private_key_info_from_der(char **oid_text_out, uint8_t **private_key_out,
139
+ size_t *private_key_len_out, const uint8_t *input,
140
+ size_t input_len);
141
+ int pq_pkcs8_encrypt_private_key_info_der(uint8_t **output, size_t *output_len,
142
+ const uint8_t *plain_der, size_t plain_der_len,
143
+ const char *passphrase, size_t passphrase_len,
144
+ int iterations);
145
+ int pq_pkcs8_decrypt_private_key_info_der(uint8_t **output, size_t *output_len,
146
+ const uint8_t *encrypted_der,
147
+ size_t encrypted_der_len, const char *passphrase,
148
+ size_t passphrase_len);
149
+ int pq_pkcs8_der_is_encrypted_private_key_info(const uint8_t *input, size_t input_len);
150
+ int pq_pkcs8_der_to_pem(char **output, size_t *output_len, const uint8_t *der, size_t der_len,
151
+ int encrypted);
152
+ int pq_pkcs8_pem_to_der(uint8_t **der_out, size_t *der_len_out, int *encrypted_out,
153
+ const char *input, size_t input_len);
154
+
135
155
  int pq_testing_mlkem_keypair_from_seed(uint8_t *public_key, uint8_t *secret_key,
136
156
  const uint8_t *seed, size_t seed_len);
137
157
  int pq_testing_mlkem512_keypair_from_seed(uint8_t *public_key, uint8_t *secret_key,
@@ -189,13 +209,18 @@ const char *pq_version(void);
189
209
  #define PQ_MLDSA_MUBYTES 64
190
210
  #define PQ_MLDSA_TRBYTES 64
191
211
 
192
- int pq_mldsa_extract_tr_from_secret_key(uint8_t *tr_out, const uint8_t *secret_key);
193
- int pq_mldsa_compute_tr_from_public_key(uint8_t *tr_out, const uint8_t *public_key);
212
+ int pq_mldsa_extract_tr_from_secret_key(uint8_t *tr_out, const uint8_t *secret_key,
213
+ size_t public_key_len,
214
+ int (*pk_from_sk)(uint8_t *, const uint8_t *));
215
+ int pq_mldsa_compute_tr_from_public_key(uint8_t *tr_out, const uint8_t *public_key,
216
+ size_t public_key_len);
194
217
 
195
218
  int pq_sign_mu(uint8_t *signature, size_t *signature_len,
196
- const uint8_t *mu, const uint8_t *secret_key);
219
+ const uint8_t *mu, const uint8_t *secret_key,
220
+ int (*signature_extmu)(uint8_t *, size_t *, const uint8_t *, const uint8_t *));
197
221
  int pq_verify_mu(const uint8_t *signature, size_t signature_len,
198
- const uint8_t *mu, const uint8_t *public_key);
222
+ const uint8_t *mu, const uint8_t *public_key, size_t expected_signature_len,
223
+ int (*verify_extmu)(const uint8_t *, size_t, const uint8_t *, const uint8_t *));
199
224
  void *pq_mu_builder_new(void);
200
225
  int pq_mu_builder_init(void *state, const uint8_t *tr,
201
226
  const uint8_t *ctx, size_t ctxlen);
@@ -2,6 +2,6 @@
2
2
  #ifndef PQCRYPTO_VERSION_H
3
3
  #define PQCRYPTO_VERSION_H
4
4
 
5
- #define PQCRYPTO_VERSION "0.5.3"
5
+ #define PQCRYPTO_VERSION "0.6.1"
6
6
 
7
7
  #endif
@@ -79,12 +79,13 @@ module PQCrypto
79
79
 
80
80
  class SecretKey < KEM::SecretKey
81
81
  def decapsulate(ciphertext)
82
- PQCrypto.__send__(:native_hybrid_kem_decapsulate_expanded_object, String(ciphertext).b, expanded_key_for_native)
82
+ PQCrypto.__send__(:native_hybrid_kem_decapsulate_expanded_object, Internal.binary_string(ciphertext), expanded_key_for_native)
83
83
  rescue ArgumentError => e
84
84
  raise InvalidCiphertextError, e.message
85
85
  end
86
86
 
87
87
  def wipe!
88
+ PQCrypto.__send__(:native_hybrid_kem_expanded_secret_key_wipe, @expanded_key) if @expanded_key
88
89
  @expanded_key = nil
89
90
  super
90
91
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PQCrypto
4
+ module Internal
5
+ module_function
6
+
7
+ def binary_string(value)
8
+ String(value).b
9
+ end
10
+
11
+ def safe_wipe(value)
12
+ return unless value.is_a?(String) && !value.frozen?
13
+
14
+ PQCrypto.secure_wipe(value)
15
+ rescue ArgumentError
16
+ nil
17
+ end
18
+
19
+ def constant_time_equal?(left, right)
20
+ PQCrypto.__send__(:native_ct_equals, left, right)
21
+ end
22
+ end
23
+ end
data/lib/pq_crypto/kem.rb CHANGED
@@ -44,6 +44,10 @@ module PQCrypto
44
44
  SecretKey.new(resolve_algorithm!(algorithm), bytes)
45
45
  end
46
46
 
47
+ def secret_key_from_seed(algorithm, seed)
48
+ SecretKey.from_seed(resolve_algorithm!(algorithm), seed)
49
+ end
50
+
47
51
  def public_key_from_pqc_container_der(der, algorithm = nil)
48
52
  resolved_algorithm, bytes = Serialization.public_key_from_pqc_container_der(algorithm, der)
49
53
  PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
@@ -64,12 +68,12 @@ module PQCrypto
64
68
  SecretKey.new(resolve_algorithm!(resolved_algorithm), bytes)
65
69
  end
66
70
 
67
- def secret_key_from_pkcs8_der(der)
68
- secret_key_from_decoded_pkcs8(*PKCS8.decode_der(der))
71
+ def secret_key_from_pkcs8_der(der, passphrase: nil)
72
+ secret_key_from_decoded_pkcs8(*PKCS8.decode_der(der, passphrase: passphrase))
69
73
  end
70
74
 
71
- def secret_key_from_pkcs8_pem(pem)
72
- secret_key_from_decoded_pkcs8(*PKCS8.decode_pem(pem))
75
+ def secret_key_from_pkcs8_pem(pem, passphrase: nil)
76
+ secret_key_from_decoded_pkcs8(*PKCS8.decode_pem(pem, passphrase: passphrase))
73
77
  end
74
78
 
75
79
  def public_key_from_spki_der(der, algorithm: nil)
@@ -101,20 +105,20 @@ module PQCrypto
101
105
  end
102
106
 
103
107
  def secret_key_from_decoded_pkcs8(algorithm, format, material)
104
- secret_material = case format
105
- when :seed
106
- _public_key, expanded = PQCrypto.__send__(native_method_for(algorithm, :keypair_from_seed), material)
107
- expanded
108
- when :both
109
- _seed, expanded = material
110
- expanded
111
- when :expanded
112
- material
113
- else
114
- raise SerializationError, "Unsupported PKCS#8 private key format: #{format.inspect}"
115
- end
116
-
117
- SecretKey.new(resolve_algorithm!(algorithm), secret_material)
108
+ algorithm = resolve_algorithm!(algorithm)
109
+
110
+ case format
111
+ when :seed
112
+ _public_key, expanded = PQCrypto.__send__(native_method_for(algorithm, :keypair_from_seed), material)
113
+ SecretKey.new(algorithm, expanded, seed: material)
114
+ when :both
115
+ seed, expanded = material
116
+ SecretKey.new(algorithm, expanded, seed: seed)
117
+ when :expanded
118
+ SecretKey.new(algorithm, material)
119
+ else
120
+ raise SerializationError, "Unsupported PKCS#8 private key format: #{format.inspect}"
121
+ end
118
122
  end
119
123
 
120
124
  def native_method_for(algorithm, operation)
@@ -154,7 +158,7 @@ module PQCrypto
154
158
 
155
159
  def initialize(algorithm, bytes)
156
160
  @algorithm = algorithm
157
- @bytes = String(bytes).b
161
+ @bytes = Internal.binary_string(bytes)
158
162
  validate_length!
159
163
  end
160
164
 
@@ -192,7 +196,7 @@ module PQCrypto
192
196
 
193
197
  def ==(other)
194
198
  return false unless other.is_a?(PublicKey) && other.algorithm == algorithm
195
- PQCrypto.__send__(:native_ct_equals, other.to_bytes, @bytes)
199
+ Internal.constant_time_equal?(other.send(:bytes_for_native), @bytes)
196
200
  end
197
201
 
198
202
  alias eql? ==
@@ -207,6 +211,10 @@ module PQCrypto
207
211
 
208
212
  private
209
213
 
214
+ def bytes_for_native
215
+ @bytes
216
+ end
217
+
210
218
  def validate_length!
211
219
  expected = KEM.details(@algorithm).fetch(:public_key_bytes)
212
220
  raise InvalidKeyError, "Invalid KEM public key length" unless @bytes.bytesize == expected
@@ -216,10 +224,20 @@ module PQCrypto
216
224
  class SecretKey
217
225
  attr_reader :algorithm
218
226
 
219
- def initialize(algorithm, bytes)
227
+ def initialize(algorithm, bytes, seed: nil)
220
228
  @algorithm = algorithm
221
- @bytes = String(bytes).b
229
+ @bytes = Internal.binary_string(bytes)
230
+ @seed = seed.nil? ? nil : Internal.binary_string(seed)
222
231
  validate_length!
232
+ validate_seed_length! if @seed
233
+ end
234
+
235
+ def self.from_seed(algorithm, seed)
236
+ seed_bytes = Internal.binary_string(seed)
237
+ _public_key, expanded = PQCrypto.__send__(KEM.send(:native_method_for, algorithm, :keypair_from_seed), seed_bytes)
238
+ new(algorithm, expanded, seed: seed_bytes)
239
+ rescue ArgumentError => e
240
+ raise InvalidKeyError, e.message
223
241
  end
224
242
 
225
243
  def to_bytes
@@ -234,42 +252,29 @@ module PQCrypto
234
252
  Serialization.secret_key_to_pqc_container_pem(@algorithm, @bytes)
235
253
  end
236
254
 
237
- def to_pkcs8_der(format: :expanded)
238
- case format
239
- when :expanded
240
- PKCS8.encode_der(@algorithm, @bytes, format: :expanded)
241
- when :seed, :both
242
- raise SerializationError, "PKCS#8 #{format.inspect} export from KEM::SecretKey requires original seed material"
243
- else
244
- raise SerializationError, "Unsupported PKCS#8 private key format: #{format.inspect}"
245
- end
255
+ def to_pkcs8_der(format: :expanded, passphrase: nil, iterations: PKCS8::ENCRYPTED_PKCS8_DEFAULT_ITERATIONS)
256
+ PKCS8.encode_der(@algorithm, pkcs8_material(format), format: format, passphrase: passphrase, iterations: iterations)
246
257
  end
247
258
 
248
- def to_pkcs8_pem(format: :expanded)
249
- case format
250
- when :expanded
251
- PKCS8.encode_pem(@algorithm, @bytes, format: :expanded)
252
- when :seed, :both
253
- raise SerializationError, "PKCS#8 #{format.inspect} export from KEM::SecretKey requires original seed material"
254
- else
255
- raise SerializationError, "Unsupported PKCS#8 private key format: #{format.inspect}"
256
- end
259
+ def to_pkcs8_pem(format: :expanded, passphrase: nil, iterations: PKCS8::ENCRYPTED_PKCS8_DEFAULT_ITERATIONS)
260
+ PKCS8.encode_pem(@algorithm, pkcs8_material(format), format: format, passphrase: passphrase, iterations: iterations)
257
261
  end
258
262
 
259
263
  def decapsulate(ciphertext)
260
- PQCrypto.__send__(KEM.send(:native_method_for, @algorithm, :decapsulate), String(ciphertext).b, @bytes)
264
+ PQCrypto.__send__(KEM.send(:native_method_for, @algorithm, :decapsulate), Internal.binary_string(ciphertext), @bytes)
261
265
  rescue ArgumentError => e
262
266
  raise InvalidCiphertextError, e.message
263
267
  end
264
268
 
265
269
  def wipe!
266
270
  PQCrypto.secure_wipe(@bytes)
271
+ PQCrypto.secure_wipe(@seed) if @seed
267
272
  self
268
273
  end
269
274
 
270
275
  def ==(other)
271
276
  return false unless other.is_a?(SecretKey) && other.algorithm == algorithm
272
- PQCrypto.__send__(:native_ct_equals, other.to_bytes, @bytes)
277
+ Internal.constant_time_equal?(other.send(:bytes_for_native), @bytes)
273
278
  end
274
279
 
275
280
  alias eql? ==
@@ -284,18 +289,48 @@ module PQCrypto
284
289
 
285
290
  private
286
291
 
292
+ def bytes_for_native
293
+ @bytes
294
+ end
295
+
287
296
  def validate_length!
288
297
  expected = KEM.details(@algorithm).fetch(:secret_key_bytes)
289
298
  raise InvalidKeyError, "Invalid KEM secret key length" unless @bytes.bytesize == expected
290
299
  end
300
+
301
+ def pkcs8_material(format)
302
+ case format
303
+ when :expanded
304
+ @bytes
305
+ when :seed
306
+ ensure_seed_available!(format)
307
+ @seed
308
+ when :both
309
+ ensure_seed_available!(format)
310
+ [@seed, @bytes]
311
+ else
312
+ raise SerializationError, "Unsupported PKCS#8 private key format: #{format.inspect}"
313
+ end
314
+ end
315
+
316
+ def validate_seed_length!
317
+ expected = PKCS8::PrivateKeyChoice.seed_bytes(@algorithm)
318
+ raise InvalidKeyError, "Invalid KEM seed length" unless @seed.bytesize == expected
319
+ end
320
+
321
+ def ensure_seed_available!(format)
322
+ return if @seed
323
+
324
+ raise SerializationError, "PKCS#8 #{format.inspect} export from KEM::SecretKey requires original seed material"
325
+ end
291
326
  end
292
327
 
293
328
  class EncapsulationResult
294
329
  attr_reader :ciphertext, :shared_secret
295
330
 
296
331
  def initialize(ciphertext, shared_secret)
297
- @ciphertext = String(ciphertext).b
298
- @shared_secret = String(shared_secret).b
332
+ @ciphertext = Internal.binary_string(ciphertext)
333
+ @shared_secret = Internal.binary_string(shared_secret)
299
334
  end
300
335
 
301
336
  def inspect
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PQCrypto
4
+ module Key
5
+ class << self
6
+ def generate(algorithm)
7
+ algorithm = resolve_algorithm!(algorithm)
8
+ case AlgorithmRegistry.fetch(algorithm).fetch(:family)
9
+ when :ml_kem
10
+ KEM.generate(algorithm)
11
+ when :ml_dsa
12
+ Signature.generate(algorithm)
13
+ when :ml_kem_hybrid
14
+ HybridKEM.generate(algorithm)
15
+ else
16
+ raise UnsupportedAlgorithmError, "Unsupported key generation algorithm: #{algorithm.inspect}"
17
+ end
18
+ end
19
+
20
+ def from_pem(pem, passphrase: nil)
21
+ text = String(pem)
22
+ if text.include?(SPKI::PEM_BEGIN)
23
+ public_key_from_spki_pem(text)
24
+ elsif text.include?(PKCS8::PEM_BEGIN) || text.include?(PKCS8::ENCRYPTED_PEM_BEGIN)
25
+ secret_key_from_pkcs8_pem(text, passphrase: passphrase)
26
+ else
27
+ raise SerializationError, "Unsupported PEM label for PQCrypto::Key.from_pem"
28
+ end
29
+ end
30
+
31
+ def from_der(der, passphrase: nil)
32
+ public_key_from_spki_der(der)
33
+ rescue SerializationError => spki_error
34
+ begin
35
+ secret_key_from_pkcs8_der(der, passphrase: passphrase)
36
+ rescue SerializationError => pkcs8_error
37
+ raise SerializationError,
38
+ "Unable to decode DER as SPKI or PKCS#8 (SPKI: #{spki_error.message}; PKCS#8: #{pkcs8_error.message})"
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def resolve_algorithm!(algorithm)
45
+ AlgorithmRegistry.fetch(algorithm)
46
+ algorithm
47
+ end
48
+
49
+ def public_key_from_spki_pem(pem)
50
+ algorithm, bytes = SPKI.decode_pem(pem)
51
+ public_key_from_algorithm_and_bytes(algorithm, bytes)
52
+ end
53
+
54
+ def public_key_from_spki_der(der)
55
+ algorithm, bytes = SPKI.decode_der(der)
56
+ public_key_from_algorithm_and_bytes(algorithm, bytes)
57
+ end
58
+
59
+ def secret_key_from_pkcs8_pem(pem, passphrase: nil)
60
+ secret_key_from_decoded_pkcs8(*PKCS8.decode_pem(pem, passphrase: passphrase))
61
+ end
62
+
63
+ def secret_key_from_pkcs8_der(der, passphrase: nil)
64
+ secret_key_from_decoded_pkcs8(*PKCS8.decode_der(der, passphrase: passphrase))
65
+ end
66
+
67
+ def secret_key_from_decoded_pkcs8(algorithm, format, material)
68
+ case AlgorithmRegistry.fetch(algorithm).fetch(:family)
69
+ when :ml_kem
70
+ KEM.send(:secret_key_from_decoded_pkcs8, algorithm, format, material)
71
+ when :ml_dsa
72
+ Signature.send(:secret_key_from_decoded_pkcs8, algorithm, format, material)
73
+ else
74
+ raise SerializationError, "PKCS#8 private key codec is not supported for #{algorithm.inspect}"
75
+ end
76
+ end
77
+
78
+ def public_key_from_algorithm_and_bytes(algorithm, bytes)
79
+ case AlgorithmRegistry.fetch(algorithm).fetch(:family)
80
+ when :ml_kem
81
+ KEM::PublicKey.new(algorithm, bytes)
82
+ when :ml_dsa
83
+ Signature::PublicKey.new(algorithm, bytes)
84
+ else
85
+ raise SerializationError, "SPKI public key codec is not supported for #{algorithm.inspect}"
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PQCrypto
4
+ module PKCS8
5
+ module DER
6
+ module_function
7
+
8
+ def encode_tlv(tag, value)
9
+ tag.chr.b + encode_length(value.bytesize) + value
10
+ end
11
+
12
+ def decode_tlv(der, offset, expected_tag:, label:)
13
+ tag = der.getbyte(offset)
14
+ raise SerializationError, "PKCS#8 #{label} is missing" if tag.nil?
15
+ unless tag == expected_tag
16
+ raise SerializationError, "PKCS#8 #{label} has unexpected tag: 0x#{tag.to_s(16).rjust(2, '0')}"
17
+ end
18
+
19
+ length, length_bytes = decode_length(der, offset + 1)
20
+ value_offset = offset + 1 + length_bytes
21
+ value_end = value_offset + length
22
+ raise SerializationError, "PKCS#8 #{label} length exceeds available data" if value_end > der.bytesize
23
+
24
+ [tag, der.byteslice(value_offset, length).b, value_end]
25
+ end
26
+
27
+ def encode_length(length)
28
+ raise SerializationError, "Invalid DER length" if length.negative?
29
+ return length.chr.b if length < 0x80
30
+
31
+ encoded = []
32
+ remaining = length
33
+ until remaining.zero?
34
+ encoded.unshift(remaining & 0xff)
35
+ remaining >>= 8
36
+ end
37
+
38
+ (0x80 | encoded.length).chr.b + encoded.pack("C*").b
39
+ end
40
+
41
+ def decode_length(der, offset)
42
+ first = der.getbyte(offset)
43
+ raise SerializationError, "PKCS#8 DER length is missing" if first.nil?
44
+
45
+ return [first, 1] if first < 0x80
46
+
47
+ length_octets = first & 0x7f
48
+ raise SerializationError, "PKCS#8 DER indefinite length is not allowed" if length_octets.zero?
49
+ raise SerializationError, "PKCS#8 DER length is too large" if length_octets > 4
50
+ if offset + 1 + length_octets > der.bytesize
51
+ raise SerializationError, "PKCS#8 DER length exceeds available data"
52
+ end
53
+
54
+ length = 0
55
+ length_octets.times do |i|
56
+ byte = der.getbyte(offset + 1 + i)
57
+ length = (length << 8) | byte
58
+ end
59
+
60
+ if length < 0x80 || (length_octets > 1 && der.getbyte(offset + 1).zero?)
61
+ raise SerializationError, "PKCS#8 DER length is not minimally encoded"
62
+ end
63
+
64
+ [length, 1 + length_octets]
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PQCrypto
4
+ module PKCS8
5
+ module PrivateKeyChoice
6
+ SEED_TAG = 0x80
7
+ EXPANDED_TAG = 0x04
8
+ BOTH_TAG = 0x30
9
+
10
+ KEYPAIR_FROM_SEED_METHODS = {
11
+ ml_kem_512: :native_ml_kem_512_keypair_from_seed,
12
+ ml_kem_768: :native_ml_kem_keypair_from_seed,
13
+ ml_kem_1024: :native_ml_kem_1024_keypair_from_seed,
14
+ ml_dsa_44: :native_ml_dsa_44_keypair_from_seed,
15
+ ml_dsa_65: :native_ml_dsa_keypair_from_seed,
16
+ ml_dsa_87: :native_ml_dsa_87_keypair_from_seed,
17
+ }.freeze
18
+
19
+ module_function
20
+
21
+ def encode(algorithm, secret_material, format)
22
+ ensure_format_supported!(algorithm, format)
23
+
24
+ case format
25
+ when :seed
26
+ encode_seed(algorithm, secret_material)
27
+ when :expanded
28
+ encode_expanded(algorithm, secret_material)
29
+ when :both
30
+ encode_both(algorithm, secret_material)
31
+ else
32
+ raise SerializationError, "Unsupported PKCS#8 private key format: #{format.inspect}"
33
+ end
34
+ end
35
+
36
+ def decode(algorithm, choice_der)
37
+ tag = choice_der.getbyte(0)
38
+ raise SerializationError, "PKCS#8 privateKey CHOICE is empty" if tag.nil?
39
+
40
+ case tag
41
+ when SEED_TAG
42
+ ensure_format_supported!(algorithm, :seed)
43
+ decode_seed(algorithm, choice_der)
44
+ when EXPANDED_TAG
45
+ ensure_format_supported!(algorithm, :expanded)
46
+ decode_expanded(algorithm, choice_der)
47
+ when BOTH_TAG
48
+ ensure_format_supported!(algorithm, :both)
49
+ decode_both(algorithm, choice_der)
50
+ else
51
+ raise SerializationError,
52
+ "Unsupported PKCS#8 #{algorithm.inspect} private key CHOICE tag: 0x#{tag.to_s(16).rjust(2, '0')}"
53
+ end
54
+ end
55
+
56
+ def validate_secret_key_algorithm!(algorithm, entry)
57
+ return if PKCS8::PRIVATE_KEY_CHOICES.key?(algorithm) && %i[ml_kem ml_dsa].include?(entry.fetch(:family))
58
+
59
+ raise SerializationError, "PKCS#8 private key codec is not supported for #{algorithm.inspect}"
60
+ end
61
+
62
+ def ensure_format_supported!(algorithm, format)
63
+ if ml_dsa_algorithm?(algorithm) && %i[seed both].include?(format) && !PKCS8.allow_ml_dsa_seed_format
64
+ raise SerializationError,
65
+ "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)"
66
+ end
67
+
68
+ return if profile(algorithm).fetch(:supported_formats).include?(format)
69
+
70
+ raise SerializationError, "Unsupported PKCS#8 private key format for #{algorithm.inspect}: #{format.inspect}"
71
+ end
72
+
73
+ def seed_bytes(algorithm)
74
+ profile(algorithm).fetch(:seed_bytes)
75
+ end
76
+
77
+ def expanded_bytes(algorithm)
78
+ profile(algorithm).fetch(:expanded_bytes)
79
+ end
80
+
81
+ def ml_dsa_algorithm?(algorithm)
82
+ %i[ml_dsa_44 ml_dsa_65 ml_dsa_87].include?(algorithm)
83
+ end
84
+
85
+ def profile(algorithm)
86
+ PKCS8::PRIVATE_KEY_CHOICES.fetch(algorithm) do
87
+ raise SerializationError, "PKCS#8 private key codec is not supported for #{algorithm.inspect}"
88
+ end
89
+ end
90
+
91
+ def validate_seed_length!(algorithm, seed)
92
+ expected = seed_bytes(algorithm)
93
+ return if seed.bytesize == expected
94
+
95
+ raise SerializationError,
96
+ "Invalid #{algorithm.inspect} seed private key length: expected #{expected}, got #{seed.bytesize}"
97
+ end
98
+
99
+ def validate_expanded_length!(algorithm, expanded)
100
+ expected = expanded_bytes(algorithm)
101
+ return if expanded.bytesize == expected
102
+
103
+ raise SerializationError,
104
+ "Invalid #{algorithm.inspect} expanded private key length: expected #{expected}, got #{expanded.bytesize}"
105
+ end
106
+
107
+ def decode_seed(algorithm, choice_der)
108
+ _tag, seed, next_offset = DER.decode_tlv(choice_der, 0, expected_tag: SEED_TAG, label: "seed")
109
+ raise SerializationError, "PKCS#8 seed contains trailing data" unless next_offset == choice_der.bytesize
110
+
111
+ validate_seed_length!(algorithm, seed)
112
+ [algorithm, :seed, seed]
113
+ end
114
+
115
+ def decode_expanded(algorithm, choice_der)
116
+ _tag, bytes, next_offset = DER.decode_tlv(choice_der, 0, expected_tag: EXPANDED_TAG, label: "expandedKey")
117
+ raise SerializationError, "PKCS#8 expandedKey contains trailing data" unless next_offset == choice_der.bytesize
118
+
119
+ validate_expanded_length!(algorithm, bytes)
120
+ [algorithm, :expanded, bytes]
121
+ end
122
+
123
+ def decode_both(algorithm, choice_der)
124
+ _tag, body, next_offset = DER.decode_tlv(choice_der, 0, expected_tag: BOTH_TAG, label: "both")
125
+ raise SerializationError, "PKCS#8 both contains trailing data" unless next_offset == choice_der.bytesize
126
+
127
+ _seed_tag, seed_bytes, offset = DER.decode_tlv(body, 0, expected_tag: EXPANDED_TAG, label: "both seed")
128
+ _expanded_tag, expanded_bytes, offset = DER.decode_tlv(body, offset, expected_tag: EXPANDED_TAG, label: "both expandedKey")
129
+ raise SerializationError, "PKCS#8 both must contain exactly 2 elements" unless offset == body.bytesize
130
+
131
+ validate_seed_length!(algorithm, seed_bytes)
132
+ validate_expanded_length!(algorithm, expanded_bytes)
133
+ verify_both_consistency!(algorithm, seed_bytes, expanded_bytes)
134
+
135
+ [algorithm, :both, [seed_bytes, expanded_bytes]]
136
+ end
137
+
138
+ def encode_seed(algorithm, secret_material)
139
+ seed = Internal.binary_string(secret_material)
140
+ validate_seed_length!(algorithm, seed)
141
+
142
+ DER.encode_tlv(SEED_TAG, seed)
143
+ end
144
+
145
+ def encode_expanded(algorithm, secret_material)
146
+ bytes = Internal.binary_string(secret_material)
147
+ validate_expanded_length!(algorithm, bytes)
148
+
149
+ DER.encode_tlv(EXPANDED_TAG, bytes)
150
+ end
151
+
152
+ def encode_both(algorithm, secret_material)
153
+ unless secret_material.is_a?(Array) && secret_material.size == 2
154
+ raise SerializationError, "PKCS#8 both format requires [seed, expandedKey]"
155
+ end
156
+
157
+ seed, expanded = secret_material
158
+ seed_bytes = Internal.binary_string(seed)
159
+ expanded_bytes = Internal.binary_string(expanded)
160
+ validate_seed_length!(algorithm, seed_bytes)
161
+ validate_expanded_length!(algorithm, expanded_bytes)
162
+
163
+ body = DER.encode_tlv(EXPANDED_TAG, seed_bytes) + DER.encode_tlv(EXPANDED_TAG, expanded_bytes)
164
+ DER.encode_tlv(BOTH_TAG, body)
165
+ end
166
+
167
+ def verify_both_consistency!(algorithm, seed, expanded)
168
+ native_method = KEYPAIR_FROM_SEED_METHODS.fetch(algorithm, nil)
169
+ return if native_method.nil?
170
+
171
+ _public_key, expected_expanded = PQCrypto.__send__(native_method, seed)
172
+ return if Internal.constant_time_equal?(expected_expanded, expanded)
173
+
174
+ raise SerializationError, consistency_error_message(algorithm)
175
+ ensure
176
+ Internal.safe_wipe(expected_expanded) if defined?(expected_expanded)
177
+ end
178
+
179
+ def consistency_error_message(algorithm)
180
+ return "seed/expandedKey inconsistency in ML-DSA PKCS#8 'both' encoding (RFC 9881 §6)" if ml_dsa_algorithm?(algorithm)
181
+
182
+ "seed/expandedKey inconsistency in PKCS#8 'both' encoding (RFC 9935 §8)"
183
+ end
184
+ end
185
+ end
186
+ end