Merge remote-tracking branch 'origin/topic/johanna/ocsp-sct-validate'

Closes #1830.

* origin/topic/johanna/ocsp-sct-validate: (82 commits)
  Tiny script changes for SSL.
  Update CT Log list
  SSL: Update OCSP/SCT scripts and documentation.
  Revert "add parameter 'status_type' to event ssl_stapled_ocsp"
  Revert "parse multiple OCSP stapling responses"
  SCT: Fix script error when mime type of file unknown.
  SCT: another memory leak in SCT parsing.
  SCT validation: fix small memory leak (public keys were not freed)
  Change end-of-connection handling for validation
  OCSP/TLS/SCT: Fix a number of test failures.
  SCT Validate: make caching a bit less aggressive.
  SSL: Fix type of ssl validation result
  TLS-SCT: compile on old versions of OpenSSL (1.0.1...)
  SCT: Add caching support for validation
  SCT: Add signed certificate timestamp validation script.
  SCT: Allow verification of SCTs in Certs.
  SCT: only compare correct OID/NID for Cert/OCSP.
  SCT: add validation of proofs for extensions and OCSP.
  SCT: pass timestamp as uint64 instead of time
  Add CT log information to Bro
  ...
This commit is contained in:
Robin Sommer 2017-07-30 08:49:41 -07:00
commit faa4150154
86 changed files with 2672 additions and 445 deletions

View file

