diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a0b5bf719..ec7a3db5d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,8 @@ Changelog removed. Users on older version of OpenSSL will need to upgrade. * **BACKWARDS INCOMPATIBLE:** Support for Python 3.6 has been removed. * Updated the minimum supported Rust version (MSRV) to 1.56.0, from 1.48.0. +* Added support for the :class:`~cryptography.x509.OCSPAcceptableResponses` + OCSP extension. .. _v40-0-1: diff --git a/docs/development/test-vectors.rst b/docs/development/test-vectors.rst index 72fdf7fab..2cb822306 100644 --- a/docs/development/test-vectors.rst +++ b/docs/development/test-vectors.rst @@ -658,8 +658,10 @@ Custom X.509 OCSP Test Vectors extensions. * ``x509/ocsp/resp-unknown-extension.der`` - An OCSP response containing an extension with an unknown OID. -* ``x509/ocsp/resp-unknown-hash-alg.der`` - AN OCSP response containing an +* ``x509/ocsp/resp-unknown-hash-alg.der`` - An OCSP response containing an invalid hash algorithm OID. +* ``x509/ocsp/req-acceptable-responses.der`` - An OCSP request containing an + acceptable responses extension. Custom PKCS12 Test Vectors ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/x509/reference.rst b/docs/x509/reference.rst index 12ac440cb..d0f864b56 100644 --- a/docs/x509/reference.rst +++ b/docs/x509/reference.rst @@ -2874,6 +2874,29 @@ OCSP Extensions :type: bytes +.. class:: OCSPAcceptableResponses(response) + :canonical: cryptography.x509.extensions.OCSPAcceptableResponses + + .. versionadded:: 41.0.0 + + OCSP acceptable responses is an extension that is only valid inside + :class:`~cryptography.x509.ocsp.OCSPRequest` objects. This allows an OCSP + client to tell the server what types of responses it supports. In practice + this is rarely used, because there is only one kind of OCSP response in + wide use. + + .. attribute:: oid + + :type: :class:`ObjectIdentifier` + + Returns + :attr:`~cryptography.x509.oid.OCSPExtensionOID.ACCEPTABLE_RESPONSES`. + + .. attribute:: nonce + + :type: bytes + + X.509 Request Attributes ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -3509,6 +3532,12 @@ instances. The following common OIDs are available as constants. Corresponds to the dotted string ``"1.3.6.1.5.5.7.48.1.2"``. + .. attribute:: ACCEPTABLE_RESPONSES + + .. versionadded:: 41.0.0 + + Corresponds to the dotted string ``"1.3.6.1.5.5.7.48.1.4"``. + .. class:: AttributeOID :canonical: cryptography.hazmat._oid.AttributeOID diff --git a/src/cryptography/hazmat/_oid.py b/src/cryptography/hazmat/_oid.py index 927ffc4c5..bc9c046c6 100644 --- a/src/cryptography/hazmat/_oid.py +++ b/src/cryptography/hazmat/_oid.py @@ -42,6 +42,7 @@ class ExtensionOID: class OCSPExtensionOID: NONCE = ObjectIdentifier("1.3.6.1.5.5.7.48.1.2") + ACCEPTABLE_RESPONSES = ObjectIdentifier("1.3.6.1.5.5.7.48.1.4") class CRLEntryExtensionOID: diff --git a/src/cryptography/x509/__init__.py b/src/cryptography/x509/__init__.py index ad924ad42..df7fd3fbb 100644 --- a/src/cryptography/x509/__init__.py +++ b/src/cryptography/x509/__init__.py @@ -54,6 +54,7 @@ from cryptography.x509.extensions import ( KeyUsage, NameConstraints, NoticeReference, + OCSPAcceptableResponses, OCSPNoCheck, OCSPNonce, PolicyConstraints, @@ -196,6 +197,7 @@ __all__ = [ "IssuingDistributionPoint", "TLSFeature", "TLSFeatureType", + "OCSPAcceptableResponses", "OCSPNoCheck", "BasicConstraints", "CRLNumber", diff --git a/src/cryptography/x509/extensions.py b/src/cryptography/x509/extensions.py index 551887b4a..6fe3888bf 100644 --- a/src/cryptography/x509/extensions.py +++ b/src/cryptography/x509/extensions.py @@ -1932,6 +1932,35 @@ class OCSPNonce(ExtensionType): return rust_x509.encode_extension_value(self) +class OCSPAcceptableResponses(ExtensionType): + oid = OCSPExtensionOID.ACCEPTABLE_RESPONSES + + def __init__(self, responses: typing.Iterable[ObjectIdentifier]) -> None: + responses = list(responses) + if any(not isinstance(r, ObjectIdentifier) for r in responses): + raise TypeError("All responses must be ObjectIdentifiers") + + self._responses = responses + + def __eq__(self, other: object) -> bool: + if not isinstance(other, OCSPAcceptableResponses): + return NotImplemented + + return self._responses == other._responses + + def __hash__(self) -> int: + return hash(tuple(self._responses)) + + def __repr__(self) -> str: + return f"" + + def __iter__(self) -> typing.Iterator[ObjectIdentifier]: + return iter(self._responses) + + def public_bytes(self) -> bytes: + return rust_x509.encode_extension_value(self) + + class IssuingDistributionPoint(ExtensionType): oid = ExtensionOID.ISSUING_DISTRIBUTION_POINT diff --git a/src/rust/src/x509/extensions.rs b/src/rust/src/x509/extensions.rs index d5473a576..79170a616 100644 --- a/src/rust/src/x509/extensions.rs +++ b/src/rust/src/x509/extensions.rs @@ -202,7 +202,7 @@ pub(crate) fn encode_extension( let ads = x509::common::encode_access_descriptions(ext.py(), ext)?; Ok(Some(asn1::write_single(&ads)?)) } - &oid::EXTENDED_KEY_USAGE_OID => { + &oid::EXTENDED_KEY_USAGE_OID | &oid::ACCEPTABLE_RESPONSES_OID => { let mut oids = vec![]; for el in ext.iter()? { let oid = py_oid_to_oid(el?)?; diff --git a/src/rust/src/x509/ocsp_req.rs b/src/rust/src/x509/ocsp_req.rs index b239869d9..47810a023 100644 --- a/src/rust/src/x509/ocsp_req.rs +++ b/src/rust/src/x509/ocsp_req.rs @@ -2,7 +2,7 @@ // 2.0, and the BSD License. See the LICENSE file in the root of this repository // for complete details. -use crate::asn1::{big_byte_slice_to_py_int, py_uint_to_big_endian_bytes}; +use crate::asn1::{big_byte_slice_to_py_int, oid_to_py_oid, py_uint_to_big_endian_bytes}; use crate::error::{CryptographyError, CryptographyResult}; use crate::x509; use crate::x509::{extensions, ocsp, oid}; @@ -118,8 +118,8 @@ impl OCSPRequest { &mut self.cached_extensions, &self.raw.borrow_value().tbs_request.request_extensions, |oid, value| { - match oid { - &oid::NONCE_OID => { + match *oid { + oid::NONCE_OID => { // This is a disaster. RFC 2560 says that the contents of the nonce is // just the raw extension value. This is nonsense, since they're always // supposed to be ASN.1 TLVs. RFC 6960 correctly specifies that the @@ -129,6 +129,19 @@ impl OCSPRequest { let nonce = asn1::parse_single::<&[u8]>(value).unwrap_or(value); Ok(Some(x509_module.call_method1("OCSPNonce", (nonce,))?)) } + oid::ACCEPTABLE_RESPONSES_OID => { + let oids = asn1::parse_single::< + asn1::SequenceOf<'_, asn1::ObjectIdentifier>, + >(value)?; + let py_oids = pyo3::types::PyList::empty(py); + for oid in oids { + py_oids.append(oid_to_py_oid(py, &oid)?)?; + } + + Ok(Some( + x509_module.call_method1("OCSPAcceptableResponses", (py_oids,))?, + )) + } _ => Ok(None), } }, diff --git a/src/rust/src/x509/oid.rs b/src/rust/src/x509/oid.rs index 55477c608..2c9b36d0a 100644 --- a/src/rust/src/x509/oid.rs +++ b/src/rust/src/x509/oid.rs @@ -41,6 +41,8 @@ pub(crate) const POLICY_CONSTRAINTS_OID: asn1::ObjectIdentifier = asn1::oid!(2, pub(crate) const EXTENDED_KEY_USAGE_OID: asn1::ObjectIdentifier = asn1::oid!(2, 5, 29, 37); pub(crate) const FRESHEST_CRL_OID: asn1::ObjectIdentifier = asn1::oid!(2, 5, 29, 46); pub(crate) const INHIBIT_ANY_POLICY_OID: asn1::ObjectIdentifier = asn1::oid!(2, 5, 29, 54); +pub(crate) const ACCEPTABLE_RESPONSES_OID: asn1::ObjectIdentifier = + asn1::oid!(1, 3, 6, 1, 5, 5, 7, 48, 1, 4); // Signing methods pub(crate) const ECDSA_WITH_SHA224_OID: asn1::ObjectIdentifier = diff --git a/tests/x509/test_ocsp.py b/tests/x509/test_ocsp.py index fd8bbfc1b..2c595db32 100644 --- a/tests/x509/test_ocsp.py +++ b/tests/x509/test_ocsp.py @@ -102,6 +102,18 @@ class TestOCSPRequest: b"{\x80Z\x1d7&\xb8\xb8OH\xd2\xf8\xbf\xd7-\xfd" ) + def test_load_request_with_acceptable_responses(self): + req = _load_data( + os.path.join("x509", "ocsp", "req-acceptable-responses.der"), + ocsp.load_der_ocsp_request, + ) + assert len(req.extensions) == 1 + ext = req.extensions[0] + assert ext.critical is False + assert ext.value == x509.OCSPAcceptableResponses( + [x509.ObjectIdentifier("1.3.6.1.5.5.7.48.1.1")] + ) + def test_load_request_with_unknown_extension(self): req = _load_data( os.path.join("x509", "ocsp", "req-ext-unknown-oid.der"), diff --git a/tests/x509/test_x509_ext.py b/tests/x509/test_x509_ext.py index a4f0f0f8b..d11ba3db0 100644 --- a/tests/x509/test_x509_ext.py +++ b/tests/x509/test_x509_ext.py @@ -6132,6 +6132,73 @@ class TestOCSPNonce: assert ext.public_bytes() == b"\x04\x0500000" +class TestOCSPAcceptableResponses: + def test_invalid_types(self): + with pytest.raises(TypeError): + x509.OCSPAcceptableResponses(38) # type:ignore[arg-type] + with pytest.raises(TypeError): + x509.OCSPAcceptableResponses([38]) # type:ignore[list-item] + + def test_eq(self): + acceptable_responses1 = x509.OCSPAcceptableResponses( + [ObjectIdentifier("1.2.3")] + ) + acceptable_responses2 = x509.OCSPAcceptableResponses( + [ObjectIdentifier("1.2.3")] + ) + assert acceptable_responses1 == acceptable_responses2 + + def test_ne(self): + acceptable_responses1 = x509.OCSPAcceptableResponses( + [ObjectIdentifier("1.2.3")] + ) + acceptable_responses2 = x509.OCSPAcceptableResponses( + [ObjectIdentifier("1.2.4")] + ) + assert acceptable_responses1 != acceptable_responses2 + assert acceptable_responses1 != object() + + def test_repr(self): + acceptable_responses = x509.OCSPAcceptableResponses([]) + assert ( + repr(acceptable_responses) + == "" + ) + + def test_hash(self): + acceptable_responses1 = x509.OCSPAcceptableResponses( + [ObjectIdentifier("1.2.3")] + ) + acceptable_responses2 = x509.OCSPAcceptableResponses( + [ObjectIdentifier("1.2.3")] + ) + acceptable_responses3 = x509.OCSPAcceptableResponses( + [ObjectIdentifier("1.2.4")] + ) + + assert hash(acceptable_responses1) == hash(acceptable_responses2) + assert hash(acceptable_responses1) != hash(acceptable_responses3) + + def test_iter(self): + acceptable_responses1 = x509.OCSPAcceptableResponses( + [ObjectIdentifier("1.2.3")] + ) + + assert list(acceptable_responses1) == [ObjectIdentifier("1.2.3")] + + def test_public_bytes(self): + ext = x509.OCSPAcceptableResponses([]) + assert ext.public_bytes() == b"\x30\x00" + + ext = x509.OCSPAcceptableResponses( + [ObjectIdentifier("1.3.6.1.5.5.7.48.1.1")] + ) + assert ( + ext.public_bytes() + == b"\x30\x0b\x06\t+\x06\x01\x05\x05\x07\x30\x01\x01" + ) + + def test_all_extension_oid_members_have_names_defined(): for oid in dir(ExtensionOID): if oid.startswith("__"): diff --git a/vectors/cryptography_vectors/x509/ocsp/req-acceptable-responses.der b/vectors/cryptography_vectors/x509/ocsp/req-acceptable-responses.der new file mode 100644 index 000000000..0afa906d2 Binary files /dev/null and b/vectors/cryptography_vectors/x509/ocsp/req-acceptable-responses.der differ