mirror of
https://github.com/zeek/zeek.git
synced 2025-10-06 16:48:19 +00:00
Merge remote-tracking branch 'origin/topic/bernhard/file-analysis-x509'
* origin/topic/bernhard/file-analysis-x509: Forgot the preamble for the new leak test (hopefully) last change -> return real opaque vec instead of any_vec Fix dump-events - it cannot be used with ssl anymore, because openssl does not give the same string results in all versions. Finishing touches of the x509 file analyzer. Revert change to only log certificates once per hour. Change x509 log - now certificates are only logged once per hour. Fix circular reference problem and a few other small things. X509 file analyzer nearly done. Verification and most other policy scripts work fine now. Add verify functionality, including the ability to get the validated chain. This means that it is now possible to get information about the root-certificates that were used to secure a connection. Second try on the event interface. Backport crash fix that made it into master with the x509_extension backport from here. Make x509 certificates an opaque type rip out x509 code from ssl analyzer. Note that since at the moment the file analyzer does not yet re-populate the info record that means quite a lot of information is simply not available. parse out extension. One event for general extensions (just returns the openssl-parsed string-value), one event for basicconstraints (is a certificate a CA or not) and one event for subject-alternative-names (only DNS parts). Very basic file-analyzer for x509 certificates. Mostly ripped from the ssl-analyzer and the topic/bernhard/x509 branch.
This commit is contained in:
commit
e8339d5c63
80 changed files with 2509 additions and 942 deletions
|
@ -108,7 +108,7 @@ public:
|
|||
* cached and passed back in to a subsequent function call in order
|
||||
* to avoid costly file handle lookups (which have to go through
|
||||
* the \c get_file_handle script-layer event). An empty string
|
||||
* indicates the associate file is not going to be analyzed further.
|
||||
* indicates the associated file is not going to be analyzed further.
|
||||
*/
|
||||
std::string DataIn(const u_char* data, uint64 len, analyzer::Tag tag,
|
||||
Connection* conn, bool is_orig,
|
||||
|
|
|
@ -2,3 +2,4 @@ add_subdirectory(data_event)
|
|||
add_subdirectory(extract)
|
||||
add_subdirectory(hash)
|
||||
add_subdirectory(unified2)
|
||||
add_subdirectory(x509)
|
||||
|
|
10
src/file_analysis/analyzer/x509/CMakeLists.txt
Normal file
10
src/file_analysis/analyzer/x509/CMakeLists.txt
Normal file
|
@ -0,0 +1,10 @@
|
|||
|
||||
include(BroPlugin)
|
||||
|
||||
include_directories(BEFORE ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
bro_plugin_begin(Bro X509)
|
||||
bro_plugin_cc(X509.cc Plugin.cc)
|
||||
bro_plugin_bif(events.bif types.bif functions.bif)
|
||||
bro_plugin_end()
|
11
src/file_analysis/analyzer/x509/Plugin.cc
Normal file
11
src/file_analysis/analyzer/x509/Plugin.cc
Normal file
|
@ -0,0 +1,11 @@
|
|||
#include "plugin/Plugin.h"
|
||||
|
||||
#include "X509.h"
|
||||
|
||||
BRO_PLUGIN_BEGIN(Bro, X509)
|
||||
BRO_PLUGIN_DESCRIPTION("X509 certificate parser");
|
||||
BRO_PLUGIN_FILE_ANALYZER("X509", X509);
|
||||
BRO_PLUGIN_BIF_FILE(events);
|
||||
BRO_PLUGIN_BIF_FILE(types);
|
||||
BRO_PLUGIN_BIF_FILE(functions);
|
||||
BRO_PLUGIN_END
|
587
src/file_analysis/analyzer/x509/X509.cc
Normal file
587
src/file_analysis/analyzer/x509/X509.cc
Normal file
|
@ -0,0 +1,587 @@
|
|||
// See the file "COPYING" in the main distribution directory for copyright.
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "X509.h"
|
||||
#include "Event.h"
|
||||
|
||||
#include "events.bif.h"
|
||||
#include "types.bif.h"
|
||||
|
||||
#include "file_analysis/Manager.h"
|
||||
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/x509v3.h>
|
||||
#include <openssl/asn1.h>
|
||||
#include <openssl/opensslconf.h>
|
||||
|
||||
using namespace file_analysis;
|
||||
|
||||
IMPLEMENT_SERIAL(X509Val, SER_X509_VAL);
|
||||
|
||||
file_analysis::X509::X509(RecordVal* args, file_analysis::File* file)
|
||||
: file_analysis::Analyzer(file_mgr->GetComponentTag("X509"), args, file)
|
||||
{
|
||||
cert_data.clear();
|
||||
}
|
||||
|
||||
bool file_analysis::X509::DeliverStream(const u_char* data, uint64 len)
|
||||
{
|
||||
// just add it to the data we have so far, since we cannot do anything else anyways...
|
||||
cert_data.append(reinterpret_cast<const char*>(data), len);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool file_analysis::X509::Undelivered(uint64 offset, uint64 len)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool file_analysis::X509::EndOfFile()
|
||||
{
|
||||
// ok, now we can try to parse the certificate with openssl. Should
|
||||
// be rather straightforward...
|
||||
const unsigned char* cert_char = reinterpret_cast<const unsigned char*>(cert_data.data());
|
||||
|
||||
::X509* ssl_cert = d2i_X509(NULL, &cert_char, cert_data.size());
|
||||
if ( ! ssl_cert )
|
||||
{
|
||||
reporter->Error("Could not parse X509 certificate (fuid %s)", GetFile()->GetID().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
X509Val* cert_val = new X509Val(ssl_cert); // cert_val takes ownership of ssl_cert
|
||||
|
||||
RecordVal* cert_record = ParseCertificate(cert_val); // parse basic information into record
|
||||
|
||||
// and send the record on to scriptland
|
||||
val_list* vl = new val_list();
|
||||
vl->append(GetFile()->GetVal()->Ref());
|
||||
vl->append(cert_val->Ref());
|
||||
vl->append(cert_record->Ref()); // we Ref it here, because we want to keep a copy around for now...
|
||||
mgr.QueueEvent(x509_certificate, vl);
|
||||
|
||||
// after parsing the certificate - parse the extensions...
|
||||
|
||||
int num_ext = X509_get_ext_count(ssl_cert);
|
||||
for ( int k = 0; k < num_ext; ++k )
|
||||
{
|
||||
X509_EXTENSION* ex = X509_get_ext(ssl_cert, k);
|
||||
if ( ! ex )
|
||||
continue;
|
||||
|
||||
ParseExtension(ex);
|
||||
}
|
||||
|
||||
// X509_free(ssl_cert); We do _not_ free the certificate here. It is refcounted
|
||||
// inside the X509Val that is sent on in the cert record to scriptland.
|
||||
//
|
||||
// The certificate will be freed when the last X509Val is Unref'd.
|
||||
|
||||
Unref(cert_record); // Unref the RecordVal that we kept around from ParseCertificate
|
||||
Unref(cert_val); // Same for cert_val
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
RecordVal* file_analysis::X509::ParseCertificate(X509Val* cert_val)
|
||||
{
|
||||
::X509* ssl_cert = cert_val->GetCertificate();
|
||||
|
||||
char buf[256]; // we need a buffer for some of the openssl functions
|
||||
memset(buf, 0, sizeof(buf));
|
||||
|
||||
RecordVal* pX509Cert = new RecordVal(BifType::Record::X509::Certificate);
|
||||
BIO *bio = BIO_new(BIO_s_mem());
|
||||
|
||||
pX509Cert->Assign(0, new Val((uint64) X509_get_version(ssl_cert), TYPE_COUNT));
|
||||
i2a_ASN1_INTEGER(bio, X509_get_serialNumber(ssl_cert));
|
||||
int len = BIO_read(bio, &(*buf), sizeof(buf));
|
||||
pX509Cert->Assign(1, new StringVal(len, buf));
|
||||
|
||||
X509_NAME_print_ex(bio, X509_get_subject_name(ssl_cert), 0, XN_FLAG_RFC2253);
|
||||
len = BIO_gets(bio, &(*buf), sizeof(buf));
|
||||
pX509Cert->Assign(2, new StringVal(len, buf));
|
||||
X509_NAME_print_ex(bio, X509_get_issuer_name(ssl_cert), 0, XN_FLAG_RFC2253);
|
||||
len = BIO_gets(bio, &(*buf), sizeof(buf));
|
||||
pX509Cert->Assign(3, new StringVal(len, buf));
|
||||
BIO_free(bio);
|
||||
|
||||
pX509Cert->Assign(4, new Val(GetTimeFromAsn1(X509_get_notBefore(ssl_cert)), TYPE_TIME));
|
||||
pX509Cert->Assign(5, new Val(GetTimeFromAsn1(X509_get_notAfter(ssl_cert)), TYPE_TIME));
|
||||
|
||||
// we only read 255 bytes because byte 256 is always 0.
|
||||
// if the string is longer than 255, that will be our null-termination,
|
||||
// otherwhise i2t does null-terminate.
|
||||
if ( ! i2t_ASN1_OBJECT(buf, 255, ssl_cert->cert_info->key->algor->algorithm) )
|
||||
buf[0] = 0;
|
||||
|
||||
pX509Cert->Assign(6, new StringVal(buf));
|
||||
|
||||
if ( ! i2t_ASN1_OBJECT(buf, 255, ssl_cert->sig_alg->algorithm) )
|
||||
buf[0] = 0;
|
||||
|
||||
pX509Cert->Assign(7, new StringVal(buf));
|
||||
|
||||
// Things we can do when we have the key...
|
||||
EVP_PKEY *pkey = X509_extract_key(ssl_cert);
|
||||
if ( pkey != NULL )
|
||||
{
|
||||
if ( pkey->type == EVP_PKEY_DSA )
|
||||
pX509Cert->Assign(8, new StringVal("dsa"));
|
||||
|
||||
else if ( pkey->type == EVP_PKEY_RSA )
|
||||
{
|
||||
pX509Cert->Assign(8, new StringVal("rsa"));
|
||||
|
||||
char *exponent = BN_bn2dec(pkey->pkey.rsa->e);
|
||||
if ( exponent != NULL )
|
||||
{
|
||||
pX509Cert->Assign(10, new StringVal(exponent));
|
||||
OPENSSL_free(exponent);
|
||||
exponent = NULL;
|
||||
}
|
||||
}
|
||||
#ifndef OPENSSL_NO_EC
|
||||
else if ( pkey->type == EVP_PKEY_EC )
|
||||
{
|
||||
pX509Cert->Assign(8, new StringVal("dsa"));
|
||||
pX509Cert->Assign(11, KeyCurve(pkey));
|
||||
}
|
||||
#endif
|
||||
|
||||
unsigned int length = KeyLength(pkey);
|
||||
if ( length > 0 )
|
||||
pX509Cert->Assign(9, new Val(length, TYPE_COUNT));
|
||||
}
|
||||
|
||||
|
||||
return pX509Cert;
|
||||
}
|
||||
|
||||
void file_analysis::X509::ParseExtension(X509_EXTENSION* ex)
|
||||
{
|
||||
char name[256];
|
||||
char oid[256];
|
||||
|
||||
ASN1_OBJECT* ext_asn = X509_EXTENSION_get_object(ex);
|
||||
const char* short_name = OBJ_nid2sn(OBJ_obj2nid(ext_asn));
|
||||
|
||||
OBJ_obj2txt(name, 255, ext_asn, 0);
|
||||
OBJ_obj2txt(oid, 255, ext_asn, 1);
|
||||
|
||||
int critical = 0;
|
||||
if ( X509_EXTENSION_get_critical(ex) != 0 )
|
||||
critical = 1;
|
||||
|
||||
BIO *bio = BIO_new(BIO_s_mem());
|
||||
if( ! X509V3_EXT_print(bio, ex, 0, 0))
|
||||
M_ASN1_OCTET_STRING_print(bio,ex->value);
|
||||
|
||||
BIO_flush(bio);
|
||||
int length = BIO_pending(bio);
|
||||
|
||||
// Use OPENSSL_malloc here. Using new or anything else can lead
|
||||
// to interesting, hard to debug segfaults.
|
||||
char *buffer = (char*) OPENSSL_malloc(length);
|
||||
BIO_read(bio, (void*)buffer, length);
|
||||
StringVal* ext_val = new StringVal(length, buffer);
|
||||
OPENSSL_free(buffer);
|
||||
BIO_free_all(bio);
|
||||
|
||||
RecordVal* pX509Ext = new RecordVal(BifType::Record::X509::Extension);
|
||||
pX509Ext->Assign(0, new StringVal(name));
|
||||
|
||||
if ( short_name and strlen(short_name) > 0 )
|
||||
pX509Ext->Assign(1, new StringVal(short_name));
|
||||
|
||||
pX509Ext->Assign(2, new StringVal(oid));
|
||||
pX509Ext->Assign(3, new Val(critical, TYPE_BOOL));
|
||||
pX509Ext->Assign(4, ext_val);
|
||||
|
||||
// send off generic extension event
|
||||
//
|
||||
// and then look if we have a specialized event for the extension we just
|
||||
// parsed. And if we have it, we send the specialized event on top of the
|
||||
// generic event that we just had. I know, that is... kind of not nice,
|
||||
// but I am not sure if there is a better way to do it...
|
||||
val_list* vl = new val_list();
|
||||
vl->append(GetFile()->GetVal()->Ref());
|
||||
vl->append(pX509Ext);
|
||||
|
||||
mgr.QueueEvent(x509_extension, vl);
|
||||
|
||||
// look if we have a specialized handler for this event...
|
||||
if ( OBJ_obj2nid(ext_asn) == NID_basic_constraints )
|
||||
ParseBasicConstraints(ex);
|
||||
|
||||
else if ( OBJ_obj2nid(ext_asn) == NID_subject_alt_name )
|
||||
ParseSAN(ex);
|
||||
}
|
||||
|
||||
void file_analysis::X509::ParseBasicConstraints(X509_EXTENSION* ex)
|
||||
{
|
||||
assert(OBJ_obj2nid(X509_EXTENSION_get_object(ex)) == NID_basic_constraints);
|
||||
|
||||
RecordVal* pBasicConstraint = new RecordVal(BifType::Record::X509::BasicConstraints);
|
||||
BASIC_CONSTRAINTS *constr = (BASIC_CONSTRAINTS *) X509V3_EXT_d2i(ex);
|
||||
|
||||
if ( constr )
|
||||
{
|
||||
pBasicConstraint->Assign(0, new Val(constr->ca ? 1 : 0, TYPE_BOOL));
|
||||
|
||||
if ( constr->pathlen )
|
||||
pBasicConstraint->Assign(1, new Val((int32_t) ASN1_INTEGER_get(constr->pathlen), TYPE_COUNT));
|
||||
|
||||
val_list* vl = new val_list();
|
||||
vl->append(GetFile()->GetVal()->Ref());
|
||||
vl->append(pBasicConstraint);
|
||||
|
||||
mgr.QueueEvent(x509_ext_basic_constraints, vl);
|
||||
}
|
||||
|
||||
else
|
||||
reporter->Error("Certificate with invalid BasicConstraint. fuid %s", GetFile()->GetID().c_str());
|
||||
}
|
||||
|
||||
void file_analysis::X509::ParseSAN(X509_EXTENSION* ext)
|
||||
{
|
||||
assert(OBJ_obj2nid(X509_EXTENSION_get_object(ext)) == NID_subject_alt_name);
|
||||
|
||||
GENERAL_NAMES *altname = (GENERAL_NAMES*)X509V3_EXT_d2i(ext);
|
||||
if ( ! altname )
|
||||
{
|
||||
reporter->Error("Could not parse subject alternative names. fuid %s", GetFile()->GetID().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
VectorVal* names = 0;
|
||||
VectorVal* emails = 0;
|
||||
VectorVal* uris = 0;
|
||||
VectorVal* ips = 0;
|
||||
|
||||
unsigned int otherfields = 0;
|
||||
|
||||
for ( int i = 0; i < sk_GENERAL_NAME_num(altname); i++ )
|
||||
{
|
||||
GENERAL_NAME *gen = sk_GENERAL_NAME_value(altname, i);
|
||||
assert(gen);
|
||||
|
||||
if ( gen->type == GEN_DNS || gen->type == GEN_URI || gen->type == GEN_EMAIL )
|
||||
{
|
||||
if ( ASN1_STRING_type(gen->d.ia5) != V_ASN1_IA5STRING )
|
||||
{
|
||||
reporter->Error("DNS-field does not contain an IA5String. fuid %s", GetFile()->GetID().c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* name = (const char*) ASN1_STRING_data(gen->d.ia5);
|
||||
StringVal* bs = new StringVal(name);
|
||||
|
||||
switch ( gen->type )
|
||||
{
|
||||
case GEN_DNS:
|
||||
if ( names == 0 )
|
||||
names = new VectorVal(internal_type("string_vec")->AsVectorType());
|
||||
|
||||
names->Assign(names->Size(), bs);
|
||||
break;
|
||||
|
||||
case GEN_URI:
|
||||
if ( uris == 0 )
|
||||
uris = new VectorVal(internal_type("string_vec")->AsVectorType());
|
||||
|
||||
uris->Assign(uris->Size(), bs);
|
||||
break;
|
||||
|
||||
case GEN_EMAIL:
|
||||
if ( emails == 0 )
|
||||
emails = new VectorVal(internal_type("string_vec")->AsVectorType());
|
||||
|
||||
emails->Assign(emails->Size(), bs);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
else if ( gen->type == GEN_IPADD )
|
||||
{
|
||||
if ( ips == 0 )
|
||||
ips = new VectorVal(internal_type("addr_vec")->AsVectorType());
|
||||
|
||||
uint32* addr = (uint32*) gen->d.ip->data;
|
||||
|
||||
if( gen->d.ip->length == 4 )
|
||||
ips->Assign(ips->Size(), new AddrVal(*addr));
|
||||
|
||||
else if ( gen->d.ip->length == 16 )
|
||||
ips->Assign(ips->Size(), new AddrVal(addr));
|
||||
|
||||
else
|
||||
{
|
||||
reporter->Error("Weird IP address length %d in subject alternative name. fuid %s", gen->d.ip->length, GetFile()->GetID().c_str());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
// reporter->Error("Subject alternative name contained unsupported fields. fuid %s", GetFile()->GetID().c_str());
|
||||
// This happens quite often - just mark it
|
||||
otherfields = 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
RecordVal* sanExt = new RecordVal(BifType::Record::X509::SubjectAlternativeName);
|
||||
|
||||
if ( names != 0 )
|
||||
sanExt->Assign(0, names);
|
||||
|
||||
if ( uris != 0 )
|
||||
sanExt->Assign(1, uris);
|
||||
|
||||
if ( emails != 0 )
|
||||
sanExt->Assign(2, emails);
|
||||
|
||||
if ( ips != 0 )
|
||||
sanExt->Assign(3, ips);
|
||||
|
||||
sanExt->Assign(4, new Val(otherfields, TYPE_BOOL));
|
||||
|
||||
val_list* vl = new val_list();
|
||||
vl->append(GetFile()->GetVal()->Ref());
|
||||
vl->append(sanExt);
|
||||
mgr.QueueEvent(x509_ext_subject_alternative_name, vl);
|
||||
}
|
||||
|
||||
StringVal* file_analysis::X509::KeyCurve(EVP_PKEY *key)
|
||||
{
|
||||
assert(key != NULL);
|
||||
|
||||
#ifdef OPENSSL_NO_EC
|
||||
// well, we do not have EC-Support...
|
||||
return NULL;
|
||||
#else
|
||||
if ( key->type != EVP_PKEY_EC )
|
||||
{
|
||||
// no EC-key - no curve name
|
||||
return NULL;
|
||||
}
|
||||
|
||||
const EC_GROUP *group;
|
||||
int nid;
|
||||
if ( (group = EC_KEY_get0_group(key->pkey.ec)) == NULL)
|
||||
// I guess we could not parse this
|
||||
return NULL;
|
||||
|
||||
nid = EC_GROUP_get_curve_name(group);
|
||||
if ( nid == 0 )
|
||||
// and an invalid nid...
|
||||
return NULL;
|
||||
|
||||
const char * curve_name = OBJ_nid2sn(nid);
|
||||
if ( curve_name == NULL )
|
||||
return NULL;
|
||||
|
||||
return new StringVal(curve_name);
|
||||
#endif
|
||||
}
|
||||
|
||||
unsigned int file_analysis::X509::KeyLength(EVP_PKEY *key)
|
||||
{
|
||||
assert(key != NULL);
|
||||
|
||||
switch(key->type) {
|
||||
case EVP_PKEY_RSA:
|
||||
return BN_num_bits(key->pkey.rsa->n);
|
||||
|
||||
case EVP_PKEY_DSA:
|
||||
return BN_num_bits(key->pkey.dsa->p);
|
||||
|
||||
#ifndef OPENSSL_NO_EC
|
||||
case EVP_PKEY_EC:
|
||||
{
|
||||
BIGNUM* ec_order = BN_new();
|
||||
if ( ! ec_order )
|
||||
// could not malloc bignum?
|
||||
return 0;
|
||||
|
||||
const EC_GROUP *group = EC_KEY_get0_group(key->pkey.ec);
|
||||
if ( ! group )
|
||||
// unknown ex-group
|
||||
return 0;
|
||||
|
||||
if ( ! EC_GROUP_get_order(group, ec_order, NULL) )
|
||||
// could not get ec-group-order
|
||||
return 0;
|
||||
|
||||
unsigned int length = BN_num_bits(ec_order);
|
||||
BN_free(ec_order);
|
||||
return length;
|
||||
}
|
||||
#endif
|
||||
default:
|
||||
return 0; // unknown public key type
|
||||
}
|
||||
|
||||
reporter->InternalError("cannot be reached");
|
||||
}
|
||||
|
||||
double file_analysis::X509::GetTimeFromAsn1(const ASN1_TIME* atime)
|
||||
{
|
||||
time_t lResult = 0;
|
||||
|
||||
char lBuffer[24];
|
||||
char* pBuffer = lBuffer;
|
||||
|
||||
size_t lTimeLength = atime->length;
|
||||
char * pString = (char *) atime->data;
|
||||
|
||||
if ( atime->type == V_ASN1_UTCTIME )
|
||||
{
|
||||
if ( lTimeLength < 11 || lTimeLength > 17 )
|
||||
return 0;
|
||||
|
||||
memcpy(pBuffer, pString, 10);
|
||||
pBuffer += 10;
|
||||
pString += 10;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
if ( lTimeLength < 13 )
|
||||
return 0;
|
||||
|
||||
memcpy(pBuffer, pString, 12);
|
||||
pBuffer += 12;
|
||||
pString += 12;
|
||||
}
|
||||
|
||||
if ((*pString == 'Z') || (*pString == '-') || (*pString == '+'))
|
||||
{
|
||||
*(pBuffer++) = '0';
|
||||
*(pBuffer++) = '0';
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
*(pBuffer++) = *(pString++);
|
||||
*(pBuffer++) = *(pString++);
|
||||
|
||||
// Skip any fractional seconds...
|
||||
if (*pString == '.')
|
||||
{
|
||||
pString++;
|
||||
while ((*pString >= '0') && (*pString <= '9'))
|
||||
pString++;
|
||||
}
|
||||
}
|
||||
|
||||
*(pBuffer++) = 'Z';
|
||||
*(pBuffer++) = '\0';
|
||||
|
||||
time_t lSecondsFromUTC;
|
||||
|
||||
if ( *pString == 'Z' )
|
||||
lSecondsFromUTC = 0;
|
||||
|
||||
else
|
||||
{
|
||||
if ((*pString != '+') && (pString[5] != '-'))
|
||||
return 0;
|
||||
|
||||
lSecondsFromUTC = ((pString[1]-'0') * 10 + (pString[2]-'0')) * 60;
|
||||
lSecondsFromUTC += (pString[3]-'0') * 10 + (pString[4]-'0');
|
||||
|
||||
if (*pString == '-')
|
||||
lSecondsFromUTC = -lSecondsFromUTC;
|
||||
}
|
||||
|
||||
tm lTime;
|
||||
lTime.tm_sec = ((lBuffer[10] - '0') * 10) + (lBuffer[11] - '0');
|
||||
lTime.tm_min = ((lBuffer[8] - '0') * 10) + (lBuffer[9] - '0');
|
||||
lTime.tm_hour = ((lBuffer[6] - '0') * 10) + (lBuffer[7] - '0');
|
||||
lTime.tm_mday = ((lBuffer[4] - '0') * 10) + (lBuffer[5] - '0');
|
||||
lTime.tm_mon = (((lBuffer[2] - '0') * 10) + (lBuffer[3] - '0')) - 1;
|
||||
lTime.tm_year = ((lBuffer[0] - '0') * 10) + (lBuffer[1] - '0');
|
||||
|
||||
if ( lTime.tm_year < 50 )
|
||||
lTime.tm_year += 100; // RFC 2459
|
||||
|
||||
lTime.tm_wday = 0;
|
||||
lTime.tm_yday = 0;
|
||||
lTime.tm_isdst = 0; // No DST adjustment requested
|
||||
|
||||
lResult = mktime(&lTime);
|
||||
|
||||
if ( lResult )
|
||||
{
|
||||
if ( 0 != lTime.tm_isdst )
|
||||
lResult -= 3600; // mktime may adjust for DST (OS dependent)
|
||||
|
||||
lResult += lSecondsFromUTC;
|
||||
}
|
||||
|
||||
else
|
||||
lResult = 0;
|
||||
|
||||
return lResult;
|
||||
}
|
||||
|
||||
X509Val::X509Val(::X509* arg_certificate) : OpaqueVal(x509_opaque_type)
|
||||
{
|
||||
certificate = arg_certificate;
|
||||
}
|
||||
|
||||
X509Val::X509Val() : OpaqueVal(x509_opaque_type)
|
||||
{
|
||||
certificate = 0;
|
||||
}
|
||||
|
||||
X509Val::~X509Val()
|
||||
{
|
||||
if ( certificate )
|
||||
X509_free(certificate);
|
||||
}
|
||||
|
||||
::X509* X509Val::GetCertificate() const
|
||||
{
|
||||
return certificate;
|
||||
}
|
||||
|
||||
bool X509Val::DoSerialize(SerialInfo* info) const
|
||||
{
|
||||
DO_SERIALIZE(SER_X509_VAL, OpaqueVal);
|
||||
|
||||
unsigned char *buf = NULL;
|
||||
|
||||
int length = i2d_X509(certificate, &buf);
|
||||
|
||||
if ( length < 0 )
|
||||
return false;
|
||||
|
||||
bool res = SERIALIZE_STR(reinterpret_cast<const char*>(buf), length);
|
||||
|
||||
OPENSSL_free(buf);
|
||||
return res;
|
||||
}
|
||||
|
||||
bool X509Val::DoUnserialize(UnserialInfo* info)
|
||||
{
|
||||
DO_UNSERIALIZE(OpaqueVal)
|
||||
|
||||
int length;
|
||||
unsigned char *certbuf, *opensslbuf;
|
||||
|
||||
if ( ! UNSERIALIZE_STR(reinterpret_cast<char **>(&certbuf), &length) )
|
||||
return false;
|
||||
|
||||
opensslbuf = certbuf; // OpenSSL likes to shift pointers around. really.
|
||||
certificate = d2i_X509(NULL, const_cast<const unsigned char**>(&opensslbuf), length);
|
||||
delete[] certbuf;
|
||||
|
||||
if ( !certificate )
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
101
src/file_analysis/analyzer/x509/X509.h
Normal file
101
src/file_analysis/analyzer/x509/X509.h
Normal file
|
@ -0,0 +1,101 @@
|
|||
// See the file "COPYING" in the main distribution directory for copyright.
|
||||
|
||||
#ifndef FILE_ANALYSIS_X509_H
|
||||
#define FILE_ANALYSIS_X509_H
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "Val.h"
|
||||
#include "../File.h"
|
||||
#include "Analyzer.h"
|
||||
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/asn1.h>
|
||||
|
||||
namespace file_analysis {
|
||||
|
||||
class X509Val;
|
||||
|
||||
class X509 : public file_analysis::Analyzer {
|
||||
public:
|
||||
virtual bool DeliverStream(const u_char* data, uint64 len);
|
||||
virtual bool Undelivered(uint64 offset, uint64 len);
|
||||
virtual bool EndOfFile();
|
||||
|
||||
/**
|
||||
* Converts an X509 certificate into a \c X509::Certificate record
|
||||
* value. This is a static function that can be called from external,
|
||||
* it doesn't depend on the state of any particular file analyzer.
|
||||
*
|
||||
* @param cert_val The certificate to converts.
|
||||
*
|
||||
* @param Returns the new record value and passes ownership to
|
||||
* caller.
|
||||
*/
|
||||
static RecordVal* ParseCertificate(X509Val* cert_val);
|
||||
|
||||
static file_analysis::Analyzer* Instantiate(RecordVal* args, File* file)
|
||||
{ return new X509(args, file); }
|
||||
|
||||
protected:
|
||||
X509(RecordVal* args, File* file);
|
||||
|
||||
private:
|
||||
void ParseExtension(X509_EXTENSION* ex);
|
||||
void ParseBasicConstraints(X509_EXTENSION* ex);
|
||||
void ParseSAN(X509_EXTENSION* ex);
|
||||
|
||||
std::string cert_data;
|
||||
|
||||
// Helpers for ParseCertificate.
|
||||
static double GetTimeFromAsn1(const ASN1_TIME * atime);
|
||||
static StringVal* KeyCurve(EVP_PKEY *key);
|
||||
static unsigned int KeyLength(EVP_PKEY *key);
|
||||
};
|
||||
|
||||
/**
|
||||
* This class wraps an OpenSSL X509 data structure.
|
||||
*
|
||||
* We need these to be able to pass OpenSSL pointers around in Bro
|
||||
* script-land. Otherwise, we cannot verify certificates from Bro
|
||||
* scriptland
|
||||
*/
|
||||
class X509Val : public OpaqueVal {
|
||||
public:
|
||||
/**
|
||||
* Construct an X509Val.
|
||||
*
|
||||
* @param certificate specifies the wrapped OpenSSL certificate
|
||||
*
|
||||
* @return A newly initialized X509Val.
|
||||
*/
|
||||
explicit X509Val(::X509* certificate);
|
||||
|
||||
/**
|
||||
* Destructor.
|
||||
*/
|
||||
~X509Val();
|
||||
|
||||
/**
|
||||
* Get the wrapped X509 certificate. Please take care, that the
|
||||
* internal OpenSSL reference counting stays the same.
|
||||
*
|
||||
* @return The wrapped OpenSSL X509 certificate.
|
||||
*/
|
||||
::X509* GetCertificate() const;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Construct an empty X509Val. Only used for deserialization
|
||||
*/
|
||||
X509Val();
|
||||
|
||||
private:
|
||||
::X509* certificate; // the wrapped certificate
|
||||
|
||||
DECLARE_SERIAL(X509Val);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
57
src/file_analysis/analyzer/x509/events.bif
Normal file
57
src/file_analysis/analyzer/x509/events.bif
Normal file
|
@ -0,0 +1,57 @@
|
|||
## Generated for encountered X509 certificates, e.g., in the clear SSL/TLS
|
||||
## connection handshake.
|
||||
##
|
||||
## See `Wikipedia <http://en.wikipedia.org/wiki/X.509>`__ for more information
|
||||
## about the X.509 format.
|
||||
##
|
||||
## f: The file.
|
||||
##
|
||||
## cert_ref: An opaque pointer to the underlying OpenSSL data structure of the
|
||||
## certificate.
|
||||
##
|
||||
## cert: The parsed certificate information.
|
||||
##
|
||||
## .. bro:see:: x509_extension x509_ext_basic_constraints
|
||||
## x509_ext_subject_alternative_name x509_parse x509_verify
|
||||
## x509_get_certificate_string
|
||||
event x509_certificate%(f: fa_file, cert_ref: opaque of x509, cert: X509::Certificate%);
|
||||
|
||||
## Generated for X509 extensions seen in a certificate.
|
||||
##
|
||||
## See `Wikipedia <http://en.wikipedia.org/wiki/X.509>`__ for more information
|
||||
## about the X.509 format.
|
||||
##
|
||||
## f: The file.
|
||||
##
|
||||
## ext: The parsed extension.
|
||||
##
|
||||
## .. bro:see:: x509_certificate x509_ext_basic_constraints
|
||||
## x509_ext_subject_alternative_name x509_parse x509_verify
|
||||
## x509_get_certificate_string
|
||||
event x509_extension%(f: fa_file, ext: X509::Extension%);
|
||||
|
||||
## Generated for the X509 basic constraints extension seen in a certificate.
|
||||
## This extension can be used to identify the subject of a certificate as a CA.
|
||||
##
|
||||
## f: The file.
|
||||
##
|
||||
## ext: The parsed basic constraints extension.
|
||||
##
|
||||
## .. bro:see:: x509_certificate x509_extension
|
||||
## x509_ext_subject_alternative_name x509_parse x509_verify
|
||||
## x509_get_certificate_string
|
||||
event x509_ext_basic_constraints%(f: fa_file, ext: X509::BasicConstraints%);
|
||||
|
||||
## Generated for the X509 subject alternative name extension seen in a certificate.
|
||||
## This extension can be used to allow additional entities to be bound to the subject
|
||||
## of the certificate. Usually it is used to specify one or multiple DNS names for
|
||||
## which a certificate is valid.
|
||||
##
|
||||
## f: The file.
|
||||
##
|
||||
## ext: The parsed subject alternative name extension.
|
||||
##
|
||||
## .. bro:see:: x509_certificate x509_extension x509_ext_basic_constraints
|
||||
## x509_parse x509_verify
|
||||
## x509_get_certificate_string
|
||||
event x509_ext_subject_alternative_name%(f: fa_file, ext: X509::SubjectAlternativeName%);
|
245
src/file_analysis/analyzer/x509/functions.bif
Normal file
245
src/file_analysis/analyzer/x509/functions.bif
Normal file
|
@ -0,0 +1,245 @@
|
|||
%%{
|
||||
#include "file_analysis/analyzer/x509/X509.h"
|
||||
#include "types.bif.h"
|
||||
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/asn1.h>
|
||||
#include <openssl/x509_vfy.h>
|
||||
|
||||
// This is the indexed map of X509 certificate stores.
|
||||
static map<Val*, X509_STORE*> x509_stores;
|
||||
|
||||
// ### NOTE: while d2i_X509 does not take a const u_char** pointer,
|
||||
// here we assume d2i_X509 does not write to <data>, so it is safe to
|
||||
// convert data to a non-const pointer. Could some X509 guru verify
|
||||
// this?
|
||||
|
||||
X509* d2i_X509_(X509** px, const u_char** in, int len)
|
||||
{
|
||||
#ifdef OPENSSL_D2I_X509_USES_CONST_CHAR
|
||||
return d2i_X509(px, in, len);
|
||||
#else
|
||||
return d2i_X509(px, (u_char**)in, len);
|
||||
#endif
|
||||
}
|
||||
|
||||
// construct an error record
|
||||
RecordVal* x509_error_record(uint64_t num, const char* reason)
|
||||
{
|
||||
RecordVal* rrecord = new RecordVal(BifType::Record::X509::Result);
|
||||
|
||||
rrecord->Assign(0, new Val(num, TYPE_COUNT));
|
||||
rrecord->Assign(1, new StringVal(reason));
|
||||
|
||||
return rrecord;
|
||||
}
|
||||
|
||||
%%}
|
||||
|
||||
## Parses a certificate into an X509::Certificate structure.
|
||||
##
|
||||
## cert: The X509 certificicate opaque handle
|
||||
##
|
||||
## Returns: A X509::Certificate structure
|
||||
##
|
||||
## .. bro: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);
|
||||
file_analysis::X509Val* h = (file_analysis::X509Val*) cert;
|
||||
|
||||
return file_analysis::X509::ParseCertificate(h);
|
||||
%}
|
||||
|
||||
## 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
|
||||
##
|
||||
## .. bro: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);
|
||||
file_analysis::X509Val* h = (file_analysis::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());
|
||||
|
||||
BIO_flush(bio);
|
||||
int length = BIO_pending(bio);
|
||||
// use OPENSS_malloc here. Otherwhise, interesting problems will happen.
|
||||
char *buffer = (char*) OPENSSL_malloc(length);
|
||||
BIO_read(bio, (void*) buffer, length);
|
||||
StringVal* ext_val = new StringVal(length, buffer);
|
||||
OPENSSL_free(buffer);
|
||||
BIO_free_all(bio);
|
||||
|
||||
return ext_val;
|
||||
%}
|
||||
|
||||
|
||||
## 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.
|
||||
##
|
||||
## .. bro:see:: x509_certificate x509_extension x509_ext_basic_constraints
|
||||
## x509_ext_subject_alternative_name x509_parse
|
||||
## x509_get_certificate_string
|
||||
function x509_verify%(certs: x509_opaque_vector, root_certs: table_string_of_string, verify_time: time &default=network_time()%): X509::Result
|
||||
%{
|
||||
X509_STORE* ctx = 0;
|
||||
int i = 0;
|
||||
|
||||
VectorVal *certs_vec = certs->AsVectorVal();
|
||||
if ( certs_vec->Size() < 1 )
|
||||
{
|
||||
reporter->Error("No certificates given in vector");
|
||||
return x509_error_record(-1, "no certificates");
|
||||
}
|
||||
|
||||
// host certificate
|
||||
unsigned int index = 0; // to prevent overloading to 0pointer
|
||||
Val *sv = certs_vec->Lookup(index);
|
||||
if ( !sv )
|
||||
{
|
||||
builtin_error("undefined value in certificate vector");
|
||||
return x509_error_record(-1, "undefined value in certificate vector");
|
||||
}
|
||||
|
||||
file_analysis::X509Val* cert_handle = (file_analysis::X509Val*) sv;
|
||||
|
||||
// If this certificate store was built previously, just reuse the old one.
|
||||
if ( x509_stores.count(root_certs) > 0 )
|
||||
ctx = x509_stores[root_certs];
|
||||
|
||||
if ( ! ctx ) // lookup to see if we have this one built already!
|
||||
{
|
||||
ctx = X509_STORE_new();
|
||||
TableVal* root_certs2 = root_certs->AsTableVal();
|
||||
ListVal* idxs = root_certs2->ConvertToPureList();
|
||||
|
||||
// Build the validation store
|
||||
for ( i = 0; i < idxs->Length(); ++i )
|
||||
{
|
||||
Val* key = idxs->Index(i);
|
||||
StringVal *sv = root_certs2->Lookup(key)->AsStringVal();
|
||||
const uint8* data = sv->Bytes();
|
||||
X509* x = d2i_X509_(NULL, &data, sv->Len());
|
||||
if ( ! x )
|
||||
{
|
||||
builtin_error(fmt("Root CA error: %s", ERR_error_string(ERR_peek_last_error(),NULL)));
|
||||
return x509_error_record((uint64) ERR_get_error(), ERR_error_string(ERR_peek_last_error(),NULL));
|
||||
}
|
||||
|
||||
X509_STORE_add_cert(ctx, x);
|
||||
}
|
||||
|
||||
delete idxs;
|
||||
|
||||
// Save the newly constructed certificate store into the cacheing map.
|
||||
x509_stores[root_certs] = ctx;
|
||||
}
|
||||
|
||||
X509* cert = cert_handle->GetCertificate();
|
||||
if ( ! cert )
|
||||
{
|
||||
builtin_error(fmt("No certificate in opaque"));
|
||||
return x509_error_record(-1, "No certificate in opaque");
|
||||
}
|
||||
|
||||
STACK_OF(X509)* untrusted_certs = sk_X509_new_null();
|
||||
if ( ! untrusted_certs )
|
||||
{
|
||||
builtin_error(fmt("Untrusted certificate stack initialization error: %s", ERR_error_string(ERR_peek_last_error(),NULL)));
|
||||
return x509_error_record((uint64) ERR_get_error(), ERR_error_string(ERR_peek_last_error(),NULL));
|
||||
}
|
||||
|
||||
for ( i = 1; i < (int) certs_vec->Size(); ++i ) // start at 1 - 0 is host cert
|
||||
{
|
||||
Val *sv = certs_vec->Lookup(i);
|
||||
// Fixme: check type
|
||||
X509* x = ((file_analysis::X509Val*) sv)->GetCertificate();
|
||||
if ( ! x )
|
||||
{
|
||||
sk_X509_pop(untrusted_certs);
|
||||
builtin_error(fmt("No certificate in opaque in stack"));
|
||||
return x509_error_record(-1, "No certificate in opaque");
|
||||
}
|
||||
|
||||
sk_X509_push(untrusted_certs, x);
|
||||
}
|
||||
|
||||
X509_STORE_CTX csc;
|
||||
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);
|
||||
|
||||
VectorVal* chainVector = 0;
|
||||
|
||||
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 )
|
||||
{
|
||||
reporter->Error("Encountered valid chain that could not be resolved");
|
||||
goto x509_verify_chainerror;
|
||||
}
|
||||
|
||||
int num_certs = sk_X509_num(chain);
|
||||
chainVector = new VectorVal(internal_type("x509_opaque_vector")->AsVectorType());
|
||||
|
||||
for ( int i = 0; i < num_certs; i++ )
|
||||
{
|
||||
X509* currcert = sk_X509_value(chain, i);
|
||||
if ( !currcert )
|
||||
{
|
||||
reporter->InternalError("OpenSSL returned null certificate");
|
||||
goto x509_verify_chainerror;
|
||||
}
|
||||
|
||||
chainVector->Assign(i, new file_analysis::X509Val(currcert)); // X509Val takes ownership
|
||||
}
|
||||
}
|
||||
|
||||
x509_verify_chainerror:
|
||||
|
||||
X509_STORE_CTX_cleanup(&csc);
|
||||
|
||||
if ( untrusted_certs )
|
||||
sk_X509_pop(untrusted_certs);
|
||||
|
||||
RecordVal* rrecord = new RecordVal(BifType::Record::X509::Result);
|
||||
|
||||
rrecord->Assign(0, new Val((uint64) csc.error, TYPE_COUNT));
|
||||
rrecord->Assign(1, new StringVal(X509_verify_cert_error_string(csc.error)));
|
||||
|
||||
if ( chainVector )
|
||||
rrecord->Assign(2, chainVector);
|
||||
|
||||
return rrecord;
|
||||
%}
|
5
src/file_analysis/analyzer/x509/types.bif
Normal file
5
src/file_analysis/analyzer/x509/types.bif
Normal file
|
@ -0,0 +1,5 @@
|
|||
type X509::Certificate: record;
|
||||
type X509::Extension: record;
|
||||
type X509::BasicConstraints: record;
|
||||
type X509::SubjectAlternativeName: record;
|
||||
type X509::Result: record;
|
Loading…
Add table
Add a link
Reference in a new issue