@ -55,6 +55,7 @@ int File::bof_buffer_size_idx = -1;
int File::bof_buffer_idx = -1;
int File::meta_mime_type_idx = -1;
int File::meta_mime_types_idx = -1;
int File::meta_inferred_idx = -1;
void File::StaticInit()
{
@ -76,6 +77,7 @@ void File::StaticInit()
bof_buffer_idx = Idx("bof_buffer", fa_file_type);
meta_mime_type_idx = Idx("mime_type", fa_metadata_type);
meta_mime_types_idx = Idx("mime_types", fa_metadata_type);
meta_inferred_idx = Idx("inferred", fa_metadata_type);
}
File::File(const string& file_id, const string& source_name, Connection* conn,
@ -290,6 +292,27 @@ void File::SetReassemblyBuffer(uint64 max)
reassembly_max_buffer = max;
}
bool File::SetMime(const string& mime_type)
{
if ( mime_type.empty() || bof_buffer.size != 0 )
return false;
did_metadata_inference = true;
bof_buffer.full = true;
if ( ! FileEventAvailable(file_sniff) )
return false;
val_list* vl = new val_list();
vl->append(val->Ref());
RecordVal* meta = new RecordVal(fa_metadata_type);
vl->append(meta);
meta->Assign(meta_mime_type_idx, new StringVal(mime_type));
meta->Assign(meta_inferred_idx, new Val(0, TYPE_BOOL));
FileEvent(file_sniff, vl);
return true;
}
void File::InferMetadata()
{
did_metadata_inference = true;

View file

@ -171,6 +171,25 @@ public:
*/
void FileEvent(EventHandlerPtr h, val_list* vl);
/**
* Sets the MIME type for a file to a specific value.
*
* Setting the MIME type has to be done before the MIME type is
* inferred from the content. After a MIME type has been set once,
* it cannot be changed anymore.
*
* This function should only be called when it does not make sense
* to perform automated MIME type detections. This is e.g. the case
* in protocols where the file type is fixed in the protocol description.
* This is for example the case for TLS and X.509 certificates.
*
* @param mime_type mime type to set
* @return true if the mime type was set. False if it could not be set because
* a mime type was already set or inferred.
*/
bool SetMime(const string& mime_type);
protected:
friend class Manager;
friend class FileReassembler;
@ -319,6 +338,7 @@ protected:
static int bof_buffer_idx;
static int mime_type_idx;
static int mime_types_idx;
static int meta_inferred_idx;
static int meta_mime_type_idx;
static int meta_mime_types_idx;

View file

@ -110,7 +110,7 @@ void Manager::SetHandle(const string& handle)
string Manager::DataIn(const u_char* data, uint64 len, uint64 offset,
analyzer::Tag tag, Connection* conn, bool is_orig,
const string& precomputed_id)
const string& precomputed_id, const string& mime_type)
{
string id = precomputed_id.empty() ? GetFileID(tag, conn, is_orig) : precomputed_id;
File* file = GetFile(id, conn, tag, is_orig);
@ -118,6 +118,9 @@ string Manager::DataIn(const u_char* data, uint64 len, uint64 offset,
if ( ! file )
return "";
if ( ! mime_type.empty() )
file->SetMime(mime_type);
file->DataIn(data, len, offset);
if ( file->IsComplete() )
@ -130,7 +133,8 @@ string Manager::DataIn(const u_char* data, uint64 len, uint64 offset,
}
string Manager::DataIn(const u_char* data, uint64 len, analyzer::Tag tag,
Connection* conn, bool is_orig, const string& precomputed_id)
Connection* conn, bool is_orig, const string& precomputed_id,
const string& mime_type)
{
string id = precomputed_id.empty() ? GetFileID(tag, conn, is_orig) : precomputed_id;
// Sequential data input shouldn't be going over multiple conns, so don't
@ -140,6 +144,9 @@ string Manager::DataIn(const u_char* data, uint64 len, analyzer::Tag tag,
if ( ! file )
return "";
if ( ! mime_type.empty() )
file->SetMime(mime_type);
file->DataIn(data, len);
if ( file->IsComplete() )

View file

@ -93,6 +93,12 @@ public:
* or false if is being sent in the opposite direction.
* @param precomputed_file_id may be set to a previous return value in order to
* bypass costly file handle lookups.
* @param mime_type may be set to the mime type of the file, if already known due
* to the protocol. This is, e.g., the case in TLS connections where X.509
* certificates are passed as files; here the type of the file is set by
* the protocol. If this parameter is given, MIME type detection will be
* disabled.
* This parameter is only used for the first bit of data for each file.
* @return a unique file ID string which, in certain contexts, may be
* cached and passed back in to a subsequent function call in order
* to avoid costly file handle lookups (which have to go through
@ -101,7 +107,8 @@ public:
*/
std::string DataIn(const u_char* data, uint64 len, uint64 offset,
analyzer::Tag tag, Connection* conn, bool is_orig,
const std::string& precomputed_file_id = "");
const std::string& precomputed_file_id = "",
const std::string& mime_type = "");
/**
* Pass in sequential file data.
@ -113,6 +120,12 @@ public:
* or false if is being sent in the opposite direction.
* @param precomputed_file_id may be set to a previous return value in order to
* bypass costly file handle lookups.
* @param mime_type may be set to the mime type of the file, if already known due
* to the protocol. This is, e.g., the case in TLS connections where X.509
* certificates are passed as files; here the type of the file is set by
* the protocol. If this parameter is give, mime type detection will be
* disabled.
* This parameter is only used for the first bit of data for each file.
* @return a unique file ID string which, in certain contexts, may be
* cached and passed back in to a subsequent function call in order
* to avoid costly file handle lookups (which have to go through
@ -121,7 +134,8 @@ public:
*/
std::string DataIn(const u_char* data, uint64 len, analyzer::Tag tag,
Connection* conn, bool is_orig,
const std::string& precomputed_file_id = "");
const std::string& precomputed_file_id = "",
const std::string& mime_type = "");
/**
* Pass in sequential file data from external source (e.g. input framework).

View file

@ -5,6 +5,7 @@ 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_cc(X509Common.cc X509.cc OCSP.cc Plugin.cc)
bro_plugin_bif(events.bif types.bif functions.bif ocsp_events.bif)
bro_plugin_pac(x509-extension.pac x509-signed_certificate_timestamp.pac)
bro_plugin_end()

View file

@ -0,0 +1,403 @@
// See the file "COPYING" in the main distribution directory for copyright.
#include <string>
#include "OCSP.h"
#include "X509.h"
#include "Event.h"
#include "types.bif.h"
#include "ocsp_events.bif.h"
#include "file_analysis/Manager.h"
#include <openssl/x509.h>
#include <openssl/x509v3.h>
#include <openssl/asn1.h>
#include <openssl/opensslconf.h>
#include "file_analysis/analyzer/x509/X509.h"
// helper function of sk_X509_value to avoid namespace problem
// sk_X509_value(X,Y) = > SKM_sk_value(X509,X,Y)
// X509 => file_analysis::X509
X509 *helper_sk_X509_value(STACK_OF(X509) *certs, int i)
{
return sk_X509_value(certs, i);
}
using namespace file_analysis;
IMPLEMENT_SERIAL(OCSP_RESPVal, SER_OCSP_RESP_VAL);
#define OCSP_STRING_BUF_SIZE 2048
static Val* get_ocsp_type(RecordVal* args, const char* name)
{
Val* rval = args->Lookup(name);
if ( ! rval )
reporter->Error("File extraction analyzer missing arg field: %s", name);
return rval;
}
static void OCSP_RESPID_bio(OCSP_RESPID *resp_id, BIO* bio)
{
if (resp_id->type == V_OCSP_RESPID_NAME)
X509_NAME_print_ex(bio, resp_id->value.byName, 0, XN_FLAG_ONELINE);
else if (resp_id->type == V_OCSP_RESPID_KEY)
i2a_ASN1_STRING(bio, resp_id->value.byKey, V_ASN1_OCTET_STRING);
}
void ocsp_add_cert_id(OCSP_CERTID *cert_id, val_list* vl, BIO* bio)
{
char buf[OCSP_STRING_BUF_SIZE];
memset(buf, 0, sizeof(buf));
i2a_ASN1_OBJECT(bio, cert_id->hashAlgorithm->algorithm);
int len = BIO_read(bio, buf, sizeof(buf));
vl->append(new StringVal(len, buf));
BIO_reset(bio);
i2a_ASN1_STRING(bio, cert_id->issuerNameHash, V_ASN1_OCTET_STRING);
len = BIO_read(bio, buf, sizeof(buf));
vl->append(new StringVal(len, buf));
BIO_reset(bio);
i2a_ASN1_STRING(bio, cert_id->issuerKeyHash, V_ASN1_OCTET_STRING);
len = BIO_read(bio, buf, sizeof(buf));
vl->append(new StringVal(len, buf));
BIO_reset(bio);
i2a_ASN1_INTEGER(bio, cert_id->serialNumber);
vl->append(new StringVal(len, buf));
BIO_reset(bio);
}
file_analysis::Analyzer* OCSP::InstantiateRequest(RecordVal* args, File* file)
{
return new OCSP(args, file, true);
}
file_analysis::Analyzer* OCSP::InstantiateReply(RecordVal* args, File* file)
{
return new OCSP(args, file, false);
}
file_analysis::OCSP::OCSP(RecordVal* args, file_analysis::File* file, bool arg_request)
: file_analysis::X509Common::X509Common(file_mgr->GetComponentTag("OCSP"), args, file), request(arg_request)
{
}
bool file_analysis::OCSP::DeliverStream(const u_char* data, uint64 len)
{
ocsp_data.append(reinterpret_cast<const char*>(data), len);
return true;
}
bool file_analysis::OCSP::Undelivered(uint64 offset, uint64 len)
{
return false;
}
// we parse the entire OCSP response in EOF, because we just pass it on
// to OpenSSL.
bool file_analysis::OCSP::EndOfFile()
{
const unsigned char* ocsp_char = reinterpret_cast<const unsigned char*>(ocsp_data.data());
if ( request )
{
OCSP_REQUEST *req = d2i_OCSP_REQUEST(NULL, &ocsp_char, ocsp_data.size());
if (!req)
{
reporter->Weird(fmt("OPENSSL Could not parse OCSP request (fuid %s)", GetFile()->GetID().c_str()));
return false;
}
ParseRequest(req, GetFile()->GetID().c_str());
OCSP_REQUEST_free(req);
}
else
{
OCSP_RESPONSE *resp = d2i_OCSP_RESPONSE(NULL, &ocsp_char, ocsp_data.size());
if (!resp)
{
reporter->Weird(fmt("OPENSSL Could not parse OCSP response (fuid %s)", GetFile()->GetID().c_str()));
return false;
}
OCSP_RESPVal* resp_val = new OCSP_RESPVal(resp); // resp_val takes ownership
ParseResponse(resp_val, GetFile()->GetID().c_str());
Unref(resp_val);
}
return true;
}
void file_analysis::OCSP::ParseRequest(OCSP_REQUEST *req, const char* fid)
{
OCSP_REQINFO *inf = req->tbsRequest;
char buf[OCSP_STRING_BUF_SIZE]; // we need a buffer for some of the openssl functions
memset(buf, 0, sizeof(buf));
// build up our response as we go along...
val_list* vl = new val_list();
vl->append(GetFile()->GetVal()->Ref());
vl->append(new Val((uint64)ASN1_INTEGER_get(inf->version), TYPE_COUNT));
BIO *bio = BIO_new(BIO_s_mem());
if (inf->requestorName != NULL)
{
GENERAL_NAME_print(bio, inf->requestorName);
int len = BIO_read(bio, buf, sizeof(buf));
vl->append(new StringVal(len, buf));
BIO_reset(bio);
}
else
vl->append(new StringVal(0, ""));
mgr.QueueEvent(ocsp_request, vl);
int req_count = OCSP_request_onereq_count(req);
for ( int i=0; i<req_count; i++ )
{
val_list* rvl = new val_list();
rvl->append(GetFile()->GetVal()->Ref());
OCSP_ONEREQ *one_req = OCSP_request_onereq_get0(req, i);
OCSP_CERTID *cert_id = OCSP_onereq_get0_id(one_req);
ocsp_add_cert_id(cert_id, rvl, bio);
mgr.QueueEvent(ocsp_request_certificate, rvl);
}
BIO_free(bio);
}
void file_analysis::OCSP::ParseResponse(OCSP_RESPVal *resp_val, const char* fid)
{
OCSP_RESPONSE *resp = resp_val->GetResp();
OCSP_RESPBYTES *resp_bytes = resp->responseBytes;
OCSP_BASICRESP *basic_resp = nullptr;
OCSP_RESPDATA *resp_data = nullptr;
OCSP_RESPID *resp_id = nullptr;
int resp_count, num_ext = 0;
VectorVal *certs_vector = nullptr;
int len = 0;
char buf[OCSP_STRING_BUF_SIZE];
memset(buf, 0, sizeof(buf));
val_list* vl = new val_list();
vl->append(GetFile()->GetVal()->Ref());
const char *status_str = OCSP_response_status_str(OCSP_response_status(resp));
StringVal* status_val = new StringVal(strlen(status_str), status_str);
vl->append(status_val->Ref());
mgr.QueueEvent(ocsp_response_status, vl);
vl = nullptr;
if (!resp_bytes)
{
Unref(status_val);
return;
}
BIO *bio = BIO_new(BIO_s_mem());
//i2a_ASN1_OBJECT(bio, resp_bytes->responseType);
//int len = BIO_read(bio, buf, sizeof(buf));
//BIO_reset(bio);
// get the basic response
basic_resp = OCSP_response_get1_basic(resp);
if ( !basic_resp )
goto clean_up;
resp_data = basic_resp->tbsResponseData;
if ( !resp_data )
goto clean_up;
vl = new val_list();
vl->append(GetFile()->GetVal()->Ref());
vl->append(resp_val->Ref());
vl->append(status_val);
vl->append(new Val((uint64)ASN1_INTEGER_get(resp_data->version), TYPE_COUNT));
// responderID
resp_id = resp_data->responderId;
OCSP_RESPID_bio(resp_id, bio);
len = BIO_read(bio, buf, sizeof(buf));
vl->append(new StringVal(len, buf));
BIO_reset(bio);
// producedAt
vl->append(new Val(GetTimeFromAsn1(resp_data->producedAt, fid, reporter), TYPE_TIME));
// responses
resp_count = sk_OCSP_SINGLERESP_num(resp_data->responses);
for ( int i=0; i<resp_count; i++ )
{
OCSP_SINGLERESP *single_resp = sk_OCSP_SINGLERESP_value(resp_data->responses, i);
if ( !single_resp )
continue;
val_list* rvl = new val_list();
rvl->append(GetFile()->GetVal()->Ref());
// cert id
OCSP_CERTID *cert_id = single_resp->certId;
ocsp_add_cert_id(cert_id, rvl, bio);
BIO_reset(bio);
// certStatus
OCSP_CERTSTATUS *cert_status = single_resp->certStatus;
const char* cert_status_str = OCSP_cert_status_str(cert_status->type);
rvl->append(new StringVal(strlen(cert_status_str), cert_status_str));
// revocation time and reason if revoked
if ( cert_status->type == V_OCSP_CERTSTATUS_REVOKED )
{
OCSP_REVOKEDINFO *revoked_info = cert_status->value.revoked;
rvl->append(new Val(GetTimeFromAsn1(revoked_info->revocationTime, fid, reporter), TYPE_TIME));
if ( revoked_info->revocationReason )
{
const char* revoke_reason = OCSP_crl_reason_str(ASN1_ENUMERATED_get(revoked_info->revocationReason));
rvl->append(new StringVal(strlen(revoke_reason), revoke_reason));
}
else
rvl->append(new StringVal(0, ""));
}
else
{
rvl->append(new Val(0, TYPE_TIME));
rvl->append(new StringVal(0, ""));
}
rvl->append(new Val(GetTimeFromAsn1(single_resp->thisUpdate, fid, reporter), TYPE_TIME));
if ( single_resp->nextUpdate )
rvl->append(new Val(GetTimeFromAsn1(single_resp->nextUpdate, fid, reporter), TYPE_TIME));
else
rvl->append(new Val(0, TYPE_TIME));
mgr.QueueEvent(ocsp_response_certificate, rvl);
num_ext = OCSP_SINGLERESP_get_ext_count(single_resp);
for ( int k = 0; k < num_ext; ++k )
{
X509_EXTENSION* ex = OCSP_SINGLERESP_get_ext(single_resp, k);
if ( ! ex )
continue;
ParseExtension(ex, ocsp_extension, false);
}
}
i2a_ASN1_OBJECT(bio, basic_resp->signatureAlgorithm->algorithm);
len = BIO_read(bio, buf, sizeof(buf));
vl->append(new StringVal(len, buf));
BIO_reset(bio);
//i2a_ASN1_OBJECT(bio, basic_resp->signature);
//len = BIO_read(bio, buf, sizeof(buf));
//ocsp_resp_record->Assign(7, new StringVal(len, buf));
//BIO_reset(bio);
certs_vector = new VectorVal(internal_type("x509_opaque_vector")->AsVectorType());
vl->append(certs_vector);
if ( basic_resp->certs )
{
int num_certs = sk_X509_num(basic_resp->certs);
for ( int i=0; i<num_certs; i++ )
{
::X509 *this_cert = X509_dup(helper_sk_X509_value(basic_resp->certs, i));
//::X509 *this_cert = X509_dup(sk_X509_value(basic_resp->certs, i));
if (this_cert)
certs_vector->Assign(i, new file_analysis::X509Val(this_cert));
else
reporter->Weird("OpenSSL returned null certificate");
}
}
mgr.QueueEvent(ocsp_response_bytes, vl);
// ok, now that we are done with the actual certificate - let's parse extensions :)
num_ext = OCSP_BASICRESP_get_ext_count(basic_resp);
for ( int k = 0; k < num_ext; ++k )
{
X509_EXTENSION* ex = OCSP_BASICRESP_get_ext(basic_resp, k);
if ( ! ex )
continue;
ParseExtension(ex, ocsp_extension, true);
}
clean_up:
if (basic_resp)
OCSP_BASICRESP_free(basic_resp);
BIO_free(bio);
}
void file_analysis::OCSP::ParseExtensionsSpecific(X509_EXTENSION* ex, bool global, ASN1_OBJECT* ext_asn, const char* oid)
{
#ifdef NID_ct_cert_scts
if ( OBJ_obj2nid(ext_asn) == NID_ct_cert_scts )
#else
if ( strcmp(oid, "1.3.6.1.4.1.11129.2.4.5") == 0 )
#endif
ParseSignedCertificateTimestamps(ex);
}
OCSP_RESPVal::OCSP_RESPVal(OCSP_RESPONSE* arg_ocsp_resp) : OpaqueVal(ocsp_resp_opaque_type)
{
ocsp_resp = arg_ocsp_resp;
}
OCSP_RESPVal::OCSP_RESPVal() : OpaqueVal(ocsp_resp_opaque_type)
{
ocsp_resp = nullptr;
}
OCSP_RESPVal::~OCSP_RESPVal()
{
if (ocsp_resp)
OCSP_RESPONSE_free(ocsp_resp);
}
OCSP_RESPONSE* OCSP_RESPVal::GetResp() const
{
return ocsp_resp;
}
bool OCSP_RESPVal::DoSerialize(SerialInfo* info) const
{
DO_SERIALIZE(SER_OCSP_RESP_VAL, OpaqueVal);
unsigned char *buf = nullptr;
int length = i2d_OCSP_RESPONSE(ocsp_resp, &buf);
if ( length < 0 )
return false;
bool res = SERIALIZE_STR(reinterpret_cast<const char*>(buf), length);
OPENSSL_free(buf);
return res;
}
bool OCSP_RESPVal::DoUnserialize(UnserialInfo* info)
{
DO_UNSERIALIZE(OpaqueVal)
int length;
unsigned char *ocsp_resp_buf, *opensslbuf;
if ( ! UNSERIALIZE_STR(reinterpret_cast<char **>(&ocsp_resp_buf), &length) )
return false;
opensslbuf = ocsp_resp_buf; // OpenSSL likes to shift pointers around. really.
ocsp_resp = d2i_OCSP_RESPONSE(nullptr, const_cast<const unsigned char**>(&opensslbuf), length);
delete [] ocsp_resp_buf;
if ( ! ocsp_resp )
return false;
return true;
}

View file

@ -0,0 +1,54 @@
// See the file "COPYING" in the main distribution directory for copyright.
#ifndef FILE_ANALYSIS_OCSP_H
#define FILE_ANALYSIS_OCSP_H
#include <string>
#include "Val.h"
#include "../File.h"
#include "Analyzer.h"
#include "X509Common.h"
#include <openssl/ocsp.h>
namespace file_analysis {
class OCSP_RESPVal;
class OCSP : public file_analysis::X509Common {
public:
bool DeliverStream(const u_char* data, uint64 len) override;
bool Undelivered(uint64 offset, uint64 len) override;
bool EndOfFile() override;
static file_analysis::Analyzer* InstantiateRequest(RecordVal* args, File* file);
static file_analysis::Analyzer* InstantiateReply(RecordVal* args, File* file);
protected:
OCSP(RecordVal* args, File* file, bool request);
private:
void ParseResponse(OCSP_RESPVal *, const char* fid = 0);
void ParseRequest(OCSP_REQUEST *, const char* fid = 0);
void ParseExtensionsSpecific(X509_EXTENSION* ex, bool, ASN1_OBJECT*, const char*) override;
std::string ocsp_data;
bool request = false; // true if ocsp request, false if reply
};
class OCSP_RESPVal: public OpaqueVal {
public:
explicit OCSP_RESPVal(OCSP_RESPONSE *);
~OCSP_RESPVal();
OCSP_RESPONSE *GetResp() const;
protected:
OCSP_RESPVal();
private:
OCSP_RESPONSE *ocsp_resp;
DECLARE_SERIAL(OCSP_RESPVal);
};
}
#endif

View file

@ -4,6 +4,7 @@
#include "plugin/Plugin.h"
#include "X509.h"
#include "OCSP.h"
namespace plugin {
namespace Bro_X509 {
@ -13,10 +14,12 @@ public:
plugin::Configuration Configure()
{
AddComponent(new ::file_analysis::Component("X509", ::file_analysis::X509::Instantiate));
AddComponent(new ::file_analysis::Component("OCSP_REQUEST", ::file_analysis::OCSP::InstantiateRequest));
AddComponent(new ::file_analysis::Component("OCSP_REPLY", ::file_analysis::OCSP::InstantiateReply));
plugin::Configuration config;
config.name = "Bro::X509";
config.description = "X509 analyzer";
config.description = "X509 and OCSP analyzer";
return config;
}
} plugin;

View file

@ -21,7 +21,7 @@ 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)
: file_analysis::X509Common::X509Common(file_mgr->GetComponentTag("X509"), args, file)
{
cert_data.clear();
}
@ -72,7 +72,7 @@ bool file_analysis::X509::EndOfFile()
if ( ! ex )
continue;
ParseExtension(ex);
ParseExtension(ex, x509_extension, false);
}
// X509_free(ssl_cert); We do _not_ free the certificate here. It is refcounted
@ -133,8 +133,8 @@ RecordVal* file_analysis::X509::ParseCertificate(X509Val* cert_val, const char*
pX509Cert->Assign(3, new StringVal(len, buf));
BIO_free(bio);
pX509Cert->Assign(5, new Val(GetTimeFromAsn1(X509_get_notBefore(ssl_cert), fid), TYPE_TIME));
pX509Cert->Assign(6, new Val(GetTimeFromAsn1(X509_get_notAfter(ssl_cert), fid), TYPE_TIME));
pX509Cert->Assign(5, new Val(GetTimeFromAsn1(X509_get_notBefore(ssl_cert), fid, reporter), TYPE_TIME));
pX509Cert->Assign(6, new Val(GetTimeFromAsn1(X509_get_notAfter(ssl_cert), fid, reporter), 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,
@ -205,101 +205,6 @@ RecordVal* file_analysis::X509::ParseCertificate(X509Val* cert_val, const char*
return pX509Cert;
}
StringVal* file_analysis::X509::GetExtensionFromBIO(BIO* bio)
{
BIO_flush(bio);
ERR_clear_error();
int length = BIO_pending(bio);
if ( ERR_peek_error() != 0 )
{
char tmp[120];
ERR_error_string_n(ERR_get_error(), tmp, sizeof(tmp));
reporter->Weird(fmt("X509::GetExtensionFromBIO: %s", tmp));
BIO_free_all(bio);
return 0;
}
if ( length == 0 )
{
BIO_free_all(bio);
return new StringVal("");
}
char* buffer = (char*) malloc(length);
if ( ! buffer )
{
// Just emit an error here and try to continue instead of aborting
// because it's unclear the length value is very reliable.
reporter->Error("X509::GetExtensionFromBIO malloc(%d) failed", length);
BIO_free_all(bio);
return 0;
}
BIO_read(bio, (void*) buffer, length);
StringVal* ext_val = new StringVal(length, buffer);
free(buffer);
BIO_free_all(bio);
return ext_val;
}
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);
StringVal* ext_val = GetExtensionFromBIO(bio);
if ( ! ext_val )
ext_val = new StringVal(0, "");
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);
@ -326,6 +231,23 @@ void file_analysis::X509::ParseBasicConstraints(X509_EXTENSION* ex)
reporter->Weird(fmt("Certificate with invalid BasicConstraint. fuid %s", GetFile()->GetID().c_str()));
}
void file_analysis::X509::ParseExtensionsSpecific(X509_EXTENSION* ex, bool global, ASN1_OBJECT* ext_asn, const char* oid)
{
// 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);
#ifdef NID_ct_cert_scts
else if ( OBJ_obj2nid(ext_asn) == NID_ct_precert_scts )
#else
else if ( strcmp(oid, "1.3.6.1.4.1.11129.2.4.2") == 0 )
#endif
ParseSignedCertificateTimestamps(ex);
}
void file_analysis::X509::ParseSAN(X509_EXTENSION* ext)
{
assert(OBJ_obj2nid(X509_EXTENSION_get_object(ext)) == NID_subject_alt_name);
@ -517,164 +439,6 @@ unsigned int file_analysis::X509::KeyLength(EVP_PKEY *key)
reporter->InternalError("cannot be reached");
}
double file_analysis::X509::GetTimeFromAsn1(const ASN1_TIME* atime, const char* arg_fid)
{
const char *fid = arg_fid ? arg_fid : "";
time_t lResult = 0;
char lBuffer[26];
char* pBuffer = lBuffer;
const char *pString = (const char *) atime->data;
unsigned int remaining = atime->length;
if ( atime->type == V_ASN1_UTCTIME )
{
if ( remaining < 11 || remaining > 17 )
{
reporter->Weird(fmt("Could not parse time in X509 certificate (fuid %s) -- UTCTime has wrong length", fid));
return 0;
}
if ( pString[remaining-1] != 'Z' )
{
// not valid according to RFC 2459 4.1.2.5.1
reporter->Weird(fmt("Could not parse UTC time in non-YY-format in X509 certificate (x509 %s)", fid));
return 0;
}
// year is first two digits in YY format. Buffer expects YYYY format.
if ( pString[0] < '5' ) // RFC 2459 4.1.2.5.1
{
*(pBuffer++) = '2';
*(pBuffer++) = '0';
}
else
{
*(pBuffer++) = '1';
*(pBuffer++) = '9';
}
memcpy(pBuffer, pString, 10);
pBuffer += 10;
pString += 10;
remaining -= 10;
}
else if ( atime->type == V_ASN1_GENERALIZEDTIME )
{
// generalized time. We apparently ignore the YYYYMMDDHH case
// for now and assume we always have minutes and seconds.
// This should be ok because it is specified as a requirement in RFC 2459 4.1.2.5.2
if ( remaining < 12 || remaining > 23 )
{
reporter->Weird(fmt("Could not parse time in X509 certificate (fuid %s) -- Generalized time has wrong length", fid));
return 0;
}
memcpy(pBuffer, pString, 12);
pBuffer += 12;
pString += 12;
remaining -= 12;
}
else
{
reporter->Weird(fmt("Invalid time type in X509 certificate (fuid %s)", fid));
return 0;
}
if ( (remaining == 0) || (*pString == 'Z') || (*pString == '-') || (*pString == '+') )
{
*(pBuffer++) = '0';
*(pBuffer++) = '0';
}
else if ( remaining >= 2 )
{
*(pBuffer++) = *(pString++);
*(pBuffer++) = *(pString++);
remaining -= 2;
// Skip any fractional seconds...
if ( (remaining > 0) && (*pString == '.') )
{
pString++;
remaining--;
while ( (remaining > 0) && (*pString >= '0') && (*pString <= '9') )
{
pString++;
remaining--;
}
}
}
else
{
reporter->Weird(fmt("Could not parse time in X509 certificate (fuid %s) -- additional char after time", fid));
return 0;
}
*(pBuffer++) = 'Z';
*(pBuffer++) = '\0';
time_t lSecondsFromUTC;
if ( remaining == 0 || *pString == 'Z' )
lSecondsFromUTC = 0;
else
{
if ( remaining < 5 )
{
reporter->Weird(fmt("Could not parse time in X509 certificate (fuid %s) -- not enough bytes remaining for offset", fid));
return 0;
}
if ((*pString != '+') && (*pString != '-'))
{
reporter->Weird(fmt("Could not parse time in X509 certificate (fuid %s) -- unknown offset type", fid));
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[12] - '0') * 10) + (lBuffer[13] - '0');
lTime.tm_min = ((lBuffer[10] - '0') * 10) + (lBuffer[11] - '0');
lTime.tm_hour = ((lBuffer[8] - '0') * 10) + (lBuffer[9] - '0');
lTime.tm_mday = ((lBuffer[6] - '0') * 10) + (lBuffer[7] - '0');
lTime.tm_mon = (((lBuffer[4] - '0') * 10) + (lBuffer[5] - '0')) - 1;
lTime.tm_year = (lBuffer[0] - '0') * 1000 + (lBuffer[1] - '0') * 100 + ((lBuffer[2] - '0') * 10) + (lBuffer[3] - '0');
if ( lTime.tm_year > 1900)
lTime.tm_year -= 1900;
lTime.tm_wday = 0;
lTime.tm_yday = 0;
lTime.tm_isdst = 0; // No DST adjustment requested
lResult = mktime(&lTime);
if ( lResult )
{
if ( lTime.tm_isdst != 0 )
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;

View file

@ -6,21 +6,18 @@
#include <string>
#include "Val.h"
#include "../File.h"
#include "Analyzer.h"
#include "X509Common.h"
#include <openssl/x509.h>
#include <openssl/asn1.h>
namespace file_analysis {
class X509Val;
class X509 : public file_analysis::Analyzer {
class X509 : public file_analysis::X509Common {
public:
virtual bool DeliverStream(const u_char* data, uint64 len);
virtual bool Undelivered(uint64 offset, uint64 len);
virtual bool EndOfFile();
bool DeliverStream(const u_char* data, uint64 len) override;
bool Undelivered(uint64 offset, uint64 len) override;
bool EndOfFile() override;
/**
* Converts an X509 certificate into a \c X509::Certificate record
@ -40,29 +37,17 @@ public:
static file_analysis::Analyzer* Instantiate(RecordVal* args, File* file)
{ return new X509(args, file); }
/**
* Retrieve an X509 extension value from an OpenSSL BIO to which it was
* written.
*
* @param bio the OpenSSL BIO to read. It will be freed by the function,
* including when an error occurs.
*
* @return The X509 extension value.
*/
static StringVal* GetExtensionFromBIO(BIO* bio);
protected:
X509(RecordVal* args, File* file);
private:
void ParseExtension(X509_EXTENSION* ex);
void ParseBasicConstraints(X509_EXTENSION* ex);
void ParseSAN(X509_EXTENSION* ex);
void ParseExtensionsSpecific(X509_EXTENSION* ex, bool, ASN1_OBJECT*, const char*) override;
std::string cert_data;
// Helpers for ParseCertificate.
static double GetTimeFromAsn1(const ASN1_TIME * atime, const char* fid);
static StringVal* KeyCurve(EVP_PKEY *key);
static unsigned int KeyLength(EVP_PKEY *key);
};

View file

@ -0,0 +1,316 @@
// See the file "COPYING" in the main distribution directory for copyright.
#include "X509Common.h"
#include "x509-extension_pac.h"
#include "events.bif.h"
#include "ocsp_events.bif.h"
#include "types.bif.h"
#include <openssl/x509.h>
#include <openssl/x509v3.h>
#include <openssl/asn1.h>
#include <openssl/opensslconf.h>
#include <openssl/err.h>
using namespace file_analysis;
X509Common::X509Common(file_analysis::Tag arg_tag, RecordVal* arg_args, File* arg_file)
: file_analysis::Analyzer(arg_tag, arg_args, arg_file)
{
}
double X509Common::GetTimeFromAsn1(const ASN1_TIME* atime, const char* arg_fid, Reporter* reporter)
{
const char *fid = arg_fid ? arg_fid : "";
time_t lResult = 0;
char lBuffer[26];
char* pBuffer = lBuffer;
const char *pString = (const char *) atime->data;
unsigned int remaining = atime->length;
if ( atime->type == V_ASN1_UTCTIME )
{
if ( remaining < 11 || remaining > 17 )
{
reporter->Weird(fmt("Could not parse time in X509 certificate (fuid %s) -- UTCTime has wrong length", fid));
return 0;
}
if ( pString[remaining-1] != 'Z' )
{
// not valid according to RFC 2459 4.1.2.5.1
reporter->Weird(fmt("Could not parse UTC time in non-YY-format in X509 certificate (x509 %s)", fid));
return 0;
}
// year is first two digits in YY format. Buffer expects YYYY format.
if ( pString[0] < '5' ) // RFC 2459 4.1.2.5.1
{
*(pBuffer++) = '2';
*(pBuffer++) = '0';
}
else
{
*(pBuffer++) = '1';
*(pBuffer++) = '9';
}
memcpy(pBuffer, pString, 10);
pBuffer += 10;
pString += 10;
remaining -= 10;
}
else if ( atime->type == V_ASN1_GENERALIZEDTIME )
{
// generalized time. We apparently ignore the YYYYMMDDHH case
// for now and assume we always have minutes and seconds.
// This should be ok because it is specified as a requirement in RFC 2459 4.1.2.5.2
if ( remaining < 12 || remaining > 23 )
{
reporter->Weird(fmt("Could not parse time in X509 certificate (fuid %s) -- Generalized time has wrong length", fid));
return 0;
}
memcpy(pBuffer, pString, 12);
pBuffer += 12;
pString += 12;
remaining -= 12;
}
else
{
reporter->Weird(fmt("Invalid time type in X509 certificate (fuid %s)", fid));
return 0;
}
if ( (remaining == 0) || (*pString == 'Z') || (*pString == '-') || (*pString == '+') )
{
*(pBuffer++) = '0';
*(pBuffer++) = '0';
}
else if ( remaining >= 2 )
{
*(pBuffer++) = *(pString++);
*(pBuffer++) = *(pString++);
remaining -= 2;
// Skip any fractional seconds...
if ( (remaining > 0) && (*pString == '.') )
{
pString++;
remaining--;
while ( (remaining > 0) && (*pString >= '0') && (*pString <= '9') )
{
pString++;
remaining--;
}
}
}
else
{
reporter->Weird(fmt("Could not parse time in X509 certificate (fuid %s) -- additional char after time", fid));
return 0;
}
*(pBuffer++) = 'Z';
*(pBuffer++) = '\0';
time_t lSecondsFromUTC;
if ( remaining == 0 || *pString == 'Z' )
lSecondsFromUTC = 0;
else
{
if ( remaining < 5 )
{
reporter->Weird(fmt("Could not parse time in X509 certificate (fuid %s) -- not enough bytes remaining for offset", fid));
return 0;
}
if ((*pString != '+') && (*pString != '-'))
{
reporter->Weird(fmt("Could not parse time in X509 certificate (fuid %s) -- unknown offset type", fid));
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[12] - '0') * 10) + (lBuffer[13] - '0');
lTime.tm_min = ((lBuffer[10] - '0') * 10) + (lBuffer[11] - '0');
lTime.tm_hour = ((lBuffer[8] - '0') * 10) + (lBuffer[9] - '0');
lTime.tm_mday = ((lBuffer[6] - '0') * 10) + (lBuffer[7] - '0');
lTime.tm_mon = (((lBuffer[4] - '0') * 10) + (lBuffer[5] - '0')) - 1;
lTime.tm_year = (lBuffer[0] - '0') * 1000 + (lBuffer[1] - '0') * 100 + ((lBuffer[2] - '0') * 10) + (lBuffer[3] - '0');
if ( lTime.tm_year > 1900)
lTime.tm_year -= 1900;
lTime.tm_wday = 0;
lTime.tm_yday = 0;
lTime.tm_isdst = 0; // No DST adjustment requested
lResult = mktime(&lTime);
if ( lResult )
{
if ( lTime.tm_isdst != 0 )
lResult -= 3600; // mktime may adjust for DST (OS dependent)
lResult += lSecondsFromUTC;
}
else
lResult = 0;
return lResult;
}
void file_analysis::X509Common::ParseSignedCertificateTimestamps(X509_EXTENSION* ext)
{
// Ok, signed certificate timestamps are a bit of an odd case out; we don't
// want to use the (basically nonexistant) OpenSSL functionality to parse them.
// Instead we have our own, self-written binpac parser to parse just them,
// which we will initialize here and tear down immediately again.
ASN1_OCTET_STRING* ext_val = X509_EXTENSION_get_data(ext);
// the octet string of the extension contains the octet string which in turn
// contains the SCT. Obviously.
unsigned char* ext_val_copy = (unsigned char*) OPENSSL_malloc(ext_val->length);
unsigned char* ext_val_second_pointer = ext_val_copy;
memcpy(ext_val_copy, ext_val->data, ext_val->length);
ASN1_OCTET_STRING* inner = d2i_ASN1_OCTET_STRING(NULL, (const unsigned char**) &ext_val_copy, ext_val->length);
if ( !inner )
{
reporter->Error("X509::ParseSignedCertificateTimestamps could not parse inner octet string");
return;
}
binpac::X509Extension::MockConnection* conn = new binpac::X509Extension::MockConnection(this);
binpac::X509Extension::SignedCertTimestampExt* interp = new binpac::X509Extension::SignedCertTimestampExt(conn);
try
{
interp->NewData(inner->data, inner->data + inner->length);
}
catch( const binpac::Exception& e )
{
// throw a warning or sth
reporter->Error("X509::ParseSignedCertificateTimestamps could not parse SCT");
}
M_ASN1_OCTET_STRING_free(inner);
OPENSSL_free(ext_val_second_pointer);
interp->FlowEOF();
delete interp;
delete conn;
}
void file_analysis::X509Common::ParseExtension(X509_EXTENSION* ex, EventHandlerPtr h, bool global)
{
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);
StringVal* ext_val = GetExtensionFromBIO(bio);
if ( ! ext_val )
ext_val = new StringVal(0, "");
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);
if ( h == ocsp_extension )
vl->append(new Val(global ? 1 : 0, TYPE_BOOL));
mgr.QueueEvent(h, vl);
// let individual analyzers parse more.
ParseExtensionsSpecific(ex, global, ext_asn, oid);
}
StringVal* file_analysis::X509Common::GetExtensionFromBIO(BIO* bio)
{
BIO_flush(bio);
ERR_clear_error();
int length = BIO_pending(bio);
if ( ERR_peek_error() != 0 )
{
char tmp[120];
ERR_error_string_n(ERR_get_error(), tmp, sizeof(tmp));
reporter->Weird(fmt("X509::GetExtensionFromBIO: %s", tmp));
BIO_free_all(bio);
return 0;
}
if ( length == 0 )
{
BIO_free_all(bio);
return new StringVal("");
}
char* buffer = (char*) malloc(length);
if ( ! buffer )
{
// Just emit an error here and try to continue instead of aborting
// because it's unclear the length value is very reliable.
reporter->Error("X509::GetExtensionFromBIO malloc(%d) failed", length);
BIO_free_all(bio);
return 0;
}
BIO_read(bio, (void*) buffer, length);
StringVal* ext_val = new StringVal(length, buffer);
free(buffer);
BIO_free_all(bio);
return ext_val;
}

