Blame SOURCES/sos-bz1594327-archive-encryption.patch

c33036
From 7b475f1da0f843b20437896737be04cc1c7bbc0a Mon Sep 17 00:00:00 2001
c33036
From: Jake Hunsaker <jhunsake@redhat.com>
c33036
Date: Fri, 25 May 2018 13:38:27 -0400
c33036
Subject: [PATCH] [sosreport] Add mechanism to encrypt final archive
c33036
c33036
Adds an option to encrypt the resulting archive that sos generates.
c33036
There are two methods for doing so:
c33036
c33036
	--encrypt-key	Uses a key-pair for asymmetric encryption
c33036
	--encrypt-pass  Uses a password for symmetric encryption
c33036
c33036
For key-pair encryption, the key-to-be-used must be imported into the
c33036
root user's keyring, as gpg does not allow for the use of keyfiles.
c33036
c33036
If the encryption process fails, sos will not abort as the unencrypted
c33036
archive will have already been created. The assumption being that the
c33036
archive is still of use and/or the user has another means of encrypting
c33036
it.
c33036
c33036
Resolves: #1320
c33036
c33036
Signed-off-by: Jake Hunsaker <jhunsake@redhat.com>
c33036
Signed-off-by: Bryn M. Reeves <bmr@redhat.com>
c33036
---
c33036
 man/en/sosreport.1     | 28 ++++++++++++++++++++++
c33036
 sos/__init__.py        | 10 ++++----
c33036
 sos/archive.py         | 63 ++++++++++++++++++++++++++++++++++++++++++++++----
c33036
 sos/sosreport.py       | 20 ++++++++++++++--
c33036
 tests/archive_tests.py |  3 ++-
c33036
 5 files changed, 113 insertions(+), 11 deletions(-)
c33036
c33036
diff --git a/man/en/sosreport.1 b/man/en/sosreport.1
c33036
index b0adcd8bb..b6051edc1 100644
c33036
--- a/man/en/sosreport.1
c33036
+++ b/man/en/sosreport.1
c33036
@@ -22,6 +22,8 @@ sosreport \- Collect and package diagnostic and support data
c33036
           [--log-size]\fR
c33036
           [--all-logs]\fR
c33036
           [-z|--compression-type method]\fR
c33036
+          [--encrypt-key KEY]\fR
c33036
+          [--encrypt-pass PASS]\fR
c33036
           [--experimental]\fR
c33036
           [-h|--help]\fR
c33036
 
c33036
@@ -120,6 +122,32 @@ increase the size of reports.
c33036
 .B \-z, \--compression-type METHOD
c33036
 Override the default compression type specified by the active policy.
c33036
 .TP
c33036
+.B \--encrypt-key KEY
c33036
+Encrypts the resulting archive that sosreport produces using GPG. KEY must be
c33036
+an existing key in the user's keyring as GPG does not allow for keyfiles.
c33036
+KEY can be any value accepted by gpg's 'recipient' option.
c33036
+
c33036
+Note that the user running sosreport must match the user owning the keyring
c33036
+from which keys will be obtained. In particular this means that if sudo is
c33036
+used to run sosreport, the keyring must also be set up using sudo
c33036
+(or direct shell access to the account).
c33036
+
c33036
+Users should be aware that encrypting the final archive will result in sos
c33036
+using double the amount of temporary disk space - the encrypted archive must be
c33036
+written as a separate, rather than replacement, file within the temp directory
c33036
+that sos writes the archive to. However, since the encrypted archive will be
c33036
+the same size as the original archive, there is no additional space consumption
c33036
+once the temporary directory is removed at the end of execution.
c33036
+
c33036
+This means that only the encrypted archive is present on disk after sos
c33036
+finishes running.
c33036
+
c33036
+If encryption fails for any reason, the original unencrypted archive is
c33036
+preserved instead.
c33036
+.TP
c33036
+.B \--encrypt-pass PASS
c33036
+The same as \--encrypt-key, but use the provided PASS for symmetric encryption
c33036
+rather than key-pair encryption.
c33036
 .TP
c33036
 .B \--batch
c33036
 Generate archive without prompting for interactive input.
