editcap: add --inject-secrets option
authorPeter Wu <peter@lekensteyn.nl>
Sat, 17 Nov 2018 21:43:14 +0000 (22:43 +0100)
committerAnders Broman <a.broman58@gmail.com>
Tue, 20 Nov 2018 05:13:37 +0000 (05:13 +0000)
Add a new option to insert decryption secrets into a pcapng file.

Change-Id: I0e024585cac9a8a328e88d32f9eb03d37d350e2a
Ping-Bug: 15252
Reviewed-on: https://code.wireshark.org/review/30693
Petri-Dish: Peter Wu <peter@lekensteyn.nl>
Tested-by: Petri Dish Buildbot
Reviewed-by: Anders Broman <a.broman58@gmail.com>
doc/editcap.pod
editcap.c
test/fixtures_ws.py
test/suite_fileformats.py

index 7f65c6268b98ef3d8f96b98526898fdaf258e156..0be66ce95e2dc2478f066a8469cd3ba5242b0ec1 100644 (file)
@@ -28,6 +28,7 @@ S<[ B<-S> E<lt>strict time adjustmentE<gt> ]>
 S<[ B<-t> E<lt>time adjustmentE<gt> ]>
 S<[ B<-T> E<lt>encapsulation typeE<gt> ]>
 S<[ B<-v> ]>