View file

@ -0,0 +1,44 @@
// See the file "COPYING" in the main distribution directory for copyright.
// Common base class for the X509 and OCSP analyzer, which share a fair amount of
// code
#ifndef FILE_ANALYSIS_X509_COMMON
#define FILE_ANALYSIS_X509_COMMON
#include "../File.h"
#include "Analyzer.h"
#include <openssl/x509.h>
#include <openssl/asn1.h>
namespace file_analysis {
class X509Common : public file_analysis::Analyzer {
public:
virtual ~X509Common() {};
/**
* Retrieve an X509 extension value from an OpenSSL BIO to which it was
* written.
*
* @param bio the OpenSSL BIO to read. It will be freed by the function,
* including when an error occurs.
*
* @return The X509 extension value.
*/
static StringVal* GetExtensionFromBIO(BIO* bio);
static double GetTimeFromAsn1(const ASN1_TIME* atime, const char* arg_fid, Reporter* reporter);
protected:
X509Common(file_analysis::Tag arg_tag, RecordVal* arg_args, File* arg_file);
void ParseExtension(X509_EXTENSION* ex, EventHandlerPtr h, bool global);
void ParseSignedCertificateTimestamps(X509_EXTENSION* ext);
virtual void ParseExtensionsSpecific(X509_EXTENSION* ex, bool, ASN1_OBJECT*, const char*) = 0;
};
}
#endif /* FILE_ANALYSIS_X509_COMMON */

