Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OCSP Response Builder APIs to add certificates by hash #11955

Open
jackenbaer opened this issue Nov 15, 2024 · 5 comments
Open

Add OCSP Response Builder APIs to add certificates by hash #11955

jackenbaer opened this issue Nov 15, 2024 · 5 comments

Comments

@jackenbaer
Copy link

Creating an OCSP Response seems to require unnecessary verbose objects "issuer" and "cert" in add_response.
I guess the Serial Number, IssuerNameHash and IssuerKeyHash would be enough.
This change would simplify the implementation of OCSP responders and align better with scenarios where responders operate based on limited certificate information.

@alex
Copy link
Member

alex commented Nov 15, 2024

We previously added https://cryptography.io/en/latest/x509/ocsp/#cryptography.x509.ocsp.OCSPRequestBuilder.add_certificate_by_hash on OCSP requests. We'd be ok adding an analogous API on responses.

Are you interested in submitting a PR?

@jackenbaer
Copy link
Author

Ah, yes ! Exactly this for responses.
Yes, I will look into this !

@alex alex changed the title OCSP Response Builder Add OCSP Response Builder APIs to add certificates by hash Jan 31, 2025
@aschaubamd
Copy link

I wanted to follow up on this. I would also be interested in this change. Has any progress been made on this request?

@alex
Copy link
Member

alex commented Feb 18, 2025

Not to my knowledge. We're still happy to take a PR for it.

@alex
Copy link
Member

alex commented Mar 15, 2025

I let an LLM spin on this for a while, it just its mind repeatedly and so the implementation isn't very useful. But the tests and docs might be useful for whoever takes this on.

diff --git a/docs/x509/ocsp.rst b/docs/x509/ocsp.rst
index beaa3537c..409f48bac 100644
--- a/docs/x509/ocsp.rst
+++ b/docs/x509/ocsp.rst
@@ -285,6 +285,56 @@ Creating Responses
             :class:`~cryptography.x509.ReasonFlags` enumeration or ``None`` if
             the ``cert`` is not revoked.
 
