@@ -11,7 +11,7 @@ defmodule AwsEncryptionSdk.Crypto.ECDSA do
1111 """
1212
1313 @ type key_pair :: { private_key :: binary ( ) , public_key :: binary ( ) }
14- @ type curve :: :secp384r1
14+ @ type curve :: :secp384r1 | :secp256r1
1515
1616 @ doc """
1717 Generates an ECDSA key pair for the P-384 curve.
@@ -183,6 +183,129 @@ defmodule AwsEncryptionSdk.Crypto.ECDSA do
183183 @ spec verify ( binary ( ) , binary ( ) , binary ( ) , curve ( ) ) :: boolean ( )
184184 def verify ( message , signature , public_key , :secp384r1 )
185185 when is_binary ( message ) and is_binary ( signature ) and is_binary ( public_key ) do
186- :crypto . verify ( :ecdsa , :sha384 , message , signature , [ public_key , :secp384r1 ] )
186+ # Normalize public key to uncompressed format if needed
187+ normalized_key = normalize_public_key ( public_key , :secp384r1 )
188+ :crypto . verify ( :ecdsa , :sha384 , message , signature , [ normalized_key , :secp384r1 ] )
189+ end
190+
191+ @ doc """
192+ Normalizes a public key to uncompressed format.
193+
194+ Handles both compressed (0x02/0x03 prefix, 49 bytes for P-384 or 33 bytes for P-256)
195+ and uncompressed (0x04 prefix, 97 bytes for P-384 or 65 bytes for P-256) formats.
196+
197+ The curve is auto-detected from the key size when possible:
198+ - 33 bytes compressed or 65 bytes uncompressed → secp256r1
199+ - 49 bytes compressed or 97 bytes uncompressed → secp384r1
200+ """
201+ @ spec normalize_public_key ( binary ( ) , curve ( ) ) :: binary ( )
202+ def normalize_public_key ( << 0x04 , _rest :: binary >> = uncompressed_key , _curve ) do
203+ # Already uncompressed
204+ uncompressed_key
205+ end
206+
207+ # 33-byte key = P-256 compressed (1 prefix + 32 bytes x)
208+ def normalize_public_key ( << prefix , _x_coord :: binary - size ( 32 ) >> = compressed_key , _curve )
209+ when prefix in [ 0x02 , 0x03 ] do
210+ decompress_ec_point ( compressed_key , :secp256r1 )
211+ end
212+
213+ # 49-byte key = P-384 compressed (1 prefix + 48 bytes x)
214+ def normalize_public_key ( << prefix , _x_coord :: binary - size ( 48 ) >> = compressed_key , _curve )
215+ when prefix in [ 0x02 , 0x03 ] do
216+ decompress_ec_point ( compressed_key , :secp384r1 )
217+ end
218+
219+ def normalize_public_key ( key , _curve ) , do: key
220+
221+ # Manual EC point decompression for NIST curves
222+ # Uses simplified modular square root since p ≡ 3 (mod 4) for both P-256 and P-384
223+ # See SEC 1 v2.0 Section 2.3.4: https://www.secg.org/sec1-v2.pdf
224+
225+ # secp256r1 (P-256): 33-byte compressed key (1 prefix + 32 bytes x)
226+ defp decompress_ec_point ( << prefix , x_bytes :: binary - size ( 32 ) >> , :secp256r1 )
227+ when prefix in [ 0x02 , 0x03 ] do
228+ # secp256r1 curve parameters
229+ # p = 2^256 - 2^224 + 2^192 + 2^96 - 1
230+ p = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF
231+
232+ # b coefficient from secp256r1 specification
233+ b = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B
234+
235+ decompress_with_params ( prefix , x_bytes , p , b , 32 )
236+ end
237+
238+ # secp384r1 (P-384): 49-byte compressed key (1 prefix + 48 bytes x)
239+ defp decompress_ec_point ( << prefix , x_bytes :: binary - size ( 48 ) >> , :secp384r1 )
240+ when prefix in [ 0x02 , 0x03 ] do
241+ # secp384r1 curve parameters
242+ # p = 2^384 - 2^128 - 2^96 + 2^32 - 1
243+ p =
244+ 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF
245+
246+ # b coefficient from secp384r1 specification
247+ b =
248+ 0xB3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF
249+
250+ decompress_with_params ( prefix , x_bytes , p , b , 48 )
251+ end
252+
253+ defp decompress_ec_point ( key , _curve ) , do: key
254+
255+ # Generic decompression using curve parameters
256+ defp decompress_with_params ( prefix , x_bytes , p , b , coord_size ) do
257+ # a = -3 (mod p), which is p - 3 for both curves
258+ a = p - 3
259+
260+ # Convert x bytes to integer
261+ x = :binary . decode_unsigned ( x_bytes , :big )
262+
263+ # Calculate y² = x³ + ax + b (mod p)
264+ # Note: The curve equation is y² = x³ - 3x + b, and a = -3
265+ x_cubed = mod_pow ( x , 3 , p )
266+ ax = mod ( a * x , p )
267+ y_squared = mod ( x_cubed + ax + b , p )
268+
269+ # Since p ≡ 3 (mod 4), we can compute sqrt using: y = y²^((p+1)/4) mod p
270+ exponent = div ( p + 1 , 4 )
271+ y = mod_pow ( y_squared , exponent , p )
272+
273+ # Determine if we need to negate y based on the prefix
274+ # prefix 0x02 = even y, prefix 0x03 = odd y
275+ y_is_odd = Integer . mod ( y , 2 ) == 1
276+ prefix_wants_odd = prefix == 0x03
277+
278+ final_y =
279+ if y_is_odd == prefix_wants_odd do
280+ y
281+ else
282+ # Use the other root: -y mod p = p - y
283+ p - y
284+ end
285+
286+ # Convert y back to binary (big-endian, zero-padded)
287+ y_bytes = :binary . encode_unsigned ( final_y , :big )
288+ y_bytes_padded = pad_to_length ( y_bytes , coord_size )
289+
290+ # Build uncompressed point: 0x04 || x || y
291+ << 0x04 , x_bytes :: binary , y_bytes_padded :: binary >>
292+ end
293+
294+ # Modular exponentiation using Erlang's built-in
295+ defp mod_pow ( base , exp , mod ) do
296+ :crypto . mod_pow ( base , exp , mod )
297+ |> :binary . decode_unsigned ( :big )
298+ end
299+
300+ # Modular reduction for potentially negative numbers
301+ defp mod ( n , p ) when n >= 0 , do: Integer . mod ( n , p )
302+ defp mod ( n , p ) , do: Integer . mod ( n + p , p )
303+
304+ # Pad binary to specified length with leading zeros
305+ defp pad_to_length ( binary , length ) when byte_size ( binary ) >= length , do: binary
306+
307+ defp pad_to_length ( binary , length ) do
308+ padding_size = length - byte_size ( binary )
309+ << 0 :: size ( padding_size * 8 ) , binary :: binary >>
187310 end
188311end
0 commit comments