// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package pki

import (
	"bytes"
	"context"
	"crypto"
	"crypto/x509"
	"encoding/base64"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"
	"testing"
	"time"

	"github.com/hashicorp/go-secure-stdlib/parseutil"
	vaulthttp "github.com/hashicorp/vault/http"
	"github.com/hashicorp/vault/sdk/helper/testhelpers/schema"
	"github.com/hashicorp/vault/sdk/logical"
	"github.com/hashicorp/vault/vault"
	"github.com/stretchr/testify/require"
	"golang.org/x/crypto/ocsp"
)

// If the ocsp_disabled flag is set to true in the crl configuration make sure we always
// return an Unauthorized error back as we assume an end-user disabling the feature does
// not want us to act as the OCSP authority and the RFC specifies this is the appropriate response.
func TestOcsp_Disabled(t *testing.T) {
	t.Parallel()
	type testArgs struct {
		reqType string
	}
	var tests []testArgs
	for _, reqType := range []string{"get", "post"} {
		tests = append(tests, testArgs{
			reqType: reqType,
		})
	}
	for _, tt := range tests {
		localTT := tt
		t.Run(localTT.reqType, func(t *testing.T) {
			b, s, testEnv := setupOcspEnv(t, "rsa")
			resp, err := CBWrite(b, s, "config/crl", map[string]interface{}{
				"ocsp_disable": "true",
			})
			requireSuccessNonNilResponse(t, resp, err)
			resp, err = SendOcspRequest(t, b, s, localTT.reqType, testEnv.leafCertIssuer1, testEnv.issuer1, crypto.SHA1)
			require.NoError(t, err)
			requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
			require.Equal(t, 401, resp.Data["http_status_code"])
			require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
			respDer := resp.Data["http_raw_body"].([]byte)

			require.Equal(t, ocsp.UnauthorizedErrorResponse, respDer)
		})
	}
}

// If we can't find the issuer within the request and have no default issuer to sign an Unknown response
// with return an UnauthorizedErrorResponse/according to/the RFC, similar to if we are disabled (lack of authority)
// This behavior differs from CRLs when an issuer is removed from a mount.
func TestOcsp_UnknownIssuerWithNoDefault(t *testing.T) {
	t.Parallel()

	_, _, testEnv := setupOcspEnv(t, "ec")
	// Create another completely empty mount so the created issuer/certificate above is unknown
	b, s := CreateBackendWithStorage(t)

	resp, err := SendOcspRequest(t, b, s, "get", testEnv.leafCertIssuer1, testEnv.issuer1, crypto.SHA1)
	require.NoError(t, err)
	requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
	require.Equal(t, 401, resp.Data["http_status_code"])
	require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
	respDer := resp.Data["http_raw_body"].([]byte)

	require.Equal(t, ocsp.UnauthorizedErrorResponse, respDer)
}

// If the issuer in the request does exist, but the request coming in associates the serial with the
// wrong issuer return an Unknown response back to the caller.
func TestOcsp_WrongIssuerInRequest(t *testing.T) {
	t.Parallel()

	b, s, testEnv := setupOcspEnv(t, "ec")
	serial := serialFromCert(testEnv.leafCertIssuer1)
	resp, err := CBWrite(b, s, "revoke", map[string]interface{}{
		"serial_number": serial,
	})
	requireSuccessNonNilResponse(t, resp, err, "revoke")

	resp, err = SendOcspRequest(t, b, s, "get", testEnv.leafCertIssuer1, testEnv.issuer2, crypto.SHA1)
	require.NoError(t, err)
	requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
	require.Equal(t, 200, resp.Data["http_status_code"])
	require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
	respDer := resp.Data["http_raw_body"].([]byte)

	ocspResp, err := ocsp.ParseResponse(respDer, testEnv.issuer1)
	require.NoError(t, err, "parsing ocsp get response")

	require.Equal(t, ocsp.Unknown, ocspResp.Status)
}

