defmodule Wax.CoseKey do
@kty 1
@alg 3
@curve -1
@key_type_OKP 1
@key_type_EC2 2
@key_type_RSA 3
@cose_alg_string %{
-65535 => "RSASSA-PKCS1-v1_5 w/ SHA-1",
-259 => "RS512 (TEMPORARY - registered 2018-04-19, expires 2019-04-19)",
-258 => "RS384 (TEMPORARY - registered 2018-04-19, expires 2019-04-19)",
-257 => "RS256 (TEMPORARY - registered 2018-04-19, expires 2019-04-19)",
-47 => "ES256K",
-39 => "PS512",
-38 => "PS384",
-37 => "PS256",
-36 => "ES512",
-35 => "ES384",
-8 => "EdDSA",
-7 => "ES256"
}
@pss_algs [-39, -38, -37]
@cose_ec_named_curves %{
1 => :secp256r1,
2 => :secp384r1,
3 => :secp521r1,
6 => :ed25519,
7 => :ed448,
8 => :secp256k1
}
@typedoc """
A cose key
## Example:
```elixir
%{
-3 => <<182, 81, 183, 218, 92, 107, 106, 120, 60, 51, 75, 104, 141, 130,
119, 232, 34, 245, 84, 203, 246, 165, 148, 179, 169, 31, 205, 126, 241,
188, 241, 176>>,
-2 => <<89, 29, 193, 225, 4, 234, 101, 162, 32, 6, 15, 14, 130, 179, 223,
207, 53, 2, 134, 184, 178, 127, 51, 145, 57, 180, 104, 242, 138, 96, 27,
221>>,
-1 => 1,
1 => 2,
3 => -7
}
```
"""
@type t :: %{required(integer()) => integer | binary()}
@type cose_alg :: integer()
@doc false
@spec supported_algs() :: %{required(cose_alg()) => String.t()}
def supported_algs() do
@cose_alg_string
end
@doc false
@spec verify(message :: binary(), t(), signature :: binary()) :: :ok | {:error, Exception.t()}
def verify(msg, %{@alg => alg} = cose_key, sig) when alg in @pss_algs do
# Use PSS padding; requires workaround for https://bugs.erlang.org/browse/ERL-878
{:RSAPublicKey, n, e} = to_erlang_public_key(cose_key)
digest = to_erlang_digest(cose_key)
if :crypto.verify(:rsa, digest, msg, sig, [e, n], rsa_padding: :rsa_pkcs1_pss_padding) do
:ok
else
{:error, %Wax.InvalidSignatureError{}}
end
end
def verify(msg, %{@alg => alg} = cose_key, sig)
when alg in unquote(Map.keys(@cose_alg_string)) do
key = to_erlang_public_key(cose_key)
digest = to_erlang_digest(cose_key)
if :public_key.verify(msg, digest, sig, key) do
:ok
else
{:error, %Wax.InvalidSignatureError{}}
end
end
def verify(_, _, _) do
{:error, %Wax.UnsupportedSignatureAlgorithmError{}}
end
@doc false
@spec to_erlang_public_key(t()) :: :public_key.public_key()
def to_erlang_public_key(%{@kty => @key_type_EC2, -1 => curve, -2 => x, -3 => y}) do
{
{:ECPoint, <<4>> <> x <> y},
# here we convert the curve name to its OID since certificates against which
# it may be compared use OIDs
# :public_key functions will work the same independantly of the format
{:namedCurve, :pubkey_cert_records.namedCurves(@cose_ec_named_curves[curve])}
}
end
def to_erlang_public_key(%{@kty => @key_type_RSA, -1 => n, -2 => e}) do
nb_bytes_n = byte_size(n)
nb_bytes_e = byte_size(e)
<<n_int::unsigned-big-integer-size(nb_bytes_n)-unit(8)>> = n
<<e_int::unsigned-big-integer-size(nb_bytes_e)-unit(8)>> = e
{:RSAPublicKey, n_int, e_int}
end
def to_erlang_public_key(%{@kty => @key_type_OKP, -1 => curve, -2 => x}) when curve in 6..7 do
{:ed_pub, @cose_ec_named_curves[curve], x}
end
@doc false
@spec to_erlang_digest(t()) :: atom()
def to_erlang_digest(%{@alg => -65535}), do: :sha
def to_erlang_digest(%{@alg => -259}), do: :sha512
def to_erlang_digest(%{@alg => -258}), do: :sha384
def to_erlang_digest(%{@alg => -257}), do: :sha256
def to_erlang_digest(%{@alg => -47, @curve => 8}), do: :sha256
def to_erlang_digest(%{@alg => -42}), do: :sha512
def to_erlang_digest(%{@alg => -41}), do: :sha256
def to_erlang_digest(%{@alg => -39}), do: :sha512
def to_erlang_digest(%{@alg => -38}), do: :sha384
def to_erlang_digest(%{@alg => -37}), do: :sha256
def to_erlang_digest(%{@alg => -36}), do: :sha512
def to_erlang_digest(%{@alg => -35}), do: :sha384
# ed25519
def to_erlang_digest(%{@alg => -8, -1 => 6}), do: :sha256
# ed448
def to_erlang_digest(%{@alg => -8, -1 => 7}), do: :sha3_256
def to_erlang_digest(%{@alg => -7}), do: :sha256
end