pq_crypto 0.5.3 → 0.6.0
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 +11 -0
- data/GET_STARTED.md +70 -9
- data/README.md +11 -6
- data/ext/pqcrypto/pq_externalmu.c +23 -18
- data/ext/pqcrypto/pqcrypto_native_api.h +10 -0
- data/ext/pqcrypto/pqcrypto_ruby_secure.c +212 -48
- data/ext/pqcrypto/pqcrypto_secure.c +83 -84
- data/ext/pqcrypto/pqcrypto_secure.h +15 -10
- data/ext/pqcrypto/pqcrypto_version.h +1 -1
- data/lib/pq_crypto/hybrid_kem.rb +1 -0
- data/lib/pq_crypto/kem.rb +71 -29
- data/lib/pq_crypto/key.rb +90 -0
- data/lib/pq_crypto/pkcs8.rb +184 -10
- data/lib/pq_crypto/signature.rb +74 -37
- data/lib/pq_crypto/version.rb +1 -1
- data/lib/pq_crypto.rb +6 -4
- metadata +7 -3
data/lib/pq_crypto/pkcs8.rb
CHANGED
|
@@ -7,8 +7,16 @@ module PQCrypto
|
|
|
7
7
|
PEM_LABEL = "PRIVATE KEY"
|
|
8
8
|
PEM_BEGIN = "-----BEGIN #{PEM_LABEL}-----"
|
|
9
9
|
PEM_END = "-----END #{PEM_LABEL}-----"
|
|
10
|
+
ENCRYPTED_PEM_LABEL = "ENCRYPTED PRIVATE KEY"
|
|
11
|
+
ENCRYPTED_PEM_BEGIN = "-----BEGIN #{ENCRYPTED_PEM_LABEL}-----"
|
|
12
|
+
ENCRYPTED_PEM_END = "-----END #{ENCRYPTED_PEM_LABEL}-----"
|
|
10
13
|
ML_KEM_SEED_BYTES = 64
|
|
11
14
|
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
|
+
ENCRYPTED_PKCS8_DEFAULT_ITERATIONS = 200_000
|
|
12
20
|
|
|
13
21
|
@allow_ml_dsa_seed_format = false
|
|
14
22
|
|
|
@@ -48,7 +56,7 @@ module PQCrypto
|
|
|
48
56
|
class << self
|
|
49
57
|
attr_accessor :allow_ml_dsa_seed_format
|
|
50
58
|
|
|
51
|
-
def encode_der(algorithm_symbol, secret_material, format:)
|
|
59
|
+
def encode_der(algorithm_symbol, secret_material, format:, passphrase: nil, iterations: ENCRYPTED_PKCS8_DEFAULT_ITERATIONS)
|
|
52
60
|
entry = AlgorithmRegistry.fetch(algorithm_symbol)
|
|
53
61
|
validate_secret_key_algorithm!(algorithm_symbol, entry)
|
|
54
62
|
ensure_format_supported!(algorithm_symbol, format)
|
|
@@ -64,28 +72,51 @@ module PQCrypto
|
|
|
64
72
|
raise SerializationError, "Unsupported PKCS#8 private key format: #{format.inspect}"
|
|
65
73
|
end
|
|
66
74
|
|
|
67
|
-
OpenSSL::ASN1::Sequence.new([
|
|
75
|
+
der = OpenSSL::ASN1::Sequence.new([
|
|
68
76
|
OpenSSL::ASN1::Integer.new(0),
|
|
69
77
|
OpenSSL::ASN1::Sequence.new([
|
|
70
78
|
OpenSSL::ASN1::ObjectId.new(AlgorithmRegistry.standard_oid(algorithm_symbol)),
|
|
71
79
|
]),
|
|
72
80
|
OpenSSL::ASN1::OctetString.new(choice_der),
|
|
73
81
|
]).to_der.b
|
|
82
|
+
|
|
83
|
+
return der if passphrase.nil?
|
|
84
|
+
|
|
85
|
+
encrypt_der(der, passphrase: passphrase, iterations: iterations)
|
|
74
86
|
rescue OpenSSL::ASN1::ASN1Error => e
|
|
75
87
|
raise SerializationError, e.message
|
|
88
|
+
ensure
|
|
89
|
+
safe_wipe(choice_der) if defined?(choice_der)
|
|
90
|
+
safe_wipe(der) if passphrase && defined?(der)
|
|
76
91
|
end
|
|
77
92
|
|
|
78
|
-
def encode_pem(algorithm_symbol, secret_material, format:)
|
|
79
|
-
der = encode_der(algorithm_symbol, secret_material, format: format)
|
|
93
|
+
def encode_pem(algorithm_symbol, secret_material, format:, passphrase: nil, iterations: ENCRYPTED_PKCS8_DEFAULT_ITERATIONS)
|
|
94
|
+
der = encode_der(algorithm_symbol, secret_material, format: format, passphrase: passphrase, iterations: iterations)
|
|
80
95
|
body = encode_base64(der).scan(/.{1,64}/).join("\n")
|
|
81
|
-
|
|
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
|
|
82
101
|
end
|
|
83
102
|
|
|
84
|
-
def decode_der(der)
|
|
103
|
+
def decode_der(der, passphrase: nil)
|
|
85
104
|
input = String(der).b
|
|
86
105
|
outer = decode_asn1(input)
|
|
87
106
|
raise SerializationError, "PKCS#8 DER contains trailing data" unless outer.to_der.b == input
|
|
88
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
|
|
119
|
+
|
|
89
120
|
raise SerializationError, "PKCS#8 OneAsymmetricKey must contain exactly 3 elements" unless outer.value.size == 3
|
|
90
121
|
|
|
91
122
|
version, algorithm_identifier, private_key = outer.value
|
|
@@ -101,9 +132,9 @@ module PQCrypto
|
|
|
101
132
|
decode_private_key_choice(algorithm, String(private_key.value).b)
|
|
102
133
|
end
|
|
103
134
|
|
|
104
|
-
def decode_pem(pem)
|
|
135
|
+
def decode_pem(pem, passphrase: nil)
|
|
105
136
|
der = der_from_pem(pem)
|
|
106
|
-
decode_der(der)
|
|
137
|
+
decode_der(der, passphrase: passphrase)
|
|
107
138
|
end
|
|
108
139
|
|
|
109
140
|
private
|
|
@@ -114,6 +145,146 @@ module PQCrypto
|
|
|
114
145
|
raise SerializationError, e.message
|
|
115
146
|
end
|
|
116
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
|
|
172
|
+
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
|
+
ensure
|
|
186
|
+
PQCrypto.secure_wipe(key) if defined?(key) && key && !key.frozen?
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def encrypted_private_key_info_der(salt, iterations, iv, encrypted)
|
|
190
|
+
OpenSSL::ASN1::Sequence.new([
|
|
191
|
+
OpenSSL::ASN1::Sequence.new([
|
|
192
|
+
OpenSSL::ASN1::ObjectId.new(PBES2_OID),
|
|
193
|
+
OpenSSL::ASN1::Sequence.new([
|
|
194
|
+
OpenSSL::ASN1::Sequence.new([
|
|
195
|
+
OpenSSL::ASN1::ObjectId.new(PBKDF2_OID),
|
|
196
|
+
OpenSSL::ASN1::Sequence.new([
|
|
197
|
+
OpenSSL::ASN1::OctetString.new(salt),
|
|
198
|
+
OpenSSL::ASN1::Integer.new(iterations),
|
|
199
|
+
OpenSSL::ASN1::Integer.new(32),
|
|
200
|
+
OpenSSL::ASN1::Sequence.new([
|
|
201
|
+
OpenSSL::ASN1::ObjectId.new(HMAC_SHA256_OID),
|
|
202
|
+
OpenSSL::ASN1::Null.new(nil),
|
|
203
|
+
]),
|
|
204
|
+
]),
|
|
205
|
+
]),
|
|
206
|
+
OpenSSL::ASN1::Sequence.new([
|
|
207
|
+
OpenSSL::ASN1::ObjectId.new(AES_256_CBC_OID),
|
|
208
|
+
OpenSSL::ASN1::OctetString.new(iv),
|
|
209
|
+
]),
|
|
210
|
+
]),
|
|
211
|
+
]),
|
|
212
|
+
OpenSSL::ASN1::OctetString.new(encrypted),
|
|
213
|
+
]).to_der.b
|
|
214
|
+
rescue OpenSSL::ASN1::ASN1Error => e
|
|
215
|
+
raise SerializationError, e.message
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def encrypted_private_key_info?(outer)
|
|
219
|
+
outer.value.size == 2 &&
|
|
220
|
+
outer.value.first.is_a?(OpenSSL::ASN1::Sequence) &&
|
|
221
|
+
outer.value.first.value.first.is_a?(OpenSSL::ASN1::ObjectId) &&
|
|
222
|
+
outer.value.first.value.first.oid == PBES2_OID
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def decode_encrypted_private_key_info(outer)
|
|
226
|
+
raise SerializationError, "Encrypted PKCS#8 must contain exactly 2 elements" unless outer.value.size == 2
|
|
227
|
+
|
|
228
|
+
algorithm_identifier, encrypted_data = outer.value
|
|
229
|
+
raise SerializationError, "Encrypted PKCS#8 encryptedData must be an OCTET STRING" unless encrypted_data.is_a?(OpenSSL::ASN1::OctetString)
|
|
230
|
+
raise SerializationError, "Encrypted PKCS#8 algorithm must be an AlgorithmIdentifier SEQUENCE" unless algorithm_identifier.is_a?(OpenSSL::ASN1::Sequence)
|
|
231
|
+
raise SerializationError, "Encrypted PKCS#8 AlgorithmIdentifier must contain PBES2 parameters" unless algorithm_identifier.value.size == 2
|
|
232
|
+
|
|
233
|
+
pbes2_oid, pbes2_params = algorithm_identifier.value
|
|
234
|
+
raise SerializationError, "Encrypted PKCS#8 algorithm must be PBES2" unless pbes2_oid.is_a?(OpenSSL::ASN1::ObjectId) && pbes2_oid.oid == PBES2_OID
|
|
235
|
+
raise SerializationError, "PBES2 parameters must be a SEQUENCE" unless pbes2_params.is_a?(OpenSSL::ASN1::Sequence)
|
|
236
|
+
raise SerializationError, "PBES2 parameters must contain KDF and encryption scheme" unless pbes2_params.value.size == 2
|
|
237
|
+
|
|
238
|
+
salt, iterations = decode_pbkdf2_params(pbes2_params.value[0])
|
|
239
|
+
iv = decode_aes256_cbc_params(pbes2_params.value[1])
|
|
240
|
+
[salt, iterations, iv, String(encrypted_data.value).b]
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def decode_pbkdf2_params(kdf_alg)
|
|
244
|
+
raise SerializationError, "PBES2 KDF must be an AlgorithmIdentifier SEQUENCE" unless kdf_alg.is_a?(OpenSSL::ASN1::Sequence)
|
|
245
|
+
raise SerializationError, "PBES2 KDF AlgorithmIdentifier must contain parameters" unless kdf_alg.value.size == 2
|
|
246
|
+
|
|
247
|
+
oid, params = kdf_alg.value
|
|
248
|
+
raise SerializationError, "PBES2 KDF must be PBKDF2" unless oid.is_a?(OpenSSL::ASN1::ObjectId) && oid.oid == PBKDF2_OID
|
|
249
|
+
raise SerializationError, "PBKDF2 params must be a SEQUENCE" unless params.is_a?(OpenSSL::ASN1::Sequence)
|
|
250
|
+
raise SerializationError, "PBKDF2 params must contain salt, iteration count, key length, and PRF" unless params.value.size == 4
|
|
251
|
+
|
|
252
|
+
salt_asn1, iterations_asn1, key_length_asn1, prf_alg = params.value
|
|
253
|
+
raise SerializationError, "PBKDF2 salt must be an OCTET STRING" unless salt_asn1.is_a?(OpenSSL::ASN1::OctetString)
|
|
254
|
+
raise SerializationError, "PBKDF2 iterations must be an INTEGER" unless iterations_asn1.is_a?(OpenSSL::ASN1::Integer)
|
|
255
|
+
raise SerializationError, "PBKDF2 key length must be an INTEGER" unless key_length_asn1.is_a?(OpenSSL::ASN1::Integer)
|
|
256
|
+
|
|
257
|
+
iterations = iterations_asn1.value.to_i
|
|
258
|
+
key_length = key_length_asn1.value.to_i
|
|
259
|
+
raise SerializationError, "PBKDF2 iterations must be positive" unless iterations.positive?
|
|
260
|
+
raise SerializationError, "PBKDF2 key length must be 32 bytes" unless key_length == 32
|
|
261
|
+
validate_hmac_sha256_prf!(prf_alg)
|
|
262
|
+
|
|
263
|
+
[String(salt_asn1.value).b, iterations]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def validate_hmac_sha256_prf!(prf_alg)
|
|
267
|
+
raise SerializationError, "PBKDF2 PRF must be an AlgorithmIdentifier SEQUENCE" unless prf_alg.is_a?(OpenSSL::ASN1::Sequence)
|
|
268
|
+
raise SerializationError, "PBKDF2 PRF AlgorithmIdentifier must contain OID and NULL" unless prf_alg.value.size == 2
|
|
269
|
+
|
|
270
|
+
oid, null = prf_alg.value
|
|
271
|
+
raise SerializationError, "PBKDF2 PRF must be HMAC-SHA256" unless oid.is_a?(OpenSSL::ASN1::ObjectId) && oid.oid == HMAC_SHA256_OID
|
|
272
|
+
raise SerializationError, "PBKDF2 PRF parameters must be NULL" unless null.is_a?(OpenSSL::ASN1::Null)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def decode_aes256_cbc_params(encryption_scheme)
|
|
276
|
+
raise SerializationError, "PBES2 encryption scheme must be an AlgorithmIdentifier SEQUENCE" unless encryption_scheme.is_a?(OpenSSL::ASN1::Sequence)
|
|
277
|
+
raise SerializationError, "PBES2 encryption scheme must contain OID and IV" unless encryption_scheme.value.size == 2
|
|
278
|
+
|
|
279
|
+
oid, iv = encryption_scheme.value
|
|
280
|
+
raise SerializationError, "PBES2 encryption scheme must be AES-256-CBC" unless oid.is_a?(OpenSSL::ASN1::ObjectId) && oid.oid == AES_256_CBC_OID
|
|
281
|
+
raise SerializationError, "AES-256-CBC IV must be an OCTET STRING" unless iv.is_a?(OpenSSL::ASN1::OctetString)
|
|
282
|
+
|
|
283
|
+
iv_bytes = String(iv.value).b
|
|
284
|
+
raise SerializationError, "AES-256-CBC IV must be 16 bytes" unless iv_bytes.bytesize == 16
|
|
285
|
+
iv_bytes
|
|
286
|
+
end
|
|
287
|
+
|
|
117
288
|
def decode_version(value)
|
|
118
289
|
raise SerializationError, "PKCS#8 version must be an INTEGER" unless value.is_a?(OpenSSL::ASN1::Integer)
|
|
119
290
|
|
|
@@ -252,6 +423,8 @@ module PQCrypto
|
|
|
252
423
|
"seed/expandedKey inconsistency in PKCS#8 'both' encoding (RFC 9935 §8)"
|
|
253
424
|
end
|
|
254
425
|
raise SerializationError, message
|
|
426
|
+
ensure
|
|
427
|
+
safe_wipe(expected_expanded) if defined?(expected_expanded)
|
|
255
428
|
end
|
|
256
429
|
|
|
257
430
|
def validate_seed_length!(algorithm, seed)
|
|
@@ -374,8 +547,9 @@ module PQCrypto
|
|
|
374
547
|
|
|
375
548
|
def der_from_pem(pem)
|
|
376
549
|
text = String(pem)
|
|
377
|
-
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/)
|
|
378
|
-
|
|
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
|
|
379
553
|
|
|
380
554
|
body = match[:body]
|
|
381
555
|
raise SerializationError, "Invalid PKCS#8 PEM: embedded NUL in body" if body.include?("\0")
|
data/lib/pq_crypto/signature.rb
CHANGED
|
@@ -46,6 +46,10 @@ module PQCrypto
|
|
|
46
46
|
SecretKey.new(algorithm, bytes)
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
def secret_key_from_seed(algorithm, seed)
|
|
50
|
+
SecretKey.from_seed(resolve_algorithm!(algorithm), seed)
|
|
51
|
+
end
|
|
52
|
+
|
|
49
53
|
def public_key_from_pqc_container_der(der, algorithm = nil)
|
|
50
54
|
resolved_algorithm, bytes = Serialization.public_key_from_pqc_container_der(algorithm, der)
|
|
51
55
|
resolve_algorithm!(resolved_algorithm)
|
|
@@ -82,12 +86,12 @@ module PQCrypto
|
|
|
82
86
|
PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
|
|
83
87
|
end
|
|
84
88
|
|
|
85
|
-
def secret_key_from_pkcs8_der(der)
|
|
86
|
-
secret_key_from_decoded_pkcs8(*PKCS8.decode_der(der))
|
|
89
|
+
def secret_key_from_pkcs8_der(der, passphrase: nil)
|
|
90
|
+
secret_key_from_decoded_pkcs8(*PKCS8.decode_der(der, passphrase: passphrase))
|
|
87
91
|
end
|
|
88
92
|
|
|
89
|
-
def secret_key_from_pkcs8_pem(pem)
|
|
90
|
-
secret_key_from_decoded_pkcs8(*PKCS8.decode_pem(pem))
|
|
93
|
+
def secret_key_from_pkcs8_pem(pem, passphrase: nil)
|
|
94
|
+
secret_key_from_decoded_pkcs8(*PKCS8.decode_pem(pem, passphrase: passphrase))
|
|
91
95
|
end
|
|
92
96
|
|
|
93
97
|
def details(algorithm)
|
|
@@ -114,10 +118,10 @@ module PQCrypto
|
|
|
114
118
|
SecretKey.new(algorithm, material)
|
|
115
119
|
when :seed
|
|
116
120
|
_public_key, expanded = PQCrypto.__send__(native_method_for(algorithm, :keypair_from_seed), material)
|
|
117
|
-
SecretKey.new(algorithm, expanded)
|
|
121
|
+
SecretKey.new(algorithm, expanded, seed: material)
|
|
118
122
|
when :both
|
|
119
123
|
_seed, expanded = material
|
|
120
|
-
SecretKey.new(algorithm, expanded)
|
|
124
|
+
SecretKey.new(algorithm, expanded, seed: _seed)
|
|
121
125
|
else
|
|
122
126
|
raise SerializationError, "Unsupported ML-DSA PKCS#8 private key format: #{format.inspect}"
|
|
123
127
|
end
|
|
@@ -145,9 +149,10 @@ module PQCrypto
|
|
|
145
149
|
context = validate_context!(context)
|
|
146
150
|
validate_io!(io)
|
|
147
151
|
|
|
152
|
+
algorithm = secret_key.algorithm
|
|
148
153
|
sk_bytes = secret_key.__send__(:bytes_for_native)
|
|
149
154
|
begin
|
|
150
|
-
tr = PQCrypto.__send__(:_native_mldsa_extract_tr, sk_bytes)
|
|
155
|
+
tr = PQCrypto.__send__(:_native_mldsa_extract_tr, algorithm, sk_bytes)
|
|
151
156
|
rescue ArgumentError => e
|
|
152
157
|
raise InvalidKeyError, e.message
|
|
153
158
|
end
|
|
@@ -159,7 +164,7 @@ module PQCrypto
|
|
|
159
164
|
_drain_io_into_builder(io, builder, chunk_size)
|
|
160
165
|
mu = PQCrypto.__send__(:_native_mldsa_mu_builder_finalize, builder)
|
|
161
166
|
builder_consumed = true
|
|
162
|
-
PQCrypto.__send__(:_native_mldsa_sign_mu, mu, sk_bytes)
|
|
167
|
+
PQCrypto.__send__(:_native_mldsa_sign_mu, algorithm, mu, sk_bytes)
|
|
163
168
|
ensure
|
|
164
169
|
PQCrypto.__send__(:_native_mldsa_mu_builder_release, builder) unless builder_consumed
|
|
165
170
|
PQCrypto.secure_wipe(tr) if tr && !tr.frozen?
|
|
@@ -173,9 +178,10 @@ module PQCrypto
|
|
|
173
178
|
context = validate_context!(context)
|
|
174
179
|
validate_io!(io)
|
|
175
180
|
|
|
181
|
+
algorithm = public_key.algorithm
|
|
176
182
|
pk_bytes = public_key.__send__(:bytes_for_native)
|
|
177
183
|
begin
|
|
178
|
-
tr = PQCrypto.__send__(:_native_mldsa_compute_tr, pk_bytes)
|
|
184
|
+
tr = PQCrypto.__send__(:_native_mldsa_compute_tr, algorithm, pk_bytes)
|
|
179
185
|
rescue ArgumentError => e
|
|
180
186
|
raise InvalidKeyError, e.message
|
|
181
187
|
end
|
|
@@ -188,7 +194,7 @@ module PQCrypto
|
|
|
188
194
|
_drain_io_into_builder(io, builder, chunk_size)
|
|
189
195
|
mu = PQCrypto.__send__(:_native_mldsa_mu_builder_finalize, builder)
|
|
190
196
|
builder_consumed = true
|
|
191
|
-
PQCrypto.__send__(:_native_mldsa_verify_mu, mu, sig_bytes, pk_bytes)
|
|
197
|
+
PQCrypto.__send__(:_native_mldsa_verify_mu, algorithm, mu, sig_bytes, pk_bytes)
|
|
192
198
|
ensure
|
|
193
199
|
PQCrypto.__send__(:_native_mldsa_mu_builder_release, builder) unless builder_consumed
|
|
194
200
|
|
|
@@ -232,10 +238,7 @@ module PQCrypto
|
|
|
232
238
|
end
|
|
233
239
|
|
|
234
240
|
def validate_streaming_algorithm!(algorithm)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
raise UnsupportedAlgorithmError,
|
|
238
|
-
"Streaming sign_io/verify_io currently supports only #{CANONICAL_ALGORITHM.inspect}"
|
|
241
|
+
resolve_algorithm!(algorithm)
|
|
239
242
|
end
|
|
240
243
|
end
|
|
241
244
|
|
|
@@ -285,14 +288,17 @@ module PQCrypto
|
|
|
285
288
|
SPKI.encode_pem(@algorithm, @bytes)
|
|
286
289
|
end
|
|
287
290
|
|
|
288
|
-
def verify(message, signature)
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
291
|
+
def verify(message, signature, context: "".b)
|
|
292
|
+
context = Signature.send(:validate_context!, context)
|
|
293
|
+
begin
|
|
294
|
+
PQCrypto.__send__(Signature.send(:native_method_for, @algorithm, :verify), String(message).b, String(signature).b, @bytes, context)
|
|
295
|
+
rescue ArgumentError => e
|
|
296
|
+
raise InvalidKeyError, e.message
|
|
297
|
+
end
|
|
292
298
|
end
|
|
293
299
|
|
|
294
|
-
def verify!(message, signature)
|
|
295
|
-
raise PQCrypto::VerificationError, "Verification failed" unless verify(message, signature)
|
|
300
|
+
def verify!(message, signature, context: "".b)
|
|
301
|
+
raise PQCrypto::VerificationError, "Verification failed" unless verify(message, signature, context: context)
|
|
296
302
|
true
|
|
297
303
|
end
|
|
298
304
|
|
|
@@ -309,7 +315,7 @@ module PQCrypto
|
|
|
309
315
|
|
|
310
316
|
def ==(other)
|
|
311
317
|
return false unless other.is_a?(PublicKey) && other.algorithm == algorithm
|
|
312
|
-
PQCrypto.__send__(:native_ct_equals, other.
|
|
318
|
+
PQCrypto.__send__(:native_ct_equals, other.send(:bytes_for_native), @bytes)
|
|
313
319
|
end
|
|
314
320
|
|
|
315
321
|
alias eql? ==
|
|
@@ -337,10 +343,20 @@ module PQCrypto
|
|
|
337
343
|
class SecretKey
|
|
338
344
|
attr_reader :algorithm
|
|
339
345
|
|
|
340
|
-
def initialize(algorithm, bytes)
|
|
346
|
+
def initialize(algorithm, bytes, seed: nil)
|
|
341
347
|
@algorithm = algorithm
|
|
342
348
|
@bytes = String(bytes).b
|
|
349
|
+
@seed = seed.nil? ? nil : String(seed).b
|
|
343
350
|
validate_length!
|
|
351
|
+
validate_seed_length! if @seed
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def self.from_seed(algorithm, seed)
|
|
355
|
+
seed_bytes = String(seed).b
|
|
356
|
+
_public_key, expanded = PQCrypto.__send__(Signature.send(:native_method_for, algorithm, :keypair_from_seed), seed_bytes)
|
|
357
|
+
new(algorithm, expanded, seed: seed_bytes)
|
|
358
|
+
rescue ArgumentError => e
|
|
359
|
+
raise InvalidKeyError, e.message
|
|
344
360
|
end
|
|
345
361
|
|
|
346
362
|
def to_bytes
|
|
@@ -355,34 +371,43 @@ module PQCrypto
|
|
|
355
371
|
Serialization.secret_key_to_pqc_container_pem(@algorithm, @bytes)
|
|
356
372
|
end
|
|
357
373
|
|
|
358
|
-
def to_pkcs8_der(format: :expanded)
|
|
374
|
+
def to_pkcs8_der(format: :expanded, passphrase: nil, iterations: PKCS8::ENCRYPTED_PKCS8_DEFAULT_ITERATIONS)
|
|
359
375
|
case format
|
|
360
376
|
when :expanded
|
|
361
|
-
PKCS8.encode_der(@algorithm, @bytes, format: :expanded)
|
|
362
|
-
when :seed
|
|
363
|
-
|
|
364
|
-
|
|
377
|
+
PKCS8.encode_der(@algorithm, @bytes, format: :expanded, passphrase: passphrase, iterations: iterations)
|
|
378
|
+
when :seed
|
|
379
|
+
ensure_seed_available!(format)
|
|
380
|
+
PKCS8.encode_der(@algorithm, @seed, format: :seed, passphrase: passphrase, iterations: iterations)
|
|
381
|
+
when :both
|
|
382
|
+
ensure_seed_available!(format)
|
|
383
|
+
PKCS8.encode_der(@algorithm, [@seed, @bytes], format: :both, passphrase: passphrase, iterations: iterations)
|
|
365
384
|
else
|
|
366
385
|
raise SerializationError, "Unsupported PKCS#8 private key format: #{format.inspect}"
|
|
367
386
|
end
|
|
368
387
|
end
|
|
369
388
|
|
|
370
|
-
def to_pkcs8_pem(format: :expanded)
|
|
389
|
+
def to_pkcs8_pem(format: :expanded, passphrase: nil, iterations: PKCS8::ENCRYPTED_PKCS8_DEFAULT_ITERATIONS)
|
|
371
390
|
case format
|
|
372
391
|
when :expanded
|
|
373
|
-
PKCS8.encode_pem(@algorithm, @bytes, format: :expanded)
|
|
374
|
-
when :seed
|
|
375
|
-
|
|
376
|
-
|
|
392
|
+
PKCS8.encode_pem(@algorithm, @bytes, format: :expanded, passphrase: passphrase, iterations: iterations)
|
|
393
|
+
when :seed
|
|
394
|
+
ensure_seed_available!(format)
|
|
395
|
+
PKCS8.encode_pem(@algorithm, @seed, format: :seed, passphrase: passphrase, iterations: iterations)
|
|
396
|
+
when :both
|
|
397
|
+
ensure_seed_available!(format)
|
|
398
|
+
PKCS8.encode_pem(@algorithm, [@seed, @bytes], format: :both, passphrase: passphrase, iterations: iterations)
|
|
377
399
|
else
|
|
378
400
|
raise SerializationError, "Unsupported PKCS#8 private key format: #{format.inspect}"
|
|
379
401
|
end
|
|
380
402
|
end
|
|
381
403
|
|
|
382
|
-
def sign(message)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
404
|
+
def sign(message, context: "".b)
|
|
405
|
+
context = Signature.send(:validate_context!, context)
|
|
406
|
+
begin
|
|
407
|
+
PQCrypto.__send__(Signature.send(:native_method_for, @algorithm, :sign), String(message).b, @bytes, context)
|
|
408
|
+
rescue ArgumentError => e
|
|
409
|
+
raise InvalidKeyError, e.message
|
|
410
|
+
end
|
|
386
411
|
end
|
|
387
412
|
|
|
388
413
|
def sign_io(io, chunk_size: 1 << 20, context: "".b)
|
|
@@ -391,12 +416,13 @@ module PQCrypto
|
|
|
391
416
|
|
|
392
417
|
def wipe!
|
|
393
418
|
PQCrypto.secure_wipe(@bytes)
|
|
419
|
+
PQCrypto.secure_wipe(@seed) if @seed
|
|
394
420
|
self
|
|
395
421
|
end
|
|
396
422
|
|
|
397
423
|
def ==(other)
|
|
398
424
|
return false unless other.is_a?(SecretKey) && other.algorithm == algorithm
|
|
399
|
-
PQCrypto.__send__(:native_ct_equals, other.
|
|
425
|
+
PQCrypto.__send__(:native_ct_equals, other.send(:bytes_for_native), @bytes)
|
|
400
426
|
end
|
|
401
427
|
|
|
402
428
|
alias eql? ==
|
|
@@ -419,6 +445,17 @@ module PQCrypto
|
|
|
419
445
|
expected = Signature.details(@algorithm).fetch(:secret_key_bytes)
|
|
420
446
|
raise InvalidKeyError, "Invalid signature secret key length" unless @bytes.bytesize == expected
|
|
421
447
|
end
|
|
448
|
+
|
|
449
|
+
def validate_seed_length!
|
|
450
|
+
expected = PKCS8::PRIVATE_KEY_CHOICES.fetch(@algorithm).fetch(:seed_bytes)
|
|
451
|
+
raise InvalidKeyError, "Invalid signature seed length" unless @seed.bytesize == expected
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def ensure_seed_available!(format)
|
|
455
|
+
return if @seed
|
|
456
|
+
|
|
457
|
+
raise SerializationError, "ML-DSA #{format.inspect} PKCS#8 export requires original seed material"
|
|
458
|
+
end
|
|
422
459
|
end
|
|
423
460
|
end
|
|
424
461
|
end
|
data/lib/pq_crypto/version.rb
CHANGED
data/lib/pq_crypto.rb
CHANGED
|
@@ -37,6 +37,7 @@ require_relative "pq_crypto/pkcs8"
|
|
|
37
37
|
require_relative "pq_crypto/kem"
|
|
38
38
|
require_relative "pq_crypto/signature"
|
|
39
39
|
require_relative "pq_crypto/hybrid_kem"
|
|
40
|
+
require_relative "pq_crypto/key"
|
|
40
41
|
|
|
41
42
|
module PQCrypto
|
|
42
43
|
SUITES = {
|
|
@@ -65,6 +66,7 @@ module PQCrypto
|
|
|
65
66
|
hybrid_kem_encapsulate
|
|
66
67
|
hybrid_kem_expand_secret_key
|
|
67
68
|
hybrid_kem_expand_secret_key_object
|
|
69
|
+
hybrid_kem_expanded_secret_key_wipe
|
|
68
70
|
hybrid_kem_decapsulate
|
|
69
71
|
hybrid_kem_decapsulate_expanded
|
|
70
72
|
hybrid_kem_decapsulate_expanded_object
|
|
@@ -164,25 +166,25 @@ module PQCrypto
|
|
|
164
166
|
KEM_KEYPAIR_METHODS = {
|
|
165
167
|
ml_kem_512: :native_ml_kem_512_keypair_from_seed,
|
|
166
168
|
ml_kem_768: :native_ml_kem_keypair_from_seed,
|
|
167
|
-
ml_kem_1024: :native_ml_kem_1024_keypair_from_seed
|
|
169
|
+
ml_kem_1024: :native_ml_kem_1024_keypair_from_seed
|
|
168
170
|
}.freeze
|
|
169
171
|
|
|
170
172
|
KEM_ENCAPSULATE_METHODS = {
|
|
171
173
|
ml_kem_512: :native_test_ml_kem_512_encapsulate_from_seed,
|
|
172
174
|
ml_kem_768: :native_test_ml_kem_encapsulate_from_seed,
|
|
173
|
-
ml_kem_1024: :native_test_ml_kem_1024_encapsulate_from_seed
|
|
175
|
+
ml_kem_1024: :native_test_ml_kem_1024_encapsulate_from_seed
|
|
174
176
|
}.freeze
|
|
175
177
|
|
|
176
178
|
MLDSA_KEYPAIR_METHODS = {
|
|
177
179
|
ml_dsa_44: :native_test_ml_dsa_44_keypair_from_seed,
|
|
178
180
|
ml_dsa_65: :native_test_sign_keypair_from_seed,
|
|
179
|
-
ml_dsa_87: :native_test_ml_dsa_87_keypair_from_seed
|
|
181
|
+
ml_dsa_87: :native_test_ml_dsa_87_keypair_from_seed
|
|
180
182
|
}.freeze
|
|
181
183
|
|
|
182
184
|
MLDSA_SIGN_METHODS = {
|
|
183
185
|
ml_dsa_44: :native_test_ml_dsa_44_sign_from_seed,
|
|
184
186
|
ml_dsa_65: :native_test_sign_from_seed,
|
|
185
|
-
ml_dsa_87: :native_test_ml_dsa_87_sign_from_seed
|
|
187
|
+
ml_dsa_87: :native_test_ml_dsa_87_sign_from_seed
|
|
186
188
|
}.freeze
|
|
187
189
|
|
|
188
190
|
def self.ml_kem_keypair_from_seed(seed, algorithm: :ml_kem_768)
|
metadata
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pq_crypto
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Roman Haydarov
|
|
8
|
+
autorequire:
|
|
8
9
|
bindir: exe
|
|
9
10
|
cert_chain: []
|
|
10
|
-
date:
|
|
11
|
+
date: 2026-05-14 00:00:00.000000000 Z
|
|
11
12
|
dependencies:
|
|
12
13
|
- !ruby/object:Gem::Dependency
|
|
13
14
|
name: rake
|
|
@@ -314,6 +315,7 @@ files:
|
|
|
314
315
|
- lib/pq_crypto/errors.rb
|
|
315
316
|
- lib/pq_crypto/hybrid_kem.rb
|
|
316
317
|
- lib/pq_crypto/kem.rb
|
|
318
|
+
- lib/pq_crypto/key.rb
|
|
317
319
|
- lib/pq_crypto/pkcs8.rb
|
|
318
320
|
- lib/pq_crypto/serialization.rb
|
|
319
321
|
- lib/pq_crypto/signature.rb
|
|
@@ -328,6 +330,7 @@ metadata:
|
|
|
328
330
|
homepage_uri: https://github.com/roman-haidarov/pq_crypto
|
|
329
331
|
source_code_uri: https://github.com/roman-haidarov/pq_crypto/tree/main
|
|
330
332
|
changelog_uri: https://github.com/roman-haidarov/pq_crypto/blob/main/CHANGELOG.md
|
|
333
|
+
post_install_message:
|
|
331
334
|
rdoc_options: []
|
|
332
335
|
require_paths:
|
|
333
336
|
- lib
|
|
@@ -342,7 +345,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
342
345
|
- !ruby/object:Gem::Version
|
|
343
346
|
version: '0'
|
|
344
347
|
requirements: []
|
|
345
|
-
rubygems_version: 3.
|
|
348
|
+
rubygems_version: 3.3.27
|
|
349
|
+
signing_key:
|
|
346
350
|
specification_version: 4
|
|
347
351
|
summary: Primitive-first post-quantum cryptography for Ruby
|
|
348
352
|
test_files: []
|