← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/lp-signing:cv2-kernel into lp-signing:master

 

Colin Watson has proposed merging ~cjwatson/lp-signing:cv2-kernel into lp-signing:master.

Commit message:
Add support for Ambarella CV2 kernel keys

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/lp-signing/+git/lp-signing/+merge/396849
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/lp-signing:cv2-kernel into lp-signing:master.
diff --git a/lp_signing/enums.py b/lp_signing/enums.py
index 8d020d4..e76b7cf 100644
--- a/lp_signing/enums.py
+++ b/lp_signing/enums.py
@@ -53,6 +53,12 @@ class KeyType(DBEnumeratedType):
         An OpenPGP signing key.
         """)
 
+    CV2_KERNEL = DBItem(7, """
+        CV2 Kernel
+
+        An Ambarella CV2 kernel signing key.
+        """)
+
 
 class OpenPGPKeyAlgorithm(EnumeratedType):
 
diff --git a/lp_signing/model/key.py b/lp_signing/model/key.py
index 6b0673c..1dc70f8 100644
--- a/lp_signing/model/key.py
+++ b/lp_signing/model/key.py
@@ -9,6 +9,7 @@ __all__ = [
 
 import base64
 from contextlib import contextmanager
+import hashlib
 import json
 import logging
 import os
@@ -416,6 +417,28 @@ class Key(Storm):
         return export_secret_result, export_public_result, key.fpr
 
     @classmethod
+    def _generateRSA(cls, tmp, key_type):
+        """Generate a new RSA key.
+
+        :param tmp: A `Path` to a temporary directory.
+        :param key_type: The `KeyType` to generate.
+        :return: A tuple of (private key, public key).
+        """
+        private_key = tmp / f"{key_type.name.lower()}.priv"
+        public_key = tmp / f"{key_type.name.lower()}.pub"
+        _log_subprocess_run([
+            "openssl", "genpkey", "-algorithm", "RSA",
+            "-out", str(private_key),
+            "-pkeyopt", "rsa_keygen_bits:2048",
+            "-pkeyopt", "rsa_keygen_pubexp:65537",
+            ], check=True)
+        _log_subprocess_run([
+            "openssl", "pkey", "-in", str(private_key), "-pubout",
+            "-out", str(public_key),
+            ], check=True)
+        return private_key.read_bytes(), public_key.read_bytes()
+
+    @classmethod
     def _getX509Fingerprint(cls, key_type, public_key):
         """Get the fingerprint of an X509 "public key" (i.e. certificate).
 
@@ -448,6 +471,18 @@ class Key(Storm):
         return result.imports[0].fpr
 
     @classmethod
+    def _getRSAFingerprint(cls, public_key):
+        """Get the fingerprint of an RSA public key.
+
+        :param public_key: The public key (`bytes`).
+        :return: The fingerprint (`str`).
+        """
+        output = subprocess.run([
+            "openssl", "pkey", "-pubin", "-outform", "DER",
+            ], input=public_key, stdout=subprocess.PIPE, check=True).stdout
+        return hashlib.sha1(output).hexdigest().upper()
+
+    @classmethod
     def generate(cls, key_type, description, openpgp_key_algorithm=None,
                  length=None):
         """Generate a new key for an archive.
@@ -476,6 +511,8 @@ class Key(Storm):
                             cls._generateOpenPGP(
                                 ctx, openpgp_key_algorithm, length,
                                 description))
+                elif key_type == KeyType.CV2_KERNEL:
+                    private_key, public_key = cls._generateRSA(tmp, key_type)
                 else:
                     raise KeyGenerationError.single(
                         f"Unknown key type {key_type.name}")
@@ -484,7 +521,9 @@ class Key(Storm):
             except Exception as e:
                 _log.error("Failed to generate key: %s", e)
                 raise KeyGenerationError.single(f"Failed to generate key: {e}")
-            if key_type != KeyType.OPENPGP:
+            if key_type in (
+                    KeyType.UEFI, KeyType.KMOD, KeyType.OPAL, KeyType.SIPL,
+                    KeyType.FIT):
                 try:
                     fingerprint = cls._getX509Fingerprint(
                         key_type, public_key)
@@ -492,6 +531,13 @@ class Key(Storm):
                     _log.error("Failed to get fingerprint of new key: %s", e)
                     raise KeyGenerationError.single(
                         f"Failed to get fingerprint of new key: {e}")
+            elif key_type == KeyType.CV2_KERNEL:
+                try:
+                    fingerprint = cls._getRSAFingerprint(public_key)
+                except subprocess.CalledProcessError as e:
+                    _log.error("Failed to get fingerprint of new key: %s", e)
+                    raise KeyGenerationError.single(
+                        f"Failed to get fingerprint of new key: {e}")
             _log.info("Generated new key with fingerprint %s", fingerprint)
         return cls.new(key_type, fingerprint, private_key, public_key)
 
@@ -510,16 +556,27 @@ class Key(Storm):
         :return: The injected `Key`.
         """
         _log.info("Injecting %s key for %s", key_type, description)
-        if key_type == KeyType.OPENPGP:
+        if key_type in (
+                KeyType.UEFI, KeyType.KMOD, KeyType.OPAL, KeyType.SIPL,
+                KeyType.FIT):
+            try:
+                fingerprint = cls._getX509Fingerprint(key_type, public_key)
+            except subprocess.CalledProcessError as e:
+                _log.error("Failed to get fingerprint of new key: %s", e)
+                raise KeyImportError.single(
+                    f"Failed to get fingerprint of new key: {e}")
+        elif key_type == KeyType.OPENPGP:
             with _temporary_path() as tmp, _gpg_context(Path(tmp)) as ctx:
                 fingerprint = cls._getOpenPGPFingerprint(ctx, public_key)
-        else:
+        elif key_type == KeyType.CV2_KERNEL:
             try:
-                fingerprint = cls._getX509Fingerprint(key_type, public_key)
+                fingerprint = cls._getRSAFingerprint(public_key)
             except subprocess.CalledProcessError as e:
                 _log.error("Failed to get fingerprint of new key: %s", e)
                 raise KeyImportError.single(
                     f"Failed to get fingerprint of new key: {e}")
+        else:
+            raise KeyImportError.single(f"Unknown key type {key_type.name}")
         _log.info("Injecting new key with fingerprint %s", fingerprint)
         try:
             key = Key.getByTypeAndFingerprint(key_type, fingerprint)
@@ -627,9 +684,15 @@ class Key(Storm):
                 # that we handle it in a separate method.
                 return self._signOpenPGP(tmp, message_name, message, mode)
 
-            key = tmp / f"{self.key_type.name.lower()}.key"
+            if self.key_type == KeyType.CV2_KERNEL:
+                key = tmp / f"{self.key_type.name.lower()}.priv"
+            else:
+                key = tmp / f"{self.key_type.name.lower()}.key"
             key.write_bytes(self.getPrivateKey())
-            cert = tmp / f"{self.key_type.name.lower()}.crt"
+            if self.key_type == KeyType.CV2_KERNEL:
+                cert = tmp / f"{self.key_type.name.lower()}.pub"
+            else:
+                cert = tmp / f"{self.key_type.name.lower()}.crt"
             cert.write_bytes(self.public_key)
             message_path = tmp / message_name
             message_path.write_bytes(message)
@@ -660,6 +723,12 @@ class Key(Storm):
                     cmd = [
                         "mkimage", "-F", "-k", str(tmp), "-r", str(sig_path),
                         ]
+            elif self.key_type == KeyType.CV2_KERNEL:
+                if mode == SignatureMode.DETACHED:
+                    cmd = [
+                        "openssl", "dgst", "-sha256", "-sign", str(key),
+                        "-out", str(sig_path), str(message_path),
+                        ]
             if cmd is None:
                 raise UnsupportedSignatureMode.single(
                     f"Signature mode {mode.name} not supported with "
diff --git a/lp_signing/model/tests/test_key.py b/lp_signing/model/tests/test_key.py
index 00115d7..38d5269 100644
--- a/lp_signing/model/tests/test_key.py
+++ b/lp_signing/model/tests/test_key.py
@@ -3,9 +3,13 @@
 
 """Test the database model for signing keys."""
 
-import re
-from datetime import datetime, timezone
+from datetime import (
+    datetime,
+    timezone,
+    )
+import hashlib
 from pathlib import Path
+import re
 from tempfile import TemporaryDirectory
 
 from fixtures import MockPatch
@@ -49,6 +53,7 @@ from lp_signing.tests.testfixtures import (
     FakeKmodSign,
     FakeMkimage,
     FakeOpenSSL,
+    FakeOpenSSLSign,
     FakeProcesses,
     FakeSBSign,
     )
@@ -369,6 +374,46 @@ class TestKey(TestCase):
         self.assertEqual(
             key, Key.getByTypeAndFingerprint(KeyType.OPENPGP, key.fingerprint))
 
+    def test_generate_cv2_kernel(self):
+        private_key = factory.generate_random_bytes(size=64)
+        public_key = factory.generate_random_bytes(size=64)
+        fingerprint = hashlib.sha1(public_key).hexdigest().upper()
+        fake_openssl = FakeOpenSSL(private_key, public_key, fingerprint)
+        self.processes_fixture.add(fake_openssl)
+        key = Key.generate(KeyType.CV2_KERNEL, "~signing-owner/ubuntu/testing")
+        now = get_transaction_timestamp(store)
+        self.assertThat(key, MatchesStructure.byEquality(
+            key_type=KeyType.CV2_KERNEL,
+            fingerprint=fingerprint,
+            public_key=public_key,
+            created_at=now,
+            updated_at=now))
+        self.assertEqual(private_key, key.getPrivateKey())
+        self.assertEqual(
+            key, Key.getByTypeAndFingerprint(KeyType.CV2_KERNEL, fingerprint))
+        genpkey_args = [
+            "openssl", "genpkey", "-algorithm", "RSA",
+            "-out", EndsWith("cv2_kernel.priv"),
+            "-pkeyopt", "rsa_keygen_bits:2048",
+            "-pkeyopt", "rsa_keygen_pubexp:65537",
+            ]
+        pkey_args = [
+            "openssl", "pkey", "-in", EndsWith("cv2_kernel.priv"),
+            "-pubout", "-out", EndsWith("cv2_kernel.pub"),
+            ]
+        pkey_der_args = ["openssl", "pkey", "-pubin", "-outform", "DER"]
+        self.assertThat(
+            self.processes_fixture.procs,
+            MatchesListwise([
+                RanCommand(genpkey_args, stdin=Is(None)),
+                RanCommand(pkey_args, stdin=Is(None)),
+                RanCommand(
+                    pkey_der_args,
+                    stdin=AfterPreprocessing(
+                        lambda f: f.getvalue(),
+                        Equals(public_key))),
+                ]))
+
     def test_addAuthorization(self):
         key = factory.create_key()
         clients = [factory.create_client() for _ in range(2)]
@@ -585,6 +630,34 @@ class TestKey(TestCase):
         mock_sign.assert_called_once_with(
             b"test data", mode=gpg.constants.SIG_MODE_CLEAR)
 
+    def test_sign_cv2_kernel_attached_unsupported(self):
+        key = factory.create_key(KeyType.CV2_KERNEL)
+        self.assertRaises(
+            UnsupportedSignatureMode,
+            key.sign, "t.cv2_kernel", b"test data", SignatureMode.ATTACHED)
+
+    def test_sign_cv2_kernel_detached(self):
+        key = factory.create_key(KeyType.CV2_KERNEL)
+        fake_openssl_sign = FakeOpenSSLSign(b"test signed data")
+        self.processes_fixture.add(fake_openssl_sign)
+        self.assertEqual(
+            b"test signed data",
+            key.sign("t.cv2_kernel", b"test data", SignatureMode.DETACHED))
+        self.assertEqual(b"test data", fake_openssl_sign.message_bytes)
+        openssl_sign_args = [
+            "openssl", "dgst", "-sha256", "-sign", EndsWith("cv2_kernel.priv"),
+            "-out", EndsWith("t.cv2_kernel.sig"), EndsWith("t.cv2_kernel"),
+            ]
+        self.assertThat(
+            self.processes_fixture.procs,
+            MatchesListwise([RanCommand(openssl_sign_args)]))
+
+    def test_sign_cv2_kernel_clear_unsupported(self):
+        key = factory.create_key(KeyType.CV2_KERNEL)
+        self.assertRaises(
+            UnsupportedSignatureMode,
+            key.sign, "t.cv2_kernel", b"test data", SignatureMode.CLEAR)
+
     def test_inject_uefi(self):
         private_key = factory.generate_random_bytes(size=64)
         public_key = factory.generate_random_bytes(size=64)
@@ -760,3 +833,29 @@ class TestKey(TestCase):
         self.assertEqual(private_key, key.getPrivateKey())
         self.assertEqual(
             key, Key.getByTypeAndFingerprint(KeyType.OPENPGP, fingerprint))
+
+    def test_inject_cv2_kernel(self):
+        private_key = factory.generate_random_bytes(size=64)
+        public_key = factory.generate_random_bytes(size=64)
+        fingerprint = hashlib.sha1(public_key).hexdigest().upper()
+        fake_openssl = FakeOpenSSL(private_key, public_key, fingerprint)
+        self.processes_fixture.add(fake_openssl)
+        description = "PPA signing-owner testing"
+        created_at = datetime.utcnow().replace(tzinfo=timezone.utc)
+        key = Key.inject(
+            KeyType.CV2_KERNEL, private_key, public_key, description,
+            created_at)
+        now = get_transaction_timestamp(store)
+        self.assertThat(key, MatchesStructure.byEquality(
+            key_type=KeyType.CV2_KERNEL,
+            fingerprint=fingerprint,
+            public_key=public_key,
+            created_at=created_at,
+            updated_at=now))
+        self.assertEqual(private_key, key.getPrivateKey())
+        self.assertEqual(
+            key, Key.getByTypeAndFingerprint(KeyType.CV2_KERNEL, fingerprint))
+        pkey_der_args = ["openssl", "pkey", "-pubin", "-outform", "DER"]
+        self.assertThat(
+            self.processes_fixture.procs,
+            MatchesListwise([RanCommand(pkey_der_args)]))
diff --git a/lp_signing/tests/test_webapi.py b/lp_signing/tests/test_webapi.py
index c4ad4ac..3cb32bb 100644
--- a/lp_signing/tests/test_webapi.py
+++ b/lp_signing/tests/test_webapi.py
@@ -9,6 +9,8 @@ import pytz
 from tempfile import TemporaryDirectory
 
 from cryptography.hazmat.backends import default_backend
+from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
+from cryptography.hazmat.primitives.serialization import load_pem_public_key
 from cryptography.x509 import (
     load_der_x509_certificate,
     load_pem_x509_certificate,
@@ -30,7 +32,9 @@ from testtools.matchers import (
     AnyMatch,
     Equals,
     HasLength,
+    IsInstance,
     Matcher,
+    MatchesAll,
     MatchesDict,
     MatchesListwise,
     MatchesRegex,
@@ -63,6 +67,7 @@ from lp_signing.tests.testfixtures import (
     FakeKmodSign,
     FakeMkimage,
     FakeOpenSSL,
+    FakeOpenSSLSign,
     FakeProcesses,
     FakeSBSign,
     )
@@ -657,6 +662,64 @@ class TestGenerateView(TestCase):
         self.assertThat(resp, HasAPIError(MatchesRegex(error_re), 500))
         self.assertNonceConsumed()
 
+    def test_generate_cv2_kernel(self):
+        # Integration test: generate and return a real CV2 kernel key.
+        resp = self.post_generate({
+            "key-type": "CV2_KERNEL",
+            "description": "PPA test-owner test-archive",
+            })
+        self.assertThat(resp, IsJSONResponse(
+            MatchesDict({
+                "fingerprint": HasLength(40),
+                "public-key": AfterPreprocessing(
+                    lambda data: load_pem_public_key(
+                        base64.b64decode(data.encode("UTF-8")),
+                        default_backend()),
+                    MatchesAll(
+                        IsInstance(RSAPublicKey),
+                        MatchesStructure(key_size=Equals(2048)))),
+                }),
+            expected_status=201))
+        self.assertNonceConsumed()
+        # The new key was committed to the database.
+        key = Key.getByTypeAndFingerprint(
+            KeyType.CV2_KERNEL, resp.json["fingerprint"])
+        self.assertThat(key, MatchesStructure.byEquality(
+            fingerprint=resp.json["fingerprint"],
+            public_key=base64.b64decode(
+                resp.json["public-key"].encode("UTF-8")),
+            authorizations=[self.client]))
+
+    def test_generate_cv2_kernel_genpkey_error(self):
+        processes_fixture = self.useFixture(FakeProcesses())
+        processes_fixture.add(lambda _: {"returncode": 1}, name="openssl")
+        resp = self.post_generate({
+            "key-type": "CV2_KERNEL",
+            "description": "PPA test-owner test-archive",
+            })
+        error_re = (
+            r"Failed to generate key: "
+            r"Command .*'openssl', 'genpkey'.* returned non-zero exit status "
+            r"1")
+        self.assertThat(resp, HasAPIError(MatchesRegex(error_re), 500))
+        self.assertNonceConsumed()
+
+    def test_generate_cv2_kernel_fingerprint_error(self):
+        processes_fixture = self.useFixture(FakeProcesses())
+        private_key = factory.generate_random_bytes(size=64)
+        public_key = factory.generate_random_bytes(size=64)
+        fake_openssl = FakeOpenSSL(private_key, public_key, None)
+        processes_fixture.add(fake_openssl)
+        resp = self.post_generate({
+            "key-type": "CV2_KERNEL",
+            "description": "PPA test-owner test-archive",
+            })
+        error_re = (
+            r"Failed to get fingerprint of new key: "
+            r"Command .*'-outform', 'DER'.* returned non-zero exit status 1")
+        self.assertThat(resp, HasAPIError(MatchesRegex(error_re), 500))
+        self.assertNonceConsumed()
+
 
 class TestSignView(TestCase):
 
@@ -758,7 +821,7 @@ class TestSignView(TestCase):
             resp,
             HasAPIError(
                 "'nonsense' is not one of ['UEFI', 'KMOD', 'OPAL', 'SIPL', "
-                "'FIT', 'OPENPGP']"))
+                "'FIT', 'OPENPGP', 'CV2_KERNEL']"))
         self.assertNonceConsumed()
 
     def test_missing_fingerprint(self):
