From cf9f1cac07130e3da2ef5e51c9232b7c206dcde2 Mon Sep 17 00:00:00 2001 From: Peter Wu Date: Fri, 22 Jun 2018 19:36:11 +0200 Subject: [PATCH] WireGuard: implement peer identification based on MAC1 Using long-term static public keys, it is possible to identify the recipient of a handshake message. Add a new UAT where such keys can be configured. Allow private keys to be configured as well since this enables decryption of the Initiation handshake message. Bug: 15011 Change-Id: I0d4df046824eac6c333e0df75f69f73d10ed8e5e Reviewed-on: https://code.wireshark.org/review/28988 Reviewed-by: Anders Broman --- epan/dissectors/packet-wireguard.c | 369 ++++++++++++++++++++++++++ test/captures/wireguard-ping-tcp.pcap | Bin 0 -> 5120 bytes test/suite_decryption.py | 53 ++++ 3 files changed, 422 insertions(+) create mode 100644 test/captures/wireguard-ping-tcp.pcap diff --git a/epan/dissectors/packet-wireguard.c b/epan/dissectors/packet-wireguard.c index 143fd257bc..ef74b302a0 100644 --- a/epan/dissectors/packet-wireguard.c +++ b/epan/dissectors/packet-wireguard.c @@ -19,6 +19,14 @@ #include #include #include +#include +#include +#include + +#if GCRYPT_VERSION_NUMBER >= 0x010800 /* 1.8.0 */ +/* Decryption requires Curve25519, ChaCha20-Poly1305 (1.7) and Blake2s (1.8). */ +#define WG_DECRYPTION_SUPPORTED +#endif void proto_reg_handoff_wg(void); void proto_register_wg(void); @@ -41,8 +49,11 @@ static int hf_wg_encrypted_packet = -1; static int hf_wg_stream = -1; static int hf_wg_response_in = -1; static int hf_wg_response_to = -1; +static int hf_wg_receiver_pubkey = -1; +static int hf_wg_receiver_pubkey_known_privkey = -1; static gint ett_wg = -1; +static gint ett_key_info = -1; static expert_field ei_wg_bad_packet_length = EI_INIT; static expert_field ei_wg_keepalive = EI_INIT; @@ -66,6 +77,50 @@ static const value_string wg_type_names[] = { { 0x00, NULL } }; +#ifdef WG_DECRYPTION_SUPPORTED +/* Decryption types. {{{ */ +/* + * Most operations operate on 32 byte units (keys and hash output). + */ +typedef struct { +#define WG_KEY_LEN 32 + guchar data[WG_KEY_LEN]; +} wg_qqword; + +/* + * Static key with the MAC1 key pre-computed and an optional private key. + */ +typedef struct wg_skey { + wg_qqword pub_key; + wg_qqword mac1_key; + wg_qqword priv_key; /* Optional, set to all zeroes if missing. */ +} wg_skey_t; + +/* + * Set of (long-term) static keys (for guessing the peer based on MAC1). + * Maps the public key to the "wg_skey_t" structure. + * Keys are populated from the UAT and key log file. + */ +static GHashTable *wg_static_keys; + +/* 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[] = { + { WG_KEY_UAT_PUBLIC, "Public" }, + { WG_KEY_UAT_PRIVATE, "Private" }, + { 0, NULL } +}; + +typedef struct { + guint key_type; /* See "wg_key_uat_type_vals". */ + char *key; +} wg_key_uat_record_t; + +static wg_key_uat_record_t *wg_key_records; +static guint num_wg_key_records; +/* Decryption types. }}} */ +#endif /* WG_DECRYPTION_SUPPORTED */ + /* * Information required to process and link messages as required on the first * sequential pass. After that it can be erased. @@ -102,6 +157,206 @@ static wmem_map_t *sessions; static guint32 wg_session_count; +#ifdef WG_DECRYPTION_SUPPORTED +/* Key conversion routines. {{{ */ +/* Import external random data as private key. */ +static void +set_private_key(wg_qqword *privkey, const wg_qqword *inkey) +{ + // The 254th bit of a Curve25519 secret will always be set in calculations, + // use this property to recognize whether a private key is set. + *privkey = *inkey; + privkey->data[31] |= 64; +} + +/* Whether a private key is initialized (see set_private_key). */ +static inline gboolean +has_private_key(const wg_qqword *secret) +{ + return !!(secret->data[31] & 64); +} + +/** + * Compute the Curve25519 public key from a private key. + */ +static void +priv_to_pub(wg_qqword *pub, const wg_qqword *priv) +{ + int r = crypto_scalarmult_curve25519_base(pub->data, priv->data); + /* The computation should always be possible. */ + DISSECTOR_ASSERT(r == 0); +} + +/* + * Returns the string representation (base64) of a public key. + * The returned value is allocated with wmem_packet_scope. + */ +static const char * +pubkey_to_string(const wg_qqword *pubkey) +{ + gchar *str = g_base64_encode(pubkey->data, WG_KEY_LEN); + gchar *ret = wmem_strdup(wmem_packet_scope(), str); + g_free(str); + return ret; +} + +static gboolean +decode_base64_key(wg_qqword *out, const char *str) +{ + gsize out_len; + gchar tmp[45]; + + if (strlen(str) + 1 != sizeof(tmp)) { + return FALSE; + } + memcpy(tmp, str, sizeof(tmp)); + g_base64_decode_inplace(tmp, &out_len); + if (out_len != WG_KEY_LEN) { + return FALSE; + } + memcpy(out->data, tmp, WG_KEY_LEN); + return TRUE; +} +/* Key conversion routines. }}} */ + +static gboolean +wg_pubkey_equal(gconstpointer v1, gconstpointer v2) +{ + const wg_qqword *pubkey1 = (const wg_qqword *)v1; + const wg_qqword *pubkey2 = (const wg_qqword *)v2; + return !memcmp(pubkey1->data, pubkey2->data, WG_KEY_LEN); +} + + +/* Protocol-specific crypto routines. {{{ */ +/** + * Computes MAC1. Caller must ensure that GCRY_MD_BLAKE2S_256 is available. + */ +static void +wg_mac1_key(const wg_qqword *static_public, wg_qqword *mac_key_out) +{ + gcry_md_hd_t hd; + if (gcry_md_open(&hd, GCRY_MD_BLAKE2S_256, 0) == 0) { + const char wg_label_mac1[] = "mac1----"; + gcry_md_write(hd, wg_label_mac1, strlen(wg_label_mac1)); + gcry_md_write(hd, static_public->data, sizeof(wg_qqword)); + memcpy(mac_key_out->data, gcry_md_read(hd, 0), sizeof(wg_qqword)); + gcry_md_close(hd); + return; + } + // caller should have checked this. + DISSECTOR_ASSERT_NOT_REACHED(); +} + +/* + * Verify that MAC(mac_key, data) matches "mac_output". + */ +static gboolean +wg_mac_verify(const wg_qqword *mac_key, + const guchar *data, guint data_len, const guint8 mac_output[16]) +{ + gboolean ok = FALSE; + gcry_md_hd_t hd; + if (gcry_md_open(&hd, GCRY_MD_BLAKE2S_128, 0) == 0) { + gcry_error_t r; + // not documented by Libgcrypt, but required for keyed blake2s + r = gcry_md_setkey(hd, mac_key->data, WG_KEY_LEN); + DISSECTOR_ASSERT(r == 0); + gcry_md_write(hd, data, data_len); + ok = memcmp(mac_output, gcry_md_read(hd, 0), 16) == 0; + gcry_md_close(hd); + } else { + // caller should have checked this. + DISSECTOR_ASSERT_NOT_REACHED(); + } + return ok; +} +/* Protocol-specific crypto routines. }}} */ + +/* + * Add a static public or private key to "wg_static_keys". + */ +static void +wg_add_static_key(const wg_qqword *tmp_key, gboolean is_private) +{ + wg_skey_t *key = g_new0(wg_skey_t, 1); + if (is_private) { + set_private_key(&key->priv_key, tmp_key); + priv_to_pub(&key->pub_key, tmp_key); + } else { + key->pub_key = *tmp_key; + } + + // If a previous pubkey exists, skip adding the new key. Do add the + // secret if it has become known in meantime. + wg_skey_t *oldkey = (wg_skey_t *)g_hash_table_lookup(wg_static_keys, &key->pub_key); + if (oldkey) { + if (!has_private_key(&oldkey->priv_key) && is_private) { + oldkey->priv_key = key->priv_key; + } + g_free(key); + return; + } + + // New key, precompute the MAC1 label. + wg_mac1_key(&key->pub_key, &key->mac1_key); + + g_hash_table_insert(wg_static_keys, &key->pub_key, key); +} + +/* UAT and key configuration. {{{ */ +static gboolean +wg_key_uat_record_update_cb(void *r, char **error) +{ + wg_key_uat_record_t *rec = (wg_key_uat_record_t *)r; + wg_qqword key; + + /* Check for valid base64-encoding. */ + if (!decode_base64_key(&key, rec->key)) { + *error = g_strdup("Invalid key"); + return FALSE; + } + + return TRUE; +} + +static void +wg_key_uat_apply(void) +{ + if (!wg_static_keys) { + // The first field of "wg_skey_t" is the pubkey (and the table key), + // its initial four bytes should be good enough as key hash. + wg_static_keys = g_hash_table_new_full(g_int_hash, wg_pubkey_equal, NULL, g_free); + } else { + g_hash_table_remove_all(wg_static_keys); + } + + /* 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]; + wg_qqword tmp_key; /* Either public or private, not sure yet. */ + + /* Populate public (and private) keys. */ + gboolean decoded = decode_base64_key(&tmp_key, rec->key); + DISSECTOR_ASSERT(decoded); + wg_add_static_key(&tmp_key, rec->key_type == WG_KEY_UAT_PRIVATE); + } +} + +static void +wg_key_uat_reset(void) +{ + /* Erase keys when the UAT is unloaded. */ + g_hash_table_destroy(wg_static_keys); + wg_static_keys = NULL; +} + +UAT_VS_DEF(wg_key_uat, key_type, wg_key_uat_record_t, guint, WG_KEY_UAT_PUBLIC, "Public") +UAT_CSTRING_CB_DEF(wg_key_uat, key, wg_key_uat_record_t) +/* UAT and key configuration. }}} */ +#endif /* WG_DECRYPTION_SUPPORTED */ + + static void wg_sessions_insert(guint32 id, wg_session_t *session) { @@ -210,6 +465,39 @@ wg_sessions_lookup(packet_info *pinfo, guint32 receiver_id, gboolean *receiver_i return NULL; } +#ifdef WG_DECRYPTION_SUPPORTED +/* + * Finds the static public key for the receiver of this message based on the + * MAC1 value. + * TODO on PINFO_FD_VISITED, reuse previously discovered keys from session? + */ +static const wg_skey_t * +wg_mac1_key_probe(tvbuff_t *tvb, gboolean is_initiation) +{ + const int mac1_offset = is_initiation ? 116 : 60; + + // Shortcut: skip MAC1 validation if no pubkeys are configured. + if (g_hash_table_size(wg_static_keys) == 0) { + return NULL; + } + + const guint8 *mac1_msgdata = tvb_get_ptr(tvb, 0, mac1_offset); + const guint8 *mac1_output = tvb_get_ptr(tvb, mac1_offset, 16); + // Find public key that matches the 16-byte MAC1 field. + GHashTableIter iter; + gpointer value; + g_hash_table_iter_init(&iter, wg_static_keys); + while (g_hash_table_iter_next(&iter, NULL, &value)) { + const wg_skey_t *skey = (wg_skey_t *)value; + if (wg_mac_verify(&skey->mac1_key, mac1_msgdata, (guint)mac1_offset, mac1_output)) { + return skey; + } + } + + return NULL; +} +#endif /* WG_DECRYPTION_SUPPORTED */ + static void wg_dissect_pubkey(proto_tree *tree, tvbuff_t *tvb, int offset, gboolean is_ephemeral) @@ -223,18 +511,43 @@ wg_dissect_pubkey(proto_tree *tree, tvbuff_t *tvb, int offset, gboolean is_ephem proto_tree_add_string(tree, hf_id, tvb, offset, 32, key_str); } +#ifdef WG_DECRYPTION_SUPPORTED +static void +wg_dissect_mac1_pubkey(proto_tree *tree, tvbuff_t *tvb, const wg_skey_t *skey) +{ + proto_item *ti; + + if (!skey) { + return; + } + + ti = proto_tree_add_string(tree, hf_wg_receiver_pubkey, tvb, 0, 0, pubkey_to_string(&skey->pub_key)); + PROTO_ITEM_SET_GENERATED(ti); + proto_tree *key_tree = proto_item_add_subtree(ti, ett_key_info); + ti = proto_tree_add_boolean(key_tree, hf_wg_receiver_pubkey_known_privkey, tvb, 0, 0, !!has_private_key(&skey->priv_key)); + PROTO_ITEM_SET_GENERATED(ti); +} +#endif /* WG_DECRYPTION_SUPPORTED */ + static int wg_dissect_handshake_initiation(tvbuff_t *tvb, packet_info *pinfo, proto_tree *wg_tree, wg_packet_info_t *wg_pinfo) { guint32 sender_id; proto_item *ti; +#ifdef WG_DECRYPTION_SUPPORTED + const wg_skey_t *skey_r = wg_mac1_key_probe(tvb, TRUE); +#endif /* WG_DECRYPTION_SUPPORTED */ + proto_tree_add_item_ret_uint(wg_tree, hf_wg_sender, tvb, 4, 4, ENC_LITTLE_ENDIAN, &sender_id); col_append_fstr(pinfo->cinfo, COL_INFO, ", sender=0x%08X", sender_id); wg_dissect_pubkey(wg_tree, tvb, 8, TRUE); proto_tree_add_item(wg_tree, hf_wg_encrypted_static, tvb, 40, 32 + AUTH_TAG_LENGTH, ENC_NA); proto_tree_add_item(wg_tree, hf_wg_encrypted_timestamp, tvb, 88, 12 + AUTH_TAG_LENGTH, ENC_NA); proto_tree_add_item(wg_tree, hf_wg_mac1, tvb, 116, 16, ENC_NA); +#ifdef WG_DECRYPTION_SUPPORTED + wg_dissect_mac1_pubkey(wg_tree, tvb, skey_r); +#endif /* WG_DECRYPTION_SUPPORTED */ proto_tree_add_item(wg_tree, hf_wg_mac2, tvb, 132, 16, ENC_NA); if (!PINFO_FD_VISITED(pinfo)) { @@ -265,6 +578,10 @@ wg_dissect_handshake_response(tvbuff_t *tvb, packet_info *pinfo, proto_tree *wg_ guint32 sender_id, receiver_id; proto_item *ti; +#ifdef WG_DECRYPTION_SUPPORTED + const wg_skey_t *skey_i = wg_mac1_key_probe(tvb, FALSE); +#endif /* WG_DECRYPTION_SUPPORTED */ + proto_tree_add_item_ret_uint(wg_tree, hf_wg_sender, tvb, 4, 4, ENC_LITTLE_ENDIAN, &sender_id); 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); @@ -272,6 +589,9 @@ wg_dissect_handshake_response(tvbuff_t *tvb, packet_info *pinfo, proto_tree *wg_ wg_dissect_pubkey(wg_tree, tvb, 12, TRUE); proto_tree_add_item(wg_tree, hf_wg_encrypted_empty, tvb, 44, 16, ENC_NA); 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; @@ -440,6 +760,9 @@ wg_init(void) void proto_register_wg(void) { +#ifdef WG_DECRYPTION_SUPPORTED + module_t *wg_module; +#endif /* WG_DECRYPTION_SUPPORTED */ expert_module_t *expert_wg; static hf_register_info hf[] = { @@ -538,10 +861,23 @@ proto_register_wg(void) FT_FRAMENUM, BASE_NONE, FRAMENUM_TYPE(FT_FRAMENUM_REQUEST), 0x0, "This is a response to the initiation message in this frame", HFILL } }, + + /* Additional fields. */ + { &hf_wg_receiver_pubkey, + { "Receiver Static Public Key", "wg.receiver_pubkey", + FT_STRING, BASE_NONE, NULL, 0x0, + "Public key of the receiver (matched based on MAC1)", HFILL } + }, + { &hf_wg_receiver_pubkey_known_privkey, + { "Has Private Key", "wg.receiver_pubkey.known_privkey", + FT_BOOLEAN, BASE_NONE, NULL, 0x0, + "Whether the corresponding private key is known (configured via prefs)", HFILL } + }, }; static gint *ett[] = { &ett_wg, + &ett_key_info, }; static ei_register_info ei[] = { @@ -555,6 +891,15 @@ proto_register_wg(void) }, }; +#ifdef WG_DECRYPTION_SUPPORTED + /* UAT for header fields */ + static uat_field_t wg_key_uat_fields[] = { + UAT_FLD_VS(wg_key_uat, key_type, "Key type", wg_key_uat_type_vals, "Public or Private"), + UAT_FLD_CSTRING(wg_key_uat, key, "Key", "Base64-encoded key"), + UAT_END_FIELDS + }; +#endif /* WG_DECRYPTION_SUPPORTED */ + proto_wg = proto_register_protocol("WireGuard Protocol", "WireGuard", "wg"); proto_register_field_array(proto_wg, hf, array_length(hf)); @@ -565,6 +910,30 @@ proto_register_wg(void) register_dissector("wg", dissect_wg, proto_wg); +#ifdef WG_DECRYPTION_SUPPORTED + wg_module = prefs_register_protocol(proto_wg, NULL); + + uat_t *wg_keys_uat = uat_new("WireGuard static keys", + sizeof(wg_key_uat_record_t), + "wg_keys", /* filename */ + TRUE, /* from_profile */ + &wg_key_records, /* data_ptr */ + &num_wg_key_records, /* numitems_ptr */ + UAT_AFFECTS_DISSECTION, /* affects dissection of packets, but not set of named fields */ + NULL, /* Help section (currently a wiki page) */ + NULL, /* copy_cb */ + wg_key_uat_record_update_cb, /* update_cb */ + NULL, /* free_cb */ + wg_key_uat_apply, /* post_update_cb */ + wg_key_uat_reset, /* reset_cb */ + wg_key_uat_fields); + + prefs_register_uat_preference(wg_module, "keys", + "WireGuard static keys", + "A table of long-term static keys to enable WireGuard peer identification or partial decryption", + wg_keys_uat); +#endif /* WG_DECRYPTION_SUPPORTED */ + register_init_routine(wg_init); sessions = wmem_map_new_autoreset(wmem_epan_scope(), wmem_file_scope(), g_direct_hash, g_direct_equal); } diff --git a/test/captures/wireguard-ping-tcp.pcap b/test/captures/wireguard-ping-tcp.pcap new file mode 100644 index 0000000000000000000000000000000000000000..79255edb76f2ebfe820d90bdc22d34330afcbd82 GIT binary patch literal 5120 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