blob: d59c5b4a9195da8347ea9bdf4245634f7d01f35d [file] [log] [blame] [edit]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package cert
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"strings"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/helper/cidrutil"
"github.com/hashicorp/vault/sdk/helper/ocsp"
"github.com/hashicorp/vault/sdk/helper/policyutil"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-multierror"
glob "github.com/ryanuber/go-glob"
)
// ParsedCert is a certificate that has been configured as trusted
type ParsedCert struct {
Entry *CertEntry
Certificates []*x509.Certificate
}
func pathLogin(b *backend) *framework.Path {
return &framework.Path{
Pattern: "login",
DisplayAttrs: &framework.DisplayAttributes{
OperationPrefix: operationPrefixCert,
OperationVerb: "login",
},
Fields: map[string]*framework.FieldSchema{
"name": {
Type: framework.TypeString,
Description: "The name of the certificate role to authenticate against.",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.loginPathWrapper(b.pathLogin),
logical.AliasLookaheadOperation: b.pathLoginAliasLookahead,
logical.ResolveRoleOperation: b.loginPathWrapper(b.pathLoginResolveRole),
},
}
}
func (b *backend) loginPathWrapper(wrappedOp func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error)) framework.OperationFunc {
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
// Make sure that the CRLs have been loaded before processing a login request,
// they might have been nil'd by an invalidate func call.
if err := b.populateCrlsIfNil(ctx, req.Storage); err != nil {
return nil, err
}
return wrappedOp(ctx, req, data)
}
}
func (b *backend) pathLoginResolveRole(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
var matched *ParsedCert
if verifyResp, resp, err := b.verifyCredentials(ctx, req, data); err != nil {
return nil, err
} else if resp != nil {
return resp, nil
} else {
matched = verifyResp
}
if matched == nil {
return logical.ErrorResponse("no certificate was matched by this request"), nil
}
return logical.ResolveRoleResponse(matched.Entry.Name)
}
func (b *backend) pathLoginAliasLookahead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
if req.Connection == nil || req.Connection.ConnState == nil {
return nil, fmt.Errorf("tls connection not found")
}
clientCerts := req.Connection.ConnState.PeerCertificates
if len(clientCerts) == 0 {
return nil, fmt.Errorf("no client certificate found")
}
return &logical.Response{
Auth: &logical.Auth{
Alias: &logical.Alias{
Name: clientCerts[0].Subject.CommonName,
},
},
}, nil
}
func (b *backend) pathLogin(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
config, err := b.Config(ctx, req.Storage)
if err != nil {
return nil, err
}
if b.configUpdated.Load() {
b.updatedConfig(config)
}
var matched *ParsedCert
if verifyResp, resp, err := b.verifyCredentials(ctx, req, data); err != nil {
return nil, err
} else if resp != nil {
return resp, nil
} else {
matched = verifyResp
}
if matched == nil {
return nil, nil
}
if len(matched.Entry.TokenBoundCIDRs) > 0 {
if req.Connection == nil {
b.Logger().Warn("token bound CIDRs found but no connection information available for validation")
return nil, logical.ErrPermissionDenied
}
if !cidrutil.RemoteAddrIsOk(req.Connection.RemoteAddr, matched.Entry.TokenBoundCIDRs) {
return nil, logical.ErrPermissionDenied
}
}
clientCerts := req.Connection.ConnState.PeerCertificates
if len(clientCerts) == 0 {
return logical.ErrorResponse("no client certificate found"), nil
}
skid := base64.StdEncoding.EncodeToString(clientCerts[0].SubjectKeyId)
akid := base64.StdEncoding.EncodeToString(clientCerts[0].AuthorityKeyId)
metadata := map[string]string{
"cert_name": matched.Entry.Name,
"common_name": clientCerts[0].Subject.CommonName,
"serial_number": clientCerts[0].SerialNumber.String(),
"subject_key_id": certutil.GetHexFormatted(clientCerts[0].SubjectKeyId, ":"),
"authority_key_id": certutil.GetHexFormatted(clientCerts[0].AuthorityKeyId, ":"),
}
// Add metadata from allowed_metadata_extensions when present,
// with sanitized oids (dash-separated instead of dot-separated) as keys.
for k, v := range b.certificateExtensionsMetadata(clientCerts[0], matched) {
metadata[k] = v
}
auth := &logical.Auth{
InternalData: map[string]interface{}{
"subject_key_id": skid,
"authority_key_id": akid,
},
DisplayName: matched.Entry.DisplayName,
Metadata: metadata,
Alias: &logical.Alias{
Name: clientCerts[0].Subject.CommonName,
},
}
if config.EnableIdentityAliasMetadata {
auth.Alias.Metadata = metadata
}
matched.Entry.PopulateTokenAuth(auth)
return &logical.Response{
Auth: auth,
}, nil
}
func (b *backend) pathLoginRenew(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
config, err := b.Config(ctx, req.Storage)
if err != nil {
return nil, err
}
if b.configUpdated.Load() {
b.updatedConfig(config)
}
if !config.DisableBinding {
var matched *ParsedCert
if verifyResp, resp, err := b.verifyCredentials(ctx, req, d); err != nil {
return nil, err
} else if resp != nil {
return resp, nil
} else {
matched = verifyResp
}
if matched == nil {
return nil, nil
}
clientCerts := req.Connection.ConnState.PeerCertificates
if len(clientCerts) == 0 {
return logical.ErrorResponse("no client certificate found"), nil
}
skid := base64.StdEncoding.EncodeToString(clientCerts[0].SubjectKeyId)
akid := base64.StdEncoding.EncodeToString(clientCerts[0].AuthorityKeyId)
// Certificate should not only match a registered certificate policy.
// Also, the identity of the certificate presented should match the identity of the certificate used during login
if req.Auth.InternalData["subject_key_id"] != skid && req.Auth.InternalData["authority_key_id"] != akid {
return nil, fmt.Errorf("client identity during renewal not matching client identity used during login")
}
}
// Get the cert and use its TTL
cert, err := b.Cert(ctx, req.Storage, req.Auth.Metadata["cert_name"])
if err != nil {
return nil, err
}
if cert == nil {
// User no longer exists, do not renew
return nil, nil
}
if !policyutil.EquivalentPolicies(cert.TokenPolicies, req.Auth.TokenPolicies) {
return nil, fmt.Errorf("policies have changed, not renewing")
}
resp := &logical.Response{Auth: req.Auth}
resp.Auth.TTL = cert.TokenTTL
resp.Auth.MaxTTL = cert.TokenMaxTTL
resp.Auth.Period = cert.TokenPeriod
return resp, nil
}
func (b *backend) verifyCredentials(ctx context.Context, req *logical.Request, d *framework.FieldData) (*ParsedCert, *logical.Response, error) {
// Get the connection state
if req.Connection == nil || req.Connection.ConnState == nil {
return nil, logical.ErrorResponse("tls connection required"), nil
}
connState := req.Connection.ConnState
if connState.PeerCertificates == nil || len(connState.PeerCertificates) == 0 {
return nil, logical.ErrorResponse("client certificate must be supplied"), nil
}
clientCert := connState.PeerCertificates[0]
// Allow constraining the login request to a single CertEntry
var certName string
if req.Auth != nil { // It's a renewal, use the saved certName
certName = req.Auth.Metadata["cert_name"]
} else if d != nil { // d is nil if handleAuthRenew call the authRenew
certName = d.Get("name").(string)
}
// Load the trusted certificates and other details
roots, trusted, trustedNonCAs, verifyConf := b.loadTrustedCerts(ctx, req.Storage, certName)
// Get the list of full chains matching the connection and validates the
// certificate itself
trustedChains, err := validateConnState(roots, connState)
if err != nil {
return nil, nil, err
}
var extraCas []*x509.Certificate
for _, t := range trusted {
extraCas = append(extraCas, t.Certificates...)
}
// If trustedNonCAs is not empty it means that client had registered a non-CA cert
// with the backend.
var retErr error
if len(trustedNonCAs) != 0 {
for _, trustedNonCA := range trustedNonCAs {
tCert := trustedNonCA.Certificates[0]
// Check for client cert being explicitly listed in the config (and matching other constraints)
if tCert.SerialNumber.Cmp(clientCert.SerialNumber) == 0 &&
bytes.Equal(tCert.AuthorityKeyId, clientCert.AuthorityKeyId) {
matches, err := b.matchesConstraints(ctx, clientCert, trustedNonCA.Certificates, trustedNonCA, verifyConf)
// matchesConstraints returns an error when OCSP verification fails,
// but some other path might still give us success. Add to the
// retErr multierror, but avoid duplicates. This way, if we reach a
// failure later, we can give additional context.
//
// XXX: If matchesConstraints is updated to generate additional,
// immediately fatal errors, we likely need to extend it to return
// another boolean (fatality) or other detection scheme.
if err != nil && (retErr == nil || !errwrap.Contains(retErr, err.Error())) {
retErr = multierror.Append(retErr, err)
}
if matches {
return trustedNonCA, nil, nil
}
}
}
}
// If no trusted chain was found, client is not authenticated
// This check happens after checking for a matching configured non-CA certs
if len(trustedChains) == 0 {
if retErr == nil {
return nil, logical.ErrorResponse(fmt.Sprintf("invalid certificate or no client certificate supplied; additionally got errors during verification: %v", retErr)), nil
}
return nil, logical.ErrorResponse("invalid certificate or no client certificate supplied"), nil
}
// Search for a ParsedCert that intersects with the validated chains and any additional constraints
for _, trust := range trusted { // For each ParsedCert in the config
for _, tCert := range trust.Certificates { // For each certificate in the entry
for _, chain := range trustedChains { // For each root chain that we matched
for _, cCert := range chain { // For each cert in the matched chain
if tCert.Equal(cCert) { // ParsedCert intersects with matched chain
match, err := b.matchesConstraints(ctx, clientCert, chain, trust, verifyConf) // validate client cert + matched chain against the config
// See note above.
if err != nil && (retErr == nil || !errwrap.Contains(retErr, err.Error())) {
retErr = multierror.Append(retErr, err)
}
// Return the first matching entry (for backwards
// compatibility, we continue to just pick the first
// one if we have multiple matches).
//
// Here, we return directly: this means that any
// future OCSP errors would be ignored; in the future,
// if these become fatal, we could revisit this
// choice and choose the first match after evaluating
// all possible candidates.
if match && err == nil {
return trust, nil, nil
}
}
}
}
}
}
if retErr != nil {
return nil, logical.ErrorResponse(fmt.Sprintf("no chain matching all constraints could be found for this login certificate; additionally got errors during verification: %v", retErr)), nil
}
return nil, logical.ErrorResponse("no chain matching all constraints could be found for this login certificate"), nil
}
func (b *backend) matchesConstraints(ctx context.Context, clientCert *x509.Certificate, trustedChain []*x509.Certificate,
config *ParsedCert, conf *ocsp.VerifyConfig,
) (bool, error) {
soFar := !b.checkForChainInCRLs(trustedChain) &&
b.matchesNames(clientCert, config) &&
b.matchesCommonName(clientCert, config) &&
b.matchesDNSSANs(clientCert, config) &&
b.matchesEmailSANs(clientCert, config) &&
b.matchesURISANs(clientCert, config) &&
b.matchesOrganizationalUnits(clientCert, config) &&
b.matchesCertificateExtensions(clientCert, config)
if config.Entry.OcspEnabled {
ocspGood, err := b.checkForCertInOCSP(ctx, clientCert, trustedChain, conf)
if err != nil {
return false, err
}
soFar = soFar && ocspGood
}
return soFar, nil
}
// matchesNames verifies that the certificate matches at least one configured
// allowed name
func (b *backend) matchesNames(clientCert *x509.Certificate, config *ParsedCert) bool {
// Default behavior (no names) is to allow all names
if len(config.Entry.AllowedNames) == 0 {
return true
}
// At least one pattern must match at least one name if any patterns are specified
for _, allowedName := range config.Entry.AllowedNames {
if glob.Glob(allowedName, clientCert.Subject.CommonName) {
return true
}
for _, name := range clientCert.DNSNames {
if glob.Glob(allowedName, name) {
return true
}
}
for _, name := range clientCert.EmailAddresses {
if glob.Glob(allowedName, name) {
return true
}
}
}
return false
}
// matchesCommonName verifies that the certificate matches at least one configured
// allowed common name
func (b *backend) matchesCommonName(clientCert *x509.Certificate, config *ParsedCert) bool {
// Default behavior (no names) is to allow all names
if len(config.Entry.AllowedCommonNames) == 0 {
return true
}
// At least one pattern must match at least one name if any patterns are specified
for _, allowedCommonName := range config.Entry.AllowedCommonNames {
if glob.Glob(allowedCommonName, clientCert.Subject.CommonName) {
return true
}
}
return false
}
// matchesDNSSANs verifies that the certificate matches at least one configured
// allowed dns entry in the subject alternate name extension
func (b *backend) matchesDNSSANs(clientCert *x509.Certificate, config *ParsedCert) bool {
// Default behavior (no names) is to allow all names
if len(config.Entry.AllowedDNSSANs) == 0 {
return true
}
// At least one pattern must match at least one name if any patterns are specified
for _, allowedDNS := range config.Entry.AllowedDNSSANs {
for _, name := range clientCert.DNSNames {
if glob.Glob(allowedDNS, name) {
return true
}
}
}
return false
}
// matchesEmailSANs verifies that the certificate matches at least one configured
// allowed email in the subject alternate name extension
func (b *backend) matchesEmailSANs(clientCert *x509.Certificate, config *ParsedCert) bool {
// Default behavior (no names) is to allow all names
if len(config.Entry.AllowedEmailSANs) == 0 {
return true
}
// At least one pattern must match at least one name if any patterns are specified
for _, allowedEmail := range config.Entry.AllowedEmailSANs {
for _, email := range clientCert.EmailAddresses {
if glob.Glob(allowedEmail, email) {
return true
}
}
}
return false
}
// matchesURISANs verifies that the certificate matches at least one configured
// allowed uri in the subject alternate name extension
func (b *backend) matchesURISANs(clientCert *x509.Certificate, config *ParsedCert) bool {
// Default behavior (no names) is to allow all names
if len(config.Entry.AllowedURISANs) == 0 {
return true
}
// At least one pattern must match at least one name if any patterns are specified
for _, allowedURI := range config.Entry.AllowedURISANs {
for _, name := range clientCert.URIs {
if glob.Glob(allowedURI, name.String()) {
return true
}
}
}
return false
}
// matchesOrganizationalUnits verifies that the certificate matches at least one configurd allowed OU
func (b *backend) matchesOrganizationalUnits(clientCert *x509.Certificate, config *ParsedCert) bool {
// Default behavior (no OUs) is to allow all OUs
if len(config.Entry.AllowedOrganizationalUnits) == 0 {
return true
}
// At least one pattern must match at least one name if any patterns are specified
for _, allowedOrganizationalUnits := range config.Entry.AllowedOrganizationalUnits {
for _, ou := range clientCert.Subject.OrganizationalUnit {
if glob.Glob(allowedOrganizationalUnits, ou) {
return true
}
}
}
return false
}
// matchesCertificateExtensions verifies that the certificate matches configured
// required extensions
func (b *backend) matchesCertificateExtensions(clientCert *x509.Certificate, config *ParsedCert) bool {
// If no required extensions, nothing to check here
if len(config.Entry.RequiredExtensions) == 0 {
return true
}
// Fail fast if we have required extensions but no extensions on the cert
if len(clientCert.Extensions) == 0 {
return false
}
// Build Client Extensions Map for Constraint Matching
// x509 Writes Extensions in ASN1 with a bitstring tag, which results in the field
// including its ASN.1 type tag bytes. For the sake of simplicity, assume string type
// and drop the tag bytes. And get the number of bytes from the tag.
clientExtMap := make(map[string]string, len(clientCert.Extensions))
for _, ext := range clientCert.Extensions {
var parsedValue string
asn1.Unmarshal(ext.Value, &parsedValue)
clientExtMap[ext.Id.String()] = parsedValue
}
// If any of the required extensions don'log match the constraint fails
for _, requiredExt := range config.Entry.RequiredExtensions {
reqExt := strings.SplitN(requiredExt, ":", 2)
clientExtValue, clientExtValueOk := clientExtMap[reqExt[0]]
if !clientExtValueOk || !glob.Glob(reqExt[1], clientExtValue) {
return false
}
}
return true
}
// certificateExtensionsMetadata returns the metadata from configured
// metadata extensions
func (b *backend) certificateExtensionsMetadata(clientCert *x509.Certificate, config *ParsedCert) map[string]string {
// If no metadata extensions are configured, return an empty map
if len(config.Entry.AllowedMetadataExtensions) == 0 {
return map[string]string{}
}
// Build a map with the accepted oid strings as keys, and the metadata keys as values.
allowedOidMap := make(map[string]string, len(config.Entry.AllowedMetadataExtensions))
for _, oidString := range config.Entry.AllowedMetadataExtensions {
// Avoid dots in metadata keys and put dashes instead,
// to allow use policy templates.
allowedOidMap[oidString] = strings.ReplaceAll(oidString, ".", "-")
}
// Collect the metadata from accepted certificate extensions.
metadata := make(map[string]string, len(config.Entry.AllowedMetadataExtensions))
for _, ext := range clientCert.Extensions {
if metadataKey, ok := allowedOidMap[ext.Id.String()]; ok {
// x509 Writes Extensions in ASN1 with a bitstring tag, which results in the field
// including its ASN.1 type tag bytes. For the sake of simplicity, assume string type
// and drop the tag bytes. And get the number of bytes from the tag.
var parsedValue string
asn1.Unmarshal(ext.Value, &parsedValue)
metadata[metadataKey] = parsedValue
}
}
return metadata
}
// loadTrustedCerts is used to load all the trusted certificates from the backend
func (b *backend) loadTrustedCerts(ctx context.Context, storage logical.Storage, certName string) (pool *x509.CertPool, trusted []*ParsedCert, trustedNonCAs []*ParsedCert, conf *ocsp.VerifyConfig) {
pool = x509.NewCertPool()
trusted = make([]*ParsedCert, 0)
trustedNonCAs = make([]*ParsedCert, 0)
var names []string
if certName != "" {
names = append(names, certName)
} else {
var err error
names, err = storage.List(ctx, "cert/")
if err != nil {
b.Logger().Error("failed to list trusted certs", "error", err)
return
}
}
conf = &ocsp.VerifyConfig{}
for _, name := range names {
entry, err := b.Cert(ctx, storage, strings.TrimPrefix(name, "cert/"))
if err != nil {
b.Logger().Error("failed to load trusted cert", "name", name, "error", err)
continue
}
if entry == nil {
// This could happen when the certName was provided and the cert doesn'log exist,
// or just if between the LIST and the GET the cert was deleted.
continue
}
parsed := parsePEM([]byte(entry.Certificate))
if len(parsed) == 0 {
b.Logger().Error("failed to parse certificate", "name", name)
continue
}
parsed = append(parsed, parsePEM([]byte(entry.OcspCaCertificates))...)
if !parsed[0].IsCA {
trustedNonCAs = append(trustedNonCAs, &ParsedCert{
Entry: entry,
Certificates: parsed,
})
} else {
for _, p := range parsed {
pool.AddCert(p)
}
// Create a ParsedCert entry
trusted = append(trusted, &ParsedCert{
Entry: entry,
Certificates: parsed,
})
}
if entry.OcspEnabled {
conf.OcspEnabled = true
conf.OcspServersOverride = append(conf.OcspServersOverride, entry.OcspServersOverride...)
if entry.OcspFailOpen {
conf.OcspFailureMode = ocsp.FailOpenTrue
} else {
conf.OcspFailureMode = ocsp.FailOpenFalse
}
conf.QueryAllServers = conf.QueryAllServers || entry.OcspQueryAllServers
}
}
return
}
func (b *backend) checkForCertInOCSP(ctx context.Context, clientCert *x509.Certificate, chain []*x509.Certificate, conf *ocsp.VerifyConfig) (bool, error) {
if !conf.OcspEnabled || len(chain) < 2 {
return true, nil
}
b.ocspClientMutex.RLock()
defer b.ocspClientMutex.RUnlock()
err := b.ocspClient.VerifyLeafCertificate(ctx, clientCert, chain[1], conf)
if err != nil {
// We want to preserve error messages when they have additional,
// potentially useful information. Just having a revoked cert
// isn't additionally useful.
if !strings.Contains(err.Error(), "has been revoked") {
return false, err
}
return false, nil
}
return true, nil
}
func (b *backend) checkForChainInCRLs(chain []*x509.Certificate) bool {
badChain := false
for _, cert := range chain {
badCRLs := b.findSerialInCRLs(cert.SerialNumber)
if len(badCRLs) != 0 {
badChain = true
break
}
}
return badChain
}
func (b *backend) checkForValidChain(chains [][]*x509.Certificate) bool {
for _, chain := range chains {
if !b.checkForChainInCRLs(chain) {
return true
}
}
return false
}
// parsePEM parses a PEM encoded x509 certificate
func parsePEM(raw []byte) (certs []*x509.Certificate) {
for len(raw) > 0 {
var block *pem.Block
block, raw = pem.Decode(raw)
if block == nil {
break
}
if (block.Type != "CERTIFICATE" && block.Type != "TRUSTED CERTIFICATE") || len(block.Headers) != 0 {
continue
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
continue
}
certs = append(certs, cert)
}
return
}
// validateConnState is used to validate that the TLS client is authorized
// by at trusted certificate. Most of this logic is lifted from the client
// verification logic here: http://golang.org/src/crypto/tls/handshake_server.go
// The trusted chains are returned.
func validateConnState(roots *x509.CertPool, cs *tls.ConnectionState) ([][]*x509.Certificate, error) {
certs := cs.PeerCertificates
if len(certs) == 0 {
return nil, nil
}
opts := x509.VerifyOptions{
Roots: roots,
Intermediates: x509.NewCertPool(),
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
if len(certs) > 1 {
for _, cert := range certs[1:] {
opts.Intermediates.AddCert(cert)
}
}
chains, err := certs[0].Verify(opts)
if err != nil {
if _, ok := err.(x509.UnknownAuthorityError); ok {
return nil, nil
}
return nil, errors.New("failed to verify client's certificate: " + err.Error())
}
return chains, nil
}