%%{ #include #include #include #include #include #include #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::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(certs), const_cast(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(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::id::find_type("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(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(×tamp_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(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(&cert_length_network)+1, 3); // 3 bytes certificate length data.append(reinterpret_cast(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(len, reinterpret_cast(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(""); } // 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(""); } auto *altname = reinterpret_cast(X509V3_EXT_d2i(ex)); if ( ! altname ) { zeek::emit_builtin_error(zeek::util::fmt("Could not get names from SAN ext")); return zeek::make_intrusive(""); } 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(ASN1_STRING_data(gen->d.ia5)); #else auto* name = reinterpret_cast(ASN1_STRING_get0_data(gen->d.ia5)); #endif std::string_view nameview {name, len}; if ( check_hostname(hostview, nameview) ) { auto retval = zeek::make_intrusive(len, name); GENERAL_NAMES_free(altname); return retval; } } GENERAL_NAMES_free(altname); } 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(""); } 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()); if ( ! bio ) { zeek::emit_builtin_error(zeek::util::fmt("Could create bio")); return zeek::make_intrusive(""); } 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(len, buf); } } return zeek::make_intrusive(""); %}