diff --git a/lib/cose.ex b/lib/cose.ex index 82b17cd..a8d8da8 100644 --- a/lib/cose.ex +++ b/lib/cose.ex @@ -21,7 +21,8 @@ defmodule COSE do direct: -6, aes_ccm_16_64_128: 10, ecdh_ss_hkdf_256: -27, - eddsa: -8 + eddsa: -8, + es256: -7 } def algorithm(alg) when is_atom(alg), do: @cose_algs[alg] def algorithm(alg) when is_integer(alg), do: invert_map(@cose_algs)[alg] @@ -30,7 +31,8 @@ defmodule COSE do alg: 1, kid: 4, iv: 5, - party_v_identity: -24 + party_v_identity: -24, + ctyp: 3 } def header(hdr) when is_atom(hdr), do: @cose_headers[hdr] def header(hdr) when is_integer(hdr), do: invert_map(@cose_headers)[hdr] diff --git a/lib/cose/keys.ex b/lib/cose/keys.ex index ee34b0f..ca5bc5e 100644 --- a/lib/cose/keys.ex +++ b/lib/cose/keys.ex @@ -2,6 +2,30 @@ defmodule COSE.Keys.Symmetric do defstruct [:kty, :kid, :alg, :key_ops, :base_iv, :k] end +defmodule ECDSASignature do + require Record + + Record.defrecord( + :ecdsa_signature, + :"ECDSA-Sig-Value", + Record.extract(:"ECDSA-Sig-Value", from_lib: "public_key/include/OTP-PUB-KEY.hrl") + ) + + def new(r, s) when is_integer(r) and is_integer(s) do + ecdsa_signature(r: r, s: s) + end + + def new(raw) when is_binary(raw) do + size = raw |> byte_size() |> div(2) + <> = raw + new(r, s) + end + + def to_der(ecdsa_signature() = signature) do + :public_key.der_encode(:"ECDSA-Sig-Value", signature) + end +end + defmodule COSE.Keys.OKP do defstruct [:kty, :kid, :alg, :key_ops, :base_iv, :crv, :x, :d] @@ -36,3 +60,38 @@ defmodule COSE.Keys.OKP do :crypto.verify(:eddsa, :sha256, to_be_verified, signature, [ver_key.x, :ed25519]) end end + +defmodule COSE.Keys.ECDSA do + def sign(:es256, to_be_signed_bytes, private_key) do + :crypto.sign(:ecdsa, :sha256, to_be_signed_bytes, [private_key, :secp256r1]) + |> encode_der_as_cose() + |> COSE.tag_as_byte() + end + + def verify(:es256, to_be_verified_bytes, %CBOR.Tag{tag: :bytes, value: cose_encoded_signature}, public_key) do + signature_der_bytes = ECDSASignature.new(cose_encoded_signature) |> ECDSASignature.to_der() + :crypto.verify(:ecdsa, :sha256, to_be_verified_bytes, signature_der_bytes, [public_key, :secp256r1]) + end + + defp encode_der_as_cose(der_signature) do + # The DER signature is a sequence of two integers, r and s, each of which is + # encoded as a signed big-endian integer. The COSE signature is a CBOR array + # of two integers, r and s, each of which is encoded as a positive big-endian + # integer. + {:"ECDSA-Sig-Value", r, s} = :public_key.der_decode(:"ECDSA-Sig-Value", der_signature) + # Convert the integers r and s into big endian binaries + r_bytes = :binary.encode_unsigned(r, :big) + s_bytes = :binary.encode_unsigned(s, :big) + # make both of these the same length by padding the shorter one with leading zeros + r_bytes = pad_leading(r_bytes, byte_size(s_bytes) - byte_size(r_bytes)) + s_bytes = pad_leading(s_bytes, byte_size(r_bytes) - byte_size(s_bytes)) + # concatenate the two integers + r_bytes <> s_bytes + end + + defp pad_leading(binary, size) when is_binary(binary) do + padding_size = max(size - byte_size(binary), 0) + padding = String.duplicate(<<0>>, padding_size) + padding <> binary + end +end diff --git a/lib/cose/messages/sign1.ex b/lib/cose/messages/sign1.ex index 6b35228..48768da 100644 --- a/lib/cose/messages/sign1.ex +++ b/lib/cose/messages/sign1.ex @@ -19,6 +19,28 @@ defmodule COSE.Messages.Sign1 do CBOR.encode(%CBOR.Tag{tag: 18, value: value}) end + def sign_encode(:es256, msg, key) do + msg = sign(:es256, msg, key, <<>>) + + value = [ + COSE.Headers.tag_phdr(msg.phdr), + msg.uhdr, + msg.payload, + msg.signature + ] + + CBOR.encode(%CBOR.Tag{tag: 18, value: value}) + end + + def sign(:es256, msg, private_key, external_aad) do + to_be_signed = CBOR.encode(sig_structure(msg, external_aad)) + + %__MODULE__{ + msg + | signature: COSE.Keys.ECDSA.sign(:es256, to_be_signed, private_key) + } + end + def sign(msg, key, external_aad \\ <<>>) do to_be_signed = CBOR.encode(sig_structure(msg, external_aad)) @@ -38,6 +60,16 @@ defmodule COSE.Messages.Sign1 do end end + def verify_decode(:es256, encoded_msg, key) do + msg = decode(encoded_msg) + + if verify(:es256, msg, key, <<>>) do + msg + else + false + end + end + def decode(encoded_msg) do {:ok, %CBOR.Tag{tag: 18, value: [phdr, uhdr, payload, signature]}, _} = CBOR.decode(encoded_msg) @@ -50,6 +82,16 @@ defmodule COSE.Messages.Sign1 do } end + def verify(:es256, msg, public_key, external_aad) do + to_be_verified = CBOR.encode(sig_structure(msg, external_aad)) + + if COSE.Keys.ECDSA.verify(:es256, to_be_verified, msg.signature, public_key) do + msg + else + false + end + end + def verify(msg, ver_key, external_aad \\ <<>>) do to_be_verified = CBOR.encode(sig_structure(msg, external_aad)) diff --git a/mix.exs b/mix.exs index 5869e29..5cfe88f 100644 --- a/mix.exs +++ b/mix.exs @@ -13,7 +13,7 @@ defmodule COSE.MixProject do def application do [ - extra_applications: [:logger, :crypto] + extra_applications: [:logger, :crypto, :public_key] ] end diff --git a/mix.lock b/mix.lock index bb4c1a4..2dccf58 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,5 @@ %{ - "b58": {:git, "https://github.com/dwyl/base58.git", "711b29b62f50394eef3602a2b52dc28d536a5894", []}, + "b58": {:hex, :b58, "1.0.3", "d300d6ae5a3de956a54b9e8220e924e4fee1a349de983df2340fe61e0e464202", [:mix], [], "hexpm", "af62a98a8661fd89978cf3a3a4b5b2ebe82209de6ac6164f0b112e36af72fc59"}, "cbor": {:hex, :cbor, "1.0.0", "35d33a26f6420ce3d2d01c0b1463a748b34c537d5609fc40116daf3666700d36", [:mix], [], "hexpm", "cc5e21e0fa5a0330715a3806c67bc294f8b65d07160f751b5bd6058bed1962ac"}, "hkdf_erlang": {:hex, :hkdf_erlang, "0.1.1", "b35538edfffefc44d6855b2bfe2dc00909c2a4365f41e47d2fe493811006955e", [:rebar3], [], "hexpm", "eb784bda0df4e964fd69f622310752198fb0218543e1a7393912abc7a5ce926e"}, } diff --git a/test/sign1_test.exs b/test/sign1_test.exs index 3f4b38c..dbd64a3 100644 --- a/test/sign1_test.exs +++ b/test/sign1_test.exs @@ -32,5 +32,35 @@ defmodule COSETest.Sign1 do verified_msg = Messages.Sign1.verify_decode(encoded_msg, key) assert verified_msg == Sign1.sign(msg, key) end + + test "round trip valid es256" do + {public_key, private_key} = :crypto.generate_key(:ecdh, :secp256r1) + msg = Sign1.build("content to sign", %{alg: :es256}) + encoded_msg = Sign1.sign_encode(:es256, msg, private_key) + assert Messages.Sign1.verify_decode(:es256, encoded_msg, public_key) + end + + # Precooked message taken from cose repo https://github.com/cose-wg/Examples/blob/master/ecdsa-examples/ecdsa-sig-01.json + test "precooked es256 message produces expected to be signed bytes" do + msg = Sign1.build("This is the content.", %{alg: :es256, ctyp: 0}) + encoded_tbs_bytes = CBOR.encode(COSE.Messages.Sign1.sig_structure(msg, <<>>)) + assert encoded_tbs_bytes == Base.decode16!("846A5369676E61747572653145A2012603004054546869732069732074686520636F6E74656E742E") + end + + test "verify precooked es256 message passes verification" do + encoded_message = Base.decode16!("D28445A201260300A10442313154546869732069732074686520636F6E74656E742E58406520BBAF2081D7E0ED0F95F76EB0733D667005F7467CEC4B87B9381A6BA1EDE8E00DF29F32A37230F39A842A54821FDD223092819D7728EFB9D3A0080B75380B") + private_key_hex = "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM=" + private_key_bytes = Base.decode64!(private_key_hex, case: :lower) + {public_key, _} = :crypto.generate_key(:ecdh, :secp256r1, private_key_bytes) + assert Messages.Sign1.verify_decode(:es256, encoded_message, public_key) + end + + test "wrong public key fails es256 message verification" do + {_, private_key} = :crypto.generate_key(:ecdh, :secp256r1) + msg = Sign1.build("content to sign", %{alg: :es256}) + encoded_msg = Sign1.sign_encode(:es256, msg, private_key) + {wrong_public_key, _} = :crypto.generate_key(:ecdh, :secp256r1) + refute Messages.Sign1.verify_decode(:es256, encoded_msg, wrong_public_key) + end end end