Skip to content

Commit 4fa69fd

Browse files
author
nbanba
committed
ssh: add support of SSHLEYLOGFILE to derive shared_secret for decryption
1 parent 832a770 commit 4fa69fd

File tree

8 files changed

+339
-9
lines changed

8 files changed

+339
-9
lines changed

README.keylog

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
OpenSSH keylog file
2+
--------------------
3+
4+
WARNING: Do not enable in production environments. This exposes session secrets.
5+
6+
Since OpenSSH is using Diffie-Hellman methode to comunicate on public network
7+
it's necessry to log somewhere the session cookie and the computed shared_secret
8+
to be able to decrypt traffic
9+
10+
Note that TLS 1.2+ is using the same methode (generate an SSLKEYLOGFILE) for
11+
traffic decryption.
12+
13+
Since TLS1.2+ using ECDHE_* cipher the `private_key` is not enough to decrypt
14+
trafic because the `session_keys` are needed and are not derived from the
15+
private_key
16+
17+
In 2025, Operating Systems TLS backend (GnuTLS or OpenSSL,...) usually provide
18+
a `keylog file` feature to help users dumping necessary informations to decrypt
19+
TLS traffic
20+
21+
As OpenSSH seems to use session keys approximatively the same way TLS 1.2+ is
22+
doing, the purpose of this feature is to add a keylog file feature to SSH client
23+
so anyone connecting a remote SSH server would be able to retrieve the computed
24+
shared_secret (or to derive it from the session private_key)
25+
26+
27+
KEYLOG file format
28+
------------------
29+
30+
One of the main goal of this feature is to be able to decrypt live SSH traffic
31+
with a tool like Wireshark / Tshark (https://gitlab.com/wireshark/wireshark).
32+
33+
So the keylog file format will be the one described in Wireshark / Tshark ssh
34+
packet dissector (in wireshark/epan/dissectors/packet-ssh.c):
35+
36+
Extract from Wireshark SSH dissector packet-ssh.c
37+
38+
/* File format: each line follows the format "<cookie> <type> <key>".
39+
* <cookie> is the hex-encoded (client or server) 16 bytes cookie
40+
* (32 characters) found in the SSH_MSG_KEXINIT of the endpoint whose
41+
* private random is disclosed.
42+
* <type> is either SHARED_SECRET or PRIVATE_KEY depending on the
43+
* type of key provided. PRIVAT_KEY is only supported for DH,
44+
* DH group exchange, and ECDH (including Curve25519) key exchanges.
45+
* <key> is the private random number that is used to generate the DH
46+
* negotiation (length depends on algorithm). In RFC4253 it is called
47+
* x for the client and y for the server.
48+
* For openssh and DH group exchange, it can be retrieved using
49+
* DH_get0_key(kex->dh, NULL, &server_random)
50+
* for groupN in file kexdh.c function kex_dh_compute_key
51+
* for custom group in file kexgexs.c function input_kex_dh_gex_init
52+
* For openssh and curve25519, it can be found in function kex_c25519_enc
53+
* in variable server_key. One may also provide the shared secret
54+
* directly if <type> is set to SHARED_SECRET.
55+
*
56+
* Example:
57+
* 90d886612f9c35903db5bb30d11f23c2 PRIVATE_KEY DEF830C22F6C927E31972FFB20B46C96D0A5F2D5E7BE5A3A8804D6BFC431619ED10AF589EEDFF4750DEA00EFD7AFDB814B6F3528729692B1F2482041521AE9DC
58+
*/
59+
60+
61+
Formatting note:
62+
----------------
63+
64+
As OpenSSH is computing the SHARED_SECRET for us, it's easier to log this session
65+
SHARED_SECRET in replacement of the session PRIVATE_KEY which will add complexity
66+
as you will have to compute the SHARED_SECRET yourself
67+
68+
So the format of the keylog file is:
69+
70+
<cookie> SHARED_SECRET <shared_secret>
71+
Example:
72+
a73e1ead2159740ae07a394b402e4acd SHARED_SECRET 2adf18b3dd7eb58f6d14b8256b9c8ee394e2f0d7b0c8b06fbcbc1ad41c331042
73+
74+
75+
76+
Keylog file first goal:
77+
-----------------------
78+
79+
The first goal of adding this support to OpenSSH is to be able to do live traffic
80+
decryption without any MIM (Man-In-the-Middle) proxy using a capture tool like Tshark
81+
which is Wireshark command line tool using a command like (output in a tcpdump style):
82+
83+
tshark -i <interface> \
84+
-n -l -t ad \
85+
-o ssh.keylog_file:/path/to/keylog_file.log \
86+
-o ssh.desegment_buffers:TRUE \
87+
-o tcp.desegment_tcp_streams:TRUE \
88+
-f 'host <src_host> and host <dst_host> and port 22' \
89+
-T fields \
90+
-e frame.number \
91+
-e frame.time_relative \
92+
-e ip.src \
93+
-e ip.dst \
94+
-e tcp.srcport \
95+
-e tcp.dstport \
96+
-e tcp.len \
97+
-e _ws.col.Protocol \
98+
-e _ws.col.Info \
99+
-e ssh.cookie \
100+
-e ssh.payload
101+
102+
103+
104+
How to use keylog file feature of OpenSSH ?
105+
---------------------------------------------
106+
107+
Simply export to environment file path and file name in SSHKEYLOGFILE variable
108+
109+
Example:
110+
export SSHKEYLOGFILE=~/ssh_keylog.log
111+
ssh user@host
112+
113+
And during session, you can see the cookie and the shared_secret logged to file
114+
~/ssh_keylog.log
115+
116+
For example:
117+
cat ~/ssh_keylog.log
118+
86f79664772735ddec07368663614c2c SHARED_SECRET 01bc538348137ed3a7fe2e720d00b6f66b06280da58a82c33a299b70f5d0f523
119+
79947161e967ab0200403669c94f1548 SHARED_SECRET f18497c66ec6993a1d769734b657a0cd2dd19659684097e1af606fabef039a32
120+
3122e0b88007d52e45593c21d7c2d104 SHARED_SECRET d19a874efd715276022c16e6b7b3a8777f993be4c8323d387e3fc844868de75b
121+
...
122+
123+
124+
OpenSSH rekeying:
125+
-----------------
126+
127+
When a "rekey" occurs, the new cookie and the new shared_secret are logged in the
128+
keylog file.
129+
It can be easily tested sith a command like:
130+
131+
ssh -F none -vvv -o RekeyLimit=1K $USER@localhost ls /
132+
# run a `tail -f` on keylog file from another terminal to see key logging in progress
133+
134+
135+
136+
Extended keylog file:
137+
---------------------
138+
139+
For those who need more detailed informations, you can also set SSHEXTKEYLOGFILE
140+
environment variable which produce a mode detailed file using format:
141+
142+
<cookie> <optionnal 'REKEY' term> KEX_ALG <kex algo> SHARED_SECRET <shared_secret>
143+
144+
For example:
145+
cat ~/ssh_ext_keylog.log
146+
4f1a61641d8864b1a941531f9638c68b KEX_ALG sntrup761x25519-sha512 SHARED_SECRET a6de23b55f6462494385e4f891035dee45ed1b7f4283e3929aa7bd362ecd295a
147+
54b9ba5193a4a8f0d01cf095a2b20d3b REKEY KEX_ALG sntrup761x25519-sha512 SHARED_SECRET a17e5303f10e753b94527fe9463cc41d914be2a8339d65137afa86ad6c99ef65
148+
8a3e42e48f007a22af4b929988048e43 REKEY KEX_ALG sntrup761x25519-sha512 SHARED_SECRET 59753eebb9db89657f5add6fdc063fedaab8fa33a330031b6f2adf76f97f6267
149+
150+
151+
How to use extended keylog file feature of OpenSSH ?
152+
-----------------------------------------------------
153+
154+
Simply export to environment file path and file name in SSHEXTKEYLOGFILE variable
155+
156+
Example:
157+
export SSHEXTKEYLOGFILE=~/ssh_ext_keylog.log
158+
ssh user@host
159+
160+
And during session, you can see the cookie, algo, rekey and the shared_secret logged to file
161+
~/ssh_ext_keylog.log
162+
163+
164+
DEBUG:
165+
------
166+
167+
You can enable DEBUG_KEX_COOKIE to validate that the cookie stored in keylog file is OK
168+
169+
To enable this debug flag, do:
170+
./configure CFLAGS="-DDEBUG_KEX_COOKIE"
171+
make
172+
173+
NOTES:
174+
------
175+
176+
This feature log the shared_secret for algo:
177+
178+
DH:
179+
- diffie-hellman-group1-sha1
180+
- diffie-hellman-group14-sha1
181+
- diffie-hellman-group14-sha256
182+
- diffie-hellman-group16-sha512
183+
- diffie-hellman-group18-sha512
184+
- diffie-hellman-group-exchange-sha1
185+
- diffie-hellman-group-exchange-sha256
186+
187+
ECDH:
188+
- ecdh-sha2-nistp256
189+
- ecdh-sha2-nistp384
190+
- ecdh-sha2-nistp521
191+
192+
ED25519 / KEMs
193+
- curve25519-sha256
194+
- sntrup761x25519-sha512
195+
- mlkem768x25519-sha256
196+
197+
It can be tested with command: (here algo is curve25519-sha256)
198+
ssh -F none -o KexAlgorithms=curve25519-sha256 -o RekeyLimit=1K ${USER}@localhost ls / 2>&1 >/dev/null
199+
200+
201+
DEVELOPEMENT NOTES:
202+
-------------------
203+
204+
To enable this feature, the following files where patched:
205+
206+
kex.h: modifying 'kex' structure to store session cookie in kex structure
207+
declaring helper action: sshlog_keylog_file
208+
209+
kex.c: adding helper action: sshlog_keylog_file
210+
copying the cookie to 'kex' structure
211+
212+
kexc25519.c: modifying kexc25519_shared_key_ext to take 'kex' structure and to call sshlog_keylog_file
213+
modifying all calls to kexc25519_shared_key_ext (adding 'kex' structure)
214+
adding skip logging for hybrid KEMs like sntrup761x25519 and mlkem768x25519
215+
calling function sshlog_keylog_file in kexc25519_shared_key_ext for algo curve25519-sha256
216+
217+
kexsntrup761x25519.c: calling function sshlog_keylog_file in kex_kem_sntrup761x25519_dec
218+
219+
kexmlkem768x25519.c : calling function sshlog_keylog_file in kex_kem_mlkem768x25519_dec
220+
221+
kexdh.c: calling function sshlog_keylog_file in kex_dh_compute_key
222+
223+
kexecdh.c: calling function sshlog_keylog_file in kex_ecdh_dec_key_group
224+
225+
226+
WARNING:
227+
--------
228+
229+
Do not enable in production environments. This exposes session secrets.
230+
231+
232+
-----------------------------------------------------------------------

kex.c

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,55 @@ kex_input_newkeys(int type, u_int32_t seq, struct ssh *ssh)
564564
return 0;
565565
}
566566

