// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package pki import ( "bytes" "context" "crypto" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/base64" "errors" "fmt" "io" "math/big" "net/http" "strings" "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 { pattern := "ocsp/" + framework.MatchAllRegex(ocspReqParam) displayAttrs := &framework.DisplayAttributes{ OperationPrefix: operationPrefixPKI, OperationVerb: "query", OperationSuffix: "ocsp-with-get-req", } return buildOcspGetWithPath(b, pattern, displayAttrs) } func buildPathUnifiedOcspGet(b *backend) *framework.Path { pattern := "unified-ocsp/" + framework.MatchAllRegex(ocspReqParam) displayAttrs := &framework.DisplayAttributes{ OperationPrefix: operationPrefixPKI, OperationVerb: "query", OperationSuffix: "unified-ocsp-with-get-req", } return buildOcspGetWithPath(b, pattern, displayAttrs) } func buildOcspGetWithPath(b *backend, pattern string, displayAttrs *framework.DisplayAttributes) *framework.Path { return &framework.Path{ Pattern: pattern, DisplayAttrs: displayAttrs, 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 { pattern := "ocsp" displayAttrs := &framework.DisplayAttributes{ OperationPrefix: operationPrefixPKI, OperationVerb: "query", OperationSuffix: "ocsp", } return buildOcspPostWithPath(b, pattern, displayAttrs) } func buildPathUnifiedOcspPost(b *backend) *framework.Path { pattern := "unified-ocsp" displayAttrs := &framework.DisplayAttributes{ OperationPrefix: operationPrefixPKI, OperationVerb: "query", OperationSuffix: "unified-ocsp", } return buildOcspPostWithPath(b, pattern, displayAttrs) } func buildOcspPostWithPath(b *backend, pattern string, displayAttrs *framework.DisplayAttributes) *framework.Path { return &framework.Path{ Pattern: pattern, DisplayAttrs: displayAttrs, 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 || (isUnifiedOcspPath(request) && !cfg.UnifiedCRL) { 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 } useUnifiedStorage := canUseUnifiedStorage(request, cfg) ocspStatus, err := getOcspStatus(sc, ocspReq, useUnifiedStorage) if err != nil { return logAndReturnInternalError(b, err), nil } caBundle, issuer, 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, issuer.RevocationSigAlg) 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 canUseUnifiedStorage(req *logical.Request, cfg *crlConfig) bool { if isUnifiedOcspPath(req) { return true } // We are operating on the existing /pki/ocsp path, both of these fields need to be enabled // for us to use the unified path. return shouldLocalPathsUseUnified(cfg) } func isUnifiedOcspPath(req *logical.Request) bool { return strings.HasPrefix(req.Path, "unified-ocsp") } 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, issuer.RevocationSigAlg) 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. // NOTE: Writing an empty update request to Vault causes a nil request.HTTPRequest, and that object // says that it is possible for its Body element to be nil as well, so check both just in case. if request.HTTPRequest == nil { return nil, errors.New("no data in request") } rawBody := request.HTTPRequest.Body if rawBody == nil { return nil, errors.New("no data in request 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.Operation) } } 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, ocspReq *ocsp.Request, useUnifiedStorage bool) (*ocspRespInfo, error) { revEntryRaw, err := fetchCertBySerialBigInt(sc, 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 } else if useUnifiedStorage { dashSerial := normalizeSerialFromBigInt(ocspReq.SerialNumber) unifiedEntry, err := getUnifiedRevocationBySerial(sc, dashSerial) if err != nil { return nil, err } if unifiedEntry != nil { info.ocspStatus = ocsp.Revoked info.revocationTimeUTC = &unifiedEntry.RevocationTimeUTC info.issuerID = unifiedEntry.CertificateIssuer } } return &info, nil } func lookupOcspIssuer(sc *storageContext, req *ocsp.Request, optRevokedIssuer issuerID) (*certutil.ParsedCertBundle, *issuerEntry, error) { reqHash := req.HashAlgorithm if !reqHash.Available() { return nil, 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, 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, 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, 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, issuer, nil } } if matchedButNoUsage { // We matched an issuer but it did not have an OCSP signing usage set so bail. return nil, nil, ErrMissingOcspUsage } return nil, 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) { // issuer name hashing taken from golang.org/x/crypto/ocsp. 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, revSigAlg x509.SignatureAlgorithm) ([]byte, error) { curTime := time.Now() duration, err := time.ParseDuration(cfg.OcspExpiry) if err != nil { return nil, err } // x/crypto/ocsp lives outside of the standard library's crypto/x509 and includes // ripped-off variants of many internal structures and functions. These // lack support for PSS signatures altogether, so if we have revSigAlg // that uses PSS, downgrade it to PKCS#1v1.5. This fixes the lack of // support in x/ocsp, at the risk of OCSP requests failing due to lack // of PKCS#1v1.5 (in say, PKCS#11 HSMs or GCP). // // Other restrictions, such as hash function selection, will still work // however. switch revSigAlg { case x509.SHA256WithRSAPSS: revSigAlg = x509.SHA256WithRSA case x509.SHA384WithRSAPSS: revSigAlg = x509.SHA384WithRSA case x509.SHA512WithRSAPSS: revSigAlg = x509.SHA512WithRSA } // Due to a bug in Go's ocsp.ParseResponse(...), we do not provision // Certificate any more on the response to help Go based OCSP clients. // This was technically unnecessary, as the Certificate given here // both signed the OCSP response and issued the leaf cert, and so // should already be trusted by the client. // // See also: https://github.com/golang/go/issues/59641 template := ocsp.Response{ IssuerHash: reqHash, Status: info.ocspStatus, SerialNumber: info.serialNumber, ThisUpdate: curTime, NextUpdate: curTime.Add(duration), ExtraExtensions: []pkix.Extension{}, SignatureAlgorithm: revSigAlg, } 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 `