Environment
- Elixir version (elixir -v): Elixir 1.20.0 (compiled with Erlang/OTP 29)
- Phoenix version (mix deps): 1.8.9, and
lib/mix/tasks/phx.gen.cert.ex is the same on main
- Operating system: macOS 15, Chrome 153
Actual behavior
Chrome refuses to connect to a server using a certificate from mix phx.gen.cert. The tab shows ERR_SSL_PROTOCOL_ERROR. Because it's a protocol error rather than a trust warning, there is no "proceed anyway", and --ignore-certificate-errors doesn't get past it either. On the server, :ssl logs TLS :server: In state :certify received CLIENT ALERT: Fatal - Decode Error (:wait_finished over TLS 1.3). curl and OpenSSL connect fine.
The certificate's public key info names the key type with the rsaEncryption algorithm identifier but leaves its parameters out. RFC 3279 §2.3.1 says they "MUST have ASN.1 type NULL". BoringSSL, Chrome's TLS library, enforces that when decoding the key (rsa_pub_decode in crypto/evp/p_rsa.cc returns EVP_R_DECODE_ERROR), so Chrome aborts the handshake.
$ openssl asn1parse -in priv/cert/selfsigned.pem | grep -A1 rsaEncryption
213:d=4 hl=2 l= 9 prim: OBJECT :rsaEncryption
224:d=3 hl=4 l= 271 prim: BIT STRING
A certificate from openssl req -x509 has a NULL between those two lines.
The parameters went away in #6332, which fixed the OTP 28 crash from #6319 by dropping parameters: :NULL from both the signature algorithm and the public key algorithm. Only the signature one needed it. On OTP 28+ the signature algorithm's parameters are an open type, so the :NULL atom no longer encodes there, which is the badmatch in #6319. The public key algorithm's parameters are a plain NULL type and still encode fine.
To find which field Chrome cares about, I re-signed the certificate from Mix.Tasks.Phx.Gen.Cert.certificate_and_key/3 four ways, served each one, and loaded it in Chrome:
signature algorithm public key algorithm Chrome
as generated no NULL no NULL fails
NULL on signature only NULL no NULL fails
NULL on public key only no NULL NULL loads
NULL on both NULL NULL loads
Reproduction from scratch:
mix phx.new demo && cd demo
mix phx.gen.cert
Add the https: config the task prints to config/dev.exs, mix phx.server, open https://localhost:4001 in Chrome.
Expected behavior
The certificate loads in Chrome like one from OpenSSL does. Putting the parameters back on the public key algorithm only is enough:
subjectPublicKeyInfo:
otp_subject_public_key_info(
- algorithm: public_key_algorithm(algorithm: @rsaEncryption),
+ algorithm: public_key_algorithm(algorithm: @rsaEncryption, parameters: :NULL),
subjectPublicKey: public_key
),
I checked that this encodes with :public_key.pkix_sign/2 on OTP 25.3, 27.2, 28.1, 28.5 and 29.0, and that the pre-#6332 form (both :NULL) still crashes on 28.1, 28.5 and 29.0, so the OTP 28 fix stays intact. The signature algorithm can stay as it is: Chrome accepts it without parameters, and RFC 4055 §5 requires implementations to accept that form.
Environment
lib/mix/tasks/phx.gen.cert.exis the same onmainActual behavior
Chrome refuses to connect to a server using a certificate from
mix phx.gen.cert. The tab showsERR_SSL_PROTOCOL_ERROR. Because it's a protocol error rather than a trust warning, there is no "proceed anyway", and--ignore-certificate-errorsdoesn't get past it either. On the server,:ssllogsTLS :server: In state :certify received CLIENT ALERT: Fatal - Decode Error(:wait_finishedover TLS 1.3). curl and OpenSSL connect fine.The certificate's public key info names the key type with the
rsaEncryptionalgorithm identifier but leaves its parameters out. RFC 3279 §2.3.1 says they "MUST have ASN.1 type NULL". BoringSSL, Chrome's TLS library, enforces that when decoding the key (rsa_pub_decodeincrypto/evp/p_rsa.ccreturnsEVP_R_DECODE_ERROR), so Chrome aborts the handshake.A certificate from
openssl req -x509has aNULLbetween those two lines.The parameters went away in #6332, which fixed the OTP 28 crash from #6319 by dropping
parameters: :NULLfrom both the signature algorithm and the public key algorithm. Only the signature one needed it. On OTP 28+ the signature algorithm's parameters are an open type, so the:NULLatom no longer encodes there, which is thebadmatchin #6319. The public key algorithm's parameters are a plainNULLtype and still encode fine.To find which field Chrome cares about, I re-signed the certificate from
Mix.Tasks.Phx.Gen.Cert.certificate_and_key/3four ways, served each one, and loaded it in Chrome:Reproduction from scratch:
Add the
https:config the task prints toconfig/dev.exs,mix phx.server, openhttps://localhost:4001in Chrome.Expected behavior
The certificate loads in Chrome like one from OpenSSL does. Putting the parameters back on the public key algorithm only is enough:
subjectPublicKeyInfo: otp_subject_public_key_info( - algorithm: public_key_algorithm(algorithm: @rsaEncryption), + algorithm: public_key_algorithm(algorithm: @rsaEncryption, parameters: :NULL), subjectPublicKey: public_key ),I checked that this encodes with
:public_key.pkix_sign/2on OTP 25.3, 27.2, 28.1, 28.5 and 29.0, and that the pre-#6332 form (both:NULL) still crashes on 28.1, 28.5 and 29.0, so the OTP 28 fix stays intact. The signature algorithm can stay as it is: Chrome accepts it without parameters, and RFC 4055 §5 requires implementations to accept that form.