567+
/* ___add helper for KEYLOG FILE support */
568+
void
569+
sshlog_keylog_file(const struct kex *kex, const u_char *shared_key, size_t shared_key_len)
570+
{
571+
/* ___add logging cookie + shared_key to keylog file in Wireshark dissector format */
572+
char *keylog_path;
573+
FILE *keylog = NULL;
574+
575+
if ((keylog_path = getenv("SSHKEYLOGFILE")) != NULL)
576+
{
577+
keylog = fopen(keylog_path, "a");
578+
if (keylog != NULL)
579+
{
580+
for (int i = 0; i < 16; i++)
581+
fprintf(keylog, "%02x", kex->cookie[i]);
582+
fprintf(keylog, " SHARED_SECRET ");
583+
for (size_t i = 0; i < shared_key_len; i++)
584+
fprintf(keylog, "%02x", shared_key[i]);
585+
fprintf(keylog, "\n");
586+
fclose(keylog);
587+
}
588+
}
589+
590+
/* ___add extended logging to optionnal extended keylog file */
591+
char *ext_keylog_path;
592+
FILE *ext_keylog = NULL;
593+
594+
if ((ext_keylog_path = getenv("SSHEXTKEYLOGFILE")) != NULL)
595+
{
596+
ext_keylog = fopen(ext_keylog_path, "a");
597+
if (ext_keylog != NULL)
598+
{
599+
// Write cookie
600+
for (int i = 0; i < 16; i++)
601+
fprintf(ext_keylog, "%02x", kex->cookie[i]);
602+
// Add optional metadata
603+
if (!(kex->flags & KEX_INITIAL))
604+
fprintf(ext_keylog, " REKEY");
605+
if (kex->name)
606+
fprintf(ext_keylog, " KEX_ALG %s", kex->name);
607+
fprintf(ext_keylog, " SHARED_SECRET ");
608+
for (size_t i = 0; i < shared_key_len; i++)
609+
fprintf(ext_keylog, "%02x", shared_key[i]);
610+
fprintf(ext_keylog, "\n");
611+
fclose(ext_keylog);
612+
}
613+
}
614+
}
615+
567616
int
568617
kex_send_kexinit(struct ssh *ssh)
569618
{
@@ -590,6 +639,17 @@ kex_send_kexinit(struct ssh *ssh)
590639
return SSH_ERR_INTERNAL_ERROR;
591640
}
592641
arc4random_buf(cookie, KEX_COOKIE_LEN);
642+
#ifdef DEBUG_KEX_COOKIE
643+
/* ___output cookie on stderr to compare with cookie in keylog file */
644+
for (int i = 0; i < 16; i++)
645+
{
646+
fprintf(stderr, "%02x", cookie[i]);
647+
}
648+
fprintf(stderr, "\n");
649+
#endif
650+
/* ___keylog file need to store cookie in kex structure */
651+
memcpy(kex->cookie, cookie, 16);
652+
memcpy(kex->client_cookie, kex->my, 16);
593653

