mirror of
https://github.com/zeek/zeek.git
synced 2025-10-07 17:18:20 +00:00
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:
commit
faa4150154
86 changed files with 2672 additions and 445 deletions
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() )
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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()
|
||||
|
|
403
src/file_analysis/analyzer/x509/OCSP.cc
Normal file
403
src/file_analysis/analyzer/x509/OCSP.cc
Normal 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;
|
||||
}
|
54
src/file_analysis/analyzer/x509/OCSP.h
Normal file
54
src/file_analysis/analyzer/x509/OCSP.h
Normal 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
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
316
src/file_analysis/analyzer/x509/X509Common.cc
Normal file
316
src/file_analysis/analyzer/x509/X509Common.cc
Normal 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;
|
||||
}
|
44
src/file_analysis/analyzer/x509/X509Common.h
Normal file
44
src/file_analysis/analyzer/x509/X509Common.h
Normal 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 */
|
|
@ -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%);
|
||||
|
|
|
@ -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*>(×tamp_network), sizeof(timestamp_network)); // timestamp -> 64 bits
|
||||
if ( precert )
|
||||
data.append("\0\1", 2); // entry-type: precert_entry
|
||||
else
|
||||
data.append("\0\0", 2); // entry-type: x509_entry
|
||||
|
||||
if ( precert )
|
||||
{
|
||||
x = X509_dup(x);
|
||||
assert(x);
|
||||
#ifdef NID_ct_precert_scts
|
||||
int pos = X509_get_ext_by_NID(x, NID_ct_precert_scts, -1);
|
||||
if ( pos < 0 )
|
||||
{
|
||||
reporter->Error("NID_ct_precert_scts not found");
|
||||
return new Val(0, TYPE_BOOL);
|
||||
}
|
||||
#else
|
||||
int num_ext = X509_get_ext_count(x);
|
||||
int pos = -1;
|
||||
for ( int k = 0; k < num_ext; ++k )
|
||||
{
|
||||
char oid[256];
|
||||
X509_EXTENSION* ex = X509_get_ext(x, k);
|
||||
ASN1_OBJECT* ext_asn = X509_EXTENSION_get_object(ex);
|
||||
OBJ_obj2txt(oid, 255, ext_asn, 1);
|
||||
if ( strcmp(oid, "1.3.6.1.4.1.11129.2.4.2") == 0 )
|
||||
{
|
||||
pos = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
X509_EXTENSION_free(X509_delete_ext(x, pos));
|
||||
#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);
|
||||
%}
|
||||
|
|
120
src/file_analysis/analyzer/x509/ocsp_events.bif
Normal file
120
src/file_analysis/analyzer/x509/ocsp_events.bif
Normal 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%);
|
54
src/file_analysis/analyzer/x509/x509-extension.pac
Normal file
54
src/file_analysis/analyzer/x509/x509-extension.pac
Normal 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);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
../../../analyzer/protocol/ssl/tls-handshake-signed_certificate_timestamp.pac
|
Loading…
Add table
Add a link
Reference in a new issue