← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~ilasc/lp-signing:add-android-kernel into lp-signing:master

 

Ioana Lasc has proposed merging ~ilasc/lp-signing:add-android-kernel into lp-signing:master.

Commit message:
Sign Android kernel boot images

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~ilasc/lp-signing/+git/lp-signing/+merge/404686
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~ilasc/lp-signing:add-android-kernel into lp-signing:master.
diff --git a/lp_signing/enums.py b/lp_signing/enums.py
index e76b7cf..43828c0 100644
--- a/lp_signing/enums.py
+++ b/lp_signing/enums.py
@@ -59,6 +59,12 @@ class KeyType(DBEnumeratedType):
         An Ambarella CV2 kernel signing key.
         """)
 
+    ANDROID_KERNEL = DBItem(8, """
+        Android Kernel
+
+        An Android kernel signing key.
+        """)
+
 
 class OpenPGPKeyAlgorithm(EnumeratedType):
 
diff --git a/lp_signing/model/key.py b/lp_signing/model/key.py
index 1dc70f8..7ba629d 100644
--- a/lp_signing/model/key.py
+++ b/lp_signing/model/key.py
@@ -350,6 +350,26 @@ class Key(Storm):
         return key.read_bytes(), cert.read_bytes()
 
     @classmethod
+    def _generateAndroidKernelKeys(cls, tmp, key_type, common_name):
+        """Generate a new key/certificate pair.
+
+        :param tmp: A `Path` to a temporary directory.
+        :param key_type: The `KeyType` to generate.
+        :param common_name: The common name for the new key.
+        :return: A tuple of (private key, public key).
+        """
+        cls._generateKeyCertPair(tmp, key_type, common_name)
+        key_file_name = tmp / f"{key_type.name.lower()}.key"
+        cert_file_name = tmp / f"{key_type.name.lower()}.crt"
+        # Convert private key from PKCS#8 to PKCS#1 format
+        PKCS1_key = tmp / f"{key_type.name.lower()}.key"
+        _log_subprocess_run([
+            "openssl", "rsa", "-in", str(key_file_name),
+            "-out", str(PKCS1_key),
+            ], check=True)
+        return PKCS1_key.read_bytes(), cert_file_name.read_bytes()
+
+    @classmethod
     def _generatePEMX509(cls, tmp, key_type, common_name):
         """Generate a new PEM/X509 key pair.
 
@@ -449,7 +469,8 @@ class Key(Storm):
         output = subprocess.run([
             "openssl", "x509",
             "-inform",
-            "PEM" if key_type in (KeyType.UEFI, KeyType.FIT) else "DER",
+            "PEM" if key_type in (
+                KeyType.UEFI, KeyType.FIT, KeyType.ANDROID_KERNEL) else "DER",
             "-noout", "-fingerprint",
             ], input=public_key, stdout=subprocess.PIPE, check=True).stdout
         return (
@@ -513,6 +534,9 @@ class Key(Storm):
                                 description))
                 elif key_type == KeyType.CV2_KERNEL:
                     private_key, public_key = cls._generateRSA(tmp, key_type)
+                elif key_type == KeyType.ANDROID_KERNEL:
+                    private_key, public_key = cls._generateAndroidKernelKeys(
+                        tmp, key_type, common_name)
                 else:
                     raise KeyGenerationError.single(
                         f"Unknown key type {key_type.name}")
@@ -523,7 +547,7 @@ class Key(Storm):
                 raise KeyGenerationError.single(f"Failed to generate key: {e}")
             if key_type in (
                     KeyType.UEFI, KeyType.KMOD, KeyType.OPAL, KeyType.SIPL,
-                    KeyType.FIT):
+                    KeyType.FIT, KeyType.ANDROID_KERNEL):
                 try:
                     fingerprint = cls._getX509Fingerprint(
                         key_type, public_key)
@@ -558,7 +582,7 @@ class Key(Storm):
         _log.info("Injecting %s key for %s", key_type, description)
         if key_type in (
                 KeyType.UEFI, KeyType.KMOD, KeyType.OPAL, KeyType.SIPL,
-                KeyType.FIT):
+                KeyType.FIT, KeyType.ANDROID_KERNEL):
             try:
                 fingerprint = cls._getX509Fingerprint(key_type, public_key)
             except subprocess.CalledProcessError as e:
