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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +108 -0
- data/README.md +11 -0
- data/SECURITY.md +46 -0
- data/ext/pqcrypto/extconf.rb +4 -0
- data/ext/pqcrypto/pqcrypto_native_api.h +6 -0
- data/ext/pqcrypto/pqcrypto_ruby_secure.c +122 -11
- data/ext/pqcrypto/pqcrypto_secure.c +553 -296
- data/ext/pqcrypto/pqcrypto_secure.h +14 -1
- data/ext/pqcrypto/pqcrypto_version.h +1 -1
- data/lib/pq_crypto/internal.rb +20 -0
- data/lib/pq_crypto/kem.rb +52 -15
- data/lib/pq_crypto/key.rb +14 -8
- data/lib/pq_crypto/pkcs8/der.rb +26 -11
- data/lib/pq_crypto/pkcs8/private_key_choice.rb +21 -25
- data/lib/pq_crypto/pkcs8.rb +13 -5
- data/lib/pq_crypto/signature.rb +61 -48
- data/lib/pq_crypto/spki.rb +39 -24
- data/lib/pq_crypto/version.rb +1 -1
- data/lib/pq_crypto.rb +9 -0
- metadata +1 -1
|
@@ -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);
|
data/lib/pq_crypto/internal.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
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.
|
|
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(
|
|
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(
|
|
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)
|
data/lib/pq_crypto/pkcs8/der.rb
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
byte = der.getbyte(offset + 1 + i)
|
|
57
|
-
length = (length << 8) | byte
|
|
58
|
-
end
|
|
58
|
+
length_octets
|
|
59
|
+
end
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
encode_component(algorithm, secret_material, format)
|
|
27
29
|
when :expanded
|
|
28
|
-
|
|
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
|
-
|
|
45
|
+
decode_component(algorithm, choice_der, :seed)
|
|
44
46
|
when EXPANDED_TAG
|
|
45
47
|
ensure_format_supported!(algorithm, :expanded)
|
|
46
|
-
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
[algorithm,
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
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(
|
|
137
|
+
DER.encode_tlv(COMPONENT_TAGS.fetch(format), bytes)
|
|
143
138
|
end
|
|
144
139
|
|
|
145
|
-
def
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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)
|
data/lib/pq_crypto/pkcs8.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
data/lib/pq_crypto/signature.rb
CHANGED
|
@@ -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),
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
146
|
+
def secret_key_checkable?(algorithm)
|
|
147
|
+
NATIVE_DISPATCH.key?(algorithm) && NATIVE_DISPATCH.fetch(algorithm).key?(:check_secret_key)
|
|
148
|
+
end
|
|
139
149
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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)
|
|
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.
|
|
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
|