594654
if ((r = sshpkt_start(ssh, SSH2_MSG_KEXINIT)) != 0 ||
595655
(r = sshpkt_putb(ssh, kex->my)) != 0 ||
@@ -630,6 +690,9 @@ kex_input_kexinit(int type, u_int32_t seq, struct ssh *ssh)
630690
return r;
631691
}
632692
}
693+
/* ___keylog file need to store cookie in kex structure */
694+
memcpy(kex->server_cookie, kex->peer, 16);
695+
633696
for (i = 0; i < PROPOSAL_MAX; i++) {
634697
if ((r = sshpkt_get_string(ssh, NULL, NULL)) != 0) {
635698
error_fr(r, "discard proposal");

kex.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,10 @@ struct kex {
187187
u_char sntrup761_client_key[crypto_kem_sntrup761_SECRETKEYBYTES]; /* KEM */
188188
u_char mlkem768_client_key[crypto_kem_mlkem768_SECRETKEYBYTES]; /* KEM */
189189
struct sshbuf *client_pub;
190+
/* ___store cookie for KEYLOG file */
191+
u_char client_cookie[16]; // optional to store client_cookie
192+
u_char server_cookie[16]; // optional to store server_cookie
193+
u_char cookie[16]; // used to store current cookie
190194
};
191195

192196
int kex_name_valid(const char *);
@@ -276,11 +280,16 @@ int kexc25519_shared_key(const u_char key[CURVE25519_SIZE],
276280
const u_char pub[CURVE25519_SIZE], struct sshbuf *out)
277281
__attribute__((__bounded__(__minbytes__, 1, CURVE25519_SIZE)))
278282
__attribute__((__bounded__(__minbytes__, 2, CURVE25519_SIZE)));
279-
int kexc25519_shared_key_ext(const u_char key[CURVE25519_SIZE],
283+
/* ___keylog file need to pass kex struct to kexc25519_shared_key_ext */
284+
int kexc25519_shared_key_ext(struct kex *kex, const u_char key[CURVE25519_SIZE],
280285
const u_char pub[CURVE25519_SIZE], struct sshbuf *out, int)
281286
__attribute__((__bounded__(__minbytes__, 1, CURVE25519_SIZE)))
282287
__attribute__((__bounded__(__minbytes__, 2, CURVE25519_SIZE)));
283288

289+
/* ___add for keylog file helper in kex.c */
290+
void sshlog_keylog_file(const struct kex *kex, const u_char *shared_key, size_t shared_key_len);
291+
292+
284293
#if defined(DEBUG_KEX) || defined(DEBUG_KEXDH) || defined(DEBUG_KEXECDH)
285294
void dump_digest(const char *, const u_char *, int);
286295
#endif

kexc25519.c

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ kexc25519_keygen(u_char key[CURVE25519_SIZE], u_char pub[CURVE25519_SIZE])
5656
}
5757

5858
int
59-
kexc25519_shared_key_ext(const u_char key[CURVE25519_SIZE],
59+
kexc25519_shared_key_ext(struct kex *kex, const u_char key[CURVE25519_SIZE],
6060
const u_char pub[CURVE25519_SIZE], struct sshbuf *out, int raw)
6161
{
6262
u_char shared_key[CURVE25519_SIZE];
@@ -77,6 +77,12 @@ kexc25519_shared_key_ext(const u_char key[CURVE25519_SIZE],
7777
r = sshbuf_put(out, shared_key, CURVE25519_SIZE);
7878
else
7979
r = sshbuf_put_bignum2_bytes(out, shared_key, CURVE25519_SIZE);
80+
/* ___add logging shared_key to keylog file befre zeroing it */
81+
if (kex->kex_type != KEX_KEM_SNTRUP761X25519_SHA512 &&
82+
kex->kex_type != KEX_KEM_MLKEM768X25519_SHA256)
83+
{
84+
sshlog_keylog_file(kex, shared_key, CURVE25519_SIZE);
85+
}
8086
explicit_bzero(shared_key, CURVE25519_SIZE);
8187
return r;
8288
}
@@ -85,7 +91,9 @@ int
8591
kexc25519_shared_key(const u_char key[CURVE25519_SIZE],
8692
const u_char pub[CURVE25519_SIZE], struct sshbuf *out)
8793
{
88-
return kexc25519_shared_key_ext(key, pub, out, 0);
94+
/* ___keylog file need to pass NULL for the struct *kex
95+
* (not a real key exchange) */
96+
return kexc25519_shared_key_ext(NULL, key, pub, out, 0);
8997
}
9098

9199
int
@@ -145,7 +153,8 @@ kex_c25519_enc(struct kex *kex, const struct sshbuf *client_blob,
145153
r = SSH_ERR_ALLOC_FAIL;
146154
goto out;
147155
}
148-
if ((r = kexc25519_shared_key_ext(server_key, client_pub, buf, 0)) < 0)
156+
/* ___keylog file need to pass kex struct to kexc25519_shared_key_ext */
157+
if ((r = kexc25519_shared_key_ext(kex, server_key, client_pub, buf, 0)) < 0)
149158
goto out;
150159
#ifdef DEBUG_KEXECDH
151160
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,
185194
r = SSH_ERR_ALLOC_FAIL;
186195
goto out;
187196
}
188-
if ((r = kexc25519_shared_key_ext(kex->c25519_client_key, server_pub,
197+
/* ___keylog file need to pass kex struct to kexc25519_shared_key_ext */
198+
if ((r = kexc25519_shared_key_ext(kex, kex->c25519_client_key, server_pub,
189199
buf, 0)) < 0)
190200
goto out;
191201
#ifdef DEBUG_KEXECDH

0 commit comments

Comments
 (0)