349 lines
8.9 KiB
Go
349 lines
8.9 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package cert
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"fmt"
|
|
"math/big"
|
|
url2 "net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fatih/structs"
|
|
"github.com/hashicorp/vault/sdk/framework"
|
|
"github.com/hashicorp/vault/sdk/helper/certutil"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
func pathListCRLs(b *backend) *framework.Path {
|
|
return &framework.Path{
|
|
Pattern: "crls/?$",
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationPrefix: operationPrefixCert,
|
|
OperationSuffix: "crls",
|
|
},
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
logical.ListOperation: &framework.PathOperation{
|
|
Callback: b.pathCRLsList,
|
|
},
|
|
},
|
|
HelpSynopsis: pathCRLsHelpSyn,
|
|
HelpDescription: pathCRLsHelpDesc,
|
|
}
|
|
}
|
|
|
|
func (b *backend) pathCRLsList(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
|
entries, err := req.Storage.List(ctx, "crls/")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list CRLs: %w", err)
|
|
}
|
|
|
|
return logical.ListResponse(entries), nil
|
|
}
|
|
|
|
func pathCRLs(b *backend) *framework.Path {
|
|
return &framework.Path{
|
|
Pattern: "crls/" + framework.GenericNameRegex("name"),
|
|
|
|
DisplayAttrs: &framework.DisplayAttributes{
|
|
OperationPrefix: operationPrefixCert,
|
|
OperationSuffix: "crl",
|
|
},
|
|
|
|
Fields: map[string]*framework.FieldSchema{
|
|
"name": {
|
|
Type: framework.TypeString,
|
|
Description: "The name of the certificate",
|
|
},
|
|
|
|
"crl": {
|
|
Type: framework.TypeString,
|
|
Description: `The public CRL that should be trusted to attest to certificates' validity statuses.
|
|
May be DER or PEM encoded. Note: the expiration time
|
|
is ignored; if the CRL is no longer valid, delete it
|
|
using the same name as specified here.`,
|
|
},
|
|
"url": {
|
|
Type: framework.TypeString,
|
|
Description: `The URL of a CRL distribution point. Only one of 'crl' or 'url' parameters should be specified.`,
|
|
},
|
|
},
|
|
|
|
Callbacks: map[logical.Operation]framework.OperationFunc{
|
|
logical.DeleteOperation: b.pathCRLDelete,
|
|
logical.ReadOperation: b.pathCRLRead,
|
|
logical.UpdateOperation: b.pathCRLWrite,
|
|
},
|
|
|
|
HelpSynopsis: pathCRLsHelpSyn,
|
|
HelpDescription: pathCRLsHelpDesc,
|
|
}
|
|
}
|
|
|
|
func (b *backend) populateCrlsIfNil(ctx context.Context, storage logical.Storage) error {
|
|
b.crlUpdateMutex.RLock()
|
|
if b.crls == nil {
|
|
b.crlUpdateMutex.RUnlock()
|
|
return b.lockThenpopulateCRLs(ctx, storage)
|
|
}
|
|
b.crlUpdateMutex.RUnlock()
|
|
return nil
|
|
}
|
|
|
|
func (b *backend) lockThenpopulateCRLs(ctx context.Context, storage logical.Storage) error {
|
|
b.crlUpdateMutex.Lock()
|
|
defer b.crlUpdateMutex.Unlock()
|
|
return b.populateCRLs(ctx, storage)
|
|
}
|
|
|
|
func (b *backend) populateCRLs(ctx context.Context, storage logical.Storage) error {
|
|
if b.crls != nil {
|
|
return nil
|
|
}
|
|
|
|
b.crls = map[string]CRLInfo{}
|
|
|
|
keys, err := storage.List(ctx, "crls/")
|
|
if err != nil {
|
|
return fmt.Errorf("error listing CRLs: %w", err)
|
|
}
|
|
if keys == nil || len(keys) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for _, key := range keys {
|
|
entry, err := storage.Get(ctx, "crls/"+key)
|
|
if err != nil {
|
|
b.crls = nil
|
|
return fmt.Errorf("error loading CRL %q: %w", key, err)
|
|
}
|
|
if entry == nil {
|
|
continue
|
|
}
|
|
var crlInfo CRLInfo
|
|
err = entry.DecodeJSON(&crlInfo)
|
|
if err != nil {
|
|
b.crls = nil
|
|
return fmt.Errorf("error decoding CRL %q: %w", key, err)
|
|
}
|
|
b.crls[key] = crlInfo
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *backend) findSerialInCRLs(serial *big.Int) map[string]RevokedSerialInfo {
|
|
b.crlUpdateMutex.RLock()
|
|
defer b.crlUpdateMutex.RUnlock()
|
|
ret := map[string]RevokedSerialInfo{}
|
|
for key, crl := range b.crls {
|
|
if crl.Serials == nil {
|
|
continue
|
|
}
|
|
if info, ok := crl.Serials[serial.String()]; ok {
|
|
ret[key] = info
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func parseSerialString(input string) (*big.Int, error) {
|
|
ret := &big.Int{}
|
|
|
|
switch {
|
|
case strings.Count(input, ":") > 0:
|
|
serialBytes := certutil.ParseHexFormatted(input, ":")
|
|
if serialBytes == nil {
|
|
return nil, fmt.Errorf("error parsing serial %q", input)
|
|
}
|
|
ret.SetBytes(serialBytes)
|
|
case strings.Count(input, "-") > 0:
|
|
serialBytes := certutil.ParseHexFormatted(input, "-")
|
|
if serialBytes == nil {
|
|
return nil, fmt.Errorf("error parsing serial %q", input)
|
|
}
|
|
ret.SetBytes(serialBytes)
|
|
default:
|
|
var success bool
|
|
ret, success = ret.SetString(input, 0)
|
|
if !success {
|
|
return nil, fmt.Errorf("error parsing serial %q", input)
|
|
}
|
|
}
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (b *backend) pathCRLDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
|
name := strings.ToLower(d.Get("name").(string))
|
|
if name == "" {
|
|
return logical.ErrorResponse(`"name" parameter cannot be empty`), nil
|
|
}
|
|
|
|
if err := b.lockThenpopulateCRLs(ctx, req.Storage); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
b.crlUpdateMutex.Lock()
|
|
defer b.crlUpdateMutex.Unlock()
|
|
|
|
_, ok := b.crls[name]
|
|
if !ok {
|
|
return logical.ErrorResponse(fmt.Sprintf(
|
|
"no such CRL %s", name,
|
|
)), nil
|
|
}
|
|
|
|
if err := req.Storage.Delete(ctx, "crls/"+name); err != nil {
|
|
return logical.ErrorResponse(fmt.Sprintf(
|
|
"error deleting crl %s: %v", name, err),
|
|
), nil
|
|
}
|
|
|
|
delete(b.crls, name)
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (b *backend) pathCRLRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
|
name := strings.ToLower(d.Get("name").(string))
|
|
if name == "" {
|
|
return logical.ErrorResponse(`"name" parameter must be set`), nil
|
|
}
|
|
|
|
if err := b.lockThenpopulateCRLs(ctx, req.Storage); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
b.crlUpdateMutex.RLock()
|
|
defer b.crlUpdateMutex.RUnlock()
|
|
|
|
var retData map[string]interface{}
|
|
|
|
crl, ok := b.crls[name]
|
|
if !ok {
|
|
return logical.ErrorResponse(fmt.Sprintf(
|
|
"no such CRL %s", name,
|
|
)), nil
|
|
}
|
|
|
|
retData = structs.New(&crl).Map()
|
|
|
|
return &logical.Response{
|
|
Data: retData,
|
|
}, nil
|
|
}
|
|
|
|
func (b *backend) pathCRLWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
|
name := strings.ToLower(d.Get("name").(string))
|
|
if name == "" {
|
|
return logical.ErrorResponse(`"name" parameter cannot be empty`), nil
|
|
}
|
|
if crlRaw, ok := d.GetOk("crl"); ok {
|
|
crl := crlRaw.(string)
|
|
certList, err := x509.ParseCRL([]byte(crl))
|
|
if err != nil {
|
|
return logical.ErrorResponse(fmt.Sprintf("failed to parse CRL: %v", err)), nil
|
|
}
|
|
if certList == nil {
|
|
return logical.ErrorResponse("parsed CRL is nil"), nil
|
|
}
|
|
|
|
b.crlUpdateMutex.Lock()
|
|
defer b.crlUpdateMutex.Unlock()
|
|
err = b.setCRL(ctx, req.Storage, certList, name, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else if urlRaw, ok := d.GetOk("url"); ok {
|
|
url := urlRaw.(string)
|
|
if url == "" {
|
|
return logical.ErrorResponse("empty CRL url"), nil
|
|
}
|
|
_, err := url2.Parse(url)
|
|
if err != nil {
|
|
return logical.ErrorResponse("invalid CRL url: %v", err), nil
|
|
}
|
|
|
|
b.crlUpdateMutex.Lock()
|
|
defer b.crlUpdateMutex.Unlock()
|
|
|
|
cdpInfo := &CDPInfo{
|
|
Url: url,
|
|
}
|
|
err = b.fetchCRL(ctx, req.Storage, name, &CRLInfo{
|
|
CDP: cdpInfo,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
return logical.ErrorResponse("one of 'crl' or 'url' must be provided"), nil
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (b *backend) setCRL(ctx context.Context, storage logical.Storage, certList *pkix.CertificateList, name string, cdp *CDPInfo) error {
|
|
if err := b.populateCRLs(ctx, storage); err != nil {
|
|
return err
|
|
}
|
|
|
|
crlInfo := CRLInfo{
|
|
CDP: cdp,
|
|
Serials: map[string]RevokedSerialInfo{},
|
|
}
|
|
|
|
if certList != nil {
|
|
for _, revokedCert := range certList.TBSCertList.RevokedCertificates {
|
|
crlInfo.Serials[revokedCert.SerialNumber.String()] = RevokedSerialInfo{}
|
|
}
|
|
}
|
|
|
|
entry, err := logical.StorageEntryJSON("crls/"+name, crlInfo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = storage.Put(ctx, entry); err != nil {
|
|
return err
|
|
}
|
|
|
|
b.crls[name] = crlInfo
|
|
return err
|
|
}
|
|
|
|
type CDPInfo struct {
|
|
Url string `json:"url" structs:"url" mapstructure:"url"`
|
|
ValidUntil time.Time `json:"valid_until" structs:"valid_until" mapstructure:"valid_until"`
|
|
}
|
|
|
|
type CRLInfo struct {
|
|
CDP *CDPInfo `json:"cdp" structs:"cdp" mapstructure:"cdp"`
|
|
Serials map[string]RevokedSerialInfo `json:"serials" structs:"serials" mapstructure:"serials"`
|
|
}
|
|
|
|
type RevokedSerialInfo struct{}
|
|
|
|
const pathCRLsHelpSyn = `
|
|
Manage Certificate Revocation Lists checked during authentication.
|
|
`
|
|
|
|
const pathCRLsHelpDesc = `
|
|
This endpoint allows you to list, create, read, update, and delete the Certificate
|
|
Revocation Lists checked during authentication, and/or CRL Distribution Point
|
|
URLs.
|
|
|
|
When any CRLs are in effect, any login will check the trust chains sent by a
|
|
client against the submitted or retrieved CRLs. Any chain containing a serial number revoked
|
|
by one or more of the CRLs causes that chain to be marked as invalid for the
|
|
authentication attempt. Conversely, *any* valid chain -- that is, a chain
|
|
in which none of the serials are revoked by any CRL -- allows authentication.
|
|
This allows authentication to succeed when interim parts of one chain have been
|
|
revoked; for instance, if a certificate is signed by two intermediate CAs due to
|
|
one of them expiring.
|
|
`
|