aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPeter Wu <peter@lekensteyn.nl>2018-07-26 19:10:31 +0200
committerAnders Broman <a.broman58@gmail.com>2018-08-08 11:25:45 +0000
commit31f4c0dce11b54c8f26e04387a6f89b326c37a24 (patch)
tree049300e8a1f64ae7755735bb9a1c3fe43856f793
parentc30b9fc8917a8c3c3b85ef939d2ebb94e03fb5ee (diff)
WireGuard: implement responder handshake decryption
Transport data decryption will follow later. Bug: 15011 Change-Id: Ib755e43ff54601405b21aeb0045b15d158bc283b Reviewed-on: https://code.wireshark.org/review/28991 Reviewed-by: Anders Broman <a.broman58@gmail.com>
-rw-r--r--epan/dissectors/packet-wireguard.c112
-rw-r--r--test/suite_decryption.py31
2 files changed, 135 insertions, 8 deletions
diff --git a/epan/dissectors/packet-wireguard.c b/epan/dissectors/packet-wireguard.c
index bbf6589762..709bcb4299 100644
--- a/epan/dissectors/packet-wireguard.c
+++ b/epan/dissectors/packet-wireguard.c
@@ -53,6 +53,7 @@ static int hf_wg_mac1 = -1;
static int hf_wg_mac2 = -1;
static int hf_wg_receiver = -1;
static int hf_wg_encrypted_empty = -1;
+static int hf_wg_handshake_ok = -1;
static int hf_wg_nonce = -1;
static int hf_wg_encrypted_cookie = -1;
static int hf_wg_counter = -1;
@@ -172,6 +173,7 @@ typedef struct {
const wg_skey_t *responder_skey; /* Spub_r based on Initiation.MAC1 (+Spriv_r if available) */
guint8 timestamp[12]; /* Initiation.timestamp (decrypted) */
gboolean timestamp_ok : 1; /* Whether the timestamp was successfully decrypted */
+ gboolean empty_ok : 1; /* Whether the empty field was successfully decrypted */
/* The following fields are only valid on the initial pass. */
const wg_ekey_t *initiator_ekey; /* Epub_i matching Initiation.Ephemeral (+Epriv_i if available) */
@@ -779,6 +781,70 @@ wg_process_initiation(tvbuff_t *tvb, wg_handshake_state_t *hs)
hs->handshake_hash = h;
hs->chaining_key = *c;
}
+
+static void
+wg_process_response(tvbuff_t *tvb, wg_handshake_state_t *hs)
+{
+ DISSECTOR_ASSERT(hs->initiator_ekey);
+ DISSECTOR_ASSERT(hs->initiator_skey);
+ DISSECTOR_ASSERT(hs->responder_ekey);
+ DISSECTOR_ASSERT(hs->responder_skey);
+
+ const gboolean has_Epriv_i = has_private_key(&hs->initiator_ekey->priv_key);
+ const gboolean has_Spriv_i = has_private_key(&hs->initiator_skey->priv_key);
+ const gboolean has_Epriv_r = has_private_key(&hs->responder_ekey->priv_key);
+
+ // Either Epriv_i + Spriv_i or Epriv_r + Epub_i + Spub_i are required.
+ if (!(has_Epriv_i && has_Spriv_i) && !has_Epriv_r) {
+ return;
+ }
+
+ const wg_qqword *ephemeral = (const wg_qqword *)tvb_get_ptr(tvb, 12, WG_KEY_LEN);
+ const guint8 *encrypted_empty = (const guint8 *)tvb_get_ptr(tvb, 44, AUTH_TAG_LENGTH);
+
+ wg_qqword ctk[3], h;
+ wg_qqword *c = &ctk[0], *t = &ctk[1], *k = &ctk[2];
+ h = hs->handshake_hash;
+ *c = hs->chaining_key;
+
+ // c = KDF1(c, msg.ephemeral)
+ wg_kdf(c, ephemeral->data, WG_KEY_LEN, 1, c);
+ // h = Hash(h || msg.ephemeral)
+ wg_mix_hash(&h, ephemeral, WG_KEY_LEN);
+ // dh1 = DH(Epriv_i, msg.ephemeral) if kType == I
+ // dh1 = DH(Epriv_r, Epub_i) if kType == R
+ wg_qqword dh1;
+ if (has_Epriv_i && has_Spriv_i) {
+ dh_x25519(&dh1, &hs->initiator_ekey->priv_key, ephemeral);
+ } else {
+ dh_x25519(&dh1, &hs->responder_ekey->priv_key, &hs->initiator_ekey->pub_key);
+ }
+ // c = KDF1(c, dh1)
+ wg_kdf(c, dh1.data, sizeof(dh1), 1, c);
+ // dh2 = DH(Spriv_i, msg.ephemeral) if kType == I
+ // dh2 = DH(Epriv_r, Spub_i) if kType == R
+ wg_qqword dh2;
+ if (has_Epriv_i && has_Spriv_i) {
+ dh_x25519(&dh2, &hs->initiator_skey->priv_key, ephemeral);
+ } else {
+ dh_x25519(&dh2, &hs->responder_ekey->priv_key, &hs->initiator_skey->pub_key);
+ }
+ // 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)) {
+ return;
+ }
+ hs->empty_ok = TRUE;
+ // h = Hash(h || msg.empty)
+ wg_mix_hash(&h, encrypted_empty, AUTH_TAG_LENGTH);
+}
#endif /* WG_DECRYPTION_SUPPORTED */
@@ -950,6 +1016,23 @@ wg_prepare_handshake_keys(const wg_skey_t *skey_r, tvbuff_t *tvb)
return hs;
}
+/*
+ * Processes a Response message, storing additional keys in the state.
+ */
+static void
+wg_prepare_handshake_responder_keys(wg_handshake_state_t *hs, tvbuff_t *tvb)
+{
+ wg_ekey_t *ekey_r = (wg_ekey_t *)wmem_map_lookup(wg_ephemeral_keys, tvb_get_ptr(tvb, 12, WG_KEY_LEN));
+
+ // Response decryption needs Epriv_r (or Epub_r + additional secrets).
+ if (!ekey_r) {
+ ekey_r = wmem_new0(wmem_file_scope(), wg_ekey_t);
+ tvb_memcpy(tvb, ekey_r->pub_key.data, 12, WG_KEY_LEN);
+ }
+
+ hs->responder_ekey = ekey_r;
+}
+
/* Converts a TAI64 label to the seconds since the Unix epoch.
* See https://cr.yp.to/libtai/tai64.html */
static gboolean tai64n_to_unix(guint64 tai64_label, guint32 nanoseconds, nstime_t *nstime)
@@ -1136,6 +1219,7 @@ wg_dissect_handshake_response(tvbuff_t *tvb, packet_info *pinfo, proto_tree *wg_
{
guint32 sender_id, receiver_id;
proto_item *ti;
+ wg_session_t *session;
#ifdef WG_DECRYPTION_SUPPORTED
wg_keylog_read();
@@ -1146,17 +1230,34 @@ wg_dissect_handshake_response(tvbuff_t *tvb, packet_info *pinfo, proto_tree *wg_
col_append_fstr(pinfo->cinfo, COL_INFO, ", sender=0x%08X", sender_id);
proto_tree_add_item_ret_uint(wg_tree, hf_wg_receiver, tvb, 8, 4, ENC_LITTLE_ENDIAN, &receiver_id);
col_append_fstr(pinfo->cinfo, COL_INFO, ", receiver=0x%08X", receiver_id);
+
+ if (!PINFO_FD_VISITED(pinfo)) {
+ session = wg_sessions_lookup_initiation(pinfo, receiver_id);
+#ifdef WG_DECRYPTION_SUPPORTED
+ if (session && session->hs) {
+ wg_prepare_handshake_responder_keys(session->hs, tvb);
+ wg_process_response(tvb, session->hs);
+ }
+#endif /* WG_DECRYPTION_SUPPORTED */
+ } else {
+ session = wg_pinfo->session;
+ }
+
wg_dissect_pubkey(wg_tree, tvb, 12, TRUE);
proto_tree_add_item(wg_tree, hf_wg_encrypted_empty, tvb, 44, 16, ENC_NA);
+#ifdef WG_DECRYPTION_SUPPORTED
+ if (session && session->hs) {
+ ti = proto_tree_add_boolean(wg_tree, hf_wg_handshake_ok, tvb, 0, 0, !!session->hs->empty_ok);
+ PROTO_ITEM_SET_GENERATED(ti);
+ }
+#endif /* WG_DECRYPTION_SUPPORTED */
proto_tree_add_item(wg_tree, hf_wg_mac1, tvb, 60, 16, ENC_NA);
#ifdef WG_DECRYPTION_SUPPORTED
wg_dissect_mac1_pubkey(wg_tree, tvb, skey_i);
#endif /* WG_DECRYPTION_SUPPORTED */
proto_tree_add_item(wg_tree, hf_wg_mac2, tvb, 76, 16, ENC_NA);
- wg_session_t *session;
if (!PINFO_FD_VISITED(pinfo)) {
- session = wg_sessions_lookup_initiation(pinfo, receiver_id);
/* XXX should probably check whether decryption succeeds before linking
* and somehow mark that this response is related but not correct. */
if (session) {
@@ -1165,8 +1266,6 @@ wg_dissect_handshake_response(tvbuff_t *tvb, packet_info *pinfo, proto_tree *wg_
wg_sessions_insert(sender_id, session);
wg_pinfo->session = session;
}
- } else {
- session = wg_pinfo->session;
}
if (session) {
ti = proto_tree_add_uint(wg_tree, hf_wg_stream, tvb, 0, 0, session->stream);
@@ -1399,6 +1498,11 @@ proto_register_wg(void)
FT_NONE, BASE_NONE, NULL, 0x0,
"Authenticated encryption of an empty string", HFILL }
},
+ { &hf_wg_handshake_ok,
+ { "Handshake decryption successful", "wg.handshake_ok",
+ FT_BOOLEAN, BASE_NONE, NULL, 0x0,
+ "Whether decryption keys were successfully derived", HFILL }
+ },
/* Cookie message */
{ &hf_wg_nonce,
diff --git a/test/suite_decryption.py b/test/suite_decryption.py
index d92eb8af7d..971d78fb5a 100644
--- a/test/suite_decryption.py
+++ b/test/suite_decryption.py
@@ -584,9 +584,9 @@ class case_decrypt_wireguard(subprocesstest.SubprocessTestCase):
self.assertIn('1\t1\t%s\t%s' % (self.key_Spub_i, ''), lines)
self.assertIn('13\t1\t%s\t%s' % (self.key_Spub_i, ''), lines)
- def test_decrypt_initiation_static_ephemeral(self):
+ def test_decrypt_full_initiator(self):
"""
- Check for full initiation decryption using Spriv_r + Epriv_i.
+ Check for full handshake decryption using Spriv_r + Epriv_i.
The public key Spub_r is provided via the key log as well.
"""
lines = self.runOne([
@@ -595,11 +595,34 @@ class case_decrypt_wireguard(subprocesstest.SubprocessTestCase):
'-e', 'wg.ephemeral.known_privkey',
'-e', 'wg.static',
'-e', 'wg.timestamp.nanoseconds',
+ '-e', 'wg.handshake_ok',
], keylog=[
' REMOTE_STATIC_PUBLIC_KEY = %s' % self.key_Spub_r,
' LOCAL_STATIC_PRIVATE_KEY = %s' % self.key_Spriv_i_alt,
' LOCAL_EPHEMERAL_PRIVATE_KEY = %s' % self.key_Epriv_i0_alt,
' LOCAL_EPHEMERAL_PRIVATE_KEY = %s' % self.key_Epriv_i1,
])
- self.assertIn('1\t1\t%s\t%s' % (self.key_Spub_i, '356537872'), lines)
- self.assertIn('13\t1\t%s\t%s' % (self.key_Spub_i, '490514356'), lines)
+ self.assertIn('1\t1\t%s\t%s\t' % (self.key_Spub_i, '356537872'), lines)
+ self.assertIn('2\t0\t\t\t1', lines)
+ self.assertIn('13\t1\t%s\t%s\t' % (self.key_Spub_i, '490514356'), lines)
+ self.assertIn('14\t0\t\t\t1', lines)
+
+ def test_decrypt_full_responder(self):
+ """Check for full handshake decryption using responder secrets."""
+ lines = self.runOne([
+ '-Tfields',
+ '-e', 'frame.number',
+ '-e', 'wg.ephemeral.known_privkey',
+ '-e', 'wg.static',
+ '-e', 'wg.timestamp.nanoseconds',
+ '-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_r0,
+ 'LOCAL_EPHEMERAL_PRIVATE_KEY=%s' % self.key_Epriv_r1,
+ ])
+ self.assertIn('1\t0\t%s\t%s\t' % (self.key_Spub_i, '356537872'), lines)
+ self.assertIn('2\t1\t\t\t1', lines)
+ self.assertIn('13\t0\t%s\t%s\t' % (self.key_Spub_i, '490514356'), lines)
+ self.assertIn('14\t1\t\t\t1', lines)