mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-02 03:27:54 +00:00
Add an OCSP responder to Vault's PKI plugin (#16723)
* Refactor existing CRL function to storage getRevocationConfig * Introduce ocsp_disable config option in config/crl * Introduce OCSPSigning usage flag on issuer * Add ocsp-request passthrough within lower layers of Vault * Add OCSP responder to Vault PKI * Add API documentation for OCSP * Add cl * Revert PKI storage migration modifications for OCSP * Smaller PR feedback items - pki.mdx doc update - parens around logical.go comment to indicate DER encoded request is related to OCSP and not the snapshots - Use AllIssuers instead of writing them all out - Drop zero initialization of crl config's Disable flag if not present - Upgrade issuer on the fly instead of an initial migration * Additional clean up backing out the writeRevocationConfig refactoring * Remove Dirty issuer flag and update comment about not writing upgrade to storage * Address PR feedback and return Unknown response when mismatching issuer * make fmt * PR Feedback. * More PR feedback - Leverage ocsp response constant - Remove duplicate errors regarding unknown issuers
This commit is contained in:
543
builtin/logical/pki/ocsp_test.go
Normal file
543
builtin/logical/pki/ocsp_test.go
Normal file
@@ -0,0 +1,543 @@
|
||||
package pki
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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",
|
||||
})
|
||||
requireSuccessNilResponse(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,
|
||||
})
|
||||
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)
|
||||
}
|
||||
|
||||
func TestOcsp_ValidRequests(t *testing.T) {
|
||||
t.Parallel()
|
||||
type testArgs struct {
|
||||
reqType string
|
||||
caKeyType string
|
||||
reqHash crypto.Hash
|
||||
}
|
||||
var tests []testArgs
|
||||
for _, reqType := range []string{"get", "post"} {
|
||||
for _, caKeyType := range []string{"rsa", "ec"} { // "ed25519" is not supported at the moment in x/crypto/ocsp
|
||||
for _, requestHash := range []crypto.Hash{crypto.SHA1, crypto.SHA256} {
|
||||
tests = append(tests, testArgs{
|
||||
reqType: reqType,
|
||||
caKeyType: caKeyType,
|
||||
reqHash: requestHash,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, tt := range tests {
|
||||
localTT := tt
|
||||
testName := fmt.Sprintf("%s-%s-%s", localTT.reqType, localTT.caKeyType, localTT.reqHash)
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
runOcspRequestTest(t, localTT.reqType, localTT.caKeyType, localTT.reqHash)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func runOcspRequestTest(t *testing.T, requestType string, caKeyType string, requestHash crypto.Hash) {
|
||||
b, s, testEnv := setupOcspEnv(t, caKeyType)
|
||||
|
||||
// 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.PublicKey, ocspResp.SignatureAlgorithm)
|
||||
requireOcspResponseSignedBy(t, ocspResp, testEnv.issuer1.PublicKey)
|
||||
|
||||
// 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.PublicKey, ocspResp.SignatureAlgorithm)
|
||||
requireOcspResponseSignedBy(t, ocspResp, testEnv.issuer1.PublicKey)
|
||||
|
||||
// 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)
|
||||
|
||||
requireOcspSignatureAlgoForKey(t, testEnv.issuer2.PublicKey, ocspResp.SignatureAlgorithm)
|
||||
requireOcspResponseSignedBy(t, ocspResp, testEnv.issuer2.PublicKey)
|
||||
}
|
||||
|
||||
func requireOcspSignatureAlgoForKey(t *testing.T, key crypto.PublicKey, algorithm x509.SignatureAlgorithm) {
|
||||
switch key.(type) {
|
||||
case *rsa.PublicKey:
|
||||
require.Equal(t, x509.SHA256WithRSA, algorithm)
|
||||
case *ecdsa.PublicKey:
|
||||
require.Equal(t, x509.ECDSAWithSHA256, algorithm)
|
||||
case ed25519.PublicKey:
|
||||
require.Equal(t, x509.PureEd25519, algorithm)
|
||||
default:
|
||||
t.Fatalf("unsupported public key type %T", key)
|
||||
}
|
||||
}
|
||||
|
||||
type ocspTestEnv struct {
|
||||
issuer1 *x509.Certificate
|
||||
issuer2 *x509.Certificate
|
||||
|
||||
issuerId1 issuerID
|
||||
issuerId2 issuerID
|
||||
|
||||
leafCertIssuer1 *x509.Certificate
|
||||
leafCertIssuer2 *x509.Certificate
|
||||
}
|
||||
|
||||
func setupOcspEnv(t *testing.T, keyType string) (*backend, logical.Storage, *ocspTestEnv) {
|
||||
b, s := createBackendWithStorage(t)
|
||||
var issuerCerts []*x509.Certificate
|
||||
var leafCerts []*x509.Certificate
|
||||
var issuerIds []issuerID
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{
|
||||
"key_type": keyType,
|
||||
"ttl": "40h",
|
||||
"common_name": "example-ocsp.com",
|
||||
})
|
||||
requireSuccessNonNilResponse(t, resp, err, "root/generate/internal")
|
||||
requireFieldsSetInResp(t, resp, "issuer_id")
|
||||
issuerId := resp.Data["issuer_id"].(issuerID)
|
||||
|
||||
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,
|
||||
})
|
||||
requireSuccessNilResponse(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)
|
||||
}
|
||||
|
||||
testEnv := &ocspTestEnv{
|
||||
issuerId1: issuerIds[0],
|
||||
issuer1: issuerCerts[0],
|
||||
leafCertIssuer1: leafCerts[0],
|
||||
|
||||
issuerId2: issuerIds[1],
|
||||
issuer2: issuerCerts[1],
|
||||
leafCertIssuer2: leafCerts[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) {
|
||||
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 {
|
||||
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, key crypto.PublicKey) {
|
||||
require.Contains(t, []x509.SignatureAlgorithm{x509.SHA256WithRSA, x509.ECDSAWithSHA256}, ocspResp.SignatureAlgorithm)
|
||||
|
||||
hasher := sha256.New()
|
||||
hashAlgo := crypto.SHA256
|
||||
hasher.Write(ocspResp.TBSResponseData)
|
||||
hashData := hasher.Sum(nil)
|
||||
|
||||
switch key.(type) {
|
||||
case *rsa.PublicKey:
|
||||
err := rsa.VerifyPKCS1v15(key.(*rsa.PublicKey), hashAlgo, hashData, ocspResp.Signature)
|
||||
require.NoError(t, err, "the ocsp response was not signed by the expected public rsa key.")
|
||||
case *ecdsa.PublicKey:
|
||||
verify := ecdsa.VerifyASN1(key.(*ecdsa.PublicKey), hashData, ocspResp.Signature)
|
||||
require.True(t, verify, "the certificate was not signed by the expected public ecdsa key.")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user