Skip to content
Merged
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
26 changes: 21 additions & 5 deletions lib/x509/private_key.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ defmodule X509.PrivateKey do

@private_key_records [:RSAPrivateKey, :ECPrivateKey, :PrivateKeyInfo]
@default_e 65537
@edwards_curves [oid(:"id-Ed25519"), oid(:"id-Ed448")]

@doc """
Generates a new RSA private key. To derive the public key, use
Expand Down Expand Up @@ -85,6 +86,15 @@ defmodule X509.PrivateKey do
)
end

def wrap(ec_private_key(parameters: {:namedCurve, curve}, privateKey: private_key))
when curve in @edwards_curves do
private_key_info(
version: :v1,
privateKeyAlgorithm: private_key_info_private_key_algorithm(algorithm: curve),
privateKey: :public_key.der_encode(:CurvePrivateKey, private_key)
)
end

def wrap(ec_private_key(parameters: parameters) = private_key) do
private_key_info(
version: :v1,
Expand Down Expand Up @@ -123,11 +133,11 @@ defmodule X509.PrivateKey do
## Options:

* `:wrap` - Wrap the private key in a PKCS#8 PrivateKeyInfo container
(default: `false`)
(default: `false`, but always `true` for EdDSA keys)
"""
@spec to_der(t(), Keyword.t()) :: binary()
def to_der(private_key, opts \\ []) do
if Keyword.get(opts, :wrap, false) do
if Keyword.get(opts, :wrap, false) or is_edwards_curve(private_key) do
private_key
|> wrap()
|> der_encode()
Expand All @@ -143,13 +153,13 @@ defmodule X509.PrivateKey do
## Options:

* `:wrap` - Wrap the private key in a PKCS#8 PrivateKeyInfo container
(default: `false`)
(default: `false`, but always `true` for EdDSA keys)
* `:password` - If a password is specified, the private key is encrypted
using 3DES; to password will be required to decode the PEM entry
using 3DES; the password will be required to decode the PEM entry
"""
@spec to_pem(t(), Keyword.t()) :: String.t()
def to_pem(private_key, opts \\ []) do
if Keyword.get(opts, :wrap, false) do
if Keyword.get(opts, :wrap, false) or is_edwards_curve(private_key) do
private_key
|> wrap()
else
Expand All @@ -160,6 +170,12 @@ defmodule X509.PrivateKey do
|> :public_key.pem_encode()
end

defp is_edwards_curve(ec_private_key(parameters: {:namedCurve, curve}))
when curve in @edwards_curves,
do: true

defp is_edwards_curve(_private_key), do: false

@doc """
Attempts to parse a private key in DER (binary) format. Raises in case of failure.

Expand Down
71 changes: 70 additions & 1 deletion lib/x509/public_key.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ defmodule X509.PublicKey do
| X509.ASN.record(:certification_request_subject_pk_info)

@public_key_records [:RSAPublicKey, :SubjectPublicKeyInfo]
@edwards_curves [oid(:"id-Ed25519"), oid(:"id-Ed448")]

@doc """
Derives the public key from the given RSA or EC private key.
Expand All @@ -35,10 +36,22 @@ defmodule X509.PublicKey do
rsa_public_key(modulus: m, publicExponent: e)
end

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

def derive(
ec_private_key(parameters: {:namedCurve, oid(:"id-Ed25519")}, privateKey: private_key)
) do
{public_key, _} = :crypto.generate_key(:eddsa, :ed25519, private_key)
{ec_point(point: public_key), {:namedCurve, oid(:"id-Ed25519")}}
end

def derive(ec_private_key(parameters: {:namedCurve, oid(:"id-Ed448")}, privateKey: private_key)) do
{public_key, _} = :crypto.generate_key(:eddsa, :ed448, private_key)
{ec_point(point: public_key), {:namedCurve, oid(:"id-Ed448")}}
end

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

Expand All @@ -65,6 +78,14 @@ defmodule X509.PublicKey do
)
end

def wrap({ec_point(point: public_key), {:namedCurve, curve}}, :SubjectPublicKeyInfo)
when curve in @edwards_curves do
subject_public_key_info(
algorithm: algorithm_identifier(algorithm: curve),
subjectPublicKey: public_key
)
end

def wrap({ec_point(point: public_key), parameters}, :SubjectPublicKeyInfo) do
subject_public_key_info(
algorithm:
Expand All @@ -87,6 +108,14 @@ defmodule X509.PublicKey do
)
end

def wrap({ec_point() = public_key, {:namedCurve, curve}}, :OTPSubjectPublicKeyInfo)
when curve in @edwards_curves do
otp_subject_public_key_info(
algorithm: public_key_algorithm(algorithm: curve),
subjectPublicKey: public_key
)
end

def wrap({ec_point() = public_key, parameters}, :OTPSubjectPublicKeyInfo) do
otp_subject_public_key_info(
algorithm:
Expand All @@ -109,6 +138,17 @@ defmodule X509.PublicKey do
)
end

def wrap(
{ec_point(point: public_key), {:namedCurve, curve}},
:CertificationRequestInfo_subjectPKInfo
)
when curve in @edwards_curves do
certification_request_subject_pk_info(
algorithm: certification_request_subject_pk_info_algorithm(algorithm: curve),
subjectPublicKey: public_key
)
end

def wrap({ec_point(point: public_key), parameters}, :CertificationRequestInfo_subjectPKInfo) do
certification_request_subject_pk_info(
algorithm:
Expand Down Expand Up @@ -148,6 +188,10 @@ defmodule X509.PublicKey do

algorithm_identifier(algorithm: oid(:"id-ecPublicKey"), parameters: parameters) ->
{ec_point(point: public_key), parameters}

algorithm_identifier(algorithm: curve, parameters: :asn1_NOVALUE)
when curve in @edwards_curves ->
{ec_point(point: public_key), {:namedCurve, curve}}
end
end

Expand All @@ -158,6 +202,10 @@ defmodule X509.PublicKey do

public_key_algorithm(algorithm: oid(:"id-ecPublicKey"), parameters: parameters) ->
{public_key, parameters}

public_key_algorithm(algorithm: curve, parameters: :asn1_NOVALUE)
when curve in @edwards_curves ->
{public_key, {:namedCurve, curve}}
end
end

Expand All @@ -173,6 +221,13 @@ defmodule X509.PublicKey do
parameters: {:asn1_OPENTYPE, parameters}
) ->
{ec_point(point: public_key), :public_key.der_decode(:EcpkParameters, parameters)}

certification_request_subject_pk_info_algorithm(
algorithm: curve,
parameters: :asn1_NOVALUE
)
when curve in @edwards_curves ->
{ec_point(point: public_key), {:namedCurve, curve}}
end
end

Expand Down Expand Up @@ -289,6 +344,20 @@ defmodule X509.PublicKey do
nil ->
{:error, :not_found}

{:SubjectPublicKeyInfo, der, :not_encrypted} ->
# Some OTP versions fail when calling `pem_entry_decode/1` on EdDSA
# keys in a PKCS#8 container; need to DER-decode and unwrap ourselves
try do
:public_key.der_decode(:SubjectPublicKeyInfo, der)
|> unwrap()
rescue
MatchError ->
{:error, :malformed}
else
public_key ->
{:ok, public_key}
end

entry ->
try do
:public_key.pem_entry_decode(entry)
Expand Down
Binary file added test/data/csr_ed25519.der
Binary file not shown.
7 changes: 7 additions & 0 deletions test/data/csr_ed25519.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-----BEGIN CERTIFICATE REQUEST-----
MIHDMHcCAQAwRDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5UMRQwEgYDVQQHDAtT
cHJpbmdmaWVsZDESMBAGA1UECgwJQUNNRSBJbmMuMCowBQYDK2VwAyEAL4aZYM2Y
tbnp96nB+VbSCu6T0Y6fw25cfAwfeis3NE2gADAFBgMrZXADQQAkwq0MlrSWn+pO
SkTfxEQ5LrHvv+sG1mwgzY86bF8u2fYpzW5Cad/AwvSsVNYE+V58F24zI75TKUXW
Rqt4luMJ
-----END CERTIFICATE REQUEST-----
Binary file added test/data/csr_ed448.der
Binary file not shown.
8 changes: 8 additions & 0 deletions test/data/csr_ed448.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBDzCBkAIBADBEMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlQxFDASBgNVBAcM
C1NwcmluZ2ZpZWxkMRIwEAYDVQQKDAlBQ01FIEluYy4wQzAFBgMrZXEDOgA4xxxV
sVd9O9ILu+TeJuNO1v++5gTXwr1wiiVedKQtc5YiiQC+obO4BzKrIGq+HO0XsNCW
KNLs+4CgADAFBgMrZXEDcwAeho3iZgYIA/mgjwXoLnNZsARJZQ+ioDcCt6X9MPQI
xzseggDA0FR1V7AQqOyTVZn25J2Y2oWpGYA57dJ7U9tO0ncbxZOUY3xDQJ/tNj9Q
RGyutrQHzw+IFz/fnsJtylWoscer9xQwb2RNYRai65QUPAA=
-----END CERTIFICATE REQUEST-----
5 changes: 5 additions & 0 deletions test/data/ed25519.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
openssl genpkey -algorithm Ed25519 -out ed25519.pem

-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIOADcg4HQSWbt/wpfa3nGudexJvgm0JicLs3ZXF/VPUQ
-----END PRIVATE KEY-----
6 changes: 6 additions & 0 deletions test/data/ed25519_aes.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIGjMF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBDReuV/7My4dfCGlph4
cxW4AgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBAgQQAhJpUB58b0H9xWYZ
McTzHwRA3SS9K5iFsZtDNYlGeZVDf+Qrm0qF0rPFNy+6VWynvEPRDt3W7Z68tDS8
rLUlr8jWWvNxWDvMM2FmSdFvdIPsUA==
-----END ENCRYPTED PRIVATE KEY-----
8 changes: 8 additions & 0 deletions test/data/ed25519_des3.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
openssl pkey -in ed25519.pem -des3 -passout pass:secret -out ed25519_des3.pem

-----BEGIN ENCRYPTED PRIVATE KEY-----
MIGSMFYGCSqGSIb3DQEFDTBJMDEGCSqGSIb3DQEFDDAkBBADj6I8W64tRu4Io0Kz
wUB1AgIIADAMBggqhkiG9w0CCQUAMBQGCCqGSIb3DQMHBAiPUvw4ntJYQgQ4Dh80
7KNbErqEGJaBLx2ToxaPMLjI42HaFgrn/teTPzyeq9IVb32L0mTDz39oaOU5gRCu
kOxMRwM=
-----END ENCRYPTED PRIVATE KEY-----
Binary file added test/data/ed25519_pkcs8.der
Binary file not shown.
5 changes: 5 additions & 0 deletions test/data/ed25519_pkcs8.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
openssl pkcs8 -in ed25519.pem -topk8 -nocrypt -out ed25519_pkcs8.pem

-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIOADcg4HQSWbt/wpfa3nGudexJvgm0JicLs3ZXF/VPUQ
-----END PRIVATE KEY-----
Binary file added test/data/ed25519_pub.der
Binary file not shown.
5 changes: 5 additions & 0 deletions test/data/ed25519_pub.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
openssl pkey -in ed25519.pem -pubout -out ed25519_pub.pem

-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAL4aZYM2Ytbnp96nB+VbSCu6T0Y6fw25cfAwfeis3NE0=
-----END PUBLIC KEY-----
6 changes: 6 additions & 0 deletions test/data/ed448.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
openssl genpkey -algorithm Ed448 -out ed448.pem

-----BEGIN PRIVATE KEY-----
MEcCAQAwBQYDK2VxBDsEOfLfnyjdiDfwxtTraFTls6xCf/89/cG+qDzx5xUyP5Uj
3j26L7/Ia5I9X3hyG6mMV95SEoIPxXAchQ==
-----END PRIVATE KEY-----
8 changes: 8 additions & 0 deletions test/data/ed448_aes.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
openssl pkey -in ed448.pem -aes128 -passout pass:secret -out ed448_aes.pem

-----BEGIN ENCRYPTED PRIVATE KEY-----
MIGzMF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBDfP0FppNEGOjxfbw6A
iNJFAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBAgQQwX7emg3GfbMyvUIF
vwz3xQRQ0vNd24sNxqvGKqxI7bU/CbsCYQrwAstde4OG/1BshyRcgGZWjkZxxtA3
0KPoENcg9tjIu/5m/QIFIcqdmAFFW/rJTuCBTofLEBsrXi7BH14=
-----END ENCRYPTED PRIVATE KEY-----
8 changes: 8 additions & 0 deletions test/data/ed448_des3.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
openssl pkey -in ed448.pem -des3 -passout pass:secret -out ed448_des3.pem

-----BEGIN ENCRYPTED PRIVATE KEY-----
MIGqMFYGCSqGSIb3DQEFDTBJMDEGCSqGSIb3DQEFDDAkBBB7GWbkDq8j+Z6FRhWP
y8UzAgIIADAMBggqhkiG9w0CCQUAMBQGCCqGSIb3DQMHBAg6OmGsVp2LNQRQhy+6
VaEa+pt3FAFckTdzeEeOC1bsbqJJUOYv9fB90ji1FZ7xms9duUdNq4taaax5XwNi
m+aGd+pK8VNsACCeMKx7ObxtW2+UlA/s2dRME/8=
-----END ENCRYPTED PRIVATE KEY-----
Binary file added test/data/ed448_pkcs8.der
Binary file not shown.
6 changes: 6 additions & 0 deletions test/data/ed448_pkcs8.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
openssl pkcs8 -in ed448.pem -topk8 -nocrypt -out ed448_pkcs8.pem

-----BEGIN PRIVATE KEY-----
MEcCAQAwBQYDK2VxBDsEOfLfnyjdiDfwxtTraFTls6xCf/89/cG+qDzx5xUyP5Uj
3j26L7/Ia5I9X3hyG6mMV95SEoIPxXAchQ==
-----END PRIVATE KEY-----
Binary file added test/data/ed448_pub.der
Binary file not shown.
6 changes: 6 additions & 0 deletions test/data/ed448_pub.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
openssl pkey -in ed448.pem -pubout -out ed448_pub.pem

-----BEGIN PUBLIC KEY-----
MEMwBQYDK2VxAzoAOMccVbFXfTvSC7vk3ibjTtb/vuYE18K9cIolXnSkLXOWIokA
vqGzuAcyqyBqvhztF7DQlijS7PuA
-----END PUBLIC KEY-----
Binary file added test/data/prime256v1_pkcs8_x509.der
Binary file not shown.
19 changes: 19 additions & 0 deletions test/integration/openssl_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,25 @@ defmodule X509.OpenSSLTest do
assert openssl(["ec", "-pubin", "-in", file, "-text", "-noout"]) =~ "ASN1 OID: prime256v1"
end

test "OpenSSL can read EdDSA private keys" do
file =
X509.PrivateKey.new_ec(:ed25519)
|> X509.PrivateKey.to_pem()
|> write_tmp()

assert openssl(["pkey", "-in", file, "-text", "-noout"]) =~ "ED25519 Private-Key"
end

test "OpenSSL can read EdDSA public keys" do
file =
X509.PrivateKey.new_ec(:ed448)
|> X509.PublicKey.derive()
|> X509.PublicKey.to_pem(wrap: true)
|> write_tmp()

assert openssl(["pkey", "-pubin", "-in", file, "-text", "-noout"]) =~ "ED448 Public-Key"
end

test "OpenSSL can read CSRs (RSA)" do
file =
X509.PrivateKey.new_rsa(2048)
Expand Down
30 changes: 17 additions & 13 deletions test/x509/csr_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -86,19 +86,23 @@ defmodule X509.CSRTest do
assert X509.CSR.public_key(csr) == X509.PublicKey.derive(context.key)
end

test "PEM decode and encode" do
pem = File.read!("test/data/csr_prime256v1.pem")
csr = X509.CSR.from_pem!(pem)
assert match?(certification_request(), csr)
assert X509.CSR.valid?(csr)

assert csr == csr |> X509.CSR.to_pem() |> X509.CSR.from_pem!()
end

test "DER decode and encode" do
der = File.read!("test/data/csr_prime256v1.der")
assert match?(certification_request(), X509.CSR.from_der!(der))
assert der == der |> X509.CSR.from_der!() |> X509.CSR.to_der()
for curve <- ["prime256v1", "ed25519", "ed448"] do
@curve curve

test "PEM decode and encode: #{@curve}" do
pem = File.read!("test/data/csr_#{@curve}.pem")
csr = X509.CSR.from_pem!(pem)
assert match?(certification_request(), csr)
assert X509.CSR.valid?(csr)

assert csr == csr |> X509.CSR.to_pem() |> X509.CSR.from_pem!()
end

test "DER decode and encode: #{@curve}" do
der = File.read!("test/data/csr_#{@curve}.der")
assert match?(certification_request(), X509.CSR.from_der!(der))
assert der == der |> X509.CSR.from_der!() |> X509.CSR.to_der()
end
end
end
end
Loading