c33036
diff --git a/sos/__init__.py b/sos/__init__.py
c33036
index ef4524c60..cd9779bdc 100644
c33036
--- a/sos/__init__.py
c33036
+++ b/sos/__init__.py
c33036
@@ -45,10 +45,10 @@ def _default(msg):
c33036
 _arg_names = [
c33036
     'add_preset', 'alloptions', 'all_logs', 'batch', 'build', 'case_id',
c33036
     'chroot', 'compression_type', 'config_file', 'desc', 'debug', 'del_preset',
c33036
-    'enableplugins', 'experimental', 'label', 'list_plugins', 'list_presets',
c33036
-    'list_profiles', 'log_size', 'noplugins', 'noreport', 'note',
c33036
-    'onlyplugins', 'plugopts', 'preset', 'profiles', 'quiet', 'sysroot',
c33036
-    'threads', 'tmp_dir', 'verbosity', 'verify'
c33036
+    'enableplugins', 'encrypt_key', 'encrypt_pass', 'experimental', 'label',
c33036
+    'list_plugins', 'list_presets', 'list_profiles', 'log_size', 'noplugins',
c33036
+    'noreport', 'note', 'onlyplugins', 'plugopts', 'preset', 'profiles',
c33036
+    'quiet', 'sysroot', 'threads', 'tmp_dir', 'verbosity', 'verify'
c33036
 ]
c33036
 
c33036
 #: Arguments with non-zero default values
c33036
@@ -84,6 +84,8 @@ class SoSOptions(object):
c33036
     del_preset = ""
c33036
     desc = ""
c33036
     enableplugins = []
c33036
+    encrypt_key = None
c33036
+    encrypt_pass = None
c33036
     experimental = False
c33036
     label = ""
c33036
     list_plugins = False
c33036
diff --git a/sos/archive.py b/sos/archive.py
c33036
index e153c09ad..263e3dd3f 100644
c33036
--- a/sos/archive.py
c33036
+++ b/sos/archive.py
c33036
@@ -142,11 +142,12 @@ class FileCacheArchive(Archive):
c33036
     _archive_root = ""
c33036
     _archive_name = ""
c33036
 
c33036
-    def __init__(self, name, tmpdir, policy, threads):
c33036
+    def __init__(self, name, tmpdir, policy, threads, enc_opts):
c33036
         self._name = name
c33036
         self._tmp_dir = tmpdir
c33036
         self._policy = policy
c33036
         self._threads = threads
c33036
+        self.enc_opts = enc_opts
c33036
         self._archive_root = os.path.join(tmpdir, name)
c33036
         with self._path_lock:
c33036
             os.makedirs(self._archive_root, 0o700)
c33036
@@ -384,12 +385,65 @@ def finalize(self, method):
c33036
                       os.stat(self._archive_name).st_size))
c33036
         self.method = method
c33036
         try:
c33036
-            return self._compress()
c33036
+            res = self._compress()
c33036
         except Exception as e:
c33036
             exp_msg = "An error occurred compressing the archive: "
c33036
             self.log_error("%s %s" % (exp_msg, e))
c33036
             return self.name()
c33036
 
c33036
+        if self.enc_opts['encrypt']:
c33036
+            try:
c33036
+                return self._encrypt(res)
c33036
+            except Exception as e:
c33036
+                exp_msg = "An error occurred encrypting the archive:"
c33036
+                self.log_error("%s %s" % (exp_msg, e))
c33036
+                return res
c33036
+        else:
c33036
+            return res
c33036
+
c33036
+    def _encrypt(self, archive):
c33036
+        """Encrypts the compressed archive using GPG.
c33036
+
c33036
+        If encryption fails for any reason, it should be logged by sos but not
c33036
+        cause execution to stop. The assumption is that the unencrypted archive
c33036
+        would still be of use to the user, and/or that the end user has another
c33036
+        means of securing the archive.
c33036
+
c33036
+        Returns the name of the encrypted archive, or raises an exception to
c33036
+        signal that encryption failed and the unencrypted archive name should
c33036
+        be used.
c33036
+        """
c33036
+        arc_name = archive.replace("sosreport-", "secured-sosreport-")
c33036
+        arc_name += ".gpg"
c33036
+        enc_cmd = "gpg --batch -o %s " % arc_name
c33036
+        env = None
c33036
+        if self.enc_opts["key"]:
c33036
+            # need to assume a trusted key here to be able to encrypt the
c33036
+            # archive non-interactively
c33036
+            enc_cmd += "--trust-model always -e -r %s " % self.enc_opts["key"]
c33036
+            enc_cmd += archive
c33036
+        if self.enc_opts["password"]:
c33036
+            # prevent change of gpg options using a long password, but also
c33036
+            # prevent the addition of quote characters to the passphrase
c33036
+            passwd = "%s" % self.enc_opts["password"].replace('\'"', '')
c33036
+            env = {"sos_gpg": passwd}
c33036
+            enc_cmd += "-c --passphrase-fd 0 "
c33036
+            enc_cmd = "/bin/bash -c \"echo $sos_gpg | %s\"" % enc_cmd
c33036
+            enc_cmd += archive
c33036
+        r = sos_get_command_output(enc_cmd, timeout=0, env=env)
c33036
+        if r["status"] == 0:
c33036
+            return arc_name
c33036
+        elif r["status"] == 2:
c33036
+            if self.enc_opts["key"]:
c33036
+                msg = "Specified key not in keyring"
c33036
+            else:
c33036
+                msg = "Could not read passphrase"
c33036
+        else:
c33036
+            # TODO: report the actual error from gpg. Currently, we cannot as
c33036
+            # sos_get_command_output() does not capture stderr
c33036
+            msg = "gpg exited with code %s" % r["status"]
c33036
+        raise Exception(msg)
c33036
+
c33036
 
