Repository: freedomofpress/securedrop

Issue:
Phase out old signing key in `securedrop-admin`
`securedrop-admin` currently still trusts the old signing key for releases. It's been more than a year since the [full key rotation](https://media.securedrop.org/media/documents/signing-key-transition.txt) (not to be confused with the recent expiry bump), so I think this can be safely phased out:

https://github.com/freedomofpress/securedrop/blob/2f2db872f60bbc9024e0b35b8ebcdc2f5695acb2/admin/securedrop_admin/__init__.py#L49-L55


Discussion:
Yup, tis an easy fix/test, adding to 2.5.0

Resolving patch:
diff --git a/admin/securedrop_admin/__init__.py b/admin/securedrop_admin/__init__.py
--- a/admin/securedrop_admin/__init__.py
+++ b/admin/securedrop_admin/__init__.py
@@ -46,11 +46,9 @@
 
 sdlog = logging.getLogger(__name__)
 
-# We list two (2) pubkeys as authorized to sign SecureDrop release artifacts,
-# to provide a transition window during key rotation. On or around v2.0.0,
-# we can remove the older of the two keys and only trust the newer going forward.
+# In the event of a key rotation, this list can be added to
+# in order to support a transition period.
 RELEASE_KEYS = [
-    "22245C81E3BAEB4138B36061310F561200F4AD77",
     "2359E6538C0613E652955E6C188EDD3B7B22E6A3",
 ]
 DEFAULT_KEYSERVER = "hkps://keys.openpgp.org"
@@ -1004,9 +1002,6 @@ def update(args: argparse.Namespace) -> int:
         ).decode("utf-8")
 
         good_sig_text = [
-            'Good signature from "SecureDrop Release Signing ' + 'Key"',
-            'Good signature from "SecureDrop Release Signing '
-            + 'Key <securedrop-release-key@freedom.press>"',
             'Good signature from "SecureDrop Release Signing '
             + 'Key <securedrop-release-key-2021@freedom.press>"',
         ]
@@ -1021,7 +1016,7 @@ def update(args: argparse.Namespace) -> int:
         # appears on the second line of the output, and that there is a single
         # match from good_sig_text[]
         if (
-            (RELEASE_KEYS[0] in gpg_lines[1] or RELEASE_KEYS[1] in gpg_lines[1])
+            any(key in gpg_lines[1] for key in RELEASE_KEYS)
             and len(good_sig_matches) == 1
             and bad_sig_text not in sig_result
         ):


Test patch:
diff --git a/admin/tests/test_securedrop-admin.py b/admin/tests/test_securedrop-admin.py
--- a/admin/tests/test_securedrop-admin.py
+++ b/admin/tests/test_securedrop-admin.py
@@ -1,7 +1,8 @@
 # -*- coding: utf-8 -*-
 #
 # SecureDrop whistleblower submission system
