Commit 2d3e267b2b for openssl.org

commit 2d3e267b2b035859b1da38644721afef8d88d792
Author: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Date:   Thu Jul 2 13:45:16 2026 +0200

    x509: add ocsptest for the OCSP stapled-response verification path

    Add test/ocsptest.c, exercising check_cert_ocsp_resp() in x509_vfy.c
    through X509_verify_cert() with X509_V_FLAG_OCSP_RESP_CHECK and
    responses attached via X509_STORE_CTX_set_ocsp_resp(). This path was
    previously only covered indirectly through the TLS multi-stapling
    tests in sslapitest.c.

    The test builds signed OCSP responses at run time from a flat
    root -> leaf PKI (the root is both the trust anchor and the authorized
    responder), and covers the good, grace-period, non-successful status,
    expired, no-response, and wrong-certificate cases, plus a mfail run
    over the success path. The PKI is generated by the test-tools ocsptest
    command.

    Assisted-by: Claude:claude-opus-4-8

    Reviewed-by: Daniel Kubec <kubec@openssl.foundation>
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    MergeDate: Mon Jul 13 15:04:59 2026
    (Merged from https://github.com/openssl/openssl/pull/31828)

diff --git a/test/build.info b/test/build.info
index 94e6d08865..a5dcc7f89d 100644
--- a/test/build.info
+++ b/test/build.info
@@ -65,7 +65,7 @@ IF[{- !$disabled{tests} -}]
           x509_time_test x509_dup_cert_test x509_check_cert_pkey_test \
           recordlentest drbgtest rand_status_test sslbuffertest \
           time_offset_test pemtest ssl_cert_table_internal_test ciphername_test \
-          servername_test ocspapitest fatalerrtest tls13ccstest \
+          servername_test ocspapitest ocsptest fatalerrtest tls13ccstest \
           sysdefaulttest errtest ssl_ctx_test build_wincrypt_test \
           context_internal_test aesgcmtest params_test evp_pkey_dparams_test \
           keymgmt_internal_test hexstr_test provider_status_test defltfips_test \
@@ -356,6 +356,10 @@ IF[{- !$disabled{tests} -}]
   INCLUDE[crltest]=../include ../apps/include
   DEPEND[crltest]=../libcrypto libtestutil.a

+  SOURCE[ocsptest]=ocsptest.c
+  INCLUDE[ocsptest]=../include ../apps/include
+  DEPEND[ocsptest]=../libcrypto libtestutil.a
+
   SOURCE[v3ext]=v3ext.c
   INCLUDE[v3ext]=../include ../apps/include
   DEPEND[v3ext]=../libcrypto libtestutil.a
diff --git a/test/ocsptest.c b/test/ocsptest.c
new file mode 100644
index 0000000000..96dc07423b
--- /dev/null
+++ b/test/ocsptest.c
@@ -0,0 +1,408 @@
+/*
+ * Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License 2.0 (the "License").  You may not use
+ * this file except in compliance with the License.  You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+#include <time.h>
+#include <openssl/bio.h>
+#include <openssl/crypto.h>
+#include <openssl/err.h>
+#include <openssl/pem.h>
+#include <openssl/x509.h>
+#include <openssl/x509_vfy.h>
+#ifndef OPENSSL_NO_OCSP
+#include <openssl/ocsp.h>
+#endif
+
+#include "testutil.h"
+
+#ifndef OPENSSL_NO_OCSP
+
+/*
+ * Fixtures for the OCSP stapled-response verification path
+ * (check_cert_ocsp_resp() in x509_vfy.c), reached from X509_verify_cert()
+ * when X509_V_FLAG_OCSP_RESP_CHECK is set and responses are attached with
+ * X509_STORE_CTX_set_ocsp_resp().
+ *
+ *   Root CA  (self-signed trust anchor)
+ *       \-- leaf  (signed by the Root CA)
+ *
+ * The flat chain makes the root both the trust anchor and the authorized OCSP
+ * responder for the leaf, and having the root key lets each test build its own
+ * signed responses at run time. The certificates have a long validity because
+ * OCSP_check_validity() compares against the wall clock and cannot be pinned
+ * via X509_VERIFY_PARAM_set_time().
+ *
+ * The arrays below are generated by the test-tools ocsptest command.
+ */
+
+static const char *kOcspTestRoot[] = {
+    "-----BEGIN CERTIFICATE-----\n",
+    "MIID+DCCAuCgAwIBAgIBATANBgkqhkiG9w0BAQsFADCBmjELMAkGA1UEBhMCVVMx\n",
+    "EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFTAT\n",
+    "BgNVBAoMDEV4YW1wbGUgQ29ycDEeMBwGA1UECwwVQ2VydGlmaWNhdGUgQXV0aG9y\n",
+    "aXR5MScwJQYDVQQDDB5FeGFtcGxlIENvcnAgT0NTUCBUZXN0IFJvb3QgQ0EwIBcN\n",
+    "MjYwMTAxMDAwMDAwWhgPMjEyNjAxMDEwMDAwMDBaMIGaMQswCQYDVQQGEwJVUzET\n",
+    "MBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzEVMBMG\n",
+    "A1UECgwMRXhhbXBsZSBDb3JwMR4wHAYDVQQLDBVDZXJ0aWZpY2F0ZSBBdXRob3Jp\n",
+    "dHkxJzAlBgNVBAMMHkV4YW1wbGUgQ29ycCBPQ1NQIFRlc3QgUm9vdCBDQTCCASIw\n",
+    "DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKuZRsVUiwQoVlwfXY0nVpvMQiJN\n",
+    "Su5YfrxzKJ9sMfPI2rEg9d8kW2qsfnCB0isj+SK+CkcfVu5aXcUVUnp08VuKpsI9\n",
+    "idkeC7bhXkQgBR1p1mBwChc+UUD6qEindJPhFxYur+7gCmpamSHYr8iYmjVe78l8\n",
+    "vRHVuw8KMsiGegvPsuxIYWdY4mgKSCu3o6DeK4XehXU4Ll5j0fGX5eJgbaI6g8qE\n",
+    "j71J6G8Y4Ur/WqzTp/GZueTY0wzU8k45HS5uWarGYB2PH8DfM2SRQBw5hYd2xIvS\n",
+    "jgFYzCXeIZFnHVCMeB//VaUyYGIaw+Dreh+Wb5zBPh9FyJkrOXYyMeFZUScCAwEA\n",
+    "AaNFMEMwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0O\n",
+    "BBYEFGSeLzT7Ur6wrLpFYVDHmNi/TQp5MA0GCSqGSIb3DQEBCwUAA4IBAQAMMukk\n",
+    "I0VRfxY1vZngu3LdnBk5If6TbQ6lmWatT4q3BrAID3L9zG/qev6DlyruvnVqoNdU\n",
+    "WcWXnzFSDInOKM64wU2oxdosMPWuAKKLHgDdCicikJtF+N0qJgOcyD+Gv9t0kwDB\n",
+    "O094nZUgrkCafJiy5y7KtZvqOjcvfEK+oD7fXX/VURv8NMOxpuassPHnHGvHqwKu\n",
+    "7jBZMjkOoZH4hJSo5EzZxRZHz6ay7Q8e+6C6PG8CIYXpIqukjVNq9jBb2yog67mW\n",
+    "lHlfnEENCyQDSmnEXAVNpJS67jmcItoD1utnDINVzu87DY9WiLZB06Y1xSp1ie7s\n",
+    "FIfS7bZ1cxVkt5wS\n",
+    "-----END CERTIFICATE-----\n",
+    NULL
+};
+
+static const char *kOcspTestRootKey[] = {
+    "-----BEGIN RSA PRIVATE KEY-----\n",
+    "MIIEowIBAAKCAQEAq5lGxVSLBChWXB9djSdWm8xCIk1K7lh+vHMon2wx88jasSD1\n",
+    "3yRbaqx+cIHSKyP5Ir4KRx9W7lpdxRVSenTxW4qmwj2J2R4LtuFeRCAFHWnWYHAK\n",
+    "Fz5RQPqoSKd0k+EXFi6v7uAKalqZIdivyJiaNV7vyXy9EdW7DwoyyIZ6C8+y7Ehh\n",
+    "Z1jiaApIK7ejoN4rhd6FdTguXmPR8Zfl4mBtojqDyoSPvUnobxjhSv9arNOn8Zm5\n",
+    "5NjTDNTyTjkdLm5ZqsZgHY8fwN8zZJFAHDmFh3bEi9KOAVjMJd4hkWcdUIx4H/9V\n",
+    "pTJgYhrD4Ot6H5ZvnME+H0XImSs5djIx4VlRJwIDAQABAoIBAAj+6w/d47/JtuVI\n",
+    "xlMSXzRMW/cyYsg7SWxAWT5/oeAW2n1zWJBkdopmv+YkAsw81uBfDWjhwraSHtz9\n",
+    "xioh8U6MO+ZuQB4VY3soi2mPiCpyPvPP9nzLc9+v2ZydcrtsjxTxnkrWjJU7afsK\n",
+    "l1nbw3x4HU1McG5RQbzYcFsaJFHJcVdxIYkDfEgnBok1ALaQuRUGfUVYZDVk6Ztg\n",
+    "fMUh5nfNSHWu6y2i8YCphprIfu/zwadicaYLYljYqgP9J1MOPwuMcoCoWgotBUCZ\n",
+    "+29qIPn1mK3ReBmYdE8IIEiSLHYJjJ7EVu8IUfSPotBGGC5PjC11HcCXx0xB9AfB\n",
+    "I8VcDikCgYEA089WizVCWZMxWnCPLDG036vsug3/rrPr/O3W5VXTdpr00CBRzund\n",
+    "7QnaxUyxw37SOAQUtzcKhC0CF3c7Dbryb8d51koROVlWC8rngoDMxiZkbZZe0kL1\n",
+    "jHkOHEOoi83uY0tFoRhYtrcOYObNT9JDifdIDYCnxbLW4UQLo/9mFmUCgYEAz2ZH\n",
+    "hxoxXJ9y3qmIcc2vZTRPdaB4r+qQi5krUgtYg1uKImrvDnnl/4xCnCG9G1s1M/wE\n",
+    "cvuFG5FWGz6TtS0TsCGUXVWZ9s67JT53l02RIVTog/VRHbKsOnj+d9oXglGcxALT\n",
+    "qG02lKocNHzrPUXEw/EIMETKR6WvATZCzhM9mpsCgYB9RoKfb27A4Cgun6husS+T\n",
+    "o3IuUR1KzSvkux+BIRQjcF8fwh3gzb3u9wcn7satJBNeAjvmaW2U47H7AxAwfMPr\n",
+    "jQXo0oIBc29LJkVrkJaNFCQOFQQcRHJLFUZdPT8xASngHKMgNvAxkW+1rIz+ixRb\n",
+    "Q6CgK9oPOkmRjtd7thFBaQKBgQCMbt0QBhRWe0D0tCbHqFaTWJBVPYt60oF9hQFo\n",
+    "VHZiu6EVHQMx8ihimT6hKdc6ps+nm4YHtXez6v07BWxOyW8DXDlx2XyfOexOk7W2\n",
+    "pbcXsr6eW4XJbipgjX0A+pPgkhJsRt26tfi3QVhH0i4XFx7c7mB1Dp9JVE7jqzIh\n",
+    "B7Y28QKBgCg93v7zP5GFmOa6zCZreIIfi1dy50otxfqYewcXy4Egt90cE67vY55u\n",
+    "3B44ySrLkKDuzX2Inrwp5Vao8b9pDQ/AisRzt617eO1H5JtRF3lzZJukYC6j4urU\n",
+    "24kB2IhZCB6/3pwGiS21ma9E6OF9tO5YaGGYo02ZqkoflN+XgoeZ\n",
+    "-----END RSA PRIVATE KEY-----\n",
+    NULL
+};
+
+static const char *kOcspTestLeaf[] = {
+    "-----BEGIN CERTIFICATE-----\n",
+    "MIIEOzCCAyOgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgZoxCzAJBgNVBAYTAlVT\n",
+    "MRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMRUw\n",
+    "EwYDVQQKDAxFeGFtcGxlIENvcnAxHjAcBgNVBAsMFUNlcnRpZmljYXRlIEF1dGhv\n",
+    "cml0eTEnMCUGA1UEAwweRXhhbXBsZSBDb3JwIE9DU1AgVGVzdCBSb290IENBMCAX\n",
+    "DTI2MDEwMTAwMDAwMFoYDzIxMjYwMTAxMDAwMDAwWjCBiDELMAkGA1UEBhMCVVMx\n",
+    "EzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xFTAT\n",
+    "BgNVBAoMDEV4YW1wbGUgQ29ycDEVMBMGA1UECwwMV2ViIFNlcnZpY2VzMR4wHAYD\n",
+    "VQQDDBVvY3NwLWxlYWYuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IB\n",
+    "DwAwggEKAoIBAQCoQbsBGnLjATBzU8eWYMeyzu6vy4scpVDdhGnTWMaSIrqoXXge\n",
+    "JXrMHavT13cv2wNTExA6BNqFZA6YLBXYpDJ5oXyOlY1SYjbapX4M0kry4tBZdGWu\n",
+    "vnh04Q07SI8JvjtScB3lAIIhGr1WrQNLfEz+O80j4Pp8kLe1fWi0joB1CG4RWiuH\n",
+    "/2Ls1rSEMTr6dABtQ+zwxRHcFlEAoR9ZEDzfN1hIQEByWJd7TMnv738usyp52wMi\n",
+    "Gh/sruYHkpsO2tVoLCwm1OMSR1BLdGO1tJWSYN2+Tj2JVleQWcwwCgzDX4OlIdPF\n",
+    "/CM/6aQA6BkDeMW//tO4Ec4X+530zPiFKcZLAgMBAAGjgZgwgZUwDAYDVR0TAQH/\n",
+    "BAIwADAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHQYDVR0O\n",
+    "BBYEFBEZYONIpHIGBtlfPSlUTcDccGMiMB8GA1UdIwQYMBaAFGSeLzT7Ur6wrLpF\n",
+    "YVDHmNi/TQp5MCAGA1UdEQQZMBeCFW9jc3AtbGVhZi5leGFtcGxlLmNvbTANBgkq\n",
+    "hkiG9w0BAQsFAAOCAQEAaFYoQjmWdBBiD/hfSgV34Jf+PVKkecuu60VKDasPLNyV\n",
+    "ZwZcW8dXF9NOYcebGIEx4rjQTC3xiHUknqfOzc2jmPdU/xBAyRiltQnUh3OFH1qn\n",
+    "bX0YNUgfluLW+YSwNkriFLuzESBVadGhlX94mQwoqYyQJ+6/Ht2j4P4ZiIDRWfgc\n",
+    "z9pN8YoCiXh/I/IxVIWunk7Dla+Gr6BDJ762iQMMzPQ5D3cRAe9BDRgGiN0lPeIa\n",
+    "LMtqDeZZ+dR0KZAr3yZcfyci8SNXwCBbjsoVjmuUj6dlN12pRKxxtBSa23KT6/Dy\n",
+    "ijXZ+PtyIJz8FVBxO0RoECLimwbUPHfNjAWjuJkugA==\n",
+    "-----END CERTIFICATE-----\n",
+    NULL
+};
+
+/* Load the fixed PKI. Any of the out params may be NULL to skip it. */
+static int load_pki(X509 **root, EVP_PKEY **root_key, X509 **leaf)
+{
+    if (root != NULL && !TEST_ptr(*root = X509_from_strings(kOcspTestRoot)))
+        return 0;
+    if (root_key != NULL
+        && !TEST_ptr(*root_key = PKEY_from_strings(kOcspTestRootKey)))
+        return 0;
+    if (leaf != NULL && !TEST_ptr(*leaf = X509_from_strings(kOcspTestLeaf)))
+        return 0;
+    return 1;
+}
+
+/*
+ * Build a signed OCSP response for |cert| (issued by |issuer|). |resp_status|
+ * is the outer response status, |cert_status| the single-response certificate
+ * status. thisUpdate and nextUpdate are the current time offset by
+ * |this_off_sec| and |next_off_sec| seconds, so callers can also produce
+ * expired or not-yet-valid responses. The response is signed by
+ * |signer|/|signer_key|. Returns a response the caller must free, or NULL.
+ */
+static OCSP_RESPONSE *make_ocsp_response(X509 *cert, X509 *issuer,
+    int resp_status, int cert_status, int this_off_sec, int next_off_sec,
+    X509 *signer, EVP_PKEY *signer_key)
+{
+    OCSP_RESPONSE *resp = NULL;
+    OCSP_BASICRESP *bs = NULL;
+    OCSP_CERTID *cid = NULL;
+    ASN1_TIME *thisupd = NULL, *nextupd = NULL, *revtime = NULL;
+
+    if (cert_status == V_OCSP_CERTSTATUS_REVOKED
+        && !TEST_ptr(revtime = X509_gmtime_adj(NULL, 0)))
+        goto end;
+
+    if (!TEST_ptr(bs = OCSP_BASICRESP_new())
+        || !TEST_ptr(thisupd = X509_time_adj_ex(NULL, 0, this_off_sec, NULL))
+        || !TEST_ptr(nextupd = X509_time_adj_ex(NULL, 0, next_off_sec, NULL))
+        || !TEST_ptr(cid = OCSP_cert_to_id(EVP_sha256(), cert, issuer))
+        || !TEST_ptr(OCSP_basic_add1_status(bs, cid, cert_status, 0, revtime,
+            thisupd, nextupd))
+        || !TEST_true(OCSP_basic_sign(bs, signer, signer_key, EVP_sha256(),
+            NULL, OCSP_NOCERTS)))
+        goto end;
+
+    resp = OCSP_response_create(resp_status, bs);
+
+end:
+    ASN1_TIME_free(revtime);
+    ASN1_TIME_free(thisupd);
+    ASN1_TIME_free(nextupd);
+    OCSP_CERTID_free(cid);
+    OCSP_BASICRESP_free(bs);
+    return resp;
+}
+
+/* Wrap |resp| into a stack, taking ownership on success. */
+static STACK_OF(OCSP_RESPONSE) *make_ocsp_resp_stack(OCSP_RESPONSE *resp)
+{
+    STACK_OF(OCSP_RESPONSE) *sk = sk_OCSP_RESPONSE_new_null();
+
+    if (!TEST_ptr(sk))
+        return NULL;
+    if (!TEST_true(sk_OCSP_RESPONSE_push(sk, resp))) {
+        sk_OCSP_RESPONSE_free(sk);
+        return NULL;
+    }
+    return sk;
+}
+
+/*
+ * Verify |leaf| against |root| with the stapled |resps| and |flags|, taking
+ * ownership of |resps|. Returns X509_V_OK or an X509_V_ERR_xxx code. The
+ * X509_verify_cert() call is wrapped for malloc-failure injection.
+ */
+static int verify_ocsp(X509 *leaf, X509 *root, STACK_OF(OCSP_RESPONSE) *resps,
+    unsigned long flags)
+{
+    X509_STORE *store = X509_STORE_new();
+    X509_STORE_CTX *ctx = X509_STORE_CTX_new();
+    X509_VERIFY_PARAM *param = X509_VERIFY_PARAM_new();
+    int status = X509_V_ERR_UNSPECIFIED;
+
+    if (!TEST_ptr(store) || !TEST_ptr(ctx) || !TEST_ptr(param))
+        goto end;
+
+    if (!TEST_true(X509_STORE_add_cert(store, root))
+        || !TEST_true(X509_STORE_CTX_init(ctx, store, leaf, NULL)))
+        goto end;
+
+    X509_STORE_CTX_set_ocsp_resp(ctx, resps);
+
+    X509_VERIFY_PARAM_set_depth(param, 16);
+    if (flags != 0)
+        X509_VERIFY_PARAM_set_flags(param, flags);
+    X509_STORE_CTX_set0_param(ctx, param);
+    param = NULL;
+
+    ERR_clear_error();
+    MFAIL_start();
+    status = X509_verify_cert(ctx) == 1 ? X509_V_OK
+                                        : X509_STORE_CTX_get_error(ctx);
+    MFAIL_end();
+
+end:
+    X509_VERIFY_PARAM_free(param);
+    X509_STORE_CTX_free(ctx);
+    X509_STORE_free(store);
+    sk_OCSP_RESPONSE_pop_free(resps, OCSP_RESPONSE_free);
+    return status;
+}
+
+/*
+ * Build a single response for the leaf and verify it, expecting |expected|.
+ * Every failure inside check_cert_ocsp_resp() surfaces as
+ * X509_V_ERR_OCSP_VERIFY_FAILED at the X509_verify_cert() level.
+ */
+static int run_ocsp_verify(int resp_status, int cert_status, int this_off_sec,
+    int next_off_sec, int expected)
+{
+    X509 *root = NULL, *leaf = NULL;
+    EVP_PKEY *root_key = NULL;
+    OCSP_RESPONSE *resp = NULL;
+    STACK_OF(OCSP_RESPONSE) *resps = NULL;
+    int testresult = 0;
+
+    if (!load_pki(&root, &root_key, &leaf))
+        goto end;
+
+    if (!TEST_ptr(resp = make_ocsp_response(leaf, root, resp_status, cert_status,
+                      this_off_sec, next_off_sec, root, root_key))
+        || !TEST_ptr(resps = make_ocsp_resp_stack(resp)))
+        goto end;
+    resp = NULL; /* owned by resps */
+
+    testresult = TEST_int_eq(verify_ocsp(leaf, root, resps,
+                                 X509_V_FLAG_OCSP_RESP_CHECK),
+        expected);
+    resps = NULL; /* freed by verify_ocsp */
+
+end:
+    sk_OCSP_RESPONSE_pop_free(resps, OCSP_RESPONSE_free);
+    OCSP_RESPONSE_free(resp);
+    EVP_PKEY_free(root_key);
+    X509_free(leaf);
+    X509_free(root);
+    return testresult;
+}
+
+/*
+ * A good response that expired a moment ago is still accepted, because stapled
+ * responses are honoured for up to five minutes past nextUpdate.
+ */
+static int test_ocsp_resp_good(void)
+{
+    return run_ocsp_verify(OCSP_RESPONSE_STATUS_SUCCESSFUL,
+        V_OCSP_CERTSTATUS_GOOD, -10 * 60, -2 * 60, X509_V_OK);
+}
+
+/* An outer response status other than successful is rejected. */
+static int test_ocsp_resp_not_successful(void)
+{
+    return run_ocsp_verify(OCSP_RESPONSE_STATUS_TRYLATER,
+        V_OCSP_CERTSTATUS_GOOD, 0, 24 * 60 * 60, X509_V_ERR_OCSP_VERIFY_FAILED);
+}
+
+/* A response more than five minutes past nextUpdate is rejected as expired. */
+static int test_ocsp_resp_expired(void)
+{
+    return run_ocsp_verify(OCSP_RESPONSE_STATUS_SUCCESSFUL,
+        V_OCSP_CERTSTATUS_GOOD, -20 * 60, -10 * 60, X509_V_ERR_OCSP_VERIFY_FAILED);
+}
+
+/* No response for the leaf's depth: sk_OCSP_RESPONSE_num() <= error_depth. */
+static int test_ocsp_resp_none(void)
+{
+    X509 *root = NULL, *leaf = NULL;
+    STACK_OF(OCSP_RESPONSE) *resps = NULL;
+    int testresult = 0;
+
+    if (!load_pki(&root, NULL, &leaf)
+        || !TEST_ptr(resps = sk_OCSP_RESPONSE_new_null()))
+        goto end;
+
+    testresult = TEST_int_eq(verify_ocsp(leaf, root, resps,
+                                 X509_V_FLAG_OCSP_RESP_CHECK),
+        X509_V_ERR_OCSP_VERIFY_FAILED);
+    resps = NULL; /* freed by verify_ocsp */
+
+end:
+    sk_OCSP_RESPONSE_pop_free(resps, OCSP_RESPONSE_free);
+    X509_free(leaf);
+    X509_free(root);
+    return testresult;
+}
+
+/*
+ * A well-formed response carrying a single response for a different certificate:
+ * no CertID matches the leaf, so no status is found for it.
+ */
+static int test_ocsp_resp_wrong_cert(void)
+{
+    X509 *root = NULL, *leaf = NULL;
+    EVP_PKEY *root_key = NULL;
+    OCSP_RESPONSE *resp = NULL;
+    STACK_OF(OCSP_RESPONSE) *resps = NULL;
+    int testresult = 0;
+
+    if (!load_pki(&root, &root_key, &leaf))
+        goto end;
+
+    /* the response is about the root, not the leaf being verified */
+    if (!TEST_ptr(resp = make_ocsp_response(root, root,
+                      OCSP_RESPONSE_STATUS_SUCCESSFUL, V_OCSP_CERTSTATUS_GOOD, 0,
+                      24 * 60 * 60, root, root_key))
+        || !TEST_ptr(resps = make_ocsp_resp_stack(resp)))
+        goto end;
+    resp = NULL; /* owned by resps */
+
+    testresult = TEST_int_eq(verify_ocsp(leaf, root, resps,
+                                 X509_V_FLAG_OCSP_RESP_CHECK),
+        X509_V_ERR_OCSP_VERIFY_FAILED);
+    resps = NULL; /* freed by verify_ocsp */
+
+end:
+    sk_OCSP_RESPONSE_pop_free(resps, OCSP_RESPONSE_free);
+    OCSP_RESPONSE_free(resp);
+    EVP_PKEY_free(root_key);
+    X509_free(leaf);
+    X509_free(root);
+    return testresult;
+}
+
+/* Exercise the success path under mfail. */
+static int test_ocsp_resp_mfail(void)
+{
+    X509 *root = NULL, *leaf = NULL;
+    EVP_PKEY *root_key = NULL;
+    OCSP_RESPONSE *resp = NULL;
+    STACK_OF(OCSP_RESPONSE) *resps = NULL;
+    int testresult = 0;
+
+    if (!load_pki(&root, &root_key, &leaf))
+        goto end;
+
+    if (!TEST_ptr(resp = make_ocsp_response(leaf, root,
+                      OCSP_RESPONSE_STATUS_SUCCESSFUL, V_OCSP_CERTSTATUS_GOOD, 0,
+                      24 * 60 * 60, root, root_key))
+        || !TEST_ptr(resps = make_ocsp_resp_stack(resp)))
+        goto end;
+    resp = NULL; /* owned by resps */
+
+    testresult = verify_ocsp(leaf, root, resps, X509_V_FLAG_OCSP_RESP_CHECK)
+        == X509_V_OK;
+    resps = NULL; /* freed by verify_ocsp */
+
+end:
+    sk_OCSP_RESPONSE_pop_free(resps, OCSP_RESPONSE_free);
+    OCSP_RESPONSE_free(resp);
+    EVP_PKEY_free(root_key);
+    X509_free(leaf);
+    X509_free(root);
+    return testresult;
+}
+
+#endif /* OPENSSL_NO_OCSP */
+
+int setup_tests(void)
+{
+#ifndef OPENSSL_NO_OCSP
+    ADD_TEST(test_ocsp_resp_good);
+    ADD_TEST(test_ocsp_resp_not_successful);
+    ADD_TEST(test_ocsp_resp_expired);
+    ADD_TEST(test_ocsp_resp_none);
+    ADD_TEST(test_ocsp_resp_wrong_cert);
+    ADD_MFAIL_NO_CHECK_TEST(test_ocsp_resp_mfail);
+#endif
+    return 1;
+}
diff --git a/test/recipes/80-test_ocsp.t b/test/recipes/80-test_ocsp.t
index 3e12a0b23e..62ee9f13ca 100644
--- a/test/recipes/80-test_ocsp.t
+++ b/test/recipes/80-test_ocsp.t
@@ -54,7 +54,7 @@ sub test_ocsp {
                   $title); });
 }

-plan tests => 13;
+plan tests => 14;

 subtest "=== VALID OCSP RESPONSES ===" => sub {
     plan tests => 7;
@@ -232,6 +232,12 @@ subtest "=== OCSP API TESTS===" => sub {
                  "running ocspapitest");
 };

+subtest "=== OCSP VERIFICATION TESTS ===" => sub {
+    plan tests => 1;
+
+    ok(run(test(["ocsptest"])), "running ocsptest");
+};
+
 subtest "=== UNTRUSTED ISSUER HINTS ===" => sub {
     plan tests => 1;