Repository: pyca/cryptography

Issue:
Two PKCS12 tests fail when OpenSSL is built without RC2 support
The following two tests fail when run with an **OpenSSL** (1.1.1d) built **without RC2** support:

 * TestPKCS12.test_load_pkcs12_ec_keys[cert-rc2-key-3des.p12-cryptography]
 * TestPKCS12.test_load_pkcs12_ec_keys[no-password.p12-None]

The error summary is: `ValueError: Invalid password or PKCS12 data`

Reproduction Environment:

 * Operating System:  FreeBSD 13.0-CURRENT #3 r354143: Tue Oct 29 13:10:35 AEDT 2019
 * OpenSSL: 1.1.1d-freebsd  10 Sep 2019 (via ports, with RC2 option disabled)
 * Python 2.7.17 / 3.7.5
 * cryptography: 2.8 (PyPI sdist)

Full backtraces follow:

TestPKCS12.test_load_pkcs12_ec_keys[cert-rc2-key-3des.p12-cryptography] 
```
TestPKCS12.test_load_pkcs12_ec_keys[cert-rc2-key-3des.p12-cryptography] 
_______________________________________________________________________

self = <tests.hazmat.primitives.test_pkcs12.TestPKCS12 object at 0x8774ecc50>, filename = 'cert-rc2-key-3des.p12', password = 'cryptography', backend = <cryptography.hazmat.backends.openssl.backend.Backend object at 0x804bf5390>

    @pytest.mark.parametrize(
        ("filename", "password"),
        [
            ("cert-key-aes256cbc.p12", b"cryptography"),
            ("cert-none-key-none.p12", b"cryptography"),
            ("cert-rc2-key-3des.p12", b"cryptography"),
            ("no-password.p12", None),
        ]
    )
    def test_load_pkcs12_ec_keys(self, filename, password, backend):
        cert = load_vectors_from_file(
            os.path.join("x509", "custom", "ca", "ca.pem"),
            lambda pemfile: x509.load_pem_x509_certificate(
                pemfile.read(), backend
            ), mode="rb"
        )
        key = load_vectors_from_file(
            os.path.join("x509", "custom", "ca", "ca_key.pem"),
            lambda pemfile: load_pem_private_key(
                pemfile.read(), None, backend
            ), mode="rb"
        )
        parsed_key, parsed_cert, parsed_more_certs = load_vectors_from_file(
            os.path.join("pkcs12", filename),
            lambda derfile: load_key_and_certificates(
                derfile.read(), password, backend
>           ), mode="rb"
        )

tests/hazmat/primitives/test_pkcs12.py:49:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
tests/utils.py:48: in load_vectors_from_file
    return loader(vector_file)
tests/hazmat/primitives/test_pkcs12.py:48: in <lambda>
    derfile.read(), password, backend
src/cryptography/hazmat/primitives/serialization/pkcs12.py:9: in load_key_and_certificates
    return backend.load_key_and_certificates_from_pkcs12(data, password)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <cryptography.hazmat.backends.openssl.backend.Backend object at 0x804bf5390>
data = "0\x82\x03R\x02\x01\x030\x82\x03\x18\x06\t*\x86H\x86\xf7\r\x01\x07\x01\xa0\x82\x03\t\x04\x82\x03\x050\x82\x03\x010\x82...2\x1a\x05\x00\x04\x14)\x00\x91\x90\xe8\x10I\xc3uw<U\xd4\xdc\xa7EM}C\x1b\x04\x08\xb7\xde6_\xa1\x95\xdd!\x02\x02\x08\x00", password = 'cryptography'

    def load_key_and_certificates_from_pkcs12(self, data, password):
        if password is not None:
            utils._check_byteslike("password", password)

        bio = self._bytes_to_bio(data)
        p12 = self._lib.d2i_PKCS12_bio(bio.bio, self._ffi.NULL)
        if p12 == self._ffi.NULL:
            self._consume_errors()
            raise ValueError("Could not deserialize PKCS12 data")

        p12 = self._ffi.gc(p12, self._lib.PKCS12_free)
        evp_pkey_ptr = self._ffi.new("EVP_PKEY **")
        x509_ptr = self._ffi.new("X509 **")
        sk_x509_ptr = self._ffi.new("Cryptography_STACK_OF_X509 **")
        with self._zeroed_null_terminated_buf(password) as password_buf:
            res = self._lib.PKCS12_parse(
                p12, password_buf, evp_pkey_ptr, x509_ptr, sk_x509_ptr
            )

        if res == 0:
            self._consume_errors()
>           raise ValueError("Invalid password or PKCS12 data")
E           ValueError: Invalid password or PKCS12 data

src/cryptography/hazmat/backends/openssl/backend.py:2433: ValueError
```

