diff --git a/epan/dissectors/packet-wireguard.c b/epan/dissectors/packet-wireguard.c index d4cdea8166..1d8192a14f 100644 --- a/epan/dissectors/packet-wireguard.c +++ b/epan/dissectors/packet-wireguard.c @@ -120,12 +120,27 @@ typedef struct wg_skey { wg_qqword priv_key; /* Optional, set to all zeroes if missing. */ } wg_skey_t; +/* + * Pre-shared key, needed while processing the handshake response message. At + * that point, ephemeral keys (from either the initiator or responder) should be + * known. Thus link the PSK to such ephemeral keys. + * + * Usually a "wg_ekey_t" contains an empty list (if there is no PSK, i.e. an + * all-zeroes PSK) or one item (if a PSK is configured). In the unlikely event + * that an ephemeral key is reused, support more than one PSK. + */ +typedef struct wg_psk { + wg_qqword psk_data; + struct wg_psk *next; +} wg_psk_t; + /* * Ephemeral key. */ typedef struct wg_ekey { wg_qqword pub_key; wg_qqword priv_key; /* Optional, set to all zeroes if missing. */ + wg_psk_t *psk_list; /* Optional, possible PSKs to try. */ } wg_ekey_t; /* @@ -148,6 +163,29 @@ static wmem_map_t *wg_ephemeral_keys; */ static FILE *wg_keylog_file; +/* + * The most recently parsed ephemeral key. If a PSK is configured, the key log + * file must have a PSK line after other keys. If not, then it is assumed that + * the session does not use a PSK. + * + * This pointer is cleared when the key log file is reset (i.e. when the capture + * file closes). + */ +static wg_ekey_t *wg_keylog_last_ekey; + +enum wg_psk_iter_state { + WG_PSK_ITER_STATE_ENTER = 0, + WG_PSK_ITER_STATE_INITIATOR, + WG_PSK_ITER_STATE_RESPONDER, + WG_PSK_ITER_STATE_EXIT +}; + +/* See wg_psk_iter_next. */ +typedef struct { + enum wg_psk_iter_state state; + wg_psk_t *next_psk; +} wg_psk_iter_context; + /* UAT adapter for populating wg_static_keys. */ enum { WG_KEY_UAT_PUBLIC, WG_KEY_UAT_PRIVATE }; static const value_string wg_key_uat_type_vals[] = { @@ -531,6 +569,54 @@ wg_add_ephemeral_privkey(const wg_qqword *priv_key) return key; } +/* PSK handling. {{{ */ +static void +wg_add_psk(wg_ekey_t *ekey, const wg_qqword *psk) +{ + wg_psk_t *psk_entry = wmem_new0(wmem_file_scope(), wg_psk_t); + psk_entry->psk_data = *psk; + psk_entry->next = ekey->psk_list; + ekey->psk_list = psk_entry; +} + +/* + * Retrieves the next PSK to try and returns TRUE if one is found or FALSE if + * there are no more to try. + */ +static gboolean +wg_psk_iter_next(wg_psk_iter_context *psk_iter, const wg_handshake_state_t *hs, + wg_qqword *psk_out) +{ + wg_psk_t *psk = psk_iter->next_psk; + while (!psk) { + /* + * Yield PSKs based on Epub_i, then those based on Epub_r, then yield an + * all-zeroes key and finally fail in the terminating state. + */ + switch (psk_iter->state) { + case WG_PSK_ITER_STATE_ENTER: + psk = hs->initiator_ekey->psk_list; + psk_iter->state = WG_PSK_ITER_STATE_INITIATOR; + break; + case WG_PSK_ITER_STATE_INITIATOR: + psk = hs->responder_ekey->psk_list; + psk_iter->state = WG_PSK_ITER_STATE_RESPONDER; + break; + case WG_PSK_ITER_STATE_RESPONDER: + memset(psk_out->data, 0, WG_KEY_LEN); + psk_iter->state = WG_PSK_ITER_STATE_EXIT; + return TRUE; + case WG_PSK_ITER_STATE_EXIT: + return FALSE; + } + } + + *psk_out = psk->psk_data; + psk_iter->next_psk = psk->next; + return TRUE; +} +/* PSK handling. }}} */ + /* UAT and key configuration. {{{ */ /* XXX this is copied verbatim from packet-ssl-utils.c - create new common API * for retrieval of runtime secrets? */ @@ -560,6 +646,7 @@ wg_keylog_reset(void) if (wg_keylog_file) { fclose(wg_keylog_file); wg_keylog_file = NULL; + wg_keylog_last_ekey = NULL; } } @@ -654,9 +741,15 @@ wg_keylog_read(void) } else if (!strcmp(key_type, "REMOTE_STATIC_PUBLIC_KEY")) { wg_add_static_key(&key, FALSE); } else if (!strcmp(key_type, "LOCAL_EPHEMERAL_PRIVATE_KEY")) { - wg_add_ephemeral_privkey(&key); + wg_keylog_last_ekey = wg_add_ephemeral_privkey(&key); } else if (!strcmp(key_type, "PRESHARED_KEY")) { - // TODO + /* Link the PSK to the last ephemeral key. */ + if (wg_keylog_last_ekey) { + wg_add_psk(wg_keylog_last_ekey, &key); + wg_keylog_last_ekey = NULL; + } else { + g_debug("Ignored PSK as no new ephemeral key was found"); + } } else { g_debug("Unrecognized key log line: %s", buf); } @@ -864,17 +957,26 @@ wg_process_response(tvbuff_t *tvb, wg_handshake_state_t *hs) } // c = KDF1(c, dh2) wg_kdf(c, dh2.data, sizeof(dh2), 1, c); - // c, t, k = KDF3(c, PSK) - // TODO apply PSK from keylog file - wg_qqword psk = {{ 0 }}; - wg_kdf(c, psk.data, WG_KEY_LEN, 3, ctk); - // h = Hash(h || t) - wg_mix_hash(&h, t, sizeof(wg_qqword)); - // empty = AEAD-Decrypt(k, 0, msg.empty, h) - if (!aead_decrypt(k, 0, encrypted_empty, AUTH_TAG_LENGTH, h.data, sizeof(wg_qqword), NULL, 0)) { + wg_qqword h_before_psk = h, c_before_psk = *c, psk; + wg_psk_iter_context psk_iter = { WG_PSK_ITER_STATE_ENTER, NULL }; + while (wg_psk_iter_next(&psk_iter, hs, &psk)) { + // c, t, k = KDF3(c, PSK) + wg_kdf(c, psk.data, WG_KEY_LEN, 3, ctk); + // h = Hash(h || t) + wg_mix_hash(&h, t, sizeof(wg_qqword)); + // empty = AEAD-Decrypt(k, 0, msg.empty, h) + if (!aead_decrypt(k, 0, encrypted_empty, AUTH_TAG_LENGTH, h.data, sizeof(wg_qqword), NULL, 0)) { + /* Possibly bad PSK, reset and try another. */ + h = h_before_psk; + *c = c_before_psk; + continue; + } + hs->empty_ok = TRUE; + break; + } + if (!hs->empty_ok) { return; } - hs->empty_ok = TRUE; // h = Hash(h || msg.empty) wg_mix_hash(&h, encrypted_empty, AUTH_TAG_LENGTH); diff --git a/test/captures/wireguard-psk.pcap b/test/captures/wireguard-psk.pcap new file mode 100644 index 0000000000..a38088b76e Binary files /dev/null and b/test/captures/wireguard-psk.pcap differ diff --git a/test/suite_decryption.py b/test/suite_decryption.py index db179bcaf9..33a3eb197f 100644 --- a/test/suite_decryption.py +++ b/test/suite_decryption.py @@ -500,6 +500,15 @@ class case_decrypt_wireguard(subprocesstest.SubprocessTestCase): key_Epriv_r0 = 'QC4/FZKhFf0b/eXEcCecmZNt6V6PXmRa4EWG1PIYTU4=' key_Epriv_i1 = 'ULv83D+y3vA0t2mgmTmWz++lpVsrP7i4wNaUEK2oX0E=' key_Epriv_r1 = 'sBv1dhsm63cbvWMv/XML+bvynBp9PTdY9Vvptu3HQlg=' + # Ephemeral keys and PSK for wireguard-psk.pcap + key_Epriv_i2 = 'iCv2VTi/BC/q0egU931KXrrQ4TSwXaezMgrhh7uCbXs=' + key_Epriv_r2 = '8G1N3LnEqYC7+NW/b6mqceVUIGBMAZSm+IpwG1U0j0w=' + key_psk2 = '//////////////////////////////////////////8=' + key_Epriv_i3 = '+MHo9sfkjPsjCx7lbVhRLDvMxYvTirOQFDSdzAW6kUQ=' + key_Epriv_r3 = '0G6t5j1B/We65MXVEBIGuRGYadwB2ITdvJovtAuATmc=' + key_psk3 = 'iIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIg=' + # dummy key that should not work with anything. + key_dummy = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=' def runOne(self, args, keylog=None, pcap_file='wireguard-ping-tcp.pcap'): if not config.have_libgcrypt17: @@ -638,3 +647,62 @@ class case_decrypt_wireguard(subprocesstest.SubprocessTestCase): self.assertIn('14\t1\t\t\t1\t\t', lines) self.assertIn('17\t\t\t\t\t\t443', lines) self.assertIn('18\t\t\t\t\t\t49472', lines) + + def test_decrypt_psk_initiator(self): + """Check whether PSKs enable decryption for initiation keys.""" + lines = self.runOne([ + '-Tfields', + '-e', 'frame.number', + '-e', 'wg.handshake_ok', + ], keylog=[ + 'REMOTE_STATIC_PUBLIC_KEY = %s' % self.key_Spub_r, + 'LOCAL_STATIC_PRIVATE_KEY = %s' % self.key_Spriv_i, + 'LOCAL_EPHEMERAL_PRIVATE_KEY=%s' % self.key_Epriv_i2, + 'PRESHARED_KEY=%s' % self.key_psk2, + 'LOCAL_EPHEMERAL_PRIVATE_KEY=%s' % self.key_Epriv_r3, + 'PRESHARED_KEY=%s' % self.key_psk3, + ], pcap_file='wireguard-psk.pcap') + self.assertIn('2\t1', lines) + self.assertIn('4\t1', lines) + + def test_decrypt_psk_responder(self): + """Check whether PSKs enable decryption for responder keys.""" + lines = self.runOne([ + '-Tfields', + '-e', 'frame.number', + '-e', 'wg.handshake_ok', + ], keylog=[ + 'REMOTE_STATIC_PUBLIC_KEY=%s' % self.key_Spub_i, + 'LOCAL_STATIC_PRIVATE_KEY=%s' % self.key_Spriv_r, + # Epriv_r2 needs psk2. This tests handling of duplicate ephemeral + # keys with multiple PSKs. It should not have adverse effects. + 'LOCAL_EPHEMERAL_PRIVATE_KEY=%s' % self.key_Epriv_r2, + 'PRESHARED_KEY=%s' % self.key_dummy, + 'LOCAL_EPHEMERAL_PRIVATE_KEY=%s' % self.key_Epriv_r2, + 'PRESHARED_KEY=%s' % self.key_psk2, + 'LOCAL_EPHEMERAL_PRIVATE_KEY=%s' % self.key_Epriv_i3, + 'PRESHARED_KEY=%s' % self.key_psk3, + # Epriv_i3 needs psk3, this tests that additional keys again have no + # bad side-effects. + 'LOCAL_EPHEMERAL_PRIVATE_KEY=%s' % self.key_Epriv_i3, + 'PRESHARED_KEY=%s' % self.key_dummy, + ], pcap_file='wireguard-psk.pcap') + self.assertIn('2\t1', lines) + self.assertIn('4\t1', lines) + + def test_decrypt_psk_wrong_orderl(self): + """Check that the wrong order of lines indeed fail decryption.""" + lines = self.runOne([ + '-Tfields', + '-e', 'frame.number', + '-e', 'wg.handshake_ok', + ], keylog=[ + 'REMOTE_STATIC_PUBLIC_KEY=%s' % self.key_Spub_i, + 'LOCAL_STATIC_PRIVATE_KEY=%s' % self.key_Spriv_r, + 'LOCAL_EPHEMERAL_PRIVATE_KEY=%s' % self.key_Epriv_r2, + 'LOCAL_EPHEMERAL_PRIVATE_KEY=%s' % self.key_Epriv_i3, + 'PRESHARED_KEY=%s' % self.key_psk2, # note: swapped with previous line + 'PRESHARED_KEY=%s' % self.key_psk3, + ], pcap_file='wireguard-psk.pcap') + self.assertIn('2\t0', lines) + self.assertIn('4\t0', lines)