// Verify that requests we can't properly decode result in the correct response of MalformedRequestError
func TestOcsp_MalformedRequests(t *testing.T) {
	t.Parallel()
	type testArgs struct {
		reqType string
	}
	var tests []testArgs
	for _, reqType := range []string{"get", "post"} {
		tests = append(tests, testArgs{
			reqType: reqType,
		})
	}
	for _, tt := range tests {
		localTT := tt
		t.Run(localTT.reqType, func(t *testing.T) {
			b, s, _ := setupOcspEnv(t, "rsa")
			badReq := []byte("this is a bad request")
			var resp *logical.Response
			var err error
			switch localTT.reqType {
			case "get":
				resp, err = sendOcspGetRequest(b, s, badReq)
			case "post":
				resp, err = sendOcspPostRequest(b, s, badReq)
			default:
				t.Fatalf("bad request type")
			}
			require.NoError(t, err)
			requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
			require.Equal(t, 400, resp.Data["http_status_code"])
			require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
			respDer := resp.Data["http_raw_body"].([]byte)

			require.Equal(t, ocsp.MalformedRequestErrorResponse, respDer)
		})
	}
}

// Validate that we properly handle a revocation entry that contains an issuer ID that no longer exists,
// the best we can do in this use case is to respond back with the default issuer that we don't know
// the issuer that they are requesting (we can't guarantee that the client is actually requesting a serial
// from that issuer)
func TestOcsp_InvalidIssuerIdInRevocationEntry(t *testing.T) {
	t.Parallel()

	b, s, testEnv := setupOcspEnv(t, "ec")
	ctx := context.Background()

	// Revoke the entry
	serial := serialFromCert(testEnv.leafCertIssuer1)
	resp, err := CBWrite(b, s, "revoke", map[string]interface{}{
		"serial_number": serial,
	})
	requireSuccessNonNilResponse(t, resp, err, "revoke")

	// Twiddle the entry so that the issuer id is no longer valid.
	storagePath := revokedPath + normalizeSerial(serial)
	var revInfo revocationInfo
	revEntry, err := s.Get(ctx, storagePath)
	require.NoError(t, err, "failed looking up storage path: %s", storagePath)
	err = revEntry.DecodeJSON(&revInfo)
	require.NoError(t, err, "failed decoding storage entry: %v", revEntry)
	revInfo.CertificateIssuer = "00000000-0000-0000-0000-000000000000"
	revEntry, err = logical.StorageEntryJSON(storagePath, revInfo)
	require.NoError(t, err, "failed re-encoding revocation info: %v", revInfo)
	err = s.Put(ctx, revEntry)
	require.NoError(t, err, "failed writing out new revocation entry: %v", revEntry)

	// Send the request
	resp, err = SendOcspRequest(t, b, s, "get", testEnv.leafCertIssuer1, testEnv.issuer1, crypto.SHA1)
	require.NoError(t, err)
	requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
	require.Equal(t, 200, resp.Data["http_status_code"])
	require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
	respDer := resp.Data["http_raw_body"].([]byte)

	ocspResp, err := ocsp.ParseResponse(respDer, testEnv.issuer1)
	require.NoError(t, err, "parsing ocsp get response")

	require.Equal(t, ocsp.Unknown, ocspResp.Status)
}

// Validate that we properly handle an unknown issuer use-case but that the default issuer
// does not have the OCSP usage flag set, we can't do much else other than reply with an
// Unauthorized response.
func TestOcsp_UnknownIssuerIdWithDefaultHavingOcspUsageRemoved(t *testing.T) {
	t.Parallel()

	b, s, testEnv := setupOcspEnv(t, "ec")
	ctx := context.Background()

	// Revoke the entry
	serial := serialFromCert(testEnv.leafCertIssuer1)
	resp, err := CBWrite(b, s, "revoke", map[string]interface{}{
		"serial_number": serial,
	})
	schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("revoke"), logical.UpdateOperation), resp, true)
	requireSuccessNonNilResponse(t, resp, err, "revoke")

	// Twiddle the entry so that the issuer id is no longer valid.
	storagePath := revokedPath + normalizeSerial(serial)
	var revInfo revocationInfo
	revEntry, err := s.Get(ctx, storagePath)
	require.NoError(t, err, "failed looking up storage path: %s", storagePath)
	err = revEntry.DecodeJSON(&revInfo)
	require.NoError(t, err, "failed decoding storage entry: %v", revEntry)
	revInfo.CertificateIssuer = "00000000-0000-0000-0000-000000000000"
	revEntry, err = logical.StorageEntryJSON(storagePath, revInfo)
	require.NoError(t, err, "failed re-encoding revocation info: %v", revInfo)
	err = s.Put(ctx, revEntry)
	require.NoError(t, err, "failed writing out new revocation entry: %v", revEntry)

	// Update our issuers to no longer have the OcspSigning usage
	resp, err = CBPatch(b, s, "issuer/"+testEnv.issuerId1.String(), map[string]interface{}{
		"usage": "read-only,issuing-certificates,crl-signing",
	})
	requireSuccessNonNilResponse(t, resp, err, "failed resetting usage flags on issuer1")
	resp, err = CBPatch(b, s, "issuer/"+testEnv.issuerId2.String(), map[string]interface{}{
		"usage": "read-only,issuing-certificates,crl-signing",
	})
	requireSuccessNonNilResponse(t, resp, err, "failed resetting usage flags on issuer2")

	// Send the request
	resp, err = SendOcspRequest(t, b, s, "get", testEnv.leafCertIssuer1, testEnv.issuer1, crypto.SHA1)
	require.NoError(t, err)
	requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
	require.Equal(t, 401, resp.Data["http_status_code"])
	require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
	respDer := resp.Data["http_raw_body"].([]byte)

	require.Equal(t, ocsp.UnauthorizedErrorResponse, respDer)
}

