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.
@@ -2,6 +2,6 @@
2
2
  #ifndef PQCRYPTO_VERSION_H
3
3
  #define PQCRYPTO_VERSION_H
4
4
 
5
- #define PQCRYPTO_VERSION "0.6.7"
5
+ #define PQCRYPTO_VERSION "0.6.8"
6
6
 
7
7
  #endif
@@ -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
- validate_algorithm_match!(algorithm, resolved_algorithm) if algorithm
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
- validate_algorithm_match!(algorithm, resolved_algorithm) if algorithm
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 validate_algorithm_match!(expected_algorithm, actual_algorithm)
145
- expected = resolve_algorithm!(expected_algorithm)
146
- return if expected == actual_algorithm
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
 
@@ -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
- length = 0
55
- length_octets.times do |i|
56
- byte = der.getbyte(offset + 1 + i)
57
- length = (length << 8) | byte
58
- end
58
+ length_octets
59
+ end
59
60
 
60
- if length < 0x80 || (length_octets > 1 && der.getbyte(offset + 1).zero?)
61
- raise SerializationError, "PKCS#8 DER length is not minimally encoded"
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
- [length, 1 + length_octets]
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
- encode_seed(algorithm, secret_material)
28
+ encode_component(algorithm, secret_material, format)
27
29
  when :expanded
28
- encode_expanded(algorithm, secret_material)
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
- decode_seed(algorithm, choice_der)
45
+ decode_component(algorithm, choice_der, :seed)
44
46
  when EXPANDED_TAG
45
47
  ensure_format_supported!(algorithm, :expanded)
46
- decode_expanded(algorithm, choice_der)
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 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
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
- 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]
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 encode_seed(algorithm, secret_material)
139
- seed = Internal.binary_string(secret_material)
140
- validate_seed_length!(algorithm, seed)
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(SEED_TAG, seed)
137
+ DER.encode_tlv(COMPONENT_TAGS.fetch(format), bytes)
143
138
  end
144
139
 
145
- def encode_expanded(algorithm, secret_material)
146
- bytes = Internal.binary_string(secret_material)
147
- validate_expanded_length!(algorithm, bytes)
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
- DER.encode_tlv(EXPANDED_TAG, bytes)
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)
@@ -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
- validate_algorithm_match!(algorithm, resolved_algorithm) if algorithm
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
- validate_algorithm_match!(algorithm, resolved_algorithm) if algorithm
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 validate_algorithm_match!(expected_algorithm, actual_algorithm)
151
- expected = resolve_algorithm!(expected_algorithm)
152
- return if expected == actual_algorithm
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
- begin
169
- tr = PQCrypto.__send__(:_native_mldsa_extract_tr, algorithm, sk_bytes)
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
- begin
198
- tr = PQCrypto.__send__(:_native_mldsa_compute_tr, algorithm, pk_bytes)
199
- rescue ArgumentError => e
200
- raise InvalidKeyError, e.message
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
- builder = PQCrypto.__send__(:_native_mldsa_mu_builder_new, tr, context)
204
- builder_consumed = false
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
- builder_consumed = true
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) unless builder_consumed
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
 
@@ -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
- raise SerializationError, "SPKI DER contains trailing data" unless outer.to_der.b == input
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
- entry = AlgorithmRegistry.fetch(algorithm)
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"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PQCrypto
4
- VERSION = "0.6.7"
4
+ VERSION = "0.6.8"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pq_crypto
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.7
4
+ version: 0.6.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roman Haydarov