Repository: nextcloud/appstore

Issue:
API: registering an app id and uploading an app release needs to check revoked certificates
In order to prevent old or lost certificates from being abused we need to check if the certificate has been revoked. This has to be done before validating the certificate on app release upload and before registering a new app id.



Discussion:
@LukasReschke need the following:
- Empty original Nextcloud CRL file in PEM format: https://github.com/nextcloud/appstore/blob/master/nextcloudappstore/core/certificate/nextcloud.crl

Then generate a new CA (e.g. imaginary) and place the following files here:
- The chain file: https://github.com/nextcloud/appstore/tree/master/nextcloudappstore/core/certificate/tests/data/certificates/imaginary.chain
- A valid and signed cert here: https://github.com/nextcloud/appstore/tree/master/nextcloudappstore/core/certificate/tests/data/certificates/imaginary-valid.crt
- Two valid and but revoked certs here: https://github.com/nextcloud/appstore/tree/master/nextcloudappstore/core/certificate/tests/data/certificates/imaginary-revoked1.crt, https://github.com/nextcloud/appstore/tree/master/nextcloudappstore/core/certificate/tests/data/certificates/imaginary-revoked2.crt
- The CRL file with the two revoked certs here: https://github.com/nextcloud/appstore/tree/master/nextcloudappstore/core/certificate/tests/data/certificates/imaginary.crl


Resolving patch:
diff --git a/nextcloudappstore/core/certificate/validator.py b/nextcloudappstore/core/certificate/validator.py
--- a/nextcloudappstore/core/certificate/validator.py
+++ b/nextcloudappstore/core/certificate/validator.py
@@ -3,7 +3,7 @@
 
 import pem
 from OpenSSL.crypto import FILETYPE_PEM, load_certificate, verify, X509, \
-    X509Store, X509StoreContext, load_crl
+    X509Store, X509StoreContext, load_crl, X509StoreFlags
 from django.conf import settings  # type: ignore
 from rest_framework.exceptions import APIException
 
