diff --git a/README.keylog b/README.keylog new file mode 100644 index 00000000000..21eaf3d2ca4 --- /dev/null +++ b/README.keylog @@ -0,0 +1,231 @@ +OpenSSH keylog file +-------------------- + +WARNING: Do not enable in production environments. This exposes session secrets. + +Since OpenSSH is using Diffie-Hellman methode to comunicate on public network +it's necessry to log somewhere the session cookie and the computed shared_secret +to be able to decrypt traffic + +Note that TLS 1.2+ is using the same methode (generate an SSLKEYLOGFILE) for +traffic decryption. + +Since TLS1.2+ using ECDHE_* cipher the `private_key` is not enough to decryot +trafic because the `session_keys` are needed and are not derived from the +private_key + +In 2025, Operating Systems TLS backend (GnuTLS or OpenSSL,...) usually provide +a `keylog file` feature to help users dumping necessary informations to decrypt +TLS traffic + +As OpenSSH seems to use session keys approximatively the same way TLS 1.2+ is +doing, the purpose of this feature is to add a keylog file feature to SSH client +so anyone connecting a remote SSH server would be able to retrieve the computed +shared_secret (or to derive it from the session private_key) + + +KEYLOG file format +------------------ + +One of the main goal of this feature is to be able to decrypt live SSH traffic +with a tool like Wireshark / Tshark (https://gitlab.com/wireshark/wireshark). + +So the keylog file format will be the one described in Wireshark / Tshark ssh +packet dissector (in wireshark/epan/dissectors/packet-ssh.c): + +Extract from Wireshark SSH dissector packet-ssh.c + + /* File format: each line follows the format " ". + * is the hex-encoded (client or server) 16 bytes cookie + * (32 characters) found in the SSH_MSG_KEXINIT of the endpoint whose + * private random is disclosed. + * is either SHARED_SECRET or PRIVATE_KEY depending on the + * type of key provided. PRIVAT_KEY is only supported for DH, + * DH group exchange, and ECDH (including Curve25519) key exchanges. + * is the private random number that is used to generate the DH + * negotiation (length depends on algorithm). In RFC4253 it is called + * x for the client and y for the server. + * For openssh and DH group exchange, it can be retrieved using + * DH_get0_key(kex->dh, NULL, &server_random) + * for groupN in file kexdh.c function kex_dh_compute_key + * for custom group in file kexgexs.c function input_kex_dh_gex_init + * For openssh and curve25519, it can be found in function kex_c25519_enc + * in variable server_key. One may also provide the shared secret + * directly if is set to SHARED_SECRET. + * + * Example: + * 90d886612f9c35903db5bb30d11f23c2 PRIVATE_KEY DEF830C22F6C927E31972FFB20B46C96D0A5F2D5E7BE5A3A8804D6BFC431619ED10AF589EEDFF4750DEA00EFD7AFDB814B6F3528729692B1F2482041521AE9DC + */ + + +Formatting note: +---------------- + +As OpenSSH is computing the SHARED_SECRET for us, it's easier to log this session +SHARED_SECRET in replacement of the session PRIVATE_KEY which will add complexity +as you will have to compute the SHARED_SECRET yourself + +So the format of the keylog file is: + + SHARED_SECRET +Example: +a73e1ead2159740ae07a394b402e4acd SHARED_SECRET 2adf18b3dd7eb58f6d14b8256b9c8ee394e2f0d7b0c8b06fbcbc1ad41c331042 + + + +Keylog file first goal: +----------------------- + +The first goal of adding this support to OpenSSH is to be able to do live traffic +decryption without any MIM (Man-In-the-Middle) proxy using a capture tool like Tshark +which is Wireshark command line tool using a command like (output in a tcpdump style): + + tshark -i \ + -n -l -t ad \ + -o ssh.keylog_file:/path/to/keylog_file.log \ + -o ssh.desegment_buffers:TRUE \ + -o tcp.desegment_tcp_streams:TRUE \ + -f 'host and host and port 22' \ + -T fields \ + -e frame.number \ + -e frame.time_relative \ + -e ip.src \ + -e ip.dst \ + -e tcp.srcport \ + -e tcp.dstport \ + -e tcp.len \ + -e _ws.col.Protocol \ + -e _ws.col.Info \ + -e ssh.cookie \ + -e ssh.payload + + + +How to use keylog file feature of OpenSSH ? +--------------------------------------------- + +Simply export to environment file path and file name in SSHKEYLOGFILE variable + +Example: + export SSHKEYLOGFILE=~/ssh_keylog.log + ssh user@host + +And during session, you can see the cookie and the shared_secret logged to file +~/ssh_keylog.log + +For example: + cat ~/ssh_keylog.log + 86f79664772735ddec07368663614c2c SHARED_SECRET 01bc538348137ed3a7fe2e720d00b6f66b06280da58a82c33a299b70f5d0f523 + 79947161e967ab0200403669c94f1548 SHARED_SECRET f18497c66ec6993a1d769734b657a0cd2dd19659684097e1af606fabef039a32 + 3122e0b88007d52e45593c21d7c2d104 SHARED_SECRET d19a874efd715276022c16e6b7b3a8777f993be4c8323d387e3fc844868de75b + ... + + +OpenSSH rekeying: +----------------- + +When a "rekey" occurs, the new cookie and the new shared_secret are logged in the +keylog file. +It can be easily tested sith a command like: + + ssh -F none -vvv -o RekeyLimit=1K $USER@localhost ls / + # run a `tail -f` on keylog file from another terminal to see key logging in progress + + + +Extended keylog file: +--------------------- + +For those who need more detailed informations, you can also set SSHEXTKEYLOGFILE +environment variable which produce a mode detailed file using format: + + KEX_ALG SHARED_SECRET + +For example: + cat ~/ssh_ext_keylog.log + 4f1a61641d8864b1a941531f9638c68b KEX_ALG sntrup761x25519-sha512 SHARED_SECRET a6de23b55f6462494385e4f891035dee45ed1b7f4283e3929aa7bd362ecd295a + 54b9ba5193a4a8f0d01cf095a2b20d3b REKEY KEX_ALG sntrup761x25519-sha512 SHARED_SECRET a17e5303f10e753b94527fe9463cc41d914be2a8339d65137afa86ad6c99ef65 + 8a3e42e48f007a22af4b929988048e43 REKEY KEX_ALG sntrup761x25519-sha512 SHARED_SECRET 59753eebb9db89657f5add6fdc063fedaab8fa33a330031b6f2adf76f97f6267 + + +How to use extended keylog file feature of OpenSSH ? +----------------------------------------------------- + +Simply export to environment file path and file name in SSHEXTKEYLOGFILE variable + +Example: + export SSHEXTKEYLOGFILE=~/ssh_ext_keylog.log + ssh user@host + +And during session, you can see the cookie, algo, rekey and the shared_secret logged to file +~/ssh_ext_keylog.log + + +DEBUG: +------ + +You can enable DEBUG_KEX_COOKIE to validate that the cookie stored in keylog file is OK + +To enable this debug flag, do: + ./configure CFLAGS="-DDEBUG_KEX_COOKIE" + make + +NOTES: +------ + +This feature log the shared_secret for algo: + +DH: +- diffie-hellman-group1-sha1 +- diffie-hellman-group14-sha1 +- diffie-hellman-group14-sha256 +- diffie-hellman-group16-sha512 +- diffie-hellman-group18-sha512 +- diffie-hellman-group-exchange-sha1 +- diffie-hellman-group-exchange-sha256 + +ECDH: +- ecdh-sha2-nistp256 +- ecdh-sha2-nistp384 +- ecdh-sha2-nistp521 + +ED25519 / KEMs +- curve25519-sha256 +- sntrup761x25519-sha512 +- mlkem768x25519-sha256 + +It can be tested with command: (here algo is curve25519-sha256) +ssh -F none -o KexAlgorithms=curve25519-sha256 -o RekeyLimit=1K ${USER}@localhost ls / 2>&1 >/dev/null + + +DEVELOPEMENT NOTES: +------------------- + +To enable this feature, the following files where patched: + +kex.h: modifying 'kex' structure to store session cookie in kex structure + declaring helper action: sshlog_keylog_file + +kex.c: adding helper action: sshlog_keylog_file + copying the cookie to 'kex' structure + +kexc25519.c: modifying kexc25519_shared_key_ext to take 'kex' structure and to call sshlog_keylog_file + modifying all calls to kexc25519_shared_key_ext (adding 'kex' structure) + adding skip logging for hybrid KEMs like sntrup761x25519 and mlkem768x25519 + calling function sshlog_keylog_file in kexc25519_shared_key_ext for algo curve25519-sha256 + +kexsntrup761x25519.c: calling function sshlog_keylog_file in kex_kem_sntrup761x25519_dec + +kexmlkem768x25519.c : calling function sshlog_keylog_file in kex_kem_mlkem768x25519_dec + +kexdh.c: calling function sshlog_keylog_file in kex_dh_compute_key + +kexecdh.c: calling function sshlog_keylog_file in kex_ecdh_dec_key_group + + +WARNING: +-------- + +Do not enable in production environments. This exposes session secrets. + + diff --git a/kex.c b/kex.c index 6b957e5e18f..fdb3ed65356 100644 --- a/kex.c +++ b/kex.c @@ -568,6 +568,55 @@ kex_input_newkeys(int type, u_int32_t seq, struct ssh *ssh) return 0; } +/* ___add helper for KEYLOG FILE support */ +void +sshlog_keylog_file(const struct kex *kex, const u_char *shared_key, size_t shared_key_len) +{ + /* ___add logging cookie + shared_key to keylog file in Wireshark dissector format */ + char *keylog_path; + FILE *keylog = NULL; + + if ((keylog_path = getenv("SSHKEYLOGFILE")) != NULL) + { + keylog = fopen(keylog_path, "a"); + if (keylog != NULL) + { + for (int i = 0; i < 16; i++) + fprintf(keylog, "%02x", kex->cookie[i]); + fprintf(keylog, " SHARED_SECRET "); + for (size_t i = 0; i < shared_key_len; i++) + fprintf(keylog, "%02x", shared_key[i]); + fprintf(keylog, "\n"); + fclose(keylog); + } + } + + /* ___add extended logging to optionnal extended keylog file */ + char *ext_keylog_path; + FILE *ext_keylog = NULL; + + if ((ext_keylog_path = getenv("SSHEXTKEYLOGFILE")) != NULL) + { + ext_keylog = fopen(ext_keylog_path, "a"); + if (ext_keylog != NULL) + { + // Write cookie + for (int i = 0; i < 16; i++) + fprintf(ext_keylog, "%02x", kex->cookie[i]); + // Add optional metadata + if (!(kex->flags & KEX_INITIAL)) + fprintf(ext_keylog, " REKEY"); + if (kex->name) + fprintf(ext_keylog, " KEX_ALG %s", kex->name); + fprintf(ext_keylog, " SHARED_SECRET "); + for (size_t i = 0; i < shared_key_len; i++) + fprintf(ext_keylog, "%02x", shared_key[i]); + fprintf(ext_keylog, "\n"); + fclose(ext_keylog); + } + } +} + int kex_send_kexinit(struct ssh *ssh) { @@ -594,6 +643,17 @@ kex_send_kexinit(struct ssh *ssh) return SSH_ERR_INTERNAL_ERROR; } arc4random_buf(cookie, KEX_COOKIE_LEN); +#ifdef DEBUG_KEX_COOKIE + /* ___output cookie on stderr to compare with cookie in keylog file */ + for (int i = 0; i < 16; i++) + { + fprintf(stderr, "%02x", cookie[i]); + } + fprintf(stderr, "\n"); +#endif + /* ___keylog file need to store cookie in kex structure */ + memcpy(kex->cookie, cookie, 16); + memcpy(kex->client_cookie, kex->my, 16); if ((r = sshpkt_start(ssh, SSH2_MSG_KEXINIT)) != 0 || (r = sshpkt_putb(ssh, kex->my)) != 0 || @@ -632,6 +692,9 @@ kex_input_kexinit(int type, u_int32_t seq, struct ssh *ssh) return r; } } + /* ___keylog file need to store cookie in kex structure */ + memcpy(kex->server_cookie, kex->peer, 16); + for (i = 0; i < PROPOSAL_MAX; i++) { if ((r = sshpkt_get_string(ssh, NULL, NULL)) != 0) { error_fr(r, "discard proposal"); diff --git a/kex.h b/kex.h index d08988b3e14..e187eeb62ce 100644 --- a/kex.h +++ b/kex.h @@ -183,6 +183,10 @@ struct kex { u_char sntrup761_client_key[crypto_kem_sntrup761_SECRETKEYBYTES]; /* KEM */ u_char mlkem768_client_key[crypto_kem_mlkem768_SECRETKEYBYTES]; /* KEM */ struct sshbuf *client_pub; + /* ___store cookie for KEYLOG file */ + u_char client_cookie[16]; // optional to store client_cookie + u_char server_cookie[16]; // optional to store server_cookie + u_char cookie[16]; // used to store current cookie }; int kex_name_valid(const char *); @@ -271,11 +275,16 @@ int kexc25519_shared_key(const u_char key[CURVE25519_SIZE], const u_char pub[CURVE25519_SIZE], struct sshbuf *out) __attribute__((__bounded__(__minbytes__, 1, CURVE25519_SIZE))) __attribute__((__bounded__(__minbytes__, 2, CURVE25519_SIZE))); -int kexc25519_shared_key_ext(const u_char key[CURVE25519_SIZE], +/* ___keylog file need to pass kex struct to kexc25519_shared_key_ext */ +int kexc25519_shared_key_ext(struct kex *kex, const u_char key[CURVE25519_SIZE], const u_char pub[CURVE25519_SIZE], struct sshbuf *out, int) __attribute__((__bounded__(__minbytes__, 1, CURVE25519_SIZE))) __attribute__((__bounded__(__minbytes__, 2, CURVE25519_SIZE))); +/* ___add for keylog file helper in kex.c */ +void sshlog_keylog_file(const struct kex *kex, const u_char *shared_key, size_t shared_key_len); + + #if defined(DEBUG_KEX) || defined(DEBUG_KEXDH) || defined(DEBUG_KEXECDH) void dump_digest(const char *, const u_char *, int); #endif diff --git a/kexc25519.c b/kexc25519.c index e106521d9cf..f7bd9d87c40 100644 --- a/kexc25519.c +++ b/kexc25519.c @@ -56,7 +56,7 @@ kexc25519_keygen(u_char key[CURVE25519_SIZE], u_char pub[CURVE25519_SIZE]) } int -kexc25519_shared_key_ext(const u_char key[CURVE25519_SIZE], +kexc25519_shared_key_ext(struct kex *kex, const u_char key[CURVE25519_SIZE], const u_char pub[CURVE25519_SIZE], struct sshbuf *out, int raw) { u_char shared_key[CURVE25519_SIZE]; @@ -77,6 +77,12 @@ kexc25519_shared_key_ext(const u_char key[CURVE25519_SIZE], r = sshbuf_put(out, shared_key, CURVE25519_SIZE); else r = sshbuf_put_bignum2_bytes(out, shared_key, CURVE25519_SIZE); + /* ___add logging shared_key to keylog file befre zeroing it */ + if (kex->kex_type != KEX_KEM_SNTRUP761X25519_SHA512 && + kex->kex_type != KEX_KEM_MLKEM768X25519_SHA256) + { + sshlog_keylog_file(kex, shared_key, CURVE25519_SIZE); + } explicit_bzero(shared_key, CURVE25519_SIZE); return r; } @@ -85,7 +91,9 @@ int kexc25519_shared_key(const u_char key[CURVE25519_SIZE], const u_char pub[CURVE25519_SIZE], struct sshbuf *out) { - return kexc25519_shared_key_ext(key, pub, out, 0); + /* ___keylog file need to pass NULL for the struct *kex + * (not a real key exchange) */ + return kexc25519_shared_key_ext(NULL, key, pub, out, 0); } int @@ -145,7 +153,8 @@ kex_c25519_enc(struct kex *kex, const struct sshbuf *client_blob, r = SSH_ERR_ALLOC_FAIL; goto out; } - if ((r = kexc25519_shared_key_ext(server_key, client_pub, buf, 0)) < 0) + /* ___keylog file need to pass kex struct to kexc25519_shared_key_ext */ + if ((r = kexc25519_shared_key_ext(kex, server_key, client_pub, buf, 0)) < 0) goto out; #ifdef DEBUG_KEXECDH dump_digest("server public key 25519:", server_pub, CURVE25519_SIZE); @@ -185,7 +194,8 @@ kex_c25519_dec(struct kex *kex, const struct sshbuf *server_blob, r = SSH_ERR_ALLOC_FAIL; goto out; } - if ((r = kexc25519_shared_key_ext(kex->c25519_client_key, server_pub, + /* ___keylog file need to pass kex struct to kexc25519_shared_key_ext */ + if ((r = kexc25519_shared_key_ext(kex, kex->c25519_client_key, server_pub, buf, 0)) < 0) goto out; #ifdef DEBUG_KEXECDH diff --git a/kexdh.c b/kexdh.c index c1084f2146e..37ab7595221 100644 --- a/kexdh.c +++ b/kexdh.c @@ -101,6 +101,9 @@ kex_dh_compute_key(struct kex *kex, BIGNUM *dh_pub, struct sshbuf *out) r = SSH_ERR_LIBCRYPTO_ERROR; goto out; } + /* ___keylog logging for DH */ + sshlog_keylog_file(kex, kbuf, kout); + #ifdef DEBUG_KEXDH dump_digest("shared secret", kbuf, kout); #endif diff --git a/kexecdh.c b/kexecdh.c index efb2e55a6d4..2e7b20508bc 100644 --- a/kexecdh.c +++ b/kexecdh.c @@ -181,9 +181,14 @@ kex_ecdh_dec_key_group(struct kex *kex, const struct sshbuf *ec_blob, r = SSH_ERR_LIBCRYPTO_ERROR; goto out; } + #ifdef DEBUG_KEXECDH dump_digest("shared secret", kbuf, klen); #endif + + /* ___keylog logging for ECDH */ + sshlog_keylog_file(kex, kbuf, klen); + if ((r = sshbuf_put_bignum2(buf, shared_secret)) != 0) goto out; *shared_secretp = buf; diff --git a/kexmlkem768x25519.c b/kexmlkem768x25519.c index 2b5d3960823..8d76cbb5341 100644 --- a/kexmlkem768x25519.c +++ b/kexmlkem768x25519.c @@ -151,7 +151,8 @@ kex_kem_mlkem768x25519_enc(struct kex *kex, goto out; /* append ECDH shared key */ client_pub += crypto_kem_mlkem768_PUBLICKEYBYTES; - if ((r = kexc25519_shared_key_ext(server_key, client_pub, buf, 1)) < 0) + /* ___keylog file need to pass kex struct to kexc25519_shared_key_ext */ + if ((r = kexc25519_shared_key_ext(kex, server_key, client_pub, buf, 1)) < 0) goto out; if ((r = ssh_digest_buffer(kex->hash_alg, buf, hash, sizeof(hash))) != 0) goto out; @@ -229,12 +230,15 @@ kex_kem_mlkem768x25519_dec(struct kex *kex, &mlkem_ciphertext, mlkem_key); if ((r = sshbuf_put(buf, mlkem_key, sizeof(mlkem_key))) != 0) goto out; - if ((r = kexc25519_shared_key_ext(kex->c25519_client_key, server_pub, + /* ___keylog file hash to pass kex struct to kexc25519_shared_key_ext */ + if ((r = kexc25519_shared_key_ext(kex, kex->c25519_client_key, server_pub, buf, 1)) < 0) goto out; if ((r = ssh_digest_buffer(kex->hash_alg, buf, hash, sizeof(hash))) != 0) goto out; + /* ___add logging hash to keylog file befre zeroing it */ + sshlog_keylog_file(kex, hash, ssh_digest_bytes(kex->hash_alg)); #ifdef DEBUG_KEXECDH dump_digest("client kem key:", mlkem_key, sizeof(mlkem_key)); dump_digest("concatenation of KEM key and ECDH shared key:", diff --git a/kexsntrup761x25519.c b/kexsntrup761x25519.c index 6bbca71fcef..724fc8f1368 100644 --- a/kexsntrup761x25519.c +++ b/kexsntrup761x25519.c @@ -128,7 +128,8 @@ kex_kem_sntrup761x25519_enc(struct kex *kex, kexc25519_keygen(server_key, server_pub); /* append ECDH shared key */ client_pub += crypto_kem_sntrup761_PUBLICKEYBYTES; - if ((r = kexc25519_shared_key_ext(server_key, client_pub, buf, 1)) < 0) + /* ___keylog file need to pass kex struct to kexc25519_shared_key_ext */ + if ((r = kexc25519_shared_key_ext(kex, server_key, client_pub, buf, 1)) < 0) goto out; if ((r = ssh_digest_buffer(kex->hash_alg, buf, hash, sizeof(hash))) != 0) goto out; @@ -195,11 +196,14 @@ kex_kem_sntrup761x25519_dec(struct kex *kex, goto out; decoded = crypto_kem_sntrup761_dec(kem_key, ciphertext, kex->sntrup761_client_key); - if ((r = kexc25519_shared_key_ext(kex->c25519_client_key, server_pub, + /* ___keylog file need to pass kex struct to kexc25519_shared_key_ext */ + if ((r = kexc25519_shared_key_ext(kex, kex->c25519_client_key, server_pub, buf, 1)) < 0) goto out; if ((r = ssh_digest_buffer(kex->hash_alg, buf, hash, sizeof(hash))) != 0) goto out; + /* ___add logging hash to keylog file before zeroing it */ + sshlog_keylog_file(kex, hash, ssh_digest_bytes(kex->hash_alg)); #ifdef DEBUG_KEXECDH dump_digest("client kem key:", kem_key, crypto_kem_sntrup761_BYTES); dump_digest("concatenation of KEM key and ECDH shared key:",