c33036
 # Compatibility version of the tarfile.TarFile class. This exists to allow
c33036
 # compatibility with PY2 runtimes that lack the 'filter' parameter to the
c33036
@@ -468,8 +522,9 @@ class TarFileArchive(FileCacheArchive):
c33036
     method = None
c33036
     _with_selinux_context = False
c33036
 
c33036
-    def __init__(self, name, tmpdir, policy, threads):
c33036
-        super(TarFileArchive, self).__init__(name, tmpdir, policy, threads)
c33036
+    def __init__(self, name, tmpdir, policy, threads, enc_opts):
c33036
+        super(TarFileArchive, self).__init__(name, tmpdir, policy, threads,
c33036
+                                             enc_opts)
c33036
         self._suffix = "tar"
c33036
         self._archive_name = os.path.join(tmpdir, self.name())
c33036
 
c33036
diff --git a/sos/sosreport.py b/sos/sosreport.py
c33036
index 60802617c..00c3e8110 100644
c33036
--- a/sos/sosreport.py
c33036
+++ b/sos/sosreport.py
c33036
@@ -316,6 +316,13 @@ def _parse_args(args):
c33036
     preset_grp.add_argument("--del-preset", type=str, action="store",
c33036
                             help="Delete the named command line preset")
c33036
 
c33036
+    encrypt_grp = parser.add_mutually_exclusive_group()
c33036
+    encrypt_grp.add_argument("--encrypt-key",
c33036
+                             help="Encrypt the final archive using a GPG "
c33036
+                                  "key-pair")
c33036
+    encrypt_grp.add_argument("--encrypt-pass",
c33036
+                             help="Encrypt the final archive using a password")
c33036
+
c33036
     return parser.parse_args(args)
c33036
 
c33036
 
c33036
@@ -431,16 +438,25 @@ def get_temp_file(self):
c33036
         return self.tempfile_util.new()
c33036
 
c33036
     def _set_archive(self):
c33036
+        enc_opts = {
c33036
+            'encrypt': True if (self.opts.encrypt_pass or
c33036
+                                self.opts.encrypt_key) else False,
c33036
+            'key': self.opts.encrypt_key,
c33036
+            'password': self.opts.encrypt_pass
c33036
+        }
c33036
+
c33036
         archive_name = os.path.join(self.tmpdir,
c33036
                                     self.policy.get_archive_name())
c33036
         if self.opts.compression_type == 'auto':
c33036
             auto_archive = self.policy.get_preferred_archive()
c33036
             self.archive = auto_archive(archive_name, self.tmpdir,
c33036
-                                        self.policy, self.opts.threads)
c33036
+                                        self.policy, self.opts.threads,
c33036
+                                        enc_opts)
c33036
 
c33036
         else:
c33036
             self.archive = TarFileArchive(archive_name, self.tmpdir,
c33036
-                                          self.policy, self.opts.threads)
c33036
+                                          self.policy, self.opts.threads,
c33036
+                                          enc_opts)
c33036
 
c33036
         self.archive.set_debug(True if self.opts.debug else False)
c33036
 
c33036
diff --git a/tests/archive_tests.py b/tests/archive_tests.py
c33036
index b4dd8d0ff..e5b329b5f 100644
c33036
--- a/tests/archive_tests.py
c33036
+++ b/tests/archive_tests.py
c33036
@@ -19,7 +19,8 @@ class TarFileArchiveTest(unittest.TestCase):
c33036
 
c33036
     def setUp(self):
c33036
         self.tmpdir = tempfile.mkdtemp()
c33036
-        self.tf = TarFileArchive('test', self.tmpdir, Policy(), 1)
c33036
+        enc = {'encrypt': False}
c33036
+        self.tf = TarFileArchive('test', self.tmpdir, Policy(), 1, enc)
c33036
 
c33036
     def tearDown(self):
c33036
         shutil.rmtree(self.tmpdir)