diff --git a/scripts/policy/protocols/ssl/validate-certs.bro b/scripts/policy/protocols/ssl/validate-certs.bro index 97072e4cab..5ab2017cc6 100644 --- a/scripts/policy/protocols/ssl/validate-certs.bro +++ b/scripts/policy/protocols/ssl/validate-certs.bro @@ -19,12 +19,17 @@ export { redef record Info += { ## Result of certificate validation for this connection. validation_status: string &log &optional; + ## Result of certificate validation for this connection, given + ## as OpenSSL validation code. + validation_code: count &optional; + ## Ordered chain of validated certificate, if validation succeeded. + valid_chain: vector of opaque of x509 &optional; }; - ## MD5 hash values for recently validated chains along with the + ## Result values for recently validated chains along with the ## validation status are kept in this table to avoid constant ## validation every time the same certificate chain is seen. - global recently_validated_certs: table[string] of string = table() + global recently_validated_certs: table[string] of X509::Result = table() &read_expire=5mins &redef; ## Use intermediate CA certificate caching when trying to validate @@ -39,6 +44,11 @@ export { ## that you encounter. Only disable if you want to find misconfigured servers. global ssl_cache_intermediate_ca: bool = T &redef; + ## Store the valid chain in c$ssl$valid_chain if validation succeeds. + ## This has a potentially high memory impact, depending on the local environment + ## and is thus disabled by default. + global ssl_store_valid_chain: bool = F &redef; + ## Event from a worker to the manager that it has encountered a new ## valid intermediate. global intermediate_add: event(key: string, value: vector of opaque of x509); @@ -83,7 +93,7 @@ event SSL::new_intermediate(key: string, value: vector of opaque of x509) } @endif -function cache_validate(chain: vector of opaque of x509): string +function cache_validate(chain: vector of opaque of x509): X509::Result { local chain_hash: vector of string = vector(); @@ -97,7 +107,10 @@ function cache_validate(chain: vector of opaque of x509): string return recently_validated_certs[chain_id]; local result = x509_verify(chain, root_certs); - recently_validated_certs[chain_id] = result$result_string; + if ( ! ssl_store_valid_chain && result?$chain_certs ) + recently_validated_certs[chain_id] = X509::Result($result=result$result, $result_string=result$result_string); + else + recently_validated_certs[chain_id] = result; # if we have a working chain where we did not store the intermediate certs # in our cache yet - do so @@ -120,7 +133,7 @@ function cache_validate(chain: vector of opaque of x509): string } } - return result$result_string; + return result; } event ssl_established(c: connection) &priority=3 @@ -133,7 +146,7 @@ event ssl_established(c: connection) &priority=3 local intermediate_chain: vector of opaque of x509 = vector(); local issuer = c$ssl$cert_chain[0]$x509$certificate$issuer; local hash = c$ssl$cert_chain[0]$sha1; - local result: string; + local result: X509::Result; # Look if we already have a working chain for the issuer of this cert. # If yes, try this chain first instead of using the chain supplied from @@ -145,9 +158,12 @@ event ssl_established(c: connection) &priority=3 intermediate_chain[i+1] = intermediate_cache[issuer][i]; result = cache_validate(intermediate_chain); - if ( result == "ok" ) + if ( result$result_string == "ok" ) { - c$ssl$validation_status = result; + c$ssl$validation_status = result$result_string; + c$ssl$validation_code = result$result; + if ( result?$chain_certs ) + c$ssl$valid_chain = result$chain_certs; return; } } @@ -163,9 +179,12 @@ event ssl_established(c: connection) &priority=3 } result = cache_validate(chain); - c$ssl$validation_status = result; + c$ssl$validation_status = result$result_string; + c$ssl$validation_code = result$result; + if ( result?$chain_certs ) + c$ssl$valid_chain = result$chain_certs; - if ( result != "ok" ) + if ( result$result_string != "ok" ) { local message = fmt("SSL certificate validation failed with (%s)", c$ssl$validation_status); NOTICE([$note=Invalid_Server_Cert, $msg=message, diff --git a/src/file_analysis/analyzer/x509/functions.bif b/src/file_analysis/analyzer/x509/functions.bif index 161a009515..83e73e5d46 100644 --- a/src/file_analysis/analyzer/x509/functions.bif +++ b/src/file_analysis/analyzer/x509/functions.bif @@ -140,6 +140,33 @@ X509* x509_get_ocsp_signer(STACK_OF(X509) *certs, OCSP_RESPID *rid) return 0; } +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; + } + } + %%} ## Parses a certificate into an X509::Certificate structure. @@ -544,7 +571,7 @@ x509_verify_chainerror: return rrecord; %} -function sct_verify%(cert: opaque of x509, logid: string, log_key: string, signature: string, timestamp: count, hash_algorithm: count%): bool +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); file_analysis::X509Val* h = (file_analysis::X509Val*) cert; @@ -553,21 +580,73 @@ function sct_verify%(cert: opaque of x509, logid: string, log_key: string, signa assert(sizeof(timestamp) >= 8); uint64_t timestamp_network = htonll(timestamp); + bool precert = issuer_key_hash->Len() > 0; + if ( precert && issuer_key_hash->Len() != 32) + { + reporter->Error("Invalid issuer_key_hash length"); + return new Val(0, TYPE_BOOL); + } + 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 - data.append("\0\0", 2); // entry-type: x509_entry + 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); + #ifdef NID_ct_precert_scts + int pos = X509_get_ext_by_NID(x, NID_ct_precert_scts, -1); + if ( pos < 0 ) + { + reporter->Error("NID_ct_precert_scts not found"); + return new Val(0, TYPE_BOOL); + } + #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)); + assert( X509_get_ext_by_NID(x, NID_ct_precert_scts, -1) == -1 ); + } unsigned char *cert_out = nullptr; - uint32 cert_length = i2d_X509(x, &cert_out); + uint32 cert_length; + if ( precert ) + { + // we also could use i2d_re_X509_tbs, for OpenSSL >= 1.0.2 + x->cert_info->enc.modified = 1; + cert_length = i2d_X509_CINF(x->cert_info, &cert_out); + data.append(reinterpret_cast(issuer_key_hash->Bytes()), issuer_key_hash->Len()); + } + else + cert_length = i2d_X509(x, &cert_out); assert( cert_out ); uint32 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 + 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. @@ -580,31 +659,11 @@ function sct_verify%(cert: opaque of x509, logid: string, log_key: string, signa string errstr; int success = 0; - const EVP_MD* hash; - // numbers from http://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-18 - switch ( hash_algorithm ) + const EVP_MD* hash = hash_to_evp(hash_algorithm); + if ( ! hash ) { - case 1: - hash = EVP_md5(); - break; - case 2: - hash = EVP_sha1(); - break; - case 3: - hash = EVP_sha224(); - break; - case 4: - hash = EVP_sha256(); - break; - case 5: - hash = EVP_sha384(); - break; - case 6: - hash = EVP_sha512(); - break; - default: - errstr = "Unknown hash algorithm"; - goto sct_verify_err; + errstr = "Unknown hash algorithm"; + goto sct_verify_err; } if ( ! key ) @@ -638,6 +697,93 @@ sct_verify_err: return new Val(0, TYPE_BOOL); %} + +%%{ +/** + * 0 -> subject name + * 1 -> issuer name + * 2 -> pubkey + */ +StringVal* x509_entity_hash(file_analysis::X509Val *cert_handle, unsigned int hash_alg, unsigned int type) + { + assert(cert_handle); + + if ( type > 2 ) + { + reporter->InternalError("Unknown type in x509_entity_hash"); + return nullptr; + } + + X509 *cert_x509 = cert_handle->GetCertificate(); + if ( cert_x509 == nullptr ) + { + 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 ) + { + builtin_error("fail to get subject/issuer name from certificate"); + return nullptr; + } + + const EVP_MD *dgst = hash_to_evp(hash_alg); + if ( dgst == nullptr ) + { + 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; + + ASN1_BIT_STRING *key = X509_get0_pubkey_bitstr(cert_x509); + if ( key == 0 ) + { + printf("No key in X509_get0_pubkey_bitstr\n"); + } + + 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 ) + { + builtin_error("Could not get SPKI"); + return nullptr; + } + res = EVP_Digest(spki, pklen, md, &len, dgst, nullptr); + OPENSSL_free(spki); + } + + if ( ! res ) + { + builtin_error("Could not perform hash"); + return nullptr; + } + + assert( len <= sizeof(md) ); + + return new StringVal(len, reinterpret_cast(md)); + } +%%} + +function x509_subject_name_hash%(cert: opaque of x509, hash_alg: count%): string + %{ + file_analysis::X509Val *cert_handle = (file_analysis::X509Val *) cert; + + return x509_entity_hash(cert_handle, hash_alg, 0); + %} + ## Get the hash of issuer name of a certificate ## ## cert: The X509 certificate opaque handle. @@ -649,78 +795,16 @@ sct_verify_err: ## .. bro:see:: x509_certificate x509_extension x509_ext_basic_constraints ## x509_ext_subject_alternative_name x509_parse ## x509_get_certificate_string x509_verify -function x509_issuer_name_hash%(cert: opaque of x509, hash_alg: string%): string +function x509_issuer_name_hash%(cert: opaque of x509, hash_alg: count%): string %{ - assert(cert); - assert(hash_alg); - file_analysis::X509Val *cert_handle = (file_analysis::X509Val *) cert; - X509 *cert_x509 = cert_handle->GetCertificate(); - if (cert_x509 == NULL) - { - builtin_error("cannot get cert from opaque"); - return NULL; - } - X509_NAME *issuer_name = NULL; - StringVal *issuer_name_str = NULL; - issuer_name = X509_get_issuer_name(cert_x509); - if (issuer_name == NULL) - { - builtin_error("fail to get issuer name from certificate"); - return NULL; - } - - const char* h = hash_alg->CheckString(); - if (h == NULL) - { - builtin_error("fail to get hash algorithm from input"); - return NULL; - } - - const EVP_MD *dgst; - if (strcmp(h, "sha1") == 0) - dgst = EVP_sha1(); - else if (strcmp(h, "sha224") == 0) - dgst = EVP_sha224(); - else if (strcmp(h, "sha256") == 0) - dgst = EVP_sha256(); - else if (strcmp(h, "sha384") == 0) - dgst = EVP_sha384(); - else if (strcmp(h, "sha512") == 0) - dgst = EVP_sha512(); - else - { - reporter->Error("Unknown digest!"); - return NULL; - } - if (dgst == NULL) - { - builtin_error("fail to allocate digest"); - return NULL; - } - - unsigned char md[EVP_MAX_MD_SIZE]; - unsigned int len = 0; - ASN1_OCTET_STRING *oct_str = ASN1_STRING_type_new(V_ASN1_OCTET_STRING); - int new_len = -1; - BIO *bio = BIO_new(BIO_s_mem()); - char buf[1024]; - memset(buf, 0, sizeof(buf)); - - if (!X509_NAME_digest(issuer_name, dgst, md, &len)) - goto err; - if (!ASN1_OCTET_STRING_set(oct_str, md, len)) - goto err; - if (i2a_ASN1_STRING(bio, oct_str, V_ASN1_OCTET_STRING) <= 0) - goto err; - new_len = BIO_read(bio, buf, sizeof(buf)); - if (new_len > 0) - issuer_name_str = new StringVal(new_len, buf); - - //NOTE: the result string may contain "\\x0a" for sha384 and sha512 - // probably need to remove it from here? -err: - BIO_free_all(bio); - return issuer_name_str; + return x509_entity_hash(cert_handle, hash_alg, 1); + %} + +function x509_spki_hash%(cert: opaque of x509, hash_alg: count%): string + %{ + file_analysis::X509Val *cert_handle = (file_analysis::X509Val *) cert; + + return x509_entity_hash(cert_handle, hash_alg, 2); %} diff --git a/testing/btest/Baseline/scripts.base.files.x509.signed_certificate_timestamp/.stdout b/testing/btest/Baseline/scripts.base.files.x509.signed_certificate_timestamp/.stdout index a27331e535..e11616d745 100644 --- a/testing/btest/Baseline/scripts.base.files.x509.signed_certificate_timestamp/.stdout +++ b/testing/btest/Baseline/scripts.base.files.x509.signed_certificate_timestamp/.stdout @@ -2,3 +2,11 @@ 0, Google 'Rocketeer' log, 1474927232.863, 4, 3 0, Google 'Aviator' log, 1474927232.112, 4, 3 0, Google 'Pilot' log, 1474927232.304, 4, 3 +Verify of, Symantec log, T +Bad verify of, Symantec log, F +Verify of, Google 'Rocketeer' log, T +Bad verify of, Google 'Rocketeer' log, F +Verify of, Google 'Aviator' log, T +Bad verify of, Google 'Aviator' log, F +Verify of, Google 'Pilot' log, T +Bad verify of, Google 'Pilot' log, F diff --git a/testing/btest/scripts/base/files/x509/signed_certificate_timestamp.test b/testing/btest/scripts/base/files/x509/signed_certificate_timestamp.test index c0fe06d4d3..8bb920ee71 100644 --- a/testing/btest/scripts/base/files/x509/signed_certificate_timestamp.test +++ b/testing/btest/scripts/base/files/x509/signed_certificate_timestamp.test @@ -1,7 +1,62 @@ # @TEST-EXEC: bro -r $TRACES/tls/certificate-with-sct.pcap %INPUT # @TEST-EXEC: btest-diff .stdout +@load protocols/ssl/validate-certs + +redef SSL::ssl_store_valid_chain = T; + +export { + type LogInfo: record { + version: count; + logid: string; + timestamp: count; + sig_alg: count; + hash_alg: count; + signature: string; + }; +} + +redef record SSL::Info += { + ct_proofs: vector of LogInfo &default=vector(); +}; + event x509_ocsp_ext_signed_certificate_timestamp(f: fa_file, version: count, logid: string, timestamp: count, hash_algorithm: count, signature_algorithm: count, signature: string) { print version, SSL::ct_logs[logid]$description, double_to_time(timestamp/1000.0), hash_algorithm, signature_algorithm; + + if ( |f$conns| != 1 ) + return; + + for ( cid in f$conns ) + { + if ( ! f$conns[cid]?$ssl ) + return; + + local c = f$conns[cid]; + } + + if ( ! c$ssl?$cert_chain || |c$ssl$cert_chain| == 0 || ! c$ssl$cert_chain[0]?$x509 ) + return; + + c$ssl$ct_proofs[|c$ssl$ct_proofs|] = LogInfo($version=version, $logid=logid, $timestamp=timestamp, $sig_alg=signature_algorithm, $hash_alg=hash_algorithm, $signature=signature); + } + +event ssl_established(c: connection) + { + if ( ! c$ssl?$cert_chain || |c$ssl$cert_chain| == 0 || ! c$ssl$cert_chain[0]?$x509 ) + return; + + if ( |c$ssl$valid_chain| < 2 ) + return; + + local cert = c$ssl$cert_chain[0]$x509$handle; + local issuer_key_hash = x509_spki_hash(c$ssl$valid_chain[1], 4); + + for ( i in c$ssl$ct_proofs ) + { + local log = c$ssl$ct_proofs[i]; + + print "Verify of", SSL::ct_logs[log$logid]$description, sct_verify(cert, log$logid, SSL::ct_logs[log$logid]$key, log$signature, log$timestamp, log$hash_alg, issuer_key_hash); + print "Bad verify of", SSL::ct_logs[log$logid]$description, sct_verify(cert, log$logid, SSL::ct_logs[log$logid]$key, log$signature, log$timestamp+1, log$hash_alg, issuer_key_hash); + } }