-# Copyright (C) 2017 Loic Dachary <loic@dachary.org>
+# Copyright (C) 2017- Freedom of the Press Foundation and SecureDrop
+# contributors
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Affero General Public License as published by
@@ -247,46 +248,13 @@ def test_get_release_key_from_valid_keyserver(self, tmpdir, caplog):
     @pytest.mark.parametrize(
         "git_output",
         [
-            b"gpg: Signature made Tue 13 Mar "
-            b"2018 01:14:11 AM UTC\n"
-            b"gpg:                using RSA key "
-            b"22245C81E3BAEB4138B36061310F561200F4AD77\n"
-            b'gpg: Good signature from "SecureDrop Release '
-            b'Signing Key" [unknown]\n',
-            b"gpg: Signature made Thu 20 Jul "
-            b"2017 08:12:25 PM EDT\n"
-            b"gpg:                using RSA key "
-            b"22245C81E3BAEB4138B36061310F561200F4AD77\n"
-            b'gpg: Good signature from "SecureDrop Release '
-            b"Signing Key "
-            b'<securedrop-release-key@freedom.press>"\n',
             b"gpg: Signature made Thu 20 Jul "
-            b"2017 08:12:25 PM EDT\n"
+            b"2022 08:12:25 PM EDT\n"
             b"gpg:                using RSA key "
             b"2359E6538C0613E652955E6C188EDD3B7B22E6A3\n"
             b'gpg: Good signature from "SecureDrop Release '
             b"Signing Key "
-            b'<securedrop-release-key-2021@freedom.press>"\n',
-            b"gpg: Signature made Thu 20 Jul "
-            b"2017 08:12:25 PM EDT\n"
-            b"gpg:                using RSA key "
-            b"2359E6538C0613E652955E6C188EDD3B7B22E6A3\n"
-            b'gpg: Good signature from "SecureDrop Release '
-            b'Signing Key" [unknown]\n'
-            b'gpg:                 aka "SecureDrop Release '
-            b"Signing Key "
-            b'<securedrop-release-key-2021@freedom.press>" '
-            b"[unknown]\n",
-            b"gpg: Signature made Thu 20 Jul "
-            b"2017 08:12:25 PM EDT\n"
-            b"gpg:                using RSA key "
-            b"22245C81E3BAEB4138B36061310F561200F4AD77\n"
-            b'gpg: Good signature from "SecureDrop Release '
-            b'Signing Key" [unknown]\n'
-            b'gpg:                 aka "SecureDrop Release '
-            b"Signing Key "
-            b'<securedrop-release-key@freedom.press>" '
-            b"[unknown]\n",
+            b'<securedrop-release-key-2021@freedom.press>" [unknown]\n',
         ],
     )
     def test_update_signature_verifies(self, tmpdir, caplog, git_output):
