VYPR
High severityNVD Advisory· Published May 27, 2026· Updated May 27, 2026

CVE-2026-42790

CVE-2026-42790

Description

Improper Certificate Validation vulnerability in Erlang OTP public_key (pubkey_cert and public_key modules) allows a DNS nameConstraints bypass via subject CommonName fallback in TLS hostname verification.

Two flaws combine to allow a subordinate CA whose DNS nameConstraints are restricted (e.g. permitted;DNS:allowed.example.com) to issue a leaf certificate that an OTP TLS client accepts as a valid identity for an out-of-scope hostname (e.g. victim.example.com):

First, pubkey_cert:validate_names/6 in lib/public_key/src/pubkey_cert.erl only checks SAN DNS entries against nameConstraints. Per RFC 5280, a permitted DNS subtree only restricts certificates that contain a DNS-typed name. A leaf with no subjectAltName therefore trivially satisfies any permitted;DNS:... constraint regardless of its subject commonName.

Second, public_key:pkix_verify_hostname/3 in lib/public_key/src/public_key.erl falls back to the subject commonName when no subjectAltName is present, extracting id-at-commonName attributes as presented IDs and matching them against the reference hostname. The strict pkix_verify_hostname_match_fun(https) matcher does not suppress this fallback.

The result is that path validation accepts a CN-only leaf under a DNS-constrained intermediate (no SAN means the nameConstraints are not triggered), and hostname verification then accepts it via the CN fallback. The bypass is reachable from stock ssl:connect with verify_peer, a trusted CA, SNI, and the canonical strict https hostname matcher.

This issue affects OTP from OTP 19.3 before OTP 26.2.5.21, 27.3.4.12, 28.5.0.1, and 29.0.1 corresponding to public_key from 1.4 before 1.15.1.7, 1.17.1.3, 1.20.3.1, and 1.21.1.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Two flaws in Erlang OTP public_key allow a DNS-constrained subordinate CA to issue a leaf certificate valid for any hostname via CN fallback when no SAN is present.

Vulnerability

CVE-2026-42790 affects the public_key application in Erlang OTP versions 19.3 up to (but excluding) 26.2.5.21, 27.3.4.12, 28.5.0.1, and 29.0.1, corresponding to public_key versions 1.4 up to 1.15.1.7, 1.17.1.3, 1.20.3.1, and 1.21.1 [1]. Two bugs in pubkey_cert.erl and public_key.erl combine: first, public_key:pkix_verify_hostname/3 falls back to the subject CommonName when no subjectAltName extension is present, extracting id-at-commonName attributes as presented IDs [2][3][4]. Second, pubkey_cert:validate_names/6 only checks SAN DNS entries against DNS nameConstraints, so a leaf certificate without a SAN (subjectAltName) trivially bypasses any permitted DNS subtree restrictions, because per RFC 5280 the nameConstraints only apply to certificates that actually contain a DNS-typed name.

Exploitation

An attacker who controls or compromises a subordinate CA whose DNS nameConstraints are restricted to e.g. permitted;DNS:allowed.example.com can issue a leaf certificate with no subjectAltName extension, but with a subject CommonName set to victim.example.com. When the Erlang OTP TLS client (e.g., ssl:connect/3 with verify_peer, a trusted CA, SNI, and the canonical strict https hostname matcher from pkix_verify_hostname_match_fun(https)) connects to a server presenting this leaf, the path validation accepts the leaf (nameConstraints are not triggered because no DNS name is in the certificate), and the hostname verification falls back to matching the CommonName against the reference hostname [2][3][4]. No additional user interaction or network position beyond standard TLS client–server communication is required.

Impact

An attacker can successfully impersonate any hostname (e.g., victim.example.com) towards an Erlang OTP application that uses the vulnerable hostname verification, even though the issuing subordinate CA was supposedly restricted to allowed.example.com. This defeats the security guarantees of DNS nameConstraints, leading to potential information disclosure, man-in-the-middle attacks, and compromise of trust in the TLS peer identity.

Mitigation

Fixed versions are OTP 26.2.5.21, 27.3.4.12, 28.5.0.1, and 29.0.1 (released concurrently with the disclosure), corresponding to public_key versions 1.15.1.7, 1.17.1.3, 1.20.3.1, and 1.21.1 [1]. The fix removes the CN fallback entirely, in adherence to RFC 9525 [2][3][4]. Users must upgrade their Erlang OTP installation to a patched version; no workaround exists because the vulnerable code path is intrinsic to the default hostname verification logic. This CVE is not listed on CISA’s Known Exploited Vulnerabilities (KEV) as of publication.

AI Insight generated on May 27, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

3
  • SSH/SSHreferences
  • Range: >=1.4 <1.15.1.7 || >=1.16 <1.17.1.3 || >=1.18 <1.20.3.1 || >=1.21 <1.21.1
  • Erlang/Otpllm-fuzzy
    Range: >=19.3 <26.2.5.21 || >=27.0 <27.3.4.12 || >=28.0 <28.5.0.1 || >=29.0 <29.0.1

Patches

6
9166ae4e6987

public_key: Add new error

