open-vault/builtin/logical/pki/ocsp.go
Steven Clark 96f1443265
Fix various trivial warnings from staticcheck in the PKI plugin (#16946)
* Fix up simple warnings in production code

* Address warnings from static check in the PKI test classes
2022-08-31 16:25:14 -04:00

417 lines
13 KiB
Go

package pki
import (
"bytes"
"context"
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"errors"
"fmt"
"io"
"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"
maximumRequestSize = 2048 // A normal simple request is 87 bytes, so give us some buffer
)
type ocspRespInfo struct {
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) {
sc := b.makeStorageContext(ctx, request.Storage)
cfg, err := b.crlBuilder.getConfigWithUpdate(sc)
if err != nil || cfg.OcspDisable {
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
}
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(cfg, 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(cfg, 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(cfg *crlConfig, 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(cfg, 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")
}
if len(base64Req) >= maximumRequestSize {
return nil, errors.New("request is too large")
}
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()
requestBytes, err := io.ReadAll(io.LimitReader(rawBody, maximumRequestSize))
if err != nil {
return nil, err
}
if len(requestBytes) >= maximumRequestSize {
return nil, errors.New("request is too large")
}
return requestBytes, 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
}
matchedButNoUsage := false
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) {
matchedButNoUsage = true
// We found a matching issuer, but it's not allowed to sign the
// response, there might be another issuer that we rotated
// that will match though, so keep iterating.
continue
}
return parsedBundle, nil
}
}
if matchedButNoUsage {
// We matched an issuer but it did not have an OCSP signing usage set so bail.
return nil, ErrMissingOcspUsage
}
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(cfg *crlConfig, caBundle *certutil.ParsedCertBundle, info *ocspRespInfo, reqHash crypto.Hash) ([]byte, error) {
curTime := time.Now()
duration, err := time.ParseDuration(cfg.OcspExpiry)
if err != nil {
return nil, err
}
template := ocsp.Response{
IssuerHash: reqHash,
Status: info.ocspStatus,
SerialNumber: info.serialNumber,
ThisUpdate: curTime,
NextUpdate: curTime.Add(duration),
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
`