From aae71ca89c18503f30602d96a0cb79e489802936 Mon Sep 17 00:00:00 2001 From: Bram Verburg Date: Fri, 20 Jun 2025 16:58:26 +0200 Subject: [PATCH 1/2] Add support for EdDSA keys --- lib/x509/private_key.ex | 26 ++++++++++--- lib/x509/public_key.ex | 57 +++++++++++++++++++++++++++- test/data/csr_ed25519.der | Bin 0 -> 198 bytes test/data/csr_ed25519.pem | 7 ++++ test/data/csr_ed448.der | Bin 0 -> 275 bytes test/data/csr_ed448.pem | 8 ++++ test/data/ed25519.pem | 5 +++ test/data/ed25519_aes.pem | 6 +++ test/data/ed25519_des3.pem | 8 ++++ test/data/ed25519_pkcs8.der | Bin 0 -> 48 bytes test/data/ed25519_pkcs8.pem | 5 +++ test/data/ed25519_pub.der | Bin 0 -> 44 bytes test/data/ed25519_pub.pem | 5 +++ test/data/ed448.pem | 6 +++ test/data/ed448_aes.pem | 8 ++++ test/data/ed448_des3.pem | 8 ++++ test/data/ed448_pkcs8.der | Bin 0 -> 73 bytes test/data/ed448_pkcs8.pem | 6 +++ test/data/ed448_pub.der | Bin 0 -> 69 bytes test/data/ed448_pub.pem | 6 +++ test/data/prime256v1_pkcs8_x509.der | Bin 0 -> 138 bytes test/integration/openssl_test.exs | 19 ++++++++++ test/x509/csr_test.exs | 30 ++++++++------- test/x509/private_key_test.exs | 54 +++++++++++++++----------- test/x509/public_key_test.exs | 36 ++++++++++++------ test/x509/test/server_test.exs | 2 +- 26 files changed, 247 insertions(+), 55 deletions(-) create mode 100644 test/data/csr_ed25519.der create mode 100644 test/data/csr_ed25519.pem create mode 100644 test/data/csr_ed448.der create mode 100644 test/data/csr_ed448.pem create mode 100644 test/data/ed25519.pem create mode 100644 test/data/ed25519_aes.pem create mode 100644 test/data/ed25519_des3.pem create mode 100644 test/data/ed25519_pkcs8.der create mode 100644 test/data/ed25519_pkcs8.pem create mode 100644 test/data/ed25519_pub.der create mode 100644 test/data/ed25519_pub.pem create mode 100644 test/data/ed448.pem create mode 100644 test/data/ed448_aes.pem create mode 100644 test/data/ed448_des3.pem create mode 100644 test/data/ed448_pkcs8.der create mode 100644 test/data/ed448_pkcs8.pem create mode 100644 test/data/ed448_pub.der create mode 100644 test/data/ed448_pub.pem create mode 100644 test/data/prime256v1_pkcs8_x509.der diff --git a/lib/x509/private_key.ex b/lib/x509/private_key.ex index ce53032..d1b9c3c 100644 --- a/lib/x509/private_key.ex +++ b/lib/x509/private_key.ex @@ -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 @@ -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, @@ -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() @@ -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 @@ -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. diff --git a/lib/x509/public_key.ex b/lib/x509/public_key.ex index b84c6be..fb5dc8b 100644 --- a/lib/x509/public_key.ex +++ b/lib/x509/public_key.ex @@ -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. @@ -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. @@ -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: @@ -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: @@ -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: @@ -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 @@ -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 @@ -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 diff --git a/test/data/csr_ed25519.der b/test/data/csr_ed25519.der new file mode 100644 index 0000000000000000000000000000000000000000..b6bd1f5c182fae1e1d56166130e5af4daa18b261 GIT binary patch literal 198 zcmXqLJZwUaV#sa4$;KSY!Y0fV8Vuub@G$v>7>XDOfn?ZuxPuFdGV{{YGE;L> z4228?K%!hcoQ}@Et_q%c$$AD_2CQt%+NlN1iVXT~GZW6v*t+xO_mu~KhF#)%H~C`U z{KI)MH9YcF+U6#{3m9N(9T`*(t>u}vW!n5#eqJv3kGNRsZG6A~HQTiug|q!uIq`Zo mziFP$bIQDb;Lw*fA=g-b#?^@D87uD#)^xq*wz^{4V@?3Z zJ!83kXm3H6YFx<@-QsCVoecXHZr;Iev|1r+pUhkF4Hu?qTzd1nVF3fokYWb8w%$i+ zY#hu#7xc5f&?}DIz~Y(8zi5Fu)Apr*4Zd(3x0Y*SIB+4PG<<`=iZ_!(XMTG!cgC&O zm68pXZ!c8`-}bvyE`4K>;qhK)@|FuexAQW-2VQ&L%F9yS8P1K`n!lh Peu{6R*rL}{L~IxU#xG{p literal 0 HcmV?d00001 diff --git a/test/data/csr_ed448.pem b/test/data/csr_ed448.pem new file mode 100644 index 0000000..7761358 --- /dev/null +++ b/test/data/csr_ed448.pem @@ -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----- diff --git a/test/data/ed25519.pem b/test/data/ed25519.pem new file mode 100644 index 0000000..47422d9 --- /dev/null +++ b/test/data/ed25519.pem @@ -0,0 +1,5 @@ +openssl genpkey -algorithm Ed25519 -out ed25519.pem + +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIOADcg4HQSWbt/wpfa3nGudexJvgm0JicLs3ZXF/VPUQ +-----END PRIVATE KEY----- diff --git a/test/data/ed25519_aes.pem b/test/data/ed25519_aes.pem new file mode 100644 index 0000000..9532a5d --- /dev/null +++ b/test/data/ed25519_aes.pem @@ -0,0 +1,6 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIGjMF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBDReuV/7My4dfCGlph4 +cxW4AgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBAgQQAhJpUB58b0H9xWYZ +McTzHwRA3SS9K5iFsZtDNYlGeZVDf+Qrm0qF0rPFNy+6VWynvEPRDt3W7Z68tDS8 +rLUlr8jWWvNxWDvMM2FmSdFvdIPsUA== +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test/data/ed25519_des3.pem b/test/data/ed25519_des3.pem new file mode 100644 index 0000000..f71d113 --- /dev/null +++ b/test/data/ed25519_des3.pem @@ -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----- diff --git a/test/data/ed25519_pkcs8.der b/test/data/ed25519_pkcs8.der new file mode 100644 index 0000000000000000000000000000000000000000..9ed23c11fcc67797def3a0db0a6694b78fc81974 GIT binary patch literal 48 zcmXreV`5}5U}a<0PAyz E06$L=F8}}l literal 0 HcmV?d00001 diff --git a/test/data/ed25519_pkcs8.pem b/test/data/ed25519_pkcs8.pem new file mode 100644 index 0000000..36c2fa3 --- /dev/null +++ b/test/data/ed25519_pkcs8.pem @@ -0,0 +1,5 @@ +openssl pkcs8 -in ed25519.pem -topk8 -nocrypt -out ed25519_pkcs8.pem + +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIOADcg4HQSWbt/wpfa3nGudexJvgm0JicLs3ZXF/VPUQ +-----END PRIVATE KEY----- diff --git a/test/data/ed25519_pub.der b/test/data/ed25519_pub.der new file mode 100644 index 0000000000000000000000000000000000000000..b8d8472e1f7afbb32502623ae6eacfdc723968ce GIT binary patch literal 44 zcmV+{0Mq|4Dli2G11n{410et}hM8c^n618R?-UYlhKZ!!){!B3?F(cH#ALu CY!br& literal 0 HcmV?d00001 diff --git a/test/data/ed25519_pub.pem b/test/data/ed25519_pub.pem new file mode 100644 index 0000000..6111be0 --- /dev/null +++ b/test/data/ed25519_pub.pem @@ -0,0 +1,5 @@ +openssl pkey -in ed25519.pem -pubout -out ed25519_pub.pem + +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAL4aZYM2Ytbnp96nB+VbSCu6T0Y6fw25cfAwfeis3NE0= +-----END PUBLIC KEY----- diff --git a/test/data/ed448.pem b/test/data/ed448.pem new file mode 100644 index 0000000..5902468 --- /dev/null +++ b/test/data/ed448.pem @@ -0,0 +1,6 @@ +openssl genpkey -algorithm Ed448 -out ed448.pem + +-----BEGIN PRIVATE KEY----- +MEcCAQAwBQYDK2VxBDsEOfLfnyjdiDfwxtTraFTls6xCf/89/cG+qDzx5xUyP5Uj +3j26L7/Ia5I9X3hyG6mMV95SEoIPxXAchQ== +-----END PRIVATE KEY----- diff --git a/test/data/ed448_aes.pem b/test/data/ed448_aes.pem new file mode 100644 index 0000000..c3e606d --- /dev/null +++ b/test/data/ed448_aes.pem @@ -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----- diff --git a/test/data/ed448_des3.pem b/test/data/ed448_des3.pem new file mode 100644 index 0000000..0e1bbae --- /dev/null +++ b/test/data/ed448_des3.pem @@ -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----- diff --git a/test/data/ed448_pkcs8.der b/test/data/ed448_pkcs8.der new file mode 100644 index 0000000000000000000000000000000000000000..5a1b273068b0540c648080c9b7d858a630a65399 GIT binary patch literal 73 zcmV-P0Ji@yM*;x=Fa-t!D`jy6I|MoM-=8Smh&S-Y)az(e<+H3pfB!xG!M>#c&*jV6h;u literal 0 HcmV?d00001 diff --git a/test/data/ed448_pkcs8.pem b/test/data/ed448_pkcs8.pem new file mode 100644 index 0000000..98ac3a6 --- /dev/null +++ b/test/data/ed448_pkcs8.pem @@ -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----- diff --git a/test/data/ed448_pub.der b/test/data/ed448_pub.der new file mode 100644 index 0000000000000000000000000000000000000000..f9bf126c9c9a1663f05882e6a52a7909ba9f416f GIT binary patch literal 69 zcmV-L0J{G$Lofvf11n{513CaW#~f9$SA9Fu3%lgrCgV=l|Gwq~*TTJUiX~ojq%Cun bB8dRLp|iLLGOHkJz8viru+Ww$((L^C3v%?G@MLqU6z)`*P@Pk+cW;I^$@~ba5K>z>% literal 0 HcmV?d00001 diff --git a/test/integration/openssl_test.exs b/test/integration/openssl_test.exs index 4921352..b7c8bde 100644 --- a/test/integration/openssl_test.exs +++ b/test/integration/openssl_test.exs @@ -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) diff --git a/test/x509/csr_test.exs b/test/x509/csr_test.exs index bedc6c0..b5e74e2 100644 --- a/test/x509/csr_test.exs +++ b/test/x509/csr_test.exs @@ -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 diff --git a/test/x509/private_key_test.exs b/test/x509/private_key_test.exs index 2aaeed7..4f3dc22 100644 --- a/test/x509/private_key_test.exs +++ b/test/x509/private_key_test.exs @@ -64,46 +64,54 @@ defmodule X509.PrivateKeyTest do test "new" do assert match?(ec_private_key(), new_ec(:secp256r1)) assert match?(ec_private_key(), new_ec(oid(:secp256r1))) + assert match?(ec_private_key(), new_ec(:ed25519)) + assert match?(ec_private_key(), new_ec(oid(:"id-Ed25519"))) + assert match?(ec_private_key(), new_ec(:ed448)) + assert match?(ec_private_key(), new_ec(oid(:"id-Ed448"))) end - test "wrap and unwrap", context do + test "wrap and unwrap: prime256v1", context do assert match?(private_key_info(), wrap(context.ec_key)) assert context.ec_key == context.ec_key |> wrap() |> unwrap() end - test "PEM decode and encode", context do - pem = File.read!("test/data/prime256v1.pem") - assert match?({:ok, ec_private_key()}, from_pem(pem)) + for curve <- ["prime256v1", "ed25519", "ed448"] do + @curve curve - assert context.ec_key == context.ec_key |> to_pem() |> from_pem!() + test "PEM decode and encode: #{@curve}", context do + pem = File.read!("test/data/#{@curve}.pem") + assert match?({:ok, ec_private_key()}, from_pem(pem)) - pem_des3 = File.read!("test/data/prime256v1_des3.pem") - assert match?({:ok, ec_private_key()}, from_pem(pem_des3, password: "secret")) + assert context.ec_key == context.ec_key |> to_pem() |> from_pem!() - pem_aes = File.read!("test/data/prime256v1_aes.pem") - assert match?({:ok, ec_private_key()}, from_pem(pem_aes, password: "secret")) - end + pem_des3 = File.read!("test/data/#{@curve}_des3.pem") + assert match?({:ok, ec_private_key()}, from_pem(pem_des3, password: "secret")) - test "PKCS8 PEM decode and encode", context do - pem = File.read!("test/data/prime256v1_pkcs8.pem") - assert match?({:ok, ec_private_key()}, from_pem(pem)) + pem_aes = File.read!("test/data/#{@curve}_aes.pem") + assert match?({:ok, ec_private_key()}, from_pem(pem_aes, password: "secret")) + end - if version(:public_key) >= [1, 6] do - # PEM encoding of PKCS8 PrivateKeyInfo requires OTP 21 or later - assert context.ec_key == context.ec_key |> to_pem(wrap: true) |> from_pem!() + test "PKCS8 PEM decode and encode: #{@curve}", context do + pem = File.read!("test/data/#{@curve}_pkcs8.pem") + assert match?({:ok, ec_private_key()}, from_pem(pem)) + + if version(:public_key) >= [1, 6] do + # PEM encoding of PKCS8 PrivateKeyInfo requires OTP 21 or later + assert context.ec_key == context.ec_key |> to_pem(wrap: true) |> from_pem!() + end + end + + test "PKCS8 DER decode and encode: #{@curve}" do + der = File.read!("test/data/#{@curve}_pkcs8.der") + assert match?(ec_private_key(), from_der!(der)) + assert der == der |> from_der!() |> to_der(wrap: true) end end - test "DER decode and encode" do + test "DER decode and encode: prime256v1}" do der = File.read!("test/data/prime256v1.der") assert match?(ec_private_key(), from_der!(der)) assert der == der |> from_der!() |> to_der() end - - test "PKCS8 DER decode and encode" do - der = File.read!("test/data/prime256v1_pkcs8.der") - assert match?(ec_private_key(), from_der!(der)) - assert der == der |> from_der!() |> to_der(wrap: true) - end end end diff --git a/test/x509/public_key_test.exs b/test/x509/public_key_test.exs index 34d306a..42c645c 100644 --- a/test/x509/public_key_test.exs +++ b/test/x509/public_key_test.exs @@ -66,6 +66,14 @@ defmodule X509.PublicKeyTest do assert match?({ec_point(), _}, derive(context.ec_key)) signature = :public_key.sign("message", :sha256, context.ec_key) assert :public_key.verify("message", :sha256, signature, derive(context.ec_key)) + + ed25519 = X509.PrivateKey.new_ec(:ed25519) + signature = :public_key.sign("message", :sha256, ed25519) + assert :public_key.verify("message", :sha256, signature, derive(ed25519)) + + ed448 = X509.PrivateKey.new_ec(:ed448) + signature = :public_key.sign("message", :sha256, ed448) + assert :public_key.verify("message", :sha256, signature, derive(ed448)) end test "wrap and unwrap", context do @@ -91,20 +99,24 @@ defmodule X509.PublicKeyTest do context.ec_pub |> wrap(:CertificationRequestInfo_subjectPKInfo) |> unwrap() end - test "PEM decode and encode", context do - pem = File.read!("test/data/prime256v1_pub.pem") - assert match?({:ok, {ec_point(), _}}, from_pem(pem)) + for curve <- ["prime256v1", "ed25519", "ed448"] do + @curve curve - assert context.ec_pub == context.ec_pub |> to_pem() |> from_pem!() - # EC public key encoding always wraps, ignoring the `wrap: false` option, - # so this test is effectively the same as the previous one - assert context.ec_pub == context.ec_pub |> to_pem(wrap: false) |> from_pem!() - end + test "PEM decode and encode: #{@curve}", context do + pem = File.read!("test/data/#{@curve}_pub.pem") + assert match?({:ok, {ec_point(), _}}, from_pem(pem)) - test "DER decode and encode" do - der = File.read!("test/data/prime256v1_pub.der") - assert match?({:ok, {ec_point(), _}}, from_der(der)) - assert der == der |> from_der!() |> to_der() + assert context.ec_pub == context.ec_pub |> to_pem() |> from_pem!() + # EC public key encoding always wraps, ignoring the `wrap: false` option, + # so this test is effectively the same as the previous one + assert context.ec_pub == context.ec_pub |> to_pem(wrap: false) |> from_pem!() + end + + test "DER decode and encode: #{@curve}" do + der = File.read!("test/data/#{@curve}_pub.der") + assert match?({:ok, {ec_point(), _}}, from_der(der)) + assert der == der |> from_der!() |> to_der() + end end end end diff --git a/test/x509/test/server_test.exs b/test/x509/test/server_test.exs index fba1680..cefa854 100644 --- a/test/x509/test/server_test.exs +++ b/test/x509/test/server_test.exs @@ -897,7 +897,7 @@ defmodule X509.Test.ServerTest do defp create_pem_files(context) do tmp_dir = System.tmp_dir!() - |> Path.join("x509_server_test#{System.get_pid()}") + |> Path.join("x509_server_test#{System.pid()}") File.mkdir(tmp_dir) From f20892c04af16b2e72e5a2b6a102075ddb613ee0 Mon Sep 17 00:00:00 2001 From: Bram Verburg Date: Fri, 20 Jun 2025 17:44:36 +0200 Subject: [PATCH 2/2] Work around for pem_entry_decode/1 on EdDSA keys on old OTP versions --- lib/x509/public_key.ex | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lib/x509/public_key.ex b/lib/x509/public_key.ex index fb5dc8b..58c7f61 100644 --- a/lib/x509/public_key.ex +++ b/lib/x509/public_key.ex @@ -344,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)