https://github.com/erlang/otpIngela Anderton AndinMay 6, 2026via github-commit-search
3 files changed · +47 24
  • lib/public_key/src/pubkey_cert.erl+15 9 modified
    @@ -237,18 +237,24 @@ validate_names(Cert, Permit, Exclude, Last, UserState, VerifyFun) ->
     			       AltSubject#'Extension'.extnValue
     		       end,
     
    -	    case (is_permitted(Name, Permit) andalso
    -		  is_permitted(AltNames, Permit) andalso
    -		  (not is_excluded(Name, Exclude)) andalso
    -		  (not is_excluded(AltNames, Exclude))) of
    -		true ->
    -		    UserState;
    -		false ->
    -		    verify_fun(Cert, {bad_cert, name_not_permitted},
    -			      UserState, VerifyFun)
    +	    case is_permitted_name(Name, Permit, Exclude) of
    +                false ->
    +                    verify_fun(Cert, {bad_cert, distinguished_name_not_permitted},
    +                               UserState, VerifyFun);
    +                true ->
    +                    case is_permitted_name(AltNames, Permit, Exclude) of
    +                        false ->
    +                            verify_fun(Cert, {bad_cert, name_not_permitted},
    +                                       UserState, VerifyFun);
    +                        true ->
    +                            UserState
    +                    end
     	    end
         end.
     
    +is_permitted_name(Name, Permit, Exclude) ->
    +    (is_permitted(Name, Permit) andalso (not is_excluded(Name, Exclude))).
    +
     %%--------------------------------------------------------------------
     -spec validate_signature(#cert{}, DER::binary(),
     			 term(),term(), term(), fun()) -> term() | no_return().
    
  • lib/public_key/src/public_key.erl+8 2 modified
    @@ -306,7 +306,9 @@ The value of the issuer part of a certificate.
     -doc """
     The reason that a certifcate gets rejected by the certificate path validation.
     """.
    --type bad_cert_reason()      :: cert_expired | invalid_issuer | invalid_signature | name_not_permitted |
    +-type bad_cert_reason()      :: cert_expired | invalid_issuer | invalid_signature |
    +                                distinguished_name_not_permitted |
    +                                name_not_permitted |
                                     missing_basic_constraint | invalid_key_usage | duplicate_cert_in_path |
                                     {key_usage_mismatch, term()} |
                                     {'policy_requirement_not_met', term()} | {'invalid_policy_mapping', term()} |
    @@ -1647,7 +1649,11 @@ Explanations of reasons for a bad certificate:
     - **invalid_signature** - Certificate was not signed by its issuer certificate
       in the chain.
     
    -- **name_not_permitted** - Invalid Subject Alternative Name extension.
    +- **distinguished_name_not_permitted** - Subject Name does not adhere to name constraints
    +  which is mandatory in RFC 5280.
    +
    +- **name_not_permitted** - Subject Alternative Name does not adhere to name constraints,
    +  which is optional in RFC 5280.
     
     - **missing_basic_constraint** - Certificate, required to have the basic
       constraints extension, does not have a basic constraints extension.
    
  • lib/public_key/test/pkits_SUITE.erl+24 13 modified
    @@ -1249,19 +1249,30 @@ valid_DN_name_constraints(Config) when is_list(Config) ->
     invalid_DN_name_constraints() ->
         [{doc,"Name constraints tests"}].
     invalid_DN_name_constraints(Config) when is_list(Config) ->
    -    run([{ "4.13.2", "Invalid DN nameConstraints Test2 EE", {bad_cert, name_not_permitted}},
    -	 { "4.13.3",  "Invalid DN nameConstraints Test3 EE", {bad_cert, name_not_permitted}},
    -	 { "4.13.7",  "Invalid DN nameConstraints Test7 EE", {bad_cert, name_not_permitted}},
    -	 { "4.13.8",  "Invalid DN nameConstraints Test8 EE", {bad_cert, name_not_permitted}},
    -	 { "4.13.9",  "Invalid DN nameConstraints Test9 EE", {bad_cert, name_not_permitted}},
    -	 { "4.13.10", "Invalid DN nameConstraints Test10 EE",{bad_cert, name_not_permitted}},
    -	 { "4.13.12", "Invalid DN nameConstraints Test12 EE",{bad_cert, name_not_permitted}},
    -	 { "4.13.13", "Invalid DN nameConstraints Test13 EE",{bad_cert, name_not_permitted}},
    -	 { "4.13.15", "Invalid DN nameConstraints Test15 EE",{bad_cert, name_not_permitted}},
    -	 { "4.13.16", "Invalid DN nameConstraints Test16 EE",{bad_cert, name_not_permitted}},
    -	 { "4.13.17", "Invalid DN nameConstraints Test17 EE",{bad_cert, name_not_permitted}},
    +    run([{ "4.13.2", "Invalid DN nameConstraints Test2 EE",
    +           {bad_cert,  distinguished_name_not_permitted}},
    +	 { "4.13.3",  "Invalid DN nameConstraints Test3 EE",
    +           {bad_cert, name_not_permitted}},
    +	 { "4.13.7",  "Invalid DN nameConstraints Test7 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.8",  "Invalid DN nameConstraints Test8 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.9",  "Invalid DN nameConstraints Test9 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.10", "Invalid DN nameConstraints Test10 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.12", "Invalid DN nameConstraints Test12 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.13", "Invalid DN nameConstraints Test13 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.15", "Invalid DN nameConstraints Test15 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.16", "Invalid DN nameConstraints Test16 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.17", "Invalid DN nameConstraints Test17 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
     	 { "4.13.20", "Invalid DN nameConstraints Test20 EE",
    -	   {bad_cert, name_not_permitted}}]).
    +	   {bad_cert, distinguished_name_not_permitted}}]).
     
     valid_rfc822_name_constraints() ->
         [{doc,"Name constraints tests"}].
    @@ -1291,7 +1302,7 @@ invalid_DN_and_rfc822_name_constraints(Config) when is_list(Config) ->
         run([{ "4.13.28", "Invalid DN and RFC822 nameConstraints Test28 EE",
     	   {bad_cert, name_not_permitted}},
     	 { "4.13.29", "Invalid DN and RFC822 nameConstraints Test29 EE",
    -	   {bad_cert, name_not_permitted}}]).
    +	   {bad_cert, distinguished_name_not_permitted}}]).
     
     valid_dns_name_constraints() ->
         [{doc,"Name constraints tests"}].
    
552b224c0261

public_key: Add new error