+S<[ B<--inject-secrets> E<lt>secrets typeE<gt>,E<lt>fileE<gt> ]>
 I<infile>
 I<outfile>
 S<[ I<packet#>[-I<packet#>] ... ]>
@@ -335,6 +336,20 @@ NOTE: The B<-w> option assumes that the packets are in chronological order.
 If the packets are NOT in chronological order then the B<-w> duplication
 removal option may not identify some duplicates.
 
+=item --inject-secrets E<lt>secrets typeE<gt>,E<lt>fileE<gt>
+
+Inserts the contents of E<lt>fileE<gt> into a Decryption Secrets Block (DSB)
+within the pcapng output file. This enables decryption without requiring
+additional configuration in protocol preferences.
+
+The file format is described by E<lt>secrets typeE<gt> which can be one of:
+
+I<tls> TLS Key Log as described at
+ L<https://developer.mozilla.org/NSS_Key_Log_Format>
+
+This option may be specified multiple times. The available options for
+E<lt>secrets typeE<gt> can be listed with B<--inject-secrets help>.
+
 =back
 
 =head1 EXAMPLES
index 18523a1c23a5a0ece24b888228fc7f601174b053..389266e05e66112730c0b7039b0d07341aac0670 100644 (file)
--- a/editcap.c
+++ b/editcap.c
@@ -43,6 +43,7 @@
 #include <getopt.h>
 #endif
 
+#include <wiretap/secrets-types.h>
 #include <wiretap/wtap.h>
 
 #include "epan/etypes.h"
@@ -175,6 +176,13 @@ static int                    do_strict_time_adjustment = FALSE;
 static struct time_adjustment strict_time_adj           = {NSTIME_INIT_ZERO, 0}; /* strict time adjustment */
 static nstime_t               previous_time             = NSTIME_INIT_ZERO; /* previous time */
 
+static const struct {
+    const char *str;
+    guint32     id;
+} secrets_types[] = {
+    { "tls", SECRETS_TYPE_TLS },
+};
+
 static int find_dct2000_real_data(guint8 *buf);
 static void handle_chopping(chop_t chop, wtap_packet_header *out_phdr,
                             const wtap_packet_header *in_phdr, guint8 **buf,
@@ -900,6 +908,25 @@ list_encap_types(FILE *stream) {
     g_free(encaps);
 }
 
+static void
+list_secrets_types(FILE *stream)
+{
+    for (guint i = 0; i < G_N_ELEMENTS(secrets_types); i++) {
+        fprintf(stream, "    %s\n", secrets_types[i].str);
+    }
+}
+
+static guint32
+lookup_secrets_type(const char *type)
+{
+    for (guint i = 0; i < G_N_ELEMENTS(secrets_types); i++) {
+        if (!strcmp(secrets_types[i].str, type)) {
+            return secrets_types[i].id;
+        }
+    }
+    return 0;
+}
+
 static int
 framenum_compare(gconstpointer a, gconstpointer b, gpointer user_data _U_)
 {
@@ -965,6 +992,7 @@ real_main(int argc, char *argv[])
         {"novlan", no_argument, NULL, 0x8100},
         {"skip-radiotap-header", no_argument, NULL, 0x8101},
         {"seed", required_argument, NULL, 0x8102},
+        {"inject-secrets", required_argument, NULL, 0x8103},
         {"help", no_argument, NULL, 'h'},
         {"version", no_argument, NULL, 'V'},
         {0, 0, 0, 0 }
@@ -992,6 +1020,8 @@ real_main(int argc, char *argv[])
     gchar        *fsuffix            = NULL;
     guint32       change_offset      = 0;
     guint         max_packet_number  = 0;
+    GArray       *dsb_types          = NULL;
+    GPtrArray    *dsb_filenames      = NULL;
     const wtap_rec              *rec;
     wtap_rec                     temp_rec;
     wtap_dump_params             params = WTAP_DUMP_PARAMS_INIT;
@@ -1073,6 +1103,36 @@ real_main(int argc, char *argv[])
             break;
         }
 
+        case 0x8103: /* --inject-secrets */
+        {
+            guint32 secrets_type_id = 0;
+            const char *secrets_filename = NULL;
+            if (strcmp("help", optarg) == 0) {
+                list_secrets_types(stdout);
+                goto clean_exit;
+            }
+            gchar **splitted = g_strsplit(optarg, ",", 2);
+            if (splitted[0]) {
+                secrets_type_id = lookup_secrets_type(splitted[0]);
+                secrets_filename = splitted[1];
+            }
+
+            if (secrets_type_id == 0) {
+                fprintf(stderr, "editcap: \"%s\" isn't a valid secrets type\n", secrets_filename);
+                g_strfreev(splitted);
+                ret = INVALID_OPTION;
+                goto clean_exit;
+            }
+            if (!dsb_filenames) {
+                dsb_types = g_array_new(FALSE, FALSE, sizeof(guint32));
+                dsb_filenames = g_ptr_array_new_with_free_func(g_free);
+            }
+            g_array_append_val(dsb_types, secrets_type_id);
+            g_ptr_array_add(dsb_filenames, g_strdup(secrets_filename));
+            g_strfreev(splitted);
+            break;
+        }
+
         case 'a':
         {
             guint frame_number;
@@ -1400,6 +1460,45 @@ real_main(int argc, char *argv[])
 
     wtap_dump_params_init(&params, wth);
 
+    if (dsb_filenames) {
+        for (guint k = 0; k < dsb_filenames->len; k++) {
+            guint32 secrets_type_id = g_array_index(dsb_types, guint32, k);
+            const char *secrets_filename = (const char *)g_ptr_array_index(dsb_filenames, k);
+            char *data;
+            gsize data_len;
+            wtap_block_t block;
+            wtapng_dsb_mandatory_t *dsb;
+            GError *err = NULL;
+
+            if (!g_file_get_contents(secrets_filename, &data, &data_len, &err)) {
+                fprintf(stderr, "editcap: \"%s\" could not be read: %s\n", secrets_filename, err->message);
+                g_clear_error(&err);
+                ret = INVALID_OPTION;
+                goto clean_exit;
+            }
+            if (data_len == 0) {
+                fprintf(stderr, "editcap: \"%s\" is an empty file, ignoring\n", secrets_filename);
+                g_free(data);
+                continue;
+            }
+            if (data_len >= G_MAXINT) {
+                fprintf(stderr, "editcap: \"%s\" is too large, ignoring\n", secrets_filename);
+                g_free(data);
+                continue;
+            }
+
+            block = wtap_block_create(WTAP_BLOCK_DSB);
+            dsb = (wtapng_dsb_mandatory_t *)wtap_block_get_mandatory_data(block);
+            dsb->secrets_type = secrets_type_id;
+            dsb->secrets_len = (guint)data_len;
+            dsb->secrets_data = data;
+            if (params.dsbs_initial == NULL) {
+                params.dsbs_initial = g_array_new(FALSE, FALSE, sizeof(wtap_block_t));
+            }
+            g_array_append_val(params.dsbs_initial, block);
+        }
+    }
+
     /*
      * If an encapsulation type was specified, override the encapsulation
      * type of the input file.
@@ -1935,6 +2034,10 @@ real_main(int argc, char *argv[])
     }
 
 clean_exit:
+    if (dsb_filenames) {
+        g_array_free(dsb_types, TRUE);
+        g_ptr_array_free(dsb_filenames, TRUE);
+    }
     g_free(params.idb_inf);
     wtap_dump_params_cleanup(&params);
     if (wth != NULL)
index 053d89e0ceb68da7661b0f12ff3e39587ac52a68..df9b9149ac35662884757b9319abab5996f23ee5 100644 (file)
@@ -111,6 +111,11 @@ def cmd_text2pcap(program):
     return program('text2pcap')
 
 
+@fixtures.fixture(scope='session')
+def cmd_editcap(program):
+    return program('editcap')
+
+
 @fixtures.fixture(scope='session')
 def cmd_wireshark(program):
     return program('wireshark')
index 66c9880929cdc8ffbcf33258437e0ad4f7c6a439..8bf341c1cdf690713acb69ba1c2cb7e9021c7564 100644 (file)
@@ -157,6 +157,64 @@ class case_fileformat_pcapng_dsb(subprocesstest.SubprocessTestCase):
             (0x544c534b, len(dsb2_contents), dsb2_contents),
         ))
 
+    def test_pcapng_dsb_2(self, cmd_editcap, dirs, capture_file, check_pcapng_dsb_fields):
+        '''Insert a single DSB into a pcapng file.'''
+        key_file = os.path.join(dirs.key_dir, 'dhe1_keylog.dat')
+        outfile = self.filename_from_id('dhe1-dsb.pcapng')
+        self.runProcess((cmd_editcap,
+            '--inject-secrets', 'tls,%s' % key_file,
+            capture_file('dhe1.pcapng.gz'), outfile
+        ))
+        with open(key_file, 'rb') as f:
+            keylog_contents = f.read()
+        check_pcapng_dsb_fields(outfile, (
+            (0x544c534b, len(keylog_contents), keylog_contents),
+        ))
+
+    def test_pcapng_dsb_3(self, cmd_editcap, dirs, capture_file, check_pcapng_dsb_fields):
+        '''Insert two DSBs into a pcapng file.'''
+        key_file1 = os.path.join(dirs.key_dir, 'dhe1_keylog.dat')
+        key_file2 = os.path.join(dirs.key_dir, 'http2-data-reassembly.keys')
+        outfile = self.filename_from_id('dhe1-dsb.pcapng')
+        self.runProcess((cmd_editcap,
+            '--inject-secrets', 'tls,%s' % key_file1,
+            '--inject-secrets', 'tls,%s' % key_file2,
+            capture_file('dhe1.pcapng.gz'), outfile
+        ))
+        with open(key_file1, 'rb') as f:
+            keylog1_contents = f.read()
+        with open(key_file2, 'rb') as f:
+            keylog2_contents = f.read()
+        check_pcapng_dsb_fields(outfile, (
+            (0x544c534b, len(keylog1_contents), keylog1_contents),
+            (0x544c534b, len(keylog2_contents), keylog2_contents),
+        ))
+
+    def test_pcapng_dsb_4(self, cmd_editcap, dirs, capture_file, check_pcapng_dsb_fields):
+        '''Insert a single DSB into a pcapng file with existing DSBs.'''
+        dsb_keys1 = os.path.join(dirs.key_dir, 'tls12-dsb-1.keys')
+        dsb_keys2 = os.path.join(dirs.key_dir, 'tls12-dsb-2.keys')
+        key_file = os.path.join(dirs.key_dir, 'dhe1_keylog.dat')
+        outfile = self.filename_from_id('tls12-dsb-extra.pcapng')
+        self.runProcess((cmd_editcap,
+            '--inject-secrets', 'tls,%s' % key_file,
+            capture_file('tls12-dsb.pcapng'), outfile
+        ))
+        with open(dsb_keys1, 'r') as f:
+            dsb1_contents = f.read().encode('utf8')
+        with open(dsb_keys2, 'r') as f:
+            dsb2_contents = f.read().encode('utf8')
+        with open(key_file, 'rb') as f:
+            keylog_contents = f.read()
+        # New DSBs are inserted before the first record. Due to the current
+        # implementation, this is inserted before other (existing) DSBs. This
+        # might change in the future if it is deemed more logical.
+        check_pcapng_dsb_fields(outfile, (
+            (0x544c534b, len(keylog_contents), keylog_contents),
+            (0x544c534b, len(dsb1_contents), dsb1_contents),
+            (0x544c534b, len(dsb2_contents), dsb2_contents),
+        ))
+
 
 @fixtures.mark_usefixtures('test_env')
 @fixtures.uses_fixtures