aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--epan/dissectors/packet-wireguard.c124
-rw-r--r--test/captures/wireguard-psk.pcapbin0 -> 736 bytes
-rw-r--r--test/suite_decryption.py68
3 files changed, 181 insertions, 11 deletions
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
@@ -121,11 +121,26 @@ typedef struct wg_skey {
} 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
--- /dev/null
+++ b/test/captures/wireguard-psk.pcap
Binary files 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)