Add ability to check if hostname is valid for a specific cert

This commit adds two new bifs, x509_check_hostname and
x509_check_cert_hostname. These bifs can be used to check if a given
hostname which can, e.g., be sent in a SNI is valid for a specific
certificate.

This PR furthermore modifies the ssl logs again, and adds information
about this to the log-file. Furthermore we now by default remove the
server certificate information from ssl.log - I doubt that this is often
looked at, it is not present in TLS 1.3, we do still have the SNI, and
if you need it you have the information in x509.log.

This also fixes a small potential problem in X509.cc assuming there
might be SAN-entries that contain null-bytes.

Baseline update will follow in another commit.
This commit is contained in:
Johanna Amann 2021-06-29 14:56:43 +01:00
parent 5479ce607a
commit 833168090a
9 changed files with 341 additions and 4 deletions

View file

@ -361,12 +361,13 @@ void X509::ParseSAN(X509_EXTENSION* ext)
continue;
}
auto len = ASN1_STRING_length(gen->d.ia5);
#if ( OPENSSL_VERSION_NUMBER < 0x10100000L ) || defined(LIBRESSL_VERSION_NUMBER)
const char* name = (const char*) ASN1_STRING_data(gen->d.ia5);
#else
const char* name = (const char*) ASN1_STRING_get0_data(gen->d.ia5);
#endif
auto bs = make_intrusive<StringVal>(name);
auto bs = make_intrusive<StringVal>(len, name);
switch ( gen->type )
{

View file

@ -134,6 +134,54 @@ const EVP_MD* hash_to_evp(int hash)
}
}
// Check a given hostname against a name given in a cert (SAN, CN) and
// return if they match.
bool check_hostname(std::string_view hostname, std::string_view certname)
{
// let's start with the easy one
if ( hostname == certname )
return true;
// ok, now there is still the chance that it is a wildcard cert.
// We go according to RFC6128 here:
// * wildcards are allowed in the leftmost label
// * wildcards are only compared against the leftmost label
// * the wildcard character may not be the only part of the label (so abc* is ok)
// * we don't accept wildcards in anything lower than the 3rd level, so *.a.top
// Certificates that use something else cannot legitimately be issued and this
// seems to match other implementations.
// first - let's see if the certname contains a wildcard character.
auto wildpos = certname.find('*');
if ( wildpos == std::string::npos )
return false;
// then let's see if certname contains at least two dots, for three levels of domains
auto firstpos = certname.find('.');
if ( firstpos == std::string::npos || certname.find('.', firstpos+1) == std::string::npos)
return false;
// let's see if the wildcard is directly before the first label separator
if ( wildpos + 1 != firstpos )
return false;
// ok, we have chances. Let's see if the hostname portions match
auto host_firstpos = hostname.find('.');
if ( host_firstpos == std::string::npos )
return false;
if ( hostname.substr(host_firstpos) != certname.substr(firstpos) )
return false;
// ok, the hostnames match and we have a wildcard. Let's see if the characters
// before the wildcard do match. If they do - yup, it is a match. If they don't,
// it is not.
if ( wildpos && hostname.substr(0, wildpos) != certname.substr(0, wildpos) )
return false;
return true;
}
%%}
## Parses a certificate into an X509::Certificate structure.
@ -924,3 +972,125 @@ function x509_set_certificate_cache_hit_callback%(f: string_any_file_hook%) : bo
return zeek::val_mgr->True();
%}
## This function checks a hostname against the name given in a certificate subject/SAN, including
## our interpretation of RFC6128 wildcard expansions. This specifically means that wildcards are
## only allowed in the leftmost label, wildcards only span one label, the wildcard has to be the
## last character before the label-separator, but additional characters are allowed before it, and
## the wildcard has to be at least at the third level (so *.a.b).
##
## hostname: Hostname to test
##
## certname: Name given in the CN/SAN of a certificate; wildcards will be expanded
##
## Returns: True if the hostname matches.
##
## .. zeek:see:: x509_check_cert_hostname
function x509_check_hostname%(hostname: string, certname: string%): bool
%{
if ( check_hostname(hostname->ToStdStringView(), certname->ToStdStringView()) )
return zeek::val_mgr->True();
return zeek::val_mgr->False();
%}
## This function checks if a hostname matches one of the hostnames given in the certificate.
##
## For our matching we adhere to RFC6128 for the labels (see :zeek:id:`x509_check_hostname`).
## Furthermore we adhere to RFC2818 and check only the names given in the SAN, if a SAN is present,
## ignoring CNs in the Subject. If no SAN is present, we will use the last CN in the subject
## for our tests.
##
## cert: The X509 certificate opaque handle.
##
## hostname: Hostname to check
##
## Returns: empty string if the hostname does not match; matched name (which can contain wildcards)
## if it did.
##
## .. zeek:see:: x509_check_hostname
function x509_check_cert_hostname%(cert_opaque: opaque of x509, hostname: string%): string
%{
auto* cert_handle = (zeek::file_analysis::detail::X509Val *) cert_opaque;
std::string_view hostview = hostname->ToStdStringView();
X509* cert = cert_handle->GetCertificate();
if ( ! cert )
{
zeek::emit_builtin_error(zeek::util::fmt("No certificate in opaque"));
return zeek::make_intrusive<zeek::StringVal>("");
}
// According to RFC5280 (4.2.1.6) and RFC2818 (3.1), if the SAN is present, the subject
// of the certificate is ignored. Let's start by looking at the SAN.
auto sanpos = X509_get_ext_by_NID(cert, NID_subject_alt_name, -1);
if ( sanpos > -1 )
{
auto* ex = X509_get_ext(cert, sanpos);
if ( ! ex )
{
zeek::emit_builtin_error(zeek::util::fmt("Could not get SAN from cert"));
return zeek::make_intrusive<zeek::StringVal>("");
}
auto *altname = reinterpret_cast<GENERAL_NAMES*>(X509V3_EXT_d2i(ex));
if ( ! altname )
{
zeek::emit_builtin_error(zeek::util::fmt("Could not get names from SAN ext"));
return zeek::make_intrusive<zeek::StringVal>("");
}
auto num_names = sk_GENERAL_NAME_num(altname);
for ( int i = 0; i < num_names; i++ )
{
auto *gen = sk_GENERAL_NAME_value(altname, i);
assert(gen);
if ( gen->type != GEN_DNS )
continue;
if ( ASN1_STRING_type(gen->d.ia5) != V_ASN1_IA5STRING )
continue;
std::size_t len = ASN1_STRING_length(gen->d.ia5);
#if ( OPENSSL_VERSION_NUMBER < 0x10100000L ) || defined(LIBRESSL_VERSION_NUMBER)
auto* name = reinterpret_cast<const char*>(ASN1_STRING_data(gen->d.ia5));
#else
auto* name = reinterpret_cast<const char*>(ASN1_STRING_get0_data(gen->d.ia5));
#endif
std::string_view nameview {name, len};
if ( check_hostname(hostview, nameview) )
return zeek::make_intrusive<zeek::StringVal>(len, name);
}
}
else
{
// ok, we have to get the last CN from the Subject. Let's do that.
auto* subject = X509_get_subject_name(cert);
if ( ! subject )
{
zeek::emit_builtin_error(zeek::util::fmt("Could not get certificate subject"));
return zeek::make_intrusive<zeek::StringVal>("");
}
int lastpos = -1;
int found_nid = -1;
while ( ( lastpos = X509_NAME_get_index_by_NID(subject, NID_commonName, lastpos) ) >= 0 )
found_nid = lastpos;
// found CN
if ( found_nid >= 0 )
{
char buf[2048];
BIO *bio = BIO_new(BIO_s_mem());
ASN1_STRING_print(bio, X509_NAME_ENTRY_get_data(X509_NAME_get_entry(subject, found_nid)));
size_t len = BIO_gets(bio, buf, sizeof(buf));
BIO_free(bio);
std::string_view cn {buf, len};
if ( check_hostname(hostview, cn) )
return zeek::make_intrusive<zeek::StringVal>(len, buf);
}
}
return zeek::make_intrusive<zeek::StringVal>("");
%}