@@ -1355,6 +1418,85 @@ class TestSignView(TestCase):
         self.assertThat(resp, HasAPIError(MatchesRegex(error_re), 500))
         self.assertNonceConsumed()
 
+    def test_sign_cv2_kernel_attached_unsupported(self):
+        key = factory.create_key(key_type=KeyType.CV2_KERNEL)
+        key.addAuthorization(self.client)
+        store.commit()
+        resp = self.post_sign(
+            {
+                "key-type": "CV2_KERNEL",
+                "fingerprint": key.fingerprint,
+                "message-name": "t.cv2_kernel",
+                "message": base64.b64encode(b"test data").decode("UTF-8"),
+                "mode": "ATTACHED",
+                })
+        self.assertThat(
+            resp,
+            HasAPIError(
+                "Signature mode ATTACHED not supported with CV2_KERNEL"))
+
+    def test_sign_cv2_kernel_detached(self):
+        key = factory.create_key(key_type=KeyType.CV2_KERNEL)
+        key.addAuthorization(self.client)
+        store.commit()
+        fake_openssl_sign = FakeOpenSSLSign(b"test signed data")
+        processes_fixture = self.useFixture(FakeProcesses())
+        processes_fixture.add(fake_openssl_sign)
+        resp = self.post_sign(
+            {
+                "key-type": "CV2_KERNEL",
+                "fingerprint": key.fingerprint,
+                "message-name": "t.cv2_kernel",
+                "message": base64.b64encode(b"test data").decode("UTF-8"),
+                "mode": "DETACHED",
+                })
+        self.assertThat(resp, IsJSONResponse(
+            MatchesDict({
+                "public-key": AfterPreprocessing(
+                    lambda data: base64.b64decode(data.encode("UTF-8")),
+                    Equals(key.public_key)),
+                "signed-message": AfterPreprocessing(
+                    lambda data: base64.b64decode(data.encode("UTF-8")),
+                    Equals(b"test signed data")),
+                })))
+        self.assertNonceConsumed()
+
+    def test_sign_cv2_kernel_detached_openssl_sign_error(self):
+        key = factory.create_key(key_type=KeyType.CV2_KERNEL)
+        key.addAuthorization(self.client)
+        store.commit()
+        processes_fixture = self.useFixture(FakeProcesses())
+        processes_fixture.add(lambda _: {"returncode": 1}, name="openssl")
+        resp = self.post_sign(
+            {
+                "key-type": "CV2_KERNEL",
+                "fingerprint": key.fingerprint,
+                "message-name": "t.cv2_kernel",
+                "message": base64.b64encode(b"test data").decode("UTF-8"),
+                "mode": "DETACHED",
+                })
+        error_re = (
+            r"Failed to sign message: "
+            r"Command .*'openssl', 'dgst'.* returned non-zero exit status 1")
+        self.assertThat(resp, HasAPIError(MatchesRegex(error_re), 500))
+        self.assertNonceConsumed()
+
+    def test_sign_cv2_kernel_clear_unsupported(self):
+        key = factory.create_key(key_type=KeyType.CV2_KERNEL)
+        key.addAuthorization(self.client)
+        store.commit()
+        resp = self.post_sign(
+            {
+                "key-type": "CV2_KERNEL",
+                "fingerprint": key.fingerprint,
+                "message-name": "t.cv2_kernel",
+                "message": base64.b64encode(b"test data").decode("UTF-8"),
+                "mode": "CLEAR",
+                })
+        self.assertThat(
+            resp,
+            HasAPIError("Signature mode CLEAR not supported with CV2_KERNEL"))
+
 
 class TestInjectView(TestCase):
 
