diff options
Diffstat (limited to '')
-rw-r--r-- | src/output-json-tls.c | 655 |
1 files changed, 655 insertions, 0 deletions
diff --git a/src/output-json-tls.c b/src/output-json-tls.c new file mode 100644 index 0000000..9771f4d --- /dev/null +++ b/src/output-json-tls.c @@ -0,0 +1,655 @@ +/* Copyright (C) 2007-2021 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + * + * \author Tom DeCanio <td@npulsetech.com> + * + * Implements TLS JSON logging portion of the engine. + */ + +#include "suricata-common.h" +#include "detect.h" +#include "pkt-var.h" +#include "conf.h" + +#include "threads.h" +#include "threadvars.h" +#include "tm-threads.h" + +#include "util-print.h" +#include "util-time.h" +#include "util-unittest.h" + +#include "util-debug.h" +#include "app-layer-parser.h" +#include "output.h" +#include "app-layer-ssl.h" +#include "app-layer.h" +#include "util-privs.h" +#include "util-buffer.h" + +#include "util-logopenfile.h" +#include "util-ja3.h" + +#include "output-json.h" +#include "output-json-tls.h" + +SC_ATOMIC_EXTERN(unsigned int, cert_id); + +#define MODULE_NAME "LogTlsLog" +#define DEFAULT_LOG_FILENAME "tls.json" + +#define LOG_TLS_DEFAULT 0 +#define LOG_TLS_EXTENDED (1 << 0) +#define LOG_TLS_CUSTOM (1 << 1) +#define LOG_TLS_SESSION_RESUMPTION (1 << 2) + +#define LOG_TLS_FIELD_VERSION (1 << 0) +#define LOG_TLS_FIELD_SUBJECT (1 << 1) +#define LOG_TLS_FIELD_ISSUER (1 << 2) +#define LOG_TLS_FIELD_SERIAL (1 << 3) +#define LOG_TLS_FIELD_FINGERPRINT (1 << 4) +#define LOG_TLS_FIELD_NOTBEFORE (1 << 5) +#define LOG_TLS_FIELD_NOTAFTER (1 << 6) +#define LOG_TLS_FIELD_SNI (1 << 7) +#define LOG_TLS_FIELD_CERTIFICATE (1 << 8) +#define LOG_TLS_FIELD_CHAIN (1 << 9) +#define LOG_TLS_FIELD_SESSION_RESUMED (1 << 10) +#define LOG_TLS_FIELD_JA3 (1 << 11) +#define LOG_TLS_FIELD_JA3S (1 << 12) +#define LOG_TLS_FIELD_CLIENT (1 << 13) /**< client fields (issuer, subject, etc) */ +#define LOG_TLS_FIELD_CLIENT_CERT (1 << 14) +#define LOG_TLS_FIELD_CLIENT_CHAIN (1 << 15) + +typedef struct { + const char *name; + uint64_t flag; +} TlsFields; + +TlsFields tls_fields[] = { { "version", LOG_TLS_FIELD_VERSION }, + { "subject", LOG_TLS_FIELD_SUBJECT }, { "issuer", LOG_TLS_FIELD_ISSUER }, + { "serial", LOG_TLS_FIELD_SERIAL }, { "fingerprint", LOG_TLS_FIELD_FINGERPRINT }, + { "not_before", LOG_TLS_FIELD_NOTBEFORE }, { "not_after", LOG_TLS_FIELD_NOTAFTER }, + { "sni", LOG_TLS_FIELD_SNI }, { "certificate", LOG_TLS_FIELD_CERTIFICATE }, + { "chain", LOG_TLS_FIELD_CHAIN }, { "session_resumed", LOG_TLS_FIELD_SESSION_RESUMED }, + { "ja3", LOG_TLS_FIELD_JA3 }, { "ja3s", LOG_TLS_FIELD_JA3S }, + { "client", LOG_TLS_FIELD_CLIENT }, { "client_certificate", LOG_TLS_FIELD_CLIENT_CERT }, + { "client_chain", LOG_TLS_FIELD_CLIENT_CHAIN }, { NULL, -1 } }; + +typedef struct OutputTlsCtx_ { + uint32_t flags; /** Store mode */ + uint64_t fields; /** Store fields */ + OutputJsonCtx *eve_ctx; +} OutputTlsCtx; + + +typedef struct JsonTlsLogThread_ { + OutputTlsCtx *tlslog_ctx; + OutputJsonThreadCtx *ctx; +} JsonTlsLogThread; + +static void JsonTlsLogSubject(JsonBuilder *js, SSLState *ssl_state) +{ + if (ssl_state->server_connp.cert0_subject) { + jb_set_string(js, "subject", + ssl_state->server_connp.cert0_subject); + } +} + +static void JsonTlsLogIssuer(JsonBuilder *js, SSLState *ssl_state) +{ + if (ssl_state->server_connp.cert0_issuerdn) { + jb_set_string(js, "issuerdn", + ssl_state->server_connp.cert0_issuerdn); + } +} + +static void JsonTlsLogSessionResumed(JsonBuilder *js, SSLState *ssl_state) +{ + if (ssl_state->flags & SSL_AL_FLAG_SESSION_RESUMED) { + /* Only log a session as 'resumed' if a certificate has not + been seen, and the session is not TLSv1.3 or later. */ + if ((ssl_state->server_connp.cert0_issuerdn == NULL && + ssl_state->server_connp.cert0_subject == NULL) && + (ssl_state->flags & SSL_AL_FLAG_STATE_SERVER_HELLO) && + ((ssl_state->flags & SSL_AL_FLAG_LOG_WITHOUT_CERT) == 0)) { + jb_set_bool(js, "session_resumed", true); + } + } +} + +static void JsonTlsLogFingerprint(JsonBuilder *js, SSLState *ssl_state) +{ + if (ssl_state->server_connp.cert0_fingerprint) { + jb_set_string(js, "fingerprint", + ssl_state->server_connp.cert0_fingerprint); + } +} + +static void JsonTlsLogSni(JsonBuilder *js, SSLState *ssl_state) +{ + if (ssl_state->client_connp.sni) { + jb_set_string(js, "sni", + ssl_state->client_connp.sni); + } +} + +static void JsonTlsLogSerial(JsonBuilder *js, SSLState *ssl_state) +{ + if (ssl_state->server_connp.cert0_serial) { + jb_set_string(js, "serial", + ssl_state->server_connp.cert0_serial); + } +} + +static void JsonTlsLogVersion(JsonBuilder *js, SSLState *ssl_state) +{ + char ssl_version[SSL_VERSION_MAX_STRLEN]; + SSLVersionToString(ssl_state->server_connp.version, ssl_version); + jb_set_string(js, "version", ssl_version); +} + +static void JsonTlsLogNotBefore(JsonBuilder *js, SSLState *ssl_state) +{ + if (ssl_state->server_connp.cert0_not_before != 0) { + sc_x509_log_timestamp(js, "notbefore", ssl_state->server_connp.cert0_not_before); + } +} + +static void JsonTlsLogNotAfter(JsonBuilder *js, SSLState *ssl_state) +{ + if (ssl_state->server_connp.cert0_not_after != 0) { + sc_x509_log_timestamp(js, "notafter", ssl_state->server_connp.cert0_not_after); + } +} + +static void JsonTlsLogJa3Hash(JsonBuilder *js, SSLState *ssl_state) +{ + if (ssl_state->client_connp.ja3_hash != NULL) { + jb_set_string(js, "hash", + ssl_state->client_connp.ja3_hash); + } +} + +static void JsonTlsLogJa3String(JsonBuilder *js, SSLState *ssl_state) +{ + if ((ssl_state->client_connp.ja3_str != NULL) && + ssl_state->client_connp.ja3_str->data != NULL) { + jb_set_string(js, "string", + ssl_state->client_connp.ja3_str->data); + } +} + +static void JsonTlsLogJa3(JsonBuilder *js, SSLState *ssl_state) +{ + if ((ssl_state->client_connp.ja3_hash != NULL) || + ((ssl_state->client_connp.ja3_str != NULL) && + ssl_state->client_connp.ja3_str->data != NULL)) { + jb_open_object(js, "ja3"); + + JsonTlsLogJa3Hash(js, ssl_state); + JsonTlsLogJa3String(js, ssl_state); + + jb_close(js); + } +} + +static void JsonTlsLogJa3SHash(JsonBuilder *js, SSLState *ssl_state) +{ + if (ssl_state->server_connp.ja3_hash != NULL) { + jb_set_string(js, "hash", + ssl_state->server_connp.ja3_hash); + } +} + +static void JsonTlsLogJa3SString(JsonBuilder *js, SSLState *ssl_state) +{ + if ((ssl_state->server_connp.ja3_str != NULL) && + ssl_state->server_connp.ja3_str->data != NULL) { + jb_set_string(js, "string", + ssl_state->server_connp.ja3_str->data); + } +} + +static void JsonTlsLogJa3S(JsonBuilder *js, SSLState *ssl_state) +{ + if ((ssl_state->server_connp.ja3_hash != NULL) || + ((ssl_state->server_connp.ja3_str != NULL) && + ssl_state->server_connp.ja3_str->data != NULL)) { + jb_open_object(js, "ja3s"); + + JsonTlsLogJa3SHash(js, ssl_state); + JsonTlsLogJa3SString(js, ssl_state); + + jb_close(js); + } +} + +static void JsonTlsLogCertificate(JsonBuilder *js, SSLStateConnp *connp) +{ + if (TAILQ_EMPTY(&connp->certs)) { + return; + } + + SSLCertsChain *cert = TAILQ_FIRST(&connp->certs); + if (cert == NULL) { + return; + } + + jb_set_base64(js, "certificate", cert->cert_data, cert->cert_len); +} + +static void JsonTlsLogChain(JsonBuilder *js, SSLStateConnp *connp) +{ + if (TAILQ_EMPTY(&connp->certs)) { + return; + } + + jb_open_array(js, "chain"); + + SSLCertsChain *cert; + TAILQ_FOREACH (cert, &connp->certs, next) { + jb_append_base64(js, cert->cert_data, cert->cert_len); + } + + jb_close(js); +} + +static bool HasClientCert(SSLStateConnp *connp) +{ + if (connp->cert0_subject || connp->cert0_issuerdn) + return true; + return false; +} + +static void JsonTlsLogClientCert( + JsonBuilder *js, SSLStateConnp *connp, const bool log_cert, const bool log_chain) +{ + if (connp->cert0_subject != NULL) { + jb_set_string(js, "subject", connp->cert0_subject); + } + if (connp->cert0_issuerdn != NULL) { + jb_set_string(js, "issuerdn", connp->cert0_issuerdn); + } + if (connp->cert0_fingerprint) { + jb_set_string(js, "fingerprint", connp->cert0_fingerprint); + } + if (connp->cert0_serial) { + jb_set_string(js, "serial", connp->cert0_serial); + } + if (connp->cert0_not_before != 0) { + char timebuf[64]; + SCTime_t ts = SCTIME_FROM_SECS(connp->cert0_not_before); + CreateUtcIsoTimeString(ts, timebuf, sizeof(timebuf)); + jb_set_string(js, "notbefore", timebuf); + } + if (connp->cert0_not_after != 0) { + char timebuf[64]; + SCTime_t ts = SCTIME_FROM_SECS(connp->cert0_not_after); + CreateUtcIsoTimeString(ts, timebuf, sizeof(timebuf)); + jb_set_string(js, "notafter", timebuf); + } + + if (log_cert) { + JsonTlsLogCertificate(js, connp); + } + if (log_chain) { + JsonTlsLogChain(js, connp); + } +} + +void JsonTlsLogJSONBasic(JsonBuilder *js, SSLState *ssl_state) +{ + /* tls subject */ + JsonTlsLogSubject(js, ssl_state); + + /* tls issuerdn */ + JsonTlsLogIssuer(js, ssl_state); + + /* tls session resumption */ + JsonTlsLogSessionResumed(js, ssl_state); +} + +static void JsonTlsLogJSONCustom(OutputTlsCtx *tls_ctx, JsonBuilder *js, + SSLState *ssl_state) +{ + /* tls subject */ + if (tls_ctx->fields & LOG_TLS_FIELD_SUBJECT) + JsonTlsLogSubject(js, ssl_state); + + /* tls issuerdn */ + if (tls_ctx->fields & LOG_TLS_FIELD_ISSUER) + JsonTlsLogIssuer(js, ssl_state); + + /* tls session resumption */ + if (tls_ctx->fields & LOG_TLS_FIELD_SESSION_RESUMED) + JsonTlsLogSessionResumed(js, ssl_state); + + /* tls serial */ + if (tls_ctx->fields & LOG_TLS_FIELD_SERIAL) + JsonTlsLogSerial(js, ssl_state); + + /* tls fingerprint */ + if (tls_ctx->fields & LOG_TLS_FIELD_FINGERPRINT) + JsonTlsLogFingerprint(js, ssl_state); + + /* tls sni */ + if (tls_ctx->fields & LOG_TLS_FIELD_SNI) + JsonTlsLogSni(js, ssl_state); + + /* tls version */ + if (tls_ctx->fields & LOG_TLS_FIELD_VERSION) + JsonTlsLogVersion(js, ssl_state); + + /* tls notbefore */ + if (tls_ctx->fields & LOG_TLS_FIELD_NOTBEFORE) + JsonTlsLogNotBefore(js, ssl_state); + + /* tls notafter */ + if (tls_ctx->fields & LOG_TLS_FIELD_NOTAFTER) + JsonTlsLogNotAfter(js, ssl_state); + + /* tls certificate */ + if (tls_ctx->fields & LOG_TLS_FIELD_CERTIFICATE) + JsonTlsLogCertificate(js, &ssl_state->server_connp); + + /* tls chain */ + if (tls_ctx->fields & LOG_TLS_FIELD_CHAIN) + JsonTlsLogChain(js, &ssl_state->server_connp); + + /* tls ja3_hash */ + if (tls_ctx->fields & LOG_TLS_FIELD_JA3) + JsonTlsLogJa3(js, ssl_state); + + /* tls ja3s */ + if (tls_ctx->fields & LOG_TLS_FIELD_JA3S) + JsonTlsLogJa3S(js, ssl_state); + + if (tls_ctx->fields & LOG_TLS_FIELD_CLIENT) { + const bool log_cert = (tls_ctx->fields & LOG_TLS_FIELD_CLIENT_CERT) != 0; + const bool log_chain = (tls_ctx->fields & LOG_TLS_FIELD_CLIENT_CHAIN) != 0; + if (HasClientCert(&ssl_state->client_connp)) { + jb_open_object(js, "client"); + JsonTlsLogClientCert(js, &ssl_state->client_connp, log_cert, log_chain); + jb_close(js); + } + } +} + +void JsonTlsLogJSONExtended(JsonBuilder *tjs, SSLState * state) +{ + JsonTlsLogJSONBasic(tjs, state); + + /* tls serial */ + JsonTlsLogSerial(tjs, state); + + /* tls fingerprint */ + JsonTlsLogFingerprint(tjs, state); + + /* tls sni */ + JsonTlsLogSni(tjs, state); + + /* tls version */ + JsonTlsLogVersion(tjs, state); + + /* tls notbefore */ + JsonTlsLogNotBefore(tjs, state); + + /* tls notafter */ + JsonTlsLogNotAfter(tjs, state); + + /* tls ja3 */ + JsonTlsLogJa3(tjs, state); + + /* tls ja3s */ + JsonTlsLogJa3S(tjs, state); + + if (HasClientCert(&state->client_connp)) { + jb_open_object(tjs, "client"); + JsonTlsLogClientCert(tjs, &state->client_connp, false, false); + jb_close(tjs); + } +} + +static int JsonTlsLogger(ThreadVars *tv, void *thread_data, const Packet *p, + Flow *f, void *state, void *txptr, uint64_t tx_id) +{ + JsonTlsLogThread *aft = (JsonTlsLogThread *)thread_data; + OutputTlsCtx *tls_ctx = aft->tlslog_ctx; + + SSLState *ssl_state = (SSLState *)state; + if (unlikely(ssl_state == NULL)) { + return 0; + } + + if ((ssl_state->server_connp.cert0_issuerdn == NULL || + ssl_state->server_connp.cert0_subject == NULL) && + ((ssl_state->flags & SSL_AL_FLAG_SESSION_RESUMED) == 0 || + (tls_ctx->flags & LOG_TLS_SESSION_RESUMPTION) == 0) && + ((ssl_state->flags & SSL_AL_FLAG_LOG_WITHOUT_CERT) == 0)) { + return 0; + } + + JsonBuilder *js = CreateEveHeader(p, LOG_DIR_FLOW, "tls", NULL, aft->tlslog_ctx->eve_ctx); + if (unlikely(js == NULL)) { + return 0; + } + + jb_open_object(js, "tls"); + + /* log custom fields */ + if (tls_ctx->flags & LOG_TLS_CUSTOM) { + JsonTlsLogJSONCustom(tls_ctx, js, ssl_state); + } + /* log extended */ + else if (tls_ctx->flags & LOG_TLS_EXTENDED) { + JsonTlsLogJSONExtended(js, ssl_state); + } + /* log basic */ + else { + JsonTlsLogJSONBasic(js, ssl_state); + } + + /* print original application level protocol when it have been changed + because of STARTTLS, HTTP CONNECT, or similar. */ + if (f->alproto_orig != ALPROTO_UNKNOWN) { + jb_set_string(js, "from_proto", + AppLayerGetProtoName(f->alproto_orig)); + } + + /* Close the tls object. */ + jb_close(js); + + OutputJsonBuilderBuffer(js, aft->ctx); + jb_free(js); + + return 0; +} + +static TmEcode JsonTlsLogThreadInit(ThreadVars *t, const void *initdata, void **data) +{ + JsonTlsLogThread *aft = SCCalloc(1, sizeof(JsonTlsLogThread)); + if (unlikely(aft == NULL)) { + return TM_ECODE_FAILED; + } + + if (initdata == NULL) { + SCLogDebug("Error getting context for eve-log tls 'initdata' argument NULL"); + goto error_exit; + } + + /* use the Output Context (file pointer and mutex) */ + aft->tlslog_ctx = ((OutputCtx *)initdata)->data; + + aft->ctx = CreateEveThreadCtx(t, aft->tlslog_ctx->eve_ctx); + if (!aft->ctx) { + goto error_exit; + } + *data = (void *)aft; + return TM_ECODE_OK; + +error_exit: + SCFree(aft); + return TM_ECODE_FAILED; +} + +static TmEcode JsonTlsLogThreadDeinit(ThreadVars *t, void *data) +{ + JsonTlsLogThread *aft = (JsonTlsLogThread *)data; + if (aft == NULL) { + return TM_ECODE_OK; + } + + FreeEveThreadCtx(aft->ctx); + + /* clear memory */ + memset(aft, 0, sizeof(JsonTlsLogThread)); + + SCFree(aft); + return TM_ECODE_OK; +} + +static OutputTlsCtx *OutputTlsInitCtx(ConfNode *conf) +{ + OutputTlsCtx *tls_ctx = SCMalloc(sizeof(OutputTlsCtx)); + if (unlikely(tls_ctx == NULL)) + return NULL; + + tls_ctx->flags = LOG_TLS_DEFAULT; + tls_ctx->fields = 0; + + if (conf == NULL) + return tls_ctx; + + const char *extended = ConfNodeLookupChildValue(conf, "extended"); + if (extended) { + if (ConfValIsTrue(extended)) { + tls_ctx->flags = LOG_TLS_EXTENDED; + } + } + + ConfNode *custom = ConfNodeLookupChild(conf, "custom"); + if (custom) { + tls_ctx->flags = LOG_TLS_CUSTOM; + ConfNode *field; + TAILQ_FOREACH(field, &custom->head, next) + { + bool valid = false; + TlsFields *valid_fields = tls_fields; + for ( ; valid_fields->name != NULL; valid_fields++) { + if (strcasecmp(field->val, valid_fields->name) == 0) { + tls_ctx->fields |= valid_fields->flag; + SCLogDebug("enabled %s", field->val); + valid = true; + break; + } + } + if (!valid) { + SCLogWarning("eve.tls: unknown 'custom' field '%s'", field->val); + } + } + } + + const char *session_resumption = ConfNodeLookupChildValue(conf, "session-resumption"); + if (session_resumption == NULL || ConfValIsTrue(session_resumption)) { + tls_ctx->flags |= LOG_TLS_SESSION_RESUMPTION; + } + + if ((tls_ctx->fields & LOG_TLS_FIELD_JA3) && + Ja3IsDisabled("fields")) { + /* JA3 is disabled, so don't log any JA3 fields */ + tls_ctx->fields &= ~LOG_TLS_FIELD_JA3; + tls_ctx->fields &= ~LOG_TLS_FIELD_JA3S; + } + + if ((tls_ctx->fields & LOG_TLS_FIELD_CERTIFICATE) && + (tls_ctx->fields & LOG_TLS_FIELD_CHAIN)) { + SCLogWarning("Both 'certificate' and 'chain' contains the top " + "certificate, so only one of them should be enabled " + "at a time"); + } + if ((tls_ctx->fields & LOG_TLS_FIELD_CLIENT_CERT) && + (tls_ctx->fields & LOG_TLS_FIELD_CLIENT_CHAIN)) { + SCLogWarning("Both 'client_certificate' and 'client_chain' contains the top " + "certificate, so only one of them should be enabled " + "at a time"); + } + + if ((tls_ctx->fields & LOG_TLS_FIELD_CLIENT) == 0) { + if (tls_ctx->fields & LOG_TLS_FIELD_CLIENT_CERT) { + SCLogConfig("enabling \"client\" as a dependency of \"client_certificate\""); + tls_ctx->fields |= LOG_TLS_FIELD_CLIENT; + } + if (tls_ctx->fields & LOG_TLS_FIELD_CLIENT_CHAIN) { + SCLogConfig("enabling \"client\" as a dependency of \"client_chain\""); + tls_ctx->fields |= LOG_TLS_FIELD_CLIENT; + } + } + + return tls_ctx; +} + +static void OutputTlsLogDeinitSub(OutputCtx *output_ctx) +{ + OutputTlsCtx *tls_ctx = output_ctx->data; + SCFree(tls_ctx); + SCFree(output_ctx); +} + +static OutputInitResult OutputTlsLogInitSub(ConfNode *conf, OutputCtx *parent_ctx) +{ + OutputInitResult result = { NULL, false }; + OutputJsonCtx *ojc = parent_ctx->data; + + OutputTlsCtx *tls_ctx = OutputTlsInitCtx(conf); + if (unlikely(tls_ctx == NULL)) + return result; + + OutputCtx *output_ctx = SCCalloc(1, sizeof(OutputCtx)); + if (unlikely(output_ctx == NULL)) { + SCFree(tls_ctx); + return result; + } + + tls_ctx->eve_ctx = ojc; + + if ((tls_ctx->fields & LOG_TLS_FIELD_CERTIFICATE) && + (tls_ctx->fields & LOG_TLS_FIELD_CHAIN)) { + SCLogWarning("Both 'certificate' and 'chain' contains the top " + "certificate, so only one of them should be enabled " + "at a time"); + } + + output_ctx->data = tls_ctx; + output_ctx->DeInit = OutputTlsLogDeinitSub; + + AppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_TLS); + + result.ctx = output_ctx; + result.ok = true; + return result; +} + +void JsonTlsLogRegister (void) +{ + /* register as child of eve-log */ + OutputRegisterTxSubModuleWithProgress(LOGGER_JSON_TX, "eve-log", "JsonTlsLog", "eve-log.tls", + OutputTlsLogInitSub, ALPROTO_TLS, JsonTlsLogger, TLS_HANDSHAKE_DONE, TLS_HANDSHAKE_DONE, + JsonTlsLogThreadInit, JsonTlsLogThreadDeinit, NULL); +} |