// Verify that if we do have a revoked certificate entry for the request, that matches an
// issuer but that issuer does not have the OcspUsage flag set that we return an Unauthorized
// response back to the caller
func TestOcsp_RevokedCertHasIssuerWithoutOcspUsage(t *testing.T) {
	b, s, testEnv := setupOcspEnv(t, "ec")

	// Revoke our certificate
	resp, err := CBWrite(b, s, "revoke", map[string]interface{}{
		"serial_number": serialFromCert(testEnv.leafCertIssuer1),
	})
	requireSuccessNonNilResponse(t, resp, err, "revoke")

	// Update our issuer to no longer have the OcspSigning usage
	resp, err = CBPatch(b, s, "issuer/"+testEnv.issuerId1.String(), map[string]interface{}{
		"usage": "read-only,issuing-certificates,crl-signing",
	})
	requireSuccessNonNilResponse(t, resp, err, "failed resetting usage flags on issuer")
	requireFieldsSetInResp(t, resp, "usage")

	// Do not assume a specific ordering for usage...
	usages, err := NewIssuerUsageFromNames(strings.Split(resp.Data["usage"].(string), ","))
	require.NoError(t, err, "failed parsing usage return value")
	require.True(t, usages.HasUsage(IssuanceUsage))
	require.True(t, usages.HasUsage(CRLSigningUsage))
	require.False(t, usages.HasUsage(OCSPSigningUsage))

	// Request an OCSP request from it, we should get an Unauthorized response back
	resp, err = SendOcspRequest(t, b, s, "get", testEnv.leafCertIssuer1, testEnv.issuer1, crypto.SHA1)
	requireSuccessNonNilResponse(t, resp, err, "ocsp get request")
	requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
	require.Equal(t, 401, resp.Data["http_status_code"])
	require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
	respDer := resp.Data["http_raw_body"].([]byte)

	require.Equal(t, ocsp.UnauthorizedErrorResponse, respDer)
}

// Verify if our matching issuer for a revocation entry has no key associated with it that
// we bail with an Unauthorized response.
func TestOcsp_RevokedCertHasIssuerWithoutAKey(t *testing.T) {
	b, s, testEnv := setupOcspEnv(t, "ec")

	// Revoke our certificate
	resp, err := CBWrite(b, s, "revoke", map[string]interface{}{
		"serial_number": serialFromCert(testEnv.leafCertIssuer1),
	})
	requireSuccessNonNilResponse(t, resp, err, "revoke")

	// Delete the key associated with our issuer
	resp, err = CBRead(b, s, "issuer/"+testEnv.issuerId1.String())
	requireSuccessNonNilResponse(t, resp, err, "failed reading issuer")
	requireFieldsSetInResp(t, resp, "key_id")
	keyId := resp.Data["key_id"].(keyID)

	// This is a bit naughty but allow me to delete the key...
	sc := b.makeStorageContext(context.Background(), s)
	issuer, err := sc.fetchIssuerById(testEnv.issuerId1)
	require.NoError(t, err, "failed to get issuer from storage")
	issuer.KeyID = ""
	err = sc.writeIssuer(issuer)
	require.NoError(t, err, "failed to write issuer update")

	resp, err = CBDelete(b, s, "key/"+keyId.String())
	requireSuccessNonNilResponse(t, resp, err, "failed deleting key")

	// Request an OCSP request from it, we should get an Unauthorized response back
	resp, err = SendOcspRequest(t, b, s, "get", testEnv.leafCertIssuer1, testEnv.issuer1, crypto.SHA1)
	requireSuccessNonNilResponse(t, resp, err, "ocsp get request")
	requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
	require.Equal(t, 401, resp.Data["http_status_code"])
	require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
	respDer := resp.Data["http_raw_body"].([]byte)

	require.Equal(t, ocsp.UnauthorizedErrorResponse, respDer)
}

