]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-ssl-iostream: Add JA3 string
authorAki Tuomi <aki.tuomi@open-xchange.com>
Thu, 21 Jul 2022 10:31:56 +0000 (13:31 +0300)
committerAki Tuomi <aki.tuomi@open-xchange.com>
Tue, 20 Dec 2022 09:28:23 +0000 (11:28 +0200)
m4/ssl.m4
src/lib-ssl-iostream/iostream-openssl-context.c
src/lib-ssl-iostream/iostream-openssl.c
src/lib-ssl-iostream/iostream-openssl.h
src/lib-ssl-iostream/iostream-ssl-private.h
src/lib-ssl-iostream/iostream-ssl.c
src/lib-ssl-iostream/iostream-ssl.h

index 2e246c625e431b2414c9823b75f680e9d4c5bc6b..d4ff3209fd3abecd578fb824f213584573478caa 100644 (file)
--- 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])
index 4086774032c045d0a83a37e4ee2ec7a3c854bfe3..30b19d12902d83ea154c3770644e93b27b67bdc1 100644 (file)
@@ -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 <openssl/pem.h>
 #include <openssl/ssl.h>
 #include <openssl/err.h>
+#include <arpa/inet.h>
 
 #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;
 }
index 7237f90703903143675c9da19a3c625cca8bc4dd..c0f52e6a9c5641d7681a2bbed2e3545c3a533683 100644 (file)
@@ -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)
index d88eb28e2cb4cfb2ed3c04f46e410c4da1cdb0a6..fabc9a072316e62223d7f49c57b4ba72fc99a212 100644 (file)
@@ -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 */
index 8c52bfad1a6e97774ba796567e50be3514f5ae03..97f13d4e51cef37fe80ba35003f36235f302be4e 100644 (file)
@@ -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);
index 2b56f7c01e39025dae68f053c9ac63af764ef4f7..249c9ff1e86a0b7ccbdd22a31681344593f6833d 100644 (file)
@@ -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);
+}
index f9b82c2f7c1ee11321dee1433f1cdea757930edb..6eac7e84120781cd88a417875e6bdbbfeb09d987 100644 (file)
@@ -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.