@@ -74,12 +74,14 @@ def validate_certificate(self, certificate: str, chain: str,
             store.add_cert(self._to_cert(str(ca)))
 
         cert = self._to_cert(certificate)
-        ctx = X509StoreContext(store, cert)
-        err_msg = 'Certificate is invalid'
 
         if crl:
-            crl = load_crl(FILETYPE_PEM, crl)
-            store.add_crl(crl)
+            parsed_crl = load_crl(FILETYPE_PEM, crl)
+            store.set_flags(X509StoreFlags.CRL_CHECK)
+            store.add_crl(parsed_crl)
+
+        ctx = X509StoreContext(store, cert)
+        err_msg = 'Certificate is invalid'
 
         try:
             result = ctx.verify_certificate()


Test patch:
diff --git a/nextcloudappstore/core/certificate/tests/data/certificates/empty.crl b/nextcloudappstore/core/certificate/tests/data/certificates/empty.crl
new file mode 100644
--- /dev/null
+++ b/nextcloudappstore/core/certificate/tests/data/certificates/empty.crl
@@ -0,0 +1,9 @@
+-----BEGIN X509 CRL-----
+MIIBHTCBhwIBATANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJBVTETMBEGA1UE
+CAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk
+Fw0xNjA5MTMxODAwMjBaFw0xNjEwMTMxODAwMjBaoA4wDDAKBgNVHRQEAwIBATAN
+BgkqhkiG9w0BAQsFAAOBgQACfHsfDH755/rOK7xFGHVtQmlVe/qPa2dxuUDzJtOd
+uFA+TdYmTiFJKwGTHBADHXG4gd+P1gqle/ZsOTz/Wh8uvPX+whwJIit7JY5eFefl
+bDp6FIa4dfWBM+jRHUSx/OmVgEJXFkmPAztk7k8KOWS1x3j3MCjEX7S3lO1HREoR
+Ew==
+-----END X509 CRL-----
diff --git a/nextcloudappstore/core/certificate/tests/data/certificates/imaginary-revoked1.crt b/nextcloudappstore/core/certificate/tests/data/certificates/imaginary-revoked1.crt
new file mode 100644
--- /dev/null
+++ b/nextcloudappstore/core/certificate/tests/data/certificates/imaginary-revoked1.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDUTCCAroCCQDTwwNPEHn/NjANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB
+VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0
+cyBQdHkgTHRkMCAXDTE2MDkxMzE3NTA0OVoYDzIyOTAwNjI4MTc1MDQ5WjAPMQ0w
+CwYDVQQDDARhcHAyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAshyK
+Lv+L3XkKvy+gO1LKErNHhaS7IAb0mtyL/OgJbzfNXRsBEnb1MFuhbRgdxQnkdMyM
+K8YpCT8V508dRDLMYKMBBWTTwrc80FtpHRzSFxLm4tQCEkncKyzLlRw23KFZ+v/7
+BGULXI11kS6n+B3vmlc70W9OTw9J5LTB1w8BsIsFfTYDHl5b0bn18ybRGvryAwbM
+xmOeG5EhOI5CG2G2EF77fZ/ztVm80bPnqQjK1tPrADQJbTcQeyWYFV+mxsme4dz9
+tZadLk67HDwigNzXbFf8YzQemFdmX5TE/cXxhKBjBV8YuHdsuQdBzDWau/N1glL6
+WSvb+HiVFBKfgq06lyRC0oW5nUpA9MZ6lhK8jAHDosAasS46nUbc1qRq9VCz0jg9
+XSYA4PKlbkPjKaLoikACD3tKkuJVKrQ9OGBqVwBvDG1mluDl0qKXZHRKyGSJqLH3
+ACa/YGQRT33IJGL10YxDb4p3YuBIQfCWZKEbc/o+u2b71SD6p2x7e8q2BBicCz5m
+GkpKk4gzIC+zi/1W/O/8aCZq/Qrwlf56gdXbnxnGOAM8Irl8r8nWq2w+31gTfUqq
+l0xZ8cmLrX6ZTDOy8bscRq1q8wVn/Eor1o/bCpSMMFsw5aqt7lMAjWSDIMIPbk0W
+GHhpnXM/nGZfYtQo6RBxe6JwoYKeCQUWVOApl5MCAwEAATANBgkqhkiG9w0BAQsF
+AAOBgQB/jUTFAUNg/lYUolYJTCL4OSs7vmYP9sERJ5aC3OIzgaTl2oQCW1efUEja
+Fv79eO1R3bdRVnbzyrv7IVXneR4HIrZtDnzDfgA8rTUQK+zS9FJDa4ZcF++OKLIE
+rIASvATqjOJHAiwxJE/SBNlBIW25v46rzbg8oSqeNmuUJfVYlQ==
+-----END CERTIFICATE-----
diff --git a/nextcloudappstore/core/certificate/tests/data/certificates/imaginary-revoked2.crt b/nextcloudappstore/core/certificate/tests/data/certificates/imaginary-revoked2.crt
new file mode 100644
--- /dev/null
+++ b/nextcloudappstore/core/certificate/tests/data/certificates/imaginary-revoked2.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDUTCCAroCCQDTwwNPEHn/NzANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB
+VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0
+cyBQdHkgTHRkMCAXDTE2MDkxMzE3NTA1OVoYDzIyOTAwNjI4MTc1MDU5WjAPMQ0w
+CwYDVQQDDARhcHAzMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1Yfq
+Ez3JdW1WfZfLITAptY/zKNryFFj4mkQ9sV9Me5J91RH8Fm9GjnO6lH1JVPEMMQyX
+yong9WDXITrPnneoTv7FS3GM+FiKiiLmNrGKUyhZDuGIOWFGRDRxGYGDqUXU+czR
+YrKS/nVzwavXMhldO9XuZkJwI3IHv5IgYND0HzXOs6kUA7ryhtdHfaA+wGcffCoh
+s0HaauIOFBA+NraO+aEGK3167ppPGRvZuCIly3oh5y5X/9th2kuv58BIdepprgQD
+FHffRi7kFfDkO7MN+M0SI+i1ceIElJSncmhoT0/Eepf9eZfTdR8MJnB9DsIhGBbg
+TueSJvgCRDrpTDqbkoTi1lkgQCBzcdF90IDmKb87JU2RgP+f5qsPRyXGQ1SGCkit
+obXJzN1rkSo1CtT5D4ifZjBiPQAbly6zoDzbjWNp+YXQYsQxVQMZH9TeV6brn1s3
+W+SdErIgylbohIKleg7kF78Xt1ZQpFaAXAUVh4ncP9aGLs6sMGCd7o1TZF++xlaa
+S759o6VOnlyw7+sj2JTIt+VnFS2i3stVQOceG3GRJBBsZlXc6UP2/nIe+E1td7wm
+fK23nKzQFFx5wM+Z6wHVGzSEPHwQPVvGPdQscI4FZ7S/II2jc6OAvzjaNp2ZgpHB
+GW7qub4673BAO8ghkT5pGOKLZfMynFZW51/5QSsCAwEAATANBgkqhkiG9w0BAQsF
+AAOBgQApjiz2vquDJuKQ4KlXYnvn4AK5QaUifUrtCLBE/cAIzA8hBHfsuaayxRUE
+LOgYLLqL1k/dRZcjfoHxfjr8qswfsWUWhhuq7CDdtcFMNImrTDtjh3yHe9Li7YTT
+UC1OLXbcx/KBKrRUPohgdlKEiRW1ayMuM2S++ffKLURCoQFIJA==
+-----END CERTIFICATE-----
diff --git a/nextcloudappstore/core/certificate/tests/data/certificates/imaginary-valid.crt b/nextcloudappstore/core/certificate/tests/data/certificates/imaginary-valid.crt
new file mode 100644
--- /dev/null
+++ b/nextcloudappstore/core/certificate/tests/data/certificates/imaginary-valid.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDUTCCAroCCQDTwwNPEHn/NTANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJB
+VTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0
+cyBQdHkgTHRkMCAXDTE2MDkxMzE3NTA0MFoYDzIyOTAwNjI4MTc1MDQwWjAPMQ0w
+CwYDVQQDDARhcHAxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAoShW
+2P9e6KY4MlXwNCRArtCmI7iv6sVLoAqtofwPvhwZ2tBvCnKYW2FBFcpvkKE4qsx6
+17dCLhaEXvZilK8L5Wa+S1shulenLRyKeQq4irtOhg/PUobAeF7e+3BpSQZQeuGN
+JkET3I66qGWPw41yx5mMUUDI0M82zq9dfQ9IC6RQ0W/x2daDdXFuIQX2YqeppWlD
+JSGSdAUkk3/Se+wUlt+8gq373FzTd+Te1UQfqOsuNXetTVhPDo5TMgrKK+w0ze9v
+7L6f/dvXJYza5jwiK+lkCH1HYIooz1GSfhuIAU8kd3oG0x5Ahz08aEUAGhyA1yNN
+Vfv26zK2cuV6pQukLAeGHwXt8oksFp5ydkHlLAyj0Pt70cA87o5pOuS53ZEm380S
+/W3F9laXGRUrmCDEhsmm0OgKxRsBdog3LL9Azk2bFcHPbEZKN3fGevgWSDQsXpxu
+kPVmPENhZzAgHviEkm9QnwhejpVBSqcPdq2WCIk+AVVSRhF6o+hhw4pCgni96Rmr
+2Wloubu0y3sxVtUPte4/rDca5i/kXLthMS1yTY3CyHpDPcUpy/lwV7QQGZV77Y5q
+vLrs8zqxoYTedMJBqI4S0kj4bgsSubci3P45/AiaoKyw3FI4fJsi+2PvbO5qCqSW
+FqI0n3Y1TT8j6R7rkkNVtFM365JxkAAh0XHpVGUCAwEAATANBgkqhkiG9w0BAQsF
+AAOBgQBVWPpImLw7+f1B4+Bsh3M7sii20Lx7NOWXN++t9xHIrC8Xqb9mYcbbAkly
+/OuTE7SGaVl6LAIboDwwmN41t/gplr6iaQkP2+6UXm0j2JSr9Kusrc8WyeAqx5FJ
+Zr3YSOuOoj/Sd5Mpx1vmQGYeziUxeOKqrsYvPQLNZHY92yQzIg==
+-----END CERTIFICATE-----
diff --git a/nextcloudappstore/core/certificate/tests/data/certificates/imaginary.chain b/nextcloudappstore/core/certificate/tests/data/certificates/imaginary.chain
new file mode 100644
--- /dev/null
+++ b/nextcloudappstore/core/certificate/tests/data/certificates/imaginary.chain
@@ -0,0 +1,15 @@
+-----BEGIN CERTIFICATE-----
+MIICWjCCAcOgAwIBAgIJAJqiaLB7aOlOMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwIBcNMTYwOTEzMTc0MjQ1WhgPMjI5MDA2MjgxNzQyNDVa
+MEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJ
+bnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ
+AoGBAM41kiKcdyQBgVMvRM9fTVUNRzF9FcTdM+MoXuctIRWbGh5H5ybFzrmOTt5g
+fnVs4qGlNyCfVW+xKPUFjEojhoOohig0cl1jHYE6K3uezERQwAfQFu5fDx8nt3dg
+1Nj20KPz3mt3r9g+Y+m7NKWVMobQ3uFuiK04XT5mFOi1H7CpAgMBAAGjUDBOMB0G
+A1UdDgQWBBTGhbR/Pnjcds1mMR1CG2l3JSAr2jAfBgNVHSMEGDAWgBTGhbR/Pnjc
+ds1mMR1CG2l3JSAr2jAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4GBAAFq
+w4G1If5x8gCR0h/ol6RBi9uxowY03fhUywHJZcSMzHMUk88Oo/dUxPEJVdIJJOss
+p3s3jR26kGHZCANN7qXjsM6c296QsG17PWgnvvlu5GV+eTMAObuzfENJnfB+Y81Q
+jT7BLBPBnNeMvvxFSWAlZABLALzIRFAYCbgWTf/D
+-----END CERTIFICATE-----
diff --git a/nextcloudappstore/core/certificate/tests/data/certificates/imaginary.crl b/nextcloudappstore/core/certificate/tests/data/certificates/imaginary.crl
new file mode 100644
--- /dev/null
+++ b/nextcloudappstore/core/certificate/tests/data/certificates/imaginary.crl
@@ -0,0 +1,10 @@
+-----BEGIN X509 CRL-----
+MIIBVzCBwQIBATANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJBVTETMBEGA1UE
+CAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk
+Fw0xNjA5MTMxODAyNTZaFw0xNjEwMTMxODAyNTZaMDgwGgIJANPDA08Qef82Fw0x
+NjA5MTMxODAxMTNaMBoCCQDTwwNPEHn/NxcNMTYwOTEzMTgwMTIwWqAOMAwwCgYD
+VR0UBAMCAQIwDQYJKoZIhvcNAQELBQADgYEAl3rWqxlFPZ2gEtb3QIiwgDaljCX4
+Ko1nGZGQSxRc89+Sj+kAeBFqzGHI+9+9dOCMXgMqD5+rvYQgJxs3gQZcIxKC9FQk
+dTsqaKzQ+9oN1yJf70K++pLVxZ7B9AJ4PMcGihierWAfWuwQrKqS8mQ50joLJkKy
+CLiJDHagrNvbE0s=
+-----END X509 CRL-----
diff --git a/nextcloudappstore/core/certificate/tests/test_validator.py b/nextcloudappstore/core/certificate/tests/test_validator.py
--- a/nextcloudappstore/core/certificate/tests/test_validator.py
+++ b/nextcloudappstore/core/certificate/tests/test_validator.py
@@ -16,66 +16,81 @@ def setUp(self) -> None:
         self.config = self.container.resolve(CertificateConfiguration)
 
     def test_get_cn(self) -> None:
-        cert = read_relative_file(__file__, 'data/certificates/app.crt')
+        cert = self._read_cert('app.crt')
         self.assertEqual('folderplayer', self.validator.get_cn(cert))
 
     def test_validate_app_id(self) -> None:
-        cert = read_relative_file(__file__, 'data/certificates/app.crt')
+        cert = self._read_cert('app.crt')
         self.validator.validate_app_id(cert, 'folderplayer')
 
     def test_validate_app_id_invalid(self) -> None:
-        cert = read_relative_file(__file__, 'data/certificates/app.crt')
+        cert = self._read_cert('app.crt')
         with self.assertRaises(CertificateAppIdMismatchException):
             self.validator.validate_app_id(cert, '/folderplayer')
 
     def test_validate_cert_signed(self) -> None:
-        cert = read_relative_file(__file__, 'data/certificates/app.crt')
-        chain = read_relative_file(__file__, 'data/certificates/owncloud.crt')
+        cert = self._read_cert('app.crt')
+        chain = self._read_cert('owncloud.crt')
         self.validator.validate_certificate(cert, chain)
 
-    def test_validate_cert_signed_not_on_crl(self) -> None:
-        cert = read_relative_file(__file__, 'data/certificates/app.crt')
-        chain = read_relative_file(__file__, 'data/certificates/owncloud.crt')
-        crl = read_relative_file(__file__, 'data/certificates/nextcloud.crl')
-        self.validator.validate_certificate(cert, chain, crl)
-
     def test_validate_old_cert_signed(self) -> None:
-        cert = read_relative_file(__file__, 'data/certificates/news-old.crt')
-        chain = read_relative_file(__file__, 'data/certificates/owncloud.crt')
+        cert = self._read_cert('news-old.crt')
+        chain = self._read_cert('owncloud.crt')
         self.validator.validate_certificate(cert, chain)
 
-    def test_validate_cert_crl(self) -> None:
-        # TBD
-        self.assertTrue(True)
-
     def test_validate_cert_not_signed(self) -> None:
-        cert = read_relative_file(__file__, 'data/certificates/app.crt')
-        chain = read_relative_file(__file__,
-                                   'data/certificates/nextcloud.crt')
+        cert = self._read_cert('app.crt')
+        chain = self._read_cert('imaginary.chain')
         with(self.assertRaises(InvalidCertificateException)):
             self.validator.validate_certificate(cert, chain)
 
     def test_signature(self) -> None:
-        cert = read_relative_file(__file__, 'data/certificates/news-old.crt')
-        sign = read_relative_file(__file__,
-                                  'data/certificates/news-old-minimal.sig')
+        cert = self._read_cert('news-old.crt')
+        sign = self._read_cert('news-old-minimal.sig')
         checksum = self._read_bin_file('data/archives/minimal.tar.gz')
         self.validator.validate_signature(cert, sign, checksum)
 
     def test_app_id_signature(self) -> None:
-        cert = read_relative_file(__file__, 'data/certificates/news-old.crt')
-        sign = read_relative_file(__file__,
-                                  'data/certificates/news-old-app-id.sig')
+        cert = self._read_cert('news-old.crt')
+        sign = self._read_cert('news-old-app-id.sig')
         self.validator.validate_signature(cert, sign, 'news'.encode())
 
     def test_bad_signature(self) -> None:
-        cert = read_relative_file(__file__, 'data/certificates/news-old.crt')
-        sign = read_relative_file(__file__,
-                                  'data/certificates/bad-news-old-minimal.sig')
+        cert = self._read_cert('news-old.crt')
+        sign = self._read_cert('bad-news-old-minimal.sig')
         checksum = self._read_bin_file('data/archives/minimal.tar.gz')
         with (self.assertRaises(InvalidSignatureException)):
             self.validator.validate_signature(cert, sign, checksum)
 
+    def test_validate_cert_signed_not_on_crl(self) -> None:
+        cert = self._read_cert('imaginary-revoked1.crt')
+        chain = self._read_cert('imaginary.chain')
+        crl = self._read_cert('empty.crl')
+        self.validator.validate_certificate(cert, chain, crl)
+
+    def test_validate_crl(self) -> None:
+        cert = self._read_cert('imaginary-valid.crt')
+        chain = self._read_cert('imaginary.chain')
+        crl = self._read_cert('imaginary.crl')
+        self.validator.validate_certificate(cert, chain, crl)
+
+    def test_validate_crl_revoked1(self) -> None:
+        cert = self._read_cert('imaginary-revoked1.crt')
+        chain = self._read_cert('imaginary.chain')
+        crl = self._read_cert('imaginary.crl')
+        with self.assertRaises(InvalidCertificateException):
+            self.validator.validate_certificate(cert, chain, crl)
+
+    def test_validate_crl_revoked2(self) -> None:
+        cert = self._read_cert('imaginary-revoked2.crt')
+        chain = self._read_cert('imaginary.chain')
+        crl = self._read_cert('imaginary.crl')
+        with self.assertRaises(InvalidCertificateException):
+            self.validator.validate_certificate(cert, chain, crl)
+
+    def _read_cert(self, file: str) -> str:
+        return read_relative_file(__file__, 'data/certificates/%s' % file)
+
     def _read_bin_file(self, rel_path: str) -> bytes:
         target_path = resolve_file_relative_path(__file__, rel_path)
         with open(target_path, 'rb') as f: