mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-11-04 04:28:08 +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,9 +920,10 @@ 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 {
 | 
					//
 | 
				
			||||||
//      type-id    OBJECT IDENTIFIER,
 | 
					//	OtherName ::= SEQUENCE {
 | 
				
			||||||
//      value      [0] EXPLICIT ANY DEFINED BY type-id }
 | 
					//	     type-id    OBJECT IDENTIFIER,
 | 
				
			||||||
 | 
					//	     value      [0] EXPLICIT ANY DEFINED BY type-id }
 | 
				
			||||||
type otherNameRaw struct {
 | 
					type otherNameRaw struct {
 | 
				
			||||||
	TypeID asn1.ObjectIdentifier
 | 
						TypeID asn1.ObjectIdentifier
 | 
				
			||||||
	Value  asn1.RawValue
 | 
						Value  asn1.RawValue
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -12,8 +12,9 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// CRLConfig holds basic CRL configuration information
 | 
					// CRLConfig holds basic CRL configuration information
 | 
				
			||||||
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,43 +54,25 @@ 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
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return &logical.Response{
 | 
						return &logical.Response{
 | 
				
			||||||
		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
 | 
				
			||||||
@@ -77,20 +79,22 @@ func (e keyEntry) isManagedPrivateKey() bool {
 | 
				
			|||||||
type issuerUsage uint
 | 
					type issuerUsage uint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const (
 | 
					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