pq_crypto 0.6.6 → 0.6.8

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.
@@ -29,7 +29,9 @@ typedef enum {
29
29
  PQ_ERROR_RANDOM = -7,
30
30
  PQ_ERROR_BUFFER = -8,
31
31
  PQ_ERROR_NOMEM = -9,
32
- PQ_ERROR_OPENSSL = -10
32
+ PQ_ERROR_OPENSSL = -10,
33
+ PQ_ERROR_INVALID_PUBLIC_KEY = -11,
34
+ PQ_ERROR_INVALID_SECRET_KEY = -12
33
35
  } pq_error_t;
34
36
 
35
37
  typedef struct {
@@ -83,6 +85,17 @@ int pq_mlkem512_decapsulate(uint8_t *shared_secret, const uint8_t *ciphertext,
83
85
  int pq_mlkem1024_decapsulate(uint8_t *shared_secret, const uint8_t *ciphertext,
84
86
  const uint8_t *secret_key);
85
87
 
88
+ int pq_mlkem_check_public_key(const uint8_t *public_key);
89
+ int pq_mlkem_check_secret_key(const uint8_t *secret_key);
90
+ int pq_mlkem512_check_public_key(const uint8_t *public_key);
91
+ int pq_mlkem512_check_secret_key(const uint8_t *secret_key);
92
+ int pq_mlkem1024_check_public_key(const uint8_t *public_key);
93
+ int pq_mlkem1024_check_secret_key(const uint8_t *secret_key);
94
+
95
+ int pq_mldsa_check_secret_key(const uint8_t *secret_key);
96
+ int pq_mldsa44_check_secret_key(const uint8_t *secret_key);
97
+ int pq_mldsa87_check_secret_key(const uint8_t *secret_key);
98
+
86
99
  int pq_sign_keypair(uint8_t *public_key, uint8_t *secret_key);
87
100
  int pq_mldsa44_sign_keypair(uint8_t *public_key, uint8_t *secret_key);
88
101
  int pq_mldsa87_sign_keypair(uint8_t *public_key, uint8_t *secret_key);
@@ -2,6 +2,6 @@
2
2
  #ifndef PQCRYPTO_VERSION_H
3
3
  #define PQCRYPTO_VERSION_H
4
4
 
5
- #define PQCRYPTO_VERSION "0.6.6"
5
+ #define PQCRYPTO_VERSION "0.6.8"
6
6
 
7
7
  #endif
@@ -8,6 +8,16 @@ module PQCrypto
8
8
  String(value).b
9
9
  end
10
10
 
11
+ def frozen_binary_string(value)
12
+ binary_string(value).freeze
13
+ end
14
+
15
+ def strict_boolean!(value, name:)
16
+ return value if value.equal?(true) || value.equal?(false)
17
+
18
+ raise ArgumentError, "#{name} must be true or false (got #{value.inspect})"
19
+ end
20
+
11
21
  def safe_wipe(value)
12
22
  return unless value.is_a?(String) && !value.frozen?
13
23
 
@@ -19,5 +29,15 @@ module PQCrypto
19
29
  def constant_time_equal?(left, right)
20
30
  PQCrypto.__send__(:native_ct_equals, left, right)
21
31
  end
32
+
33
+ def validate_algorithm_match!(expected, actual, algorithms:, family:)
34
+ unless algorithms.key?(expected)
35
+ raise SerializationError, "Unsupported #{family} algorithm: #{expected.inspect}"
36
+ end
37
+ return if expected == actual
38
+
39
+ raise SerializationError,
40
+ "Expected #{expected.inspect}, got #{actual.inspect} (SPKI key algorithm mismatch)"
41
+ end
22
42
  end
23
43
  end
data/lib/pq_crypto/kem.rb CHANGED
@@ -14,18 +14,24 @@ module PQCrypto
14
14
  keypair_from_seed: :native_ml_kem_512_keypair_from_seed,
15
15
  encapsulate: :native_ml_kem_512_encapsulate,
16
16
  decapsulate: :native_ml_kem_512_decapsulate,
17
+ check_public_key: :native_ml_kem_512_check_public_key,
18
+ check_secret_key: :native_ml_kem_512_check_secret_key,
17
19
  }.freeze,
18
20
  ml_kem_768: {
19
21
  keypair: :native_ml_kem_keypair,
20
22
  keypair_from_seed: :native_ml_kem_keypair_from_seed,
21
23
  encapsulate: :native_ml_kem_encapsulate,
22
24
  decapsulate: :native_ml_kem_decapsulate,
25
+ check_public_key: :native_ml_kem_check_public_key,
26
+ check_secret_key: :native_ml_kem_check_secret_key,
23
27
  }.freeze,
24
28
  ml_kem_1024: {
25
29
  keypair: :native_ml_kem_1024_keypair,
26
30
  keypair_from_seed: :native_ml_kem_1024_keypair_from_seed,
27
31
  encapsulate: :native_ml_kem_1024_encapsulate,
28
32
  decapsulate: :native_ml_kem_1024_decapsulate,
33
+ check_public_key: :native_ml_kem_1024_check_public_key,
34
+ check_secret_key: :native_ml_kem_1024_check_secret_key,
29
35
  }.freeze,
30
36
  }.freeze