// Verify if for some reason an end-user has rotated an existing certificate using the same
// key so our algo matches multiple issuers and one has OCSP usage disabled. We expect that
// even if a prior issuer issued the certificate, the new matching issuer can respond and sign
// the response to the caller on its behalf.
//
// NOTE: This test is a bit at the mercy of iteration order of the issuer ids.
//
//	If it becomes flaky, most likely something is wrong in the code
//	and not the test.
func TestOcsp_MultipleMatchingIssuersOneWithoutSigningUsage(t *testing.T) {
	b, s, testEnv := setupOcspEnv(t, "ec")

	// Create a matching issuer as issuer1 with the same backing key
	resp, err := CBWrite(b, s, "root/rotate/existing", map[string]interface{}{
		"key_ref":     testEnv.keyId1,
		"ttl":         "40h",
		"common_name": "example-ocsp.com",
	})
	requireSuccessNonNilResponse(t, resp, err, "rotate issuer failed")
	requireFieldsSetInResp(t, resp, "issuer_id")
	rotatedCert := parseCert(t, resp.Data["certificate"].(string))

	// Remove ocsp signing from our issuer
	resp, err = CBPatch(b, s, "issuer/"+testEnv.issuerId1.String(), map[string]interface{}{
		"usage": "read-only,issuing-certificates,crl-signing",
	})
	requireSuccessNonNilResponse(t, resp, err, "failed resetting usage flags on issuer")
	requireFieldsSetInResp(t, resp, "usage")
	// Do not assume a specific ordering for usage...
	usages, err := NewIssuerUsageFromNames(strings.Split(resp.Data["usage"].(string), ","))
	require.NoError(t, err, "failed parsing usage return value")
	require.True(t, usages.HasUsage(IssuanceUsage))
	require.True(t, usages.HasUsage(CRLSigningUsage))
	require.False(t, usages.HasUsage(OCSPSigningUsage))

	// Request an OCSP request from it, we should get a Good response back, from the rotated cert
	resp, err = SendOcspRequest(t, b, s, "get", testEnv.leafCertIssuer1, testEnv.issuer1, crypto.SHA1)
	requireSuccessNonNilResponse(t, resp, err, "ocsp get request")
	requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
	require.Equal(t, 200, resp.Data["http_status_code"])
	require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
	respDer := resp.Data["http_raw_body"].([]byte)

	ocspResp, err := ocsp.ParseResponse(respDer, testEnv.issuer1)
	require.NoError(t, err, "parsing ocsp get response")

	require.Equal(t, ocsp.Good, ocspResp.Status)
	require.Equal(t, crypto.SHA1, ocspResp.IssuerHash)
	require.Equal(t, 0, ocspResp.RevocationReason)
	require.Equal(t, testEnv.leafCertIssuer1.SerialNumber, ocspResp.SerialNumber)

	requireOcspSignatureAlgoForKey(t, rotatedCert.SignatureAlgorithm, ocspResp.SignatureAlgorithm)
	requireOcspResponseSignedBy(t, ocspResp, rotatedCert)
}

