| // 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. |
| ` |