Added support for OCSP AcceptableResponses extension (#8617)

fixes #8589
This commit is contained in:
Alex Gaynor 2023-03-26 20:51:04 -04:00 committed by GitHub
parent 55c13c3114
commit 89228a9deb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 164 additions and 5 deletions

View file

@ -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:

View file

@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~

View file

@ -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

View file

@ -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:

View file

@ -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",

View file

@ -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"<OCSPAcceptableResponses(responses={self._responses})>"
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

View file

@ -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?)?;

View file

@ -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),
}
},

View file

@ -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 =

View file

@ -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"),

View file

@ -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)
== "<OCSPAcceptableResponses(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("__"):