+    .. method:: add_response_by_hash(issuer_name_hash, issuer_key_hash, serial_number, algorithm, cert_status, this_update, next_update, revocation_time, revocation_reason)
+
+        .. versionadded:: 43.0.0
+
+        Like :meth:`~cryptography.x509.ocsp.OCSPResponseBuilder.add_response`,
+        but takes the hash of the issuer's name and key instead of requiring
+        the full certificates. This is useful when generating OCSP responses
+        when you don't have access to the full certificate objects, but you do
+        have the relevant hashes.
+
+        :param issuer_name_hash: The hash of the issuer's DER encoded name using
+            the same hash algorithm as the one specified in the ``algorithm`` parameter.
+        :type issuer_name_hash: bytes
+
+        :param issuer_key_hash: The hash of the issuer's public key bit string
+            DER encoding using the same hash algorithm as the one specified in
+            the ``algorithm`` parameter.
+        :type issuer_key_hash: bytes
+
+        :param serial_number: The serial number of the certificate being checked.
+        :type serial_number: int
+
+        :param algorithm: A
+            :class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm`
+            instance. For OCSP only
+            :class:`~cryptography.hazmat.primitives.hashes.SHA1`,
+            :class:`~cryptography.hazmat.primitives.hashes.SHA224`,
+            :class:`~cryptography.hazmat.primitives.hashes.SHA256`,
+            :class:`~cryptography.hazmat.primitives.hashes.SHA384`, and
+            :class:`~cryptography.hazmat.primitives.hashes.SHA512` are allowed.
+
+        :param cert_status: An item from the
+            :class:`~cryptography.x509.ocsp.OCSPCertStatus` enumeration.
+
+        :param this_update: A naïve :class:`datetime.datetime` object
+            representing the most recent time in UTC at which the status being
+            indicated is known by the responder to be correct.
+
+        :param next_update: A naïve :class:`datetime.datetime` object or
+            ``None``. The time in UTC at or before which newer information will
+            be available about the status of the certificate.
+
+        :param revocation_time: A naïve :class:`datetime.datetime` object or
+            ``None`` if the certificate is not revoked. The time in UTC at which
+            the certificate was revoked.
+
+        :param revocation_reason: An item from the
+            :class:`~cryptography.x509.ReasonFlags` enumeration or ``None`` if
+            the certificate is not revoked.
+
     .. method:: certificates(certs)
 
         Add additional certificates that should be used to verify the
diff --git a/src/cryptography/x509/ocsp.py b/src/cryptography/x509/ocsp.py
index 513db6e1e..eb7811f33 100644
--- a/src/cryptography/x509/ocsp.py
+++ b/src/cryptography/x509/ocsp.py
@@ -59,31 +59,80 @@ class OCSPCertStatus(utils.Enum):
 class _SingleResponse:
     def __init__(
         self,
-        cert: x509.Certificate,
-        issuer: x509.Certificate,
-        algorithm: hashes.HashAlgorithm,
-        cert_status: OCSPCertStatus,
-        this_update: datetime.datetime,
-        next_update: datetime.datetime | None,
-        revocation_time: datetime.datetime | None,
-        revocation_reason: x509.ReasonFlags | None,
+        cert: x509.Certificate | None = None,
+        issuer: x509.Certificate | None = None,
+        algorithm: hashes.HashAlgorithm | None = None,
+        cert_status: OCSPCertStatus | None = None,
+        this_update: datetime.datetime | None = None,
+        next_update: datetime.datetime | None = None,
+        revocation_time: datetime.datetime | None = None,
+        revocation_reason: x509.ReasonFlags | None = None,
+        issuer_name_hash: bytes | None = None,
+        issuer_key_hash: bytes | None = None,
+        serial_number: int | None = None,
     ):
-        if not isinstance(cert, x509.Certificate) or not isinstance(
-            issuer, x509.Certificate
+        self._using_hashes = False
+
+        # Hash-based initialization
+        if (
+            issuer_name_hash is not None
+            and issuer_key_hash is not None
+            and serial_number is not None
         ):
-            raise TypeError("cert and issuer must be a Certificate")
+            if cert is not None or issuer is not None:
+                raise ValueError("Cannot provide both certificates and hashes")
+
+            self._using_hashes = True
+            self._issuer_name_hash = issuer_name_hash
+            self._issuer_key_hash = issuer_key_hash
+            self._serial_number = serial_number
+
+            if algorithm is None:
+                raise ValueError(
+                    "Algorithm must be provided when using hashes"
+                )
+
+            _verify_algorithm(algorithm)
+            self._algorithm = algorithm
+
+        # Certificate-based initialization
+        elif cert is not None and issuer is not None:
+            if not isinstance(cert, x509.Certificate) or not isinstance(
+                issuer, x509.Certificate
+            ):
+                raise TypeError("cert and issuer must be a Certificate")
+
+            if algorithm is None:
+                raise ValueError(
+                    "Algorithm must be provided when using certificates"
+                )
+
+            _verify_algorithm(algorithm)
+
+            self._cert = cert
+            self._issuer = issuer
+            self._algorithm = algorithm
+        else:
+            raise ValueError(
+                "Must provide either (cert, issuer) or "
+                "(issuer_name_hash, issuer_key_hash, serial_number)"
+            )
+
+        # Common validation
+        if cert_status is None:
+            raise ValueError("cert_status must be provided")
+
+        if this_update is None:
+            raise ValueError("this_update must be provided")
 
-        _verify_algorithm(algorithm)
         if not isinstance(this_update, datetime.datetime):
             raise TypeError("this_update must be a datetime object")
+
         if next_update is not None and not isinstance(
             next_update, datetime.datetime
         ):
             raise TypeError("next_update must be a datetime object or None")
 
-        self._cert = cert
-        self._issuer = issuer
-        self._algorithm = algorithm
         self._this_update = this_update
         self._next_update = next_update
 
@@ -91,6 +140,7 @@ class _SingleResponse:
             raise TypeError(
                 "cert_status must be an item from the OCSPCertStatus enum"
             )
+
         if cert_status is not OCSPCertStatus.REVOKED:
             if revocation_time is not None:
                 raise ValueError(
@@ -125,6 +175,9 @@ class _SingleResponse:
         self._revocation_reason = revocation_reason
 
 
+# No longer needed since we're using threading.local
+
+
 OCSPRequest = ocsp.OCSPRequest
 OCSPResponse = ocsp.OCSPResponse
 OCSPSingleResponse = ocsp.OCSPSingleResponse
@@ -259,6 +312,99 @@ class OCSPResponseBuilder:
             self._extensions,
         )
 
+    def add_response_by_hash(
+        self,
+        issuer_name_hash: bytes,
+        issuer_key_hash: bytes,
+        serial_number: int,
+        algorithm: hashes.HashAlgorithm,
+        cert_status: OCSPCertStatus,
+        this_update: datetime.datetime,
+        next_update: datetime.datetime | None,
+        revocation_time: datetime.datetime | None,
+        revocation_reason: x509.ReasonFlags | None,
+    ) -> OCSPResponseBuilder:
+        """
+        Like add_response, but takes the hash of the issuer's name and
+        public key instead of requiring the full certificates.
+        """
+        if self._response is not None:
+            raise ValueError("Only one response per OCSPResponse.")
+
+        if not isinstance(serial_number, int):
+            raise TypeError("serial_number must be an integer")
+
+        _verify_algorithm(algorithm)
+        utils._check_bytes("issuer_name_hash", issuer_name_hash)
+        utils._check_bytes("issuer_key_hash", issuer_key_hash)
+        if algorithm.digest_size != len(
+            issuer_name_hash
+        ) or algorithm.digest_size != len(issuer_key_hash):
+            raise ValueError(
+                "issuer_name_hash and issuer_key_hash must be the same length "
+                "as the digest size of the algorithm"
+            )
+
+        if not isinstance(cert_status, OCSPCertStatus):
+            raise TypeError(
+                "cert_status must be an item from the OCSPCertStatus enum"
+            )
+        if not isinstance(this_update, datetime.datetime):
+            raise TypeError("this_update must be a datetime object")
+        if next_update is not None and not isinstance(
+            next_update, datetime.datetime
+        ):
+            raise TypeError("next_update must be a datetime object or None")
+
+        if cert_status is not OCSPCertStatus.REVOKED:
+            if revocation_time is not None:
+                raise ValueError(
+                    "revocation_time can only be provided if the certificate "
+                    "is revoked"
+                )
+            if revocation_reason is not None:
+                raise ValueError(
+                    "revocation_reason can only be provided if the certificate"
+                    " is revoked"
+                )
+        else:
+            if not isinstance(revocation_time, datetime.datetime):
+                raise TypeError("revocation_time must be a datetime object")
+
+            revocation_time = _convert_to_naive_utc_time(revocation_time)
+            if revocation_time < _EARLIEST_UTC_TIME:
+                raise ValueError(
+                    "The revocation_time must be on or after 1950 January 1."
+                )
+
+            if revocation_reason is not None and not isinstance(
+                revocation_reason, x509.ReasonFlags
+            ):
+                raise TypeError(
+                    "revocation_reason must be an item from the ReasonFlags "
+                    "enum or None"
+                )
+
+        # Create a _SingleResponse with hash-based initialization
+        singleresp = _SingleResponse(
+            cert_status=cert_status,
+            this_update=this_update,
+            next_update=next_update,
+            revocation_time=revocation_time,
+            revocation_reason=revocation_reason,
+            algorithm=algorithm,
+            issuer_name_hash=issuer_name_hash,
+            issuer_key_hash=issuer_key_hash,
+            serial_number=serial_number,
+        )
+
+        return OCSPResponseBuilder(
+            singleresp,
+            self._responder_id,
+            self._certs,
+            self._extensions,
+        )
+
     def responder_id(
         self, encoding: OCSPResponderEncoding, responder_cert: x509.Certificate
     ) -> OCSPResponseBuilder:
diff --git a/src/rust/src/x509/ocsp.rs b/src/rust/src/x509/ocsp.rs
index b632532f1..e81f65b2f 100644
--- a/src/rust/src/x509/ocsp.rs
+++ b/src/rust/src/x509/ocsp.rs
@@ -79,6 +79,7 @@ pub(crate) fn certid_new<'p>(
     issuer: &'p Certificate,
     hash_algorithm: &pyo3::Bound<'p, pyo3::PyAny>,
 ) -> CryptographyResult<CertID<'p>> {
+    // Compute hashes from the certificates
     let issuer_der = asn1::write_single(&cert.raw.borrow_dependent().tbs_cert.issuer)?;
     let issuer_name_hash =
         pyo3::pybacked::PyBackedBytes::from(hash_data(py, hash_algorithm, &issuer_der)?);
diff --git a/src/rust/src/x509/ocsp_resp.rs b/src/rust/src/x509/ocsp_resp.rs
index 706b68c6a..12d8f6499 100644
--- a/src/rust/src/x509/ocsp_resp.rs
+++ b/src/rust/src/x509/ocsp_resp.rs
@@ -4,6 +4,7 @@
 
 use std::sync::Arc;
 
+use cryptography_x509::ocsp_req::CertID;
 use cryptography_x509::ocsp_resp::{
     self, OCSPResponse as RawOCSPResponse, SingleResponse, SingleResponse as RawSingleResponse,
 };
@@ -666,6 +667,9 @@ fn singleresp_py_revocation_time_utc<'p>(
     }
 }
 
+// The create_ocsp_response_by_hash function has been replaced by a more integrated
+// approach using _SingleResponse with hash-based initialization
+
 #[pyo3::pyfunction]
 pub(crate) fn create_ocsp_response(
     py: pyo3::Python<'_>,
@@ -678,8 +682,6 @@ pub(crate) fn create_ocsp_response(
         .getattr(pyo3::intern!(py, "value"))?
         .extract::<u32>()?;
 
-    let py_cert: pyo3::PyRef<'_, x509::certificate::Certificate>;
-    let py_issuer: pyo3::PyRef<'_, x509::certificate::Certificate>;
     let borrowed_cert;
     let py_certs: Option<Vec<pyo3::PyRef<'_, x509::certificate::Certificate>>>;
     if response_status != SUCCESSFUL_RESPONSE {
@@ -692,13 +694,8 @@ pub(crate) fn create_ocsp_response(
     }
 
     let py_single_resp = builder.getattr(pyo3::intern!(py, "_response"))?;
-    py_cert = py_single_resp
-        .getattr(pyo3::intern!(py, "_cert"))?
-        .extract()?;
-    py_issuer = py_single_resp
-        .getattr(pyo3::intern!(py, "_issuer"))?
-        .extract()?;
-    let py_cert_hash_algorithm = py_single_resp.getattr(pyo3::intern!(py, "_algorithm"))?;
+
+    // Check if the response is a dictionary (from add_response_by_hash) or a _SingleResponse object
     let (responder_cert, responder_encoding): (
         pyo3::Bound<'_, x509::certificate::Certificate>,
         pyo3::Bound<'_, pyo3::PyAny>,
@@ -706,6 +703,16 @@ pub(crate) fn create_ocsp_response(
         .getattr(pyo3::intern!(py, "_responder_id"))?
         .extract()?;
 
+    let ka_vec = cryptography_keepalive::KeepAlive::new();
+    let ka_bytes = cryptography_keepalive::KeepAlive::new();
+
+    // Check if the _SingleResponse is using hashes (has _using_hashes attribute set to True)
+    let is_by_hash = py_single_resp.hasattr(pyo3::intern!(py, "_using_hashes"))?
+        && py_single_resp
+            .getattr(pyo3::intern!(py, "_using_hashes"))?
+            .extract::<bool>()?;
+
+    // Common code to process cert_status, next_update, this_update regardless of the source
     let py_cert_status = py_single_resp.getattr(pyo3::intern!(py, "_cert_status"))?;
     let cert_status = if py_cert_status.is(&types::OCSP_CERT_STATUS_GOOD.get(py)?) {
         ocsp_resp::CertStatus::Good(())
@@ -733,6 +740,7 @@ pub(crate) fn create_ocsp_response(
             revocation_reason,
         })
     };
+
     let next_update = if !py_single_resp
         .getattr(pyo3::intern!(py, "_next_update"))?
         .is_none()
@@ -745,19 +753,159 @@ pub(crate) fn create_ocsp_response(
     } else {
         None
     };
+
     let py_this_update = py_single_resp.getattr(pyo3::intern!(py, "_this_update"))?;
     let this_update = asn1::X509GeneralizedTime::new(py_to_datetime(py, py_this_update)?)?;
 
-    let ka_vec = cryptography_keepalive::KeepAlive::new();
-    let ka_bytes = cryptography_keepalive::KeepAlive::new();
+    // Get the hash algorithm
+    let py_hash_algorithm = py_single_resp.getattr(pyo3::intern!(py, "_algorithm"))?;
+
+    let responses = if is_by_hash {
+        // This is from a _SingleResponse with hash-based initialization
+        // Get the hash algorithm name first
+        let hash_name: pyo3::pybacked::PyBackedStr = py_hash_algorithm
+            .getattr(pyo3::intern!(py, "name"))?
+            .extract()?;
+
+        // Extract required Python objects
+        let issuer_name_hash_obj =
+            py_single_resp.getattr(pyo3::intern!(py, "_issuer_name_hash"))?;
+        let issuer_key_hash_obj = py_single_resp.getattr(pyo3::intern!(py, "_issuer_key_hash"))?;
+        let serial_number_obj = py_single_resp.getattr(pyo3::intern!(py, "_serial_number"))?;
+
+        // Extract bytes and store in keepalive to extend their lifetime
+        let issuer_name_hash_bytes: Vec<u8> = issuer_name_hash_obj.extract()?;
+        let issuer_key_hash_bytes: Vec<u8> = issuer_key_hash_obj.extract()?;
+
+        // Convert Vec<u8> to Python bytes and store in keepalive
+        let issuer_name_hash_py = pyo3::types::PyBytes::new(py, &issuer_name_hash_bytes);
+        let issuer_key_hash_py = pyo3::types::PyBytes::new(py, &issuer_key_hash_bytes);
+
+        // Store in keepalive for extended lifetime
+        let issuer_name_hash =
+            ka_bytes.add(pyo3::pybacked::PyBackedBytes::from(issuer_name_hash_py));
+        let issuer_key_hash = ka_bytes.add(pyo3::pybacked::PyBackedBytes::from(issuer_key_hash_py));
+
+        // Create serial number
+        let bit_length = serial_number_obj
+            .call_method0(pyo3::intern!(py, "bit_length"))?
+            .extract::<usize>()?;
+        let byte_length = (bit_length + 7) / 8;
+
+        let serial_bytes_py = py.import(pyo3::intern!(py, "int"))?.call_method1(
+            pyo3::intern!(py, "to_bytes"),
+            (
+                &serial_number_obj,
+                byte_length,
+                pyo3::intern!(py, "big"),
+                pyo3::intern!(py, "True"),
+            ),
+        )?;
+
+        let serial_bytes: Vec<u8> = serial_bytes_py.extract()?;
+        // Convert to Python bytes and store in keepalive
+        let serial_bytes_py_obj = pyo3::types::PyBytes::new(py, &serial_bytes);
+        let serial_bytes_ref =
+            ka_bytes.add(pyo3::pybacked::PyBackedBytes::from(serial_bytes_py_obj));
+
+        // Create BigInt from the keepalive bytes
+        let serial = asn1::BigInt::new(serial_bytes_ref)
+            .ok_or_else(|| pyo3::exceptions::PyValueError::new_err("Invalid serial number"))?;
+
+        // Create CertID with the extended lifetime values
+        let cert_id = CertID {
+            hash_algorithm: ocsp::HASH_NAME_TO_ALGORITHM_IDENTIFIERS[&*hash_name].clone(),
+            issuer_name_hash,
+            issuer_key_hash,
+            serial_number: serial,
+        };
+
+        vec![SingleResponse {
+            cert_id,
+            cert_status,
+            next_update,
+            this_update,
+            raw_single_extensions: None,
+        }]
+    } else {
+        // This is from add_response with the _SingleResponse object
+        let py_cert_obj = py_single_resp.getattr(pyo3::intern!(py, "_cert"))?;
+        let py_issuer_obj = py_single_resp.getattr(pyo3::intern!(py, "_issuer"))?;
+
+        // Create a scope for handling all the lifetime-bounded objects
+        let cert_id = {
+            // Extract certificates within this scope
+            let py_cert =
+                py_cert_obj.extract::<pyo3::PyRef<'_, x509::certificate::Certificate>>()?;
+            let py_issuer =
+                py_issuer_obj.extract::<pyo3::PyRef<'_, x509::certificate::Certificate>>()?;
+
+            // Get the hash name for the algorithm identifier
+            let hash_name: pyo3::pybacked::PyBackedStr = py_hash_algorithm
+                .getattr(pyo3::intern!(py, "name"))?
+                .extract()?;
+
+            // 1. Get issuer name hash
+            let issuer_der = asn1::write_single(&py_cert.raw.borrow_dependent().tbs_cert.issuer)?;
+            let issuer_name_hash_py = ocsp::hash_data(py, &py_hash_algorithm, &issuer_der)?;
+            let issuer_name_hash_vec: Vec<u8> = issuer_name_hash_py.extract()?;
+
+            // 2. Get issuer key hash
+            let issuer_key_bytes = py_issuer
+                .raw
+                .borrow_dependent()
+                .tbs_cert
+                .spki
+                .subject_public_key
+                .as_bytes();
+            let issuer_key_hash_py = ocsp::hash_data(py, &py_hash_algorithm, issuer_key_bytes)?;
+            let issuer_key_hash_vec: Vec<u8> = issuer_key_hash_py.extract()?;
 
-    let responses = vec![SingleResponse {
-        cert_id: ocsp::certid_new(py, &ka_bytes, &py_cert, &py_issuer, &py_cert_hash_algorithm)?,
-        cert_status,
-        next_update,
-        this_update,
-        raw_single_extensions: None,
-    }];
+            // 3. Get serial number bytes
+            let serial_bytes = py_cert
+                .raw
+                .borrow_dependent()
+                .tbs_cert
+                .serial
+                .as_bytes()
+                .to_vec();
+
+            // Convert to Python objects and store in keepalive
+            let issuer_name_hash_py_bytes = pyo3::types::PyBytes::new(py, &issuer_name_hash_vec);
+            let issuer_key_hash_py_bytes = pyo3::types::PyBytes::new(py, &issuer_key_hash_vec);
+            let serial_py_bytes = pyo3::types::PyBytes::new(py, &serial_bytes);
+
+            // Add to keepalive
+            let issuer_name_hash = ka_bytes.add(pyo3::pybacked::PyBackedBytes::from(
+                issuer_name_hash_py_bytes,
+            ));
+            let issuer_key_hash = ka_bytes.add(pyo3::pybacked::PyBackedBytes::from(
+                issuer_key_hash_py_bytes,
+            ));
+            let serial_bytes_ka =
+                ka_bytes.add(pyo3::pybacked::PyBackedBytes::from(serial_py_bytes));
+
+            // Create BigInt from the bytes
+            let serial_number = asn1::BigInt::new(serial_bytes_ka)
+                .ok_or_else(|| pyo3::exceptions::PyValueError::new_err("Invalid serial number"))?;
+
+            // Create and return the CertID
+            CertID {
+                hash_algorithm: ocsp::HASH_NAME_TO_ALGORITHM_IDENTIFIERS[&*hash_name].clone(),
+                issuer_name_hash,
+                issuer_key_hash,
+                serial_number,
+            }
+        };
+
+        vec![SingleResponse {
+            cert_id,
+            cert_status,
+            next_update,
+            this_update,
+            raw_single_extensions: None,
+        }]
+    };
 
     borrowed_cert = responder_cert.borrow();
     let by_key_hash;
diff --git a/tests/x509/test_ocsp.py b/tests/x509/test_ocsp.py
index d7723b288..783238256 100644
--- a/tests/x509/test_ocsp.py
+++ b/tests/x509/test_ocsp.py
@@ -387,53 +387,210 @@ class TestOCSPResponseBuilder:
                 None,
             )
 
-    def test_invalid_add_response(self):
+    def test_add_response_by_hash_custom(self):
+        # First create a response the normal way
         cert, issuer = _cert_and_issuer()
-        time = datetime.datetime.now(datetime.timezone.utc).replace(
-            tzinfo=None
+        private_key = _generate_root()[1]
+        current_time = datetime.datetime.now().replace(microsecond=0)
+
+        # Get hash values from the certificate
+        h_name = hashes.Hash(hashes.SHA1())
+        h_name.update(cert.issuer.public_bytes())
+        issuer_name_hash = h_name.finalize()
+
+        h_key = hashes.Hash(hashes.SHA1())
+        h_key.update(
+            issuer.public_key().public_bytes(
+                serialization.Encoding.DER,
+                serialization.PublicFormat.PKCS1,
+            )
+        )
+        issuer_key_hash = h_key.finalize()
+
+        # Create a response using certificate objects
+        cert_builder = ocsp.OCSPResponseBuilder()
+        cert_builder = cert_builder.add_response(
+            cert,
+            issuer,
+            hashes.SHA1(),
+            ocsp.OCSPCertStatus.GOOD,
+            current_time,
+            current_time,
+            None,
+            None,
         )
-        reason = x509.ReasonFlags.cessation_of_operation
+        cert_builder = cert_builder.responder_id(
+            ocsp.OCSPResponderEncoding.HASH, issuer
+        )
+        cert_response = cert_builder.sign(private_key, hashes.SHA256())
+
+        # Create an equivalent response using hashes
+        hash_builder = ocsp.OCSPResponseBuilder()
+        hash_builder = hash_builder.add_response_by_hash(
+            issuer_name_hash,
+            issuer_key_hash,
+            cert.serial_number,
+            hashes.SHA1(),
+            ocsp.OCSPCertStatus.GOOD,
+            current_time,
+            current_time,
+            None,
+            None,
+        )
+        hash_builder = hash_builder.responder_id(
+            ocsp.OCSPResponderEncoding.HASH, issuer
+        )
+        hash_response = hash_builder.sign(private_key, hashes.SHA256())
+
+        # Both responses should have the same hash and key values
+        assert cert_response.issuer_key_hash == hash_response.issuer_key_hash
+        assert cert_response.issuer_name_hash == hash_response.issuer_name_hash
+        assert cert_response.serial_number == hash_response.serial_number
+        assert cert_response.this_update == hash_response.this_update
+        assert cert_response.next_update == hash_response.next_update
+
+    def test_add_response_by_hash_revoked_custom(self):
+        # Test creating a revoked response with the hash-based API
+        cert, issuer = _cert_and_issuer()
+        private_key = _generate_root()[1]
+        current_time = datetime.datetime.now().replace(microsecond=0)
+        revocation_time = current_time - datetime.timedelta(days=1)
+        revocation_reason = x509.ReasonFlags.key_compromise
+
+        # Get hash values
+        h_name = hashes.Hash(hashes.SHA256())
+        h_name.update(cert.issuer.public_bytes())
+        issuer_name_hash = h_name.finalize()
+
+        h_key = hashes.Hash(hashes.SHA256())
+        h_key.update(
+            issuer.public_key().public_bytes(
+                serialization.Encoding.DER,
+                serialization.PublicFormat.PKCS1,
+            )
+        )
+        issuer_key_hash = h_key.finalize()
+
+        # Create a revoked response with hashes
         builder = ocsp.OCSPResponseBuilder()
-        with pytest.raises(TypeError):
-            builder.add_response(
-                "bad",  # type:ignore[arg-type]
-                issuer,
-                hashes.SHA256(),
+        builder = builder.add_response_by_hash(
+            issuer_name_hash,
+            issuer_key_hash,
+            cert.serial_number,
+            hashes.SHA256(),
+            ocsp.OCSPCertStatus.REVOKED,
+            current_time,
+            current_time + datetime.timedelta(days=7),
+            revocation_time,
+            revocation_reason,
+        )
+        builder = builder.responder_id(ocsp.OCSPResponderEncoding.NAME, issuer)
+        response = builder.sign(private_key, hashes.SHA256())
+
+        # Verify the response attributes
+        assert response.certificate_status == ocsp.OCSPCertStatus.REVOKED
+        assert response.revocation_time == revocation_time
+        assert response.revocation_reason == revocation_reason
+        assert response.this_update == current_time
+        assert response.next_update == current_time + datetime.timedelta(
+            days=7
+        )
+
+    def test_add_response_by_hash_twice(self):
+        time = datetime.datetime.now()
+        builder = ocsp.OCSPResponseBuilder()
+        builder = builder.add_response_by_hash(
+            b"0" * 20,
+            b"0" * 20,
+            1,
+            hashes.SHA1(),
+            ocsp.OCSPCertStatus.GOOD,
+            time,
+            time,
+            None,
+            None,
+        )
+        with pytest.raises(ValueError):
+            builder.add_response_by_hash(
+                b"0" * 20,
+                b"0" * 20,
+                1,
+                hashes.SHA1(),
                 ocsp.OCSPCertStatus.GOOD,
                 time,
                 time,
                 None,
                 None,
             )
-        with pytest.raises(TypeError):
-            builder.add_response(
-                cert,
-                "bad",  # type:ignore[arg-type]
-                hashes.SHA256(),
+
+    def test_response_methods_exclusivity(self):
+        """Test add_response and add_response_by_hash exclusivity"""
+        cert, issuer = _cert_and_issuer()
+        time = datetime.datetime.now()
+
+        # Test add_response followed by add_response_by_hash
+        builder = ocsp.OCSPResponseBuilder()
+        builder = builder.add_response(
+            cert,
+            issuer,
+            hashes.SHA1(),
+            ocsp.OCSPCertStatus.GOOD,
+            time,
+            time,
+            None,
+            None,
+        )
+        with pytest.raises(ValueError):
+            builder.add_response_by_hash(
+                b"0" * 20,
+                b"0" * 20,
+                1,
+                hashes.SHA1(),
                 ocsp.OCSPCertStatus.GOOD,
                 time,
                 time,
                 None,
                 None,
             )
+
+        # Test add_response_by_hash followed by add_response
+        builder = ocsp.OCSPResponseBuilder()
+        builder = builder.add_response_by_hash(
+            b"0" * 20,
+            b"0" * 20,
+            1,
+            hashes.SHA1(),
+            ocsp.OCSPCertStatus.GOOD,
+            time,
+            time,
+            None,
+            None,
+        )
         with pytest.raises(ValueError):
             builder.add_response(
                 cert,
                 issuer,
-                "notahash",  # type:ignore[arg-type]
+                hashes.SHA1(),
                 ocsp.OCSPCertStatus.GOOD,
                 time,
                 time,
                 None,
                 None,
             )
+
+    def test_invalid_add_response(self):
+        cert, issuer = _cert_and_issuer()
+        time = datetime.datetime.now(datetime.timezone.utc).replace(
+            tzinfo=None
+        )
+        builder = ocsp.OCSPResponseBuilder()
         with pytest.raises(TypeError):
             builder.add_response(
-                cert,
+                "bad",  # type:ignore[arg-type]
                 issuer,
                 hashes.SHA256(),
                 ocsp.OCSPCertStatus.GOOD,
-                "bad",  # type:ignore[arg-type]
+                time,
                 time,
                 None,
                 None,
@@ -441,69 +598,112 @@ class TestOCSPResponseBuilder:
         with pytest.raises(TypeError):
             builder.add_response(
                 cert,
-                issuer,
+                "bad",  # type:ignore[arg-type]
                 hashes.SHA256(),
                 ocsp.OCSPCertStatus.GOOD,
                 time,
-                "bad",  # type:ignore[arg-type]
+                time,
                 None,
                 None,
             )
 
+    def test_invalid_add_response_by_hash(self):
+        builder = ocsp.OCSPResponseBuilder()
+        time = datetime.datetime.now()
+        cert, issuer = _cert_and_issuer()
+
+        # Test invalid hash types
         with pytest.raises(TypeError):
-            builder.add_response(
-                cert,
-                issuer,
-                hashes.SHA256(),
-                0,  # type:ignore[arg-type]
+            builder.add_response_by_hash(
+                "not-bytes",  # type:ignore[arg-type]
+                b"0" * 20,
+                1,
+                hashes.SHA1(),
+                ocsp.OCSPCertStatus.GOOD,
                 time,
                 time,
                 None,
                 None,
             )
-        with pytest.raises(ValueError):
-            builder.add_response(
-                cert,
-                issuer,
-                hashes.SHA256(),
+        with pytest.raises(TypeError):
+            builder.add_response_by_hash(
+                b"0" * 20,
+                "not-bytes",  # type:ignore[arg-type]
+                1,
+                hashes.SHA1(),
                 ocsp.OCSPCertStatus.GOOD,
                 time,
                 time,
+                None,
+                None,
+            )
+
+        # Test invalid serial number
+        with pytest.raises(TypeError):
+            builder.add_response_by_hash(
+                b"0" * 20,
+                b"0" * 20,
+                "not-an-int",  # type:ignore[arg-type]
+                hashes.SHA1(),
+                ocsp.OCSPCertStatus.GOOD,
                 time,
+                time,
+                None,
                 None,
             )
+
+        # Test invalid algorithm
         with pytest.raises(ValueError):
-            builder.add_response(
-                cert,
-                issuer,
-                hashes.SHA256(),
+            builder.add_response_by_hash(
+                b"0" * 20,
+                b"0" * 20,
+                1,
+                "invalid-algorithm",  # type:ignore[arg-type]
                 ocsp.OCSPCertStatus.GOOD,
                 time,
                 time,
                 None,
-                reason,
+                None,
             )
-        with pytest.raises(TypeError):
-            builder.add_response(
-                cert,
-                issuer,
-                hashes.SHA256(),
-                ocsp.OCSPCertStatus.REVOKED,
+
+        # Test hash length validation
+        with pytest.raises(ValueError):
+            builder.add_response_by_hash(
+                b"0" * 19,  # Wrong length for SHA1
+                b"0" * 20,
+                1,
+                hashes.SHA1(),
+                ocsp.OCSPCertStatus.GOOD,
                 time,
                 time,
                 None,
-                reason,
+                None,
             )
-        with pytest.raises(TypeError):
-            builder.add_response(
-                cert,
-                issuer,
-                hashes.SHA256(),
-                ocsp.OCSPCertStatus.REVOKED,
+        with pytest.raises(ValueError):
+            builder.add_response_by_hash(
+                b"0" * 20,
+                b"0" * 19,  # Wrong length for SHA1
+                1,
+                hashes.SHA1(),
+                ocsp.OCSPCertStatus.GOOD,
                 time,
                 time,
+                None,
+                None,
+            )
+
+        # Test disallowed hash algorithm
+        with pytest.raises(ValueError):
+            builder.add_response_by_hash(
+                b"0" * 16,
+                b"0" * 16,
+                1,
+                hashes.MD5(),
+                ocsp.OCSPCertStatus.GOOD,
+                time,
                 time,
-                0,  # type:ignore[arg-type]
+                None,
+                None,
             )
         with pytest.raises(ValueError):
             builder.add_response(
@@ -1067,6 +1267,203 @@ class TestOCSPResponseBuilder:
         with pytest.raises(TypeError):
             builder.sign(private_key, None)
 
+    def test_add_response_by_hash(self):
+        cert, issuer = _cert_and_issuer()
+        root_cert, private_key = _generate_root()
+
+        # Generate standard response
+        standard_builder = ocsp.OCSPResponseBuilder()
+        this_update = (
+            datetime.datetime.now().replace(tzinfo=None).replace(microsecond=0)
+        )
+        next_update = this_update + datetime.timedelta(days=1)
+
+        standard_builder = standard_builder.add_response(
+            cert=cert,
+            issuer=issuer,
+            algorithm=hashes.SHA256(),
+            cert_status=ocsp.OCSPCertStatus.GOOD,
+            this_update=this_update,
+            next_update=next_update,
+            revocation_time=None,
+            revocation_reason=None,
+        ).responder_id(ocsp.OCSPResponderEncoding.NAME, root_cert)
+
+        standard_response = standard_builder.sign(private_key, hashes.SHA256())
+
+        # Now generate the same response using add_response_by_hash
+        # First, compute the hashes manually
+        issuer_name = issuer.subject.public_bytes()
+        issuer_key = issuer.public_key().public_bytes(
+            encoding=serialization.Encoding.DER,
+            format=serialization.PublicFormat.PKCS1,
+        )
+
+        digest = hashes.Hash(hashes.SHA256())
+        digest.update(issuer_name)
+        issuer_name_hash = digest.finalize()
+
+        digest = hashes.Hash(hashes.SHA256())
+        digest.update(issuer_key)
+        issuer_key_hash = digest.finalize()
+
+        # Create response by hash
+        hash_builder = ocsp.OCSPResponseBuilder()
+        hash_builder = hash_builder.add_response_by_hash(
+            issuer_name_hash=issuer_name_hash,
+            issuer_key_hash=issuer_key_hash,
+            serial_number=cert.serial_number,
+            algorithm=hashes.SHA256(),
+            cert_status=ocsp.OCSPCertStatus.GOOD,
+            this_update=this_update,
+            next_update=next_update,
+            revocation_time=None,
+            revocation_reason=None,
+        ).responder_id(ocsp.OCSPResponderEncoding.NAME, root_cert)
+
+        hash_response = hash_builder.sign(private_key, hashes.SHA256())
+
+        # The two responses should contain the same CertID
+        assert (
+            hash_response.issuer_name_hash
+            == standard_response.issuer_name_hash
+        )
+        assert (
+            hash_response.issuer_key_hash == standard_response.issuer_key_hash
+        )
+        assert hash_response.serial_number == standard_response.serial_number
+
+    def test_add_response_by_hash_revoked(self):
+        cert, issuer = _cert_and_issuer()
+        root_cert, private_key = _generate_root()
+
+        # Compute the hashes manually
+        issuer_name = issuer.subject.public_bytes()
+        issuer_key = issuer.public_key().public_bytes(
+            encoding=serialization.Encoding.DER,
+            format=serialization.PublicFormat.PKCS1,
+        )
+
+        digest = hashes.Hash(hashes.SHA256())
+        digest.update(issuer_name)
+        issuer_name_hash = digest.finalize()
+
+        digest = hashes.Hash(hashes.SHA256())
+        digest.update(issuer_key)
+        issuer_key_hash = digest.finalize()
+
+        # Create response by hash with revoked status
+        this_update = (
+            datetime.datetime.now().replace(tzinfo=None).replace(microsecond=0)
+        )
+        next_update = this_update + datetime.timedelta(days=1)
+        revocation_time = this_update - datetime.timedelta(days=1)
+
+        hash_builder = ocsp.OCSPResponseBuilder()
+        hash_builder = hash_builder.add_response_by_hash(
+            issuer_name_hash=issuer_name_hash,
+            issuer_key_hash=issuer_key_hash,
+            serial_number=cert.serial_number,
+            algorithm=hashes.SHA256(),
+            cert_status=ocsp.OCSPCertStatus.REVOKED,
+            this_update=this_update,
+            next_update=next_update,
+            revocation_time=revocation_time,
+            revocation_reason=x509.ReasonFlags.key_compromise,
+        ).responder_id(ocsp.OCSPResponderEncoding.NAME, root_cert)
+
+        hash_response = hash_builder.sign(private_key, hashes.SHA256())
+
+        # Check that the status is set correctly
+        assert hash_response.certificate_status == ocsp.OCSPCertStatus.REVOKED
+        assert (
+            hash_response.revocation_reason == x509.ReasonFlags.key_compromise
+        )
+        _check_ocsp_response_times(
+            hash_response, this_update, next_update, revocation_time
+        )
+
+    def test_add_response_by_hash_validates_parameters(self):
+        cert, issuer = _cert_and_issuer()
+
+        # Compute hashes
+        issuer_name = issuer.subject.public_bytes()
+        issuer_key = issuer.public_key().public_bytes(
+            encoding=serialization.Encoding.DER,
+            format=serialization.PublicFormat.PKCS1,
+        )
+
+        digest = hashes.Hash(hashes.SHA256())
+        digest.update(issuer_name)
+        issuer_name_hash = digest.finalize()
+
+        digest = hashes.Hash(hashes.SHA256())
+        digest.update(issuer_key)
+        issuer_key_hash = digest.finalize()
+
+        # Test parameters validation
+        builder = ocsp.OCSPResponseBuilder()
+        this_update = (
+            datetime.datetime.now().replace(tzinfo=None).replace(microsecond=0)
+        )
+        next_update = this_update + datetime.timedelta(days=1)
+
+        # Test invalid serial_number
+        with pytest.raises(TypeError):
+            builder.add_response_by_hash(
+                issuer_name_hash=issuer_name_hash,
+                issuer_key_hash=issuer_key_hash,
+                serial_number="not an integer",  # type: ignore[arg-type]
+                algorithm=hashes.SHA256(),
+                cert_status=ocsp.OCSPCertStatus.GOOD,
+                this_update=this_update,
+                next_update=next_update,
+                revocation_time=None,
+                revocation_reason=None,
+            )
+
+        # Test invalid hashes
+        with pytest.raises(ValueError):
+            builder.add_response_by_hash(
+                issuer_name_hash=b"too-short",
+                issuer_key_hash=issuer_key_hash,
+                serial_number=cert.serial_number,
+                algorithm=hashes.SHA256(),
+                cert_status=ocsp.OCSPCertStatus.GOOD,
+                this_update=this_update,
+                next_update=next_update,
+                revocation_time=None,
+                revocation_reason=None,
+            )
+
+        # Test invalid cert_status
+        with pytest.raises(TypeError):
+            builder.add_response_by_hash(
+                issuer_name_hash=issuer_name_hash,
+                issuer_key_hash=issuer_key_hash,
+                serial_number=cert.serial_number,
+                algorithm=hashes.SHA256(),
+                cert_status="invalid status",  # type: ignore[arg-type]
+                this_update=this_update,
+                next_update=next_update,
+                revocation_time=None,
+                revocation_reason=None,
+            )
+
+        # Test revocation_time without revoked status
+        with pytest.raises(ValueError):
+            builder.add_response_by_hash(
+                issuer_name_hash=issuer_name_hash,
+                issuer_key_hash=issuer_key_hash,
+                serial_number=cert.serial_number,
+                algorithm=hashes.SHA256(),
+                cert_status=ocsp.OCSPCertStatus.GOOD,
+                this_update=this_update,
+                next_update=next_update,
+                revocation_time=datetime.datetime.now(),
+                revocation_reason=None,
+            )
+
 
 class TestSignedCertificateTimestampsExtension:
     def test_init(self):
@@ -1557,6 +1954,275 @@ class TestOCSPEdDSA:
         with pytest.raises(ValueError):
             builder.sign(private_key, hashes.SHA256())
 
+    def test_add_response_by_hash(self):
+        builder = ocsp.OCSPResponseBuilder()
+        cert, issuer = _cert_and_issuer()
+        root_cert, private_key = _generate_root()
+        current_time = (
+            datetime.datetime.now(datetime.timezone.utc)
+            .replace(tzinfo=None)
+            .replace(microsecond=0)
+        )
+        this_update = current_time - datetime.timedelta(days=1)
+        next_update = this_update + datetime.timedelta(days=7)
+
+        # Generate the issuer name hash and key hash
+        hash_alg = hashes.SHA1()
+        h = hashes.Hash(hash_alg)
+        h.update(issuer.subject.public_bytes())
+        issuer_name_hash = h.finalize()
+
+        h = hashes.Hash(hash_alg)
+        h.update(
+            issuer.public_key().public_bytes(
+                serialization.Encoding.DER,
+                serialization.PublicFormat.SubjectPublicKeyInfo,
+            )
+        )
+        issuer_key_hash = h.finalize()
+
+        builder = builder.responder_id(
+            ocsp.OCSPResponderEncoding.NAME, root_cert
+        ).add_response_by_hash(
+            issuer_name_hash=issuer_name_hash,
+            issuer_key_hash=issuer_key_hash,
+            serial_number=cert.serial_number,
+            algorithm=hash_alg,
+            cert_status=ocsp.OCSPCertStatus.GOOD,
+            this_update=this_update,
+            next_update=next_update,
+            revocation_time=None,
+            revocation_reason=None,
+        )
+        resp = builder.sign(private_key, hashes.SHA256())
+        assert resp.certificate_status == ocsp.OCSPCertStatus.GOOD
+        _check_ocsp_response_times(
+            resp,
+            this_update=this_update,
+            next_update=next_update,
+            revocation_time=None,
+        )
+        assert resp.serial_number == cert.serial_number
+        assert resp.issuer_name_hash == issuer_name_hash
+        assert resp.issuer_key_hash == issuer_key_hash
+
+    def test_add_response_by_hash_revoked(self):
+        builder = ocsp.OCSPResponseBuilder()
+        cert, issuer = _cert_and_issuer()
+        root_cert, private_key = _generate_root()
+        current_time = (
+            datetime.datetime.now(datetime.timezone.utc)
+            .replace(tzinfo=None)
+            .replace(microsecond=0)
+        )
+        this_update = current_time - datetime.timedelta(days=1)
+        next_update = this_update + datetime.timedelta(days=7)
+        revoked_date = this_update - datetime.timedelta(days=300)
+
+        # Generate the issuer name hash and key hash
+        hash_alg = hashes.SHA1()
+        h = hashes.Hash(hash_alg)
+        h.update(issuer.subject.public_bytes())
+        issuer_name_hash = h.finalize()
+
+        h = hashes.Hash(hash_alg)
+        h.update(
+            issuer.public_key().public_bytes(
+                serialization.Encoding.DER,
+                serialization.PublicFormat.SubjectPublicKeyInfo,
+            )
+        )
+        issuer_key_hash = h.finalize()
+
+        builder = builder.responder_id(
+            ocsp.OCSPResponderEncoding.NAME, root_cert
+        ).add_response_by_hash(
+            issuer_name_hash=issuer_name_hash,
+            issuer_key_hash=issuer_key_hash,
+            serial_number=cert.serial_number,
+            algorithm=hash_alg,
+            cert_status=ocsp.OCSPCertStatus.REVOKED,
+            this_update=this_update,
+            next_update=next_update,
+            revocation_time=revoked_date,
+            revocation_reason=x509.ReasonFlags.key_compromise,
+        )
+        resp = builder.sign(private_key, hashes.SHA256())
+        assert resp.certificate_status == ocsp.OCSPCertStatus.REVOKED
+        assert resp.revocation_reason is x509.ReasonFlags.key_compromise
+        _check_ocsp_response_times(
+            resp,
+            this_update=this_update,
+            next_update=next_update,
+            revocation_time=revoked_date,
+        )
+        assert resp.serial_number == cert.serial_number
+        assert resp.issuer_name_hash == issuer_name_hash
+        assert resp.issuer_key_hash == issuer_key_hash
+
+    def test_add_response_and_add_response_by_hash_validation(self):
+        builder = ocsp.OCSPResponseBuilder()
+        cert, issuer = _cert_and_issuer()
+        root_cert, private_key = _generate_root()
+        current_time = (
+            datetime.datetime.now(datetime.timezone.utc)
+            .replace(tzinfo=None)
+            .replace(microsecond=0)
+        )
+        this_update = current_time - datetime.timedelta(days=1)
+        next_update = this_update + datetime.timedelta(days=7)
+
+        # Build first response with add_response
+        builder1 = builder.responder_id(
+            ocsp.OCSPResponderEncoding.NAME, root_cert
+        ).add_response(
+            cert,
+            issuer,
+            hashes.SHA1(),
+            ocsp.OCSPCertStatus.GOOD,
+            this_update,
+            next_update,
+            None,
+            None,
+        )
+        resp1 = builder1.sign(private_key, hashes.SHA256())
+
+        # Build second response with add_response_by_hash using the same params
+        hash_alg = hashes.SHA1()
+        h = hashes.Hash(hash_alg)
+        h.update(issuer.subject.public_bytes())
+        issuer_name_hash = h.finalize()
+
+        h = hashes.Hash(hash_alg)
+        h.update(
+            issuer.public_key().public_bytes(
+                serialization.Encoding.DER,
+                serialization.PublicFormat.SubjectPublicKeyInfo,
+            )
+        )
+        issuer_key_hash = h.finalize()
+
+        builder2 = (
+            ocsp.OCSPResponseBuilder()
+            .responder_id(ocsp.OCSPResponderEncoding.NAME, root_cert)
+            .add_response_by_hash(
+                issuer_name_hash=issuer_name_hash,
+                issuer_key_hash=issuer_key_hash,
+                serial_number=cert.serial_number,
+                algorithm=hash_alg,
+                cert_status=ocsp.OCSPCertStatus.GOOD,
+                this_update=this_update,
+                next_update=next_update,
+                revocation_time=None,
+                revocation_reason=None,
+            )
+        )
+        resp2 = builder2.sign(private_key, hashes.SHA256())
+
+        # The two responses should contain identical information
+        assert resp1.certificate_status == resp2.certificate_status
+        assert resp1.issuer_name_hash == resp2.issuer_name_hash
+        assert resp1.issuer_key_hash == resp2.issuer_key_hash
+        assert resp1.serial_number == resp2.serial_number
+        assert resp1.hash_algorithm.name == resp2.hash_algorithm.name
+
+    def test_add_response_by_hash_validations(self):
+        builder = ocsp.OCSPResponseBuilder()
+        cert, issuer = _cert_and_issuer()
+        root_cert, private_key = _generate_root()
+        current_time = (
+            datetime.datetime.now(datetime.timezone.utc)
+            .replace(tzinfo=None)
+            .replace(microsecond=0)
+        )
+        this_update = current_time - datetime.timedelta(days=1)
+        next_update = this_update + datetime.timedelta(days=7)
+
+        # Generate valid hashes
+        hash_alg = hashes.SHA1()
+        h = hashes.Hash(hash_alg)
+        h.update(issuer.subject.public_bytes())
+        issuer_name_hash = h.finalize()
+
+        h = hashes.Hash(hash_alg)
+        h.update(
+            issuer.public_key().public_bytes(
+                serialization.Encoding.DER,
+                serialization.PublicFormat.SubjectPublicKeyInfo,
+            )
+        )
+        issuer_key_hash = h.finalize()
+
+        # Test with non-integer serial number
+        with pytest.raises(TypeError):
+            builder.add_response_by_hash(
+                issuer_name_hash=issuer_name_hash,
+                issuer_key_hash=issuer_key_hash,
+                serial_number="not an int",
+                algorithm=hash_alg,
+                cert_status=ocsp.OCSPCertStatus.GOOD,
+                this_update=this_update,
+                next_update=next_update,
+                revocation_time=None,
+                revocation_reason=None,
+            )
+
+        # Test with invalid algorithm
+        with pytest.raises(ValueError):
+            builder.add_response_by_hash(
+                issuer_name_hash=issuer_name_hash,
+                issuer_key_hash=issuer_key_hash,
+                serial_number=cert.serial_number,
+                algorithm=hashes.MD5(),
+                cert_status=ocsp.OCSPCertStatus.GOOD,
+                this_update=this_update,
+                next_update=next_update,
+                revocation_time=None,
+                revocation_reason=None,
+            )
+
+        # Test with mis-sized hash
+        with pytest.raises(ValueError):
+            builder.add_response_by_hash(
+                issuer_name_hash=issuer_name_hash[:-1],  # Wrong size
+                issuer_key_hash=issuer_key_hash,
+                serial_number=cert.serial_number,
+                algorithm=hash_alg,
+                cert_status=ocsp.OCSPCertStatus.GOOD,
+                this_update=this_update,
+                next_update=next_update,
+                revocation_time=None,
+                revocation_reason=None,
+            )
+
+        # Test with invalid this_update
+        with pytest.raises(TypeError):
+            builder.add_response_by_hash(
+                issuer_name_hash=issuer_name_hash,
+                issuer_key_hash=issuer_key_hash,
+                serial_number=cert.serial_number,
+                algorithm=hash_alg,
+                cert_status=ocsp.OCSPCertStatus.GOOD,
+                this_update="not a datetime",
+                next_update=next_update,
+                revocation_time=None,
+                revocation_reason=None,
+            )
+
+        # Test with revocation_time on a good certificate
+        with pytest.raises(ValueError):
+            builder.add_response_by_hash(
+                issuer_name_hash=issuer_name_hash,
+                issuer_key_hash=issuer_key_hash,
+                serial_number=cert.serial_number,
+                algorithm=hash_alg,
+                cert_status=ocsp.OCSPCertStatus.GOOD,
+                this_update=this_update,
+                next_update=next_update,
+                revocation_time=current_time,
+                revocation_reason=None,
+            )
+
     @pytest.mark.supported(
         only_if=lambda backend: backend.ed25519_supported(),
         skip_message="Requires OpenSSL with Ed25519 support / OCSP",

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

3 participants