31
37
 
@@ -68,23 +74,29 @@ module PQCrypto
68
74
  SecretKey.new(resolve_algorithm!(resolved_algorithm), bytes)
69
75
  end
70
76
 
71
- def secret_key_from_pkcs8_der(der, passphrase: nil)
72
- secret_key_from_decoded_pkcs8(*PKCS8.decode_der(der, passphrase: passphrase))
77
+ def secret_key_from_pkcs8_der(der, passphrase: nil, require_encrypted: false)
78
+ require_encrypted = Internal.strict_boolean!(require_encrypted, name: "require_encrypted")
79
+ secret_key_from_decoded_pkcs8(
80
+ *PKCS8.decode_der(der, passphrase: passphrase, require_encrypted: require_encrypted)
81
+ )
73
82
  end
74
83
 
75
- def secret_key_from_pkcs8_pem(pem, passphrase: nil)
76
- secret_key_from_decoded_pkcs8(*PKCS8.decode_pem(pem, passphrase: passphrase))
84
+ def secret_key_from_pkcs8_pem(pem, passphrase: nil, require_encrypted: false)
85
+ require_encrypted = Internal.strict_boolean!(require_encrypted, name: "require_encrypted")
86
+ secret_key_from_decoded_pkcs8(
87
+ *PKCS8.decode_pem(pem, passphrase: passphrase, require_encrypted: require_encrypted)
88
+ )
77
89
  end
78
90
 
79
91
  def public_key_from_spki_der(der, algorithm: nil)
80
92
  resolved_algorithm, bytes = SPKI.decode_der(der)
81
- validate_algorithm_match!(algorithm, resolved_algorithm) if algorithm
93
+ validate_spki_algorithm!(algorithm, resolved_algorithm) if algorithm
82
94
  PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
83
95
  end
84
96
 
85
97
  def public_key_from_spki_pem(pem, algorithm: nil)
86
98
  resolved_algorithm, bytes = SPKI.decode_pem(pem)
87
- validate_algorithm_match!(algorithm, resolved_algorithm) if algorithm
99
+ validate_spki_algorithm!(algorithm, resolved_algorithm) if algorithm
88
100
  PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
89
101
  end
90
102
 
@@ -121,18 +133,17 @@ module PQCrypto
121
133
  end
122
134
  end
123
135
 
136
+ def checkable?(algorithm)
137
+ NATIVE_DISPATCH.key?(algorithm) && NATIVE_DISPATCH.fetch(algorithm).key?(:check_public_key)
138
+ end
139
+
124
140
  def native_method_for(algorithm, operation)
125
141
  NATIVE_DISPATCH.fetch(resolve_algorithm!(algorithm)).fetch(operation)
126
142
  end
127
143
 
128
- def validate_algorithm_match!(expected_algorithm, actual_algorithm)
129
- expected = resolve_algorithm!(expected_algorithm)
130
- return if expected == actual_algorithm
131
-
132
- raise SerializationError,
133
- "Expected #{expected.inspect}, got #{actual_algorithm.inspect} (SPKI key algorithm mismatch)"
134
- rescue UnsupportedAlgorithmError => e
135
- raise SerializationError, e.message
144
+ def validate_spki_algorithm!(expected_algorithm, actual_algorithm)
145
+ Internal.validate_algorithm_match!(expected_algorithm, actual_algorithm,
146
+ algorithms: DETAILS, family: "KEM")
136
147
  end
137
148
  end
138
149
 