@@ -1501,7 +1643,7 @@ class TestInjectView(TestCase):
             resp,
             HasAPIError(
                 "'nonsense' is not one of ['UEFI', 'KMOD', 'OPAL', 'SIPL', "
-                "'FIT', 'OPENPGP']"))
+                "'FIT', 'OPENPGP', 'CV2_KERNEL']"))
         self.assertNonceConsumed()
 
     def test_inject_uefi(self):
@@ -1814,6 +1956,50 @@ class TestInjectView(TestCase):
         self.assertThat(resp, HasAPIError(MatchesRegex(error_re), 500))
         self.assertNonceConsumed()
 
+    def test_inject_cv2_kernel(self):
+        # Integration test: inject a real CV2 kernel key and return its
+        # fingerprint.
+        with TemporaryDirectory() as tmp:
+            private_key, public_key = Key._generateRSA(
+                Path(tmp), KeyType.CV2_KERNEL)
+
+        resp = self.post_inject({
+            "key-type": "CV2_KERNEL",
+            "private-key": base64.b64encode(private_key).decode("UTF-8"),
+            "public-key": base64.b64encode(public_key).decode("UTF-8"),
+            "created-at": datetime.utcnow().isoformat(),
+            "description": "PPA test-owner test-archive",
+            })
+        self.assertThat(resp, IsJSONResponse(
+            MatchesDict({"fingerprint": HasLength(40)}),
+            expected_status=200))
+        self.assertNonceConsumed()
+        # The new key was committed to the database.
+        key = Key.getByTypeAndFingerprint(
+            KeyType.CV2_KERNEL, resp.json["fingerprint"])
+        self.assertThat(key, MatchesStructure.byEquality(
+            fingerprint=resp.json["fingerprint"],
+            authorizations=[self.clients[0]]))
+
+    def test_inject_cv2_kernel_fingerprint_error(self):
+        processes_fixture = self.useFixture(FakeProcesses())
+        private_key = factory.generate_random_bytes(size=64)
+        public_key = factory.generate_random_bytes(size=64)
+        fake_openssl = FakeOpenSSL(private_key, public_key, None)
+        processes_fixture.add(fake_openssl)
+        resp = self.post_inject({
+            "key-type": "CV2_KERNEL",
+            "private-key": "",
+            "public-key": "",
+            "created-at": datetime.utcnow().isoformat(),
+            "description": "PPA test-owner test-archive",
+            })
+        error_re = (
+            r"Failed to get fingerprint of new key: "
+            r"Command .*'-outform', 'DER'.* returned non-zero exit status 1")
+        self.assertThat(resp, HasAPIError(MatchesRegex(error_re), 500))
+        self.assertNonceConsumed()
+
     def test_inject_duplicate_key_different_clients(self):
         common_name = Key._generateKeyCommonName(
             "PPA test-owner test-archive", 'FIT')