https://github.com/erlang/otpIngela Anderton AndinMay 6, 2026via github-commit-search
3 files changed · +43 23
  • lib/public_key/doc/src/public_key.xml+4 1 modified
    @@ -628,8 +628,11 @@ fun(OtpCert :: #'OTPCertificate'{},
     	<tag>invalid_signature</tag>
     	<item><p>Certificate was not signed by its issuer certificate in the chain.</p></item>
     
    +	<tag>distinguished_name_not_permitted</tag>
    +	<item><p>Subject Name does not adhere to name constraints which is mandatory in RFC 5280</p></item>
    +
     	<tag>name_not_permitted</tag>
    -	<item><p>Invalid Subject Alternative Name extension.</p></item>
    +	<item><p>Subject Alternative Name extension does not adhere to name constraints which is optional in RFC 5280</p></item>
     
     	<tag>missing_basic_constraint</tag>
     	<item><p>Certificate, required to have the basic constraints extension, does not have
    
  • lib/public_key/src/pubkey_cert.erl+15 9 modified
    @@ -231,18 +231,24 @@ validate_names(OtpCert, Permit, Exclude, Last, UserState, VerifyFun) ->
     			       AltSubject#'Extension'.extnValue
     		       end,
     
    -	    case (is_permitted(Name, Permit) andalso
    -		  is_permitted(AltNames, Permit) andalso
    -		  (not is_excluded(Name, Exclude)) andalso
    -		  (not is_excluded(AltNames, Exclude))) of
    -		true ->
    -		    UserState;
    -		false ->
    -		    verify_fun(OtpCert, {bad_cert, name_not_permitted},
    -			      UserState, VerifyFun)
    +	    case is_permitted_name(Name, Permit, Exclude) of
    +                false ->
    +                    verify_fun(OtpCert, {bad_cert, distinguished_name_not_permitted},
    +                               UserState, VerifyFun);
    +                true ->
    +                    case is_permitted_name(AltNames, Permit, Exclude) of
    +                        false ->
    +                            verify_fun(OtpCert, {bad_cert, name_not_permitted},
    +                                       UserState, VerifyFun);
    +                        true ->
    +                            UserState
    +                    end
     	    end
         end.
     
    +is_permitted_name(Name, Permit, Exclude) ->
    +    (is_permitted(Name, Permit) andalso (not is_excluded(Name, Exclude))).
    +
     %%--------------------------------------------------------------------
     -spec validate_signature(#'OTPCertificate'{}, DER::binary(),
     			 term(),term(), term(), fun()) -> term() | no_return().
    
  • lib/public_key/test/pkits_SUITE.erl+24 13 modified
    @@ -1249,19 +1249,30 @@ valid_DN_name_constraints(Config) when is_list(Config) ->
     invalid_DN_name_constraints() ->
         [{doc,"Name constraints tests"}].
     invalid_DN_name_constraints(Config) when is_list(Config) ->
    -    run([{ "4.13.2", "Invalid DN nameConstraints Test2 EE", {bad_cert, name_not_permitted}},
    -	 { "4.13.3",  "Invalid DN nameConstraints Test3 EE", {bad_cert, name_not_permitted}},
    -	 { "4.13.7",  "Invalid DN nameConstraints Test7 EE", {bad_cert, name_not_permitted}},
    -	 { "4.13.8",  "Invalid DN nameConstraints Test8 EE", {bad_cert, name_not_permitted}},
    -	 { "4.13.9",  "Invalid DN nameConstraints Test9 EE", {bad_cert, name_not_permitted}},
    -	 { "4.13.10", "Invalid DN nameConstraints Test10 EE",{bad_cert, name_not_permitted}},
    -	 { "4.13.12", "Invalid DN nameConstraints Test12 EE",{bad_cert, name_not_permitted}},
    -	 { "4.13.13", "Invalid DN nameConstraints Test13 EE",{bad_cert, name_not_permitted}},
    -	 { "4.13.15", "Invalid DN nameConstraints Test15 EE",{bad_cert, name_not_permitted}},
    -	 { "4.13.16", "Invalid DN nameConstraints Test16 EE",{bad_cert, name_not_permitted}},
    -	 { "4.13.17", "Invalid DN nameConstraints Test17 EE",{bad_cert, name_not_permitted}},
    +    run([{ "4.13.2", "Invalid DN nameConstraints Test2 EE",
    +           {bad_cert,  distinguished_name_not_permitted}},
    +	 { "4.13.3",  "Invalid DN nameConstraints Test3 EE",
    +           {bad_cert, name_not_permitted}},
    +	 { "4.13.7",  "Invalid DN nameConstraints Test7 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.8",  "Invalid DN nameConstraints Test8 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.9",  "Invalid DN nameConstraints Test9 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.10", "Invalid DN nameConstraints Test10 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.12", "Invalid DN nameConstraints Test12 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.13", "Invalid DN nameConstraints Test13 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.15", "Invalid DN nameConstraints Test15 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.16", "Invalid DN nameConstraints Test16 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.17", "Invalid DN nameConstraints Test17 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
     	 { "4.13.20", "Invalid DN nameConstraints Test20 EE",
    -	   {bad_cert, name_not_permitted}}]).
    +	   {bad_cert, distinguished_name_not_permitted}}]).
     
     valid_rfc822_name_constraints() ->
         [{doc,"Name constraints tests"}].
    @@ -1291,7 +1302,7 @@ invalid_DN_and_rfc822_name_constraints(Config) when is_list(Config) ->
         run([{ "4.13.28", "Invalid DN and RFC822 nameConstraints Test28 EE",
     	   {bad_cert, name_not_permitted}},
     	 { "4.13.29", "Invalid DN and RFC822 nameConstraints Test29 EE",
    -	   {bad_cert, name_not_permitted}}]).
    +	   {bad_cert, distinguished_name_not_permitted}}]).
     
     valid_dns_name_constraints() ->
         [{doc,"Name constraints tests"}].
    
153f0cd7d726

public_key: Add new error

https://github.com/erlang/otpIngela Anderton AndinMay 6, 2026via github-commit-search
3 files changed · +47 24
  • lib/public_key/src/pubkey_cert.erl+15 9 modified
    @@ -237,18 +237,24 @@ validate_names(Cert, Permit, Exclude, Last, UserState, VerifyFun) ->
     			       AltSubject#'Extension'.extnValue
     		       end,
     
    -	    case (is_permitted(Name, Permit) andalso
    -		  is_permitted(AltNames, Permit) andalso
    -		  (not is_excluded(Name, Exclude)) andalso
    -		  (not is_excluded(AltNames, Exclude))) of
    -		true ->
    -		    UserState;
    -		false ->
    -		    verify_fun(Cert, {bad_cert, name_not_permitted},
    -			      UserState, VerifyFun)
    +	    case is_permitted_name(Name, Permit, Exclude) of
    +                false ->
    +                    verify_fun(Cert, {bad_cert, distinguished_name_not_permitted},
    +                               UserState, VerifyFun);
    +                true ->
    +                    case is_permitted_name(AltNames, Permit, Exclude) of
    +                        false ->
    +                            verify_fun(Cert, {bad_cert, name_not_permitted},
    +                                       UserState, VerifyFun);
    +                        true ->
    +                            UserState
    +                    end
     	    end
         end.
     
    +is_permitted_name(Name, Permit, Exclude) ->
    +    (is_permitted(Name, Permit) andalso (not is_excluded(Name, Exclude))).
    +
     %%--------------------------------------------------------------------
     -spec validate_signature(#cert{}, DER::binary(),
     			 term(),term(), term(), fun()) -> term() | no_return().
    
  • lib/public_key/src/public_key.erl+8 2 modified
    @@ -323,7 +323,9 @@ The value of the issuer part of a certificate.
     -doc """
     The reason that a certifcate gets rejected by the certificate path validation.
     """.
    --type bad_cert_reason()      :: cert_expired | invalid_issuer | invalid_signature | name_not_permitted |
    +-type bad_cert_reason()      :: cert_expired | invalid_issuer | invalid_signature |
    +                                distinguished_name_not_permitted |
    +                                name_not_permitted |
                                     missing_basic_constraint | invalid_key_usage | duplicate_cert_in_path |
                                     {key_usage_mismatch, term()} |
                                     {'policy_requirement_not_met', term()} | {'invalid_policy_mapping', term()} |
    @@ -2011,7 +2013,11 @@ Explanations of reasons for a bad certificate:
     - **invalid_signature** - Certificate was not signed by its issuer certificate
       in the chain.
     
    -- **name_not_permitted** - Invalid Subject Alternative Name extension.
    +- **distinguished_name_not_permitted** - Subject Name does not adhere to name constraints
    +  which is mandatory in RFC 5280.
    +
    +- **name_not_permitted** - Subject Alternative Name does not adhere to name constraints,
    +  which is optional in RFC 5280.
     
     - **missing_basic_constraint** - Certificate, required to have the basic
       constraints extension, does not have a basic constraints extension.
    
  • lib/public_key/test/pkits_SUITE.erl+24 13 modified
    @@ -1251,19 +1251,30 @@ valid_DN_name_constraints(Config) when is_list(Config) ->
     invalid_DN_name_constraints() ->
         [{doc,"Name constraints tests"}].
     invalid_DN_name_constraints(Config) when is_list(Config) ->
    -    run([{ "4.13.2", "Invalid DN nameConstraints Test2 EE", {bad_cert, name_not_permitted}},
    -	 { "4.13.3",  "Invalid DN nameConstraints Test3 EE", {bad_cert, name_not_permitted}},
    -	 { "4.13.7",  "Invalid DN nameConstraints Test7 EE", {bad_cert, name_not_permitted}},
    -	 { "4.13.8",  "Invalid DN nameConstraints Test8 EE", {bad_cert, name_not_permitted}},
    -	 { "4.13.9",  "Invalid DN nameConstraints Test9 EE", {bad_cert, name_not_permitted}},
    -	 { "4.13.10", "Invalid DN nameConstraints Test10 EE",{bad_cert, name_not_permitted}},
    -	 { "4.13.12", "Invalid DN nameConstraints Test12 EE",{bad_cert, name_not_permitted}},
    -	 { "4.13.13", "Invalid DN nameConstraints Test13 EE",{bad_cert, name_not_permitted}},
    -	 { "4.13.15", "Invalid DN nameConstraints Test15 EE",{bad_cert, name_not_permitted}},
    -	 { "4.13.16", "Invalid DN nameConstraints Test16 EE",{bad_cert, name_not_permitted}},
    -	 { "4.13.17", "Invalid DN nameConstraints Test17 EE",{bad_cert, name_not_permitted}},
    +    run([{ "4.13.2", "Invalid DN nameConstraints Test2 EE",
    +           {bad_cert,  distinguished_name_not_permitted}},
    +	 { "4.13.3",  "Invalid DN nameConstraints Test3 EE",
    +           {bad_cert, name_not_permitted}},
    +	 { "4.13.7",  "Invalid DN nameConstraints Test7 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.8",  "Invalid DN nameConstraints Test8 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.9",  "Invalid DN nameConstraints Test9 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.10", "Invalid DN nameConstraints Test10 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.12", "Invalid DN nameConstraints Test12 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.13", "Invalid DN nameConstraints Test13 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.15", "Invalid DN nameConstraints Test15 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.16", "Invalid DN nameConstraints Test16 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
    +	 { "4.13.17", "Invalid DN nameConstraints Test17 EE",
    +           {bad_cert, distinguished_name_not_permitted}},
     	 { "4.13.20", "Invalid DN nameConstraints Test20 EE",
    -	   {bad_cert, name_not_permitted}}]).
    +	   {bad_cert, distinguished_name_not_permitted}}]).
     
     valid_rfc822_name_constraints() ->
         [{doc,"Name constraints tests"}].
    @@ -1293,7 +1304,7 @@ invalid_DN_and_rfc822_name_constraints(Config) when is_list(Config) ->
         run([{ "4.13.28", "Invalid DN and RFC822 nameConstraints Test28 EE",
     	   {bad_cert, name_not_permitted}},
     	 { "4.13.29", "Invalid DN and RFC822 nameConstraints Test29 EE",
    -	   {bad_cert, name_not_permitted}}]).
    +	   {bad_cert, distinguished_name_not_permitted}}]).
     
     valid_dns_name_constraints() ->
         [{doc,"Name constraints tests"}].
    