@@ -158,8 +169,19 @@ module PQCrypto
158
169
 
159
170
  def initialize(algorithm, bytes)
160
171
  @algorithm = algorithm
161
- @bytes = Internal.binary_string(bytes)
172
+ @bytes = Internal.frozen_binary_string(bytes)
162
173
  validate_length!
174
+ validate_structure!
175
+ end
176
+
177
+ def valid?
178
+ if @algorithm == :ml_kem_768_x25519_xwing
179
+ mlkem_half = @bytes.byteslice(0, KEM.details(:ml_kem_768).fetch(:public_key_bytes))
180
+ return PQCrypto.__send__(:native_ml_kem_check_public_key, mlkem_half)
181
+ end
182
+ return true unless KEM.send(:checkable?, @algorithm)
183
+
184
+ PQCrypto.__send__(KEM.send(:native_method_for, @algorithm, :check_public_key), @bytes)
163
185
  end
164
186
 
165
187
  def to_bytes
@@ -219,6 +241,10 @@ module PQCrypto
219
241
  expected = KEM.details(@algorithm).fetch(:public_key_bytes)
220
242
  raise InvalidKeyError, "Invalid KEM public key length" unless @bytes.bytesize == expected
221
243
  end
244
+
245
+ def validate_structure!
246
+ raise InvalidKeyError, "Invalid #{@algorithm} public key: FIPS 203 modulus check failed" unless valid?
247
+ end
222
248
  end
223
249
 
224
250
  class SecretKey
@@ -230,6 +256,13 @@ module PQCrypto
230
256
  @seed = seed.nil? ? nil : Internal.binary_string(seed)
231
257
  validate_length!
232
258
  validate_seed_length! if @seed
259
+ validate_structure!
260
+ end
261
+
262
+ def valid?
263
+ return true unless KEM.send(:checkable?, @algorithm)
264
+
265
+ PQCrypto.__send__(KEM.send(:native_method_for, @algorithm, :check_secret_key), @bytes)
233
266
  end
234
267
 
235
268
  def self.from_seed(algorithm, seed)
@@ -298,6 +331,10 @@ module PQCrypto
298
331
  raise InvalidKeyError, "Invalid KEM secret key length" unless @bytes.bytesize == expected
299
332
  end
300
333
 
334
+ def validate_structure!
335
+ raise InvalidKeyError, "Invalid #{@algorithm} secret key: FIPS 203 hash check failed" unless valid?
336
+ end
337
+
301
338
  def pkcs8_material(format)
302
339
  case format
303
340
  when :expanded
data/lib/pq_crypto/key.rb CHANGED
@@ -17,22 +17,24 @@ module PQCrypto
17
17
  end
18
18
  end
19
19
 
20
- def from_pem(pem, passphrase: nil)
20
+ def from_pem(pem, passphrase: nil, require_encrypted: false)
21
+ require_encrypted = Internal.strict_boolean!(require_encrypted, name: "require_encrypted")
21
22
  text = String(pem)
22
23
  if text.include?(SPKI::PEM_BEGIN)
23
24
  public_key_from_spki_pem(text)
24
25
  elsif text.include?(PKCS8::PEM_BEGIN) || text.include?(PKCS8::ENCRYPTED_PEM_BEGIN)
25
- secret_key_from_pkcs8_pem(text, passphrase: passphrase)
26
+ secret_key_from_pkcs8_pem(text, passphrase: passphrase, require_encrypted: require_encrypted)
26
27
  else
27
28
  raise SerializationError, "Unsupported PEM label for PQCrypto::Key.from_pem"
28
29
  end
29
30
  end
30
31
 
31
- def from_der(der, passphrase: nil)
32
+ def from_der(der, passphrase: nil, require_encrypted: false)
33
+ require_encrypted = Internal.strict_boolean!(require_encrypted, name: "require_encrypted")
32
34
  public_key_from_spki_der(der)
33
35
  rescue SerializationError => spki_error
34
36
  begin
35
- secret_key_from_pkcs8_der(der, passphrase: passphrase)
37
+ secret_key_from_pkcs8_der(der, passphrase: passphrase, require_encrypted: require_encrypted)
36
38
  rescue SerializationError => pkcs8_error
37
39
  raise SerializationError,
38
40
  "Unable to decode DER as SPKI or PKCS#8 (SPKI: #{spki_error.message}; PKCS#8: #{pkcs8_error.message})"