View file

@ -1,4 +1,4 @@
## Generated for encountered X509 certificates, e.g., in the clear SSL/TLS
## 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
@ -13,7 +13,7 @@
##
## .. bro:see:: x509_extension x509_ext_basic_constraints
## x509_ext_subject_alternative_name x509_parse x509_verify
## x509_get_certificate_string
## x509_get_certificate_string x509_ocsp_ext_signed_certificate_timestamp
event x509_certificate%(f: fa_file, cert_ref: opaque of x509, cert: X509::Certificate%);
## Generated for X509 extensions seen in a certificate.
@ -27,7 +27,7 @@ event x509_certificate%(f: fa_file, cert_ref: opaque of x509, cert: X509::Certif
##
## .. bro:see:: x509_certificate x509_ext_basic_constraints
## x509_ext_subject_alternative_name x509_parse x509_verify
## x509_get_certificate_string
## x509_get_certificate_string x509_ocsp_ext_signed_certificate_timestamp
event x509_extension%(f: fa_file, ext: X509::Extension%);
## Generated for the X509 basic constraints extension seen in a certificate.
@ -39,7 +39,7 @@ event x509_extension%(f: fa_file, ext: X509::Extension%);
##
## .. bro:see:: x509_certificate x509_extension
## x509_ext_subject_alternative_name x509_parse x509_verify
## x509_get_certificate_string
## x509_get_certificate_string x509_ocsp_ext_signed_certificate_timestamp
event x509_ext_basic_constraints%(f: fa_file, ext: X509::BasicConstraints%);
## Generated for the X509 subject alternative name extension seen in a certificate.
@ -52,6 +52,34 @@ event x509_ext_basic_constraints%(f: fa_file, ext: X509::BasicConstraints%);
## ext: The parsed subject alternative name extension.
##
## .. bro:see:: x509_certificate x509_extension x509_ext_basic_constraints
## x509_parse x509_verify
## x509_parse x509_verify x509_ocsp_ext_signed_certificate_timestamp
## x509_get_certificate_string
event x509_ext_subject_alternative_name%(f: fa_file, ext: X509::SubjectAlternativeName%);
## Generated for the signed_certificate_timestamp X509 extension as defined in
## :rfc:`6962`. The extension is used to transmit signed proofs that are
## used for Certificate Transparency. Raised when the extension is encountered
## in an X.509 certificate or in an OCSP reply.
##
## f: The file.
##
## version: the version of the protocol to which the SCT conforms. Always
## should be 0 (representing version 1)
##
## logid: 32 bit key id
##
## timestamp: the NTP Time when the entry was logged measured since
## the epoch, ignoring leap seconds, in milliseconds.
##
## signature_and_hashalgorithm: signature and hash algorithm used for the
## digitally_signed struct
##
## signature: signature part of the digitally_signed struct
##
## .. bro:see:: ssl_extension_signed_certificate_timestamp x509_extension x509_ext_basic_constraints
## x509_parse x509_verify x509_ext_subject_alternative_name
## x509_get_certificate_string ssl_extension_signed_certificate_timestamp
## sct_verify ocsp_request ocsp_request_certificate ocsp_response_status
## ocsp_response_bytes ocsp_response_certificate
## x509_ocsp_ext_signed_certificate_timestamp
event x509_ocsp_ext_signed_certificate_timestamp%(f: fa_file, version: count, logid: string, timestamp: count, hash_algorithm: count, signature_algorithm: count, signature: string%);

View file

@ -1,6 +1,7 @@
%%{
#include "file_analysis/analyzer/x509/X509.h"
#include "types.bif.h"
#include "net_util.h"
#include <openssl/x509.h>
#include <openssl/asn1.h>
@ -139,6 +140,35 @@ X509* x509_get_ocsp_signer(STACK_OF(X509) *certs, OCSP_RESPID *rid)
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;
}
}
%%}
## Parses a certificate into an X509::Certificate structure.
@ -455,7 +485,7 @@ x509_ocsp_cleanup:
##
## .. bro:see:: x509_certificate x509_extension x509_ext_basic_constraints
## x509_ext_subject_alternative_name x509_parse
## x509_get_certificate_string x509_ocsp_verify
## 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 = x509_get_root_store(root_certs->AsTableVal());
@ -542,3 +572,297 @@ x509_verify_chainerror:
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.
##
## .. bro: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);
file_analysis::X509Val* h = (file_analysis::X509Val*) cert;
X509* x = ((file_analysis::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)
{
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<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));
#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 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<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.
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);
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 new Val(success, TYPE_BOOL);
sct_verify_err:
if (mdctx)
EVP_MD_CTX_destroy(mdctx);
if (key)
EVP_PKEY_free(key);
reporter->Error("%s", errstr.c_str());
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));
}
%%}
## 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.
##
## .. bro: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
%{
file_analysis::X509Val *cert_handle = (file_analysis::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.
##
## .. bro: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
%{
file_analysis::X509Val *cert_handle = (file_analysis::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.
##
## .. bro: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
%{
file_analysis::X509Val *cert_handle = (file_analysis::X509Val *) cert;
return x509_entity_hash(cert_handle, hash_alg, 2);
%}

View file

@ -0,0 +1,120 @@
## Event that is raised when encountering an OCSP request, e.g. in an HTTP
## connection. See :rfc:`6960` for more details.
##
## This event is raised exactly once for each OCSP Request.
##
## f: The file.
##
## req: version: the version of the OCSP request. Typically 0 (Version 1).
##
## requestorName: name of the OCSP requestor. This attribute is optional; if
## it is not set, an empty string is returned here.
##
## .. bro:see:: ocsp_request_certificate ocsp_response_status
## ocsp_response_bytes ocsp_response_certificate ocsp_extension
## x509_ocsp_ext_signed_certificate_timestamp
event ocsp_request%(f: fa_file, version: count, requestorName: string%);
## Event that is raised when encountering an OCSP request for a certificate,
## e.g. in an HTTP connection. See :rfc:`6960` for more details.
##
## Note that a single OCSP request can contain requests for several certificates.
## Thus this event can fire several times for one OCSP request, each time
## requesting information for a different (or in theory even the same) certificate.
##
## f: The file.
##
## hashAlgorithm: The hash algorithm used for the issuerKeyHash.
##
## issuerKeyHash: Hash of the issuers public key.
##
## serialNumber: Serial number of the certificate for which the status is requested.
##
## .. bro:see:: ocsp_request ocsp_response_status
## ocsp_response_bytes ocsp_response_certificate ocsp_extension
## x509_ocsp_ext_signed_certificate_timestamp
event ocsp_request_certificate%(f: fa_file, hashAlgorithm: string, issuerNameHash: string, issuerKeyHash: string, serialNumber: string%);
## This event is raised when encountering an OCSP reply, e.g. in an HTTP
## connection or a TLS extension. See :rfc:`6960` for more details.
##
## This event is raised exactly once for each OCSP reply.
##
## f: The file.
##
## status: The status of the OCSP response (e.g. succesful, malformedRequest, tryLater).
##
## .. bro:see:: ocsp_request ocsp_request_certificate
## ocsp_response_bytes ocsp_response_certificate ocsp_extension
## x509_ocsp_ext_signed_certificate_timestamp
event ocsp_response_status%(f: fa_file, status: string%);
## This event is raised when encountering an OCSP response that contains response information.
## An OCSP reply can be encountered, for example, in an HTTP connection or
## a TLS extension. See :rfc:`6960` for more details on OCSP.
##
## f: The file.
##
## req_ref: An opaque pointer to the underlying OpenSSL data structure of the
## OCSP response.
##
## status: The status of the OCSP response (e.g. succesful, malformedRequest, tryLater).
##
## version: Version of the OCSP response (typically - for version 1).
##
## responderId: The id of the OCSP responder; either a public key hash or a distinguished name.
##
## producedAt: Time at which the reply was produced.
##
## signatureAlgorithm: Algorithm used for the OCSP signature.
##
## certs: Optional list of certificates that are sent with the OCSP response; these typically
## are needed to perform validation of the reply.
##
## .. bro:see:: ocsp_request ocsp_request_certificate ocsp_response_status
## ocsp_response_certificate ocsp_extension
## x509_ocsp_ext_signed_certificate_timestamp
event ocsp_response_bytes%(f: fa_file, resp_ref: opaque of ocsp_resp, status: string, version: count, responderId: string, producedAt: time, signatureAlgorithm: string, certs: x509_opaque_vector%);
## This event is raised for each SingleResponse contained in an OCSP response.
## See :rfc:`6960` for more details on OCSP.
##
## f: The file.
##
## hashAlgorithm: The hash algorithm used for issuerNameHash and issuerKeyHash.
##
## issuerNameHash: Hash of the issuer's distinguished name.
##
## issuerKeyHash: Hash of the issuer's public key.
##
## serialNumber: Serial number of the affected certificate.
##
## certStatus: Status of the certificate.
##
## revokeTime: Time the certificate was revoked, 0 if not revoked.
##
## revokeTeason: Reason certificate was revoked; empty string if not revoked or not specified.
##
## thisUpdate: Time this response was generated.
##
## nextUpdate: Time next response will be ready; 0 if not supploed.
##
## .. bro:see:: ocsp_request ocsp_request_certificate ocsp_response_status
## ocsp_response_bytes ocsp_extension
## x509_ocsp_ext_signed_certificate_timestamp
event ocsp_response_certificate%(f: fa_file, hashAlgorithm: string, issuerNameHash: string, issuerKeyHash: string, serialNumber: string, certStatus: string, revokeTime: time, revokeReason: string, thisUpdate: time, nextUpdate: time%);
## This event is raised when an OCSP extension is encountered in an OCSP response.
## See :rfc:`6960` for more details on OCSP.
##
## f: The file.
##
## ext: The parsed extension (same format as X.509 extensions).
##
## global_resp: T if extension encountered in the global response (in ResponseData),
## F when encountered in a SingleResponse.
##
## .. bro:see:: ocsp_request ocsp_request_certificate ocsp_response_status
## ocsp_response_bytes ocsp_response_certificate
## x509_ocsp_ext_signed_certificate_timestamp
event ocsp_extension%(f: fa_file, ext: X509::Extension, global_resp: bool%);

View file

@ -0,0 +1,54 @@
# Binpac analyzer for X.509 extensions
# we just use it for the SignedCertificateTimestamp at the moment
%include binpac.pac
%include bro.pac
%extern{
#include "types.bif.h"
#include "file_analysis/File.h"
#include "events.bif.h"
%}
analyzer X509Extension withcontext {
connection: MockConnection;
flow: SignedCertTimestampExt;
};
connection MockConnection(bro_analyzer: BroFileAnalyzer) {
upflow = SignedCertTimestampExt;
downflow = SignedCertTimestampExt;
};
%include x509-signed_certificate_timestamp.pac
# The base record
type HandshakeRecord() = record {
signed_certificate_timestamp_list: SignedCertificateTimestampList(this)[] &transient;
} &byteorder = bigendian;
flow SignedCertTimestampExt {
flowunit = HandshakeRecord withcontext(connection, this);
};
refine connection MockConnection += {
function proc_signedcertificatetimestamp(rec: HandshakeRecord, version: uint8, logid: const_bytestring, timestamp: uint64, digitally_signed_algorithms: SignatureAndHashAlgorithm, digitally_signed_signature: const_bytestring) : bool
%{
BifEvent::generate_x509_ocsp_ext_signed_certificate_timestamp((analyzer::Analyzer *) bro_analyzer(),
bro_analyzer()->GetFile()->GetVal()->Ref(),
version,
new StringVal(logid.length(), reinterpret_cast<const char*>(logid.begin())),
timestamp,
digitally_signed_algorithms->HashAlgorithm(),
digitally_signed_algorithms->SignatureAlgorithm(),
new StringVal(digitally_signed_signature.length(), reinterpret_cast<const char*>(digitally_signed_signature.begin()))
);
return true;
%}
};
refine typeattr SignedCertificateTimestamp += &let {
proc : bool = $context.connection.proc_signedcertificatetimestamp(rec, version, logid, timestamp, digitally_signed_algorithms, digitally_signed_signature);
};

View file

@ -0,0 +1 @@
../../../analyzer/protocol/ssl/tls-handshake-signed_certificate_timestamp.pac