← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~apw/launchpad/signing-fit into lp:launchpad

 

Andy Whitcroft has proposed merging lp:~apw/launchpad/signing-fit into lp:launchpad with lp:~apw/launchpad/signing-sipl as a prerequisite.

Commit message:
Add u-boot Flat Image Tree signing support. u-boot supports generation and verification of FIT images in a similar fashion to secure boot.  These signatures are expressed as signature nodes in the DTB contained within the FIT image.  Add support for performing FIT signing against *.fit files in the signing custom uploads.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~apw/launchpad/signing-fit/+merge/368277
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~apw/launchpad/signing-fit into lp:launchpad.
=== modified file 'lib/lp/archivepublisher/signing.py'
--- lib/lp/archivepublisher/signing.py	2019-06-03 15:06:10 +0000
+++ lib/lp/archivepublisher/signing.py	2019-06-03 15:06:10 +0000
@@ -97,6 +97,8 @@
             self.opal_x509 = None
             self.sipl_pem = None
             self.sipl_x509 = None
+            self.fit_key = None
+            self.fit_cert = None
             self.autokey = False
         else:
             self.uefi_key = os.path.join(pubconf.signingroot, "uefi.key")
@@ -107,6 +109,11 @@
             self.opal_x509 = os.path.join(pubconf.signingroot, "opal.x509")
             self.sipl_pem = os.path.join(pubconf.signingroot, "sipl.pem")
             self.sipl_x509 = os.path.join(pubconf.signingroot, "sipl.x509")
+            # Note: the signature tool allows a collection of keys and takes
+            #       a directory name with all valid keys.  Avoid mixing the
+            #       other signing types' keys with the fit keys.
+            self.fit_key = os.path.join(pubconf.signingroot, "fit", "fit.key")
+            self.fit_cert = os.path.join(pubconf.signingroot, "fit", "fit.crt")
             self.autokey = pubconf.signingautokey
 
         self.setComponents(tarfile_path)
@@ -182,6 +189,8 @@
                     yield (os.path.join(dirpath, filename), self.signOpal)
                 elif filename.endswith(".sipl"):
                     yield (os.path.join(dirpath, filename), self.signSipl)
+                elif filename.endswith(".fit"):
+                    yield (os.path.join(dirpath, filename), self.signFit)
 
     def getKeys(self, which, generate, *keynames):
         """Validate and return the uefi key and cert for encryption."""
@@ -213,29 +222,33 @@
         common_name = "PPA %s %s" % (owner, archive)
         return common_name[0:64 - len(suffix)] + suffix
 
-    def generateUefiKeys(self):
-        """Generate new UEFI Keys for this archive."""
-        directory = os.path.dirname(self.uefi_key)
+    def generateKeyCrtPair(self, key_type, key_filename, cert_filename):
+        """Generate new Key/Crt key pairs."""
+        directory = os.path.dirname(key_filename)
         if not os.path.exists(directory):
             os.makedirs(directory)
 
         common_name = self.generateKeyCommonName(
-            self.archive.owner.name, self.archive.name)
+            self.archive.owner.name, self.archive.name, key_type)
         subject = '/CN=' + common_name + '/'
 
         old_mask = os.umask(0o077)
         try:
             new_key_cmd = [
                 'openssl', 'req', '-new', '-x509', '-newkey', 'rsa:2048',
-                '-subj', subject, '-keyout', self.uefi_key,
-                '-out', self.uefi_cert, '-days', '3650', '-nodes', '-sha256',
+                '-subj', subject, '-keyout', key_filename,
+                '-out', cert_filename, '-days', '3650', '-nodes', '-sha256',
                 ]
-            self.callLog("UEFI keygen", new_key_cmd)
+            self.callLog(key_type + " keygen", new_key_cmd)
         finally:
             os.umask(old_mask)
 
-        if os.path.exists(self.uefi_cert):
-            os.chmod(self.uefi_cert, 0o644)
+        if os.path.exists(cert_filename):
+            os.chmod(cert_filename, 0o644)
+
+    def generateUefiKeys(self):
+        """Generate new UEFI Keys for this archive."""
+        self.generateKeyCrtPair("UEFI", self.uefi_key, self.uefi_cert)
 
     def signUefi(self, image):
         """Attempt to sign an image."""