@@ -56,12 +58,16 @@ module PQCrypto
56
58
  public_key_from_algorithm_and_bytes(algorithm, bytes)
57
59
  end
58
60
 
59
- def secret_key_from_pkcs8_pem(pem, passphrase: nil)
60
- secret_key_from_decoded_pkcs8(*PKCS8.decode_pem(pem, passphrase: passphrase))
61
+ def secret_key_from_pkcs8_pem(pem, passphrase: nil, require_encrypted: false)
62
+ secret_key_from_decoded_pkcs8(
63
+ *PKCS8.decode_pem(pem, passphrase: passphrase, require_encrypted: require_encrypted)
64
+ )
61
65
  end
62
66
 
63
- def secret_key_from_pkcs8_der(der, passphrase: nil)
64
- secret_key_from_decoded_pkcs8(*PKCS8.decode_der(der, passphrase: passphrase))
67
+ def secret_key_from_pkcs8_der(der, passphrase: nil, require_encrypted: false)
68
+ secret_key_from_decoded_pkcs8(
69
+ *PKCS8.decode_der(der, passphrase: passphrase, require_encrypted: require_encrypted)
70
+ )
65
71
  end
66
72
 
67
73
  def secret_key_from_decoded_pkcs8(algorithm, format, material)
@@ -44,24 +44,39 @@ module PQCrypto
44
44
 
45
45
  return [first, 1] if first < 0x80
46
46
 
47
+ length_octets = long_length_octet_count(first)
48
+ length = decode_long_length(der, offset, length_octets)
49
+
50
+ [length, 1 + length_octets]
51
+ end
52
+
53
+ def long_length_octet_count(first)
47
54
  length_octets = first & 0x7f
48
55
  raise SerializationError, "PKCS#8 DER indefinite length is not allowed" if length_octets.zero?
49
56
  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
57
 
54
- length = 0
55
- length_octets.times do |i|
56
- byte = der.getbyte(offset + 1 + i)
57
- length = (length << 8) | byte
58
- end
58
+ length_octets
59
+ end
59
60
 
60
- if length < 0x80 || (length_octets > 1 && der.getbyte(offset + 1).zero?)
61
- raise SerializationError, "PKCS#8 DER length is not minimally encoded"
61
+ def decode_long_length(der, offset, length_octets)
62
+ ensure_long_length_available!(der, offset, length_octets)
63
+ length = length_octets.times.reduce(0) do |total, index|
64
+ (total << 8) | der.getbyte(offset + 1 + index)
62
65
  end
66
+ ensure_minimal_long_length!(der, offset, length, length_octets)
67
+ length
68
+ end
63
69
 
64
- [length, 1 + length_octets]
70
+ def ensure_long_length_available!(der, offset, length_octets)
71
+ return if offset + 1 + length_octets <= der.bytesize
72
+
73
+ raise SerializationError, "PKCS#8 DER length exceeds available data"
74
+ end
75
+
76
+ def ensure_minimal_long_length!(der, offset, length, length_octets)
77
+ return unless length < 0x80 || (length_octets > 1 && der.getbyte(offset + 1).zero?)
78
+
79
+ raise SerializationError, "PKCS#8 DER length is not minimally encoded"
65
80
  end
66
81
  end
67
82
  end
@@ -6,6 +6,8 @@ module PQCrypto
6
6
  SEED_TAG = 0x80
7
7
  EXPANDED_TAG = 0x04
8
8
  BOTH_TAG = 0x30
9
+ COMPONENT_TAGS = { seed: SEED_TAG, expanded: EXPANDED_TAG }.freeze
10
+ COMPONENT_LABELS = { seed: "seed", expanded: "expandedKey" }.freeze
9
11
 