fb67c6d1836f

public_key: Adhere to RFC 9525

https://github.com/erlang/otpIngela Anderton AndinMay 5, 2026via github-commit-search
2 files changed · +31 77
  • lib/public_key/src/public_key.erl+2 24 modified
    @@ -1901,26 +1901,8 @@ pkix_verify_hostname(Cert = #'OTPCertificate'{tbsCertificate = TbsCert}, Referen
         %% PresentedIDs example: [{dNSName,"ewstest.ericsson.com"}, {dNSName,"www.ericsson.com"}]}
         case PresentedIDs of
     	[] ->
    -	    %% Fallback to CN-ids [rfc6125, ch6]
    -	    case TbsCert#'OTPTBSCertificate'.subject of
    -		{rdnSequence,RDNseq} ->
    -		    PresentedCNs =
    -			[{cn, to_string(V)}
    -			 || ATVs <- RDNseq, % RDNseq is list-of-lists
    -			    #'AttributeTypeAndValue'{type = ?'id-at-commonName',
    -						     value = {_T,V}} <- ATVs
    -						% _T = kind of string (teletexString etc)
    -			],
    -		    %% Example of PresentedCNs:  [{cn,"www.ericsson.se"}]
    -		    %% match ReferenceIDs to PresentedCNs
    -		    verify_hostname_match_loop(verify_hostname_fqnds(ReferenceIDs, FqdnFun),
    -					       PresentedCNs,
    -					       MatchFun, FailCB, Cert);
    -		
    -		_ ->
    -		    false
    -	    end;
    -	_ ->
    +          false;
    +        _ ->
     	    %% match ReferenceIDs to PresentedIDs
     	    case verify_hostname_match_loop(ReferenceIDs, PresentedIDs,
     					    MatchFun, FailCB, Cert) of
    @@ -2805,10 +2787,6 @@ verify_hostname_fqnds(L, FqdnFun) ->
     verify_hostname_match_default(Ref, Pres) ->
         verify_hostname_match_default0(to_lower_ascii(Ref), to_lower_ascii(Pres)).
     
    -verify_hostname_match_default0(FQDN=[_|_], {cn,FQDN}) -> 
    -    not lists:member($*, FQDN);
    -verify_hostname_match_default0(FQDN=[_|_], {cn,Name=[_|_]}) -> 
    -    verify_hostname_match_wildcard(FQDN, Name);
     verify_hostname_match_default0({dns_id,R}, {dNSName,P}) ->
         R==P;
     verify_hostname_match_default0({uri_id,R}, {uniformResourceIdentifier,P}) ->
    
  • lib/public_key/test/public_key_SUITE.erl+29 53 modified
    @@ -1165,25 +1165,14 @@ pkix_verify_hostname_cn(Config) ->
         DataDir = proplists:get_value(data_dir, Config),
         {ok,Bin} = file:read_file(filename:join(DataDir,"pkix_verify_hostname_cn.pem")),
         Cert = public_key:pkix_decode_cert(element(2,hd(public_key:pem_decode(Bin))), otp),
    -
    -    %% Check that 1) only CNs are checked,
    -    %%            2) an empty label does not match a wildcard and
    -    %%            3) a wildcard does not match more than one label
    +    %% Fallback hostname check against CommonName is no longer allowed
         false = public_key:pkix_verify_hostname(Cert, [{dns_id,"erlang.org"},
     						   {dns_id,"foo.EXAMPLE.com"},
     						   {dns_id,"b.a.foo.EXAMPLE.com"}]),
    -
    -    %% Check that a hostname is extracted from a https-uri and used for checking:
    -    true =  public_key:pkix_verify_hostname(Cert, [{uri_id,"HTTPS://EXAMPLE.com"}]),
    -
    -    %% Check wildcard matching one label:
    -    true =  public_key:pkix_verify_hostname(Cert, [{dns_id,"a.foo.EXAMPLE.com"}]),
    -
    -    %% Check wildcard with surrounding chars matches one label:
    -    true =  public_key:pkix_verify_hostname(Cert, [{dns_id,"accb.bar.EXAMPLE.com"}]),
    -
    -    %% Check that a wildcard with surrounding chars matches an empty string:
    -    true =  public_key:pkix_verify_hostname(Cert, [{uri_id,"https://ab.bar.EXAMPLE.com"}]).
    +    false =  public_key:pkix_verify_hostname(Cert, [{uri_id,"HTTPS://EXAMPLE.com"}]),
    +    false =  public_key:pkix_verify_hostname(Cert, [{dns_id,"a.foo.EXAMPLE.com"}]),
    +    false =  public_key:pkix_verify_hostname(Cert, [{dns_id,"accb.bar.EXAMPLE.com"}]),
    +    false =  public_key:pkix_verify_hostname(Cert, [{uri_id,"https://ab.bar.EXAMPLE.com"}]).
     
     %%--------------------------------------------------------------------
     %% To generate the PEM file contents:
    @@ -1233,63 +1222,50 @@ pkix_verify_hostname_subjAltName(Config) ->
         ok.
     
     %%--------------------------------------------------------------------
    -%% Uses the pem-file for pkix_verify_hostname_cn
    -%% Subject: C=SE, CN=example.com, CN=*.foo.example.com, CN=a*b.bar.example.com, O=erlang.org
    +%% Uses the pem-file for pkix_verify_hostname_subjAltName.pem
    +%% Subject: Subject Alt Names: 
    +%%              [{dNSName,"kb.example.org"},
    +%%              {uniformResourceIdentifier,"http://www.example.org"},
    +%%              {uniformResourceIdentifier,"https://wws.example.org"}]}]
     pkix_verify_hostname_options(Config) ->
         DataDir = proplists:get_value(data_dir, Config),
    -    {ok,Bin} = file:read_file(filename:join(DataDir,"pkix_verify_hostname_cn.pem")),
    +    {ok,Bin} = file:read_file(filename:join(DataDir,"pkix_verify_hostname_subjAltName.pem")),
         Cert = public_key:pkix_decode_cert(element(2,hd(public_key:pem_decode(Bin))), otp),
         
         %% Check that the fail_callback is called and is presented the correct certificate:
    -    true = public_key:pkix_verify_hostname(Cert, [{dns_id,"erlang.org"}],
    +    true = public_key:pkix_verify_hostname(Cert, [{dns_id,"kb.example.org"}],
     					   [{fail_callback,
     					     fun(#'OTPCertificate'{}=C) when C==Cert -> 
     						     true; % To test the return value matters
     						(#'OTPCertificate'{}=C) -> 
     						     ct:log("~p:~p: Wrong cert:~n~p~nExpect~n~p",
     							    [?MODULE, ?LINE, C, Cert]),
    -						     ct:fail("Wrong cert, see log");
    -						(C) -> 
    +                                                     ct:fail("Wrong cert, see log");
    +						(C) ->
     						     ct:log("~p:~p: Bad cert: ~p",[?MODULE,?LINE,C]),
    -						     ct:fail("Bad cert, see log")
    +                                                     ct:fail("Bad cert, see log")
     					     end}]),
    -    
    -    %% Check the callback for user-provided match functions:
    -    true =  public_key:pkix_verify_hostname(Cert, [{dns_id,"very.wrong.domain"}],
    -					    [{match_fun,
    -					      fun("very.wrong.domain", {cn,"example.com"}) ->
    -						      true;
    -						 (_, _) ->
    -						      false
    -					      end}]),
    -    false = public_key:pkix_verify_hostname(Cert, [{dns_id,"not.example.com"}],
    +    false = public_key:pkix_verify_hostname(Cert, [{dns_id,"not.example.org"}],
     					    [{match_fun, fun(_, _) -> default end}]),
    -    true =  public_key:pkix_verify_hostname(Cert, [{dns_id,"example.com"}],
    +    true =  public_key:pkix_verify_hostname(Cert, [{dns_id,"kb.example.org"}],
     					    [{match_fun, fun(_, _) -> default end}]),
     
         %% Check the callback for user-provided fqdn extraction:
         true =  public_key:pkix_verify_hostname(Cert, [{uri_id,"some://very.wrong.domain"}],
    -					    [{fqdn_fun,
    -					      fun({uri_id, "some://very.wrong.domain"}) ->
    -						      "example.com";
    -						 (_) ->
    -						      ""
    -					      end}]),
    -    true =  public_key:pkix_verify_hostname(Cert, [{uri_id,"https://example.com"}],
    -					    [{fqdn_fun, fun(_) -> default end}]),
    +                                            [{fqdn_fun,
    +                                              fun({uri_id, "some://very.wrong.domain"}) ->
    +                                                      "kb.example.org";
    +                                                 (_) ->
    +                                                      ""
    +                                              end}]),
    +    true =  public_key:pkix_verify_hostname(Cert, [{uri_id,"https://wws.example.org"}],
    +                                            [{fqdn_fun, fun(_) -> default end}]),
         false =  public_key:pkix_verify_hostname(Cert, [{uri_id,"some://very.wrong.domain"}]),
     
    -    true = public_key:pkix_verify_hostname(Cert, [{dns_id,"example.com"}]),
    -    true = public_key:pkix_verify_hostname(Cert, [{dns_id,"abb.bar.example.com"}]),
    -    false = public_key:pkix_verify_hostname(Cert, [{dns_id,"example.com"},
    -                                                   {dns_id,"abb.bar.example.com"}],
    -                                            [{fqdn_fun,fun(_)->undefined end}]),
    -    %% Test that a common name is matched fully, that is do not allow prefix matches
    -    %% with less dots (".")
    -    {ok, PrefixBin} = file:read_file(filename:join(DataDir,"prefix-dots.pem")),
    -    PrefixCert = public_key:pkix_decode_cert(element(2,hd(public_key:pem_decode(PrefixBin))), otp),
    -    true = public_key:pkix_verify_hostname(PrefixCert, [{dns_id,"..a"}]),
    -    false = public_key:pkix_verify_hostname(PrefixCert, [{dns_id,".a"}]).
    +    true = public_key:pkix_verify_hostname(Cert,
    +                                           [{dns_id,"foobar.example.org"}],
    +                                           [{match_fun,
    +                                             public_key:pkix_verify_hostname_match_fun(https)}]).
     
     %%--------------------------------------------------------------------
     %% To generate the PEM file contents:
    
21abed64eb20

public_key: Adhere to RFC 9525

https://github.com/erlang/otpIngela Anderton AndinMay 5, 2026via github-commit-search
2 files changed · +30 75
  • lib/public_key/src/public_key.erl+2 24 modified
    @@ -2265,26 +2265,8 @@ pkix_verify_hostname(Cert = #'OTPCertificate'{tbsCertificate = TbsCert}, Referen
         %% PresentedIDs example: [{dNSName,"ewstest.ericsson.com"}, {dNSName,"www.ericsson.com"}]}
         case PresentedIDs of
     	[] ->
    -	    %% Fallback to CN-ids [rfc6125, ch6]
    -	    case TbsCert#'OTPTBSCertificate'.subject of
    -		{rdnSequence,RDNseq} ->
    -		    PresentedCNs =
    -			[{cn, to_string(V)}
    -			 || ATVs <- RDNseq, % RDNseq is list-of-lists
    -			    #'AttributeTypeAndValue'{type = ?'id-at-commonName',
    -						     value = {_T,V}} <- ATVs
    -						% _T = kind of string (teletexString etc)
    -			],
    -		    %% Example of PresentedCNs:  [{cn,"www.ericsson.se"}]
    -		    %% match ReferenceIDs to PresentedCNs
    -		    verify_hostname_match_loop(verify_hostname_fqdns(ReferenceIDs, FqdnFun),
    -					       PresentedCNs,
    -					       MatchFun, FailCB, Cert);
    -		
    -		_ ->
    -		    false
    -	    end;
    -	_ ->
    +          false;
    +        _ ->
     	    %% match ReferenceIDs to PresentedIDs
     	    case verify_hostname_match_loop(ReferenceIDs, PresentedIDs,
     					    MatchFun, FailCB, Cert) of
    @@ -3201,10 +3183,6 @@ verify_hostname_fqdns(L, FqdnFun) ->
     verify_hostname_match_default(Ref, Pres) ->
         verify_hostname_match_default0(to_lower_ascii(Ref), to_lower_ascii(Pres)).
     
    -verify_hostname_match_default0(FQDN=[_|_], {cn,FQDN}) -> 
    -    not lists:member($*, FQDN);
    -verify_hostname_match_default0(FQDN=[_|_], {cn,Name=[_|_]}) -> 
    -    verify_hostname_match_wildcard(FQDN, Name);
     verify_hostname_match_default0({dns_id,R}, {dNSName,P}) ->
         R==P;
     verify_hostname_match_default0({uri_id,R}, {uniformResourceIdentifier,P}) ->
    
  • lib/public_key/test/public_key_SUITE.erl+28 51 modified
    @@ -1525,25 +1525,14 @@ pkix_verify_hostname_cn(Config) ->
         DataDir = proplists:get_value(data_dir, Config),
         {ok,Bin} = file:read_file(filename:join(DataDir,"pkix_verify_hostname_cn.pem")),
         Cert = public_key:pkix_decode_cert(element(2,hd(public_key:pem_decode(Bin))), otp),
    -
    -    %% Check that 1) only CNs are checked,
    -    %%            2) an empty label does not match a wildcard and
    -    %%            3) a wildcard does not match more than one label
    +    %% Fallback hostname check against CommonName is no longer allowed
         false = public_key:pkix_verify_hostname(Cert, [{dns_id,"erlang.org"},
     						   {dns_id,"foo.EXAMPLE.com"},
     						   {dns_id,"b.a.foo.EXAMPLE.com"}]),
    -
    -    %% Check that a hostname is extracted from a https-uri and used for checking:
    -    true =  public_key:pkix_verify_hostname(Cert, [{uri_id,"HTTPS://EXAMPLE.com"}]),
    -
    -    %% Check wildcard matching one label:
    -    true =  public_key:pkix_verify_hostname(Cert, [{dns_id,"a.foo.EXAMPLE.com"}]),
    -
    -    %% Check wildcard with surrounding chars matches one label:
    -    true =  public_key:pkix_verify_hostname(Cert, [{dns_id,"accb.bar.EXAMPLE.com"}]),
    -
    -    %% Check that a wildcard with surrounding chars matches an empty string:
    -    true =  public_key:pkix_verify_hostname(Cert, [{uri_id,"https://ab.bar.EXAMPLE.com"}]).
    +    false =  public_key:pkix_verify_hostname(Cert, [{uri_id,"HTTPS://EXAMPLE.com"}]),
    +    false =  public_key:pkix_verify_hostname(Cert, [{dns_id,"a.foo.EXAMPLE.com"}]),
    +    false =  public_key:pkix_verify_hostname(Cert, [{dns_id,"accb.bar.EXAMPLE.com"}]),
    +    false =  public_key:pkix_verify_hostname(Cert, [{uri_id,"https://ab.bar.EXAMPLE.com"}]).
     
     %%--------------------------------------------------------------------
     %% To generate the PEM file contents:
    @@ -1593,63 +1582,51 @@ pkix_verify_hostname_subjAltName(Config) ->
         ok.
     
     %%--------------------------------------------------------------------
    -%% Uses the pem-file for pkix_verify_hostname_cn
    -%% Subject: C=SE, CN=example.com, CN=*.foo.example.com, CN=a*b.bar.example.com, O=erlang.org
    +%% Uses the pem-file for pkix_verify_hostname_subjAltName.pem
    +%% Subject: Subject Alt Names: 
    +%%              [{dNSName,"kb.example.org"},
    +%%              {uniformResourceIdentifier,"http://www.example.org"},
    +%%              {uniformResourceIdentifier,"https://wws.example.org"}]}]
     pkix_verify_hostname_options(Config) ->
         DataDir = proplists:get_value(data_dir, Config),
    -    {ok,Bin} = file:read_file(filename:join(DataDir,"pkix_verify_hostname_cn.pem")),
    +    {ok,Bin} = file:read_file(filename:join(DataDir,"pkix_verify_hostname_subjAltName.pem")),
         Cert = public_key:pkix_decode_cert(element(2,hd(public_key:pem_decode(Bin))), otp),
     
         %% Check that the fail_callback is called and is presented the correct certificate:
    -    true = public_key:pkix_verify_hostname(Cert, [{dns_id,"erlang.org"}],
    +    true = public_key:pkix_verify_hostname(Cert, [{dns_id,"kb.example.org"}],
     					   [{fail_callback,
     					     fun(#'OTPCertificate'{}=C) when C==Cert ->
     						     true; % To test the return value matters
     						(#'OTPCertificate'{}=C) ->
     						     ct:log("~p:~p: Wrong cert:~n~p~nExpect~n~p",
     							    [?MODULE, ?LINE, C, Cert]),
    -						     ct:fail("Wrong cert, see log");
    +                                                     ct:fail("Wrong cert, see log");
     						(C) ->
     						     ct:log("~p:~p: Bad cert: ~p",[?MODULE,?LINE,C]),
    -						     ct:fail("Bad cert, see log")
    +                                                     ct:fail("Bad cert, see log")
     					     end}]),
     
    -    %% Check the callback for user-provided match functions:
    -    true =  public_key:pkix_verify_hostname(Cert, [{dns_id,"very.wrong.domain"}],
    -					    [{match_fun,
    -					      fun("very.wrong.domain", {cn,"example.com"}) ->
    -						      true;
    -						 (_, _) ->
    -						      false
    -					      end}]),
    -    false = public_key:pkix_verify_hostname(Cert, [{dns_id,"not.example.com"}],
    +    false = public_key:pkix_verify_hostname(Cert, [{dns_id,"not.example.org"}],
     					    [{match_fun, fun(_, _) -> default end}]),
    -    true =  public_key:pkix_verify_hostname(Cert, [{dns_id,"example.com"}],
    +    true =  public_key:pkix_verify_hostname(Cert, [{dns_id,"kb.example.org"}],
     					    [{match_fun, fun(_, _) -> default end}]),
     
         %% Check the callback for user-provided fqdn extraction:
         true =  public_key:pkix_verify_hostname(Cert, [{uri_id,"some://very.wrong.domain"}],
    -					    [{fqdn_fun,
    -					      fun({uri_id, "some://very.wrong.domain"}) ->
    -						      "example.com";
    -						 (_) ->
    -						      ""
    -					      end}]),
    -    true =  public_key:pkix_verify_hostname(Cert, [{uri_id,"https://example.com"}],
    -					    [{fqdn_fun, fun(_) -> default end}]),
    +                                            [{fqdn_fun,
    +                                              fun({uri_id, "some://very.wrong.domain"}) ->
    +                                                      "kb.example.org";
    +                                                 (_) ->
    +                                                      ""
    +                                              end}]),
    +    true =  public_key:pkix_verify_hostname(Cert, [{uri_id,"https://wws.example.org"}],
    +                                            [{fqdn_fun, fun(_) -> default end}]),
         false =  public_key:pkix_verify_hostname(Cert, [{uri_id,"some://very.wrong.domain"}]),
     
    -    true = public_key:pkix_verify_hostname(Cert, [{dns_id,"example.com"}]),
    -    true = public_key:pkix_verify_hostname(Cert, [{dns_id,"abb.bar.example.com"}]),
    -    false = public_key:pkix_verify_hostname(Cert, [{dns_id,"example.com"},
    -                                                   {dns_id,"abb.bar.example.com"}],
    -                                            [{fqdn_fun,fun(_)->undefined end}]),
    -    %% Test that a common name is matched fully, that is do not allow prefix matches
    -    %% with less dots (".")
    -    {ok, PrefixBin} = file:read_file(filename:join(DataDir,"prefix-dots.pem")),
    -    PrefixCert = public_key:pkix_decode_cert(element(2,hd(public_key:pem_decode(PrefixBin))), otp),
    -    true = public_key:pkix_verify_hostname(PrefixCert, [{dns_id,"..a"}]),
    -    false = public_key:pkix_verify_hostname(PrefixCert, [{dns_id,".a"}]).
    +    true = public_key:pkix_verify_hostname(Cert,
    +                                           [{dns_id,"foobar.example.org"}],
    +                                           [{match_fun,
    +                                             public_key:pkix_verify_hostname_match_fun(https)}]).
     
     %%--------------------------------------------------------------------
     %% To generate the PEM file contents:
    
0769050c69d7

public_key: Adhere to RFC 9525

https://github.com/erlang/otpIngela Anderton AndinMay 5, 2026via github-commit-search
2 files changed · +31 77
  • lib/public_key/src/public_key.erl+2 24 modified
    @@ -1274,26 +1274,8 @@ pkix_verify_hostname(Cert = #'OTPCertificate'{tbsCertificate = TbsCert}, Referen
         %% PresentedIDs example: [{dNSName,"ewstest.ericsson.com"}, {dNSName,"www.ericsson.com"}]}
         case PresentedIDs of
     	[] ->
    -	    %% Fallback to CN-ids [rfc6125, ch6]
    -	    case TbsCert#'OTPTBSCertificate'.subject of
    -		{rdnSequence,RDNseq} ->
    -		    PresentedCNs =
    -			[{cn, to_string(V)}
    -			 || ATVs <- RDNseq, % RDNseq is list-of-lists
    -			    #'AttributeTypeAndValue'{type = ?'id-at-commonName',
    -						     value = {_T,V}} <- ATVs
    -						% _T = kind of string (teletexString etc)
    -			],
    -		    %% Example of PresentedCNs:  [{cn,"www.ericsson.se"}]
    -		    %% match ReferenceIDs to PresentedCNs
    -		    verify_hostname_match_loop(verify_hostname_fqnds(ReferenceIDs, FqdnFun),
    -					       PresentedCNs,
    -					       MatchFun, FailCB, Cert);
    -		
    -		_ ->
    -		    false
    -	    end;
    -	_ ->
    +          false;
    +        _ ->
     	    %% match ReferenceIDs to PresentedIDs
     	    case verify_hostname_match_loop(ReferenceIDs, PresentedIDs,
     					    MatchFun, FailCB, Cert) of
    @@ -1997,10 +1979,6 @@ verify_hostname_fqnds(L, FqdnFun) ->
     verify_hostname_match_default(Ref, Pres) ->
         verify_hostname_match_default0(to_lower_ascii(Ref), to_lower_ascii(Pres)).
     
    -verify_hostname_match_default0(FQDN=[_|_], {cn,FQDN}) -> 
    -    not lists:member($*, FQDN);
    -verify_hostname_match_default0(FQDN=[_|_], {cn,Name=[_|_]}) -> 
    -    verify_hostname_match_wildcard(FQDN, Name);
     verify_hostname_match_default0({dns_id,R}, {dNSName,P}) ->
         R==P;
     verify_hostname_match_default0({uri_id,R}, {uniformResourceIdentifier,P}) ->
    
  • lib/public_key/test/public_key_SUITE.erl+29 53 modified
    @@ -1114,25 +1114,14 @@ pkix_verify_hostname_cn(Config) ->
         DataDir = proplists:get_value(data_dir, Config),
         {ok,Bin} = file:read_file(filename:join(DataDir,"pkix_verify_hostname_cn.pem")),
         Cert = public_key:pkix_decode_cert(element(2,hd(public_key:pem_decode(Bin))), otp),
    -
    -    %% Check that 1) only CNs are checked,
    -    %%            2) an empty label does not match a wildcard and
    -    %%            3) a wildcard does not match more than one label
    +    %% Fallback hostname check against CommonName is no longer allowed
         false = public_key:pkix_verify_hostname(Cert, [{dns_id,"erlang.org"},
     						   {dns_id,"foo.EXAMPLE.com"},
     						   {dns_id,"b.a.foo.EXAMPLE.com"}]),
    -
    -    %% Check that a hostname is extracted from a https-uri and used for checking:
    -    true =  public_key:pkix_verify_hostname(Cert, [{uri_id,"HTTPS://EXAMPLE.com"}]),
    -
    -    %% Check wildcard matching one label:
    -    true =  public_key:pkix_verify_hostname(Cert, [{dns_id,"a.foo.EXAMPLE.com"}]),
    -
    -    %% Check wildcard with surrounding chars matches one label:
    -    true =  public_key:pkix_verify_hostname(Cert, [{dns_id,"accb.bar.EXAMPLE.com"}]),
    -
    -    %% Check that a wildcard with surrounding chars matches an empty string:
    -    true =  public_key:pkix_verify_hostname(Cert, [{uri_id,"https://ab.bar.EXAMPLE.com"}]).
    +    false =  public_key:pkix_verify_hostname(Cert, [{uri_id,"HTTPS://EXAMPLE.com"}]),
    +    false =  public_key:pkix_verify_hostname(Cert, [{dns_id,"a.foo.EXAMPLE.com"}]),
    +    false =  public_key:pkix_verify_hostname(Cert, [{dns_id,"accb.bar.EXAMPLE.com"}]),
    +    false =  public_key:pkix_verify_hostname(Cert, [{uri_id,"https://ab.bar.EXAMPLE.com"}]).
     
     %%--------------------------------------------------------------------
     %% To generate the PEM file contents:
    @@ -1182,63 +1171,50 @@ pkix_verify_hostname_subjAltName(Config) ->
         ok.
     
     %%--------------------------------------------------------------------
    -%% Uses the pem-file for pkix_verify_hostname_cn
    -%% Subject: C=SE, CN=example.com, CN=*.foo.example.com, CN=a*b.bar.example.com, O=erlang.org
    +%% Uses the pem-file for pkix_verify_hostname_subjAltName.pem
    +%% Subject: Subject Alt Names: 
    +%%              [{dNSName,"kb.example.org"},
    +%%              {uniformResourceIdentifier,"http://www.example.org"},
    +%%              {uniformResourceIdentifier,"https://wws.example.org"}]}]
     pkix_verify_hostname_options(Config) ->
         DataDir = proplists:get_value(data_dir, Config),
    -    {ok,Bin} = file:read_file(filename:join(DataDir,"pkix_verify_hostname_cn.pem")),
    +    {ok,Bin} = file:read_file(filename:join(DataDir,"pkix_verify_hostname_subjAltName.pem")),
         Cert = public_key:pkix_decode_cert(element(2,hd(public_key:pem_decode(Bin))), otp),
         
         %% Check that the fail_callback is called and is presented the correct certificate:
    -    true = public_key:pkix_verify_hostname(Cert, [{dns_id,"erlang.org"}],
    +    true = public_key:pkix_verify_hostname(Cert, [{dns_id,"kb.example.org"}],
     					   [{fail_callback,
     					     fun(#'OTPCertificate'{}=C) when C==Cert -> 
     						     true; % To test the return value matters
     						(#'OTPCertificate'{}=C) -> 
     						     ct:log("~p:~p: Wrong cert:~n~p~nExpect~n~p",
     							    [?MODULE, ?LINE, C, Cert]),
    -						     ct:fail("Wrong cert, see log");
    -						(C) -> 
    +                                                     ct:fail("Wrong cert, see log");
    +						(C) ->
     						     ct:log("~p:~p: Bad cert: ~p",[?MODULE,?LINE,C]),
    -						     ct:fail("Bad cert, see log")
    +                                                     ct:fail("Bad cert, see log")
     					     end}]),
    -    
    -    %% Check the callback for user-provided match functions:
    -    true =  public_key:pkix_verify_hostname(Cert, [{dns_id,"very.wrong.domain"}],
    -					    [{match_fun,
    -					      fun("very.wrong.domain", {cn,"example.com"}) ->
    -						      true;
    -						 (_, _) ->
    -						      false
    -					      end}]),
    -    false = public_key:pkix_verify_hostname(Cert, [{dns_id,"not.example.com"}],
    +    false = public_key:pkix_verify_hostname(Cert, [{dns_id,"not.example.org"}],
     					    [{match_fun, fun(_, _) -> default end}]),
    -    true =  public_key:pkix_verify_hostname(Cert, [{dns_id,"example.com"}],
    +    true =  public_key:pkix_verify_hostname(Cert, [{dns_id,"kb.example.org"}],
     					    [{match_fun, fun(_, _) -> default end}]),
     
         %% Check the callback for user-provided fqdn extraction:
         true =  public_key:pkix_verify_hostname(Cert, [{uri_id,"some://very.wrong.domain"}],
    -					    [{fqdn_fun,
    -					      fun({uri_id, "some://very.wrong.domain"}) ->
    -						      "example.com";
    -						 (_) ->
    -						      ""
    -					      end}]),
    -    true =  public_key:pkix_verify_hostname(Cert, [{uri_id,"https://example.com"}],
    -					    [{fqdn_fun, fun(_) -> default end}]),
    +                                            [{fqdn_fun,
    +                                              fun({uri_id, "some://very.wrong.domain"}) ->
    +                                                      "kb.example.org";
    +                                                 (_) ->
    +                                                      ""
    +                                              end}]),
    +    true =  public_key:pkix_verify_hostname(Cert, [{uri_id,"https://wws.example.org"}],
    +                                            [{fqdn_fun, fun(_) -> default end}]),
         false =  public_key:pkix_verify_hostname(Cert, [{uri_id,"some://very.wrong.domain"}]),
     
    -    true = public_key:pkix_verify_hostname(Cert, [{dns_id,"example.com"}]),
    -    true = public_key:pkix_verify_hostname(Cert, [{dns_id,"abb.bar.example.com"}]),
    -    false = public_key:pkix_verify_hostname(Cert, [{dns_id,"example.com"},
    -                                                   {dns_id,"abb.bar.example.com"}],
    -                                            [{fqdn_fun,fun(_)->undefined end}]),
    -    %% Test that a common name is matched fully, that is do not allow prefix matches
    -    %% with less dots (".")
    -    {ok, PrefixBin} = file:read_file(filename:join(DataDir,"prefix-dots.pem")),
    -    PrefixCert = public_key:pkix_decode_cert(element(2,hd(public_key:pem_decode(PrefixBin))), otp),
    -    true = public_key:pkix_verify_hostname(PrefixCert, [{dns_id,"..a"}]),
    -    false = public_key:pkix_verify_hostname(PrefixCert, [{dns_id,".a"}]).
    +    true = public_key:pkix_verify_hostname(Cert,
    +                                           [{dns_id,"foobar.example.org"}],
    +                                           [{match_fun,
    +                                             public_key:pkix_verify_hostname_match_fun(https)}]).
     
     %%--------------------------------------------------------------------
     %% To generate the PEM file contents:
    

Vulnerability mechanics

Root cause

"Missing enforcement of DNS nameConstraints on the subject distinguished name combined with an unchecked CN fallback in hostname verification allows a leaf certificate with no SAN to bypass nameConstraints entirely."

Attack vector

A subordinate CA whose DNS nameConstraints are restricted (e.g. `permitted;DNS:allowed.example.com`) can issue a leaf certificate that has no subjectAltName extension but whose subject commonName is set to an out-of-scope hostname (e.g. `victim.example.com`). During path validation, `pubkey_cert:validate_names/6` only checks SAN DNS entries against nameConstraints; a leaf with no SAN trivially satisfies any `permitted;DNS:...` constraint regardless of its subject commonName. Then `public_key:pkix_verify_hostname/3` falls back to the subject commonName when no SAN is present, and the strict `pkix_verify_hostname_match_fun(https)` matcher does not suppress this fallback. The bypass is reachable from stock `ssl:connect` with `verify_peer`, a trusted CA, SNI, and the canonical strict https hostname matcher.

Affected code

The vulnerability spans two functions. In `lib/public_key/src/pubkey_cert.erl`, `validate_names/6` checks SAN DNS entries against nameConstraints but does not enforce constraints on the subject distinguished name when no SAN is present. In `lib/public_key/src/public_key.erl`, `pkix_verify_hostname/3` falls back to the subject commonName when no subjectAltName is present, extracting `id-at-commonName` attributes as presented IDs and matching them against the reference hostname [patch_id=2713972][patch_id=2713982].

What the fix does

The patches apply two complementary fixes. First, in `pubkey_cert.erl`, the `validate_names/6` function is refactored to check the subject distinguished name and the subjectAltName separately, returning `distinguished_name_not_permitted` when the DN violates nameConstraints and `name_not_permitted` when the SAN does [patch_id=2713983][patch_id=2713982][patch_id=2713981]. Second, in `public_key.erl`, the CN fallback in `pkix_verify_hostname/3` is removed entirely — when no subjectAltName is present the function now returns `false` instead of extracting and matching commonName attributes [patch_id=2713972][patch_id=2713973][patch_id=2713974]. The test suite is updated to reflect that CN-only certificates no longer match any reference ID, and the `bad_cert_reason()` type is extended with the new `distinguished_name_not_permitted` atom.

Preconditions

  • configThe attacker must control or compromise a subordinate CA whose certificate includes DNS nameConstraints (e.g. permitted;DNS:allowed.example.com)
  • inputThe attacker must be able to issue a leaf certificate with no subjectAltName extension and a subject commonName set to the target hostname
  • configThe client must use ssl:connect with verify_peer, trust the constrained CA, and use the default strict https hostname matcher

Generated on May 27, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.