// Make sure OCSP GET/POST requests work through the entire stack, and not just
// through the quicker backend layer the other tests are doing.
func TestOcsp_HigherLevel(t *testing.T) {
	t.Parallel()
	coreConfig := &vault.CoreConfig{
		LogicalBackends: map[string]logical.Factory{
			"pki": Factory,
		},
	}
	cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
		HandlerFunc: vaulthttp.Handler,
	})
	cluster.Start()
	defer cluster.Cleanup()
	client := cluster.Cores[0].Client
	mountPKIEndpoint(t, client, "pki")
	resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{
		"key_type":    "ec",
		"common_name": "root-ca.com",
		"ttl":         "600h",
	})

	require.NoError(t, err, "error generating root ca: %v", err)
	require.NotNil(t, resp, "expected ca info from root")

	issuerCert := parseCert(t, resp.Data["certificate"].(string))

	resp, err = client.Logical().Write("pki/roles/example", map[string]interface{}{
		"allowed_domains":  "example.com",
		"allow_subdomains": "true",
		"no_store":         "false", // make sure we store this cert
		"max_ttl":          "1h",
		"key_type":         "ec",
	})
	require.NoError(t, err, "error setting up pki role: %v", err)

	resp, err = client.Logical().Write("pki/issue/example", map[string]interface{}{
		"common_name": "test.example.com",
		"ttl":         "15m",
	})
	require.NoError(t, err, "error issuing certificate: %v", err)
	require.NotNil(t, resp, "got nil response from issuing request")
	certToRevoke := parseCert(t, resp.Data["certificate"].(string))
	serialNum := resp.Data["serial_number"].(string)

	// Revoke the certificate
	resp, err = client.Logical().Write("pki/revoke", map[string]interface{}{
		"serial_number": serialNum,
	})
	require.NoError(t, err, "error revoking certificate: %v", err)
	require.NotNil(t, resp, "got nil response from revoke")

	// Make sure that OCSP handler responds properly
	ocspReq := generateRequest(t, crypto.SHA256, certToRevoke, issuerCert)
	ocspPostReq := client.NewRequest(http.MethodPost, "/v1/pki/ocsp")
	ocspPostReq.Headers.Set("Content-Type", "application/ocsp-request")
	ocspPostReq.BodyBytes = ocspReq
	rawResp, err := client.RawRequest(ocspPostReq)
	require.NoError(t, err, "failed sending ocsp post request")

	require.Equal(t, 200, rawResp.StatusCode)
	require.Equal(t, ocspResponseContentType, rawResp.Header.Get("Content-Type"))
	bodyReader := rawResp.Body
	respDer, err := io.ReadAll(bodyReader)
	bodyReader.Close()
	require.NoError(t, err, "failed reading response body")

	ocspResp, err := ocsp.ParseResponse(respDer, issuerCert)
	require.NoError(t, err, "parsing ocsp get response")

	require.Equal(t, ocsp.Revoked, ocspResp.Status)
	require.Equal(t, certToRevoke.SerialNumber, ocspResp.SerialNumber)

	// Test OCSP Get request for ocsp
	urlEncoded := base64.StdEncoding.EncodeToString(ocspReq)
	if strings.Contains(urlEncoded, "//") {
		// workaround known redirect bug that is difficult to fix
		t.Skipf("VAULT-13630 - Skipping GET OCSP test with encoded issuer cert containing // triggering redirection bug")
	}

	ocspGetReq := client.NewRequest(http.MethodGet, "/v1/pki/ocsp/"+urlEncoded)
	ocspGetReq.Headers.Set("Content-Type", "application/ocsp-request")
	rawResp, err = client.RawRequest(ocspGetReq)
	require.NoError(t, err, "failed sending ocsp get request")

	require.Equal(t, 200, rawResp.StatusCode)
	require.Equal(t, ocspResponseContentType, rawResp.Header.Get("Content-Type"))
	bodyReader = rawResp.Body
	respDer, err = io.ReadAll(bodyReader)
	bodyReader.Close()
	require.NoError(t, err, "failed reading response body")

	ocspResp, err = ocsp.ParseResponse(respDer, issuerCert)
	require.NoError(t, err, "parsing ocsp get response")

	require.Equal(t, ocsp.Revoked, ocspResp.Status)
	require.Equal(t, certToRevoke.SerialNumber, ocspResp.SerialNumber)
}

