Am I affected?
Description
Improper Validation of Certificate with Host Mismatch vulnerability in deadtrickster ssl_verify_fun allows an adversary in the middle to impersonate a TLS peer via a certificate that carries a partial-label wildcard. try_match_wildcard/4 in src/ssl_verify_hostname.erl anchors only the text before and after the * of an identifier such as f*.example.net, so the wildcard also consumes dots and the certificate is accepted for f.evil.example.net. RFC 6125 confines a wildcard to the left-most label. The same matcher lets *.168.1.1 match the IP literal 192.168.1.1. Whole-label wildcards are matched correctly.
Erlang/OTP 20.0 and later check the hostname in ssl before a user verify_fun sees valid_peer, so a client that keeps server_name_indication set rejects the mismatch regardless. Exposed are clients that disable SNI, servers that verify client certificate hostnames with this library, and direct callers of verify_cert_hostname/2.
This issue affects ssl_verify_fun: from 1.0.1 before 1.1.8.
Technical analysis
1. Identifier parsing. ssl_verify_hostname:try_match_hostname/2 strips trailing dots, tries an exact case-insensitive match, and otherwise calls parse_and_validate_wildcard_identifier/2. That function accepts an identifier with one * in the left-most label and at least two labels after it, and classifies it as SingleCharW when the label is exactly *.
2. Whole-label wildcard, correct. For *.example.net the matcher compares the text after the * with the hostname from its first dot onward, so a.b.example.net fails because .b.example.net differs from .example.net.
3. Partial-label wildcard, incorrect. For f*.example.net the SingleCharW branch is skipped. try_match_wildcard/4 then checks only that the hostname ends with .example.net and starts with f. Nothing checks the span consumed by the *, so f.evil.example.net and f.a.b.example.net pass. RFC 6125 section 6.4.3 limits a wildcard to the left-most label. The wildcard_not_in_a_label/2 guard only rejects internationalized xn-- labels and does not help.
4. IP literals. The reference identifier is treated as a DNS name whatever its shape, so verify_cert_hostname/2 with hostname 192.168.1.1 matches a dNSName of *.168.1.1 through the whole-label branch. RFC 6125 does not apply wildcard matching to IP addresses, and OTP's ssl converts an IP-literal string to an {ip, ...} reference that only matches iPAddress entries.
5. Interaction with Erlang/OTP. When ssl_verify_hostname:verify_fun/3 is used as verify_fun, OTP 20.0 and later call ssl_certificate:validate/4 for valid_peer before the user fun. In the client role with server_name_indication set to a hostname, or with a peer IP from a socket upgrade, OTP runs public_key:pkix_verify_hostname/3 and hands the user fun {bad_cert, hostname_check_failed} instead of valid_peer, which ssl_verify_fun turns into {fail, ...}. OTP's default match function allows no wildcards, and its https match function (public_key:pkix_verify_hostname_match_fun/1) splits both names on dots and matches the wildcard within the first label only. The ssl_verify_fun result therefore decides the outcome only with {server_name_indication, disable}, in the server role, on direct calls to verify_cert_hostname/2, or on OTP older than 20.0.
Proof of concept
- Issue a leaf certificate whose only
subjectAltNameentry isdNSNamef*.example.net, for example withpublic_key:pkix_test_data/1, and decode it withpublic_key:pkix_decode_cert(Der, otp). - Call
ssl_verify_hostname:verify_cert_hostname(Cert, "foo.example.net"). It returns{valid, "foo.example.net"}, as expected. - Call
ssl_verify_hostname:verify_cert_hostname(Cert, "f.evil.example.net"). It returns{valid, "f.evil.example.net"}although the hostname spans two labels where the wildcard sits. - Call
ssl_verify_hostname:verify_cert_hostname(Cert, "a.b.example.net")as a control. It returns{fail, unable_to_match_altnames}. - For the IP-literal variant, issue a leaf with
dNSName*.168.1.1and callverify_cert_hostname(Cert, "192.168.1.1"). It returns{valid, "192.168.1.1"}. - To observe the full TLS path, serve the first certificate from
ssl:listen/2and connect with{verify, verify_peer}, the CA incacerts,{verify_fun, {fun ssl_verify_hostname:verify_fun/3, [{check_hostname, "f.evil.example.net"}]}}and{server_name_indication, disable}. The handshake succeeds. With{server_name_indication, "f.evil.example.net"}instead, OTP 20.0 or later rejects the certificate withhostname_check_failedbefore ssl_verify_fun runs.
Weaknesses & attack patterns
Weakness
CWE-297
·
Improper Validation of Certificate with Host Mismatch
in catalog →
MITRE ↗
Attack patterns
CAPEC-94
·
Adversary in the Middle (AiTM)
MITRE ↗
A holder of a certificate for f*.example.net from a CA the client trusts can impersonate any host named f.<label>.example.net to a client that relies on ssl_verify_fun alone for hostname verification, and read or alter the traffic of that connection.
Affected — Hex / ssl_verify_fun Hex.pm ↗ Repository ↗
modules · source files · routines
Affected — GitHub / deadtrickster/ssl_verify_fun.erl Repository ↗
modules · source files · routines
Workarounds
On Erlang/OTP 21 or later, keep server_name_indication set to the target hostname and pass {customize_hostname_check, [{match_fun, public_key:pkix_verify_hostname_match_fun(https)}]} in the ssl options. The hostname check in ssl then rejects the mismatch before ssl_verify_fun runs, and legitimate wildcard certificates continue to match. hackney and tls_certificate_check already set both options.
Configurations
ssl_verify_fun decides the outcome only where it is the sole hostname check. Erlang/OTP 20.0 and later verify the hostname in ssl before the user verify_fun is called with valid_peer, as long as server_name_indication is a hostname or the connection was upgraded from a socket. A deployment is exposed when it sets {server_name_indication, disable}, when a TLS server uses ssl_verify_hostname:verify_fun/3 to verify client certificates, when it calls ssl_verify_hostname:verify_cert_hostname/2 directly, or when it runs Erlang/OTP older than 20.0. The attacker also needs a certificate with a partial-label wildcard from a CA the client trusts, which the CA/Browser Forum Baseline Requirements do not allow, so the realistic exposure is private PKI.
References
Credits
CVSS breakdown
CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N