10
12
  KEYPAIR_FROM_SEED_METHODS = {
11
13
  ml_kem_512: :native_ml_kem_512_keypair_from_seed,
@@ -23,9 +25,9 @@ module PQCrypto
23
25
 
24
26
  case format
25
27
  when :seed
26
- encode_seed(algorithm, secret_material)
28
+ encode_component(algorithm, secret_material, format)
27
29
  when :expanded
28
- encode_expanded(algorithm, secret_material)
30
+ encode_component(algorithm, secret_material, format)
29
31
  when :both
30
32
  encode_both(algorithm, secret_material)
31
33
  else
@@ -40,10 +42,10 @@ module PQCrypto
40
42
  case tag
41
43
  when SEED_TAG
42
44
  ensure_format_supported!(algorithm, :seed)
43
- decode_seed(algorithm, choice_der)
45
+ decode_component(algorithm, choice_der, :seed)
44
46
  when EXPANDED_TAG
45
47
  ensure_format_supported!(algorithm, :expanded)
46
- decode_expanded(algorithm, choice_der)
48
+ decode_component(algorithm, choice_der, :expanded)
47
49
  when BOTH_TAG
48
50
  ensure_format_supported!(algorithm, :both)
49
51
  decode_both(algorithm, choice_der)
@@ -104,20 +106,13 @@ module PQCrypto
104
106
  "Invalid #{algorithm.inspect} expanded private key length: expected #{expected}, got #{expanded.bytesize}"
105
107
  end
106
108
 
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
109
+ def decode_component(algorithm, choice_der, format)
110
+ label = COMPONENT_LABELS.fetch(format)
111
+ _tag, bytes, next_offset = DER.decode_tlv(choice_der, 0, expected_tag: COMPONENT_TAGS.fetch(format), label: label)
112
+ raise SerializationError, "PKCS#8 #{label} contains trailing data" unless next_offset == choice_der.bytesize
110
113
 
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]
114
+ validate_component_length!(algorithm, format, bytes)
115
+ [algorithm, format, bytes]
121
116
  end
122
117
 
123
118
  def decode_both(algorithm, choice_der)
@@ -135,18 +130,19 @@ module PQCrypto
135
130
  [algorithm, :both, [seed_bytes, expanded_bytes]]
136
131
  end
137
132
 
138
- def encode_seed(algorithm, secret_material)
139
- seed = Internal.binary_string(secret_material)
140
- validate_seed_length!(algorithm, seed)
133
+ def encode_component(algorithm, secret_material, format)
134
+ bytes = Internal.binary_string(secret_material)
135
+ validate_component_length!(algorithm, format, bytes)
141
136
 
142
- DER.encode_tlv(SEED_TAG, seed)
137
+ DER.encode_tlv(COMPONENT_TAGS.fetch(format), bytes)
143
138
  end
144
139
 
145
- def encode_expanded(algorithm, secret_material)
146
- bytes = Internal.binary_string(secret_material)
147
- validate_expanded_length!(algorithm, bytes)
140
+ def validate_component_length!(algorithm, format, bytes)
141
+ expected = profile(algorithm).fetch(format == :seed ? :seed_bytes : :expanded_bytes)
142
+ return if bytes.bytesize == expected
148
143
 
149
- DER.encode_tlv(EXPANDED_TAG, bytes)
144
+ raise SerializationError,
145
+ "Invalid #{algorithm.inspect} #{format} private key length: expected #{expected}, got #{bytes.bytesize}"
150
146
  end
151
147
 
152
148
  def encode_both(algorithm, secret_material)
@@ -70,10 +70,16 @@ module PQCrypto
70
70
  der = encode_der(algorithm, secret_material, format: format, passphrase: passphrase, iterations: iterations)
71
71
  pkcs8_native { PQCrypto.__send__(:native_pkcs8_der_to_pem, der, !passphrase.nil?) }
72
72
  end
73
-
74
- def decode_der(der, passphrase: nil)
73
+ def decode_der(der, passphrase: nil, require_encrypted: false)
74
+ require_encrypted = Internal.strict_boolean!(require_encrypted, name: "require_encrypted")
75
75
  input = Internal.binary_string(der)
76
- return decode_encrypted_der(input, passphrase: passphrase) if encrypted_der?(input)
76
+ encrypted = encrypted_der?(input)
77
+
78
+ if require_encrypted && !encrypted
79
+ raise SerializationError, "PKCS#8 input is not encrypted, but require_encrypted: true was requested"
80
+ end
81
+
82
+ return decode_encrypted_der(input, passphrase: passphrase) if encrypted
77
83
 
78
84
  oid, choice_der = private_key_info_from_der(input)
79
85
  algorithm = AlgorithmRegistry.by_standard_oid(oid)
@@ -83,15 +89,17 @@ module PQCrypto
83
89
  PrivateKeyChoice.validate_secret_key_algorithm!(algorithm, entry)
84
90
  PrivateKeyChoice.decode(algorithm, Internal.binary_string(choice_der))
