Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lib/cose.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand Down
59 changes: 59 additions & 0 deletions lib/cose/keys.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@ defmodule COSE.Keys.Symmetric do
defstruct [:kty, :kid, :alg, :key_ops, :base_iv, :k]
end

defmodule ECDSASignature do
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case we keep this module (see comment below about struct definition), the name should be COSE.Keys. ECDSASignature.

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)
<<r::size(size)-unit(8), s::size(size)-unit(8)>> = 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]

Expand Down Expand Up @@ -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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason for not following defining the key struct here, as in the rest of the library?

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
42 changes: 42 additions & 0 deletions lib/cose/messages/sign1.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the introduced functions duplicate a fair amount of the body, including sign_encode(:es256 ...), sign(:es256, ...), etc. Could it be improved?

def sign(msg, key, external_aad \\ <<>>) do
to_be_signed = CBOR.encode(sig_structure(msg, external_aad))

Expand All @@ -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)
Expand All @@ -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))

Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ defmodule COSE.MixProject do

def application do
[
extra_applications: [:logger, :crypto]
extra_applications: [:logger, :crypto, :public_key]
]
end

Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
@@ -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"},
}
30 changes: 30 additions & 0 deletions test/sign1_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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