@@ -295,6 +263,9 @@ def test_update_signature_verifies(self, tmpdir, caplog, git_output):
         patchers = [
             mock.patch("securedrop_admin.check_for_updates", return_value=(True, "0.6.1")),
             mock.patch("subprocess.check_call"),
+            # securedrop-admin checks if there is a branch with the same name as a tag
+            # that is being verified, and bails if there is. To ensure the verification
+            # succeeds, we have to mock the "not a valid ref" output it looks for.
             mock.patch(
                 "subprocess.check_output",
                 side_effect=[
@@ -322,7 +293,49 @@ def test_update_unexpected_exception_git_refs(self, tmpdir, caplog):
         args = argparse.Namespace(root=git_repo_path)
 
         git_output = (
-            b"gpg: Signature made Tue 13 Mar 2018 01:14:11 AM UTC\n"
+            b"gpg: Signature made Tue 13 Mar 2022 01:14:11 AM UTC\n"
+            b"gpg:                using RSA key "
+            b"2359E6538C0613E652955E6C188EDD3B7B22E6A3\n"
+            b'gpg: Good signature from "SecureDrop Release '
+            b'Signing Key <securedrop-release-key-2021@freedom.press>" [unknown]\n'
+        )
+
+        patchers = [
+            mock.patch("securedrop_admin.check_for_updates", return_value=(True, "0.6.1")),
+            mock.patch("subprocess.check_call"),
+            mock.patch(
+                "subprocess.check_output",
+                side_effect=[
+                    git_output,
+                    subprocess.CalledProcessError(1, "cmd", b"a random error"),
+                ],
+            ),
+        ]
+
+        for patcher in patchers:
+            patcher.start()
+
+        try:
+            ret_code = securedrop_admin.update(args)
+            assert "Applying SecureDrop updates..." in caplog.text
+            assert "Signature verification successful." not in caplog.text
+            assert "Updated to SecureDrop" not in caplog.text
+            assert ret_code == 1
+        finally:
+            for patcher in patchers:
+                patcher.stop()
+
+    def test_outdated_signature_does_not_verify(self, tmpdir, caplog):
+        """
+        When a tag is signed with a release key that is no longer valid
+            Then the signature of a current tag should not verify
+        """
+
+        git_repo_path = str(tmpdir)
+        args = argparse.Namespace(root=git_repo_path)
+
+        git_output = (
+            b"gpg: Signature made Tue 13 Mar 2022 01:14:11 AM UTC\n"
             b"gpg:                using RSA key "
             b"22245C81E3BAEB4138B36061310F561200F4AD77\n"
             b'gpg: Good signature from "SecureDrop Release '
@@ -336,7 +349,7 @@ def test_update_unexpected_exception_git_refs(self, tmpdir, caplog):
                 "subprocess.check_output",
                 side_effect=[
                     git_output,
-                    subprocess.CalledProcessError(1, "cmd", b"a random error"),
+                    subprocess.CalledProcessError(1, "cmd", b"not a valid ref"),
                 ],
             ),
         ]
@@ -359,11 +372,11 @@ def test_update_signature_does_not_verify(self, tmpdir, caplog):
         args = argparse.Namespace(root=git_repo_path)
 
         git_output = (
-            b"gpg: Signature made Tue 13 Mar 2018 01:14:11 AM UTC\n"
+            b"gpg: Signature made Tue 13 Mar 2022 01:14:11 AM UTC\n"
             b"gpg:                using RSA key "
-            b"22245C81E3BAEB4138B36061310F561200F4AD77\n"
+            b"2359E6538C0613E652955E6C188EDD3B7B22E6A3\n"
             b'gpg: BAD signature from "SecureDrop Release '
-            b'Signing Key" [unknown]\n'
+            b'Signing Key <securedrop-release-key-2021@freedom.press>" [unknown]\n'
         )
 
         with mock.patch("securedrop_admin.check_for_updates", return_value=(True, "0.6.1")):
@@ -380,11 +393,11 @@ def test_update_malicious_key_named_fingerprint(self, tmpdir, caplog):
         args = argparse.Namespace(root=git_repo_path)
 
         git_output = (
-            b"gpg: Signature made Tue 13 Mar 2018 01:14:11 AM UTC\n"
+            b"gpg: Signature made Tue 13 Mar 2022 01:14:11 AM UTC\n"
             b"gpg:                using RSA key "
             b"1234567812345678123456781234567812345678\n"
-            b'gpg: Good signature from "22245C81E3BAEB4138'
-            b'B36061310F561200F4AD77" [unknown]\n'
+            b'gpg: Good signature from "2359E6538C0613E652'
+            b'955E6C188EDD3B7B22E6A3" [unknown]\n'
         )
 
         with mock.patch("securedrop_admin.check_for_updates", return_value=(True, "0.6.1")):
@@ -401,11 +414,12 @@ def test_update_malicious_key_named_good_sig(self, tmpdir, caplog):
         args = argparse.Namespace(root=git_repo_path)
 
         git_output = (
-            b"gpg: Signature made Tue 13 Mar 2018 01:14:11 AM UTC\n"
+            b"gpg: Signature made Tue 13 Mar 2022 01:14:11 AM UTC\n"
             b"gpg:                using RSA key "
             b"1234567812345678123456781234567812345678\n"
             b"gpg: Good signature from Good signature from "
-            b'"SecureDrop Release Signing Key" [unknown]\n'
+            b'"SecureDrop Release Signing Key <securedrop-release-key-2021@freedom.press>" '
+            b"[unknown]\n"
         )
 
         with mock.patch("securedrop_admin.check_for_updates", return_value=(True, "0.6.1")):
@@ -422,12 +436,13 @@ def test_update_malicious_key_named_good_sig_fingerprint(self, tmpdir, caplog):
         args = argparse.Namespace(root=git_repo_path)
 
         git_output = (
-            b"gpg: Signature made Tue 13 Mar 2018 01:14:11 AM UTC\n"
+            b"gpg: Signature made Tue 13 Mar 2022 01:14:11 AM UTC\n"
             b"gpg:                using RSA key "
             b"1234567812345678123456781234567812345678\n"
             b"gpg: Good signature from 22245C81E3BAEB4138"
-            b"B36061310F561200F4AD77 Good signature from "
-            b'"SecureDrop Release Signing Key" [unknown]\n'
+            b"955E6C188EDD3B7B22E6A3 Good signature from "
+            b'"SecureDrop Release Signing Key <securedrop-release-key-2021@freedom.press>" '
+            b"[unknown]\n"
         )
 
         with mock.patch("securedrop_admin.check_for_updates", return_value=(True, "0.6.1")):