open-vault/builtin/logical/pki/path_ocsp_test.go
Alexander Scheel 4190212bbb
Remove extraneous certificate from OCSP response (#20201)
* Remove extraneous certificate from OCSP response

Since the issuer used to sign the certificate also signs the OCSP
response, no additional information is added by sending the issuer again
in the certs field of the BasicOCSPResponse structure. Removing it saves
bytes and avoids confusing Go-based OCSP verifiers which cannot handle
the cert issuer being duplicated in the certs field.

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add changelog entry

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

---------

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
2023-04-17 16:40:26 +00:00

724 lines
28 KiB
Go

// 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/vault/sdk/helper/testhelpers/schema"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/vault"
"github.com/hashicorp/vault/sdk/logical"
"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)
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)
}
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)
})
}
}
func runOcspRequestTest(t *testing.T, requestType string, caKeyType string, caKeyBits int, caKeySigBits int, requestHash crypto.Hash) {
b, s, testEnv := setupOcspEnvWithCaKeyConfig(t, caKeyType, caKeyBits, caKeySigBits)
// 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
thisUpdate := ocspResp.ThisUpdate
nextUpdate := ocspResp.NextUpdate
require.True(t, thisUpdate.Before(nextUpdate),
fmt.Sprintf("thisUpdate %s, should have been before nextUpdate: %s", thisUpdate, nextUpdate))
nextUpdateDiff := nextUpdate.Sub(thisUpdate)
expectedDiff, err := time.ParseDuration(defaultCrlConfig.OcspExpiry)
require.NoError(t, err, "failed to parse default ocsp expiry value")
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))
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)
}
func setupOcspEnvWithCaKeyConfig(t *testing.T, keyType string, caKeyBits int, caKeySigBits int) (*backend, logical.Storage, *ocspTestEnv) {
b, s := CreateBackendWithStorage(t)
var issuerCerts []*x509.Certificate
var leafCerts []*x509.Certificate
var issuerIds []issuerID
var keyIds []keyID
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
}
func generateRequest(t *testing.T, requestHash crypto.Hash, cert *x509.Certificate, issuer *x509.Certificate) []byte {
t.Helper()
opts := &ocsp.RequestOptions{Hash: requestHash}
ocspRequestDer, err := ocsp.CreateRequest(cert, issuer, opts)
require.NoError(t, err, "Failed generating OCSP request")
return ocspRequestDer
}
func requireOcspResponseSignedBy(t *testing.T, ocspResp *ocsp.Response, issuer *x509.Certificate) {
t.Helper()
err := ocspResp.CheckSignatureFrom(issuer)
require.NoError(t, err, "Failed signature verification of ocsp response: %w", err)
}