Repository: pyca/cryptography

Issue:
x509._Certificate.public_key does not work with RSASSA_PSS certs
Cause: in `cryptography/hazmat/backends/openssl/backend.py` `_evp_pkey_to_public_key`
cryptography is only testing for `self._lib.EVP_PKEY_RSA` and not `EVP_PKEY_RSA_PSS` as well.

I guess the same problem will occur with RSA OAEP certificates.


* Versions of Python, ``cryptography``, ``cffi``, ``pip``, and ``setuptools``
  you're using
    * Python 3.7.3
    * cryptography 2.6.1
    * cffi 1.11.5
    * pip 19.0.3
    * setuptools 40.4.3
* How you installed ``cryptography`` : pip

1. Create a self-signed certificate with RSA 2048 RSASSA_PSS; OpenSSL can self-verify this
example cert, i.e., `openssl verify -CAfile  sample.pem sample.pem` gives `OK`.
```
-----BEGIN CERTIFICATE-----
MIIDLTCCAhWgAwIBAgIUCD6ZpVYd5vCgzRh5ZIqYupE2VKIwDQYJKoZIhvcNAQEK
MAAwJTESMBAGA1UECgwJYm9vdHN0cmFwMQ8wDQYDVQQDDAZyb290Y2EwHhcNMTYw
MTAxMDAwMDAwWhcNNDAwMTAxMDAwMDAwWjAlMRIwEAYDVQQKDAlib290c3RyYXAx
DzANBgNVBAMMBnJvb3RjYTCCAVYwQQYJKoZIhvcNAQEKMDSgDzANBglghkgBZQME
AgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEFAKIDAgEgA4IBDwAwggEK
AoIBAQCuXwLURTDREKWTeBxUQWQvj/hDVc0+PruZtBF5voNAZCjKSOKHFLasmCDw
JuEHjj7ZHDF1JWZVGUbz3p5P+TiFmO/c1Wgb5IyAxdiDUGZvSVX3uC/X8EG/1MQz
bwcDpqiadoIjL59jUJ0g2BJnx81NvpNgpe0rmK7aU52sPKJme31Ttd/lO8VJ3Mps
lzpH0qzDJEcE3+lBF+AOJf2XDbPTFlbuPvDZHE5tVmdYC2IOad8U0Q/FLOhpMFOX
j0n6tKf+Z14+7+xu7RV3gj/NMm0CXWG3ibTSOSrbyvreI0dHgZL57RdqCSE9scjA
/1tD0a7UINhPBDZc6HaUqQUsQCPhAgMBAAGjITAfMB0GA1UdDgQWBBSpiALqV+wo
zwcvMEvwYFLKe/vDPzANBgkqhkiG9w0BAQowAAOCAQEAEdp7sFyQA9g3Vk1KsrAB
UFKqEe1a0azE4TRz2SRktRCswgv7iae0CiBGtPrzBNS6MlketixTfF1npEi7wuDn
/00XRdgHCBIRGvemATx8oSP4qVrHUud2y/DLZOZBGYiasSHHgybsnikFZppGFp7m
1D3Tti+GEsaANwRH5GW7h8f9hTMluwXlnNwyT/83yKq9Uih0eFEWHtf8hFswpCEK
4swEBwBHUiCZs1O702H36xEnoayOWnIWAkV8ZccEjfbCHs+rnU+nGI5UfXg86VRb
9iqcII7xyzPxb/HZRBusI9Uu/xiiqLjWqmx/ZMeadhiQndCoOj2yR8YT+G1BqcLa
pA==
-----END CERTIFICATE-----
```

2. Load this cert into cryptography
```
cert = x509.load_pem_x509_certificate(b'-----BEGIN ...', backend=default_backend())
cert.public_key()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/Projects/venv37/lib/python3.7/site-packages/cryptography/hazmat/backends/openssl/x509.py", line 89, in public_key
    return self._backend._evp_pkey_to_public_key(pkey)
  File "/home/Projects/venv37/lib/python3.7/site-packages/cryptography/hazmat/backends/openssl/backend.py", line 556, in _evp_pkey_to_public_key
    raise UnsupportedAlgorithm("Unsupported key type.")
cryptography.exceptions.UnsupportedAlgorithm: Unsupported key type.
```



Discussion:
This is related to #2850, but I'm going to leave this open so we can separate the parsing/signing components here.
This also affects `load_der_public_key`: the PSS NID (912) doesn't seem to have a useful counterpart to `EVP_PKEY_get1_RSA()` used in parsing component.
Looking into this it appears there may be several missing pieces in OpenSSL. `EVP_PKEY_get0_RSA` looks for the non-PSS NID, and even if you recompile with that check removed you can't get to the RSA_PSS_PARAMS because there's no getter for `RSA_PSS_PARAMS *pss` in the opaque RSA struct. Some work is going to need to be done upstream.
This may work in 1.1.1 (which has `RSA_get0_pss_params` and knows the PSS NID for get0_RSA). 3.0 is deprecating `RSA_get0_pss_params` in favor of a new set of getters for a new `RSA_PSS_PARAMS_30` struct, but we can (likely) ignore that until such time as we're 3.0+ only: https://github.com/openssl/openssl/commit/15671090f46364a0e92456b32ead7b4714ae0b5e#diff-44d6e290c824b579440a474b82116446R41
This was actually only added in 1.1.1e and then deprecated in 3.0.0. I think we should wait for 3.0 to decide what to do here.