@@ -368,6 +381,26 @@
         cmdl = ["kmodsign", "-D", "sha512", pem, cert, image, image + ".sig"]
         return self.callLog("Sipl signing", cmdl)
 
+    def generateFitKeys(self):
+        """Generate new Fit Keys for this archive."""
+        self.generateKeyCrtPair("Fit", self.fit_key, self.fit_cert)
+
+    def signFit(self, image):
+        """Attempt to sign an image."""
+        image_signed = "%s.signed" % image
+        remove_if_exists(image_signed)
+        (key, cert) = self.getKeys('Fit', self.generateFitKeys,
+            self.fit_key, self.fit_cert)
+        if not key or not cert:
+            return
+        self.publishPublicKey(cert)
+        # Make a copy of the image as mkimage signs in place and in
+        # signed-only mode we will remove the original file.
+        shutil.copy(image, image_signed)
+        cmdl = ["mkimage", "-F", "-k", os.path.dirname(key), "-r",
+            image_signed]
+        return self.callLog("Fit signing", cmdl)
+
     def convertToTarball(self):
         """Convert unpacked output to signing tarball."""
         tarfilename = os.path.join(self.tmpdir, "signed.tar.gz")

=== modified file 'lib/lp/archivepublisher/tests/test_signing.py'
--- lib/lp/archivepublisher/tests/test_signing.py	2019-06-03 15:06:10 +0000
+++ lib/lp/archivepublisher/tests/test_signing.py	2019-06-03 15:06:10 +0000
@@ -85,6 +85,8 @@
         self.callers = {
             "UEFI signing": 0,
             "UEFI keygen": 0,
+            "Fit signing": 0,
+            "Fit keygen": 0,
             "Kmod signing": 0,
             "Kmod keygen key": 0,
             "Kmod keygen cert": 0,
@@ -111,6 +113,15 @@
             write_file(self.upload.uefi_key, b"")
             write_file(self.upload.uefi_cert, b"")
 
+        elif description == "Fit signing":
+            filename = cmdl[-1]
+            if filename.endswith(".fit"):
+                write_file(filename + ".signed", b"")
+
+        elif description == "Fit keygen":
+            write_file(self.upload.fit_key, b"")
+            write_file(self.upload.fit_cert, b"")
+
         elif description == "Kmod signing":
             filename = cmdl[-1]
             if filename.endswith(".ko.sig"):
@@ -191,7 +202,7 @@
             purpose=ArchivePurpose.PPA)
         self.signing_dir = os.path.join(
             self.temp_dir, "signing", "signing-owner", "testing")
-        self.testcase_cn = '/CN=PPA signing-owner testing/'
+        self.testcase_cn = 'PPA signing-owner testing'
         pubconf = getPubConfig(self.archive)
         if not os.path.exists(pubconf.temproot):
             os.makedirs(pubconf.temproot)
@@ -211,6 +222,15 @@
             write_file(self.key, b"")
             write_file(self.cert, b"")
 
+    def setUpFitKeys(self, create=True):
+        # We expect and need the fit keys to be in their own
+        # directory as part of key protection for mkimage.
+        self.fit_key = os.path.join(self.signing_dir, "fit", "fit.key")
+        self.fit_cert = os.path.join(self.signing_dir, "fit", "fit.crt")
+        if create:
+            write_file(self.fit_key, b"")
+            write_file(self.fit_cert, b"")
+
     def setUpKmodKeys(self, create=True):
         self.kmod_pem = os.path.join(self.signing_dir, "kmod.pem")
         self.kmod_x509 = os.path.join(self.signing_dir, "kmod.x509")
@@ -255,6 +275,7 @@
         upload = SigningUpload()
         # Under no circumstances is it safe to execute actual commands.
         self.fake_call = FakeMethod(result=0)
+        self.fake_copyfile = FakeMethod(result=0)
         upload.callLog = FakeMethodCallLog(upload=upload)
         self.useFixture(MonkeyPatch("subprocess.call", self.fake_call))
         upload.process(self.archive, self.path, self.suite)
@@ -269,6 +290,7 @@
         upload.signKmod = FakeMethod()
         upload.signOpal = FakeMethod()
         upload.signSipl = FakeMethod()
+        upload.signFit = FakeMethod()
         # Under no circumstances is it safe to execute actual commands.
         fake_call = FakeMethod(result=0)
         self.useFixture(MonkeyPatch("subprocess.call", fake_call))
