mirror of
https://github.com/zeek/zeek.git
synced 2025-10-02 14:48:21 +00:00
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:
parent
5479ce607a
commit
833168090a
9 changed files with 341 additions and 4 deletions
|
@ -6,10 +6,10 @@
|
|||
module SSL;
|
||||
|
||||
export {
|
||||
## Set this to false to remove the server certificate subject and
|
||||
## Set this to true to includd the server certificate subject and
|
||||
## issuer from the SSL log file. This information is still available
|
||||
## in x509.log.
|
||||
const log_include_server_certificate_subject_issuer = T &redef;
|
||||
const log_include_server_certificate_subject_issuer = F &redef;
|
||||
|
||||
## Set this to true to include the client certificate subject
|
||||
## and issuer in the SSL logfile. This information is rarely present
|
||||
|
@ -47,6 +47,11 @@ export {
|
|||
## client.
|
||||
client_issuer: string &log &optional;
|
||||
|
||||
## Set to true if the hostname sent in the SNI matches the certificate.
|
||||
## Set to false if they do not match. Unset if the client did not send
|
||||
## an SNI.
|
||||
sni_matches_cert: bool &log &optional;
|
||||
|
||||
## Current number of certificates seen from either side. Used
|
||||
## to create file handles.
|
||||
server_depth: count &default=0;
|
||||
|
@ -108,7 +113,7 @@ event zeek_init() &priority=5
|
|||
if ( ! log_include_server_certificate_subject_issuer )
|
||||
{
|
||||
add ssl_filter$exclude["subject"];
|
||||
add ssl_filter$exclude["isser"];
|
||||
add ssl_filter$exclude["issuer"];
|
||||
}
|
||||
if ( ! log_include_client_certificate_subject_issuer )
|
||||
{
|
||||
|
@ -168,6 +173,14 @@ hook ssl_finishing(c: connection) &priority=20
|
|||
if ( c$ssl?$cert_chain && |c$ssl$cert_chain| > 0 &&
|
||||
c$ssl$cert_chain[0]?$x509 )
|
||||
{
|
||||
if ( c$ssl?$server_name )
|
||||
{
|
||||
if ( x509_check_cert_hostname(c$ssl$cert_chain[0]$x509$handle, c$ssl$server_name) != "" )
|
||||
c$ssl$sni_matches_cert = T;
|
||||
else
|
||||
c$ssl$sni_matches_cert = F;
|
||||
}
|
||||
|
||||
c$ssl$subject = c$ssl$cert_chain[0]$x509$certificate$subject;
|
||||
c$ssl$issuer = c$ssl$cert_chain[0]$x509$certificate$issuer;
|
||||
}
|
||||
|
|
|
@ -986,6 +986,12 @@ string StringVal::ToStdString() const
|
|||
return string((char*)bs->Bytes(), bs->Len());
|
||||
}
|
||||
|
||||
string_view StringVal::ToStdStringView() const
|
||||
{
|
||||
auto* bs = AsString();
|
||||
return string_view((char*)bs->Bytes(), bs->Len());
|
||||
}
|
||||
|
||||
StringVal* StringVal::ToUpper()
|
||||
{
|
||||
string_val->ToUpper();
|
||||
|
|
|
@ -539,6 +539,7 @@ public:
|
|||
// { return AsString()->ExpandedString(format); }
|
||||
|
||||
std::string ToStdString() const;
|
||||
std::string_view ToStdStringView() const;
|
||||
StringVal* ToUpper();
|
||||
|
||||
const String* Get() const { return string_val; }
|
||||
|
|
|
@ -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 )
|
||||
{
|
||||
|
|
|
@ -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>("");
|
||||
%}
|
||||
|
|
19
testing/btest/Baseline/bifs.x509_check_hostname/.stdout
Normal file
19
testing/btest/Baseline/bifs.x509_check_hostname/.stdout
Normal file
|
@ -0,0 +1,19 @@
|
|||
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
|
||||
hi, www.zeek.org, F
|
||||
ww.zeek.org, www.zeek.org, F
|
||||
www.zeek.org, www.zeek.org, T
|
||||
www.zeek.org, *, F
|
||||
www.zeek.org, zeek.org, F
|
||||
www.zeek.org, *.zeek.org, T
|
||||
www.zeek.org, a*.zeek.org, F
|
||||
www.zeek.org, ww*.zeek.org, T
|
||||
www.zeek.org, wa*.zeek.org, F
|
||||
www.zeek.org, ww*.leek.com, F
|
||||
www.zeek.org, *.*.com, F
|
||||
, , T
|
||||
www.zeek.org\x00testing, *.zeek.org, F
|
||||
zeek.org, zeek.org, T
|
||||
zeek.org, *.org, F
|
||||
a.b.zeek.org, *.b.zeek.org, T
|
||||
a.b.zeek.org, *.zeek.org, F
|
||||
a.b.zeek.org, *.a.zeek.org, F
|
|
@ -0,0 +1,41 @@
|
|||
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
|
||||
CN=*.google.com,O=Google Inc,L=Mountain View,ST=California,C=US, www.google.com, *.google.com
|
||||
CN=*.google.com,O=Google Inc,L=Mountain View,ST=California,C=US, www.zeek.org,
|
||||
CN=*.google.com,O=Google Inc,L=Mountain View,ST=California,C=US, hello.android.com, *.android.com
|
||||
CN=*.google.com,O=Google Inc,L=Mountain View,ST=California,C=US, g.co, g.co
|
||||
CN=*.google.com,O=Google Inc,L=Mountain View,ST=California,C=US, Google Internet Authority G2,
|
||||
CN=Google Internet Authority G2,O=Google Inc,C=US, www.google.com,
|
||||
CN=Google Internet Authority G2,O=Google Inc,C=US, www.zeek.org,
|
||||
CN=Google Internet Authority G2,O=Google Inc,C=US, hello.android.com,
|
||||
CN=Google Internet Authority G2,O=Google Inc,C=US, g.co,
|
||||
CN=Google Internet Authority G2,O=Google Inc,C=US, Google Internet Authority G2, Google Internet Authority G2
|
||||
CN=GeoTrust Global CA,O=GeoTrust Inc.,C=US, www.google.com,
|
||||
CN=GeoTrust Global CA,O=GeoTrust Inc.,C=US, www.zeek.org,
|
||||
CN=GeoTrust Global CA,O=GeoTrust Inc.,C=US, hello.android.com,
|
||||
CN=GeoTrust Global CA,O=GeoTrust Inc.,C=US, g.co,
|
||||
CN=GeoTrust Global CA,O=GeoTrust Inc.,C=US, Google Internet Authority G2,
|
||||
CN=*.google.com,O=Google Inc,L=Mountain View,ST=California,C=US, www.google.com, *.google.com
|
||||
CN=*.google.com,O=Google Inc,L=Mountain View,ST=California,C=US, www.zeek.org,
|
||||
CN=*.google.com,O=Google Inc,L=Mountain View,ST=California,C=US, hello.android.com, *.android.com
|
||||
CN=*.google.com,O=Google Inc,L=Mountain View,ST=California,C=US, g.co, g.co
|
||||
CN=*.google.com,O=Google Inc,L=Mountain View,ST=California,C=US, Google Internet Authority G2,
|
||||
CN=Google Internet Authority G2,O=Google Inc,C=US, www.google.com,
|
||||
CN=Google Internet Authority G2,O=Google Inc,C=US, www.zeek.org,
|
||||
CN=Google Internet Authority G2,O=Google Inc,C=US, hello.android.com,
|
||||
CN=Google Internet Authority G2,O=Google Inc,C=US, g.co,
|
||||
CN=Google Internet Authority G2,O=Google Inc,C=US, Google Internet Authority G2, Google Internet Authority G2
|
||||
CN=GeoTrust Global CA,O=GeoTrust Inc.,C=US, www.google.com,
|
||||
CN=GeoTrust Global CA,O=GeoTrust Inc.,C=US, www.zeek.org,
|
||||
CN=GeoTrust Global CA,O=GeoTrust Inc.,C=US, hello.android.com,
|
||||
CN=GeoTrust Global CA,O=GeoTrust Inc.,C=US, g.co,
|
||||
CN=GeoTrust Global CA,O=GeoTrust Inc.,C=US, Google Internet Authority G2,
|
||||
CN=Bro, Bro,
|
||||
CN=Bro, Broo,
|
||||
CN=Bro, www.zeek.org,
|
||||
CN=Bro, 9566.alt.helloIamADomain.example, 9566.alt.helloIamADomain.example
|
||||
CN=WIN2K8R2.awakecoding.ath.cx, WIN2K8R2.awakecoding.ath.cx, WIN2K8R2.awakecoding.ath.cx
|
||||
CN=WIN2K8R2.awakecoding.ath.cx, awakecoding.ath.cx,
|
||||
CN=WIN2K8R2.awakecoding.ath.cx, www.zeek.org,
|
||||
CN=WIN2K8R2.awakecoding.ath.cx, WIN2K8R2.awakecoding.ath.cx, WIN2K8R2.awakecoding.ath.cx
|
||||
CN=WIN2K8R2.awakecoding.ath.cx, awakecoding.ath.cx,
|
||||
CN=WIN2K8R2.awakecoding.ath.cx, www.zeek.org,
|
31
testing/btest/bifs/x509_check_hostname.zeek
Normal file
31
testing/btest/bifs/x509_check_hostname.zeek
Normal file
|
@ -0,0 +1,31 @@
|
|||
# Test the x509_check_hostname bif.
|
||||
|
||||
# @TEST-EXEC: zeek -b %INPUT
|
||||
# @TEST-EXEC: btest-diff .stdout
|
||||
|
||||
function check_it(host: string, cert: string)
|
||||
{
|
||||
print host, cert, x509_check_hostname(host, cert);
|
||||
}
|
||||
|
||||
event zeek_init()
|
||||
{
|
||||
check_it("hi", "www.zeek.org");
|
||||
check_it("ww.zeek.org", "www.zeek.org");
|
||||
check_it("www.zeek.org", "www.zeek.org");
|
||||
check_it("www.zeek.org", "*");
|
||||
check_it("www.zeek.org", "zeek.org");
|
||||
check_it("www.zeek.org", "*.zeek.org");
|
||||
check_it("www.zeek.org", "a*.zeek.org");
|
||||
check_it("www.zeek.org", "ww*.zeek.org");
|
||||
check_it("www.zeek.org", "wa*.zeek.org");
|
||||
check_it("www.zeek.org", "ww*.leek.com");
|
||||
check_it("www.zeek.org", "*.*.com");
|
||||
check_it("", "");
|
||||
check_it("www.zeek.org\x00testing", "*.zeek.org");
|
||||
check_it("zeek.org", "zeek.org");
|
||||
check_it("zeek.org", "*.org");
|
||||
check_it("a.b.zeek.org", "*.b.zeek.org");
|
||||
check_it("a.b.zeek.org", "*.zeek.org");
|
||||
check_it("a.b.zeek.org", "*.a.zeek.org");
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
# Test that certificate event caching works as expected.
|
||||
|
||||
# @TEST-EXEC: zeek -b -r $TRACES/tls/google-duplicate.trace common.zeek google-duplicate.zeek
|
||||
# @TEST-EXEC: cat $TRACES/tls/tls-fragmented-handshake.pcap.gz | gunzip | zeek -b -r - common.zeek fragmented.zeek
|
||||
# @TEST-EXEC: zeek -b -r $TRACES/rdp/rdp-to-ssl.pcap common.zeek rdp.zeek
|
||||
# @TEST-EXEC: btest-diff .stdout
|
||||
|
||||
@TEST-START-FILE common.zeek
|
||||
|
||||
@load base/protocols/ssl
|
||||
|
||||
function test_it(cert_ref: opaque of x509, name: string, subject: string)
|
||||
{
|
||||
print subject, name, x509_check_cert_hostname(cert_ref, name);
|
||||
}
|
||||
|
||||
@TEST-END-FILE
|
||||
|
||||
@TEST-START-FILE google-duplicate.zeek
|
||||
|
||||
event x509_certificate(f: fa_file, cert_ref: opaque of x509, cert: X509::Certificate)
|
||||
{
|
||||
test_it(cert_ref, "www.google.com", cert$subject);
|
||||
test_it(cert_ref, "www.zeek.org", cert$subject);
|
||||
test_it(cert_ref, "hello.android.com", cert$subject);
|
||||
test_it(cert_ref, "g.co", cert$subject);
|
||||
test_it(cert_ref, "Google Internet Authority G2", cert$subject);
|
||||
}
|
||||
|
||||
@TEST-END-FILE
|
||||
|
||||
@TEST-START-FILE fragmented.zeek
|
||||
|
||||
event x509_certificate(f: fa_file, cert_ref: opaque of x509, cert: X509::Certificate)
|
||||
{
|
||||
test_it(cert_ref, "Bro", cert$subject);
|
||||
test_it(cert_ref, "Broo", cert$subject);
|
||||
test_it(cert_ref, "www.zeek.org", cert$subject);
|
||||
test_it(cert_ref, "9566.alt.helloIamADomain.example", cert$subject);
|
||||
}
|
||||
|
||||
@TEST-END-FILE
|
||||
|
||||
@TEST-START-FILE rdp.zeek
|
||||
|
||||
@load base/protocols/rdp
|
||||
|
||||
event x509_certificate(f: fa_file, cert_ref: opaque of x509, cert: X509::Certificate)
|
||||
{
|
||||
test_it(cert_ref, "WIN2K8R2.awakecoding.ath.cx", cert$subject);
|
||||
test_it(cert_ref, "awakecoding.ath.cx", cert$subject);
|
||||
test_it(cert_ref, "www.zeek.org", cert$subject);
|
||||
}
|
||||
|
||||
@TEST-END-FILE
|
Loading…
Add table
Add a link
Reference in a new issue