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:
Johanna Amann 2017-03-17 15:53:47 -07:00
parent 41a2028dee
commit 115a676d08
4 changed files with 277 additions and 111 deletions

View file

@ -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,

View file

@ -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*>(&timestamp_network), sizeof(timestamp_network)); // timestamp -> 64 bits
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);
#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,29 +659,9 @@ 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;
}
@ -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);
%}

View file

@ -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

View file

@ -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);
}
}