// TestOcsp_NextUpdate make sure that we are setting the appropriate values
// for the NextUpdate field within our responses.
func TestOcsp_NextUpdate(t *testing.T) {
	// Within the runOcspRequestTest, with a ocspExpiry of 0,
	// we will validate that NextUpdate was not set in the response
	runOcspRequestTest(t, "POST", "ec", 0, 0, crypto.SHA256, 0)

	// Within the runOcspRequestTest, with a ocspExpiry of 24 hours, we will validate
	// that NextUpdate is set and has a time 24 hours larger than ThisUpdate
	runOcspRequestTest(t, "POST", "ec", 0, 0, crypto.SHA256, 24*time.Hour)
}

func TestOcsp_ValidRequests(t *testing.T) {
	type caKeyConf struct {
		keyType string
		keyBits int
		sigBits int
	}
	t.Parallel()
	type testArgs struct {
		reqType string
		keyConf caKeyConf
		reqHash crypto.Hash
	}
	var tests []testArgs
	for _, reqType := range []string{"get", "post"} {
		for _, keyConf := range []caKeyConf{
			{"rsa", 0, 0},
			{"rsa", 0, 384},
			{"rsa", 0, 512},
			{"ec", 0, 0},
			{"ec", 521, 0},
		} {
			// "ed25519" is not supported at the moment in x/crypto/ocsp
			for _, requestHash := range []crypto.Hash{crypto.SHA1, crypto.SHA256, crypto.SHA384, crypto.SHA512} {
				tests = append(tests, testArgs{
					reqType: reqType,
					keyConf: keyConf,
					reqHash: requestHash,
				})
			}
		}
	}
	for _, tt := range tests {
		localTT := tt
		testName := fmt.Sprintf("%s-%s-keybits-%d-sigbits-%d-reqHash-%s", localTT.reqType, localTT.keyConf.keyType,
			localTT.keyConf.keyBits,
			localTT.keyConf.sigBits,
			localTT.reqHash)
		t.Run(testName, func(t *testing.T) {
			runOcspRequestTest(t, localTT.reqType, localTT.keyConf.keyType, localTT.keyConf.keyBits,
				localTT.keyConf.sigBits, localTT.reqHash, 12*time.Hour)
		})
	}
}

