Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions README.keylog
Original file line number Diff line number Diff line change
@@ -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 "<cookie> <type> <key>".
* <cookie> 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.
* <type> 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.
* <key> 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 <type> 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:

<cookie> SHARED_SECRET <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 <interface> \
-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 <src_host> and host <dst_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:

<cookie> <optionnal 'REKEY' term> KEX_ALG <kex algo> SHARED_SECRET <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.


63 changes: 63 additions & 0 deletions kex.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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 ||
Expand Down Expand Up @@ -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");
Expand Down
11 changes: 10 additions & 1 deletion kex.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 *);
Expand Down Expand Up @@ -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
Expand Down
18 changes: 14 additions & 4 deletions kexc25519.c
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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;
}
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading