mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-03 03:58:01 +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"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
atomic2 "go.uber.org/atomic"
|
||||||
|
|
||||||
"github.com/hashicorp/vault/sdk/helper/consts"
|
"github.com/hashicorp/vault/sdk/helper/consts"
|
||||||
|
|
||||||
"github.com/armon/go-metrics"
|
"github.com/armon/go-metrics"
|
||||||
@@ -84,6 +86,8 @@ func Backend(conf *logical.BackendConfig) *backend {
|
|||||||
"issuer/+/der",
|
"issuer/+/der",
|
||||||
"issuer/+/json",
|
"issuer/+/json",
|
||||||
"issuers",
|
"issuers",
|
||||||
|
"ocsp", // OCSP POST
|
||||||
|
"ocsp/*", // OCSP GET
|
||||||
},
|
},
|
||||||
|
|
||||||
LocalStorage: []string{
|
LocalStorage: []string{
|
||||||
@@ -158,6 +162,10 @@ func Backend(conf *logical.BackendConfig) *backend {
|
|||||||
pathFetchValidRaw(&b),
|
pathFetchValidRaw(&b),
|
||||||
pathFetchValid(&b),
|
pathFetchValid(&b),
|
||||||
pathFetchListCerts(&b),
|
pathFetchListCerts(&b),
|
||||||
|
|
||||||
|
// OCSP APIs
|
||||||
|
buildPathOcspGet(&b),
|
||||||
|
buildPathOcspPost(&b),
|
||||||
},
|
},
|
||||||
|
|
||||||
Secrets: []*framework.Secret{
|
Secrets: []*framework.Secret{
|
||||||
@@ -179,6 +187,7 @@ func Backend(conf *logical.BackendConfig) *backend {
|
|||||||
b.pkiStorageVersion.Store(0)
|
b.pkiStorageVersion.Store(0)
|
||||||
|
|
||||||
b.crlBuilder = &crlBuilder{}
|
b.crlBuilder = &crlBuilder{}
|
||||||
|
b.isOcspDisabled = atomic2.NewBool(false)
|
||||||
return &b
|
return &b
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +208,9 @@ type backend struct {
|
|||||||
|
|
||||||
// Write lock around issuers and keys.
|
// Write lock around issuers and keys.
|
||||||
issuersLock sync.RWMutex
|
issuersLock sync.RWMutex
|
||||||
|
|
||||||
|
// Optimization to not read the CRL config on every OCSP request.
|
||||||
|
isOcspDisabled *atomic2.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
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
|
// initialize is used to perform a possible PKI storage migration if needed
|
||||||
func (b *backend) initialize(ctx context.Context, _ *logical.InitializationRequest) error {
|
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
|
// 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.
|
// the storage flag midway through the request stream of other requests.
|
||||||
b.issuersLock.Lock()
|
b.issuersLock.Lock()
|
||||||
@@ -320,6 +335,14 @@ func (b *backend) initialize(ctx context.Context, _ *logical.InitializationReque
|
|||||||
return nil
|
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 {
|
func (b *backend) useLegacyBundleCaStorage() bool {
|
||||||
// This helper function is here to choose whether or not we use the newer
|
// 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.
|
// 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 {
|
} else {
|
||||||
b.Logger().Debug("Ignoring invalidation updates for issuer as the PKI migration has yet to complete.")
|
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"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -150,6 +151,10 @@ func (sc *storageContext) fetchCAInfoByIssuerId(issuerId issuerID, usage issuerU
|
|||||||
return caInfo, nil
|
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
|
// Allows fetching certificates from the backend; it handles the slightly
|
||||||
// separate pathing for CRL, and revoked certificates.
|
// 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
|
// otherNameRaw describes a name related to a certificate which is not in one
|
||||||
// of the standard name formats. RFC 5280, 4.2.1.6:
|
// of the standard name formats. RFC 5280, 4.2.1.6:
|
||||||
|
//
|
||||||
// OtherName ::= SEQUENCE {
|
// OtherName ::= SEQUENCE {
|
||||||
// type-id OBJECT IDENTIFIER,
|
// type-id OBJECT IDENTIFIER,
|
||||||
// value [0] EXPLICIT ANY DEFINED BY type-id }
|
// value [0] EXPLICIT ANY DEFINED BY type-id }
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package pki
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -26,6 +27,64 @@ func TestBackend_CRL_EnableDisableRoot(t *testing.T) {
|
|||||||
crlEnableDisableTestForBackend(t, b, s, []string{caSerial})
|
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) {
|
func TestBackend_CRL_AllKeyTypeSigAlgos(t *testing.T) {
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
KeyType string
|
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
|
// Builds a CRL by going through the list of revoked certificates and building
|
||||||
// a new CRL with the stored revocation times and serial numbers.
|
// 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 {
|
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 {
|
if err != nil {
|
||||||
return errutil.InternalError{Err: fmt.Sprintf("error fetching CRL config information: %s", err)}
|
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 {
|
type crlConfig struct {
|
||||||
Expiry string `json:"expiry"`
|
Expiry string `json:"expiry"`
|
||||||
Disable bool `json:"disable"`
|
Disable bool `json:"disable"`
|
||||||
|
OcspDisable bool `json:"ocsp_disable"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func pathConfigCRL(b *backend) *framework.Path {
|
func pathConfigCRL(b *backend) *framework.Path {
|
||||||
@@ -30,6 +31,10 @@ valid; defaults to 72 hours`,
|
|||||||
Type: framework.TypeBool,
|
Type: framework.TypeBool,
|
||||||
Description: `If set to true, disables generating the CRL entirely.`,
|
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{
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -80,12 +65,14 @@ func (b *backend) pathCRLRead(ctx context.Context, req *logical.Request, _ *fram
|
|||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"expiry": config.Expiry,
|
"expiry": config.Expiry,
|
||||||
"disable": config.Disable,
|
"disable": config.Disable,
|
||||||
|
"ocsp_disable": config.OcspDisable,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *backend) pathCRLWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -105,6 +92,12 @@ func (b *backend) pathCRLWrite(ctx context.Context, req *logical.Request, d *fra
|
|||||||
config.Disable = disableRaw.(bool)
|
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)
|
entry, err := logical.StorageEntryJSON("config/crl", config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ const (
|
|||||||
|
|
||||||
maxRolesToScanOnIssuerChange = 100
|
maxRolesToScanOnIssuerChange = 100
|
||||||
maxRolesToFindOnIssuerChange = 10
|
maxRolesToFindOnIssuerChange = 10
|
||||||
|
|
||||||
|
latestIssuerVersion = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
type keyID string
|
type keyID string
|
||||||
@@ -80,17 +82,19 @@ const (
|
|||||||
ReadOnlyUsage issuerUsage = iota
|
ReadOnlyUsage issuerUsage = iota
|
||||||
IssuanceUsage issuerUsage = 1 << iota
|
IssuanceUsage issuerUsage = 1 << iota
|
||||||
CRLSigningUsage 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
|
// 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,
|
// mask field on the IssuerEntry and handle migrations to a newer mask,
|
||||||
// inferring a value for the new bits.
|
// inferring a value for the new bits.
|
||||||
AllIssuerUsages issuerUsage = ReadOnlyUsage | IssuanceUsage | CRLSigningUsage
|
AllIssuerUsages = ReadOnlyUsage | IssuanceUsage | CRLSigningUsage | OCSPSigningUsage
|
||||||
)
|
)
|
||||||
|
|
||||||
var namedIssuerUsages = map[string]issuerUsage{
|
var namedIssuerUsages = map[string]issuerUsage{
|
||||||
"read-only": ReadOnlyUsage,
|
"read-only": ReadOnlyUsage,
|
||||||
"issuing-certificates": IssuanceUsage,
|
"issuing-certificates": IssuanceUsage,
|
||||||
"crl-signing": CRLSigningUsage,
|
"crl-signing": CRLSigningUsage,
|
||||||
|
"ocsp-signing": OCSPSigningUsage,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *issuerUsage) ToggleUsage(usages ...issuerUsage) {
|
func (i *issuerUsage) ToggleUsage(usages ...issuerUsage) {
|
||||||
@@ -151,6 +155,7 @@ type issuerEntry struct {
|
|||||||
RevocationTime int64 `json:"revocation_time"`
|
RevocationTime int64 `json:"revocation_time"`
|
||||||
RevocationTimeUTC time.Time `json:"revocation_time_utc"`
|
RevocationTimeUTC time.Time `json:"revocation_time_utc"`
|
||||||
AIAURIs *certutil.URLEntries `json:"aia_uris,omitempty"`
|
AIAURIs *certutil.URLEntries `json:"aia_uris,omitempty"`
|
||||||
|
Version uint `json:"version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type localCRLConfigEntry struct {
|
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)}
|
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to fetch pki key: %v", err)}
|
||||||
}
|
}
|
||||||
if entry == nil {
|
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())}
|
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)}
|
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to fetch pki issuer: %v", err)}
|
||||||
}
|
}
|
||||||
if entry == nil {
|
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())}
|
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 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 {
|
func (sc *storageContext) writeIssuer(issuer *issuerEntry) error {
|
||||||
@@ -679,7 +703,8 @@ func (sc *storageContext) importIssuer(certValue string, issuerName string) (*is
|
|||||||
result.Name = issuerName
|
result.Name = issuerName
|
||||||
result.Certificate = certValue
|
result.Certificate = certValue
|
||||||
result.LeafNotAfterBehavior = certutil.ErrNotAfterBehavior
|
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
|
// We shouldn't add CSRs or multiple certificates in this
|
||||||
countCertificates := strings.Count(result.Certificate, "-BEGIN ")
|
countCertificates := strings.Count(result.Certificate, "-BEGIN ")
|
||||||
@@ -1048,3 +1073,22 @@ func (sc *storageContext) checkForRolesReferencing(issuerId string) (timeout boo
|
|||||||
|
|
||||||
return false, inUseBy, nil
|
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,
|
SerialNumber: cb.SerialNumber,
|
||||||
LeafNotAfterBehavior: certutil.ErrNotAfterBehavior,
|
LeafNotAfterBehavior: certutil.ErrNotAfterBehavior,
|
||||||
}
|
}
|
||||||
issuer.Usage.ToggleUsage(IssuanceUsage, CRLSigningUsage)
|
issuer.Usage.ToggleUsage(AllIssuerUsages)
|
||||||
|
|
||||||
return issuer, cb, nil
|
return issuer, cb, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,6 +164,41 @@ func Test_KeysIssuerImport(t *testing.T) {
|
|||||||
require.Equal(t, "", key2Ref.Name)
|
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) {
|
func genIssuerAndKey(t *testing.T, b *backend, s logical.Storage) (issuerEntry, keyEntry) {
|
||||||
certBundle := genCertBundle(t, b, s)
|
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",
|
Certificate: strings.TrimSpace(certBundle.Certificate) + "\n",
|
||||||
CAChain: certBundle.CAChain,
|
CAChain: certBundle.CAChain,
|
||||||
SerialNumber: certBundle.SerialNumber,
|
SerialNumber: certBundle.SerialNumber,
|
||||||
|
Usage: AllIssuerUsages,
|
||||||
|
Version: latestIssuerVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
return pkiIssuer, pkiKey
|
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) {
|
func CBDelete(b *backend, s logical.Storage, path string) (*logical.Response, error) {
|
||||||
return CBReq(b, s, logical.DeleteOperation, path, make(map[string]interface{}))
|
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"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/big"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -27,7 +28,11 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func serialFromCert(cert *x509.Certificate) string {
|
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 {
|
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)
|
bufferedBody := newBufferedReader(r.Body)
|
||||||
r.Body = bufferedBody
|
r.Body = bufferedBody
|
||||||
|
|
||||||
// If we are uploading a snapshot we don't want to parse it. Instead
|
// If we are uploading a snapshot or receiving an ocsp-request (which
|
||||||
// we will simply add the HTTP request to the logical request object
|
// is der encoded) we don't want to parse it. Instead, we will simply
|
||||||
// for later consumption.
|
// add the HTTP request to the logical request object for later consumption.
|
||||||
if path == "sys/storage/raft/snapshot" || path == "sys/storage/raft/snapshot-force" {
|
contentType := r.Header.Get("Content-Type")
|
||||||
|
if path == "sys/storage/raft/snapshot" || path == "sys/storage/raft/snapshot-force" || isOcspRequest(contentType) {
|
||||||
passHTTPReq = true
|
passHTTPReq = true
|
||||||
origBody = r.Body
|
origBody = r.Body
|
||||||
} else {
|
} else {
|
||||||
@@ -121,7 +122,7 @@ func buildLogicalRequestNoAuth(perfStandby bool, w http.ResponseWriter, r *http.
|
|||||||
return nil, nil, status, fmt.Errorf("error reading data")
|
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)
|
formData, err := parseFormRequest(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status := http.StatusBadRequest
|
status := http.StatusBadRequest
|
||||||
@@ -209,6 +210,15 @@ func buildLogicalRequestNoAuth(perfStandby bool, w http.ResponseWriter, r *http.
|
|||||||
return req, origBody, 0, nil
|
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) {
|
func buildLogicalPath(r *http.Request) (string, int, error) {
|
||||||
ns, err := namespace.FromContext(r.Context())
|
ns, err := namespace.FromContext(r.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ update your API calls accordingly.
|
|||||||
- [Read Issuer Certificate](#read-issuer-certificate)
|
- [Read Issuer Certificate](#read-issuer-certificate)
|
||||||
- [Read Default Issuer Certificate Chain](#read-default-issuer-certificate-chain)
|
- [Read Default Issuer Certificate Chain](#read-default-issuer-certificate-chain)
|
||||||
- [Read Issuer CRL](#read-issuer-crl)
|
- [Read Issuer CRL](#read-issuer-crl)
|
||||||
|
- [OCSP Request](#ocsp-request)
|
||||||
- [List Certificates](#list-certificates)
|
- [List Certificates](#list-certificates)
|
||||||
- [Read Certificate](#read-certificate)
|
- [Read Certificate](#read-certificate)
|
||||||
- [Managing Keys and Issuers](#managing-keys-and-issuers)
|
- [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
|
### List Certificates
|
||||||
|
|
||||||
This endpoint returns a list of the current certificates by serial number only.
|
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",
|
"key_id": "baadd98d-ec5a-66ac-06b7-dfc91c02c9cf",
|
||||||
"leaf_not_after_behavior": "truncate",
|
"leaf_not_after_behavior": "truncate",
|
||||||
"manual_chain": null,
|
"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 `/ca_chain` path. Setting `manual_chain` thus allows controlling
|
||||||
the presented chain as desired.
|
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:
|
usages for this issuer. Valid options are:
|
||||||
|
|
||||||
- `read-only`, to allow this issuer to be read; implict; always allowed;
|
- `read-only`, to allow this issuer to be read; implict; always allowed;
|
||||||
- `issuing-certificates`, to allow this issuer to be used for issuing other
|
- `issuing-certificates`, to allow this issuer to be used for issuing other
|
||||||
certificates; or
|
certificates; or
|
||||||
- `crl-signing`, to allow this issuer to be used for signing CRLs.
|
- `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,
|
~> 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,
|
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
|
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
|
(and the CRL updated), but preventing new certificate issuance. After all
|
||||||
certificates issued under this certificate have expired, this certificate
|
certificates issued under this certificate have expired, this certificate
|
||||||
could be marked `usage=read-only`, freezing the CRL. Finally, after a grace
|
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",
|
"key_id": "baadd98d-ec5a-66ac-06b7-dfc91c02c9cf",
|
||||||
"leaf_not_after_behavior": "truncate",
|
"leaf_not_after_behavior": "truncate",
|
||||||
"manual_chain": null,
|
"manual_chain": null,
|
||||||
"usage": "read-only,issuing-certificates,crl-signing",
|
"usage": "read-only,issuing-certificates,crl-signing,ocsp-signing",
|
||||||
"revocation_signature_algorithm": "",
|
"revocation_signature_algorithm": "",
|
||||||
"issuing_certificates": ["<url1>", "<url2>"],
|
"issuing_certificates": ["<url1>", "<url2>"],
|
||||||
"crl_distribution_points": ["<url1>", "<url2>"],
|
"crl_distribution_points": ["<url1>", "<url2>"],
|
||||||
@@ -2952,7 +2982,8 @@ $ curl \
|
|||||||
"lease_duration": 0,
|
"lease_duration": 0,
|
||||||
"data": {
|
"data": {
|
||||||
"disable": false,
|
"disable": false,
|
||||||
"expiry": "72h"
|
"expiry": "72h",
|
||||||
|
"ocsp_disable": false
|
||||||
},
|
},
|
||||||
"auth": null
|
"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
|
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.
|
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
|
~> **Note**: This parameter is global, across all clusters and issuers. Use
|
||||||
the per-issuer `usage` field to disable CRL building for a specific
|
the per-issuer `usage` field to disable CRL building for a specific
|
||||||
issuer, while leaving the global CRL building enabled.
|
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.
|
- `expiry` `(string: "72h")` - The amount of time the generated CRL should be valid.
|
||||||
- `disable` `(bool: false)` - Disables or enables CRL building.
|
- `disable` `(bool: false)` - Disables or enables CRL building.
|
||||||
|
- `ocsp_disable` `(bool: false)` - Disables or enables the OCSP responder in Vault.
|
||||||
|
|
||||||
#### Sample Payload
|
#### Sample Payload
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"expiry": "48h"
|
"expiry": "48h",
|
||||||
|
"disable": "false",
|
||||||
|
"ocsp_disable": "false"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user