From 22662743dfef44d4aceaf8b6be2319c285fce160 Mon Sep 17 00:00:00 2001 From: Ruby Iris Juric Date: Thu, 26 Jun 2025 11:35:14 +1000 Subject: [PATCH] Fix signature algorithm for certificates signed by EdDSA keys Previously, when supplying an EdDSA key to functions in `X509.Certificate` that create certificates, the resulting certificate would contain AlgorithmIdentifiers for ECDSA signatures (eg. `ecdsa-with-sha256`), despite containing an EdDSA signature. Instead, we now check for Edwards curves being used in an ECPrivateKey in `X509.SignatureAlgorithm.new`, and return a proper AlgorithmIdentifier for that curve as specified by RFC 8410. --- lib/x509/signature_algorithm.ex | 8 +++ test/data/README.md | 1 + test/data/selfsigned_ed25519.pem | 11 +++ test/integration/openssl_test.exs | 112 ++++++++++++++++++++++++++++++ test/x509/certificate_test.exs | 78 +++++++++++++++++++++ 5 files changed, 210 insertions(+) create mode 100644 test/data/selfsigned_ed25519.pem diff --git a/lib/x509/signature_algorithm.ex b/lib/x509/signature_algorithm.ex index efe3ac0..8225868 100644 --- a/lib/x509/signature_algorithm.ex +++ b/lib/x509/signature_algorithm.ex @@ -5,6 +5,8 @@ defmodule X509.SignatureAlgorithm do alias X509.Util + @edwards_curves [oid(:"id-Ed25519"), oid(:"id-Ed448")] + # Returns a signature algorithm record for the given public key type and hash # algorithm; this is essentially the reverse of # `:public_key.pkix_sign_types/1` @@ -15,6 +17,11 @@ defmodule X509.SignatureAlgorithm do new(hash, algorithm, type) end + def new(hash, ec_private_key(parameters: {:namedCurve, curve}), type) + when curve in @edwards_curves do + new(hash, {:eddsa, curve}, type) + end + def new(hash, rsa_private_key(), type) do new(hash, :rsa, type) end @@ -81,6 +88,7 @@ defmodule X509.SignatureAlgorithm do defp algorithm(:sha384, :ecdsa), do: {oid(:"ecdsa-with-SHA384"), :asn1_NOVALUE} defp algorithm(:sha512, :rsa), do: {oid(:sha512WithRSAEncryption), null()} defp algorithm(:sha512, :ecdsa), do: {oid(:"ecdsa-with-SHA512"), :asn1_NOVALUE} + defp algorithm(_, {:eddsa, curve}), do: {curve, :asn1_NOVALUE} defp algorithm(hash, :rsa) do raise ArgumentError, "Unsupported hashing algorithm for RSA signing: #{inspect(hash)}" diff --git a/test/data/README.md b/test/data/README.md index 9bf7447..f0f9909 100644 --- a/test/data/README.md +++ b/test/data/README.md @@ -75,4 +75,5 @@ Generating PEM output: ```bash openssl req -new -key rsa.pem -days 365 -x509 -subj "/C=US/ST=NT/L=Springfield/O=ACME Inc." -out selfsigned_rsa.pem openssl req -new -key prime256v1.pem -days 365 -x509 -subj "/C=US/ST=NT/L=Springfield/O=ACME Inc." -out selfsigned_prime256v1.pem +openssl req -new -key ed25519.pem -days 365 -x509 -subj "/C=US/ST=NT/L=Springfield/O=ACME Inc." -out selfsigned_ed25519.pem ``` diff --git a/test/data/selfsigned_ed25519.pem b/test/data/selfsigned_ed25519.pem new file mode 100644 index 0000000..7258983 --- /dev/null +++ b/test/data/selfsigned_ed25519.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBnTCCAU+gAwIBAgIUeEEI1nx0VCdQZpYoyu3TVWuRJEQwBQYDK2VwMEQxCzAJ +BgNVBAYTAlVTMQswCQYDVQQIDAJOVDEUMBIGA1UEBwwLU3ByaW5nZmllbGQxEjAQ +BgNVBAoMCUFDTUUgSW5jLjAeFw0yNTA2MjYwMTMzMDFaFw0yNjA2MjYwMTMzMDFa +MEQxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOVDEUMBIGA1UEBwwLU3ByaW5nZmll +bGQxEjAQBgNVBAoMCUFDTUUgSW5jLjAqMAUGAytlcAMhAC+GmWDNmLW56fepwflW +0gruk9GOn8NuXHwMH3orNzRNo1MwUTAdBgNVHQ4EFgQUpMRJar2ZQODRQeO6oTs5 +LNyci4swHwYDVR0jBBgwFoAUpMRJar2ZQODRQeO6oTs5LNyci4swDwYDVR0TAQH/ +BAUwAwEB/zAFBgMrZXADQQDOCrEui+5Na4Az6wLFUyAOTZtUbFfGqaLuf2e11zwy +zqJTjVgIEtGGkq0sXy38aQThOp39F86Kvusl+dbPA8AD +-----END CERTIFICATE----- diff --git a/test/integration/openssl_test.exs b/test/integration/openssl_test.exs index b7c8bde..358e18e 100644 --- a/test/integration/openssl_test.exs +++ b/test/integration/openssl_test.exs @@ -141,6 +141,26 @@ defmodule X509.OpenSSLTest do assert openssl_out =~ "DNS:acme.com, DNS:www.acme.com" end + test "OpenSSL can read certificates (EdDSA)" do + file = + X509.PrivateKey.new_ec(:ed25519) + |> X509.Certificate.self_signed( + "/C=US/ST=NT/L=Springfield/O=ACME Inc.", + extensions: [ + subject_alt_name: + X509.Certificate.Extension.subject_alt_name(["acme.com", "www.acme.com"]) + ] + ) + |> X509.Certificate.to_pem() + |> write_tmp() + + openssl_out = openssl(["x509", "-in", file, "-text", "-noout"]) + assert openssl_out =~ ~r(Subject: C ?= ?US, ST ?= ?NT, L ?= ?Springfield, O ?= ?ACME Inc.) + assert openssl_out =~ "Public Key Algorithm: ED25519" + assert openssl_out =~ "Signature Algorithm: ED25519" + assert openssl_out =~ "DNS:acme.com, DNS:www.acme.com" + end + test "OpenSSL can read CRLs (RSA)" do ca_key = X509.PrivateKey.new_rsa(512) ca = X509.Certificate.self_signed(ca_key, "/CN=My Root CA", template: :root_ca) @@ -212,6 +232,42 @@ defmodule X509.OpenSSLTest do assert openssl_out =~ "Serial Number: FF" assert openssl_out =~ "Key Compromise" end + + test "OpenSSL can read CRLs (EdDSA)" do + ca_key = X509.PrivateKey.new_ec(:ed25519) + ca = X509.Certificate.self_signed(ca_key, "/CN=My Root CA", template: :root_ca) + + cert = + X509.PrivateKey.new_ec(:ed25519) + |> X509.PublicKey.derive() + |> X509.Certificate.new("/CN=Sample", ca, ca_key, + serial: 0xFF, + extensions: [ + crl_distribution_points: + X509.Certificate.Extension.crl_distribution_points(["http://localhost/test.crl"]) + ] + ) + + entry = + X509.CRL.Entry.new(cert, DateTime.utc_now(), [ + X509.CRL.Extension.reason_code(:keyCompromise) + ]) + + file = + entry + |> List.wrap() + |> X509.CRL.new(ca, ca_key) + |> X509.CRL.to_pem() + |> write_tmp() + + openssl_out = openssl(["crl", "-in", file, "-text", "-noout"]) + assert openssl_out =~ "Certificate Revocation List (CRL)" + assert openssl_out =~ "Signature Algorithm: ED25519" + assert openssl_out =~ ~r(Issuer: /?CN ?= ?My Root CA) + assert openssl_out =~ "X509v3 Authority Key Identifier:" + assert openssl_out =~ "Serial Number: FF" + assert openssl_out =~ "Key Compromise" + end end describe "DER encode" do @@ -333,6 +389,26 @@ defmodule X509.OpenSSLTest do assert openssl_out =~ "DNS:acme.com, DNS:www.acme.com" end + test "OpenSSL can read certificates (EdDSA)" do + file = + X509.PrivateKey.new_ec(:ed25519) + |> X509.Certificate.self_signed( + "/C=US/ST=NT/L=Springfield/O=ACME Inc.", + extensions: [ + subject_alt_name: + X509.Certificate.Extension.subject_alt_name(["acme.com", "www.acme.com"]) + ] + ) + |> X509.Certificate.to_der() + |> write_tmp() + + openssl_out = openssl(["x509", "-in", file, "-inform", "der", "-text", "-noout"]) + assert openssl_out =~ ~r(Subject: C ?= ?US, ST ?= ?NT, L ?= ?Springfield, O ?= ?ACME Inc.) + assert openssl_out =~ "Public Key Algorithm: ED25519" + assert openssl_out =~ "Signature Algorithm: ED25519" + assert openssl_out =~ "DNS:acme.com, DNS:www.acme.com" + end + test "OpenSSL can read CRLs (RSA)" do ca_key = X509.PrivateKey.new_rsa(512) ca = X509.Certificate.self_signed(ca_key, "/CN=My Root CA", template: :root_ca) @@ -404,6 +480,42 @@ defmodule X509.OpenSSLTest do assert openssl_out =~ "Serial Number: FF" assert openssl_out =~ "Key Compromise" end + + test "OpenSSL can read CRLs (EdDSA)" do + ca_key = X509.PrivateKey.new_ec(:ed25519) + ca = X509.Certificate.self_signed(ca_key, "/CN=My Root CA", template: :root_ca) + + cert = + X509.PrivateKey.new_ec(:ed25519) + |> X509.PublicKey.derive() + |> X509.Certificate.new("/CN=Sample", ca, ca_key, + serial: 0xFF, + extensions: [ + crl_distribution_points: + X509.Certificate.Extension.crl_distribution_points(["http://localhost/test.crl"]) + ] + ) + + entry = + X509.CRL.Entry.new(cert, DateTime.utc_now(), [ + X509.CRL.Extension.reason_code(:keyCompromise) + ]) + + file = + entry + |> List.wrap() + |> X509.CRL.new(ca, ca_key) + |> X509.CRL.to_der() + |> write_tmp() + + openssl_out = openssl(["crl", "-in", file, "-inform", "der", "-text", "-noout"]) + assert openssl_out =~ "Certificate Revocation List (CRL)" + assert openssl_out =~ "Signature Algorithm: ED25519" + assert openssl_out =~ ~r(Issuer: /?CN ?= ?My Root CA) + assert openssl_out =~ "X509v3 Authority Key Identifier:" + assert openssl_out =~ "Serial Number: FF" + assert openssl_out =~ "Key Compromise" + end end defp openssl(args) do diff --git a/test/x509/certificate_test.exs b/test/x509/certificate_test.exs index 10bebf9..559359c 100644 --- a/test/x509/certificate_test.exs +++ b/test/x509/certificate_test.exs @@ -8,6 +8,7 @@ defmodule X509.CertificateTest do [ rsa_key: X509.PrivateKey.new_rsa(512), ec_key: X509.PrivateKey.new_ec(:secp256r1), + eddsa_key: X509.PrivateKey.new_ec(:ed25519), selfsigned_rsa: "test/data/selfsigned_rsa.pem" |> File.read!() @@ -23,6 +24,14 @@ defmodule X509.CertificateTest do selfsigned_ecdsa_key: "test/data/prime256v1.pem" |> File.read!() + |> X509.PrivateKey.from_pem!(), + selfsigned_eddsa: + "test/data/selfsigned_ed25519.pem" + |> File.read!() + |> X509.Certificate.from_pem!(), + selfsigned_eddsa_key: + "test/data/ed25519.pem" + |> File.read!() |> X509.PrivateKey.from_pem!() ] end @@ -192,6 +201,75 @@ defmodule X509.CertificateTest do end end + describe "EdDSA" do + test :new, context do + cert = + context.eddsa_key + |> X509.PublicKey.derive() + |> X509.Certificate.new( + "/C=US/ST=NT/L=Springfield/O=ACME Inc./CN=Example", + context.selfsigned_eddsa, + context.selfsigned_eddsa_key + ) + + assert match?(otp_certificate(), cert) + refute :public_key.pkix_is_self_signed(cert) + assert :public_key.pkix_is_issuer(cert, context.selfsigned_eddsa) + assert {:ok, _} = :public_key.pkix_path_validation(context.selfsigned_eddsa, [cert], []) + + assert cert + |> X509.Certificate.to_der() + |> :public_key.pkix_verify(X509.PublicKey.derive(context.selfsigned_eddsa_key)) + end + + test "intermediate", context do + root_key = X509.PrivateKey.new_ec(:ed25519) + + root = + root_key + |> X509.Certificate.self_signed( + "/C=US/ST=NT/L=Springfield/O=ACME Inc./CN=Intermediate CA", + template: :root_ca + ) + + intermediate_key = X509.PrivateKey.new_ec(:ed25519) + + intermediate = + intermediate_key + |> X509.PublicKey.derive() + |> X509.Certificate.new( + "/C=US/ST=NT/L=Springfield/O=ACME Inc./CN=Intermediate CA", + root, + root_key, + template: :ca + ) + + cert = + context.eddsa_key + |> X509.PublicKey.derive() + |> X509.Certificate.new( + "/C=US/ST=NT/L=Springfield/O=ACME Inc./CN=Example", + intermediate, + intermediate_key + ) + + assert {:ok, _} = :public_key.pkix_path_validation(root, [intermediate, cert], []) + end + + test :self_signed, context do + cert = + context.eddsa_key + |> X509.Certificate.self_signed("/C=US/ST=NT/L=Springfield/O=ACME Inc.") + + assert match?(otp_certificate(), cert) + assert :public_key.pkix_is_self_signed(cert) + + assert cert + |> X509.Certificate.to_der() + |> :public_key.pkix_verify(X509.PublicKey.derive(context.eddsa_key)) + end + end + test :version, context do assert :v3 == X509.Certificate.version(context.selfsigned_rsa) end