| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package cert |
| |
| import ( |
| "context" |
| "crypto/x509" |
| "fmt" |
| "io" |
| "net/http" |
| "strings" |
| "sync" |
| "sync/atomic" |
| "time" |
| |
| "github.com/hashicorp/go-hclog" |
| "github.com/hashicorp/go-multierror" |
| "github.com/hashicorp/vault/sdk/framework" |
| "github.com/hashicorp/vault/sdk/helper/ocsp" |
| "github.com/hashicorp/vault/sdk/logical" |
| ) |
| |
| const operationPrefixCert = "cert" |
| |
| func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) { |
| b := Backend() |
| if err := b.Setup(ctx, conf); err != nil { |
| return nil, err |
| } |
| return b, nil |
| } |
| |
| func Backend() *backend { |
| var b backend |
| b.Backend = &framework.Backend{ |
| Help: backendHelp, |
| PathsSpecial: &logical.Paths{ |
| Unauthenticated: []string{ |
| "login", |
| }, |
| }, |
| Paths: []*framework.Path{ |
| pathConfig(&b), |
| pathLogin(&b), |
| pathListCerts(&b), |
| pathCerts(&b), |
| pathListCRLs(&b), |
| pathCRLs(&b), |
| }, |
| AuthRenew: b.loginPathWrapper(b.pathLoginRenew), |
| Invalidate: b.invalidate, |
| BackendType: logical.TypeCredential, |
| InitializeFunc: b.initialize, |
| PeriodicFunc: b.updateCRLs, |
| } |
| |
| b.crlUpdateMutex = &sync.RWMutex{} |
| return &b |
| } |
| |
| type backend struct { |
| *framework.Backend |
| MapCertId *framework.PathMap |
| |
| crls map[string]CRLInfo |
| crlUpdateMutex *sync.RWMutex |
| ocspClientMutex sync.RWMutex |
| ocspClient *ocsp.Client |
| configUpdated atomic.Bool |
| } |
| |
| func (b *backend) initialize(ctx context.Context, req *logical.InitializationRequest) error { |
| bConf, err := b.Config(ctx, req.Storage) |
| if err != nil { |
| b.Logger().Error(fmt.Sprintf("failed to load backend configuration: %v", err)) |
| return err |
| } |
| |
| if bConf != nil { |
| b.updatedConfig(bConf) |
| } |
| |
| if err := b.lockThenpopulateCRLs(ctx, req.Storage); err != nil { |
| b.Logger().Error(fmt.Sprintf("failed to populate CRLs: %v", err)) |
| return err |
| } |
| |
| return nil |
| } |
| |
| func (b *backend) invalidate(_ context.Context, key string) { |
| switch { |
| case strings.HasPrefix(key, "crls/"): |
| b.crlUpdateMutex.Lock() |
| defer b.crlUpdateMutex.Unlock() |
| b.crls = nil |
| case key == "config": |
| b.configUpdated.Store(true) |
| } |
| } |
| |
| func (b *backend) initOCSPClient(cacheSize int) { |
| b.ocspClient = ocsp.New(func() hclog.Logger { |
| return b.Logger() |
| }, cacheSize) |
| } |
| |
| func (b *backend) updatedConfig(config *config) { |
| b.ocspClientMutex.Lock() |
| defer b.ocspClientMutex.Unlock() |
| b.initOCSPClient(config.OcspCacheSize) |
| b.configUpdated.Store(false) |
| return |
| } |
| |
| func (b *backend) fetchCRL(ctx context.Context, storage logical.Storage, name string, crl *CRLInfo) error { |
| response, err := http.Get(crl.CDP.Url) |
| if err != nil { |
| return err |
| } |
| if response.StatusCode == http.StatusOK { |
| body, err := io.ReadAll(response.Body) |
| if err != nil { |
| return err |
| } |
| certList, err := x509.ParseCRL(body) |
| if err != nil { |
| return err |
| } |
| crl.CDP.ValidUntil = certList.TBSCertList.NextUpdate |
| return b.setCRL(ctx, storage, certList, name, crl.CDP) |
| } |
| return fmt.Errorf("unexpected response code %d fetching CRL from %s", response.StatusCode, crl.CDP.Url) |
| } |
| |
| func (b *backend) updateCRLs(ctx context.Context, req *logical.Request) error { |
| b.crlUpdateMutex.Lock() |
| defer b.crlUpdateMutex.Unlock() |
| var errs *multierror.Error |
| for name, crl := range b.crls { |
| if crl.CDP != nil && time.Now().After(crl.CDP.ValidUntil) { |
| if err := b.fetchCRL(ctx, req.Storage, name, &crl); err != nil { |
| errs = multierror.Append(errs, err) |
| } |
| } |
| } |
| return errs.ErrorOrNil() |
| } |
| |
| func (b *backend) storeConfig(ctx context.Context, storage logical.Storage, config *config) error { |
| entry, err := logical.StorageEntryJSON("config", config) |
| if err != nil { |
| return err |
| } |
| |
| if err := storage.Put(ctx, entry); err != nil { |
| return err |
| } |
| b.updatedConfig(config) |
| return nil |
| } |
| |
| const backendHelp = ` |
| The "cert" credential provider allows authentication using |
| TLS client certificates. A client connects to Vault and uses |
| the "login" endpoint to generate a client token. |
| |
| Trusted certificates are configured using the "certs/" endpoint |
| by a user with root access. A certificate authority can be trusted, |
| which permits all keys signed by it. Alternatively, self-signed |
| certificates can be trusted avoiding the need for a CA. |
| ` |