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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/ext/pqcrypto/extconf.rb +2 -0
- data/ext/pqcrypto/pqcrypto_ruby_secure.c +139 -0
- data/ext/pqcrypto/pqcrypto_secure.c +532 -0
- data/ext/pqcrypto/pqcrypto_secure.h +20 -0
- data/ext/pqcrypto/pqcrypto_version.h +1 -1
- data/lib/pq_crypto/hybrid_kem.rb +1 -1
- data/lib/pq_crypto/internal.rb +23 -0
- data/lib/pq_crypto/kem.rb +27 -34
- data/lib/pq_crypto/pkcs8/der.rb +68 -0
- data/lib/pq_crypto/pkcs8/private_key_choice.rb +186 -0
- data/lib/pq_crypto/pkcs8.rb +51 -468
- data/lib/pq_crypto/serialization.rb +19 -29
- data/lib/pq_crypto/signature.rb +28 -35
- data/lib/pq_crypto/version.rb +1 -1
- data/lib/pq_crypto.rb +10 -0
- metadata +4 -1
data/lib/pq_crypto/pkcs8.rb
CHANGED
|
@@ -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(
|
|
60
|
-
entry = AlgorithmRegistry.fetch(
|
|
61
|
-
validate_secret_key_algorithm!(
|
|
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
|
|
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(
|
|
94
|
-
der = encode_der(
|
|
95
|
-
|
|
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 =
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
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
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
|
471
|
-
|
|
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
|
|
475
|
-
|
|
113
|
+
def encrypted_der?(der)
|
|
114
|
+
pkcs8_native { PQCrypto.__send__(:native_pkcs8_encrypted_der?, der) }
|
|
476
115
|
end
|
|
477
116
|
|
|
478
|
-
def
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
121
|
+
pkcs8_native { PQCrypto.__send__(:native_pkcs8_encrypt_der, der, String(passphrase), iterations) }
|
|
505
122
|
end
|
|
506
123
|
|
|
507
|
-
def
|
|
508
|
-
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
72
|
-
algorithm, bytes = PQCrypto.__send__(
|
|
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
|
|