85
91
  rescue ArgumentError => e
92
+ raise if e.message.start_with?("require_encrypted")
86
93
  raise SerializationError, e.message
87
94
  ensure
88
95
  Internal.safe_wipe(choice_der) if defined?(choice_der)
89
96
  end
90
97
 
91
- def decode_pem(pem, passphrase: nil)
98
+ def decode_pem(pem, passphrase: nil, require_encrypted: false)
99
+ require_encrypted = Internal.strict_boolean!(require_encrypted, name: "require_encrypted")
92
100
  _encrypted, der = pkcs8_native { PQCrypto.__send__(:native_pkcs8_pem_to_der, String(pem)) }
93
101
  begin
94
- decode_der(der, passphrase: passphrase)
102
+ decode_der(der, passphrase: passphrase, require_encrypted: require_encrypted)
95
103
  ensure
96
104
  Internal.safe_wipe(der)
97
105
  end
@@ -14,18 +14,21 @@ module PQCrypto
14
14
  sign: :native_ml_dsa_44_sign,
15
15
  verify: :native_ml_dsa_44_verify,
16
16
  keypair_from_seed: :native_ml_dsa_44_keypair_from_seed,
17
+ check_secret_key: :native_ml_dsa_44_check_secret_key,
17
18
  }.freeze,
18
19
  ml_dsa_65: {
19
20
  keypair: :native_sign_keypair,
20
21
  sign: :native_sign,
21
22
  verify: :native_verify,
22
23
  keypair_from_seed: :native_ml_dsa_keypair_from_seed,
24
+ check_secret_key: :native_ml_dsa_check_secret_key,
23
25
  }.freeze,
24
26
  ml_dsa_87: {
25
27
  keypair: :native_ml_dsa_87_keypair,
26
28
  sign: :native_ml_dsa_87_sign,
27
29
  verify: :native_ml_dsa_87_verify,
28
30
  keypair_from_seed: :native_ml_dsa_87_keypair_from_seed,
31
+ check_secret_key: :native_ml_dsa_87_check_secret_key,
29
32
  }.freeze,
30
33
  }.freeze
31
34
 
@@ -33,7 +36,8 @@ module PQCrypto
33
36
  def generate(algorithm = CANONICAL_ALGORITHM)
34
37
  algorithm = resolve_algorithm!(algorithm)
35
38
  public_key, secret_key = PQCrypto.__send__(native_method_for(algorithm, :keypair))
36
- Keypair.new(PublicKey.new(algorithm, public_key), SecretKey.new(algorithm, secret_key))
39
+ Keypair.new(PublicKey.new(algorithm, public_key),
40
+ SecretKey.new(algorithm, secret_key, freshly_generated: true))
37
41
  end
38
42
 
39
43
  def public_key_from_bytes(algorithm, bytes)
@@ -76,22 +80,28 @@ module PQCrypto
76
80
 
77
81
  def public_key_from_spki_der(der, algorithm: nil)
78
82
  resolved_algorithm, bytes = SPKI.decode_der(der)
79
- validate_algorithm_match!(algorithm, resolved_algorithm) if algorithm
83
+ validate_spki_algorithm!(algorithm, resolved_algorithm) if algorithm
80
84
  PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
81
85
  end
82
86
 
83
87
  def public_key_from_spki_pem(pem, algorithm: nil)
84
88
  resolved_algorithm, bytes = SPKI.decode_pem(pem)
85
- validate_algorithm_match!(algorithm, resolved_algorithm) if algorithm
89
+ validate_spki_algorithm!(algorithm, resolved_algorithm) if algorithm
86
90
  PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
87
91
  end
88
92
 
89
- def secret_key_from_pkcs8_der(der, passphrase: nil)
90
- secret_key_from_decoded_pkcs8(*PKCS8.decode_der(der, passphrase: passphrase))
93
+ def secret_key_from_pkcs8_der(der, passphrase: nil, require_encrypted: false)
94
+ require_encrypted = Internal.strict_boolean!(require_encrypted, name: "require_encrypted")
95
+ secret_key_from_decoded_pkcs8(
96
+ *PKCS8.decode_der(der, passphrase: passphrase, require_encrypted: require_encrypted)
97
+ )
91
98
  end
92
99
 
