Erlang/OTP
As of Erlang/OTP 27, The OTP ssl module is responsible for TLS peer verification.
Verifying a certificate hostname
https://www.erlang.org/docs/27/apps/ssl/ssl.html
- The default for
Verifywas changed toverify_peerin Erlang/OTP 26. - If not specified,
SNIwill default to theHostargument of connect/3,4 unless it is of typeinet:ip_address(). The hostname will also be used in the hostname verification of the peer certificate usingpublic_key:pkix_verify_hostname/2. The special valuedisableprevents the Server Name Indication extension from being sent and disables the hostname verification check. - For example, here is how to use standard hostname checking for HTTPS implemented in Public_Key:
{customize_hostname_check, [{match_fun, public_key:pkix_verify_hostname_match_fun(https)}]}
But what happens if {customize_hostname_check, HostNameCheckOpts :: list()} is not specified?
Verifying a certificate hostname gives an introduction to the certificate hostname verification process, and pkix_verify_hostname_match_fun/1 gives a hint on how the default match function works:
Currently supported https fun will allow wildcard certificate matching as specified by the HTTP standard. Note that for instance LDAP have a different set of wildcard matching rules. If you do not want to allow wildcard certificates (recommended from a security perspective) or otherwise customize the hostname match the default match function used by ssl application will be sufficient.
But as described in Hostname extraction:
Suppose you have some URI with a very special protocol-part:
myspecial://example.com". Since this a non-standard URI there will be no hostname extracted for matching CN-names in theSubject.
The only standard URI protocol accepted by verify_hostname_extract_fqdn_default/1 is https. So unless you have an https:// URI, extraction of hostname from the URI is not performed and verification of wildcard certificates fails.
The relevant code in OTP starts with ssl_handshake:validate_certificate_chain/8 and ssl_handshake:certify/9, where ServerName is crafted from ssl_handshake:server_name/3 which returns a string().
Execution then continues at ssl_handshake:path_validate/11, CB:path_validation/10, where VerifyState is composed and passed to ssl_handshake:validation_fun_and_state/4, ssl_certificate:validate/4, and then validate/4 then calls verify_hostname/4 if hostname is not disable.
validate(Cert, valid_peer, UserState0 = #{role := client, server_name := Hostname,
customize_hostname_check := Customize},
LogLevel) when Hostname =/= disable ->
case verify_hostname(Hostname, Customize, Cert, UserState0) of
{valid, UserState} ->
common_cert_validation(Cert, UserState, LogLevel);
Error ->
Error
end;If the Hostname is not an IP address nor a tuple, which is the most common case, verify_hostname/4 turns it into {dns_id, Hostname} and calls public_key:pkix_verify_hostname/3 for verification.
verify_hostname(Hostname, Customize, Cert, UserState) ->
HostId = case inet:parse_strict_address(Hostname) of
{ok, IP} -> {ip, IP};
_ -> {dns_id, Hostname}
end,
case public_key:pkix_verify_hostname(Cert, [HostId], Customize) of
true -> {valid, UserState};
false -> {fail, {bad_cert, hostname_check_failed}}
end.pkix_verify_hostname/3 then defines a few defaults that can be overridden in HostNameCheckOpts:
MatchFun = proplists:get_value(match_fun, Opts, undefined),
FailCB = proplists:get_value(fail_callback, Opts, fun(_Cert) -> false end),
FqdnFun = proplists:get_value(fqdn_fun, Opts, fun verify_hostname_extract_fqdn_default/1),verify_hostname_extract_fqdn_default/1 is used as the default FqdnFun, and the default MatchFun is undefined.
With an X.509 Subject Alternative Name extension in the certificate (an important assumption, see below), pkix_verify_hostname/3 calls verify_hostname_match_loop/5 first to match ReferenceIDs (hostname or IP address in URI) with PresentedIDs extracted from subjectAltName.
PresentedIDs =
try lists:keyfind(?'id-ce-subjectAltName',
#'Extension'.extnID,
TbsCert#'OTPTBSCertificate'.extensions)
of
#'Extension'{extnValue = ExtVals} ->
[{T,to_string(V)} || {T,V} <- ExtVals];
[..snip..]
_ ->
Try to extract DNS-IDs from URIs etc
DNS_ReferenceIDs =
[{dns_id,X} || X <- verify_hostname_fqnds(ReferenceIDs, FqdnFun)],
verify_hostname_match_loop(DNS_ReferenceIDs, PresentedIDs,
MatchFun, FailCB, Cert);
true ->
true
Here extracted FQDNs are wrapped into DNS_ReferenceIDs of type dns_id is given to the function verify_hostname_match_loop/5, which would fail in the same way, meaning that wildcard certificates are not supported by default if the certificate has an X.509 Subject Alternative Name extension.
Libraries using TLS
Mint
CA store
Since this commit (v1.6.1), Mint defaults to using Erlang certificate store (see public_key:cacerts_get/0 and friends) if available, instead of CAStore.
If you are running your application in Docker, make sure to install ca-certificates in your container. For Debian slim images, you can use
RUN apt-get update -y && \
apt-get install -y libstdc++6 openssl libncurses5 locales ca-certificates \
&& apt-get clean && rm -f /var/lib/apt/lists/*_*which also installs several other useful packages recommended by https://hexdocs.pm/phoenix/releases.html.
Verifying certificate hostname
Mint, along with Finch and Req’s default adapter Req.Steps.run_finch/1 that depends on it, sets :customize_hostname_check to its custom version by default. The most prominent clause of which is
# Wildcard domain handling for DNS ID entries in the subjectAltName X.509
# extension. Note that this is a subset of the wildcard patterns implemented
# by OTP when matching against the subject CN attribute, but this is the only
# wildcard usage defined by the CA/Browser Forum's Baseline Requirements, and
# therefore the only pattern used in commercially issued certificates.
defp match_fun({:dns_id, reference}, {:dNSName, [?*, ?. | presented]}) do
...
end
The comments were added around the time OTP-20.0 was released, but the fallback logic in pkix_verify_hostname/3 has not changed since as of Erlang/OTP 27. If an X.509 Subject Alternative Name extension is NOT present, the PresentedCNs is checked instead.
%% Fallback to CN-ids [rfc6125, ch6]
case TbsCert#'OTPTBSCertificate'.subject of
{rdnSequence,RDNseq} ->
PresentedCNs =
[{cn, to_string(V)}
|| ...Redix
Reference documentation: https://hexdocs.pm/redix/1.5.2/Redix.html#module-ssl
CA store
If the CAStore dependency is available, Redix will pick up its CA certificate store file automatically.
Otherwise, you have to configure a different CA certificate store by passing in the :cacertfile or :cacerts socket options.
Verifying certificate hostname
Without additional socket options, Redix follows the default behavior and fails to match any wildcard certificate with an X.509 Subject Alternative Name extension, which is the only pattern used in commercially issued certificates.
Therefore, some Redis servers that use wildcard certificates, notably Amazon ElastiCache, require additional socket options for successful verification (requires OTP 21.0 or later):
Redix.start_link(
host: "example.com", port: 9999, ssl: true,
socket_opts: [
customize_hostname_check: [
match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
]
]
)Default ssl_opts
In connector.ex, :verify_peer is enabled by default and since this commit the default depth has been increased to 3 to support longer certificate chains.
@default_ssl_opts [verify: :verify_peer, depth: 3]