mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-02 19:47: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:
@@ -8,6 +8,8 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
atomic2 "go.uber.org/atomic"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/helper/consts"
|
||||
|
||||
"github.com/armon/go-metrics"
|
||||
@@ -84,6 +86,8 @@ func Backend(conf *logical.BackendConfig) *backend {
|
||||
"issuer/+/der",
|
||||
"issuer/+/json",
|
||||
"issuers",
|
||||
"ocsp", // OCSP POST
|
||||
"ocsp/*", // OCSP GET
|
||||
},
|
||||
|
||||
LocalStorage: []string{
|
||||
@@ -158,6 +162,10 @@ func Backend(conf *logical.BackendConfig) *backend {
|
||||
pathFetchValidRaw(&b),
|
||||
pathFetchValid(&b),
|
||||
pathFetchListCerts(&b),
|
||||
|
||||
// OCSP APIs
|
||||
buildPathOcspGet(&b),
|
||||
buildPathOcspPost(&b),
|
||||
},
|
||||
|
||||
Secrets: []*framework.Secret{
|
||||
@@ -179,6 +187,7 @@ func Backend(conf *logical.BackendConfig) *backend {
|
||||
b.pkiStorageVersion.Store(0)
|
||||
|
||||
b.crlBuilder = &crlBuilder{}
|
||||
b.isOcspDisabled = atomic2.NewBool(false)
|
||||
return &b
|
||||
}
|
||||
|
||||
@@ -199,6 +208,9 @@ type backend struct {
|
||||
|
||||
// Write lock around issuers and keys.
|
||||
issuersLock sync.RWMutex
|
||||
|
||||
// Optimization to not read the CRL config on every OCSP request.
|
||||
isOcspDisabled *atomic2.Bool
|
||||
}
|
||||
|
||||
type (
|
||||
@@ -295,6 +307,9 @@ func (b *backend) metricsWrap(callType string, roleMode int, ofunc roleOperation
|
||||
|
||||
// initialize is used to perform a possible PKI storage migration if needed
|
||||
func (b *backend) initialize(ctx context.Context, _ *logical.InitializationRequest) error {
|
||||
// load ocsp enabled status
|
||||
setOcspStatus(b, ctx)
|
||||
|
||||
// Grab the lock prior to the updating of the storage lock preventing us flipping
|
||||
// the storage flag midway through the request stream of other requests.
|
||||
b.issuersLock.Lock()
|
||||
@@ -320,6 +335,14 @@ func (b *backend) initialize(ctx context.Context, _ *logical.InitializationReque
|
||||
return nil
|
||||
}
|
||||
|
||||
func setOcspStatus(b *backend, ctx context.Context) {
|
||||
sc := b.makeStorageContext(ctx, b.storage)
|
||||
config, err := sc.getRevocationConfig()
|
||||
if config != nil && err == nil {
|
||||
b.isOcspDisabled.Store(config.OcspDisable)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) useLegacyBundleCaStorage() bool {
|
||||
// This helper function is here to choose whether or not we use the newer
|
||||
// issuer/key storage format or the older legacy ca bundle format.
|
||||
@@ -373,6 +396,9 @@ func (b *backend) invalidate(ctx context.Context, key string) {
|
||||
} else {
|
||||
b.Logger().Debug("Ignoring invalidation updates for issuer as the PKI migration has yet to complete.")
|
||||
}
|
||||
case key == "config/crl":
|
||||
// We may need to reload our OCSP status flag
|
||||
setOcspStatus(b, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/url"
|
||||
"regexp"
|
||||
@@ -150,6 +151,10 @@ func (sc *storageContext) fetchCAInfoByIssuerId(issuerId issuerID, usage issuerU
|
||||
return caInfo, nil
|
||||
}
|
||||
|
||||
func fetchCertBySerialBigInt(ctx context.Context, b *backend, req *logical.Request, prefix string, serial *big.Int) (*logical.StorageEntry, error) {
|
||||
return fetchCertBySerial(ctx, b, req, prefix, serialFromBigInt(serial))
|
||||
}
|
||||
|
||||
// Allows fetching certificates from the backend; it handles the slightly
|
||||
// separate pathing for CRL, and revoked certificates.
|
||||
//
|
||||
@@ -915,6 +920,7 @@ func signCert(b *backend,
|
||||
|
||||
// otherNameRaw describes a name related to a certificate which is not in one
|
||||
// of the standard name formats. RFC 5280, 4.2.1.6:
|
||||
//
|
||||
// OtherName ::= SEQUENCE {
|
||||
// type-id OBJECT IDENTIFIER,
|
||||
// value [0] EXPLICIT ANY DEFINED BY type-id }
|
||||
|
||||
@@ -3,6 +3,7 @@ package pki
|
||||
import (
|
||||
"context"
|
||||
"encoding/asn1"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -26,6 +27,64 @@ func TestBackend_CRL_EnableDisableRoot(t *testing.T) {
|
||||
crlEnableDisableTestForBackend(t, b, s, []string{caSerial})
|
||||
}
|
||||
|
||||
func TestBackend_CRLConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
expiry string
|
||||
disable bool
|
||||
ocspDisable bool
|
||||
}{
|
||||
{expiry: "24h", disable: true, ocspDisable: true},
|
||||
{expiry: "16h", disable: false, ocspDisable: true},
|
||||
{expiry: "8h", disable: true, ocspDisable: false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
name := fmt.Sprintf("%s-%t-%t", tc.expiry, tc.disable, tc.ocspDisable)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
b, s := createBackendWithStorage(t)
|
||||
|
||||
resp, err := CBWrite(b, s, "config/crl", map[string]interface{}{
|
||||
"expiry": tc.expiry,
|
||||
"disable": tc.disable,
|
||||
"ocsp_disable": tc.ocspDisable,
|
||||
})
|
||||
requireSuccessNilResponse(t, resp, err)
|
||||
|
||||
resp, err = CBRead(b, s, "config/crl")
|
||||
requireSuccessNonNilResponse(t, resp, err)
|
||||
requireFieldsSetInResp(t, resp, "disable", "expiry", "ocsp_disable")
|
||||
|
||||
require.Equal(t, tc.expiry, resp.Data["expiry"])
|
||||
require.Equal(t, tc.disable, resp.Data["disable"])
|
||||
require.Equal(t, tc.ocspDisable, resp.Data["ocsp_disable"])
|
||||
})
|
||||
}
|
||||
|
||||
badValueTests := []struct {
|
||||
expiry string
|
||||
disable string
|
||||
ocspDisable string
|
||||
}{
|
||||
{expiry: "not a duration", disable: "true", ocspDisable: "true"},
|
||||
{expiry: "16h", disable: "not a boolean", ocspDisable: "true"},
|
||||
{expiry: "8h", disable: "true", ocspDisable: "not a boolean"},
|
||||
}
|
||||
for _, tc := range badValueTests {
|
||||
name := fmt.Sprintf("bad-%s-%s-%s", tc.expiry, tc.disable, tc.ocspDisable)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
b, s := createBackendWithStorage(t)
|
||||
|
||||
_, err := CBWrite(b, s, "config/crl", map[string]interface{}{
|
||||
"expiry": tc.expiry,
|
||||
"disable": tc.disable,
|
||||
"ocsp_disable": tc.ocspDisable,
|
||||
})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackend_CRL_AllKeyTypeSigAlgos(t *testing.T) {
|
||||
type testCase struct {
|
||||
KeyType string
|
||||
|
||||
@@ -611,7 +611,7 @@ func augmentWithRevokedIssuers(issuerIDEntryMap map[issuerID]*issuerEntry, issue
|
||||
// Builds a CRL by going through the list of revoked certificates and building
|
||||
// a new CRL with the stored revocation times and serial numbers.
|
||||
func buildCRL(sc *storageContext, forceNew bool, thisIssuerId issuerID, revoked []pkix.RevokedCertificate, identifier crlID, crlNumber int64) error {
|
||||
crlInfo, err := sc.Backend.CRL(sc.Context, sc.Storage)
|
||||
crlInfo, err := sc.getRevocationConfig()
|
||||
if err != nil {
|
||||
return errutil.InternalError{Err: fmt.Sprintf("error fetching CRL config information: %s", err)}
|
||||
}
|
||||
|
||||
396
builtin/logical/pki/ocsp.go
Normal file
396
builtin/logical/pki/ocsp.go
Normal file
@@ -0,0 +1,396 @@
|
||||
package pki
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/helper/errutil"
|
||||
|
||||
"golang.org/x/crypto/ocsp"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/helper/certutil"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
const (
|
||||
ocspReqParam = "req"
|
||||
ocspResponseContentType = "application/ocsp-response"
|
||||
)
|
||||
|
||||
type ocspRespInfo struct {
|
||||
formattedSerialNumber string
|
||||
serialNumber *big.Int
|
||||
ocspStatus int
|
||||
revocationTimeUTC *time.Time
|
||||
issuerID issuerID
|
||||
}
|
||||
|
||||
// These response variables should not be mutated, instead treat them as constants
|
||||
var (
|
||||
OcspUnauthorizedResponse = &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
logical.HTTPContentType: ocspResponseContentType,
|
||||
logical.HTTPStatusCode: http.StatusUnauthorized,
|
||||
logical.HTTPRawBody: ocsp.UnauthorizedErrorResponse,
|
||||
},
|
||||
}
|
||||
OcspMalformedResponse = &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
logical.HTTPContentType: ocspResponseContentType,
|
||||
logical.HTTPStatusCode: http.StatusBadRequest,
|
||||
logical.HTTPRawBody: ocsp.MalformedRequestErrorResponse,
|
||||
},
|
||||
}
|
||||
OcspInternalErrorResponse = &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
logical.HTTPContentType: ocspResponseContentType,
|
||||
logical.HTTPStatusCode: http.StatusInternalServerError,
|
||||
logical.HTTPRawBody: ocsp.InternalErrorErrorResponse,
|
||||
},
|
||||
}
|
||||
|
||||
ErrMissingOcspUsage = errors.New("issuer entry did not have the OCSPSigning usage")
|
||||
ErrIssuerHasNoKey = errors.New("issuer has no key")
|
||||
ErrUnknownIssuer = errors.New("unknown issuer")
|
||||
)
|
||||
|
||||
func buildPathOcspGet(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "ocsp/" + framework.MatchAllRegex(ocspReqParam),
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
ocspReqParam: {
|
||||
Type: framework.TypeString,
|
||||
Description: "base-64 encoded ocsp request",
|
||||
},
|
||||
},
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ReadOperation: &framework.PathOperation{
|
||||
Callback: b.ocspHandler,
|
||||
},
|
||||
},
|
||||
|
||||
HelpSynopsis: pathOcspHelpSyn,
|
||||
HelpDescription: pathOcspHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
func buildPathOcspPost(b *backend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "ocsp",
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.UpdateOperation: &framework.PathOperation{
|
||||
Callback: b.ocspHandler,
|
||||
},
|
||||
},
|
||||
|
||||
HelpSynopsis: pathOcspHelpSyn,
|
||||
HelpDescription: pathOcspHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) ocspHandler(ctx context.Context, request *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
if b.isOcspDisabled.Load() {
|
||||
return OcspUnauthorizedResponse, nil
|
||||
}
|
||||
|
||||
derReq, err := fetchDerEncodedRequest(request, data)
|
||||
if err != nil {
|
||||
return OcspMalformedResponse, nil
|
||||
}
|
||||
|
||||
ocspReq, err := ocsp.ParseRequest(derReq)
|
||||
if err != nil {
|
||||
return OcspMalformedResponse, nil
|
||||
}
|
||||
|
||||
sc := b.makeStorageContext(ctx, request.Storage)
|
||||
|
||||
ocspStatus, err := getOcspStatus(sc, request, ocspReq)
|
||||
if err != nil {
|
||||
return logAndReturnInternalError(b, err), nil
|
||||
}
|
||||
|
||||
caBundle, err := lookupOcspIssuer(sc, ocspReq, ocspStatus.issuerID)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrUnknownIssuer) {
|
||||
// Since we were not able to find a matching issuer for the incoming request
|
||||
// generate an Unknown OCSP response. This might turn into an Unauthorized if
|
||||
// we find out that we don't have a default issuer or it's missing the proper Usage flags
|
||||
return generateUnknownResponse(sc, ocspReq), nil
|
||||
}
|
||||
if errors.Is(err, ErrMissingOcspUsage) {
|
||||
// If we did find a matching issuer but aren't allowed to sign, the spec says
|
||||
// we should be responding with an Unauthorized response as we don't have the
|
||||
// ability to sign the response.
|
||||
// https://www.rfc-editor.org/rfc/rfc5019#section-2.2.3
|
||||
return OcspUnauthorizedResponse, nil
|
||||
}
|
||||
return logAndReturnInternalError(b, err), nil
|
||||
}
|
||||
|
||||
byteResp, err := genResponse(caBundle, ocspStatus, ocspReq.HashAlgorithm)
|
||||
if err != nil {
|
||||
return logAndReturnInternalError(b, err), nil
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
logical.HTTPContentType: ocspResponseContentType,
|
||||
logical.HTTPStatusCode: http.StatusOK,
|
||||
logical.HTTPRawBody: byteResp,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func generateUnknownResponse(sc *storageContext, ocspReq *ocsp.Request) *logical.Response {
|
||||
// Generate an Unknown OCSP response, signing with the default issuer from the mount as we did
|
||||
// not match the request's issuer. If no default issuer can be used, return with Unauthorized as there
|
||||
// isn't much else we can do at this point.
|
||||
config, err := sc.getIssuersConfig()
|
||||
if err != nil {
|
||||
return logAndReturnInternalError(sc.Backend, err)
|
||||
}
|
||||
|
||||
if config.DefaultIssuerId == "" {
|
||||
// If we don't have any issuers or default issuers set, no way to sign a response so Unauthorized it is.
|
||||
return OcspUnauthorizedResponse
|
||||
}
|
||||
|
||||
caBundle, issuer, err := getOcspIssuerParsedBundle(sc, config.DefaultIssuerId)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrUnknownIssuer) || errors.Is(err, ErrIssuerHasNoKey) {
|
||||
// We must have raced on a delete/update of the default issuer, anyways
|
||||
// no way to sign a response so Unauthorized it is.
|
||||
return OcspUnauthorizedResponse
|
||||
}
|
||||
return logAndReturnInternalError(sc.Backend, err)
|
||||
}
|
||||
|
||||
if !issuer.Usage.HasUsage(OCSPSigningUsage) {
|
||||
// If we don't have any issuers or default issuers set, no way to sign a response so Unauthorized it is.
|
||||
return OcspUnauthorizedResponse
|
||||
}
|
||||
|
||||
info := &ocspRespInfo{
|
||||
serialNumber: ocspReq.SerialNumber,
|
||||
ocspStatus: ocsp.Unknown,
|
||||
}
|
||||
|
||||
byteResp, err := genResponse(caBundle, info, ocspReq.HashAlgorithm)
|
||||
if err != nil {
|
||||
return logAndReturnInternalError(sc.Backend, err)
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
logical.HTTPContentType: ocspResponseContentType,
|
||||
logical.HTTPStatusCode: http.StatusOK,
|
||||
logical.HTTPRawBody: byteResp,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func fetchDerEncodedRequest(request *logical.Request, data *framework.FieldData) ([]byte, error) {
|
||||
switch request.Operation {
|
||||
case logical.ReadOperation:
|
||||
// The param within the GET request should have a base64 encoded version of a DER request.
|
||||
base64Req := data.Get(ocspReqParam).(string)
|
||||
if base64Req == "" {
|
||||
return nil, errors.New("no base64 encoded ocsp request was found")
|
||||
}
|
||||
|
||||
return base64.StdEncoding.DecodeString(base64Req)
|
||||
case logical.UpdateOperation:
|
||||
// POST bodies should contain the binary form of the DER request.
|
||||
rawBody := request.HTTPRequest.Body
|
||||
defer rawBody.Close()
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
_, err := buf.ReadFrom(rawBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported request method: %s", request.HTTPRequest.Method)
|
||||
}
|
||||
}
|
||||
|
||||
func logAndReturnInternalError(b *backend, err error) *logical.Response {
|
||||
// Since OCSP might be a high traffic endpoint, we will log at debug level only
|
||||
// any internal errors we do get. There is no way for us to return to the end-user
|
||||
// errors, so we rely on the log statement to help in debugging possible
|
||||
// issues in the field.
|
||||
b.Logger().Debug("OCSP internal error", "error", err)
|
||||
return OcspInternalErrorResponse
|
||||
}
|
||||
|
||||
func getOcspStatus(sc *storageContext, request *logical.Request, ocspReq *ocsp.Request) (*ocspRespInfo, error) {
|
||||
revEntryRaw, err := fetchCertBySerialBigInt(sc.Context, sc.Backend, request, revokedPath, ocspReq.SerialNumber)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info := ocspRespInfo{
|
||||
serialNumber: ocspReq.SerialNumber,
|
||||
ocspStatus: ocsp.Good,
|
||||
}
|
||||
|
||||
if revEntryRaw != nil {
|
||||
var revEntry revocationInfo
|
||||
if err := revEntryRaw.DecodeJSON(&revEntry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
info.ocspStatus = ocsp.Revoked
|
||||
info.revocationTimeUTC = &revEntry.RevocationTimeUTC
|
||||
info.issuerID = revEntry.CertificateIssuer // This might be empty if the CRL hasn't been rebuilt
|
||||
}
|
||||
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func lookupOcspIssuer(sc *storageContext, req *ocsp.Request, optRevokedIssuer issuerID) (*certutil.ParsedCertBundle, error) {
|
||||
reqHash := req.HashAlgorithm
|
||||
if !reqHash.Available() {
|
||||
return nil, x509.ErrUnsupportedAlgorithm
|
||||
}
|
||||
|
||||
// This will prime up issuerIds, with either the optRevokedIssuer value if set
|
||||
// or if we are operating in legacy storage mode, the shim bundle id or finally
|
||||
// a list of all our issuers in this mount.
|
||||
issuerIds, err := lookupIssuerIds(sc, optRevokedIssuer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, issuerId := range issuerIds {
|
||||
parsedBundle, issuer, err := getOcspIssuerParsedBundle(sc, issuerId)
|
||||
if err != nil {
|
||||
// A bit touchy here as if we get an ErrUnknownIssuer for an issuer id that we picked up
|
||||
// from a revocation entry, we still return an ErrUnknownOcspIssuer as we can't validate
|
||||
// the end-user actually meant this specific issuer's cert with serial X.
|
||||
if errors.Is(err, ErrUnknownIssuer) || errors.Is(err, ErrIssuerHasNoKey) {
|
||||
// This skips either bad issuer ids, or root certs with no keys that we can't use.
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure the client and Vault are talking about the same issuer, otherwise
|
||||
// we might have a case of a matching serial number for a different issuer which
|
||||
// we should not respond back in the affirmative about.
|
||||
matches, err := doesRequestMatchIssuer(parsedBundle, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if matches {
|
||||
if !issuer.Usage.HasUsage(OCSPSigningUsage) {
|
||||
// We found the correct issuer, but it's not allowed to sign the
|
||||
// response so give up.
|
||||
return nil, ErrMissingOcspUsage
|
||||
}
|
||||
|
||||
return parsedBundle, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrUnknownIssuer
|
||||
}
|
||||
|
||||
func getOcspIssuerParsedBundle(sc *storageContext, issuerId issuerID) (*certutil.ParsedCertBundle, *issuerEntry, error) {
|
||||
issuer, bundle, err := sc.fetchCertBundleByIssuerId(issuerId, true)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case errutil.UserError:
|
||||
// Most likely the issuer id no longer exists skip it
|
||||
return nil, nil, ErrUnknownIssuer
|
||||
default:
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if issuer.KeyID == "" {
|
||||
// No point if the key does not exist from the issuer to use as a signer.
|
||||
return nil, nil, ErrIssuerHasNoKey
|
||||
}
|
||||
|
||||
caBundle, err := parseCABundle(sc.Context, sc.Backend, bundle)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return caBundle, issuer, nil
|
||||
}
|
||||
|
||||
func lookupIssuerIds(sc *storageContext, optRevokedIssuer issuerID) ([]issuerID, error) {
|
||||
if optRevokedIssuer != "" {
|
||||
return []issuerID{optRevokedIssuer}, nil
|
||||
}
|
||||
|
||||
if sc.Backend.useLegacyBundleCaStorage() {
|
||||
return []issuerID{legacyBundleShimID}, nil
|
||||
}
|
||||
|
||||
return sc.listIssuers()
|
||||
}
|
||||
|
||||
func doesRequestMatchIssuer(parsedBundle *certutil.ParsedCertBundle, req *ocsp.Request) (bool, error) {
|
||||
var pkInfo struct {
|
||||
Algorithm pkix.AlgorithmIdentifier
|
||||
PublicKey asn1.BitString
|
||||
}
|
||||
if _, err := asn1.Unmarshal(parsedBundle.Certificate.RawSubjectPublicKeyInfo, &pkInfo); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
h := req.HashAlgorithm.New()
|
||||
h.Write(pkInfo.PublicKey.RightAlign())
|
||||
issuerKeyHash := h.Sum(nil)
|
||||
|
||||
h.Reset()
|
||||
h.Write(parsedBundle.Certificate.RawSubject)
|
||||
issuerNameHash := h.Sum(nil)
|
||||
|
||||
return bytes.Equal(req.IssuerKeyHash, issuerKeyHash) && bytes.Equal(req.IssuerNameHash, issuerNameHash), nil
|
||||
}
|
||||
|
||||
func genResponse(caBundle *certutil.ParsedCertBundle, info *ocspRespInfo, reqHash crypto.Hash) ([]byte, error) {
|
||||
curTime := time.Now()
|
||||
template := ocsp.Response{
|
||||
IssuerHash: reqHash,
|
||||
Status: info.ocspStatus,
|
||||
SerialNumber: info.serialNumber,
|
||||
ThisUpdate: curTime,
|
||||
NextUpdate: curTime,
|
||||
Certificate: caBundle.Certificate,
|
||||
ExtraExtensions: []pkix.Extension{},
|
||||
}
|
||||
|
||||
if info.ocspStatus == ocsp.Revoked {
|
||||
template.RevokedAt = *info.revocationTimeUTC
|
||||
template.RevocationReason = ocsp.Unspecified
|
||||
}
|
||||
|
||||
return ocsp.CreateResponse(caBundle.Certificate, caBundle.Certificate, template, caBundle.PrivateKey)
|
||||
}
|
||||
|
||||
const pathOcspHelpSyn = `
|
||||
Query a certificate's revocation status through OCSP'
|
||||
`
|
||||
|
||||
const pathOcspHelpDesc = `
|
||||
This endpoint expects DER encoded OCSP requests and returns DER encoded OCSP responses
|
||||
`
|
||||
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.")
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
type crlConfig struct {
|
||||
Expiry string `json:"expiry"`
|
||||
Disable bool `json:"disable"`
|
||||
OcspDisable bool `json:"ocsp_disable"`
|
||||
}
|
||||
|
||||
func pathConfigCRL(b *backend) *framework.Path {
|
||||
@@ -30,6 +31,10 @@ valid; defaults to 72 hours`,
|
||||
Type: framework.TypeBool,
|
||||
Description: `If set to true, disables generating the CRL entirely.`,
|
||||
},
|
||||
"ocsp_disable": {
|
||||
Type: framework.TypeBool,
|
||||
Description: `If set to true, ocsp unauthorized responses will be returned.`,
|
||||
},
|
||||
},
|
||||
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
@@ -49,29 +54,9 @@ valid; defaults to 72 hours`,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) CRL(ctx context.Context, s logical.Storage) (*crlConfig, error) {
|
||||
entry, err := s.Get(ctx, "config/crl")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result crlConfig
|
||||
result.Expiry = b.crlLifetime.String()
|
||||
result.Disable = false
|
||||
|
||||
if entry == nil {
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
if err := entry.DecodeJSON(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (b *backend) pathCRLRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||
config, err := b.CRL(ctx, req.Storage)
|
||||
sc := b.makeStorageContext(ctx, req.Storage)
|
||||
config, err := sc.getRevocationConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -80,12 +65,14 @@ func (b *backend) pathCRLRead(ctx context.Context, req *logical.Request, _ *fram
|
||||
Data: map[string]interface{}{
|
||||
"expiry": config.Expiry,
|
||||
"disable": config.Disable,
|
||||
"ocsp_disable": config.OcspDisable,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *backend) pathCRLWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
config, err := b.CRL(ctx, req.Storage)
|
||||
sc := b.makeStorageContext(ctx, req.Storage)
|
||||
config, err := sc.getRevocationConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -105,6 +92,12 @@ func (b *backend) pathCRLWrite(ctx context.Context, req *logical.Request, d *fra
|
||||
config.Disable = disableRaw.(bool)
|
||||
}
|
||||
|
||||
var oldOcspDisable bool
|
||||
if ocspDisableRaw, ok := d.GetOk("ocsp_disable"); ok {
|
||||
oldOcspDisable = config.OcspDisable
|
||||
config.OcspDisable = ocspDisableRaw.(bool)
|
||||
}
|
||||
|
||||
entry, err := logical.StorageEntryJSON("config/crl", config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -127,6 +120,10 @@ func (b *backend) pathCRLWrite(ctx context.Context, req *logical.Request, d *fra
|
||||
}
|
||||
}
|
||||
|
||||
if oldOcspDisable != config.OcspDisable {
|
||||
setOcspStatus(b, ctx)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ const (
|
||||
|
||||
maxRolesToScanOnIssuerChange = 100
|
||||
maxRolesToFindOnIssuerChange = 10
|
||||
|
||||
latestIssuerVersion = 1
|
||||
)
|
||||
|
||||
type keyID string
|
||||
@@ -80,17 +82,19 @@ const (
|
||||
ReadOnlyUsage issuerUsage = iota
|
||||
IssuanceUsage issuerUsage = 1 << iota
|
||||
CRLSigningUsage issuerUsage = 1 << iota
|
||||
OCSPSigningUsage issuerUsage = 1 << iota
|
||||
|
||||
// When adding a new usage in the future, we'll need to create a usage
|
||||
// mask field on the IssuerEntry and handle migrations to a newer mask,
|
||||
// inferring a value for the new bits.
|
||||
AllIssuerUsages issuerUsage = ReadOnlyUsage | IssuanceUsage | CRLSigningUsage
|
||||
AllIssuerUsages = ReadOnlyUsage | IssuanceUsage | CRLSigningUsage | OCSPSigningUsage
|
||||
)
|
||||
|
||||
var namedIssuerUsages = map[string]issuerUsage{
|
||||
"read-only": ReadOnlyUsage,
|
||||
"issuing-certificates": IssuanceUsage,
|
||||
"crl-signing": CRLSigningUsage,
|
||||
"ocsp-signing": OCSPSigningUsage,
|
||||
}
|
||||
|
||||
func (i *issuerUsage) ToggleUsage(usages ...issuerUsage) {
|
||||
@@ -151,6 +155,7 @@ type issuerEntry struct {
|
||||
RevocationTime int64 `json:"revocation_time"`
|
||||
RevocationTimeUTC time.Time `json:"revocation_time_utc"`
|
||||
AIAURIs *certutil.URLEntries `json:"aia_uris,omitempty"`
|
||||
Version uint `json:"version"`
|
||||
}
|
||||
|
||||
type localCRLConfigEntry struct {
|
||||
@@ -204,7 +209,6 @@ func (sc *storageContext) fetchKeyById(keyId keyID) (*keyEntry, error) {
|
||||
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to fetch pki key: %v", err)}
|
||||
}
|
||||
if entry == nil {
|
||||
// FIXME: Dedicated/specific error for this?
|
||||
return nil, errutil.UserError{Err: fmt.Sprintf("pki key id %s does not exist", keyId.String())}
|
||||
}
|
||||
|
||||
@@ -561,7 +565,6 @@ func (sc *storageContext) fetchIssuerById(issuerId issuerID) (*issuerEntry, erro
|
||||
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to fetch pki issuer: %v", err)}
|
||||
}
|
||||
if entry == nil {
|
||||
// FIXME: Dedicated/specific error for this?
|
||||
return nil, errutil.UserError{Err: fmt.Sprintf("pki issuer id %s does not exist", issuerId.String())}
|
||||
}
|
||||
|
||||
@@ -570,7 +573,28 @@ func (sc *storageContext) fetchIssuerById(issuerId issuerID) (*issuerEntry, erro
|
||||
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to decode pki issuer with id %s: %v", issuerId.String(), err)}
|
||||
}
|
||||
|
||||
return &issuer, nil
|
||||
return sc.upgradeIssuerIfRequired(&issuer), nil
|
||||
}
|
||||
|
||||
func (sc *storageContext) upgradeIssuerIfRequired(issuer *issuerEntry) *issuerEntry {
|
||||
// *NOTE*: Don't attempt to write out the issuer here as it may cause ErrReadOnly that will direct the
|
||||
// request all the way up to the primary cluster which would be horrible for local cluster operations such
|
||||
// as generating a leaf cert or a revoke.
|
||||
// Also even though we could tell if we are the primary cluster's active node, we can't tell if we have the
|
||||
// a full rw issuer lock, so it might not be safe to write.
|
||||
if issuer.Version == latestIssuerVersion {
|
||||
return issuer
|
||||
}
|
||||
|
||||
if issuer.Version == 0 {
|
||||
// handle our new OCSPSigning usage flag for earlier versions
|
||||
if issuer.Usage.HasUsage(CRLSigningUsage) && !issuer.Usage.HasUsage(OCSPSigningUsage) {
|
||||
issuer.Usage.ToggleUsage(OCSPSigningUsage)
|
||||
}
|
||||
}
|
||||
|
||||
issuer.Version = latestIssuerVersion
|
||||
return issuer
|
||||
}
|
||||
|
||||
func (sc *storageContext) writeIssuer(issuer *issuerEntry) error {
|
||||
@@ -679,7 +703,8 @@ func (sc *storageContext) importIssuer(certValue string, issuerName string) (*is
|
||||
result.Name = issuerName
|
||||
result.Certificate = certValue
|
||||
result.LeafNotAfterBehavior = certutil.ErrNotAfterBehavior
|
||||
result.Usage.ToggleUsage(IssuanceUsage, CRLSigningUsage)
|
||||
result.Usage.ToggleUsage(AllIssuerUsages)
|
||||
result.Version = latestIssuerVersion
|
||||
|
||||
// We shouldn't add CSRs or multiple certificates in this
|
||||
countCertificates := strings.Count(result.Certificate, "-BEGIN ")
|
||||
@@ -1048,3 +1073,22 @@ func (sc *storageContext) checkForRolesReferencing(issuerId string) (timeout boo
|
||||
|
||||
return false, inUseBy, nil
|
||||
}
|
||||
|
||||
func (sc *storageContext) getRevocationConfig() (*crlConfig, error) {
|
||||
entry, err := sc.Storage.Get(sc.Context, "config/crl")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result crlConfig
|
||||
if entry == nil {
|
||||
result.Expiry = sc.Backend.crlLifetime.String()
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
if err = entry.DecodeJSON(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ func getLegacyCertBundle(ctx context.Context, s logical.Storage) (*issuerEntry,
|
||||
SerialNumber: cb.SerialNumber,
|
||||
LeafNotAfterBehavior: certutil.ErrNotAfterBehavior,
|
||||
}
|
||||
issuer.Usage.ToggleUsage(IssuanceUsage, CRLSigningUsage)
|
||||
issuer.Usage.ToggleUsage(AllIssuerUsages)
|
||||
|
||||
return issuer, cb, nil
|
||||
}
|
||||
|
||||
@@ -164,6 +164,41 @@ func Test_KeysIssuerImport(t *testing.T) {
|
||||
require.Equal(t, "", key2Ref.Name)
|
||||
}
|
||||
|
||||
func Test_IssuerUpgrade(t *testing.T) {
|
||||
t.Parallel()
|
||||
b, s := createBackendWithStorage(t)
|
||||
sc := b.makeStorageContext(ctx, s)
|
||||
|
||||
// Make sure that we add OCSP signing to v0 issuers if CRLSigning is enabled
|
||||
issuer, _ := genIssuerAndKey(t, b, s)
|
||||
issuer.Version = 0
|
||||
issuer.Usage.ToggleUsage(OCSPSigningUsage)
|
||||
|
||||
err := sc.writeIssuer(&issuer)
|
||||
require.NoError(t, err, "failed writing out issuer")
|
||||
|
||||
newIssuer, err := sc.fetchIssuerById(issuer.ID)
|
||||
require.NoError(t, err, "failed fetching issuer")
|
||||
|
||||
require.Equal(t, uint(1), newIssuer.Version)
|
||||
require.True(t, newIssuer.Usage.HasUsage(OCSPSigningUsage))
|
||||
|
||||
// If CRLSigning is not present on a v0, we should not have OCSP signing after upgrade.
|
||||
issuer, _ = genIssuerAndKey(t, b, s)
|
||||
issuer.Version = 0
|
||||
issuer.Usage.ToggleUsage(OCSPSigningUsage)
|
||||
issuer.Usage.ToggleUsage(CRLSigningUsage)
|
||||
|
||||
err = sc.writeIssuer(&issuer)
|
||||
require.NoError(t, err, "failed writing out issuer")
|
||||
|
||||
newIssuer, err = sc.fetchIssuerById(issuer.ID)
|
||||
require.NoError(t, err, "failed fetching issuer")
|
||||
|
||||
require.Equal(t, uint(1), newIssuer.Version)
|
||||
require.False(t, newIssuer.Usage.HasUsage(OCSPSigningUsage))
|
||||
}
|
||||
|
||||
func genIssuerAndKey(t *testing.T, b *backend, s logical.Storage) (issuerEntry, keyEntry) {
|
||||
certBundle := genCertBundle(t, b, s)
|
||||
|
||||
@@ -183,6 +218,8 @@ func genIssuerAndKey(t *testing.T, b *backend, s logical.Storage) (issuerEntry,
|
||||
Certificate: strings.TrimSpace(certBundle.Certificate) + "\n",
|
||||
CAChain: certBundle.CAChain,
|
||||
SerialNumber: certBundle.SerialNumber,
|
||||
Usage: AllIssuerUsages,
|
||||
Version: latestIssuerVersion,
|
||||
}
|
||||
|
||||
return pkiIssuer, pkiKey
|
||||
|
||||
@@ -295,3 +295,27 @@ func CBList(b *backend, s logical.Storage, path string) (*logical.Response, erro
|
||||
func CBDelete(b *backend, s logical.Storage, path string) (*logical.Response, error) {
|
||||
return CBReq(b, s, logical.DeleteOperation, path, make(map[string]interface{}))
|
||||
}
|
||||
|
||||
func requireFieldsSetInResp(t *testing.T, resp *logical.Response, fields ...string) {
|
||||
var missingFields []string
|
||||
for _, field := range fields {
|
||||
value, ok := resp.Data[field]
|
||||
if !ok || value == nil {
|
||||
missingFields = append(missingFields, field)
|
||||
}
|
||||
}
|
||||
|
||||
require.Empty(t, missingFields, "The following fields were required but missing from response:\n%v", resp.Data)
|
||||
}
|
||||
|
||||
func requireSuccessNonNilResponse(t *testing.T, resp *logical.Response, err error, msgAndArgs ...interface{}) {
|
||||
require.NoError(t, err, msgAndArgs...)
|
||||
require.False(t, resp.IsError(), msgAndArgs...)
|
||||
require.NotNil(t, resp, msgAndArgs...)
|
||||
}
|
||||
|
||||
func requireSuccessNilResponse(t *testing.T, resp *logical.Response, err error, msgAndArgs ...interface{}) {
|
||||
require.NoError(t, err, msgAndArgs...)
|
||||
require.False(t, resp.IsError(), msgAndArgs...)
|
||||
require.Nil(t, resp, msgAndArgs...)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
@@ -27,7 +28,11 @@ var (
|
||||
)
|
||||
|
||||
func serialFromCert(cert *x509.Certificate) string {
|
||||
return strings.TrimSpace(certutil.GetHexFormatted(cert.SerialNumber.Bytes(), ":"))
|
||||
return serialFromBigInt(cert.SerialNumber)
|
||||
}
|
||||
|
||||
func serialFromBigInt(serial *big.Int) string {
|
||||
return strings.TrimSpace(certutil.GetHexFormatted(serial.Bytes(), ":"))
|
||||
}
|
||||
|
||||
func normalizeSerial(serial string) string {
|
||||
|
||||
4
changelog/16723.txt
Normal file
4
changelog/16723.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
```release-note:feature
|
||||
secrets/pki: Add an OCSP responder that implements a subset of RFC6960, answering single serial number OCSP requests for
|
||||
a specific cluster's revoked certificates in a mount.
|
||||
```
|
||||
@@ -103,10 +103,11 @@ func buildLogicalRequestNoAuth(perfStandby bool, w http.ResponseWriter, r *http.
|
||||
bufferedBody := newBufferedReader(r.Body)
|
||||
r.Body = bufferedBody
|
||||
|
||||
// If we are uploading a snapshot we don't want to parse it. Instead
|
||||
// we will simply add the HTTP request to the logical request object
|
||||
// for later consumption.
|
||||
if path == "sys/storage/raft/snapshot" || path == "sys/storage/raft/snapshot-force" {
|
||||
// If we are uploading a snapshot or receiving an ocsp-request (which
|
||||
// is der encoded) we don't want to parse it. Instead, we will simply
|
||||
// add the HTTP request to the logical request object for later consumption.
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if path == "sys/storage/raft/snapshot" || path == "sys/storage/raft/snapshot-force" || isOcspRequest(contentType) {
|
||||
passHTTPReq = true
|
||||
origBody = r.Body
|
||||
} else {
|
||||
@@ -121,7 +122,7 @@ func buildLogicalRequestNoAuth(perfStandby bool, w http.ResponseWriter, r *http.
|
||||
return nil, nil, status, fmt.Errorf("error reading data")
|
||||
}
|
||||
|
||||
if isForm(head, r.Header.Get("Content-Type")) {
|
||||
if isForm(head, contentType) {
|
||||
formData, err := parseFormRequest(r)
|
||||
if err != nil {
|
||||
status := http.StatusBadRequest
|
||||
@@ -209,6 +210,15 @@ func buildLogicalRequestNoAuth(perfStandby bool, w http.ResponseWriter, r *http.
|
||||
return req, origBody, 0, nil
|
||||
}
|
||||
|
||||
func isOcspRequest(contentType string) bool {
|
||||
contentType, _, err := mime.ParseMediaType(contentType)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return contentType == "application/ocsp-request"
|
||||
}
|
||||
|
||||
func buildLogicalPath(r *http.Request) (string, int, error) {
|
||||
ns, err := namespace.FromContext(r.Context())
|
||||
if err != nil {
|
||||
|
||||
@@ -34,6 +34,7 @@ update your API calls accordingly.
|
||||
- [Read Issuer Certificate](#read-issuer-certificate)
|
||||
- [Read Default Issuer Certificate Chain](#read-default-issuer-certificate-chain)
|
||||
- [Read Issuer CRL](#read-issuer-crl)
|
||||
- [OCSP Request](#ocsp-request)
|
||||
- [List Certificates](#list-certificates)
|
||||
- [Read Certificate](#read-certificate)
|
||||
- [Managing Keys and Issuers](#managing-keys-and-issuers)
|
||||
@@ -1143,6 +1144,34 @@ $ curl \
|
||||
}
|
||||
```
|
||||
|
||||
### OCSP Request
|
||||
|
||||
This endpoint retrieves an OCSP response (revocation status) for a given serial number. The request/response formats are
|
||||
based on [RFC 6960](https://datatracker.ietf.org/doc/html/rfc6960)
|
||||
|
||||
At this time there are certain limitations of the OCSP implementation at this path.
|
||||
1. Only a single serial number within the request will appear in the response
|
||||
1. None of the extensions defined in the RFC are supported for requests or responses
|
||||
1. Ed25519 backed CA's are not supported for OCSP requests
|
||||
1. Note that this api will not work with the Vault client as both request and responses are DER encoded
|
||||
|
||||
These are unauthenticated endpoints.
|
||||
|
||||
| Method | Path | Response Format |
|
||||
| :----- |:-----------------------------------------------|:----------------------------------------------------------------------------------|
|
||||
| `GET` | `/pki/ocsp/<base 64 encoded ocsp DER request>` | DER [\[1\]](#vault-cli-with-der-pem-responses "Vault CLI With DER/PEM Responses") |
|
||||
| `POST` | `/pki/ocsp` | DER [\[1\]](#vault-cli-with-der-pem-responses "Vault CLI With DER/PEM Responses") |
|
||||
|
||||
#### Parameters
|
||||
|
||||
- None
|
||||
|
||||
#### Sample Request
|
||||
|
||||
```shell-session
|
||||
openssl ocsp -no_nonce -issuer issuer.pem -CAfile ca_chain.pem -cert cert-to-revoke.pem -text -url $VAULT_ADDR/v1/pki/ocsp
|
||||
```
|
||||
|
||||
### List Certificates
|
||||
|
||||
This endpoint returns a list of the current certificates by serial number only.
|
||||
@@ -1891,7 +1920,7 @@ $ curl \
|
||||
"key_id": "baadd98d-ec5a-66ac-06b7-dfc91c02c9cf",
|
||||
"leaf_not_after_behavior": "truncate",
|
||||
"manual_chain": null,
|
||||
"usage": "read-only,issuing-certificates,crl-signing"
|
||||
"usage": "read-only,issuing-certificates,crl-signing,ocsp-signing"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1966,18 +1995,19 @@ do so, import a new issuer and a new `issuer_id` will be assigned.
|
||||
the `/ca_chain` path. Setting `manual_chain` thus allows controlling
|
||||
the presented chain as desired.
|
||||
|
||||
- `usage` `([]string: read-only,issuing-certificates,crl-signing)` - Allowed
|
||||
- `usage` `([]string: read-only,issuing-certificates,crl-signing,ocsp-signing)` - Allowed
|
||||
usages for this issuer. Valid options are:
|
||||
|
||||
- `read-only`, to allow this issuer to be read; implict; always allowed;
|
||||
- `issuing-certificates`, to allow this issuer to be used for issuing other
|
||||
certificates; or
|
||||
- `crl-signing`, to allow this issuer to be used for signing CRLs.
|
||||
- `ocsp-signing`, to allow this issuer to be used for signing OCSP responses
|
||||
|
||||
~> Note: The `usage` field allows for a soft-delete capability on the issuer,
|
||||
or to prevent use of the issuer prior to it being enabled. For example,
|
||||
as issuance is rotated to a new issuer, the old issuer could be marked
|
||||
`usage=read-only,crl-signing`, allowing existing certificates to be revoked
|
||||
`usage=read-only,crl-signing,ocsp-signing`, allowing existing certificates to be revoked
|
||||
(and the CRL updated), but preventing new certificate issuance. After all
|
||||
certificates issued under this certificate have expired, this certificate
|
||||
could be marked `usage=read-only`, freezing the CRL. Finally, after a grace
|
||||
@@ -2051,7 +2081,7 @@ $ curl \
|
||||
"key_id": "baadd98d-ec5a-66ac-06b7-dfc91c02c9cf",
|
||||
"leaf_not_after_behavior": "truncate",
|
||||
"manual_chain": null,
|
||||
"usage": "read-only,issuing-certificates,crl-signing",
|
||||
"usage": "read-only,issuing-certificates,crl-signing,ocsp-signing",
|
||||
"revocation_signature_algorithm": "",
|
||||
"issuing_certificates": ["<url1>", "<url2>"],
|
||||
"crl_distribution_points": ["<url1>", "<url2>"],
|
||||
@@ -2952,7 +2982,8 @@ $ curl \
|
||||
"lease_duration": 0,
|
||||
"data": {
|
||||
"disable": false,
|
||||
"expiry": "72h"
|
||||
"expiry": "72h",
|
||||
"ocsp_disable": false
|
||||
},
|
||||
"auth": null
|
||||
}
|
||||
@@ -2964,6 +2995,9 @@ This endpoint allows setting the duration for which the generated CRL should be
|
||||
marked valid. If the CRL is disabled, it will return a signed but zero-length
|
||||
CRL for any request. If enabled, it will re-build the CRL.
|
||||
|
||||
If the `ocsp_disable` key is set to `true`, the OCSP responder will always
|
||||
respond with an Unauthorized OCSP response to any request.
|
||||
|
||||
~> **Note**: This parameter is global, across all clusters and issuers. Use
|
||||
the per-issuer `usage` field to disable CRL building for a specific
|
||||
issuer, while leaving the global CRL building enabled.
|
||||
@@ -2983,12 +3017,15 @@ the CRL.
|
||||
|
||||
- `expiry` `(string: "72h")` - The amount of time the generated CRL should be valid.
|
||||
- `disable` `(bool: false)` - Disables or enables CRL building.
|
||||
- `ocsp_disable` `(bool: false)` - Disables or enables the OCSP responder in Vault.
|
||||
|
||||
#### Sample Payload
|
||||
|
||||
```json
|
||||
{
|
||||
"expiry": "48h"
|
||||
"expiry": "48h",
|
||||
"disable": "false",
|
||||
"ocsp_disable": "false"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user