@@ -723,7 +747,7 @@ class Key(Storm):
                     cmd = [
                         "mkimage", "-F", "-k", str(tmp), "-r", str(sig_path),
                         ]
-            elif self.key_type == KeyType.CV2_KERNEL:
+            elif self.key_type in (KeyType.CV2_KERNEL, KeyType.ANDROID_KERNEL):
                 if mode == SignatureMode.DETACHED:
                     cmd = [
                         "openssl", "dgst", "-sha256", "-sign", str(key),
diff --git a/lp_signing/model/tests/test_key.py b/lp_signing/model/tests/test_key.py
index 38d5269..7ce5ceb 100644
--- a/lp_signing/model/tests/test_key.py
+++ b/lp_signing/model/tests/test_key.py
@@ -414,6 +414,54 @@ class TestKey(TestCase):
                         Equals(public_key))),
                 ]))
 
+    def test_generate_android_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.ANDROID_KERNEL,
+            "~signing-owner/ubuntu/testing")
+        now = get_transaction_timestamp(store)
+        self.assertThat(key, MatchesStructure.byEquality(
+            key_type=KeyType.ANDROID_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.ANDROID_KERNEL, fingerprint))
+
+        genpkey_args = [
+            "openssl", "req", "-new", "-x509", "-newkey", "rsa:2048",
+            "-subj", r"/CN=~signing-owner\/ubuntu\/testing Android Kernel/",
+            "-keyout", EndsWith("android_kernel.key"),
+            "-out", EndsWith("android_kernel.crt"), "-days", "10956",
+            "-nodes", "-sha256",
+            ]
+
+        pkey_args = [
+            "openssl", "rsa", "-in", EndsWith("android_kernel.key"),
+            "-out", EndsWith("android_kernel.key"),
+            ]
+        pkey_der_args = ["openssl", "x509", "-inform", "PEM",
+                         "-noout", "-fingerprint"]
+
+        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)]
@@ -658,6 +706,36 @@ class TestKey(TestCase):
             UnsupportedSignatureMode,
             key.sign, "t.cv2_kernel", b"test data", SignatureMode.CLEAR)
 
+    def test_sign_android_kernel_attached_unsupported(self):
+        key = factory.create_key(KeyType.ANDROID_KERNEL)
+        self.assertRaises(
+            UnsupportedSignatureMode,
+            key.sign, "t.android_kernel", b"test data", SignatureMode.ATTACHED)
+
+    def test_sign_android_kernel_detached(self):
+        key = factory.create_key(KeyType.ANDROID_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.android_kernel", b"test data", SignatureMode.DETACHED))
+        self.assertEqual(b"test data", fake_openssl_sign.message_bytes)
+        openssl_sign_args = [
+            "openssl", "dgst", "-sha256", "-sign",
+            EndsWith("android_kernel.key"),
+            "-out", EndsWith("t.android_kernel.sig"),
+            EndsWith("t.android_kernel"),
+            ]
+        self.assertThat(
+            self.processes_fixture.procs,
+            MatchesListwise([RanCommand(openssl_sign_args)]))
+
+    def test_sign_android_kernel_clear_unsupported(self):
+        key = factory.create_key(KeyType.ANDROID_KERNEL)
+        self.assertRaises(
+            UnsupportedSignatureMode,
+            key.sign, "t.android_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)
@@ -859,3 +937,31 @@ class TestKey(TestCase):
         self.assertThat(
             self.processes_fixture.procs,
             MatchesListwise([RanCommand(pkey_der_args)]))
+
+    def test_inject_android_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.ANDROID_KERNEL, private_key, public_key, description,
+            created_at)
+        now = get_transaction_timestamp(store)
+        self.assertThat(key, MatchesStructure.byEquality(
+            key_type=KeyType.ANDROID_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.ANDROID_KERNEL, fingerprint))
+        pkey_der_args = ["openssl", "x509", "-inform",
+                         "PEM", "-noout", "-fingerprint"]
+        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 3cb32bb..c53decd 100644
--- a/lp_signing/tests/test_webapi.py
+++ b/lp_signing/tests/test_webapi.py
@@ -720,6 +720,84 @@ class TestGenerateView(TestCase):
         self.assertThat(resp, HasAPIError(MatchesRegex(error_re), 500))
         self.assertNonceConsumed()
 
+    def test_generate_android_kernel(self):
+        # Integration test: generate and return a real Android kernel key.
+        resp = self.post_generate(
+            {
+                "key-type": "ANDROID_KERNEL",
+                "description": "PPA test-owner test-archive",
+                })
+
+        self.assertThat(resp, IsJSONResponse(
+            MatchesDict({
+                "fingerprint": HasLength(40),
+                "public-key": AfterPreprocessing(
+                    lambda data: load_pem_x509_certificate(
+                        base64.b64decode(data.encode("UTF-8")),
+                        default_backend()),
+                    MatchesStructure(
+                        subject=AfterPreprocessing(
+                            lambda subject: subject.rfc4514_string(),
+                            Equals("CN=PPA test-owner test-archive "
+                                   "Android Kernel")))),
+                }),
+            expected_status=201))
+
+        self.assertNonceConsumed()
+        # The new key was committed to the database.
+        key = Key.getByTypeAndFingerprint(
+            KeyType.ANDROID_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_android_kernel_req_error(self):
+        processes_fixture = self.useFixture(FakeProcesses())
+        processes_fixture.add(lambda _: {"returncode": 1}, name="openssl")
+        resp = self.post_generate(
+            {
+                "key-type": "ANDROID_KERNEL",
+                "description": "PPA test-owner test-archive",
+                })
+        error_re = (
+            r"Failed to generate key: "
+            r"Command .*'openssl', 'req'.* returned non-zero exit status 1")
+        self.assertThat(resp, HasAPIError(MatchesRegex(error_re), 500))
+        self.assertNonceConsumed()
+
+    def test_generate_android_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": "ANDROID_KERNEL",
+                "description": "PPA test-owner test-archive",
+                })
+        error_re = (
+            r"Failed to get fingerprint of new key: "
+            r"Command .*'-fingerprint'.* returned non-zero exit status 1")
+        self.assertThat(resp, HasAPIError(MatchesRegex(error_re), 500))
+        self.assertNonceConsumed()
+
+    def test_generate_android_kernel_genpkey_error(self):
+        processes_fixture = self.useFixture(FakeProcesses())
+        processes_fixture.add(lambda _: {"returncode": 1}, name="openssl")
+        resp = self.post_generate({
+            "key-type": "ANDROID_KERNEL",
+            "description": "PPA test-owner test-archive",
+            })
+        error_re = (
+            r"Failed to generate key: "
+            r"Command .*'req', '-new'.* returned non-zero exit status "
+            r"1")
+        self.assertThat(resp, HasAPIError(MatchesRegex(error_re), 500))
+        self.assertNonceConsumed()
+
 
 class TestSignView(TestCase):
 
@@ -821,7 +899,7 @@ class TestSignView(TestCase):
             resp,
             HasAPIError(
                 "'nonsense' is not one of ['UEFI', 'KMOD', 'OPAL', 'SIPL', "
-                "'FIT', 'OPENPGP', 'CV2_KERNEL']"))
+                "'FIT', 'OPENPGP', 'CV2_KERNEL', 'ANDROID_KERNEL']"))
         self.assertNonceConsumed()
 
     def test_missing_fingerprint(self):
@@ -1643,7 +1721,7 @@ class TestInjectView(TestCase):
             resp,
             HasAPIError(
                 "'nonsense' is not one of ['UEFI', 'KMOD', 'OPAL', 'SIPL', "
-                "'FIT', 'OPENPGP', 'CV2_KERNEL']"))
+                "'FIT', 'OPENPGP', 'CV2_KERNEL', 'ANDROID_KERNEL']"))
         self.assertNonceConsumed()
 
     def test_inject_uefi(self):
@@ -2313,7 +2391,7 @@ class TestAddAuthorizationView(TestCase):
             resp,
             HasAPIError(
                 "'nonsense' is not one of ['UEFI', 'KMOD', 'OPAL', 'SIPL', "
-                "'FIT', 'OPENPGP', 'CV2_KERNEL']"))
+                "'FIT', 'OPENPGP', 'CV2_KERNEL', 'ANDROID_KERNEL']"))
         self.assertNonceConsumed()
 
     def test_unknown_key(self):