TestPKCS12.test_load_pkcs12_ec_keys[no-password.p12-None
```
TestPKCS12.test_load_pkcs12_ec_keys[no-password.p12-None
_________________________________________________________________________

self = <tests.hazmat.primitives.test_pkcs12.TestPKCS12 object at 0x8775bc950>, filename = 'no-password.p12', password = None, backend = <cryptography.hazmat.backends.openssl.backend.Backend object at 0x804bf5390>

    @pytest.mark.parametrize(
        ("filename", "password"),
        [
            ("cert-key-aes256cbc.p12", b"cryptography"),
            ("cert-none-key-none.p12", b"cryptography"),
            ("cert-rc2-key-3des.p12", b"cryptography"),
            ("no-password.p12", None),
        ]
    )
    def test_load_pkcs12_ec_keys(self, filename, password, backend):
        cert = load_vectors_from_file(
            os.path.join("x509", "custom", "ca", "ca.pem"),
            lambda pemfile: x509.load_pem_x509_certificate(
                pemfile.read(), backend
            ), mode="rb"
        )
        key = load_vectors_from_file(
            os.path.join("x509", "custom", "ca", "ca_key.pem"),
            lambda pemfile: load_pem_private_key(
                pemfile.read(), None, backend
            ), mode="rb"
        )
        parsed_key, parsed_cert, parsed_more_certs = load_vectors_from_file(
            os.path.join("pkcs12", filename),
            lambda derfile: load_key_and_certificates(
                derfile.read(), password, backend
>           ), mode="rb"
        )

tests/hazmat/primitives/test_pkcs12.py:49:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
tests/utils.py:48: in load_vectors_from_file
    return loader(vector_file)
tests/hazmat/primitives/test_pkcs12.py:48: in <lambda>
    derfile.read(), password, backend
src/cryptography/hazmat/primitives/serialization/pkcs12.py:9: in load_key_and_certificates
    return backend.load_key_and_certificates_from_pkcs12(data, password)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <cryptography.hazmat.backends.openssl.backend.Backend object at 0x804bf5390>
data = '0\x82\x03\xb0\x02\x01\x030\x82\x03w\x06\t*\x86H\x86\xf7\r\x01\x07\x01\xa0\x82\x03h\x04\x82\x03d0\x82\x03`0\x82\x02\'\...5\x00\x04\x14mL\xfc\xde\xc7i\xdc\xfd\xd4\x06x\xd7\x9d{\x19\xebm\x14"\xe4\x04\x08u+\xf6\x1b\x90\xbb\xee\xad\x02\x01\x01', password = None

    def load_key_and_certificates_from_pkcs12(self, data, password):
        if password is not None:
            utils._check_byteslike("password", password)

        bio = self._bytes_to_bio(data)
        p12 = self._lib.d2i_PKCS12_bio(bio.bio, self._ffi.NULL)
        if p12 == self._ffi.NULL:
            self._consume_errors()
            raise ValueError("Could not deserialize PKCS12 data")

        p12 = self._ffi.gc(p12, self._lib.PKCS12_free)
        evp_pkey_ptr = self._ffi.new("EVP_PKEY **")
        x509_ptr = self._ffi.new("X509 **")
        sk_x509_ptr = self._ffi.new("Cryptography_STACK_OF_X509 **")
        with self._zeroed_null_terminated_buf(password) as password_buf:
            res = self._lib.PKCS12_parse(
                p12, password_buf, evp_pkey_ptr, x509_ptr, sk_x509_ptr
            )

        if res == 0:
            self._consume_errors()
>           raise ValueError("Invalid password or PKCS12 data")
E           ValueError: Invalid password or PKCS12 data

src/cryptography/hazmat/backends/openssl/backend.py:2433: ValueError

_________________________________________________________________________
2 failed, 102886 passed, 4194 skipped, 3 warnings in 1021.08 seconds
_________________________________________________________________________
```




Discussion:
Thanks for tracking this down. I have no idea what the right fix is.
(Well, the fix is clearly to skip the test, I just don't know how to wire the "when" through all the paths)
We could make a private RC2 class that we only use for doing cipher_supported checks. That might work for this?
That sounds reasonable.

On Thu, Nov 14, 2019 at 9:30 PM Paul Kehrer <notifications@github.com>
wrote:

> We could make a private RC2 class that we only use for doing
> cipher_supported checks. That might work for this?
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/pyca/cryptography/issues/5065?email_source=notifications&email_token=AAAAGBBMY2HNB4QH5TQ44GTQTYCULA5CNFSM4JNUNWY2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEEECMLQ#issuecomment-554182190>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAAAGBBMBRFMCJW6S2P5HHTQTYCULANCNFSM4JNUNWYQ>
> .
>


-- 
All that is necessary for evil to succeed is for good people to do nothing.


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
@@ -94,6 +94,11 @@
 _MemoryBIO = collections.namedtuple("_MemoryBIO", ["bio", "char_ptr"])
 
 
+# Not actually supported, just used as a marker for some serialization tests.
+class _RC2(object):
+    pass
+
+
 @utils.register_interface(CipherBackend)
 @utils.register_interface(CMACBackend)
 @utils.register_interface(DERSerializationBackend)
@@ -292,6 +297,10 @@ def _register_default_ciphers(self):
             type(None),
             GetCipherByName("rc4")
         )
+        # We don't actually support RC2, this is just used by some tests.
+        self.register_cipher_adapter(
+            _RC2, type(None), GetCipherByName("rc2")
+        )
         self.register_cipher_adapter(
             ChaCha20,
             type(None),


Test patch:
diff --git a/tests/hazmat/primitives/test_pkcs12.py b/tests/hazmat/primitives/test_pkcs12.py
--- a/tests/hazmat/primitives/test_pkcs12.py
+++ b/tests/hazmat/primitives/test_pkcs12.py
@@ -10,6 +10,7 @@
 
 from cryptography import x509
 from cryptography.hazmat.backends.interfaces import DERSerializationBackend
+from cryptography.hazmat.backends.openssl.backend import _RC2
 from cryptography.hazmat.primitives.serialization import load_pem_private_key
 from cryptography.hazmat.primitives.serialization.pkcs12 import (
     load_key_and_certificates
@@ -20,16 +21,7 @@
 
 @pytest.mark.requires_backend_interface(interface=DERSerializationBackend)
 class TestPKCS12(object):
-    @pytest.mark.parametrize(
-        ("filename", "password"),
-        [
-            ("cert-key-aes256cbc.p12", b"cryptography"),
-            ("cert-none-key-none.p12", b"cryptography"),
-            ("cert-rc2-key-3des.p12", b"cryptography"),
-            ("no-password.p12", None),
-        ]
-    )
-    def test_load_pkcs12_ec_keys(self, filename, password, backend):
+    def _test_load_pkcs12_ec_keys(self, filename, password, backend):
         cert = load_vectors_from_file(
             os.path.join("x509", "custom", "ca", "ca.pem"),
             lambda pemfile: x509.load_pem_x509_certificate(
@@ -52,6 +44,30 @@ def test_load_pkcs12_ec_keys(self, filename, password, backend):
         assert parsed_key.private_numbers() == key.private_numbers()
         assert parsed_more_certs == []
 
+    @pytest.mark.parametrize(
+        ("filename", "password"),
+        [
+            ("cert-key-aes256cbc.p12", b"cryptography"),
+            ("cert-none-key-none.p12", b"cryptography"),
+        ]
+    )
+    def test_load_pkcs12_ec_keys(self, filename, password, backend):
+        self._test_load_pkcs12_ec_keys(filename, password, backend)
+
+    @pytest.mark.parametrize(
+        ("filename", "password"),
+        [
+            ("cert-rc2-key-3des.p12", b"cryptography"),
+            ("no-password.p12", None),
+        ]
+    )
+    @pytest.mark.supported(
+        only_if=lambda backend: backend.cipher_supported(_RC2(), None),
+        skip_message="Does not support RC2"
+    )
+    def test_load_pkcs12_ec_keys_rc2(self, filename, password, backend):
+        self._test_load_pkcs12_ec_keys(filename, password, backend)
+
     def test_load_pkcs12_cert_only(self, backend):
         cert = load_vectors_from_file(
             os.path.join("x509", "custom", "ca", "ca.pem"),