Resolving patch:
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -706,6 +706,19 @@ def _evp_pkey_to_public_key(self, evp_pkey) -> PUBLIC_KEY_TYPES:
             self.openssl_assert(rsa_cdata != self._ffi.NULL)
             rsa_cdata = self._ffi.gc(rsa_cdata, self._lib.RSA_free)
             return _RSAPublicKey(self, rsa_cdata, evp_pkey)
+        elif (
+            key_type == self._lib.EVP_PKEY_RSA_PSS
+            and not self._lib.CRYPTOGRAPHY_IS_LIBRESSL
+            and not self._lib.CRYPTOGRAPHY_IS_BORINGSSL
+            and not self._lib.CRYPTOGRAPHY_OPENSSL_LESS_THAN_111E
+        ):
+            rsa_cdata = self._lib.EVP_PKEY_get1_RSA(evp_pkey)
+            self.openssl_assert(rsa_cdata != self._ffi.NULL)
+            rsa_cdata = self._ffi.gc(rsa_cdata, self._lib.RSA_free)
+            bio = self._create_mem_bio_gc()
+            res = self._lib.i2d_RSAPublicKey_bio(bio, rsa_cdata)
+            self.openssl_assert(res == 1)
+            return self.load_der_public_key(self._read_mem_bio(bio))
         elif key_type == self._lib.EVP_PKEY_DSA:
             dsa_cdata = self._lib.EVP_PKEY_get1_DSA(evp_pkey)
             self.openssl_assert(dsa_cdata != self._ffi.NULL)


Test patch:
diff --git a/tests/hazmat/primitives/test_rsa.py b/tests/hazmat/primitives/test_rsa.py
--- a/tests/hazmat/primitives/test_rsa.py
+++ b/tests/hazmat/primitives/test_rsa.py
@@ -296,6 +296,30 @@ def test_load_pss_keys_strips_constraints(self, path, backend):
             signature, b"whatever", padding.PKCS1v15(), hashes.SHA224()
         )
 
+    @pytest.mark.supported(
+        only_if=lambda backend: (
+            not backend._lib.CRYPTOGRAPHY_IS_LIBRESSL
+            and not backend._lib.CRYPTOGRAPHY_IS_BORINGSSL
+            and not backend._lib.CRYPTOGRAPHY_OPENSSL_LESS_THAN_111E
+        ),
+        skip_message="Does not support RSA PSS loading",
+    )
+    def test_load_pss_pub_keys_strips_constraints(self, backend):
+        key = load_vectors_from_file(
+            filename=os.path.join(
+                "asymmetric", "PKCS8", "rsa_pss_2048_pub.der"
+            ),
+            loader=lambda p: serialization.load_der_public_key(
+                p.read(),
+            ),
+            mode="rb",
+        )
+        assert isinstance(key, rsa.RSAPublicKey)
+        with pytest.raises(InvalidSignature):
+            key.verify(
+                b"badsig", b"whatever", padding.PKCS1v15(), hashes.SHA256()
+            )
+
     @pytest.mark.supported(
         only_if=lambda backend: (
             backend._lib.CRYPTOGRAPHY_IS_LIBRESSL
diff --git a/tests/x509/test_x509.py b/tests/x509/test_x509.py
--- a/tests/x509/test_x509.py
+++ b/tests/x509/test_x509.py
@@ -688,6 +688,33 @@ def test_get_revoked_certificate_doesnt_reorder(self, backend):
         assert crl[2].serial_number == 3
 
 
+class TestRSAPSSCertificate:
+    @pytest.mark.supported(
+        only_if=lambda backend: (
+            not backend._lib.CRYPTOGRAPHY_IS_LIBRESSL
+            and not backend._lib.CRYPTOGRAPHY_IS_BORINGSSL
+            and not backend._lib.CRYPTOGRAPHY_OPENSSL_LESS_THAN_111E
+        ),
+        skip_message="Does not support RSA PSS loading",
+    )
+    def test_load_cert_pub_key(self, backend):
+        cert = _load_cert(
+            os.path.join("x509", "custom", "rsa_pss_cert.pem"),
+            x509.load_pem_x509_certificate,
+            backend,
+        )
+        assert isinstance(cert, x509.Certificate)
+        expected_pub_key = _load_cert(
+            os.path.join("asymmetric", "PKCS8", "rsa_pss_2048_pub.der"),
+            serialization.load_der_public_key,
+            backend,
+        )
+        assert isinstance(expected_pub_key, rsa.RSAPublicKey)
+        pub_key = cert.public_key()
+        assert isinstance(pub_key, rsa.RSAPublicKey)
+        assert pub_key.public_numbers() == expected_pub_key.public_numbers()
+
+
 class TestRSACertificate:
     def test_load_pem_cert(self, backend):
         cert = _load_cert(