93
- def secret_key_from_pkcs8_pem(pem, passphrase: nil)
94
- secret_key_from_decoded_pkcs8(*PKCS8.decode_pem(pem, passphrase: passphrase))
100
+ def secret_key_from_pkcs8_pem(pem, passphrase: nil, require_encrypted: false)
101
+ require_encrypted = Internal.strict_boolean!(require_encrypted, name: "require_encrypted")
102
+ secret_key_from_decoded_pkcs8(
103
+ *PKCS8.decode_pem(pem, passphrase: passphrase, require_encrypted: require_encrypted)
104
+ )
95
105
  end
96
106
 
97
107
  def details(algorithm)
@@ -118,7 +128,7 @@ module PQCrypto
118
128
  SecretKey.new(algorithm, material)
119
129
  when :seed
120
130
  _public_key, expanded = PQCrypto.__send__(native_method_for(algorithm, :keypair_from_seed), material)
121
- SecretKey.new(algorithm, expanded, seed: material)
131
+ SecretKey.new(algorithm, expanded, seed: material, freshly_generated: true)
122
132
  when :both
123
133
  _seed, expanded = material
124
134
  SecretKey.new(algorithm, expanded, seed: _seed)
@@ -133,14 +143,13 @@ module PQCrypto
133
143
  NATIVE_DISPATCH.fetch(resolve_algorithm!(algorithm)).fetch(operation)
134
144
  end
135
145
 
136
- def validate_algorithm_match!(expected_algorithm, actual_algorithm)
137
- expected = resolve_algorithm!(expected_algorithm)
138
- return if expected == actual_algorithm
146
+ def secret_key_checkable?(algorithm)
147
+ NATIVE_DISPATCH.key?(algorithm) && NATIVE_DISPATCH.fetch(algorithm).key?(:check_secret_key)
148
+ end
139
149
 
140
- raise SerializationError,
141
- "Expected #{expected.inspect}, got #{actual_algorithm.inspect} (SPKI key algorithm mismatch)"
142
- rescue UnsupportedAlgorithmError => e
143
- raise SerializationError, e.message
150
+ def validate_spki_algorithm!(expected_algorithm, actual_algorithm)
151
+ Internal.validate_algorithm_match!(expected_algorithm, actual_algorithm,
152
+ algorithms: DETAILS, family: "signature")
144
153
  end
145
154
 
146
155
  def _streaming_sign(secret_key, io, chunk_size, context)
@@ -151,25 +160,12 @@ module PQCrypto
151
160
 
152
161
  algorithm = secret_key.algorithm
153
162
  sk_bytes = secret_key.__send__(:bytes_for_native)
154
- begin
155
- tr = PQCrypto.__send__(:_native_mldsa_extract_tr, algorithm, sk_bytes)
156
- rescue ArgumentError => e
157
- raise InvalidKeyError, e.message
158
- end
159
-
160
- builder = PQCrypto.__send__(:_native_mldsa_mu_builder_new, tr, context)
161
- builder_consumed = false
162
- mu = nil
163
- begin
164
- _drain_io_into_builder(io, builder, chunk_size)
165
- mu = PQCrypto.__send__(:_native_mldsa_mu_builder_finalize, builder)
166
- builder_consumed = true
163
+ tr = extract_tr(:_native_mldsa_extract_tr, algorithm, sk_bytes)
164
+ with_streaming_mu(tr, io, chunk_size, context) do |mu|
167
165
  PQCrypto.__send__(:_native_mldsa_sign_mu, algorithm, mu, sk_bytes)
168
- ensure
169
- PQCrypto.__send__(:_native_mldsa_mu_builder_release, builder) unless builder_consumed
170
- PQCrypto.secure_wipe(tr) if tr && !tr.frozen?
171
- PQCrypto.secure_wipe(mu) if mu && !mu.frozen?
172
166
  end
167
+ ensure
168
+ Internal.safe_wipe(tr) if defined?(tr)
173
169
  end
174
170
 
175
171
  def _streaming_verify(public_key, io, signature, chunk_size, context)
@@ -180,26 +176,32 @@ module PQCrypto
180
176
 
181
177
  algorithm = public_key.algorithm
182
178
  pk_bytes = public_key.__send__(:bytes_for_native)
183
- begin
184
- tr = PQCrypto.__send__(:_native_mldsa_compute_tr, algorithm, pk_bytes)
185
- rescue ArgumentError => e
186
- raise InvalidKeyError, e.message
179
+ sig_bytes = Internal.binary_string(signature)
180
+ tr = extract_tr(:_native_mldsa_compute_tr, algorithm, pk_bytes)
181
+ with_streaming_mu(tr, io, chunk_size, context) do |mu|
182
+ PQCrypto.__send__(:_native_mldsa_verify_mu, algorithm, mu, sig_bytes, pk_bytes)
187
183
  end
184
+ ensure
185
+ Internal.safe_wipe(tr) if defined?(tr)
186
+ end
187
+
188
+ def extract_tr(native_method, algorithm, key_bytes)
189
+ PQCrypto.__send__(native_method, algorithm, key_bytes)
190
+ rescue ArgumentError => e
191
+ raise InvalidKeyError, e.message
192
+ end
188
193
 
189
- builder = PQCrypto.__send__(:_native_mldsa_mu_builder_new, tr, context)
190
- builder_consumed = false
194
+ def with_streaming_mu(tr, io, chunk_size, context)
195
+ builder = nil
191
196
  mu = nil
192
- sig_bytes = Internal.binary_string(signature)
193
197
  begin
198
+ builder = PQCrypto.__send__(:_native_mldsa_mu_builder_new, tr, context)
194
199
  _drain_io_into_builder(io, builder, chunk_size)
195
200
  mu = PQCrypto.__send__(:_native_mldsa_mu_builder_finalize, builder)
196
- builder_consumed = true
197
- PQCrypto.__send__(:_native_mldsa_verify_mu, algorithm, mu, sig_bytes, pk_bytes)
201
+ yield mu
198
202
  ensure
199
- PQCrypto.__send__(:_native_mldsa_mu_builder_release, builder) unless builder_consumed
200
-
201
- PQCrypto.secure_wipe(tr) if tr && !tr.frozen?
202
- PQCrypto.secure_wipe(mu) if mu && !mu.frozen?
203
+ PQCrypto.__send__(:_native_mldsa_mu_builder_release, builder) if builder
204
+ Internal.safe_wipe(mu) if defined?(mu)
203
205
  end
204
206
  end
205
207
 
@@ -264,7 +266,7 @@ module PQCrypto
264
266
 
265
267
  def initialize(algorithm, bytes)
266
268
  @algorithm = algorithm
267
- @bytes = Internal.binary_string(bytes)
269
+ @bytes = Internal.frozen_binary_string(bytes)
268
270
  validate_length!
269
271
  end
270
272
 
@@ -343,18 +345,25 @@ module PQCrypto
343
345
  class SecretKey
344
346
  attr_reader :algorithm
345
347
 
346
- def initialize(algorithm, bytes, seed: nil)
348
+ def initialize(algorithm, bytes, seed: nil, freshly_generated: false)
347
349
  @algorithm = algorithm
348
350
  @bytes = Internal.binary_string(bytes)
349
351
  @seed = seed.nil? ? nil : Internal.binary_string(seed)
350
352
  validate_length!
351
353
  validate_seed_length! if @seed
354
+ validate_structure! unless freshly_generated
355
+ end
356
+
357
+ def valid?
358
+ return true unless Signature.send(:secret_key_checkable?, @algorithm)
359
+
360
+ PQCrypto.__send__(Signature.send(:native_method_for, @algorithm, :check_secret_key), @bytes)
352
361
  end
353
362
 
354
363
  def self.from_seed(algorithm, seed)
355
364
  seed_bytes = Internal.binary_string(seed)
356
365
  _public_key, expanded = PQCrypto.__send__(Signature.send(:native_method_for, algorithm, :keypair_from_seed), seed_bytes)
357
- new(algorithm, expanded, seed: seed_bytes)
366
+ new(algorithm, expanded, seed: seed_bytes, freshly_generated: true)
358
367
  rescue ArgumentError => e
359
368
  raise InvalidKeyError, e.message
360
369
  end
@@ -424,6 +433,10 @@ module PQCrypto
424
433
  raise InvalidKeyError, "Invalid signature secret key length" unless @bytes.bytesize == expected
425
434
  end
426
435
 
436
+ def validate_structure!
437
+ raise InvalidKeyError, "Invalid #{@algorithm} secret key: ML-DSA structure check failed" unless valid?
438
+ end
439
+
427
440
  def pkcs8_material(format)
428
441
  case format
429
442
  when :expanded