730 lines
28 KiB
Go
730 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)
|
|
require.Equal(t, rotatedCert, ocspResp.Certificate)
|
|
|
|
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, issuerCert, ocspResp.Certificate)
|
|
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, issuerCert, ocspResp.Certificate)
|
|
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, testEnv.issuer1, ocspResp.Certificate)
|
|
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, testEnv.issuer1, ocspResp.Certificate)
|
|
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, testEnv.issuer2, ocspResp.Certificate)
|
|
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)
|
|
}
|