diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 903bf380..b7466fe9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -26,6 +26,7 @@ Backward-incompatible changes: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Removed the deprecated ``OpenSSL.crypto.CRL``, ``OpenSSL.crypto.Revoked``, ``OpenSSL.crypto.dump_crl``, and ``OpenSSL.crypto.load_crl``. ``cryptography.x509``'s CRL functionality should be used instead. +- Removed deprecated ``OpenSSL.crypto.X509Extension``, ``OpenSSL.crypto.X509Req.add_extension``, ``OpenSSL.crypto.X509Req.get_extensions``, ``OpenSSL.crypto.X509.add_extension``, ``OpenSSL.crypto.X509.get_extensions``. ``cryptography.x509`` should be used instead. - Removed the deprecated ``OpenSSL.crypto.sign`` and ``OpenSSL.crypto.verify``. ``cryptography.hazmat.primitives.asymmetric``'s signature APIs should be used instead. Deprecations: diff --git a/doc/api/crypto.rst b/doc/api/crypto.rst index eafd0fde..f84e5d0a 100644 --- a/doc/api/crypto.rst +++ b/doc/api/crypto.rst @@ -148,16 +148,6 @@ PKey objects Key type constants. -.. _openssl-509ext: - -X509Extension objects ---------------------- - -.. autoclass:: X509Extension - :members: - :special-members: - :exclude-members: __weakref__ - Exceptions ---------- diff --git a/src/OpenSSL/crypto.py b/src/OpenSSL/crypto.py index 366007e8..7ad431de 100644 --- a/src/OpenSSL/crypto.py +++ b/src/OpenSSL/crypto.py @@ -5,9 +5,8 @@ import functools import sys import typing -import warnings from base64 import b16encode -from collections.abc import Iterable, Sequence +from collections.abc import Sequence from functools import partial from typing import ( Any, @@ -63,7 +62,6 @@ def deprecated(msg: str, **kwargs: object) -> Callable[[_T], _T]: "X509", "Error", "PKey", - "X509Extension", "X509Name", "X509Req", "X509Store", @@ -776,180 +774,6 @@ def get_components(self) -> list[tuple[bytes, bytes]]: return result -@deprecated( - "X509Extension support in pyOpenSSL is deprecated. You should use the " - "APIs in cryptography." -) -class X509Extension: - """ - An X.509 v3 certificate extension. - - .. deprecated:: 23.3.0 - Use cryptography's X509 APIs instead. - """ - - def __init__( - self, - type_name: bytes, - critical: bool, - value: bytes, - subject: X509 | None = None, - issuer: X509 | None = None, - ) -> None: - """ - Initializes an X509 extension. - - :param type_name: The name of the type of extension_ to create. - :type type_name: :py:data:`bytes` - - :param bool critical: A flag indicating whether this is a critical - extension. - - :param value: The OpenSSL textual representation of the extension's - value. - :type value: :py:data:`bytes` - - :param subject: Optional X509 certificate to use as subject. - :type subject: :py:class:`X509` - - :param issuer: Optional X509 certificate to use as issuer. - :type issuer: :py:class:`X509` - - .. _extension: https://www.openssl.org/docs/manmaster/man5/ - x509v3_config.html#STANDARD-EXTENSIONS - """ - ctx = _ffi.new("X509V3_CTX*") - - # A context is necessary for any extension which uses the r2i - # conversion method. That is, X509V3_EXT_nconf may segfault if passed - # a NULL ctx. Start off by initializing most of the fields to NULL. - _lib.X509V3_set_ctx(ctx, _ffi.NULL, _ffi.NULL, _ffi.NULL, _ffi.NULL, 0) - - # We have no configuration database - but perhaps we should (some - # extensions may require it). - _lib.X509V3_set_ctx_nodb(ctx) - - # Initialize the subject and issuer, if appropriate. ctx is a local, - # and as far as I can tell none of the X509V3_* APIs invoked here steal - # any references, so no need to mess with reference counts or - # duplicates. - if issuer is not None: - if not isinstance(issuer, X509): - raise TypeError("issuer must be an X509 instance") - ctx.issuer_cert = issuer._x509 - if subject is not None: - if not isinstance(subject, X509): - raise TypeError("subject must be an X509 instance") - ctx.subject_cert = subject._x509 - - if critical: - # There are other OpenSSL APIs which would let us pass in critical - # separately, but they're harder to use, and since value is already - # a pile of crappy junk smuggling a ton of utterly important - # structured data, what's the point of trying to avoid nasty stuff - # with strings? (However, X509V3_EXT_i2d in particular seems like - # it would be a better API to invoke. I do not know where to get - # the ext_struc it desires for its last parameter, though.) - value = b"critical," + value - - extension = _lib.X509V3_EXT_nconf(_ffi.NULL, ctx, type_name, value) - if extension == _ffi.NULL: - _raise_current_error() - self._extension = _ffi.gc(extension, _lib.X509_EXTENSION_free) - - @property - def _nid(self) -> Any: - return _lib.OBJ_obj2nid( - _lib.X509_EXTENSION_get_object(self._extension) - ) - - _prefixes: typing.ClassVar[dict[int, str]] = { - _lib.GEN_EMAIL: "email", - _lib.GEN_DNS: "DNS", - _lib.GEN_URI: "URI", - } - - def _subjectAltNameString(self) -> str: - names = _ffi.cast( - "GENERAL_NAMES*", _lib.X509V3_EXT_d2i(self._extension) - ) - - names = _ffi.gc(names, _lib.GENERAL_NAMES_free) - parts = [] - for i in range(_lib.sk_GENERAL_NAME_num(names)): - name = _lib.sk_GENERAL_NAME_value(names, i) - try: - label = self._prefixes[name.type] - except KeyError: - bio = _new_mem_buf() - _lib.GENERAL_NAME_print(bio, name) - parts.append(_bio_to_string(bio).decode("utf-8")) - else: - value = _ffi.buffer(name.d.ia5.data, name.d.ia5.length)[ - : - ].decode("utf-8") - parts.append(label + ":" + value) - return ", ".join(parts) - - def __str__(self) -> str: - """ - :return: a nice text representation of the extension - """ - if _lib.NID_subject_alt_name == self._nid: - return self._subjectAltNameString() - - bio = _new_mem_buf() - print_result = _lib.X509V3_EXT_print(bio, self._extension, 0, 0) - _openssl_assert(print_result != 0) - - return _bio_to_string(bio).decode("utf-8") - - def get_critical(self) -> bool: - """ - Returns the critical field of this X.509 extension. - - :return: The critical field. - """ - return _lib.X509_EXTENSION_get_critical(self._extension) - - def get_short_name(self) -> bytes: - """ - Returns the short type name of this X.509 extension. - - The result is a byte string such as :py:const:`b"basicConstraints"`. - - :return: The short type name. - :rtype: :py:data:`bytes` - - .. versionadded:: 0.12 - """ - obj = _lib.X509_EXTENSION_get_object(self._extension) - nid = _lib.OBJ_obj2nid(obj) - # OpenSSL 3.1.0 has a bug where nid2sn returns NULL for NIDs that - # previously returned UNDEF. This is a workaround for that issue. - # https://github.com/openssl/openssl/commit/908ba3ed9adbb3df90f76 - buf = _lib.OBJ_nid2sn(nid) - if buf != _ffi.NULL: - return _ffi.string(buf) - else: - return b"UNDEF" - - def get_data(self) -> bytes: - """ - Returns the data of the X509 extension, encoded as ASN.1. - - :return: The ASN.1 encoded data of this X509 extension. - :rtype: :py:data:`bytes` - - .. versionadded:: 0.12 - """ - octet_result = _lib.X509_EXTENSION_get_data(self._extension) - string_result = _ffi.cast("ASN1_STRING*", octet_result) - char_result = _lib.ASN1_STRING_get0_data(string_result) - result_length = _lib.ASN1_STRING_length(string_result) - return _ffi.buffer(char_result, result_length)[:] - - @deprecated( "CSR support in pyOpenSSL is deprecated. You should use the APIs " "in cryptography." @@ -1079,77 +903,6 @@ def get_subject(self) -> X509Name: return name - def add_extensions(self, extensions: Iterable[X509Extension]) -> None: - """ - Add extensions to the certificate signing request. - - :param extensions: The X.509 extensions to add. - :type extensions: iterable of :py:class:`X509Extension` - :return: ``None`` - """ - warnings.warn( - ( - "This API is deprecated and will be removed in a future " - "version of pyOpenSSL. You should use pyca/cryptography's " - "X.509 APIs instead." - ), - DeprecationWarning, - stacklevel=2, - ) - - stack = _lib.sk_X509_EXTENSION_new_null() - _openssl_assert(stack != _ffi.NULL) - - stack = _ffi.gc(stack, _lib.sk_X509_EXTENSION_free) - - for ext in extensions: - if not isinstance(ext, X509Extension): - raise ValueError("One of the elements is not an X509Extension") - - # TODO push can fail (here and elsewhere) - _lib.sk_X509_EXTENSION_push(stack, ext._extension) - - add_result = _lib.X509_REQ_add_extensions(self._req, stack) - _openssl_assert(add_result == 1) - - def get_extensions(self) -> list[X509Extension]: - """ - Get X.509 extensions in the certificate signing request. - - :return: The X.509 extensions in this request. - :rtype: :py:class:`list` of :py:class:`X509Extension` objects. - - .. versionadded:: 0.15 - """ - warnings.warn( - ( - "This API is deprecated and will be removed in a future " - "version of pyOpenSSL. You should use pyca/cryptography's " - "X.509 APIs instead." - ), - DeprecationWarning, - stacklevel=2, - ) - - exts = [] - native_exts_obj = _lib.X509_REQ_get_extensions(self._req) - native_exts_obj = _ffi.gc( - native_exts_obj, - lambda x: _lib.sk_X509_EXTENSION_pop_free( - x, - _ffi.addressof(_lib._original_lib, "X509_EXTENSION_free"), - ), - ) - - for i in range(_lib.sk_X509_EXTENSION_num(native_exts_obj)): - ext = X509Extension.__new__(X509Extension) - extension = _lib.X509_EXTENSION_dup( - _lib.sk_X509_EXTENSION_value(native_exts_obj, i) - ) - ext._extension = _ffi.gc(extension, _lib.X509_EXTENSION_free) - exts.append(ext) - return exts - def sign(self, pkey: PKey, digest: str) -> None: """ Sign the certificate signing request with this key and digest type. @@ -1627,64 +1380,6 @@ def get_extension_count(self) -> int: """ return _lib.X509_get_ext_count(self._x509) - def add_extensions(self, extensions: Iterable[X509Extension]) -> None: - """ - Add extensions to the certificate. - - :param extensions: The extensions to add. - :type extensions: An iterable of :py:class:`X509Extension` objects. - :return: ``None`` - """ - warnings.warn( - ( - "This API is deprecated and will be removed in a future " - "version of pyOpenSSL. You should use pyca/cryptography's " - "X.509 APIs instead." - ), - DeprecationWarning, - stacklevel=2, - ) - - for ext in extensions: - if not isinstance(ext, X509Extension): - raise ValueError("One of the elements is not an X509Extension") - - add_result = _lib.X509_add_ext(self._x509, ext._extension, -1) - _openssl_assert(add_result == 1) - - def get_extension(self, index: int) -> X509Extension: - """ - Get a specific extension of the certificate by index. - - Extensions on a certificate are kept in order. The index - parameter selects which extension will be returned. - - :param int index: The index of the extension to retrieve. - :return: The extension at the specified index. - :rtype: :py:class:`X509Extension` - :raises IndexError: If the extension index was out of bounds. - - .. versionadded:: 0.12 - """ - warnings.warn( - ( - "This API is deprecated and will be removed in a future " - "version of pyOpenSSL. You should use pyca/cryptography's " - "X.509 APIs instead." - ), - DeprecationWarning, - stacklevel=2, - ) - - ext = X509Extension.__new__(X509Extension) - ext._extension = _lib.X509_get_ext(self._x509, index) - if ext._extension == _ffi.NULL: - raise IndexError("extension index out of bounds") - - extension = _lib.X509_EXTENSION_dup(ext._extension) - ext._extension = _ffi.gc(extension, _lib.X509_EXTENSION_free) - return ext - class X509StoreFlags: """ diff --git a/tests/test_crypto.py b/tests/test_crypto.py index 7b07b441..b9aa7f2a 100644 --- a/tests/test_crypto.py +++ b/tests/test_crypto.py @@ -36,7 +36,6 @@ X509, Error, PKey, - X509Extension, X509Name, X509Req, X509Store, @@ -875,198 +874,6 @@ def x509_data() -> tuple[PKey, X509]: return pkey, x509 -class TestX509Ext: - """ - Tests for `OpenSSL.crypto.X509Extension`. - """ - - def test_str(self) -> None: - """ - The string representation of `X509Extension` instances as - returned by `str` includes stuff. - """ - # This isn't necessarily the best string representation. Perhaps it - # will be changed/improved in the future. - assert ( - str(X509Extension(b"basicConstraints", True, b"CA:false")) - == "CA:FALSE" - ) - - def test_construction(self) -> None: - """ - `X509Extension` accepts an extension type name, a critical flag, - and an extension value and returns an `X509Extension` instance. - """ - basic = X509Extension(b"basicConstraints", True, b"CA:true") - assert isinstance(basic, X509Extension) - - comment = X509Extension(b"nsComment", False, b"pyOpenSSL unit test") - assert isinstance(comment, X509Extension) - - @pytest.mark.parametrize( - "type_name, critical, value", - [ - (b"thisIsMadeUp", False, b"hi"), - (b"basicConstraints", False, b"blah blah"), - # Exercise a weird one (an extension which uses the r2i method). - # This exercises the codepath that requires a non-NULL ctx to be - # passed to X509V3_EXT_nconf. It can't work now because we provide - # no configuration database. It might be made to work in the - # future. - ( - b"proxyCertInfo", - True, - b"language:id-ppl-anyLanguage,pathlen:1,policy:text:AB", - ), - ], - ) - def test_invalid_extension( - self, type_name: bytes, critical: bool, value: bytes - ) -> None: - """ - `X509Extension` raises something if it is passed a bad - extension name or value. - """ - with pytest.raises(Error): - X509Extension(type_name, critical, value) - - @pytest.mark.parametrize("critical_flag", [True, False]) - def test_get_critical(self, critical_flag: bool) -> None: - """ - `X509ExtensionType.get_critical` returns the value of the - extension's critical flag. - """ - ext = X509Extension(b"basicConstraints", critical_flag, b"CA:true") - assert ext.get_critical() == critical_flag - - @pytest.mark.parametrize( - "short_name, value", - [(b"basicConstraints", b"CA:true"), (b"nsComment", b"foo bar")], - ) - def test_get_short_name(self, short_name: bytes, value: bytes) -> None: - """ - `X509ExtensionType.get_short_name` returns a string giving the - short type name of the extension. - """ - ext = X509Extension(short_name, True, value) - assert ext.get_short_name() == short_name - - def test_get_data(self) -> None: - """ - `X509Extension.get_data` returns a string giving the data of - the extension. - """ - ext = X509Extension(b"basicConstraints", True, b"CA:true") - # Expect to get back the DER encoded form of CA:true. - assert ext.get_data() == b"0\x03\x01\x01\xff" - - def test_unused_subject(self, x509_data: tuple[PKey, X509]) -> None: - """ - The `subject` parameter to `X509Extension` may be provided for an - extension which does not use it and is ignored in this case. - """ - pkey, x509 = x509_data - ext1 = X509Extension( - b"basicConstraints", False, b"CA:TRUE", subject=x509 - ) - x509.add_extensions([ext1]) - x509.sign(pkey, "sha256") - # This is a little lame. Can we think of a better way? - text = dump_certificate(FILETYPE_TEXT, x509) - assert b"X509v3 Basic Constraints:" in text - assert b"CA:TRUE" in text - - def test_subject(self, x509_data: tuple[PKey, X509]) -> None: - """ - If an extension requires a subject, the `subject` parameter to - `X509Extension` provides its value. - """ - pkey, x509 = x509_data - ext3 = X509Extension( - b"subjectKeyIdentifier", False, b"hash", subject=x509 - ) - x509.add_extensions([ext3]) - x509.sign(pkey, "sha256") - text = dump_certificate(FILETYPE_TEXT, x509) - assert b"X509v3 Subject Key Identifier:" in text - - def test_missing_subject(self) -> None: - """ - If an extension requires a subject and the `subject` parameter - is given no value, something happens. - """ - with pytest.raises(Error): - X509Extension(b"subjectKeyIdentifier", False, b"hash") - - @pytest.mark.parametrize("bad_obj", [True, object(), "hello", []]) - def test_invalid_subject(self, bad_obj: object) -> None: - """ - If the `subject` parameter is given a value which is not an - `X509` instance, `TypeError` is raised. - """ - with pytest.raises(TypeError): - X509Extension( - b"basicConstraints", - False, - b"CA:TRUE", - subject=bad_obj, # type: ignore[arg-type] - ) - - def test_unused_issuer(self, x509_data: tuple[PKey, X509]) -> None: - """ - The `issuer` parameter to `X509Extension` may be provided for an - extension which does not use it and is ignored in this case. - """ - pkey, x509 = x509_data - ext1 = X509Extension( - b"basicConstraints", False, b"CA:TRUE", issuer=x509 - ) - x509.add_extensions([ext1]) - x509.sign(pkey, "sha256") - text = dump_certificate(FILETYPE_TEXT, x509) - assert b"X509v3 Basic Constraints:" in text - assert b"CA:TRUE" in text - - def test_issuer(self, x509_data: tuple[PKey, X509]) -> None: - """ - If an extension requires an issuer, the `issuer` parameter to - `X509Extension` provides its value. - """ - pkey, x509 = x509_data - ext2 = X509Extension( - b"authorityKeyIdentifier", False, b"issuer:always", issuer=x509 - ) - x509.add_extensions([ext2]) - x509.sign(pkey, "sha256") - text = dump_certificate(FILETYPE_TEXT, x509) - assert b"X509v3 Authority Key Identifier:" in text - assert b"DirName:/CN=Yoda root CA" in text - - def test_missing_issuer(self) -> None: - """ - If an extension requires an issue and the `issuer` parameter is - given no value, something happens. - """ - with pytest.raises(Error): - X509Extension( - b"authorityKeyIdentifier", False, b"keyid:always,issuer:always" - ) - - @pytest.mark.parametrize("bad_obj", [True, object(), "hello", []]) - def test_invalid_issuer(self, bad_obj: object) -> None: - """ - If the `issuer` parameter is given a value which is not an - `X509` instance, `TypeError` is raised. - """ - with pytest.raises(TypeError): - X509Extension( - b"basicConstraints", - False, - b"keyid:always,issuer:always", - issuer=bad_obj, # type: ignore[arg-type] - ) - - class TestPKey: """ Tests for `OpenSSL.crypto.PKey`. @@ -1704,67 +1511,6 @@ def test_get_subject(self) -> None: subject.commonName = "bar" assert subject.commonName == "bar" - def test_add_extensions(self) -> None: - """ - `X509Req.add_extensions` accepts a `list` of `X509Extension` instances - and adds them to the X509 request. - """ - request = X509Req() - request.add_extensions( - [X509Extension(b"basicConstraints", True, b"CA:false")] - ) - exts = request.get_extensions() - assert len(exts) == 1 - assert exts[0].get_short_name() == b"basicConstraints" - assert exts[0].get_critical() == 1 - assert exts[0].get_data() == b"0\x00" - - def test_get_extensions(self) -> None: - """ - `X509Req.get_extensions` returns a `list` of extensions added to this - X509 request. - """ - request = X509Req() - exts = request.get_extensions() - assert exts == [] - request.add_extensions( - [ - X509Extension(b"basicConstraints", True, b"CA:true"), - X509Extension(b"keyUsage", False, b"digitalSignature"), - ] - ) - exts = request.get_extensions() - assert len(exts) == 2 - assert exts[0].get_short_name() == b"basicConstraints" - assert exts[0].get_critical() == 1 - assert exts[0].get_data() == b"0\x03\x01\x01\xff" - assert exts[1].get_short_name() == b"keyUsage" - assert exts[1].get_critical() == 0 - assert exts[1].get_data() == b"\x03\x02\x07\x80" - # Requesting it a second time should return the same list - exts = request.get_extensions() - assert len(exts) == 2 - - def test_undef_oid(self) -> None: - assert ( - X509Extension( - b"1.2.3.4.5.6.7", False, b"DER:05:00" - ).get_short_name() - == b"UNDEF" - ) - - def test_add_extensions_wrong_args(self) -> None: - """ - `X509Req.add_extensions` raises `TypeError` if called with a - non-`list`. Or it raises `ValueError` if called with a `list` - containing objects other than `X509Extension` instances. - """ - request = X509Req() - with pytest.raises(TypeError): - request.add_extensions(object()) # type: ignore[arg-type] - with pytest.raises(ValueError): - request.add_extensions([object()]) # type: ignore[list-item] - def test_verify_wrong_args(self) -> None: """ `X509Req.verify` raises `TypeError` if passed anything other than a @@ -2161,71 +1907,6 @@ def test_extension_count(self) -> None: c = self._extcert(pkey, [ca, key, san]) assert c.get_extension_count() == 3 - def test_get_extension(self) -> None: - """ - `X509.get_extension` takes an integer and returns an - `X509Extension` corresponding to the extension at that index. - """ - pkey = load_privatekey( - FILETYPE_PEM, client_key_pem - ).to_cryptography_key() - assert isinstance(pkey, rsa.RSAPrivateKey) - ca = x509.BasicConstraints(ca=False, path_length=None) - key = x509.KeyUsage( - digital_signature=True, - content_commitment=False, - key_encipherment=False, - data_encipherment=False, - key_agreement=False, - key_cert_sign=False, - crl_sign=False, - encipher_only=False, - decipher_only=False, - ) - san = x509.SubjectAlternativeName([x509.DNSName("example.com")]) - - cert = self._extcert(pkey, [ca, key, san]) - - ext = cert.get_extension(0) - assert isinstance(ext, X509Extension) - assert ext.get_critical() - assert ext.get_short_name() == b"basicConstraints" - - ext = cert.get_extension(1) - assert isinstance(ext, X509Extension) - assert not ext.get_critical() - assert ext.get_short_name() == b"keyUsage" - - ext = cert.get_extension(2) - assert isinstance(ext, X509Extension) - assert ext.get_critical() - assert ext.get_short_name() == b"subjectAltName" - - with pytest.raises(IndexError): - cert.get_extension(-1) - with pytest.raises(IndexError): - cert.get_extension(4) - with pytest.raises(TypeError): - cert.get_extension("hello") # type: ignore[arg-type] - - def test_nullbyte_subjectAltName(self) -> None: - """ - The fields of a `subjectAltName` extension on an X509 may contain NUL - bytes and this value is reflected in the string representation of the - extension object. - """ - cert = load_certificate(FILETYPE_PEM, nulbyteSubjectAltNamePEM) - - ext = cert.get_extension(3) - assert ext.get_short_name() == b"subjectAltName" - assert ( - b"DNS:altnull.python.org\x00example.com, " - b"email:null@python.org\x00user@example.org, " - b"URI:http://null.python.org\x00http://example.org, " - b"IP Address:192.0.2.1, IP Address:2001:DB8:0:0:0:0:0:1" - == str(ext).encode("ascii").strip() - ) - def test_invalid_digest_algorithm(self) -> None: """ `X509.digest` raises `ValueError` if called with an unrecognized hash