@@ -2127,7 +2313,7 @@ class TestAddAuthorizationView(TestCase):
             resp,
             HasAPIError(
                 "'nonsense' is not one of ['UEFI', 'KMOD', 'OPAL', 'SIPL', "
-                "'FIT', 'OPENPGP']"))
+                "'FIT', 'OPENPGP', 'CV2_KERNEL']"))
         self.assertNonceConsumed()
 
     def test_unknown_key(self):
diff --git a/lp_signing/tests/testfixtures.py b/lp_signing/tests/testfixtures.py
index ccaec93..c012c42 100644
--- a/lp_signing/tests/testfixtures.py
+++ b/lp_signing/tests/testfixtures.py
@@ -3,6 +3,7 @@
 
 """Test fixtures."""
 
+import hashlib
 import io
 import json
 from pathlib import Path
@@ -207,6 +208,31 @@ class FakeOpenSSL:
                     info["stdout"] = io.BytesIO(output.encode("UTF-8"))
                 else:
                     info["returncode"] = 1
+        elif args[1] == "genpkey":
+            if "-out" in args:
+                private_key_path = args[args.index("-out") + 1]
+                Path(private_key_path).write_bytes(self.private_key)
+        elif args[1] == "pkey":
+            if "-out" in args:
+                public_key_path = args[args.index("-out") + 1]
+                Path(public_key_path).write_bytes(self.public_key)
+            if ("-outform" in args and
+                    args[args.index("-outform") + 1] == "DER"):
+                if self.fingerprint is not None:
+                    # For plain RSA keys, Key._getRSAFingerprint computes
+                    # the fingerprint in Python based on the DER encoding of
+                    # the public key.  Check that this matches
+                    # self.fingerprint to avoid confusion.
+                    public_key_fingerprint = hashlib.sha1(
+                        self.public_key).hexdigest().upper()
+                    if public_key_fingerprint != self.fingerprint:
+                        raise AssertionError(
+                            f"Fingerprint of self.public_key does not match "
+                            f"self.fingerprint ({public_key_fingerprint} != "
+                            f"{self.fingerprint}")
+                    info["stdout"] = io.BytesIO(self.public_key)
+                else:
+                    info["returncode"] = 1
         return info
 
 
@@ -256,3 +282,21 @@ class FakeMkimage:
         # mkimage signs in place.
         Path(message_path).write_bytes(self.sig_data)
         return {}
+
+
+class FakeOpenSSLSign:
+
+    name = "openssl"
+
+    def __init__(self, sig_data):
+        self.sig_data = sig_data
+        self.message_bytes = None
+
+    def __call__(self, proc_args):
+        args = proc_args["args"]
+        if args[1] == "dgst":
+            message_path = args[-1]
+            self.message_bytes = Path(message_path).read_bytes()
+            sig_path = args[args.index("-out") + 1]
+            Path(sig_path).write_bytes(self.sig_data)
+            return {}