open-vault/builtin/logical/pki/path_fetch.go
Alexander Scheel e03fb14be4
Support for generating Delta CRLs (#16773)
* Allow generation of up-to-date delta CRLs

While switching to periodic rebuilds of CRLs alleviates the constant
rebuild pressure on Vault during times of high revocation, the CRL
proper becomes stale. One response to this is to switch to OCSP, but not
every system has support for this. Additionally, OCSP usually requires
connectivity and isn't used to augment a pre-distributed CRL (and is
instead used independently).

By generating delta CRLs containing only new revocations, an existing
CRL can be supplemented with newer revocations without requiring Vault
to rebuild all complete CRLs. Admins can periodically fetch the delta
CRL and add it to the existing CRL and applications should be able to
support using serials from both.

Because delta CRLs are emptied when the next complete CRL is rebuilt, it
is important that applications fetch the delta CRL and correlate it to
their complete CRL; if their complete CRL is older than the delta CRL's
extension number, applications MUST fetch the newer complete CRL to
ensure they have a correct combination.

This modifies the revocation process and adds several new configuration
options, controlling whether Delta CRLs are enabled and when we'll
rebuild it.

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add tests for delta CRLs

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add changelog entry

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add documentation on delta CRLs

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Address review feedback: fix several bugs

Thanks Steve!

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Correctly invoke periodic func on active nodes

We need to ensure we read the updated config (in case of OCSP request
handling on standby nodes), but otherwise want to avoid CRL/DeltaCRL
re-building.

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
2022-08-29 11:37:09 -04:00

358 lines
9.5 KiB
Go

