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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/GET_STARTED.md +70 -9
- data/README.md +11 -6
- data/ext/pqcrypto/extconf.rb +2 -0
- data/ext/pqcrypto/pq_externalmu.c +23 -18
- data/ext/pqcrypto/pqcrypto_native_api.h +10 -0
- data/ext/pqcrypto/pqcrypto_ruby_secure.c +351 -48
- data/ext/pqcrypto/pqcrypto_secure.c +615 -84
- data/ext/pqcrypto/pqcrypto_secure.h +35 -10
- data/ext/pqcrypto/pqcrypto_version.h +1 -1
- data/lib/pq_crypto/hybrid_kem.rb +2 -1
- data/lib/pq_crypto/internal.rb +23 -0
- data/lib/pq_crypto/kem.rb +79 -44
- data/lib/pq_crypto/key.rb +90 -0
- 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 +61 -304
- data/lib/pq_crypto/serialization.rb +19 -29
- data/lib/pq_crypto/signature.rb +81 -51
- data/lib/pq_crypto/version.rb +1 -1
- data/lib/pq_crypto.rb +16 -4
- metadata +10 -3
|
@@ -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
|
-
|
|
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);
|
data/lib/pq_crypto/hybrid_kem.rb
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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),
|
|
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
|
-
|
|
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 =
|
|
298
|
-
@shared_secret =
|
|
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
|