@@ -290,6 +312,7 @@
         self.tarfile.add_file("1.0/empty.ko", b"")
         self.tarfile.add_file("1.0/empty.opal", b"")
         self.tarfile.add_file("1.0/empty.sipl", b"")
+        self.tarfile.add_file("1.0/empty.fit", b"")
         upload = self.process_emulate()
         self.assertContentEqual([], upload.callLog.caller_list())
 
@@ -301,6 +324,7 @@
         self.tarfile.add_file("1.0/empty.ko", b"")
         self.tarfile.add_file("1.0/empty.opal", b"")
         self.tarfile.add_file("1.0/empty.sipl", b"")
+        self.tarfile.add_file("1.0/empty.fit", b"")
         upload = self.process_emulate()
         self.assertContentEqual([], upload.callLog.caller_list())
 
@@ -314,6 +338,7 @@
         self.tarfile.add_file("1.0/empty.ko", b"")
         self.tarfile.add_file("1.0/empty.opal", b"")
         self.tarfile.add_file("1.0/empty.sipl", b"")
+        self.tarfile.add_file("1.0/empty.fit", b"")
         upload = self.process_emulate()
         expected_callers = [
             ('UEFI signing', 1),
@@ -330,6 +355,7 @@
         self.tarfile.add_file("1.0/empty.ko", b"")
         self.tarfile.add_file("1.0/empty.opal", b"")
         self.tarfile.add_file("1.0/empty.sipl", b"")
+        self.tarfile.add_file("1.0/empty.fit", b"")
         upload = self.process_emulate()
         expected_callers = [
             ('UEFI keygen', 1),
@@ -339,10 +365,12 @@
             ('Opal keygen cert', 1),
             ('Sipl keygen key', 1),
             ('Sipl keygen cert', 1),
+            ('Fit keygen', 1),
             ('UEFI signing', 1),
             ('Kmod signing', 1),
             ('Opal signing', 1),
             ('Sipl signing', 1),
+            ('Fit signing', 1),
         ]
         self.assertContentEqual(expected_callers, upload.callLog.caller_list())
 
@@ -417,11 +445,13 @@
         self.setUpKmodKeys()
         self.setUpOpalKeys()
         self.setUpSiplKeys()
+        self.setUpFitKeys()
         self.openArchive("test", "1.0", "amd64")
         self.tarfile.add_file("1.0/empty.efi", b"")
         self.tarfile.add_file("1.0/empty.ko", b"")
         self.tarfile.add_file("1.0/empty.opal", b"")
         self.tarfile.add_file("1.0/empty.sipl", b"")
+        self.tarfile.add_file("1.0/empty.fit", b"")
         self.process_emulate()
         self.assertThat(self.getSignedPath("test", "amd64"), SignedMatches([
             "1.0/SHA256SUMS",
@@ -429,6 +459,7 @@
             "1.0/empty.ko", "1.0/empty.ko.sig", "1.0/control/kmod.x509",
             "1.0/empty.opal", "1.0/empty.opal.sig", "1.0/control/opal.x509",
             "1.0/empty.sipl", "1.0/empty.sipl.sig", "1.0/control/sipl.x509",
+            "1.0/empty.fit", "1.0/empty.fit.signed", "1.0/control/fit.crt",
             ]))
 
     def test_options_tarball(self):
@@ -438,12 +469,14 @@
         self.setUpKmodKeys()
         self.setUpOpalKeys()
         self.setUpSiplKeys()
+        self.setUpFitKeys()
         self.openArchive("test", "1.0", "amd64")
         self.tarfile.add_file("1.0/control/options", b"tarball")
         self.tarfile.add_file("1.0/empty.efi", b"")
         self.tarfile.add_file("1.0/empty.ko", b"")
         self.tarfile.add_file("1.0/empty.opal", b"")
         self.tarfile.add_file("1.0/empty.sipl", b"")
+        self.tarfile.add_file("1.0/empty.fit", b"")
         self.process_emulate()
         self.assertThat(self.getSignedPath("test", "amd64"), SignedMatches([
             "1.0/SHA256SUMS",
@@ -461,6 +494,8 @@
                 '1.0/control/opal.x509',
                 '1.0/empty.sipl', '1.0/empty.sipl.sig',
                 '1.0/control/sipl.x509',
+                '1.0/empty.fit', '1.0/empty.fit.signed',
+                '1.0/control/fit.crt',
                 ], tarball.getnames())
 
     def test_options_signed_only(self):
@@ -470,12 +505,14 @@
         self.setUpKmodKeys()
         self.setUpOpalKeys()
         self.setUpSiplKeys()
+        self.setUpFitKeys()
         self.openArchive("test", "1.0", "amd64")
         self.tarfile.add_file("1.0/control/options", b"signed-only")
         self.tarfile.add_file("1.0/empty.efi", b"")
         self.tarfile.add_file("1.0/empty.ko", b"")
         self.tarfile.add_file("1.0/empty.opal", b"")
         self.tarfile.add_file("1.0/empty.sipl", b"")
+        self.tarfile.add_file("1.0/empty.fit", b"")
         self.process_emulate()
         self.assertThat(self.getSignedPath("test", "amd64"), SignedMatches([
             "1.0/SHA256SUMS", "1.0/control/options",
@@ -483,6 +520,7 @@
             "1.0/empty.ko.sig", "1.0/control/kmod.x509",
             "1.0/empty.opal.sig", "1.0/control/opal.x509",
             "1.0/empty.sipl.sig", "1.0/control/sipl.x509",
+            "1.0/empty.fit.signed", "1.0/control/fit.crt",
             ]))
 
     def test_options_tarball_signed_only(self):
@@ -493,12 +531,14 @@
         self.setUpKmodKeys()
         self.setUpOpalKeys()
         self.setUpSiplKeys()
+        self.setUpFitKeys()
         self.openArchive("test", "1.0", "amd64")
         self.tarfile.add_file("1.0/control/options", b"tarball\nsigned-only")
         self.tarfile.add_file("1.0/empty.efi", b"")
         self.tarfile.add_file("1.0/empty.ko", b"")
         self.tarfile.add_file("1.0/empty.opal", b"")
         self.tarfile.add_file("1.0/empty.sipl", b"")
+        self.tarfile.add_file("1.0/empty.fit", b"")
         self.process_emulate()
         self.assertThat(self.getSignedPath("test", "amd64"), SignedMatches([
             "1.0/SHA256SUMS",
@@ -513,12 +553,17 @@
                 '1.0/empty.ko.sig', '1.0/control/kmod.x509',
                 '1.0/empty.opal.sig', '1.0/control/opal.x509',
                 '1.0/empty.sipl.sig', '1.0/control/sipl.x509',
+                '1.0/empty.fit.signed', '1.0/control/fit.crt',
                 ], tarball.getnames())
 
     def test_no_signed_files(self):
         # Tarballs containing no *.efi files are extracted without complaint.
         # Nothing is signed.
         self.setUpUefiKeys()
+        self.setUpKmodKeys()
+        self.setUpOpalKeys()
+        self.setUpSiplKeys()
+        self.setUpFitKeys()
         self.openArchive("empty", "1.0", "amd64")
         self.tarfile.add_file("1.0/hello", b"world")
         upload = self.process()
@@ -528,6 +573,7 @@
         self.assertEqual(0, upload.signKmod.call_count)
         self.assertEqual(0, upload.signOpal.call_count)
         self.assertEqual(0, upload.signSipl.call_count)
+        self.assertEqual(0, upload.signFit.call_count)
 
     def test_already_exists(self):
         # If the target directory already exists, processing fails.
@@ -595,7 +641,71 @@
         args = fake_call.calls[0][0][0]
         expected_cmd = [
             'openssl', 'req', '-new', '-x509', '-newkey', 'rsa:2048',
-            '-subj', self.testcase_cn, '-keyout', self.key, '-out', self.cert,
+            '-subj', '/CN=' + self.testcase_cn + ' UEFI/',
+            '-keyout', self.key, '-out', self.cert,
+            '-days', '3650', '-nodes', '-sha256',
+            ]
+        self.assertEqual(expected_cmd, args)
+
+    def test_correct_fit_signing_command_executed(self):
+        # Check that calling signFit() will generate the expected command
+        # when appropriate keys are present.
+        self.setUpFitKeys()
+        fake_call = FakeMethod(result=0)
+        self.useFixture(MonkeyPatch("subprocess.call", fake_call))
+        fake_copy = FakeMethod(result=0)
+        self.useFixture(MonkeyPatch("shutil.copy", fake_copy))
+        upload = SigningUpload()
+        upload.generateFitKeys = FakeMethod()
+        upload.setTargetDirectory(
+            self.archive, "test_1.0_amd64.tar.gz", "distroseries")
+        upload.signFit('t.fit')
+        # Confirm the copy was performed.
+        self.assertEqual(1, fake_copy.call_count)
+        args = fake_copy.calls[0][0]
+        expected_copy = ('t.fit', 't.fit.signed')
+        self.assertEqual(expected_copy, args)
+        # Assert command form.
+        args = fake_call.calls[0][0][0]
+        expected_cmd = [
+            'mkimage', '-F', '-k', os.path.dirname(self.fit_key), '-r',
+            't.fit.signed',
+        ]
+        self.assertEqual(expected_cmd, args)
+        self.assertEqual(0, upload.generateFitKeys.call_count)
+
+    def test_correct_fit_signing_command_executed_no_keys(self):
+        # Check that calling signFit() will generate no commands when
+        # no keys are present.
+        self.setUpFitKeys(create=False)
+        fake_call = FakeMethod(result=0)
+        self.useFixture(MonkeyPatch("subprocess.call", fake_call))
+        upload = SigningUpload()
+        upload.generateFitKeys = FakeMethod()
+        upload.setTargetDirectory(
+            self.archive, "test_1.0_amd64.tar.gz", "distroseries")
+        upload.signUefi('t.fit')
+        self.assertEqual(0, fake_call.call_count)
+        self.assertEqual(0, upload.generateFitKeys.call_count)
+
+    def test_correct_fit_keygen_command_executed(self):
+        # Check that calling generateFitKeys() will generate the
+        # expected command.
+        self.setUpPPA()
+        self.setUpFitKeys(create=False)
+        fake_call = FakeMethod(result=0)
+        self.useFixture(MonkeyPatch("subprocess.call", fake_call))
+        upload = SigningUpload()
+        upload.setTargetDirectory(
+            self.archive, "test_1.0_amd64.tar.gz", "distroseries")
+        upload.generateFitKeys()
+        self.assertEqual(1, fake_call.call_count)
+        # Assert the actual command matches.
+        args = fake_call.calls[0][0][0]
+        expected_cmd = [
+            'openssl', 'req', '-new', '-x509', '-newkey', 'rsa:2048',
+            '-subj', '/CN=' + self.testcase_cn + ' Fit/',
+            '-keyout', self.fit_key, '-out', self.fit_cert,
             '-days', '3650', '-nodes', '-sha256',
             ]
         self.assertEqual(expected_cmd, args)
@@ -610,7 +720,7 @@
         text = upload.generateOpensslConfig('Kmod', upload.openssl_config_kmod)
 
         id_re = re.compile(r'^# KMOD openssl config\b')
-        cn_re = re.compile(r'\bCN\s*=\s*' + self.testcase_cn[4:-1] + '\s+Kmod')
+        cn_re = re.compile(r'\bCN\s*=\s*' + self.testcase_cn + '\s+Kmod')
         eku_re = re.compile(
             r'\bextendedKeyUsage\s*=\s*'
             r'codeSigning,1.3.6.1.4.1.2312.16.1.2\s*\b')
@@ -696,7 +806,7 @@
         text = upload.generateOpensslConfig('Opal', upload.openssl_config_opal)
 
         id_re = re.compile(r'^# OPAL openssl config\b')
-        cn_re = re.compile(r'\bCN\s*=\s*' + self.testcase_cn[4:-1] + '\s+Opal')
+        cn_re = re.compile(r'\bCN\s*=\s*' + self.testcase_cn + '\s+Opal')
 
         self.assertIn('[ req ]', text)
         self.assertIsNotNone(id_re.search(text))
@@ -779,7 +889,7 @@
         text = upload.generateOpensslConfig('Sipl', upload.openssl_config_sipl)
 
         id_re = re.compile(r'^# SIPL openssl config\b')
-        cn_re = re.compile(r'\bCN\s*=\s*' + self.testcase_cn[4:-1] + '\s+Sipl')
+        cn_re = re.compile(r'\bCN\s*=\s*' + self.testcase_cn + '\s+Sipl')
 
         self.assertIn('[ req ]', text)
         self.assertIsNotNone(id_re.search(text))
@@ -860,6 +970,14 @@
         upload = self.process()
         self.assertEqual(1, upload.signUefi.call_count)
 
+    def test_signs_fit_image(self):
+        # Each image in the tarball is signed.
+        self.setUpFitKeys()
+        self.openArchive("test", "1.0", "amd64")
+        self.tarfile.add_file("1.0/empty.fit", b"")
+        upload = self.process()
+        self.assertEqual(1, upload.signFit.call_count)
+
     def test_signs_kmod_image(self):
         # Each image in the tarball is signed.
         self.setUpKmodKeys()
@@ -886,7 +1004,6 @@
 
     def test_signs_combo_image(self):
         # Each image in the tarball is signed.
-        self.setUpKmodKeys()
         self.openArchive("test", "1.0", "amd64")
         self.tarfile.add_file("1.0/empty.efi", b"")
         self.tarfile.add_file("1.0/empty.ko", b"")
@@ -898,11 +1015,17 @@
         self.tarfile.add_file("1.0/empty2.sipl", b"")
         self.tarfile.add_file("1.0/empty3.sipl", b"")
         self.tarfile.add_file("1.0/empty4.sipl", b"")
+        self.tarfile.add_file("1.0/empty.fit", b"")
+        self.tarfile.add_file("1.0/empty2.fit", b"")
+        self.tarfile.add_file("1.0/empty3.fit", b"")
+        self.tarfile.add_file("1.0/empty4.fit", b"")
+        self.tarfile.add_file("1.0/empty5.fit", b"")
         upload = self.process()
         self.assertEqual(1, upload.signUefi.call_count)
         self.assertEqual(2, upload.signKmod.call_count)
         self.assertEqual(3, upload.signOpal.call_count)
         self.assertEqual(4, upload.signSipl.call_count)
+        self.assertEqual(5, upload.signFit.call_count)
 
     def test_installed(self):
         # Files in the tarball are installed correctly.
@@ -974,6 +1097,43 @@
         self.assertEqual(stat.S_IMODE(os.stat(self.key).st_mode), 0o600)
         self.assertEqual(stat.S_IMODE(os.stat(self.cert).st_mode), 0o644)
 
+    def test_create_fit_keys_autokey_off(self):
+        # Keys are not created.
+        self.setUpFitKeys(create=False)
+        self.assertFalse(os.path.exists(self.fit_key))
+        self.assertFalse(os.path.exists(self.fit_cert))
+        fake_call = FakeMethod(result=0)
+        self.useFixture(MonkeyPatch("subprocess.call", fake_call))
+        upload = SigningUpload()
+        upload.callLog = FakeMethodCallLog(upload=upload)
+        upload.setTargetDirectory(
+            self.archive, "test_1.0_amd64.tar.gz", "distroseries")
+        upload.signFit(os.path.join(self.makeTemporaryDirectory(), 'fit'))
+        self.assertEqual(0, upload.callLog.caller_count('Fit keygen'))
+        self.assertFalse(os.path.exists(self.fit_key))
+        self.assertFalse(os.path.exists(self.fit_cert))
+
+    def test_create_fit_keys_autokey_on(self):
+        # Keys are created on demand.
+        self.setUpPPA()
+        self.setUpFitKeys(create=False)
+        self.assertFalse(os.path.exists(self.fit_key))
+        self.assertFalse(os.path.exists(self.fit_cert))
+        fake_call = FakeMethod(result=0)
+        self.useFixture(MonkeyPatch("subprocess.call", fake_call))
+        fake_copy = FakeMethod(result=0)
+        self.useFixture(MonkeyPatch("shutil.copy", fake_copy))
+        upload = SigningUpload()
+        upload.callLog = FakeMethodCallLog(upload=upload)
+        upload.setTargetDirectory(
+            self.archive, "test_1.0_amd64.tar.gz", "distroseries")
+        upload.signFit(os.path.join(self.makeTemporaryDirectory(), 't.fit'))
+        self.assertEqual(1, upload.callLog.caller_count('Fit keygen'))
+        self.assertTrue(os.path.exists(self.fit_key))
+        self.assertTrue(os.path.exists(self.fit_cert))
+        self.assertEqual(stat.S_IMODE(os.stat(self.fit_key).st_mode), 0o600)
+        self.assertEqual(stat.S_IMODE(os.stat(self.fit_cert).st_mode), 0o644)
+
     def test_create_kmod_keys_autokey_off(self):
         # Keys are not created.
         self.setUpKmodKeys(create=False)


Follow ups