pq_crypto 0.6.7 → 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 +9 -0
- data/ext/pqcrypto/pqcrypto_ruby_secure.c +41 -11
- data/ext/pqcrypto/pqcrypto_secure.c +501 -296
- data/ext/pqcrypto/pqcrypto_version.h +1 -1
- data/lib/pq_crypto/internal.rb +10 -0
- data/lib/pq_crypto/kem.rb +5 -10
- data/lib/pq_crypto/pkcs8/der.rb +26 -11
- data/lib/pq_crypto/pkcs8/private_key_choice.rb +21 -25
- data/lib/pq_crypto/signature.rb +28 -40
- data/lib/pq_crypto/spki.rb +36 -23
- data/lib/pq_crypto/version.rb +1 -1
- metadata +1 -1
data/lib/pq_crypto/internal.rb
CHANGED
|
@@ -29,5 +29,15 @@ module PQCrypto
|
|
|
29
29
|
def constant_time_equal?(left, right)
|
|
30
30
|
PQCrypto.__send__(:native_ct_equals, left, right)
|
|
31
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
|
|
32
42
|
end
|
|
33
43
|
end
|
data/lib/pq_crypto/kem.rb
CHANGED
|
@@ -90,13 +90,13 @@ module PQCrypto
|
|
|
90
90
|
|
|
91
91
|
def public_key_from_spki_der(der, algorithm: nil)
|
|
92
92
|
resolved_algorithm, bytes = SPKI.decode_der(der)
|
|
93
|
-
|
|
93
|
+
validate_spki_algorithm!(algorithm, resolved_algorithm) if algorithm
|
|
94
94
|
PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
|
|
95
95
|
end
|
|
96
96
|
|
|
97
97
|
def public_key_from_spki_pem(pem, algorithm: nil)
|
|
98
98
|
resolved_algorithm, bytes = SPKI.decode_pem(pem)
|
|
99
|
-
|
|
99
|
+
validate_spki_algorithm!(algorithm, resolved_algorithm) if algorithm
|
|
100
100
|
PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
|
|
101
101
|
end
|
|
102
102
|
|
|
@@ -141,14 +141,9 @@ module PQCrypto
|
|
|
141
141
|
NATIVE_DISPATCH.fetch(resolve_algorithm!(algorithm)).fetch(operation)
|
|
142
142
|
end
|
|
143
143
|
|
|
144
|
-
def
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
raise SerializationError,
|
|
149
|
-
"Expected #{expected.inspect}, got #{actual_algorithm.inspect} (SPKI key algorithm mismatch)"
|
|
150
|
-
rescue UnsupportedAlgorithmError => e
|
|
151
|
-
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")
|
|
152
147
|
end
|
|
153
148
|
end
|
|
154
149
|
|
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/signature.rb
CHANGED
|
@@ -80,13 +80,13 @@ module PQCrypto
|
|
|
80
80
|
|
|
81
81
|
def public_key_from_spki_der(der, algorithm: nil)
|
|
82
82
|
resolved_algorithm, bytes = SPKI.decode_der(der)
|
|
83
|
-
|
|
83
|
+
validate_spki_algorithm!(algorithm, resolved_algorithm) if algorithm
|
|
84
84
|
PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
def public_key_from_spki_pem(pem, algorithm: nil)
|
|
88
88
|
resolved_algorithm, bytes = SPKI.decode_pem(pem)
|
|
89
|
-
|
|
89
|
+
validate_spki_algorithm!(algorithm, resolved_algorithm) if algorithm
|
|
90
90
|
PublicKey.new(resolve_algorithm!(resolved_algorithm), bytes)
|
|
91
91
|
end
|
|
92
92
|
|
|
@@ -147,14 +147,9 @@ module PQCrypto
|
|
|
147
147
|
NATIVE_DISPATCH.key?(algorithm) && NATIVE_DISPATCH.fetch(algorithm).key?(:check_secret_key)
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
-
def
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
raise SerializationError,
|
|
155
|
-
"Expected #{expected.inspect}, got #{actual_algorithm.inspect} (SPKI key algorithm mismatch)"
|
|
156
|
-
rescue UnsupportedAlgorithmError => e
|
|
157
|
-
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")
|
|
158
153
|
end
|
|
159
154
|
|
|
160
155
|
def _streaming_sign(secret_key, io, chunk_size, context)
|
|
@@ -165,25 +160,12 @@ module PQCrypto
|
|
|
165
160
|
|
|
166
161
|
algorithm = secret_key.algorithm
|
|
167
162
|
sk_bytes = secret_key.__send__(:bytes_for_native)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
rescue ArgumentError => e
|
|
171
|
-
raise InvalidKeyError, e.message
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
builder = PQCrypto.__send__(:_native_mldsa_mu_builder_new, tr, context)
|
|
175
|
-
builder_consumed = false
|
|
176
|
-
mu = nil
|
|
177
|
-
begin
|
|
178
|
-
_drain_io_into_builder(io, builder, chunk_size)
|
|
179
|
-
mu = PQCrypto.__send__(:_native_mldsa_mu_builder_finalize, builder)
|
|
180
|
-
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|
|
|
181
165
|
PQCrypto.__send__(:_native_mldsa_sign_mu, algorithm, mu, sk_bytes)
|
|
182
|
-
ensure
|
|
183
|
-
PQCrypto.__send__(:_native_mldsa_mu_builder_release, builder) unless builder_consumed
|
|
184
|
-
PQCrypto.secure_wipe(tr) if tr && !tr.frozen?
|
|
185
|
-
PQCrypto.secure_wipe(mu) if mu && !mu.frozen?
|
|
186
166
|
end
|
|
167
|
+
ensure
|
|
168
|
+
Internal.safe_wipe(tr) if defined?(tr)
|
|
187
169
|
end
|
|
188
170
|
|
|
189
171
|
def _streaming_verify(public_key, io, signature, chunk_size, context)
|
|
@@ -194,26 +176,32 @@ module PQCrypto
|
|
|
194
176
|
|
|
195
177
|
algorithm = public_key.algorithm
|
|
196
178
|
pk_bytes = public_key.__send__(:bytes_for_native)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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)
|
|
201
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
|
|
202
193
|
|
|
203
|
-
|
|
204
|
-
|
|
194
|
+
def with_streaming_mu(tr, io, chunk_size, context)
|
|
195
|
+
builder = nil
|
|
205
196
|
mu = nil
|
|
206
|
-
sig_bytes = Internal.binary_string(signature)
|
|
207
197
|
begin
|
|
198
|
+
builder = PQCrypto.__send__(:_native_mldsa_mu_builder_new, tr, context)
|
|
208
199
|
_drain_io_into_builder(io, builder, chunk_size)
|
|
209
200
|
mu = PQCrypto.__send__(:_native_mldsa_mu_builder_finalize, builder)
|
|
210
|
-
|
|
211
|
-
PQCrypto.__send__(:_native_mldsa_verify_mu, algorithm, mu, sig_bytes, pk_bytes)
|
|
201
|
+
yield mu
|
|
212
202
|
ensure
|
|
213
|
-
PQCrypto.__send__(:_native_mldsa_mu_builder_release, builder)
|
|
214
|
-
|
|
215
|
-
PQCrypto.secure_wipe(tr) if tr && !tr.frozen?
|
|
216
|
-
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)
|
|
217
205
|
end
|
|
218
206
|
end
|
|
219
207
|
|
data/lib/pq_crypto/spki.rb
CHANGED
|
@@ -39,30 +39,9 @@ module PQCrypto
|
|
|
39
39
|
def decode_der(der)
|
|
40
40
|
input = String(der).b
|
|
41
41
|
outer = decode_asn1(input)
|
|
42
|
-
|
|
43
|
-
raise SerializationError, "SPKI must be an ASN.1 SEQUENCE" unless outer.is_a?(OpenSSL::ASN1::Sequence)
|
|
44
|
-
raise SerializationError, "SPKI SEQUENCE must contain exactly 2 elements" unless outer.value.size == 2
|
|
45
|
-
|
|
46
|
-
algorithm_identifier, subject_public_key = outer.value
|
|
42
|
+
algorithm_identifier, subject_public_key = spki_elements(outer, input)
|
|
47
43
|
algorithm = decode_algorithm_identifier(algorithm_identifier)
|
|
48
|
-
|
|
49
|
-
validate_public_key_algorithm!(algorithm, entry)
|
|
50
|
-
|
|
51
|
-
unless subject_public_key.is_a?(OpenSSL::ASN1::BitString)
|
|
52
|
-
raise SerializationError, "SPKI subjectPublicKey must be a BIT STRING"
|
|
53
|
-
end
|
|
54
|
-
unless subject_public_key.unused_bits.zero?
|
|
55
|
-
raise SerializationError, "SPKI subjectPublicKey must have zero unused bits"
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
bytes = String(subject_public_key.value).b
|
|
59
|
-
expected = entry.fetch(:public_key_bytes)
|
|
60
|
-
unless bytes.bytesize == expected
|
|
61
|
-
raise SerializationError,
|
|
62
|
-
"Invalid #{algorithm.inspect} SPKI public key length: expected #{expected}, got #{bytes.bytesize}"
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
[algorithm, bytes]
|
|
44
|
+
[algorithm, decode_subject_public_key(algorithm, subject_public_key)]
|
|
66
45
|
rescue OpenSSL::OpenSSLError, TypeError => e
|
|
67
46
|
raise SerializationError, e.message
|
|
68
47
|
end
|
|
@@ -80,6 +59,40 @@ module PQCrypto
|
|
|
80
59
|
raise SerializationError, e.message
|
|
81
60
|
end
|
|
82
61
|
|
|
62
|
+
def spki_elements(outer, input)
|
|
63
|
+
raise SerializationError, "SPKI DER contains trailing data" unless outer.to_der.b == input
|
|
64
|
+
raise SerializationError, "SPKI must be an ASN.1 SEQUENCE" unless outer.is_a?(OpenSSL::ASN1::Sequence)
|
|
65
|
+
raise SerializationError, "SPKI SEQUENCE must contain exactly 2 elements" unless outer.value.size == 2
|
|
66
|
+
|
|
67
|
+
outer.value
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def decode_subject_public_key(algorithm, subject_public_key)
|
|
71
|
+
entry = AlgorithmRegistry.fetch(algorithm)
|
|
72
|
+
validate_public_key_algorithm!(algorithm, entry)
|
|
73
|
+
validate_subject_public_key!(subject_public_key)
|
|
74
|
+
|
|
75
|
+
bytes = String(subject_public_key.value).b
|
|
76
|
+
validate_public_key_length!(algorithm, bytes, entry.fetch(:public_key_bytes))
|
|
77
|
+
bytes
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def validate_subject_public_key!(subject_public_key)
|
|
81
|
+
unless subject_public_key.is_a?(OpenSSL::ASN1::BitString)
|
|
82
|
+
raise SerializationError, "SPKI subjectPublicKey must be a BIT STRING"
|
|
83
|
+
end
|
|
84
|
+
return if subject_public_key.unused_bits.zero?
|
|
85
|
+
|
|
86
|
+
raise SerializationError, "SPKI subjectPublicKey must have zero unused bits"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def validate_public_key_length!(algorithm, bytes, expected)
|
|
90
|
+
return if bytes.bytesize == expected
|
|
91
|
+
|
|
92
|
+
raise SerializationError,
|
|
93
|
+
"Invalid #{algorithm.inspect} SPKI public key length: expected #{expected}, got #{bytes.bytesize}"
|
|
94
|
+
end
|
|
95
|
+
|
|
83
96
|
def decode_algorithm_identifier(value)
|
|
84
97
|
unless value.is_a?(OpenSSL::ASN1::Sequence)
|
|
85
98
|
raise SerializationError, "SPKI algorithm must be an AlgorithmIdentifier SEQUENCE"
|
data/lib/pq_crypto/version.rb
CHANGED