From: Aki Tuomi Date: Thu, 21 Jul 2022 10:31:56 +0000 (+0300) Subject: lib-ssl-iostream: Add JA3 string X-Git-Tag: 2.4.0~3264 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=3ca8852ec531b83c86c30b890b7f0200cf86105a;p=thirdparty%2Fdovecot%2Fcore.git lib-ssl-iostream: Add JA3 string --- diff --git a/m4/ssl.m4 b/m4/ssl.m4 index 2e246c625e..d4ff3209fd 100644 --- a/m4/ssl.m4 +++ b/m4/ssl.m4 @@ -125,6 +125,7 @@ AC_DEFUN([DOVECOT_SSL], [ DOVECOT_CHECK_SSL_FUNC([RSA_set0_key]) DOVECOT_CHECK_SSL_FUNC([SSL_CIPHER_get_kx_nid]) DOVECOT_CHECK_SSL_FUNC([SSL_clear_options]) + DOVECOT_CHECK_SSL_FUNC([SSL_client_hello_get0_ciphers]) DOVECOT_CHECK_SSL_FUNC([SSL_CTX_set0_tmp_dh_pkey]) DOVECOT_CHECK_SSL_FUNC([SSL_CTX_set_ciphersuites]) DOVECOT_CHECK_SSL_FUNC([SSL_CTX_set_ecdh_auto]) diff --git a/src/lib-ssl-iostream/iostream-openssl-context.c b/src/lib-ssl-iostream/iostream-openssl-context.c index 4086774032..30b19d1290 100644 --- a/src/lib-ssl-iostream/iostream-openssl-context.c +++ b/src/lib-ssl-iostream/iostream-openssl-context.c @@ -1,6 +1,8 @@ /* Copyright (c) 2009-2018 Dovecot authors, see the included COPYING file */ #include "lib.h" +#include "str.h" +#include "hex-binary.h" #include "safe-memset.h" #include "iostream-openssl.h" #include "dovecot-openssl-common.h" @@ -13,6 +15,7 @@ #include #include #include +#include #ifndef HAVE_EVP_PKEY_get0_DH # define EVP_PKEY_get0_DH(x) ((x)->pkey.dh) @@ -368,6 +371,169 @@ static int ssl_servername_callback(SSL *ssl, int *al ATTR_UNUSED, return SSL_TLSEXT_ERR_OK; } +#ifdef HAVE_SSL_client_hello_get0_ciphers + +static const int ssl_ja3_grease[] = { + 0x0a0a, + 0x1a1a, + 0x2a2a, + 0x3a3a, + 0x4a4a, + 0x5a5a, + 0x6a6a, + 0x7a7a, + 0x8a8a, + 0x9a9a, + 0xaaaa, + 0xbaba, + 0xcaca, + 0xdada, + 0xeaea, + 0xfafa, +}; + +static bool +ssl_ja3_is_ext_greased(int id) +{ + for (size_t i = 0; i < N_ELEMENTS(ssl_ja3_grease); ++i) + if (id == ssl_ja3_grease[i]) + return TRUE; + return FALSE; +} + +static const int ssl_ja3_nid_list[] = { + NID_sect163k1, /* sect163k1 (1) */ + NID_sect163r1, /* sect163r1 (2) */ + NID_sect163r2, /* sect163r2 (3) */ + NID_sect193r1, /* sect193r1 (4) */ + NID_sect193r2, /* sect193r2 (5) */ + NID_sect233k1, /* sect233k1 (6) */ + NID_sect233r1, /* sect233r1 (7) */ + NID_sect239k1, /* sect239k1 (8) */ + NID_sect283k1, /* sect283k1 (9) */ + NID_sect283r1, /* sect283r1 (10) */ + NID_sect409k1, /* sect409k1 (11) */ + NID_sect409r1, /* sect409r1 (12) */ + NID_sect571k1, /* sect571k1 (13) */ + NID_sect571r1, /* sect571r1 (14) */ + NID_secp160k1, /* secp160k1 (15) */ + NID_secp160r1, /* secp160r1 (16) */ + NID_secp160r2, /* secp160r2 (17) */ + NID_secp192k1, /* secp192k1 (18) */ + NID_X9_62_prime192v1, /* secp192r1 (19) */ + NID_secp224k1, /* secp224k1 (20) */ + NID_secp224r1, /* secp224r1 (21) */ + NID_secp256k1, /* secp256k1 (22) */ + NID_X9_62_prime256v1, /* secp256r1 (23) */ + NID_secp384r1, /* secp384r1 (24) */ + NID_secp521r1, /* secp521r1 (25) */ + NID_brainpoolP256r1, /* brainpoolP256r1 (26) */ + NID_brainpoolP384r1, /* brainpoolP384r1 (27) */ + NID_brainpoolP512r1, /* brainpool512r1 (28) */ + NID_X25519, /* X25519 (29) */ + NID_X448, /* X448 (30) */ +}; + +static int ssl_ja3_nid_to_cid(int nid) +{ + for (size_t i = 0; i < N_ELEMENTS(ssl_ja3_nid_list); i++) + if (nid == ssl_ja3_nid_list[i]) + return ((int)i)+1; + + if (nid == NID_ffdhe2048) + return 0x100; + else if (nid == NID_ffdhe3072) + return 0x101; + else if (nid == NID_ffdhe4096) + return 0x102; + else if (nid == NID_ffdhe6144) + return 0x103; + else if (nid == NID_ffdhe8192) + return 0x104; + return nid; +} + +static int ssl_clienthello_callback(SSL *ssl, int *al ATTR_UNUSED, + void *context ATTR_UNUSED) +{ + struct ssl_iostream *ssl_io = + SSL_get_ex_data(ssl, dovecot_ssl_extdata_index); + + int ver = SSL_version(ssl)-1; + const unsigned char *ciphers = NULL; + size_t nciphers = 0; + string_t *ja3 = str_new(ssl_io->ctx->pool, 64); + + str_printfa(ja3, "%d,", ver); + nciphers = SSL_client_hello_get0_ciphers(ssl, &ciphers); + + for (size_t i = 0; i < nciphers; i += 2) { + if (i > 0) + str_append_c(ja3, '-'); + uint16_t cipher = be16_to_cpu_unaligned(&ciphers[i]); + str_printfa(ja3, "%u", cipher); + } + str_append_c(ja3, ','); + + int *exts = NULL; + size_t nexts = 0; + if (SSL_client_hello_get1_extensions_present(ssl, &exts, &nexts) == 1) { + bool first = TRUE; + for (size_t i = 0; i < nexts; i++) { + if (ssl_ja3_is_ext_greased(exts[i])) + continue; + if (first) + first = FALSE; + else + str_append_c(ja3, '-'); + str_printfa(ja3, "%d", exts[i]); + } + OPENSSL_free(exts); + } + str_append_c(ja3, ','); + + const unsigned char *ext = NULL; + size_t extlen; + + /* Process extension 10 - groups */ + if (SSL_client_hello_get0_ext(ssl, 10, &ext, &extlen) == 1 && + extlen > 0) { + bool first = TRUE; + unsigned short veclen = be16_to_cpu_unaligned(ext); + if (veclen+2 == extlen) { + for (size_t i = 2; i < extlen; i+=2) { + uint16_t group = be16_to_cpu_unaligned(&ext[i]); + if (ssl_ja3_is_ext_greased(group)) + continue; + if (first) + first = FALSE; + else + str_append_c(ja3, '-'); + str_printfa(ja3, "%u", ssl_ja3_nid_to_cid(group)); + } + } + } + str_append_c(ja3, ','); + + /* Process extension 11 - ec point formats */ + ext = NULL; + if (SSL_client_hello_get0_ext(ssl, 11, &ext, &extlen) == 1 && + extlen > 0 && extlen == ext[0]+1) { + for (size_t i = 1; i < extlen; i++) { + if (i > 1) + str_append_c(ja3, '-'); + str_printfa(ja3, "%u", ext[i]); + } + } + + /* Store ja3 string */ + ssl_io->ja3_str = str_c(ja3); + + return SSL_CLIENT_HELLO_SUCCESS; +} + +#endif + static int ssl_iostream_context_load_ca(struct ssl_iostream_context *ctx, const struct ssl_iostream_settings *set, @@ -510,6 +676,9 @@ ssl_iostream_context_set(struct ssl_iostream_context *ctx, if (set->verbose) i_debug("OpenSSL library doesn't support SNI"); } +#ifdef HAVE_SSL_client_hello_get0_ciphers + SSL_CTX_set_client_hello_cb(ctx->ssl_ctx, ssl_clienthello_callback, ctx); +#endif } return 0; } diff --git a/src/lib-ssl-iostream/iostream-openssl.c b/src/lib-ssl-iostream/iostream-openssl.c index 7237f90703..c0f52e6a9c 100644 --- a/src/lib-ssl-iostream/iostream-openssl.c +++ b/src/lib-ssl-iostream/iostream-openssl.c @@ -682,7 +682,8 @@ static int openssl_iostream_handshake(struct ssl_iostream *ssl_io) const char *reason, *error = NULL; int ret; - i_assert(!ssl_io->handshaked); + if (ssl_io->handshaked) + return openssl_iostream_bio_sync(ssl_io, OPENSSL_IOSTREAM_SYNC_TYPE_HANDSHAKE); /* we are being destroyed, so do not do any more handshaking */ if (ssl_io->destroyed) @@ -921,6 +922,13 @@ openssl_iostream_get_protocol_name(struct ssl_iostream *ssl_io) return SSL_get_version(ssl_io->ssl); } +static const char * +openssl_iostream_get_ja3(struct ssl_iostream *ssl_io) +{ + if (!ssl_io->handshaked) + return NULL; + return ssl_io->ja3_str; +} static const struct iostream_ssl_vfuncs ssl_vfuncs = { .global_init = openssl_iostream_global_init, @@ -952,6 +960,7 @@ static const struct iostream_ssl_vfuncs ssl_vfuncs = { .get_cipher = openssl_iostream_get_cipher, .get_pfs = openssl_iostream_get_pfs, .get_protocol_name = openssl_iostream_get_protocol_name, + .get_ja3 = openssl_iostream_get_ja3, }; void ssl_iostream_openssl_init(void) diff --git a/src/lib-ssl-iostream/iostream-openssl.h b/src/lib-ssl-iostream/iostream-openssl.h index d88eb28e2c..fabc9a0723 100644 --- a/src/lib-ssl-iostream/iostream-openssl.h +++ b/src/lib-ssl-iostream/iostream-openssl.h @@ -47,6 +47,7 @@ struct ssl_iostream { char *sni_host; char *last_error; char *plain_stream_errstr; + const char *ja3_str; int plain_stream_errno; /* copied settings */ diff --git a/src/lib-ssl-iostream/iostream-ssl-private.h b/src/lib-ssl-iostream/iostream-ssl-private.h index 8c52bfad1a..97f13d4e51 100644 --- a/src/lib-ssl-iostream/iostream-ssl-private.h +++ b/src/lib-ssl-iostream/iostream-ssl-private.h @@ -50,6 +50,7 @@ struct iostream_ssl_vfuncs { const char *(*get_cipher)(struct ssl_iostream *ssl_io, unsigned int *bits_r); const char *(*get_pfs)(struct ssl_iostream *ssl_io); const char *(*get_protocol_name)(struct ssl_iostream *ssl_io); + const char *(*get_ja3)(struct ssl_iostream *ssl_io); }; void iostream_ssl_module_init(const struct iostream_ssl_vfuncs *vfuncs); diff --git a/src/lib-ssl-iostream/iostream-ssl.c b/src/lib-ssl-iostream/iostream-ssl.c index 2b56f7c01e..249c9ff1e8 100644 --- a/src/lib-ssl-iostream/iostream-ssl.c +++ b/src/lib-ssl-iostream/iostream-ssl.c @@ -342,3 +342,8 @@ const char *ssl_iostream_get_protocol_name(struct ssl_iostream *ssl_io) { return ssl_vfuncs->get_protocol_name(ssl_io); } + +const char *ssl_iostream_get_ja3(struct ssl_iostream *ssl_io) +{ + return ssl_vfuncs->get_ja3(ssl_io); +} diff --git a/src/lib-ssl-iostream/iostream-ssl.h b/src/lib-ssl-iostream/iostream-ssl.h index f9b82c2f7c..6eac7e8412 100644 --- a/src/lib-ssl-iostream/iostream-ssl.h +++ b/src/lib-ssl-iostream/iostream-ssl.h @@ -126,6 +126,12 @@ const char *ssl_iostream_get_peer_name(struct ssl_iostream *ssl_io); const char *ssl_iostream_get_compression(struct ssl_iostream *ssl_io); const char *ssl_iostream_get_server_name(struct ssl_iostream *ssl_io); const char *ssl_iostream_get_security_string(struct ssl_iostream *ssl_io); + +/* Returns ClientHello based JA3 string. Will return NULL + if it is not available due to no handshake performed, or + OpenSSL version is earlier than 1.1. */ +const char *ssl_iostream_get_ja3(struct ssl_iostream *ssl_io); + /* Returns SSL context's current used cipher algorithm. Returns NULL if SSL handshake has not been performed.