245 lines
7.8 KiB
Go
245 lines
7.8 KiB
Go
package pki
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/vault/sdk/helper/certutil"
|
|
"github.com/hashicorp/vault/sdk/helper/errutil"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
type revocationInfo struct {
|
|
CertificateBytes []byte `json:"certificate_bytes"`
|
|
RevocationTime int64 `json:"revocation_time"`
|
|
RevocationTimeUTC time.Time `json:"revocation_time_utc"`
|
|
}
|
|
|
|
// Revokes a cert, and tries to be smart about error recovery
|
|
func revokeCert(ctx context.Context, b *backend, req *logical.Request, serial string, fromLease bool) (*logical.Response, error) {
|
|
// As this backend is self-contained and this function does not hook into
|
|
// third parties to manage users or resources, if the mount is tainted,
|
|
// revocation doesn't matter anyways -- the CRL that would be written will
|
|
// be immediately blown away by the view being cleared. So we can simply
|
|
// fast path a successful exit.
|
|
if b.System().Tainted() {
|
|
return nil, nil
|
|
}
|
|
|
|
signingBundle, caErr := fetchCAInfo(ctx, b, req)
|
|
switch caErr.(type) {
|
|
case errutil.UserError:
|
|
return logical.ErrorResponse(fmt.Sprintf("could not fetch the CA certificate: %s", caErr)), nil
|
|
case errutil.InternalError:
|
|
return nil, fmt.Errorf("error fetching CA certificate: %s", caErr)
|
|
}
|
|
if signingBundle == nil {
|
|
return nil, errors.New("CA info not found")
|
|
}
|
|
colonSerial := strings.Replace(strings.ToLower(serial), "-", ":", -1)
|
|
if colonSerial == certutil.GetHexFormatted(signingBundle.Certificate.SerialNumber.Bytes(), ":") {
|
|
return logical.ErrorResponse("adding CA to CRL is not allowed"), nil
|
|
}
|
|
|
|
alreadyRevoked := false
|
|
var revInfo revocationInfo
|
|
|
|
revEntry, err := fetchCertBySerial(ctx, req, "revoked/", serial)
|
|
if err != nil {
|
|
switch err.(type) {
|
|
case errutil.UserError:
|
|
return logical.ErrorResponse(err.Error()), nil
|
|
case errutil.InternalError:
|
|
return nil, err
|
|
}
|
|
}
|
|
if revEntry != nil {
|
|
// Set the revocation info to the existing values
|
|
alreadyRevoked = true
|
|
err = revEntry.DecodeJSON(&revInfo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error decoding existing revocation info")
|
|
}
|
|
}
|
|
|
|
if !alreadyRevoked {
|
|
certEntry, err := fetchCertBySerial(ctx, req, "certs/", serial)
|
|
if err != nil {
|
|
switch err.(type) {
|
|
case errutil.UserError:
|
|
return logical.ErrorResponse(err.Error()), nil
|
|
case errutil.InternalError:
|
|
return nil, err
|
|
}
|
|
}
|
|
if certEntry == nil {
|
|
if fromLease {
|
|
// We can't write to revoked/ or update the CRL anyway because we don't have the cert,
|
|
// and there's no reason to expect this will work on a subsequent
|
|
// retry. Just give up and let the lease get deleted.
|
|
b.Logger().Warn("expired certificate revoke failed because not found in storage, treating as success", "serial", serial)
|
|
return nil, nil
|
|
}
|
|
return logical.ErrorResponse(fmt.Sprintf("certificate with serial %s not found", serial)), nil
|
|
}
|
|
|
|
cert, err := x509.ParseCertificate(certEntry.Value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing certificate: %w", err)
|
|
}
|
|
if cert == nil {
|
|
return nil, fmt.Errorf("got a nil certificate")
|
|
}
|
|
|
|
// Add a little wiggle room because leases are stored with a second
|
|
// granularity
|
|
if cert.NotAfter.Before(time.Now().Add(2 * time.Second)) {
|
|
return nil, nil
|
|
}
|
|
|
|
// Compatibility: Don't revoke CAs if they had leases. New CAs going
|
|
// forward aren't issued leases.
|
|
if cert.IsCA && fromLease {
|
|
return nil, nil
|
|
}
|
|
|
|
currTime := time.Now()
|
|
revInfo.CertificateBytes = certEntry.Value
|
|
revInfo.RevocationTime = currTime.Unix()
|
|
revInfo.RevocationTimeUTC = currTime.UTC()
|
|
|
|
revEntry, err = logical.StorageEntryJSON("revoked/"+normalizeSerial(serial), revInfo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating revocation entry")
|
|
}
|
|
|
|
err = req.Storage.Put(ctx, revEntry)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error saving revoked certificate to new location")
|
|
}
|
|
|
|
}
|
|
|
|
crlErr := buildCRL(ctx, b, req, false)
|
|
switch crlErr.(type) {
|
|
case errutil.UserError:
|
|
return logical.ErrorResponse(fmt.Sprintf("Error during CRL building: %s", crlErr)), nil
|
|
case errutil.InternalError:
|
|
return nil, fmt.Errorf("error encountered during CRL building: %w", crlErr)
|
|
}
|
|
|
|
resp := &logical.Response{
|
|
Data: map[string]interface{}{
|
|
"revocation_time": revInfo.RevocationTime,
|
|
},
|
|
}
|
|
if !revInfo.RevocationTimeUTC.IsZero() {
|
|
resp.Data["revocation_time_rfc3339"] = revInfo.RevocationTimeUTC.Format(time.RFC3339Nano)
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
// Builds a CRL by going through the list of revoked certificates and building
|
|
// a new CRL with the stored revocation times and serial numbers.
|
|
func buildCRL(ctx context.Context, b *backend, req *logical.Request, forceNew bool) error {
|
|
crlInfo, err := b.CRL(ctx, req.Storage)
|
|
if err != nil {
|
|
return errutil.InternalError{Err: fmt.Sprintf("error fetching CRL config information: %s", err)}
|
|
}
|
|
|
|
crlLifetime := b.crlLifetime
|
|
var revokedCerts []pkix.RevokedCertificate
|
|
var revInfo revocationInfo
|
|
var revokedSerials []string
|
|
|
|
if crlInfo != nil {
|
|
if crlInfo.Expiry != "" {
|
|
crlDur, err := time.ParseDuration(crlInfo.Expiry)
|
|
if err != nil {
|
|
return errutil.InternalError{Err: fmt.Sprintf("error parsing CRL duration of %s", crlInfo.Expiry)}
|
|
}
|
|
crlLifetime = crlDur
|
|
}
|
|
|
|
if crlInfo.Disable {
|
|
if !forceNew {
|
|
return nil
|
|
}
|
|
goto WRITE
|
|
}
|
|
}
|
|
|
|
revokedSerials, err = req.Storage.List(ctx, "revoked/")
|
|
if err != nil {
|
|
return errutil.InternalError{Err: fmt.Sprintf("error fetching list of revoked certs: %s", err)}
|
|
}
|
|
|
|
for _, serial := range revokedSerials {
|
|
revokedEntry, err := req.Storage.Get(ctx, "revoked/"+serial)
|
|
if err != nil {
|
|
return errutil.InternalError{Err: fmt.Sprintf("unable to fetch revoked cert with serial %s: %s", serial, err)}
|
|
}
|
|
if revokedEntry == nil {
|
|
return errutil.InternalError{Err: fmt.Sprintf("revoked certificate entry for serial %s is nil", serial)}
|
|
}
|
|
if revokedEntry.Value == nil || len(revokedEntry.Value) == 0 {
|
|
// TODO: In this case, remove it and continue? How likely is this to
|
|
// happen? Alternately, could skip it entirely, or could implement a
|
|
// delete function so that there is a way to remove these
|
|
return errutil.InternalError{Err: fmt.Sprintf("found revoked serial but actual certificate is empty")}
|
|
}
|
|
|
|
err = revokedEntry.DecodeJSON(&revInfo)
|
|
if err != nil {
|
|
return errutil.InternalError{Err: fmt.Sprintf("error decoding revocation entry for serial %s: %s", serial, err)}
|
|
}
|
|
|
|
revokedCert, err := x509.ParseCertificate(revInfo.CertificateBytes)
|
|
if err != nil {
|
|
return errutil.InternalError{Err: fmt.Sprintf("unable to parse stored revoked certificate with serial %s: %s", serial, err)}
|
|
}
|
|
|
|
// NOTE: We have to change this to UTC time because the CRL standard
|
|
// mandates it but Go will happily encode the CRL without this.
|
|
newRevCert := pkix.RevokedCertificate{
|
|
SerialNumber: revokedCert.SerialNumber,
|
|
}
|
|
if !revInfo.RevocationTimeUTC.IsZero() {
|
|
newRevCert.RevocationTime = revInfo.RevocationTimeUTC
|
|
} else {
|
|
newRevCert.RevocationTime = time.Unix(revInfo.RevocationTime, 0).UTC()
|
|
}
|
|
revokedCerts = append(revokedCerts, newRevCert)
|
|
}
|
|
|
|
WRITE:
|
|
signingBundle, caErr := fetchCAInfo(ctx, b, req)
|
|
switch caErr.(type) {
|
|
case errutil.UserError:
|
|
return errutil.UserError{Err: fmt.Sprintf("could not fetch the CA certificate: %s", caErr)}
|
|
case errutil.InternalError:
|
|
return errutil.InternalError{Err: fmt.Sprintf("error fetching CA certificate: %s", caErr)}
|
|
}
|
|
|
|
crlBytes, err := signingBundle.Certificate.CreateCRL(rand.Reader, signingBundle.PrivateKey, revokedCerts, time.Now(), time.Now().Add(crlLifetime))
|
|
if err != nil {
|
|
return errutil.InternalError{Err: fmt.Sprintf("error creating new CRL: %s", err)}
|
|
}
|
|
|
|
err = req.Storage.Put(ctx, &logical.StorageEntry{
|
|
Key: "crl",
|
|
Value: crlBytes,
|
|
})
|
|
if err != nil {
|
|
return errutil.InternalError{Err: fmt.Sprintf("error storing CRL: %s", err)}
|
|
}
|
|
|
|
return nil
|
|
}
|