zeek/src/file_analysis/analyzer/x509/functions.bif
Johanna Amann 833168090a 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.
2021-06-29 15:00:48 +01:00

1096 lines
34 KiB
C++

%%{
#include <openssl/x509.h>
#include <openssl/asn1.h>
#include <openssl/x509_vfy.h>
#include <openssl/ocsp.h>
#include <openssl/pem.h>
#include <openssl/err.h>
#include "zeek/file_analysis/analyzer/x509/X509.h"
#include "zeek/net_util.h"
#include "zeek/file_analysis/analyzer/x509/types.bif.h"
// construct an error record
static zeek::RecordValPtr x509_result_record(uint64_t num, const char* reason, zeek::ValPtr chainVector = nullptr)
{
auto rrecord = zeek::make_intrusive<zeek::RecordVal>(zeek::BifType::Record::X509::Result);
rrecord->Assign(0, num);
rrecord->Assign(1, reason);
if ( chainVector )
rrecord->Assign(2, std::move(chainVector));
return rrecord;
}
// get all cretificates starting at the second one (assuming the first one is the host certificate)
STACK_OF(X509)* x509_get_untrusted_stack(zeek::VectorVal* certs_vec)
{
STACK_OF(X509)* untrusted_certs = sk_X509_new_null();
if ( ! untrusted_certs )
{
zeek::emit_builtin_error(zeek::util::fmt("Untrusted certificate stack initialization error: %s",
ERR_error_string(ERR_get_error(),NULL)));
return 0;
}
for ( int i = 1; i < (int) certs_vec->Size(); ++i ) // start at 1 - 0 is host cert
{
auto sv = certs_vec->ValAt(i);
if ( ! sv )
continue;
// Fixme: check type
X509* x = ((zeek::file_analysis::detail::X509Val*) sv.get())->GetCertificate();
if ( ! x )
{
sk_X509_free(untrusted_certs);
zeek::emit_builtin_error(zeek::util::fmt("No certificate in opaque in stack"));
return 0;
}
sk_X509_push(untrusted_certs, x);
}
return untrusted_certs;
}
// We need this function to be able to identify the signer certificate of an
// OCSP request out of a list of possible certificates.
X509* x509_get_ocsp_signer(const STACK_OF(X509)* certs,
OCSP_BASICRESP* basic_resp)
{
const ASN1_OCTET_STRING* key = nullptr;
const X509_NAME* name = nullptr;
#if ( OPENSSL_VERSION_NUMBER < 0x10100000L ) || defined(LIBRESSL_VERSION_NUMBER)
OCSP_RESPID* resp_id = basic_resp->tbsResponseData->responderId;
if ( resp_id->type == V_OCSP_RESPID_NAME )
name = resp_id->value.byName;
else if ( resp_id->type == V_OCSP_RESPID_KEY )
key = resp_id->value.byKey;
else
return 0;
#else
if ( ! OCSP_resp_get0_id(basic_resp, &key, &name) )
return 0;
#endif
if ( name )
return X509_find_by_subject(const_cast<STACK_OF(X509)*>(certs),
const_cast<X509_NAME*>(name));
// Just like OpenSSL, we just support SHA-1 lookups and bail out otherwhise.
if ( key->length != SHA_DIGEST_LENGTH )
return 0;
unsigned char* key_hash = key->data;
for ( int i = 0; i < sk_X509_num(certs); ++i )
{
unsigned char digest[SHA_DIGEST_LENGTH];
X509* cert = sk_X509_value(certs, i);
if ( ! X509_pubkey_digest(cert, EVP_sha1(), digest, NULL) )
// digest failed for this certificate, try with next
continue;
if ( memcmp(digest, key_hash, SHA_DIGEST_LENGTH) == 0 )
// keys match, return certificate
return cert;
}
return 0;
}
// Convert hash algorithm registry numbers to the OpenSSL EVP_MD.
// Mapping at https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-18
const EVP_MD* hash_to_evp(int hash)
{
switch ( hash )
{
case 1:
return EVP_md5();
break;
case 2:
return EVP_sha1();
break;
case 3:
return EVP_sha224();
break;
case 4:
return EVP_sha256();
break;
case 5:
return EVP_sha384();
break;
case 6:
return EVP_sha512();
break;
default:
return nullptr;
}
}
// 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.
##
## cert: The X509 certificate opaque handle.
##
## Returns: A X509::Certificate structure.
##
## .. zeek:see:: x509_certificate x509_extension x509_ext_basic_constraints
## x509_ext_subject_alternative_name x509_verify
## x509_get_certificate_string
function x509_parse%(cert: opaque of x509%): X509::Certificate
%{
assert(cert);
auto* h = (zeek::file_analysis::detail::X509Val*) cert;
return zeek::file_analysis::detail::X509::ParseCertificate(h);
%}
## Constructs an opaque of X509 from a der-formatted string.
##
## Note: this function is mostly meant for testing purposes
##
## .. zeek:see:: x509_certificate x509_extension x509_ext_basic_constraints
## x509_ext_subject_alternative_name x509_verify
## x509_get_certificate_string x509_parse
function x509_from_der%(der: string%): opaque of x509
%{
const u_char* data = der->Bytes();
return zeek::make_intrusive<zeek::file_analysis::detail::X509Val>(d2i_X509(nullptr, &data, der->Len()));
%}
## Returns the string form of a certificate.
##
## cert: The X509 certificate opaque handle.
##
## pem: A boolean that specifies if the certificate is returned
## in pem-form (true), or as the raw ASN1 encoded binary
## (false).
##
## Returns: X509 certificate as a string.
##
## .. zeek:see:: x509_certificate x509_extension x509_ext_basic_constraints
## x509_ext_subject_alternative_name x509_parse x509_verify
function x509_get_certificate_string%(cert: opaque of x509, pem: bool &default=F%): string
%{
assert(cert);
auto* h = (zeek::file_analysis::detail::X509Val*) cert;
BIO *bio = BIO_new(BIO_s_mem());
if ( pem )
PEM_write_bio_X509(bio, h->GetCertificate());
else
i2d_X509_bio(bio, h->GetCertificate());
auto ext_val = zeek::file_analysis::detail::X509::GetExtensionFromBIO(bio);
if ( ! ext_val )
ext_val = zeek::val_mgr->EmptyString();
return ext_val;
%}
## Verifies an OCSP reply.
##
## certs: Specifies the certificate chain to use. Server certificate first.
##
## ocsp_reply: the ocsp reply to validate.
##
## root_certs: A list of root certificates to validate the certificate chain.
##
## verify_time: Time for the validity check of the certificates.
##
## Returns: A record of type X509::Result containing the result code of the
## verify operation.
##
## .. zeek:see:: x509_certificate x509_extension x509_ext_basic_constraints
## x509_ext_subject_alternative_name x509_parse
## x509_get_certificate_string x509_verify
function x509_ocsp_verify%(certs: x509_opaque_vector, ocsp_reply: string, root_certs: table_string_of_string, verify_time: time &default=network_time()%): X509::Result
%{
zeek::RecordValPtr rval;
X509_STORE* ctx = zeek::file_analysis::detail::X509::GetRootStore(root_certs->AsTableVal());
if ( ! ctx )
return x509_result_record(-1, "Problem initializing root store");
zeek::VectorVal *certs_vec = certs->AsVectorVal();
if ( certs_vec->Size() < 1 )
{
zeek::reporter->Error("No certificates given in vector");
return x509_result_record(-1, "no certificates");
}
// host certificate
unsigned int index = 0; // to prevent overloading to 0pointer
auto sv = certs_vec->ValAt(index);
if ( ! sv )
{
zeek::emit_builtin_error("undefined value in certificate vector");
return x509_result_record(-1, "undefined value in certificate vector");
}
auto* cert_handle = (zeek::file_analysis::detail::X509Val*) sv.get();
X509* cert = cert_handle->GetCertificate();
if ( ! cert )
{
zeek::emit_builtin_error(zeek::util::fmt("No certificate in opaque"));
return x509_result_record(-1, "No certificate in opaque");
}
const unsigned char* start = ocsp_reply->Bytes();
STACK_OF(X509)* untrusted_certs = x509_get_untrusted_stack(certs_vec);
if ( ! untrusted_certs )
return x509_result_record(-1, "Problem initializing list of untrusted certificates");
// from here, always goto cleanup. Initialize all other required variables...
time_t vtime = (time_t) verify_time;
OCSP_BASICRESP *basic = 0;
OCSP_SINGLERESP *single = 0;
X509_STORE_CTX *csc = 0;
OCSP_CERTID *certid = 0;
stack_st_X509* ocsp_certs = nullptr;
int status = -1;
int out = -1;
int result = -1;
X509* issuer_certificate = 0;
X509* signer = 0;
ASN1_GENERALIZEDTIME* thisUpdate = nullptr;
ASN1_GENERALIZEDTIME* nextUpdate = nullptr;
int type = -1;
OCSP_RESPONSE *resp = d2i_OCSP_RESPONSE(NULL, &start, ocsp_reply->Len());
if ( ! resp )
{
rval = x509_result_record(-1, "Could not parse OCSP response");
goto x509_ocsp_cleanup;
}
status = OCSP_response_status(resp);
if ( status != OCSP_RESPONSE_STATUS_SUCCESSFUL )
{
rval = x509_result_record(-2, OCSP_response_status_str(status));
goto x509_ocsp_cleanup;
}
basic = OCSP_response_get1_basic(resp);
if ( ! basic )
{
rval = x509_result_record(-1, "Could not parse OCSP response");
goto x509_ocsp_cleanup;
}
// the following code took me _forever_ to get right.
// The OCSP_basic_verify command takes a list of certificates. However (which is not immediately
// visible or understandable), those are only used to find the signer certificate. They are _not_
// used for chain building during the actual verification (this would be stupid). But - if we sneakily
// inject the certificates in the certificate list of the OCSP reply, they actually are used during
// the lookup.
// Yay.
issuer_certificate = 0;
for ( int i = 0; i < sk_X509_num(untrusted_certs); i++)
{
OCSP_basic_add1_cert(basic, sk_X509_value(untrusted_certs, i));
if ( X509_NAME_cmp(X509_get_issuer_name(cert), X509_get_subject_name(sk_X509_value(untrusted_certs, i))) == 0 )
issuer_certificate = sk_X509_value(untrusted_certs, i);
}
// Because we actually want to be able to give nice error messages that show why we were
// not able to verify the OCSP response - do our own verification logic first.
#if ( OPENSSL_VERSION_NUMBER < 0x10100000L ) || defined(LIBRESSL_VERSION_NUMBER)
signer = x509_get_ocsp_signer(basic->certs, basic);
#else
signer = x509_get_ocsp_signer(OCSP_resp_get0_certs(basic), basic);
#endif
/*
Do this perhaps - OpenSSL also cannot do it, so I do not really feel bad about it.
Needs a different lookup because the root store is no stack of X509 certs
if ( ! signer )
// if we did not find it in the certificates that were sent, search in the root store
signer = x509_get_ocsp_signer(ocsp_certs, basic);
*/
if ( ! signer )
{
rval = x509_result_record(-1, "Could not find OCSP responder certificate");
goto x509_ocsp_cleanup;
}
{
auto basic_certs = OCSP_resp_get0_certs(basic);
if ( basic_certs )
ocsp_certs = sk_X509_dup(basic_certs);
assert(ocsp_certs);
}
csc = X509_STORE_CTX_new();
X509_STORE_CTX_init(csc, ctx, signer, ocsp_certs);
X509_STORE_CTX_set_time(csc, 0, (time_t) verify_time);
X509_STORE_CTX_set_purpose(csc, X509_PURPOSE_OCSP_HELPER);
result = X509_verify_cert(csc);
if ( result != 1 )
{
const char *reason = X509_verify_cert_error_string(X509_STORE_CTX_get_error(csc));
rval = x509_result_record(result, X509_verify_cert_error_string(X509_STORE_CTX_get_error(csc)));
goto x509_ocsp_cleanup;
}
// We pass OCSP_NOVERIFY to let OCSP_basic_verify skip the chain verification.
// With that, it only verifies the signature of the basic response and we are responsible
// for the chain ourselves. We have to do that since we cannot get OCSP_basic_verify to use our timestamp.
out = OCSP_basic_verify(basic, NULL, ctx, OCSP_NOVERIFY);
if ( out < 1 )
{
rval = x509_result_record(out, ERR_error_string(ERR_get_error(),NULL));
goto x509_ocsp_cleanup;
}
// ok, now we verified the OCSP response. This means that we have a valid chain tying it
// to a root that we trust and that the signature also hopefully is valid. This does not yet
// mean that the ocsp response actually matches the certificate the server sent us or that
// the OCSP response even says that the certificate is valid.
// let's start this out by checking that the response is actually for the certificate we want
// to validate and not for something completely unrelated that the server is trying to trick us
// into accepting.
if ( issuer_certificate )
certid = OCSP_cert_to_id(NULL, cert, issuer_certificate);
else
{
// issuer not in list sent by server, check store
X509_OBJECT *obj = X509_OBJECT_new();
int lookup = X509_STORE_get_by_subject(csc, X509_LU_X509, X509_get_subject_name(cert), obj);
if ( lookup <= 0)
{
rval = x509_result_record(lookup, "Could not find issuer of host certificate");
X509_OBJECT_free(obj);
goto x509_ocsp_cleanup;
}
certid = OCSP_cert_to_id(NULL, cert,X509_OBJECT_get0_X509( obj));
X509_OBJECT_free(obj);
}
if ( ! certid )
{
rval = x509_result_record(-1, "Certificate ID construction failed");
goto x509_ocsp_cleanup;
}
// for now, assume we have one reply...
single = OCSP_resp_get0(basic, 0);
if ( ! single )
{
rval = x509_result_record(-1, "Could not lookup OCSP response information");
goto x509_ocsp_cleanup;
}
if ( OCSP_id_cmp(certid, (OCSP_CERTID*)OCSP_SINGLERESP_get0_id(single)) != 0 )
return x509_result_record(-1, "OCSP reply is not for host certificate");
// next - check freshness of proof...
type = OCSP_single_get0_status(single, NULL, NULL, &thisUpdate, &nextUpdate);
if ( type == -1 )
{
rval = x509_result_record(-1, "OCSP reply failed to retrieve update times");
goto x509_ocsp_cleanup;
}
if ( ! thisUpdate )
{
rval = x509_result_record(-1, "OCSP reply missing thisUpdate field");
goto x509_ocsp_cleanup;
}
if ( ! nextUpdate )
{
rval = x509_result_record(-1, "OCSP reply missing nextUpdate field");
goto x509_ocsp_cleanup;
}
if ( ! ASN1_GENERALIZEDTIME_check(thisUpdate) )
{
rval = x509_result_record(-1, "OCSP reply contains invalid thisUpdate field");
goto x509_ocsp_cleanup;
}
if ( ! ASN1_GENERALIZEDTIME_check(nextUpdate) )
{
rval = x509_result_record(-1, "OCSP reply contains invalid nextUpdate field");
goto x509_ocsp_cleanup;
}
// now - nearly done. Check freshness and status code.
// There is a function to check the freshness of the ocsp reply in the ocsp code of OpenSSL. But - it only
// supports comparing it against the current time, not against arbitrary times. Hence it is kind of unusable
// for us...
// Well, we will do it manually.
if ( X509_cmp_time(thisUpdate, &vtime) > 0 )
rval = x509_result_record(-1, "OCSP reply specifies time in future");
else if ( X509_cmp_time(nextUpdate, &vtime) < 0 )
rval = x509_result_record(-1, "OCSP reply expired");
else if ( type != V_OCSP_CERTSTATUS_GOOD )
rval = x509_result_record(-1, OCSP_cert_status_str(type));
// if we have no error so far, we are done.
if ( !rval )
rval = x509_result_record(1, OCSP_cert_status_str(type));
x509_ocsp_cleanup:
if ( ocsp_certs )
sk_X509_free(ocsp_certs);
if ( untrusted_certs )
sk_X509_free(untrusted_certs);
if ( resp )
OCSP_RESPONSE_free(resp);
if ( basic )
OCSP_BASICRESP_free(basic);
if ( csc )
{
X509_STORE_CTX_cleanup(csc);
X509_STORE_CTX_free(csc);
}
if ( certid )
OCSP_CERTID_free(certid);
return rval;
%}
## Verifies a certificate.
##
## certs: Specifies a certificate chain that is being used to validate
## the given certificate against the root store given in *root_certs*.
## The host certificate has to be at index 0.
##
## root_certs: A list of root certificates to validate the certificate chain.
##
## verify_time: Time for the validity check of the certificates.
##
## Returns: A record of type X509::Result containing the result code of the
## verify operation. In case of success also returns the full
## certificate chain.
##
## .. zeek:see:: x509_certificate x509_extension x509_ext_basic_constraints
## x509_ext_subject_alternative_name x509_parse
## x509_get_certificate_string x509_ocsp_verify sct_verify
function x509_verify%(certs: x509_opaque_vector, root_certs: table_string_of_string, verify_time: time &default=network_time()%): X509::Result
%{
X509_STORE* ctx = zeek::file_analysis::detail::X509::GetRootStore(root_certs->AsTableVal());
if ( ! ctx )
return x509_result_record(-1, "Problem initializing root store");
zeek::VectorVal *certs_vec = certs->AsVectorVal();
if ( ! certs_vec || certs_vec->Size() < 1 )
{
zeek::reporter->Error("No certificates given in vector");
return x509_result_record(-1, "no certificates");
}
// host certificate
unsigned int index = 0; // to prevent overloading to 0pointer
auto sv = certs_vec->ValAt(index);
if ( !sv )
{
zeek::emit_builtin_error("undefined value in certificate vector");
return x509_result_record(-1, "undefined value in certificate vector");
}
auto* cert_handle = (zeek::file_analysis::detail::X509Val*) sv.get();
X509* cert = cert_handle->GetCertificate();
if ( ! cert )
{
zeek::emit_builtin_error(zeek::util::fmt("No certificate in opaque"));
return x509_result_record(-1, "No certificate in opaque");
}
STACK_OF(X509)* untrusted_certs = x509_get_untrusted_stack(certs_vec);
if ( ! untrusted_certs )
return x509_result_record(-1, "Problem initializing list of untrusted certificates");
X509_STORE_CTX *csc = X509_STORE_CTX_new();
X509_STORE_CTX_init(csc, ctx, cert, untrusted_certs);
X509_STORE_CTX_set_time(csc, 0, (time_t) verify_time);
X509_STORE_CTX_set_flags(csc, X509_V_FLAG_USE_CHECK_TIME);
int result = X509_verify_cert(csc);
zeek::VectorValPtr chainVector;
if ( result == 1 ) // we have a valid chain. try to get it...
{
STACK_OF(X509)* chain = X509_STORE_CTX_get1_chain(csc); // get1 = deep copy
if ( ! chain )
{
zeek::reporter->Error("Encountered valid chain that could not be resolved");
sk_X509_pop_free(chain, X509_free);
goto x509_verify_chainerror;
}
int num_certs = sk_X509_num(chain);
chainVector = zeek::make_intrusive<zeek::VectorVal>(zeek::id::find_type<VectorType>("x509_opaque_vector"));
for ( int i = 0; i < num_certs; i++ )
{
X509* currcert = sk_X509_value(chain, i);
if ( currcert )
// X509Val takes ownership of currcert.
chainVector->Assign(i, zeek::make_intrusive<zeek::file_analysis::detail::X509Val>(currcert));
else
{
zeek::reporter->InternalWarning("OpenSSL returned null certificate");
sk_X509_pop_free(chain, X509_free);
goto x509_verify_chainerror;
}
}
sk_X509_free(chain);
}
x509_verify_chainerror:
auto rrecord = x509_result_record(X509_STORE_CTX_get_error(csc), X509_verify_cert_error_string(X509_STORE_CTX_get_error(csc)), std::move(chainVector));
X509_STORE_CTX_cleanup(csc);
X509_STORE_CTX_free(csc);
sk_X509_free(untrusted_certs);
return rrecord;
%}
## Verifies a Signed Certificate Timestamp as used for Certificate Transparency.
## See RFC6962 for more details.
##
## cert: Certificate against which the SCT should be validated.
##
## logid: Log id of the SCT.
##
## log_key: Public key of the Log that issued the SCT proof.
##
## timestamp: Timestamp at which the proof was generated.
##
## hash_algorithm: Hash algorithm that was used for the SCT proof.
##
## issuer_key_hash: The SHA-256 hash of the certificate issuer's public key.
## This only has to be provided if the SCT was encountered in an X.509
## certificate extension; in that case, it is necessary for validation.
##
## Returns: T if the validation could be performed succesfully, F otherwhise.
##
## .. zeek:see:: ssl_extension_signed_certificate_timestamp
## x509_ocsp_ext_signed_certificate_timestamp
## x509_verify
function sct_verify%(cert: opaque of x509, logid: string, log_key: string, signature: string, timestamp: count, hash_algorithm: count, issuer_key_hash: string &default=""%): bool
%{
assert(cert);
auto* h = (zeek::file_analysis::detail::X509Val*) cert;
X509* x = ((zeek::file_analysis::detail::X509Val*) h)->GetCertificate();
assert(sizeof(timestamp) >= 8);
uint64_t timestamp_network = htonll(timestamp);
bool precert = issuer_key_hash->Len() > 0;
if ( precert && issuer_key_hash->Len() != 32)
{
zeek::reporter->Error("Invalid issuer_key_hash length");
return zeek::val_mgr->False();
}
std::string data;
data.push_back(0); // version
data.push_back(0); // signature_type -> certificate_timestamp
data.append(reinterpret_cast<const char*>(&timestamp_network), sizeof(timestamp_network)); // timestamp -> 64 bits
if ( precert )
data.append("\0\1", 2); // entry-type: precert_entry
else
data.append("\0\0", 2); // entry-type: x509_entry
if ( precert )
{
x = X509_dup(x);
assert(x);
// In OpenSSL 1.0.2+, we can get the extension by using NID_ct_precert_scts.
// In OpenSSL <= 1.0.1, this is not yet defined yet, so we have to manually
// look it up by performing a string comparison on the oid.
#ifdef NID_ct_precert_scts
int pos = X509_get_ext_by_NID(x, NID_ct_precert_scts, -1);
if ( pos < 0 )
{
zeek::reporter->Error("NID_ct_precert_scts not found");
return zeek::val_mgr->False();
}
#else
int num_ext = X509_get_ext_count(x);
int pos = -1;
for ( int k = 0; k < num_ext; ++k )
{
char oid[256];
X509_EXTENSION* ex = X509_get_ext(x, k);
ASN1_OBJECT* ext_asn = X509_EXTENSION_get_object(ex);
OBJ_obj2txt(oid, 255, ext_asn, 1);
if ( strcmp(oid, "1.3.6.1.4.1.11129.2.4.2") == 0 )
{
pos = k;
break;
}
}
#endif
X509_EXTENSION_free(X509_delete_ext(x, pos));
#ifdef NID_ct_precert_scts
assert( X509_get_ext_by_NID(x, NID_ct_precert_scts, -1) == -1 );
#endif
}
unsigned char *cert_out = nullptr;
uint32_t cert_length;
if ( precert )
{
#if ( OPENSSL_VERSION_NUMBER < 0x10002000L ) || defined(LIBRESSL_VERSION_NUMBER)
x->cert_info->enc.modified = 1;
cert_length = i2d_X509_CINF(x->cert_info, &cert_out);
#else
cert_length = i2d_re_X509_tbs(x, &cert_out);
#endif
data.append(reinterpret_cast<const char*>(issuer_key_hash->Bytes()), issuer_key_hash->Len());
}
else
cert_length = i2d_X509(x, &cert_out);
assert( cert_out );
uint32_t cert_length_network = htonl(cert_length);
assert( sizeof(cert_length_network) == 4);
data.append(reinterpret_cast<const char*>(&cert_length_network)+1, 3); // 3 bytes certificate length
data.append(reinterpret_cast<const char*>(cert_out), cert_length); // der-encoded certificate
OPENSSL_free(cert_out);
if ( precert )
X509_free(x);
data.append("\0\0", 2); // no extensions
// key is given as a DER-encoded SubjectPublicKeyInfo.
const unsigned char *key_char = log_key->Bytes();
EVP_PKEY* key = d2i_PUBKEY(nullptr, &key_char, log_key->Len());
EVP_MD_CTX *mdctx = EVP_MD_CTX_create();
assert(mdctx);
std::string errstr;
int success = 0;
const EVP_MD* hash = hash_to_evp(hash_algorithm);
if ( ! hash )
{
errstr = "Unknown hash algorithm";
goto sct_verify_err;
}
if ( ! key )
{
errstr = "Could not load log key";
goto sct_verify_err;
}
if ( ! EVP_DigestVerifyInit(mdctx, NULL, hash, NULL, key) )
{
errstr = "Could not init signature verification";
goto sct_verify_err;
}
if ( ! EVP_DigestVerifyUpdate(mdctx, data.data(), data.size()) )
{
errstr = "Could not update digest for verification";
goto sct_verify_err;
}
#ifdef NID_ct_precert_scts
success = EVP_DigestVerifyFinal(mdctx, signature->Bytes(), signature->Len());
#else
// older versions of OpenSSL use a non-const-char *sigh*
// I don't think they actually manipulate the value though.
// todo - this needs a cmake test
success = EVP_DigestVerifyFinal(mdctx, (unsigned char*) signature->Bytes(), signature->Len());
#endif
EVP_MD_CTX_destroy(mdctx);
EVP_PKEY_free(key);
return zeek::val_mgr->Bool(success);
sct_verify_err:
if (mdctx)
EVP_MD_CTX_destroy(mdctx);
if (key)
EVP_PKEY_free(key);
zeek::reporter->Error("%s", errstr.c_str());
return zeek::val_mgr->False();
%}
%%{
/**
* 0 -> subject name
* 1 -> issuer name
* 2 -> pubkey
*/
zeek::StringValPtr x509_entity_hash(zeek::file_analysis::detail::X509Val *cert_handle, unsigned int hash_alg, unsigned int type)
{
assert(cert_handle);
if ( type > 2 )
{
zeek::reporter->InternalError("Unknown type in x509_entity_hash");
return nullptr;
}
X509 *cert_x509 = cert_handle->GetCertificate();
if ( cert_x509 == nullptr )
{
zeek::emit_builtin_error("cannot get cert from opaque");
return nullptr;
}
X509_NAME *subject_name = X509_get_subject_name(cert_x509);
X509_NAME *issuer_name = X509_get_issuer_name(cert_x509);
if ( subject_name == nullptr || issuer_name == nullptr )
{
zeek::emit_builtin_error("fail to get subject/issuer name from certificate");
return nullptr;
}
const EVP_MD *dgst = hash_to_evp(hash_alg);
if ( dgst == nullptr )
{
zeek::emit_builtin_error("Unknown hash algorithm.");
return nullptr;
}
unsigned char md[EVP_MAX_MD_SIZE];
memset(md, 0, sizeof(md));
unsigned int len = 0;
int res = 0;
if ( type == 0 )
res = X509_NAME_digest(subject_name, dgst, md, &len);
else if ( type == 1 )
res = X509_NAME_digest(issuer_name, dgst, md, &len);
else if ( type == 2 )
{
unsigned char *spki = nullptr;
int pklen = i2d_X509_PUBKEY(X509_get_X509_PUBKEY(cert_x509), &spki);
if ( ! pklen )
{
zeek::emit_builtin_error("Could not get SPKI");
return nullptr;
}
res = EVP_Digest(spki, pklen, md, &len, dgst, nullptr);
OPENSSL_free(spki);
}
if ( ! res )
{
zeek::emit_builtin_error("Could not perform hash");
return nullptr;
}
assert( len <= sizeof(md) );
return zeek::make_intrusive<zeek::StringVal>(len, reinterpret_cast<const char*>(md));
}
%%}
## Get the hash of the subject's distinguished name.
##
## cert: The X509 certificate opaque handle.
##
## hash_alg: the hash algorithm to use, according to the IANA mapping at
## https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-18
##
## Returns: The hash as a string.
##
## .. zeek:see:: x509_issuer_name_hash x509_spki_hash
## x509_verify sct_verify
function x509_subject_name_hash%(cert: opaque of x509, hash_alg: count%): string
%{
auto* cert_handle = (zeek::file_analysis::detail::X509Val *) cert;
return x509_entity_hash(cert_handle, hash_alg, 0);
%}
## Get the hash of the issuer's distinguished name.
##
## cert: The X509 certificate opaque handle.
##
## hash_alg: the hash algorithm to use, according to the IANA mapping at
## https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-18
##
## Returns: The hash as a string.
##
## .. zeek:see:: x509_subject_name_hash x509_spki_hash
## x509_verify sct_verify
function x509_issuer_name_hash%(cert: opaque of x509, hash_alg: count%): string
%{
auto* cert_handle = (zeek::file_analysis::detail::X509Val *) cert;
return x509_entity_hash(cert_handle, hash_alg, 1);
%}
## Get the hash of the Subject Public Key Information of the certificate.
##
## cert: The X509 certificate opaque handle.
##
## hash_alg: the hash algorithm to use, according to the IANA mapping at
## https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-18
##
## Returns: The hash as a string.
##
## .. zeek:see:: x509_subject_name_hash x509_issuer_name_hash
## x509_verify sct_verify
function x509_spki_hash%(cert: opaque of x509, hash_alg: count%): string
%{
auto* cert_handle = (zeek::file_analysis::detail::X509Val *) cert;
return x509_entity_hash(cert_handle, hash_alg, 2);
%}
## This function can be used to set up certificate caching. It has to be passed a table[string] which
## can contain any type.
##
## After this is set up, for each certificate encountered, the X509 analyzer will check if the entry
## tbl[sha256 of certificate] is set. If this is the case, the X509 analyzer will skip all further
## processing, and instead just call the callback that is set with
## zeek:id:`x509_set_certificate_cache_hit_callback`.
##
## tbl: Table to use as the certificate cache.
##
## Returns: Always returns true.
##
## .. note:: The base scripts use this function to set up certificate caching. You should only change the
## cache table if you are sure you will not conflict with the base scripts.
##
## .. zeek:see:: x509_set_certificate_cache_hit_callback
function x509_set_certificate_cache%(tbl: string_any_table%) : bool
%{
zeek::file_analysis::detail::X509::SetCertificateCache({zeek::NewRef{}, tbl->AsTableVal()});
return zeek::val_mgr->True();
%}
## This function sets up the callback that is called when an entry is matched against the table set
## by :zeek:id:`x509_set_certificate_cache`.
##
## f: The callback that will be called when encountering a certificate in the cache table.
##
## Returns: Always returns true.
##
## .. note:: The base scripts use this function to set up certificate caching. You should only change the
## callback function if you are sure you will not conflict with the base scripts.
##
## .. zeek:see:: x509_set_certificate_cache
function x509_set_certificate_cache_hit_callback%(f: string_any_file_hook%) : bool
%{
zeek::file_analysis::detail::X509::SetCertificateCacheHitCallback({zeek::NewRef{}, f->AsFunc()});
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>("");
%}