package pki
import (
"context"
"encoding/pem"
"fmt"
"strings"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/errutil"
"github.com/hashicorp/vault/sdk/logical"
)
// Returns the CA in raw format
func pathFetchCA(b *backend) *framework.Path {
return &framework.Path{
Pattern: `ca(/pem)?`,
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.pathFetchRead,
},
},
HelpSynopsis: pathFetchHelpSyn,
HelpDescription: pathFetchHelpDesc,
}
}
// Returns the CA chain
func pathFetchCAChain(b *backend) *framework.Path {
return &framework.Path{
Pattern: `(cert/)?ca_chain`,
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.pathFetchRead,
},
},
HelpSynopsis: pathFetchHelpSyn,
HelpDescription: pathFetchHelpDesc,
}
}
// Returns the CRL in raw format
func pathFetchCRL(b *backend) *framework.Path {
return &framework.Path{
Pattern: `crl(/pem|/delta(/pem)?)?`,
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.pathFetchRead,
},
},
HelpSynopsis: pathFetchHelpSyn,
HelpDescription: pathFetchHelpDesc,
}
}
// Returns any valid (non-revoked) cert in raw format.
func pathFetchValidRaw(b *backend) *framework.Path {
return &framework.Path{
Pattern: `cert/(?P<serial>[0-9A-Fa-f-:]+)/raw(/pem)?`,
Fields: map[string]*framework.FieldSchema{
"serial": {
Type: framework.TypeString,
Description: `Certificate serial number, in colon- or
hyphen-separated octal`,
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.pathFetchRead,
},
},
HelpSynopsis: pathFetchHelpSyn,
HelpDescription: pathFetchHelpDesc,
}
}
// Returns any valid (non-revoked) cert. Since "ca" fits the pattern, this path
// also handles returning the CA cert in a non-raw format.
func pathFetchValid(b *backend) *framework.Path {
return &framework.Path{
Pattern: `cert/(?P<serial>[0-9A-Fa-f-:]+)`,
Fields: map[string]*framework.FieldSchema{
"serial": {
Type: framework.TypeString,
Description: `Certificate serial number, in colon- or
hyphen-separated octal`,
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.pathFetchRead,
},
},
HelpSynopsis: pathFetchHelpSyn,
HelpDescription: pathFetchHelpDesc,
}
}
// This returns the CRL in a non-raw format
func pathFetchCRLViaCertPath(b *backend) *framework.Path {
return &framework.Path{
Pattern: `cert/(crl|delta-crl)`,
Operations: map[logical.Operation]framework.OperationHandler{
logical.ReadOperation: &framework.PathOperation{
Callback: b.pathFetchRead,
},
},
HelpSynopsis: pathFetchHelpSyn,
HelpDescription: pathFetchHelpDesc,
}
}
// This returns the list of serial numbers for certs
func pathFetchListCerts(b *backend) *framework.Path {
return &framework.Path{
Pattern: "certs/?$",
Operations: map[logical.Operation]framework.OperationHandler{
logical.ListOperation: &framework.PathOperation{
Callback: b.pathFetchCertList,
},
},
HelpSynopsis: pathFetchHelpSyn,
HelpDescription: pathFetchHelpDesc,
}
}
func (b *backend) pathFetchCertList(ctx context.Context, req *logical.Request, _ *framework.FieldData) (response *logical.Response, retErr error) {
entries, err := req.Storage.List(ctx, "certs/")
if err != nil {
return nil, err
}
for i := range entries {
entries[i] = denormalizeSerial(entries[i])
}
return logical.ListResponse(entries), nil
}
func (b *backend) pathFetchRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (response *logical.Response, retErr error) {
var serial, pemType, contentType string
var certEntry, revokedEntry *logical.StorageEntry
var funcErr error
var certificate []byte
var fullChain []byte
var revocationTime int64
response = &logical.Response{
Data: map[string]interface{}{},
}
// Some of these need to return raw and some non-raw;
// this is basically handled by setting contentType or not.
// Errors don't cause an immediate exit, because the raw
// paths still need to return raw output.
switch {
case req.Path == "ca" || req.Path == "ca/pem":
serial = "ca"
contentType = "application/pkix-cert"
if req.Path == "ca/pem" {
pemType = "CERTIFICATE"
contentType = "application/pem-certificate-chain"
}
case req.Path == "ca_chain" || req.Path == "cert/ca_chain":
serial = "ca_chain"
if req.Path == "ca_chain" {
contentType = "application/pkix-cert"
}
case req.Path == "crl" || req.Path == "crl/pem" || req.Path == "crl/delta" || req.Path == "crl/delta/pem":
serial = legacyCRLPath
if req.Path == "crl/delta" || req.Path == "crl/delta/pem" {
serial = deltaCRLPath
}
contentType = "application/pkix-crl"
if req.Path == "crl/pem" || req.Path == "crl/delta/pem" {
pemType = "X509 CRL"
contentType = "application/x-pem-file"
}
case req.Path == "cert/crl" || req.Path == "cert/delta-crl":
serial = legacyCRLPath
if req.Path == "cert/delta-crl" {
serial = deltaCRLPath
}
pemType = "X509 CRL"
case strings.HasSuffix(req.Path, "/pem") || strings.HasSuffix(req.Path, "/raw"):
serial = data.Get("serial").(string)
contentType = "application/pkix-cert"
if strings.HasSuffix(req.Path, "/pem") {
pemType = "CERTIFICATE"
contentType = "application/pem-certificate-chain"
}
default:
serial = data.Get("serial").(string)
pemType = "CERTIFICATE"
}
if len(serial) == 0 {
response = logical.ErrorResponse("The serial number must be provided")
goto reply
}
// Prefer fetchCAInfo to fetchCertBySerial for CA certificates.
if serial == "ca_chain" || serial == "ca" {
sc := b.makeStorageContext(ctx, req.Storage)
caInfo, err := sc.fetchCAInfo(defaultRef, ReadOnlyUsage)
if err != nil {
switch err.(type) {
case errutil.UserError:
response = logical.ErrorResponse(err.Error())
goto reply
default:
retErr = err
goto reply
}
}
if serial == "ca_chain" {
rawChain := caInfo.GetFullChain()
var chainStr string
for _, ca := range rawChain {
block := pem.Block{
Type: "CERTIFICATE",
Bytes: ca.Bytes,
}
chainStr = strings.Join([]string{chainStr, strings.TrimSpace(string(pem.EncodeToMemory(&block)))}, "\n")
}
fullChain = []byte(strings.TrimSpace(chainStr))
certificate = fullChain
} else if serial == "ca" {
certificate = caInfo.Certificate.Raw
if len(pemType) != 0 {
block := pem.Block{
Type: pemType,
Bytes: certificate,
}
// This is convoluted on purpose to ensure that we don't have trailing
// newlines via various paths
certificate = []byte(strings.TrimSpace(string(pem.EncodeToMemory(&block))))
}
}
goto reply
}
certEntry, funcErr = fetchCertBySerial(ctx, b, req, req.Path, serial)
if funcErr != nil {
switch funcErr.(type) {
case errutil.UserError:
response = logical.ErrorResponse(funcErr.Error())
goto reply
default:
retErr = funcErr
goto reply
}
}
if certEntry == nil {
response = nil
goto reply
}
certificate = certEntry.Value
if len(pemType) != 0 {
block := pem.Block{
Type: pemType,
Bytes: certEntry.Value,
}
// This is convoluted on purpose to ensure that we don't have trailing
// newlines via various paths
certificate = []byte(strings.TrimSpace(string(pem.EncodeToMemory(&block))))
}
revokedEntry, funcErr = fetchCertBySerial(ctx, b, req, "revoked/", serial)
if funcErr != nil {
switch funcErr.(type) {
case errutil.UserError:
response = logical.ErrorResponse(funcErr.Error())
goto reply
default:
retErr = funcErr
goto reply
}
}
if revokedEntry != nil {
var revInfo revocationInfo
err := revokedEntry.DecodeJSON(&revInfo)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("Error decoding revocation entry for serial %s: %s", serial, err)), nil
}
revocationTime = revInfo.RevocationTime
}
reply:
switch {
case len(contentType) != 0:
response = &logical.Response{
Data: map[string]interface{}{
logical.HTTPContentType: contentType,
logical.HTTPRawBody: certificate,
},
}
if retErr != nil {
if b.Logger().IsWarn() {
b.Logger().Warn("possible error, but cannot return in raw response. Note that an empty CA probably means none was configured, and an empty CRL is possibly correct", "error", retErr)
}
}
retErr = nil
if len(certificate) > 0 {
response.Data[logical.HTTPStatusCode] = 200
} else {
response.Data[logical.HTTPStatusCode] = 204
}
case retErr != nil:
response = nil
return
case response == nil:
return
case response.IsError():
return response, nil
default:
response.Data["certificate"] = string(certificate)
response.Data["revocation_time"] = revocationTime
if len(fullChain) > 0 {
response.Data["ca_chain"] = string(fullChain)
}
}
return
}
const pathFetchHelpSyn = `
Fetch a CA, CRL, CA Chain, or non-revoked certificate.
`
const pathFetchHelpDesc = `
This allows certificates to be fetched. Use /cert/:serial for JSON responses.
Using "ca" or "crl" as the value fetches the appropriate information in DER encoding. Add "/pem" to either to get PEM encoding.
Using "ca_chain" as the value fetches the certificate authority trust chain in PEM encoding.
Otherwise, specify a serial number to fetch the specified certificate. Add "/raw" to get just the certificate in DER form, "/raw/pem" to get the PEM encoded certificate.
`