Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow EC key to be generated from KDF output #35

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changes

## v0.9.0

* [X509.PrivateKey] Generate an EC private key from pseudo-random (e.g. KDF)
data (`new_ec/2`)
* [X509.PublicKey] Calculate the public key from a raw EC private key; add EC
point multiplication (`mul/2,3`)

## v0.8.1

### Fixes
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Add `x509` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:x509, "~> 0.8"}
{:x509, "~> 0.9"}
]
end
```
Expand Down
116 changes: 101 additions & 15 deletions lib/x509/private_key.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,29 @@ defmodule X509.PrivateKey do
@moduledoc """
Functions for generating, reading and writing RSA and EC private keys.

Note that this module uses Erlang/OTP's `:public_key` application, which
does not support all curve names returned by the `:crypto.ec_curves/0`
function. In particular, the NIST Prime curves must be selected by their
SECG id, e.g. NIST P-256 is `:secp256r1` rather than `:prime256v1`. Please
refer to [RFC4492 appendix A](https://tools.ietf.org/search/rfc4492#appendix-A)
for a mapping table.

## Example use with `:public_key`

Encryption and decryption:
### Encryption and decryption

iex> private_key = X509.PrivateKey.new_rsa(2048)
iex> private_key = X509.PrivateKey.new_rsa(4096)
iex> public_key = X509.PublicKey.derive(private_key)
iex> plaintext = "Hello, world!"
iex> ciphertext = :public_key.encrypt_public(plaintext, public_key)
iex> :public_key.decrypt_private(ciphertext, private_key)
"Hello, world!"

Signing and signature verification:
Note that in practice it is not a good idea to directly encrypt a message
with asymmetrical cryptography. The examples above are deliberate
over-simpliciations intended to highlight the `:public_key` API calls.

### Signing and signature verification

iex> private_key = X509.PrivateKey.new_ec(:secp256r1)
iex> public_key = X509.PublicKey.derive(private_key)
Expand All @@ -24,9 +35,20 @@ defmodule X509.PrivateKey do
iex> :public_key.verify(message, :sha256, signature, public_key)
true

Note that in practice it is not a good idea to directly encrypt a message
with asymmetrical cryptography. The examples above are deliberate
over-simpliciations intended to highlight the `:public_key` API calls.
### Key exchange

iex> private_key1 = X509.PrivateKey.new_ec(:secp256r1)
iex> {public_key1, _} = X509.PublicKey.derive(private_key1)
iex> private_key2 = X509.PrivateKey.new_ec(:secp256r1)
iex> {public_key2, _} = X509.PublicKey.derive(private_key2)
iex> shared_secret1 = :public_key.compute_key(public_key2, private_key1)
iex> shared_secret2 = :public_key.compute_key(public_key1, private_key2)
iex> shared_secret1 == shared_secret2
true

Since `:public_key.compute_key/2.3` takes an EC point as its first parameter,
we extract the point from the return value of `X509.PublicKey.derive/1` using
pattern matching.
"""

@typedoc "RSA or EC private key"
Expand Down Expand Up @@ -54,21 +76,85 @@ defmodule X509.PrivateKey do
Generates a new EC private key. To derive the public key, use
`X509.PublicKey.derive/1`.

The first parameter must specify a named curve. The curve can be specified
as an atom or an OID tuple.

Note that this function uses Erlang/OTP's `:public_key` application, which
does not support all curve names returned by the `:crypto.ec_curves/0`
function. In particular, the NIST Prime curves must be selected by their
SECG id, e.g. NIST P-256 is `:secp256r1` rather than `:prime256v1`. Please
refer to [RFC4492 appendix A](https://tools.ietf.org/search/rfc4492#appendix-A)
for a mapping table.
The curve can be specified as an atom or an OID tuple.
"""
@spec new_ec(:crypto.ec_named_curve() | :public_key.oid()) :: :public_key.ec_private_key()
def new_ec(curve) when is_atom(curve) or is_tuple(curve) do
:public_key.generate_key({:namedCurve, curve})
end

@doc """
Deterministically generates an EC private key from a (pseudo)random seed. To
derive the public key, use `X509.PublicKey.derive/1`.

The first parameter must specify a named curve. The curve can be specified
as an atom or an OID tuple.

The second parameter is the seed value, which is typically the output of a
secure KDF:

* If the selected curve is defined over a prime field or characteristic 2
field the procedure in NIST FIPS-186-4 B.4.1 "Key Pair Generation Using
Extra Random Bits" is used. The `returned_bits` argument must be a binary
that is at least 64 bits (8 bytes) longer than the length of the order of
the curve.

* For the `:x22519` and `:x448` curves, the `returned_bits` argument must
match the bit-size of the curve (i.e. 256 or 448 bits). The value is
clamped according to the curve requirements and wrapped into an EC
private key record.
"""
@spec new_ec(:crypto.ec_named_curve() | :public_key.oid(), binary()) ::
:public_key.ec_private_key()
def new_ec(curve, returned_bits) when is_tuple(curve) do
# FIXME: avoid calls to undocumented functions in :public_key app
new_ec(:pubkey_cert_records.namedCurves(curve), returned_bits)
end

def new_ec(:x25519 = curve, <<returned_bits::little-size(256)>>) do
import Bitwise

clamped =
returned_bits
|> band(~~~7)
|> band(~~~(128 <<< (8 * 31)))
|> bor(64 <<< (8 * 31))

priv = <<clamped::little-size(256)>>
pub = :crypto.compute_key(:ecdh, <<9::integer-little-size(256)>>, priv, curve)
ec_private_key(version: 1, privateKey: priv, parameters: {:namedCurve, curve}, publicKey: pub)
end

def new_ec(:x448 = curve, <<returned_bits::little-size(448)>>) do
import Bitwise

clamped =
returned_bits
|> band(~~~3)
|> bor(128 <<< (8 * 55))

priv = <<clamped::little-size(448)>>
pub = :crypto.compute_key(:ecdh, <<5::integer-little-size(448)>>, priv, curve)
ec_private_key(version: 1, privateKey: priv, parameters: {:namedCurve, curve}, publicKey: pub)
end

def new_ec(curve, returned_bits) when is_atom(curve) and is_binary(returned_bits) do
{_field, _curve, _g, n, _h} = :crypto.ec_curve(curve)

# NIST FIPS-186-4 B.4.1
if byte_size(returned_bits) < byte_size(n) + 8,
do: raise(ArgumentError, "`returned_bits` must be at least #{byte_size(n) + 8} bytes")

d =
returned_bits
|> :binary.decode_unsigned()
|> rem(:binary.decode_unsigned(n) - 1)
|> Kernel.+(1)

{pub, priv} = :crypto.generate_key(:ecdh, curve, d)
ec_private_key(version: 1, privateKey: priv, parameters: {:namedCurve, curve}, publicKey: pub)
end

@doc """
Wraps a private key in a PKCS#8 PrivateKeyInfo container.
"""
Expand Down
72 changes: 71 additions & 1 deletion lib/x509/public_key.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,87 @@ defmodule X509.PublicKey do
@public_key_records [:RSAPublicKey, :SubjectPublicKeyInfo]

@doc """
Derives the public key from the given RSA or EC private key.
Extracts or calculates the public key from the given RSA or EC private key.
"""
@spec derive(X509.PrivateKey.t()) :: t()
def derive(rsa_private_key(modulus: m, publicExponent: e)) do
rsa_public_key(modulus: m, publicExponent: e)
end

# If the public key is not available we have to calculate it ourselves
def derive(
ec_private_key(
privateKey: priv,
parameters: {:namedCurve, curve},
publicKey: :asn1_NOVALUE
)
) do
derive(priv, curve)
end

def derive(ec_private_key(parameters: params, publicKey: pub)) do
{ec_point(point: pub), params}
end

@doc """
Extracts or calculates the public key from a raw EC private key.

The private key may be specified as an integer or a binary. The curve can be
specified as an atom or an OID tuple.
"""
@spec derive(binary(), :crypto.ec_named_curve() | :public_key.oid()) :: t()
def derive(priv, curve) when is_tuple(curve) do
# FIXME: avoid calls to undocumented functions in :public_key app
derive(priv, :pubkey_cert_records.namedCurves(curve))
end

def derive(<<priv::integer-little-size(256)>>, :x25519 = curve) do
pub = :crypto.compute_key(:ecdh, <<9::integer-little-size(256)>>, priv, curve)
{ec_point(point: pub), {:namedCurve, curve}}
end

def derive(<<priv::integer-little-size(448)>>, :x448 = curve) do
pub = :crypto.compute_key(:ecdh, <<5::integer-little-size(448)>>, priv, curve)
{ec_point(point: pub), {:namedCurve, curve}}
end

def derive(priv, curve) when is_binary(priv) and is_atom(curve) do
{pub, _} = :crypto.generate_key(:ecdh, curve, priv)
{ec_point(point: pub), {:namedCurve, curve}}
end

@doc """
Performs point multiplication on an elliptic curve.

The point may be specified as a public key tuple, an ECPoint record or a
binary. These last two require the curve to be specified as an atom or OID.
The multiplier may be specified as an integer or a binary.

Returns a public key tuple containing the new ECPoint and the curve
parameters.
"""
@spec mul(t(), integer() | binary()) :: t()
def mul({ec_point, {:namedCurve, curve}}, multiplier) do
mul(ec_point, multiplier, curve)
end

@spec mul(binary(), integer() | binary(), :crypto.ec_named_curve() | :public_key.oid()) :: t()
def mul(point, multiplier, curve) when is_tuple(curve) do
# FIXME: avoid calls to undocumented functions in :public_key app
mul(point, multiplier, :pubkey_cert_records.namedCurves(curve))
end

def mul(ec_point(point: point), multiplier, curve) do
mul(point, multiplier, curve)
end

def mul(point, multiplier, curve) do
# TODO: this doesn't work for x25519 and x448
{f, c, _g, n, h} = :crypto.ec_curve(curve)
{pub, _} = :crypto.generate_key(:ecdh, {f, c, point, n, h}, multiplier)
{ec_point(point: pub), {:namedCurve, curve}}
end

@doc """
Wraps a public key in a SubjectPublicKeyInfo (or similar) container.

Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule X509.MixProject do
use Mix.Project

@version "0.8.1"
@version "0.9.0"

def project do
[
Expand Down
12 changes: 7 additions & 5 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
%{
"earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.21.0", "7af8cd3e3df2fe355e99dabd2d4dcecc6e76eb417200e3b7a3da362d52730e3c", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"},
"curve25519": {:hex, :curve25519, "1.0.3", "101248eeb2ed28f1b8d2d423cf560bfc99f5e22bae5a2bb113fd24efad05fae1", [:mix], [], "hexpm", "aa936079df2bcefb5cffa68840dc048f222fb7a5cb5827c735dacddba30e3b23"},
"curve448": {:hex, :curve448, "1.0.3", "d86308f3be3a59548d9e7168979d94be95e42e8a5139785119154e56ff7c4aed", [:mix], [], "hexpm", "009ee5072067743d6074f3903d53f0d7fb83b1411920c3f0032b41582e50c09a"},
"earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm", "e3be2bc3ae67781db529b80aa7e7c49904a988596e2dbff897425b48b3581161"},
"ex_doc": {:hex, :ex_doc, "0.21.0", "7af8cd3e3df2fe355e99dabd2d4dcecc6e76eb417200e3b7a3da362d52730e3c", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "ef679a81de63385c7e72597e81ca1276187505eeacb38281a672d2822254ff1a"},
"makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a10c6eb62cca416019663129699769f0c2ccf39428b3bb3c0cb38c718a0c186d"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm", "5c040b8469c1ff1b10093d3186e2e10dbe483cd73d79ec017993fb3985b8a9b3"},
}
6 changes: 6 additions & 0 deletions test/x509/private_key_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ defmodule X509.PrivateKeyTest do
assert match?(ec_private_key(), new_ec(oid(:secp256r1)))

assert_raise(FunctionClauseError, fn -> new_ec(:no_such_curve) end)

ec1 = new_ec(:secp256r1, "1234567890123456789012345678901234567890")
ec2 = new_ec(oid(:secp256r1), "1234567890123456789012345678901234567890")
assert ec1 == ec2

assert_raise(ArgumentError, fn -> new_ec(:secp256r1, "12345678901234567890123456789012") end)
end

test "wrap and unwrap", context do
Expand Down
30 changes: 30 additions & 0 deletions test/x509/public_key_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,31 @@ defmodule X509.PublicKeyTest do
assert :public_key.verify("message", :sha256, signature, derive(context.ec_key))
end

test "mul" do
# This is actually the P-256 base point; would be better to find test
# vectors with a different base point...
p =
point(
:secp256r1,
"6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296",
"4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5"
)

assert X509.PublicKey.mul(p, 10) ==
point(
:secp256r1,
"CEF66D6B2A3A993E591214D1EA223FB545CA6C471C48306E4C36069404C5723F",
"878662A229AAAE906E123CDD9D3B4C10590DED29FE751EEECA34BBAA44AF0773"
)

assert X509.PublicKey.mul(p, 112_233_445_566_778_899_112_233_445_566_778_899) ==
point(
:secp256r1,
"1B7E046A076CC25E6D7FA5003F6729F665CC3241B5ADAB12B498CD32F2803264",
"BFEA79BE2B666B073DB69A2A241ADAB0738FE9D2DD28B5604EB8C8CF097C457B"
)
end

test "wrap and unwrap", context do
assert match?(subject_public_key_info(), wrap(context.ec_pub))
assert context.ec_pub == context.ec_pub |> wrap() |> unwrap()
Expand Down Expand Up @@ -107,4 +132,9 @@ defmodule X509.PublicKeyTest do
assert der == der |> from_der!() |> to_der()
end
end

defp point(curve, x, y) do
{{:ECPoint, <<4, Base.decode16!(x)::binary, Base.decode16!(y)::binary>>},
{:namedCurve, curve}}
end
end