func runOcspRequestTest(t *testing.T, requestType string, caKeyType string,
	caKeyBits int, caKeySigBits int, requestHash crypto.Hash, ocspExpiry time.Duration,
) {
	b, s, testEnv := setupOcspEnvWithCaKeyConfig(t, caKeyType, caKeyBits, caKeySigBits, ocspExpiry)

	// Non-revoked cert
	resp, err := SendOcspRequest(t, b, s, requestType, testEnv.leafCertIssuer1, testEnv.issuer1, requestHash)
	requireSuccessNonNilResponse(t, resp, err, "ocsp get request")
	requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
	require.Equal(t, 200, resp.Data["http_status_code"])
	require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
	respDer := resp.Data["http_raw_body"].([]byte)

	ocspResp, err := ocsp.ParseResponse(respDer, testEnv.issuer1)
	require.NoError(t, err, "parsing ocsp get response")

	require.Equal(t, ocsp.Good, ocspResp.Status)
	require.Equal(t, requestHash, ocspResp.IssuerHash)
	require.Equal(t, 0, ocspResp.RevocationReason)
	require.Equal(t, testEnv.leafCertIssuer1.SerialNumber, ocspResp.SerialNumber)

	requireOcspSignatureAlgoForKey(t, testEnv.issuer1.SignatureAlgorithm, ocspResp.SignatureAlgorithm)
	requireOcspResponseSignedBy(t, ocspResp, testEnv.issuer1)

	// Now revoke it
	resp, err = CBWrite(b, s, "revoke", map[string]interface{}{
		"serial_number": serialFromCert(testEnv.leafCertIssuer1),
	})
	requireSuccessNonNilResponse(t, resp, err, "revoke")

	resp, err = SendOcspRequest(t, b, s, requestType, testEnv.leafCertIssuer1, testEnv.issuer1, requestHash)
	requireSuccessNonNilResponse(t, resp, err, "ocsp get request with revoked")
	requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
	require.Equal(t, 200, resp.Data["http_status_code"])
	require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
	respDer = resp.Data["http_raw_body"].([]byte)

	ocspResp, err = ocsp.ParseResponse(respDer, testEnv.issuer1)
	require.NoError(t, err, "parsing ocsp get response with revoked")

	require.Equal(t, ocsp.Revoked, ocspResp.Status)
	require.Equal(t, requestHash, ocspResp.IssuerHash)
	require.Equal(t, 0, ocspResp.RevocationReason)
	require.Equal(t, testEnv.leafCertIssuer1.SerialNumber, ocspResp.SerialNumber)

	requireOcspSignatureAlgoForKey(t, testEnv.issuer1.SignatureAlgorithm, ocspResp.SignatureAlgorithm)
	requireOcspResponseSignedBy(t, ocspResp, testEnv.issuer1)

	// Request status for our second issuer
	resp, err = SendOcspRequest(t, b, s, requestType, testEnv.leafCertIssuer2, testEnv.issuer2, requestHash)
	requireSuccessNonNilResponse(t, resp, err, "ocsp get request")
	requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
	require.Equal(t, 200, resp.Data["http_status_code"])
	require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
	respDer = resp.Data["http_raw_body"].([]byte)

	ocspResp, err = ocsp.ParseResponse(respDer, testEnv.issuer2)
	require.NoError(t, err, "parsing ocsp get response")

	require.Equal(t, ocsp.Good, ocspResp.Status)
	require.Equal(t, requestHash, ocspResp.IssuerHash)
	require.Equal(t, 0, ocspResp.RevocationReason)
	require.Equal(t, testEnv.leafCertIssuer2.SerialNumber, ocspResp.SerialNumber)

	// Verify that our thisUpdate and nextUpdate fields are updated as expected
	resp, err = CBRead(b, s, "config/crl")
	requireSuccessNonNilResponse(t, resp, err, "failed reading from config/crl")
	requireFieldsSetInResp(t, resp, "ocsp_expiry")
	ocspExpiryRaw := resp.Data["ocsp_expiry"].(string)
	expectedDiff, err := parseutil.ParseDurationSecond(ocspExpiryRaw)
	require.NoError(t, err, "failed to parse default ocsp expiry value")

	thisUpdate := ocspResp.ThisUpdate
	require.Less(t, time.Since(thisUpdate), 10*time.Second, "expected ThisUpdate field to be within the last 10 seconds")
	if expectedDiff != 0 {
		nextUpdate := ocspResp.NextUpdate
		require.False(t, nextUpdate.IsZero(), "nextUpdate field value should have been a non-zero time")
		require.True(t, thisUpdate.Before(nextUpdate),
			fmt.Sprintf("thisUpdate %s, should have been before nextUpdate: %s", thisUpdate, nextUpdate))
		nextUpdateDiff := nextUpdate.Sub(thisUpdate)
		require.Equal(t, expectedDiff, nextUpdateDiff,
			fmt.Sprintf("the delta between thisUpdate %s and nextUpdate: %s should have been around: %s but was %s",
				thisUpdate, nextUpdate, defaultCrlConfig.OcspExpiry, nextUpdateDiff))
	} else {
		// With the config value set to 0, we shouldn't have a NextUpdate field set
		require.True(t, ocspResp.NextUpdate.IsZero(), "nextUpdate value was not zero as expected was: %v", ocspResp.NextUpdate)
	}
	requireOcspSignatureAlgoForKey(t, testEnv.issuer2.SignatureAlgorithm, ocspResp.SignatureAlgorithm)
	requireOcspResponseSignedBy(t, ocspResp, testEnv.issuer2)
}

func requireOcspSignatureAlgoForKey(t *testing.T, expected x509.SignatureAlgorithm, actual x509.SignatureAlgorithm) {
	t.Helper()

	require.Equal(t, expected.String(), actual.String())
}

type ocspTestEnv struct {
	issuer1 *x509.Certificate
	issuer2 *x509.Certificate

	issuerId1 issuerID
	issuerId2 issuerID

	leafCertIssuer1 *x509.Certificate
	leafCertIssuer2 *x509.Certificate

	keyId1 keyID
	keyId2 keyID
}

func setupOcspEnv(t *testing.T, keyType string) (*backend, logical.Storage, *ocspTestEnv) {
	return setupOcspEnvWithCaKeyConfig(t, keyType, 0, 0, 12*time.Hour)
}

