From cea1f2e60948847996a6af0d30787ce4492e8fe6 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sat, 9 Mar 2024 18:06:12 -0500 Subject: [PATCH] Remove deprecated PKCS12 and NetscapeSPKI classes (#1288) --- CHANGELOG.rst | 16 +++ doc/api/crypto.rst | 18 --- src/OpenSSL/crypto.py | 300 ---------------------------------------- tests/test_crypto.py | 314 ------------------------------------------ 4 files changed, 16 insertions(+), 632 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7f6d60c0e..7a7958b8a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,22 @@ Changelog Versions are year-based with a strict backward-compatibility policy. The third digit is only for regressions. +24.1.0 (UNRELEASED) +------------------- + +Backward-incompatible changes: +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* Removed the deprecated ``OpenSSL.crypto.PKCS12`` and + ``OpenSSL.crypto.NetscapeSPKI``. ``OpenSSL.crypto.PKCS12`` may be replaced + by the PKCS#12 APIs in the ``cryptography`` package. + +Deprecations: +^^^^^^^^^^^^^ + +Changes: +^^^^^^^^ + 24.0.0 (2024-01-22) ------------------- diff --git a/doc/api/crypto.rst b/doc/api/crypto.rst index 56eeb8315..926ae5809 100644 --- a/doc/api/crypto.rst +++ b/doc/api/crypto.rst @@ -160,14 +160,6 @@ PKey objects Key type constants. -.. _openssl-pkcs12: - -PKCS12 objects --------------- - -.. autoclass:: PKCS12 - :members: - .. _openssl-509ext: X509Extension objects @@ -178,16 +170,6 @@ X509Extension objects :special-members: :exclude-members: __weakref__ -.. _openssl-netscape-spki: - -NetscapeSPKI objects --------------------- - -.. autoclass:: NetscapeSPKI - :members: - :special-members: - :exclude-members: __weakref__ - .. _crl: CRL objects diff --git a/src/OpenSSL/crypto.py b/src/OpenSSL/crypto.py index 1707488c7..80a6c1958 100644 --- a/src/OpenSSL/crypto.py +++ b/src/OpenSSL/crypto.py @@ -77,8 +77,6 @@ "dump_privatekey", "Revoked", "CRL", - "PKCS12", - "NetscapeSPKI", "load_publickey", "load_privatekey", "dump_certificate_request", @@ -2617,304 +2615,6 @@ def export( ) -class PKCS12: - """ - A PKCS #12 archive. - """ - - def __init__(self) -> None: - self._pkey: Optional[PKey] = None - self._cert: Optional[X509] = None - self._cacerts: Optional[List[X509]] = None - self._friendlyname: Optional[bytes] = None - - def get_certificate(self) -> Optional[X509]: - """ - Get the certificate in the PKCS #12 structure. - - :return: The certificate, or :py:const:`None` if there is none. - :rtype: :py:class:`X509` or :py:const:`None` - """ - return self._cert - - def set_certificate(self, cert: X509) -> None: - """ - Set the certificate in the PKCS #12 structure. - - :param cert: The new certificate, or :py:const:`None` to unset it. - :type cert: :py:class:`X509` or :py:const:`None` - - :return: ``None`` - """ - if not isinstance(cert, X509): - raise TypeError("cert must be an X509 instance") - self._cert = cert - - def get_privatekey(self) -> Optional[PKey]: - """ - Get the private key in the PKCS #12 structure. - - :return: The private key, or :py:const:`None` if there is none. - :rtype: :py:class:`PKey` - """ - return self._pkey - - def set_privatekey(self, pkey: PKey) -> None: - """ - Set the certificate portion of the PKCS #12 structure. - - :param pkey: The new private key, or :py:const:`None` to unset it. - :type pkey: :py:class:`PKey` or :py:const:`None` - - :return: ``None`` - """ - if not isinstance(pkey, PKey): - raise TypeError("pkey must be a PKey instance") - self._pkey = pkey - - def get_ca_certificates(self) -> Optional[Tuple[X509, ...]]: - """ - Get the CA certificates in the PKCS #12 structure. - - :return: A tuple with the CA certificates in the chain, or - :py:const:`None` if there are none. - :rtype: :py:class:`tuple` of :py:class:`X509` or :py:const:`None` - """ - if self._cacerts is not None: - return tuple(self._cacerts) - return None - - def set_ca_certificates(self, cacerts: Optional[Iterable[X509]]) -> None: - """ - Replace or set the CA certificates within the PKCS12 object. - - :param cacerts: The new CA certificates, or :py:const:`None` to unset - them. - :type cacerts: An iterable of :py:class:`X509` or :py:const:`None` - - :return: ``None`` - """ - if cacerts is None: - self._cacerts = None - else: - cacerts = list(cacerts) - for cert in cacerts: - if not isinstance(cert, X509): - raise TypeError( - "iterable must only contain X509 instances" - ) - self._cacerts = cacerts - - def set_friendlyname(self, name: Optional[bytes]) -> None: - """ - Set the friendly name in the PKCS #12 structure. - - :param name: The new friendly name, or :py:const:`None` to unset. - :type name: :py:class:`bytes` or :py:const:`None` - - :return: ``None`` - """ - if name is None: - self._friendlyname = None - elif not isinstance(name, bytes): - raise TypeError( - f"name must be a byte string or None (not {name!r})" - ) - self._friendlyname = name - - def get_friendlyname(self) -> Optional[bytes]: - """ - Get the friendly name in the PKCS# 12 structure. - - :returns: The friendly name, or :py:const:`None` if there is none. - :rtype: :py:class:`bytes` or :py:const:`None` - """ - return self._friendlyname - - def export( - self, - passphrase: Optional[bytes] = None, - iter: int = 2048, - maciter: int = 1, - ) -> bytes: - """ - Dump a PKCS12 object as a string. - - For more information, see the :c:func:`PKCS12_create` man page. - - :param passphrase: The passphrase used to encrypt the structure. Unlike - some other passphrase arguments, this *must* be a string, not a - callback. - :type passphrase: :py:data:`bytes` - - :param iter: Number of times to repeat the encryption step. - :type iter: :py:data:`int` - - :param maciter: Number of times to repeat the MAC step. - :type maciter: :py:data:`int` - - :return: The string representation of the PKCS #12 structure. - :rtype: - """ - passphrase = _text_to_bytes_and_warn("passphrase", passphrase) - - if self._cacerts is None: - cacerts = _ffi.NULL - else: - cacerts = _lib.sk_X509_new_null() - cacerts = _ffi.gc(cacerts, _lib.sk_X509_free) - for cert in self._cacerts: - _lib.sk_X509_push(cacerts, cert._x509) - - if passphrase is None: - passphrase = _ffi.NULL - - friendlyname = self._friendlyname - if friendlyname is None: - friendlyname = _ffi.NULL - - if self._pkey is None: - pkey = _ffi.NULL - else: - pkey = self._pkey._pkey - - if self._cert is None: - cert = _ffi.NULL - else: - cert = self._cert._x509 - - pkcs12 = _lib.PKCS12_create( - passphrase, - friendlyname, - pkey, - cert, - cacerts, - _lib.NID_pbe_WithSHA1And3_Key_TripleDES_CBC, - _lib.NID_pbe_WithSHA1And3_Key_TripleDES_CBC, - iter, - maciter, - 0, - ) - if pkcs12 == _ffi.NULL: - _raise_current_error() - pkcs12 = _ffi.gc(pkcs12, _lib.PKCS12_free) - - bio = _new_mem_buf() - _lib.i2d_PKCS12_bio(bio, pkcs12) - return _bio_to_string(bio) - - -utils.deprecated( - PKCS12, - __name__, - ( - "PKCS#12 support in pyOpenSSL is deprecated. You should use the APIs " - "in cryptography." - ), - DeprecationWarning, - name="PKCS12", -) - - -class NetscapeSPKI: - """ - A Netscape SPKI object. - """ - - def __init__(self) -> None: - spki = _lib.NETSCAPE_SPKI_new() - self._spki = _ffi.gc(spki, _lib.NETSCAPE_SPKI_free) - - def sign(self, pkey: PKey, digest: str) -> None: - """ - Sign the certificate request with this key and digest type. - - :param pkey: The private key to sign with. - :type pkey: :py:class:`PKey` - - :param digest: The message digest to use. - :type digest: :py:class:`str` - - :return: ``None`` - """ - if pkey._only_public: - raise ValueError("Key has only public part") - - if not pkey._initialized: - raise ValueError("Key is uninitialized") - - digest_obj = _lib.EVP_get_digestbyname(_byte_string(digest)) - if digest_obj == _ffi.NULL: - raise ValueError("No such digest method") - - sign_result = _lib.NETSCAPE_SPKI_sign( - self._spki, pkey._pkey, digest_obj - ) - _openssl_assert(sign_result > 0) - - def verify(self, key: PKey) -> bool: - """ - Verifies a signature on a certificate request. - - :param PKey key: The public key that signature is supposedly from. - - :return: ``True`` if the signature is correct. - :rtype: bool - - :raises OpenSSL.crypto.Error: If the signature is invalid, or there was - a problem verifying the signature. - """ - answer = _lib.NETSCAPE_SPKI_verify(self._spki, key._pkey) - if answer <= 0: - _raise_current_error() - return True - - def b64_encode(self) -> bytes: - """ - Generate a base64 encoded representation of this SPKI object. - - :return: The base64 encoded string. - :rtype: :py:class:`bytes` - """ - encoded = _lib.NETSCAPE_SPKI_b64_encode(self._spki) - result = _ffi.string(encoded) - _lib.OPENSSL_free(encoded) - return result - - def get_pubkey(self) -> PKey: - """ - Get the public key of this certificate. - - :return: The public key. - :rtype: :py:class:`PKey` - """ - pkey = PKey.__new__(PKey) - pkey._pkey = _lib.NETSCAPE_SPKI_get_pubkey(self._spki) - _openssl_assert(pkey._pkey != _ffi.NULL) - pkey._pkey = _ffi.gc(pkey._pkey, _lib.EVP_PKEY_free) - pkey._only_public = True - return pkey - - def set_pubkey(self, pkey: PKey) -> None: - """ - Set the public key of the certificate - - :param pkey: The public key - :return: ``None`` - """ - set_result = _lib.NETSCAPE_SPKI_set_pubkey(self._spki, pkey._pkey) - _openssl_assert(set_result == 1) - - -utils.deprecated( - NetscapeSPKI, - __name__, - "NetscapeSPKI support in pyOpenSSL is deprecated.", - DeprecationWarning, - name="NetscapeSPKI", -) - - class _PassphraseHelper: def __init__( self, diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 76d6be631..c0f809e53 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -50,8 +50,6 @@ with pytest.warns(DeprecationWarning): from OpenSSL.crypto import ( CRL, - PKCS12, - NetscapeSPKI, Revoked, X509Extension, dump_crl, @@ -2337,273 +2335,6 @@ def test_load_locations_raises_error_on_failure(self, tmpdir): store.load_locations(cafile=str(invalid_ca_file)) -class TestPKCS12: - """ - Test for `OpenSSL.crypto.PKCS12`. - """ - - def test_type(self): - """ - `PKCS12` is a type object. - """ - assert is_consistent_type(PKCS12, "PKCS12") - - def test_empty_construction(self): - """ - `PKCS12` returns a new instance of `PKCS12` with no certificate, - private key, CA certificates, or friendly name. - """ - p12 = PKCS12() - assert None is p12.get_certificate() - assert None is p12.get_privatekey() - assert None is p12.get_ca_certificates() - assert None is p12.get_friendlyname() - - def test_type_errors(self): - """ - The `PKCS12` setter functions (`set_certificate`, `set_privatekey`, - `set_ca_certificates`, and `set_friendlyname`) raise `TypeError` - when passed objects of types other than those expected. - """ - p12 = PKCS12() - for bad_arg in [3, PKey(), X509]: - with pytest.raises(TypeError): - p12.set_certificate(bad_arg) - for bad_arg in [3, "legbone", X509()]: - with pytest.raises(TypeError): - p12.set_privatekey(bad_arg) - for bad_arg in [3, X509(), (3, 4), (PKey(),)]: - with pytest.raises(TypeError): - p12.set_ca_certificates(bad_arg) - for bad_arg in [6, ("foo", "bar")]: - with pytest.raises(TypeError): - p12.set_friendlyname(bad_arg) - - def test_key_only(self): - """ - A `PKCS12` with only a private key can be exported using - `PKCS12.export`. - """ - passwd = b"blah" - p12 = PKCS12() - pkey = load_privatekey(FILETYPE_PEM, root_key_pem) - p12.set_privatekey(pkey) - assert None is p12.get_certificate() - assert pkey == p12.get_privatekey() - p12.export(passphrase=passwd, iter=2, maciter=3) - - def test_cert_only(self): - """ - A `PKCS12` with only a certificate can be exported using - `PKCS12.export`. - """ - passwd = b"blah" - p12 = PKCS12() - cert = load_certificate(FILETYPE_PEM, root_cert_pem) - p12.set_certificate(cert) - assert cert == p12.get_certificate() - assert None is p12.get_privatekey() - p12.export(passphrase=passwd, iter=2, maciter=3) - - def gen_pkcs12(self, cert_pem=None, key_pem=None, ca_pem=None): - """ - Generate a PKCS12 object with components from PEM. Verify that the set - functions return None. - """ - p12 = PKCS12() - - ret = p12.set_certificate(load_certificate(FILETYPE_PEM, cert_pem)) - assert ret is None - - ret = p12.set_privatekey(load_privatekey(FILETYPE_PEM, key_pem)) - assert ret is None - - if ca_pem: - ret = p12.set_ca_certificates( - (load_certificate(FILETYPE_PEM, ca_pem),) - ) - assert ret is None - return p12 - - def check_recovery( - self, p12_str, key=None, cert=None, ca=None, passwd=b"", extra=() - ): - """ - Use openssl program to confirm three components are recoverable from a - PKCS12 string. - """ - recovered_key = _runopenssl( - p12_str, - b"pkcs12", - b"-nocerts", - b"-nodes", - b"-passin", - b"pass:" + passwd, - *extra, - ).replace(b"\r\n", b"\n") - assert recovered_key[-len(key) :] == key - - recovered_cert = _runopenssl( - p12_str, - b"pkcs12", - b"-clcerts", - b"-nodes", - b"-passin", - b"pass:" + passwd, - b"-nokeys", - *extra, - ).replace(b"\r\n", b"\n") - assert recovered_cert[-len(cert) :] == cert - - if ca: - recovered_cert = _runopenssl( - p12_str, - b"pkcs12", - b"-cacerts", - b"-nodes", - b"-passin", - b"pass:" + passwd, - b"-nokeys", - *extra, - ).replace(b"\r\n", b"\n") - assert recovered_cert[-len(ca) :] == ca - - def test_replace(self): - """ - `PKCS12.set_certificate` replaces the certificate in a PKCS12 - cluster. `PKCS12.set_privatekey` replaces the private key. - `PKCS12.set_ca_certificates` replaces the CA certificates. - """ - p12 = self.gen_pkcs12(client_cert_pem, client_key_pem, root_cert_pem) - p12.set_certificate(load_certificate(FILETYPE_PEM, server_cert_pem)) - p12.set_privatekey(load_privatekey(FILETYPE_PEM, server_key_pem)) - root_cert = load_certificate(FILETYPE_PEM, root_cert_pem) - client_cert = load_certificate(FILETYPE_PEM, client_cert_pem) - p12.set_ca_certificates([root_cert]) # not a tuple - assert 1 == len(p12.get_ca_certificates()) - assert root_cert == p12.get_ca_certificates()[0] - p12.set_ca_certificates([client_cert, root_cert]) - assert 2 == len(p12.get_ca_certificates()) - assert client_cert == p12.get_ca_certificates()[0] - assert root_cert == p12.get_ca_certificates()[1] - - def test_friendly_name(self): - """ - The *friendlyName* of a PKCS12 can be set and retrieved via - `PKCS12.get_friendlyname` and `PKCS12_set_friendlyname`, and a - `PKCS12` with a friendly name set can be dumped with `PKCS12.export`. - """ - passwd = b'Dogmeat[]{}!@#$%^&*()~`?/.,<>-_+=";:' - p12 = self.gen_pkcs12(server_cert_pem, server_key_pem, root_cert_pem) - for friendly_name in [b"Serverlicious", None, b"###"]: - p12.set_friendlyname(friendly_name) - assert p12.get_friendlyname() == friendly_name - p12.export(passphrase=passwd, iter=2, maciter=3) - - def test_various_empty_passphrases(self): - """ - Test that missing, None, and '' passphrases are identical for PKCS12 - export. - """ - p12 = self.gen_pkcs12(client_cert_pem, client_key_pem, root_cert_pem) - passwd = b"" - dumped_p12_empty = p12.export(iter=2, maciter=0, passphrase=passwd) - dumped_p12_none = p12.export(iter=3, maciter=2, passphrase=None) - dumped_p12_nopw = p12.export(iter=9, maciter=4) - for dumped_p12 in [dumped_p12_empty, dumped_p12_none, dumped_p12_nopw]: - self.check_recovery( - dumped_p12, - key=client_key_pem, - cert=client_cert_pem, - ca=root_cert_pem, - passwd=passwd, - ) - - def test_removing_ca_cert(self): - """ - Passing `None` to `PKCS12.set_ca_certificates` removes all CA - certificates. - """ - p12 = self.gen_pkcs12(server_cert_pem, server_key_pem, root_cert_pem) - p12.set_ca_certificates(None) - assert None is p12.get_ca_certificates() - - def test_export_without_mac(self): - """ - Exporting a PKCS12 with a `maciter` of `-1` excludes the MAC entirely. - """ - passwd = b"Lake Michigan" - p12 = self.gen_pkcs12(server_cert_pem, server_key_pem, root_cert_pem) - dumped_p12 = p12.export(maciter=-1, passphrase=passwd, iter=2) - self.check_recovery( - dumped_p12, - key=server_key_pem, - cert=server_cert_pem, - passwd=passwd, - extra=(b"-nomacver",), - ) - - def test_load_without_mac(self): - """ - Loading a PKCS12 without a MAC does something other than crash. - """ - passwd = b"Lake Michigan" - p12 = self.gen_pkcs12(server_cert_pem, server_key_pem, root_cert_pem) - p12.export(maciter=-1, passphrase=passwd, iter=2) - - def test_zero_len_list_for_ca(self): - """ - A PKCS12 with an empty CA certificates list can be exported. - """ - passwd = b"Hobie 18" - p12 = self.gen_pkcs12(server_cert_pem, server_key_pem) - p12.set_ca_certificates([]) - assert () == p12.get_ca_certificates() - dumped_p12 = p12.export(passphrase=passwd, iter=3) - self.check_recovery( - dumped_p12, key=server_key_pem, cert=server_cert_pem, passwd=passwd - ) - - def test_export_without_args(self): - """ - All the arguments to `PKCS12.export` are optional. - """ - p12 = self.gen_pkcs12(server_cert_pem, server_key_pem, root_cert_pem) - dumped_p12 = p12.export() # no args - self.check_recovery( - dumped_p12, key=server_key_pem, cert=server_cert_pem, passwd=b"" - ) - - def test_export_without_bytes(self): - """ - Test `PKCS12.export` with text not bytes as passphrase - """ - p12 = self.gen_pkcs12(server_cert_pem, server_key_pem, root_cert_pem) - - with pytest.warns(DeprecationWarning) as w: - warnings.simplefilter("always") - dumped_p12 = p12.export(passphrase=b"randomtext".decode("ascii")) - msg = "{} for passphrase is no longer accepted, use bytes".format( - WARNING_TYPE_EXPECTED - ) - assert msg == str(w[-1].message) - self.check_recovery( - dumped_p12, - key=server_key_pem, - cert=server_cert_pem, - passwd=b"randomtext", - ) - - def test_key_cert_mismatch(self): - """ - `PKCS12.export` raises an exception when a key and certificate - mismatch. - """ - p12 = self.gen_pkcs12(server_cert_pem, client_key_pem, root_cert_pem) - with pytest.raises(Error): - p12.export() - - def _runopenssl(pem, *args): """ Run the command line openssl tool with the given arguments and write @@ -3073,51 +2804,6 @@ def test_bad_certificate(self): load_certificate(FILETYPE_ASN1, b"lol") -class TestNetscapeSPKI(_PKeyInteractionTestsMixin): - """ - Tests for `OpenSSL.crypto.NetscapeSPKI`. - """ - - def signable(self): - """ - Return a new `NetscapeSPKI` for use with signing tests. - """ - return NetscapeSPKI() - - def test_type(self): - """ - `NetscapeSPKI` can be used to create instances of that type. - """ - assert is_consistent_type(NetscapeSPKI, "NetscapeSPKI") - - def test_construction(self): - """ - `NetscapeSPKI` returns an instance of `NetscapeSPKI`. - """ - nspki = NetscapeSPKI() - assert isinstance(nspki, NetscapeSPKI) - - def test_invalid_attribute(self): - """ - Accessing a non-existent attribute of a `NetscapeSPKI` instance - causes an `AttributeError` to be raised. - """ - nspki = NetscapeSPKI() - with pytest.raises(AttributeError): - nspki.foo - - def test_b64_encode(self): - """ - `NetscapeSPKI.b64_encode` encodes the certificate to a base64 blob. - """ - nspki = NetscapeSPKI() - pkey = load_privatekey(FILETYPE_PEM, root_key_pem) - nspki.set_pubkey(pkey) - nspki.sign(pkey, GOOD_DIGEST) - blob = nspki.b64_encode() - assert isinstance(blob, bytes) - - class TestRevoked: """ Tests for `OpenSSL.crypto.Revoked`.