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>
This commit is contained in:
Peter Wu 2018-07-26 19:10:31 +02:00 committed by Anders Broman
parent c30b9fc891
commit 31f4c0dce1
2 changed files with 135 additions and 8 deletions

View File

@ -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,

View File

@ -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)