func setupOcspEnvWithCaKeyConfig(t *testing.T, keyType string, caKeyBits int, caKeySigBits int, ocspExpiry time.Duration) (*backend, logical.Storage, *ocspTestEnv) {
	b, s := CreateBackendWithStorage(t)
	var issuerCerts []*x509.Certificate
	var leafCerts []*x509.Certificate
	var issuerIds []issuerID
	var keyIds []keyID

	resp, err := CBWrite(b, s, "config/crl", map[string]interface{}{
		"ocsp_enable": true,
		"ocsp_expiry": fmt.Sprintf("%ds", int(ocspExpiry.Seconds())),
	})
	requireSuccessNonNilResponse(t, resp, err, "config/crl failed")

	for i := 0; i < 2; i++ {
		resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{
			"key_type":       keyType,
			"key_bits":       caKeyBits,
			"signature_bits": caKeySigBits,
			"ttl":            "40h",
			"common_name":    "example-ocsp.com",
		})
		requireSuccessNonNilResponse(t, resp, err, "root/generate/internal")
		requireFieldsSetInResp(t, resp, "issuer_id", "key_id")
		issuerId := resp.Data["issuer_id"].(issuerID)
		keyId := resp.Data["key_id"].(keyID)

		resp, err = CBWrite(b, s, "roles/test"+strconv.FormatInt(int64(i), 10), map[string]interface{}{
			"allow_bare_domains": true,
			"allow_subdomains":   true,
			"allowed_domains":    "foobar.com",
			"no_store":           false,
			"generate_lease":     false,
			"issuer_ref":         issuerId,
			"key_type":           keyType,
		})
		requireSuccessNonNilResponse(t, resp, err, "roles/test"+strconv.FormatInt(int64(i), 10))

		resp, err = CBWrite(b, s, "issue/test"+strconv.FormatInt(int64(i), 10), map[string]interface{}{
			"common_name": "test.foobar.com",
		})
		requireSuccessNonNilResponse(t, resp, err, "roles/test"+strconv.FormatInt(int64(i), 10))
		requireFieldsSetInResp(t, resp, "certificate", "issuing_ca", "serial_number")
		leafCert := parseCert(t, resp.Data["certificate"].(string))
		issuingCa := parseCert(t, resp.Data["issuing_ca"].(string))

		issuerCerts = append(issuerCerts, issuingCa)
		leafCerts = append(leafCerts, leafCert)
		issuerIds = append(issuerIds, issuerId)
		keyIds = append(keyIds, keyId)
	}

	testEnv := &ocspTestEnv{
		issuerId1:       issuerIds[0],
		issuer1:         issuerCerts[0],
		leafCertIssuer1: leafCerts[0],
		keyId1:          keyIds[0],

		issuerId2:       issuerIds[1],
		issuer2:         issuerCerts[1],
		leafCertIssuer2: leafCerts[1],
		keyId2:          keyIds[1],
	}

	return b, s, testEnv
}

func SendOcspRequest(t *testing.T, b *backend, s logical.Storage, getOrPost string, cert, issuer *x509.Certificate, requestHash crypto.Hash) (*logical.Response, error) {
	t.Helper()

	ocspRequest := generateRequest(t, requestHash, cert, issuer)

	switch strings.ToLower(getOrPost) {
	case "get":
		return sendOcspGetRequest(b, s, ocspRequest)
	case "post":
		return sendOcspPostRequest(b, s, ocspRequest)
	default:
		t.Fatalf("unsupported value for SendOcspRequest getOrPost arg: %s", getOrPost)
	}
	return nil, nil
}

func sendOcspGetRequest(b *backend, s logical.Storage, ocspRequest []byte) (*logical.Response, error) {
	urlEncoded := base64.StdEncoding.EncodeToString(ocspRequest)
	return CBRead(b, s, "ocsp/"+urlEncoded)
}

func sendOcspPostRequest(b *backend, s logical.Storage, ocspRequest []byte) (*logical.Response, error) {
	reader := io.NopCloser(bytes.NewReader(ocspRequest))
	resp, err := b.HandleRequest(context.Background(), &logical.Request{
		Operation:  logical.UpdateOperation,
		Path:       "ocsp",
		Storage:    s,
		MountPoint: "pki/",
		HTTPRequest: &http.Request{
			Body: reader,
		},
	})

	return resp, err
}
