mirror of
https://github.com/zeek/zeek.git
synced 2025-10-02 14:48:21 +00:00
SCT: Allow verification of SCTs in Certs.
This is much more complex than the TLS Extension/OCSP cases. We need to first alter the certificate and remove the extension from it, before extracting the tbscert. Furthermore, we need the key hash of the issuing certificate to be able to validate the proof - which means that we need a valid certificate chain. Missing: documentation, nice integration so that we can just add a script and use this in Bro.
This commit is contained in:
parent
41a2028dee
commit
115a676d08
4 changed files with 277 additions and 111 deletions
|
@ -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,
|
||||
|
|
|
@ -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<char*>(×tamp_network), sizeof(timestamp_network)); // timestamp -> 64 bits
|
||||
data.append("\0\0", 2); // entry-type: x509_entry
|
||||
data.append(reinterpret_cast<const char*>(×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<const char*>(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<char*>(&cert_length_network)+1, 3); // 3 bytes certificate length
|
||||
data.append(reinterpret_cast<char*>(cert_out), cert_length); // der-encoded certificate
|
||||
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.
|
||||
|
@ -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<const char*>(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);
|
||||
%}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue