WireGuard: add keylog for initiation decryption with ephemeral keys
As UATs are currently unable to receive keys dynamically without manual user interaction followed by rescanning of the pcap, add a mechanism like ssl.keylog_file. Such keys can be extracted using the tools from contrib/examples/extract-handshakes/ in the WireGuard source tree. Now decryption of Initiation messages is also possible when keys (Epriv_i) are captured from the initiator side. Bug: 15011 Change-Id: If998bf26e818487187cc618d2eb6d4d8f5b2cc0a Reviewed-on: https://code.wireshark.org/review/28990 Reviewed-by: Anders Broman <a.broman58@gmail.com>
This commit is contained in:
parent
5b61737dc9
commit
c30b9fc891
|
@ -15,11 +15,17 @@
|
|||
|
||||
#include <config.h>
|
||||
|
||||
#include <errno.h>
|
||||
|
||||
/* Start with G_MESSAGES_DEBUG=packet-wireguard to see messages. */
|
||||
#define G_LOG_DOMAIN "packet-wireguard"
|
||||
|
||||
#include <epan/packet.h>
|
||||
#include <epan/expert.h>
|
||||
#include <epan/prefs.h>
|
||||
#include <epan/proto_data.h>
|
||||
#include <epan/uat.h>
|
||||
#include <wsutil/file_util.h>
|
||||
#include <wsutil/ws_printf.h> /* ws_g_warning */
|
||||
#include <wsutil/wsgcrypt.h>
|
||||
#include <wsutil/curve25519.h>
|
||||
|
@ -67,6 +73,10 @@ static gint ett_key_info = -1;
|
|||
static expert_field ei_wg_bad_packet_length = EI_INIT;
|
||||
static expert_field ei_wg_keepalive = EI_INIT;
|
||||
|
||||
#ifdef WG_DECRYPTION_SUPPORTED
|
||||
static const char *pref_keylog_file;
|
||||
#endif /* WG_DECRYPTION_SUPPORTED */
|
||||
|
||||
|
||||
// Length of AEAD authentication tag
|
||||
#define AUTH_TAG_LENGTH 16
|
||||
|
@ -127,6 +137,12 @@ static GHashTable *wg_static_keys;
|
|||
*/
|
||||
static wmem_map_t *wg_ephemeral_keys;
|
||||
|
||||
/*
|
||||
* Key log file handle. Opened on demand (when keys are actually looked up),
|
||||
* closed when the capture file closes.
|
||||
*/
|
||||
static FILE *wg_keylog_file;
|
||||
|
||||
/* 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[] = {
|
||||
|
@ -470,7 +486,156 @@ wg_add_static_key(const wg_qqword *tmp_key, gboolean is_private)
|
|||
g_hash_table_insert(wg_static_keys, &key->pub_key, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the given ephemeral private key.
|
||||
*/
|
||||
static wg_ekey_t *
|
||||
wg_add_ephemeral_privkey(const wg_qqword *priv_key)
|
||||
{
|
||||
wg_qqword pub_key;
|
||||
priv_to_pub(&pub_key, priv_key);
|
||||
wg_ekey_t *key = (wg_ekey_t *)wmem_map_lookup(wg_ephemeral_keys, &pub_key);
|
||||
if (!key) {
|
||||
key = wmem_new0(wmem_file_scope(), wg_ekey_t);
|
||||
key->pub_key = pub_key;
|
||||
set_private_key(&key->priv_key, priv_key);
|
||||
wmem_map_insert(wg_ephemeral_keys, &key->pub_key, key);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
/* UAT and key configuration. {{{ */
|
||||
/* XXX this is copied verbatim from packet-ssl-utils.c - create new common API
|
||||
* for retrieval of runtime secrets? */
|
||||
static gboolean
|
||||
file_needs_reopen(FILE *fp, const char *filename)
|
||||
{
|
||||
ws_statb64 open_stat, current_stat;
|
||||
|
||||
/* consider a file deleted when stat fails for either file,
|
||||
* or when the residing device / inode has changed. */
|
||||
if (0 != ws_fstat64(ws_fileno(fp), &open_stat))
|
||||
return TRUE;
|
||||
if (0 != ws_stat64(filename, ¤t_stat))
|
||||
return TRUE;
|
||||
|
||||
/* Note: on Windows, ino may be 0. Existing files cannot be deleted on
|
||||
* Windows, but hopefully the size is a good indicator when a file got
|
||||
* removed and recreated */
|
||||
return open_stat.st_dev != current_stat.st_dev ||
|
||||
open_stat.st_ino != current_stat.st_ino ||
|
||||
open_stat.st_size > current_stat.st_size;
|
||||
}
|
||||
|
||||
static void
|
||||
wg_keylog_reset(void)
|
||||
{
|
||||
if (wg_keylog_file) {
|
||||
fclose(wg_keylog_file);
|
||||
wg_keylog_file = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
wg_keylog_read(void)
|
||||
{
|
||||
if (!pref_keylog_file || !*pref_keylog_file) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reopen file if it got deleted.
|
||||
if (wg_keylog_file && file_needs_reopen(wg_keylog_file, pref_keylog_file)) {
|
||||
g_debug("Key log file got changed or deleted, trying to re-open.");
|
||||
wg_keylog_reset();
|
||||
}
|
||||
|
||||
if (!wg_keylog_file) {
|
||||
wg_keylog_file = ws_fopen(pref_keylog_file, "r");
|
||||
if (!wg_keylog_file) {
|
||||
g_debug("Failed to open key log file %s: %s", pref_keylog_file, g_strerror(errno));
|
||||
return;
|
||||
}
|
||||
g_debug("Opened key log file %s", pref_keylog_file);
|
||||
}
|
||||
|
||||
/* File format: each line follows the format "<type>=<key>" (leading spaces
|
||||
* and spaces around '=' as produced by extract-handshakes.sh are ignored).
|
||||
* For available <type>s, see below. <key> is the base64-encoded key (44
|
||||
* characters).
|
||||
*
|
||||
* Example:
|
||||
* LOCAL_STATIC_PRIVATE_KEY = AKeZaHwBxjiKLFnkY2unvEdOTtg4AL+M9dQXfopFVFk=
|
||||
* REMOTE_STATIC_PUBLIC_KEY = YDCttCs9e1J52/g9vEnwJJa+2x6RqaayAYMpSVQfGEY=
|
||||
* LOCAL_EPHEMERAL_PRIVATE_KEY = sLGLJSOQfyz7JNJ5ZDzFf3Uz1rkiCMMjbWerNYcPFFU=
|
||||
* PRESHARED_KEY = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
|
||||
*/
|
||||
|
||||
for (;;) {
|
||||
char buf[512];
|
||||
if (!fgets(buf, sizeof(buf), wg_keylog_file)) {
|
||||
if (feof(wg_keylog_file)) {
|
||||
clearerr(wg_keylog_file);
|
||||
} else if (ferror(wg_keylog_file)) {
|
||||
g_debug("Error while reading %s, closing it.", pref_keylog_file);
|
||||
wg_keylog_reset();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
gsize bytes_read = strlen(buf);
|
||||
/* fgets includes the \n at the end of the line. */
|
||||
if (bytes_read > 0 && buf[bytes_read - 1] == '\n') {
|
||||
buf[bytes_read - 1] = 0;
|
||||
bytes_read--;
|
||||
}
|
||||
if (bytes_read > 0 && buf[bytes_read - 1] == '\r') {
|
||||
buf[bytes_read - 1] = 0;
|
||||
bytes_read--;
|
||||
}
|
||||
|
||||
g_debug("Read key log line: %s", buf);
|
||||
|
||||
/* Strip leading spaces. */
|
||||
char *p = buf;
|
||||
while (*p == ' ') {
|
||||
++p;
|
||||
}
|
||||
const char *key_type = p;
|
||||
const char *key_value = NULL;
|
||||
p = strchr(p, '=');
|
||||
if (p && key_type != p) {
|
||||
key_value = p + 1;
|
||||
/* Strip '=' and spaces before it (after key type). */
|
||||
do {
|
||||
*p = '\0';
|
||||
--p;
|
||||
} while (*p == ' ');
|
||||
/* Strip spaces after '=' (before key value) */
|
||||
while (*key_value == ' ') {
|
||||
++key_value;
|
||||
}
|
||||
}
|
||||
|
||||
wg_qqword key;
|
||||
if (!key_value || !decode_base64_key(&key, key_value)) {
|
||||
g_debug("Unrecognized key log line: %s", buf);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!strcmp(key_type, "LOCAL_STATIC_PRIVATE_KEY")) {
|
||||
wg_add_static_key(&key, TRUE);
|
||||
} 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);
|
||||
} else if (!strcmp(key_type, "PRESHARED_KEY")) {
|
||||
// TODO
|
||||
} else {
|
||||
g_debug("Unrecognized key log line: %s", buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean
|
||||
wg_key_uat_record_update_cb(void *r, char **error)
|
||||
{
|
||||
|
@ -497,6 +662,10 @@ wg_key_uat_apply(void)
|
|||
g_hash_table_remove_all(wg_static_keys);
|
||||
}
|
||||
|
||||
// As static keys from the key log file also end up in "wg_static_keys",
|
||||
// reset the file pointer such that it will be fully read later.
|
||||
wg_keylog_reset();
|
||||
|
||||
/* Convert base64-encoded strings to wg_skey_t and derive pubkey. */
|
||||
for (guint i = 0; i < num_wg_key_records; i++) {
|
||||
wg_key_uat_record_t *rec = &wg_key_records[i];
|
||||
|
@ -904,6 +1073,7 @@ wg_dissect_handshake_initiation(tvbuff_t *tvb, packet_info *pinfo, proto_tree *w
|
|||
proto_item *ti;
|
||||
|
||||
#ifdef WG_DECRYPTION_SUPPORTED
|
||||
wg_keylog_read();
|
||||
const wg_skey_t *skey_r = wg_mac1_key_probe(tvb, TRUE);
|
||||
wg_handshake_state_t *hs = NULL;
|
||||
|
||||
|
@ -968,6 +1138,7 @@ wg_dissect_handshake_response(tvbuff_t *tvb, packet_info *pinfo, proto_tree *wg_
|
|||
proto_item *ti;
|
||||
|
||||
#ifdef WG_DECRYPTION_SUPPORTED
|
||||
wg_keylog_read();
|
||||
const wg_skey_t *skey_i = wg_mac1_key_probe(tvb, FALSE);
|
||||
#endif /* WG_DECRYPTION_SUPPORTED */
|
||||
|
||||
|
@ -1358,6 +1529,13 @@ proto_register_wg(void)
|
|||
"A table of long-term static keys to enable WireGuard peer identification or partial decryption",
|
||||
wg_keys_uat);
|
||||
|
||||
prefs_register_filename_preference(wg_module, "keylog_file", "Key log filename",
|
||||
"The path to the file which contains a list of secrets in the following format:\n"
|
||||
"\"<key-type> = <base64-encoded-key>\" (without quotes, leading spaces and spaces around '=' are ignored).\n"
|
||||
"<key-type> is one of: LOCAL_STATIC_PRIVATE_KEY, REMOTE_STATIC_PUBLIC_KEY, "
|
||||
"LOCAL_EPHEMERAL_PRIVATE_KEY or PRESHARED_KEY.",
|
||||
&pref_keylog_file, FALSE);
|
||||
|
||||
if (!wg_decrypt_init()) {
|
||||
ws_g_warning("%s: decryption will not be possible due to lack of algorithms support", G_STRFUNC);
|
||||
}
|
||||
|
@ -1366,6 +1544,9 @@ proto_register_wg(void)
|
|||
#endif /* WG_DECRYPTION_SUPPORTED */
|
||||
|
||||
register_init_routine(wg_init);
|
||||
#ifdef WG_DECRYPTION_SUPPORTED
|
||||
register_cleanup_routine(wg_keylog_reset);
|
||||
#endif /* WG_DECRYPTION_SUPPORTED */
|
||||
sessions = wmem_map_new_autoreset(wmem_epan_scope(), wmem_file_scope(), g_direct_hash, g_direct_equal);
|
||||
}
|
||||
|
||||
|
|
|
@ -487,19 +487,29 @@ class case_decrypt_kerberos(subprocesstest.SubprocessTestCase):
|
|||
self.assertTrue(self.grepOutput('cc:da:7d:48:21:9f:73:c3:b2:83:11:c4:ba:72:42:b3'))
|
||||
|
||||
class case_decrypt_wireguard(subprocesstest.SubprocessTestCase):
|
||||
# The "foo_alt" keys are similar as "foo" except that some bits are changed.
|
||||
# The crypto library should be able to handle this and internally the
|
||||
# dissector uses MSB to recognize whether a private key is set.
|
||||
key_Spriv_i = 'AKeZaHwBxjiKLFnkY2unvEdOTtg4AL+M9dQXfopFVFk='
|
||||
key_Spriv_i_alt = 'B6eZaHwBxjiKLFnkY2unvEdOTtg4AL+M9dQXfopFVJk='
|
||||
key_Spub_i = 'Igge9KzRytKNwrgkzDE/8hrLu6Ly0OqVdvOPWhA5KR4='
|
||||
key_Spriv_r = 'cFIxTUyBs1Qil414hBwEgvasEax8CKJ5IS5ZougplWs='
|
||||
key_Spub_r = 'YDCttCs9e1J52/g9vEnwJJa+2x6RqaayAYMpSVQfGEY='
|
||||
key_Epriv_i0 = 'sLGLJSOQfyz7JNJ5ZDzFf3Uz1rkiCMMjbWerNYcPFFU='
|
||||
key_Epriv_i0_alt = 't7GLJSOQfyz7JNJ5ZDzFf3Uz1rkiCMMjbWerNYcPFJU='
|
||||
key_Epriv_r0 = 'QC4/FZKhFf0b/eXEcCecmZNt6V6PXmRa4EWG1PIYTU4='
|
||||
key_Epriv_i1 = 'ULv83D+y3vA0t2mgmTmWz++lpVsrP7i4wNaUEK2oX0E='
|
||||
key_Epriv_r1 = 'sBv1dhsm63cbvWMv/XML+bvynBp9PTdY9Vvptu3HQlg='
|
||||
|
||||
def runOne(self, args, pcap_file='wireguard-ping-tcp.pcap'):
|
||||
def runOne(self, args, keylog=None, pcap_file='wireguard-ping-tcp.pcap'):
|
||||
if not config.have_libgcrypt17:
|
||||
self.skipTest('Requires Gcrypt 1.7 or later')
|
||||
capture_file = os.path.join(config.capture_dir, pcap_file)
|
||||
if keylog:
|
||||
keylog_file = self.filename_from_id('wireguard.keys')
|
||||
args += ['-owg.keylog_file:%s' % keylog_file]
|
||||
with open(keylog_file, 'w') as f:
|
||||
f.write("\n".join(keylog))
|
||||
proc = self.runProcess([config.cmd_tshark, '-r', capture_file] + args,
|
||||
env=config.test_env)
|
||||
lines = proc.stdout_str.splitlines()
|
||||
|
@ -554,3 +564,42 @@ class case_decrypt_wireguard(subprocesstest.SubprocessTestCase):
|
|||
# static pubkey is unknown because Spub_i is not added to wg_keys.
|
||||
self.assertIn('1\t%s\t0\t0\t%s' % (self.key_Spub_i, '356537872'), lines)
|
||||
self.assertIn('13\t%s\t0\t0\t%s' % (self.key_Spub_i, '490514356'), lines)
|
||||
|
||||
def test_decrypt_initiation_ephemeral_only(self):
|
||||
"""Check for partial decryption using Epriv_i."""
|
||||
lines = self.runOne([
|
||||
'-ouat:wg_keys:"Public","%s"' % self.key_Spub_r,
|
||||
'-Y', 'wg.type==1',
|
||||
'-Tfields',
|
||||
'-e', 'frame.number',
|
||||
'-e', 'wg.ephemeral.known_privkey',
|
||||
'-e', 'wg.static',
|
||||
'-e', 'wg.timestamp.nanoseconds',
|
||||
], keylog=[
|
||||
'LOCAL_EPHEMERAL_PRIVATE_KEY=%s' % self.key_Epriv_i0,
|
||||
'LOCAL_EPHEMERAL_PRIVATE_KEY=%s' % self.key_Epriv_i1,
|
||||
])
|
||||
# The current implementation tries to write as much decrypted data as
|
||||
# possible, even if the full handshake cannot be derived.
|
||||
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):
|
||||
"""
|
||||
Check for full initiation decryption using Spriv_r + Epriv_i.
|
||||
The public key Spub_r is provided via the key log as well.
|
||||
"""
|
||||
lines = self.runOne([
|
||||
'-Tfields',
|
||||
'-e', 'frame.number',
|
||||
'-e', 'wg.ephemeral.known_privkey',
|
||||
'-e', 'wg.static',
|
||||
'-e', 'wg.timestamp.nanoseconds',
|
||||
], 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)
|
||||
|
|
Loading…
Reference in New Issue