WireGuard: implement decryption with PSKs
authorPeter Wu <peter@lekensteyn.nl>
Wed, 1 Aug 2018 00:10:40 +0000 (02:10 +0200)
committerAnders Broman <a.broman58@gmail.com>
Wed, 8 Aug 2018 11:26:06 +0000 (11:26 +0000)
This imposes an additional requirement on the key log file, PSKs are
only linked to the most recently seen ephemeral key. This means that the
key log might contain duplicate PSK lines, but at least the dissector
won't have to try all keys and thereby save CPU time.

Bug: 15011
Change-Id: I368fa16269c96c4a1ff3bcb4e376c21f38fa2689
Reviewed-on: https://code.wireshark.org/review/28993
Petri-Dish: Peter Wu <peter@lekensteyn.nl>
Tested-by: Petri Dish Buildbot
Reviewed-by: Anders Broman <a.broman58@gmail.com>
epan/dissectors/packet-wireguard.c
test/captures/wireguard-psk.pcap [new file with mode: 0644]
test/suite_decryption.py

index d4cdea8..1d8192a 100644 (file)
@@ -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 (file)
index 0000000..a38088b
Binary files /dev/null and b/test/captures/wireguard-psk.pcap differ
index db179bc..33a3eb1 100644 (file)
@@ -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)