mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 09:42:25 +00:00
* Cache trusted cert values, invalidating when anything changes * rename to something more indicative * defer * changelog * Use an LRU cache rather than a static map so we can't use too much memory. Add docs, unit tests * Don't add to cache if disabled. But this races if just a bool, so make the disabled an atomic
765 lines
24 KiB
Go
765 lines
24 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package cert
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/asn1"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/errwrap"
|
|
"github.com/hashicorp/go-multierror"
|
|
"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"
|
|
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.getTrustedCerts(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))
|
|
hexExtMap := make(map[string]string, len(clientCert.Extensions))
|
|
|
|
for _, ext := range clientCert.Extensions {
|
|
var parsedValue string
|
|
_, err := asn1.Unmarshal(ext.Value, &parsedValue)
|
|
if err != nil {
|
|
clientExtMap[ext.Id.String()] = ""
|
|
} else {
|
|
clientExtMap[ext.Id.String()] = parsedValue
|
|
}
|
|
|
|
hexExtMap[ext.Id.String()] = hex.EncodeToString(ext.Value)
|
|
}
|
|
|
|
// If any of the required extensions don't match the constraint fails
|
|
for _, requiredExt := range config.Entry.RequiredExtensions {
|
|
reqExt := strings.SplitN(requiredExt, ":", 2)
|
|
if len(reqExt) != 2 {
|
|
return false
|
|
}
|
|
|
|
if reqExt[0] == "hex" {
|
|
reqHexExt := strings.SplitN(reqExt[1], ":", 2)
|
|
if len(reqHexExt) != 2 {
|
|
return false
|
|
}
|
|
|
|
clientExtValue, clientExtValueOk := hexExtMap[reqHexExt[0]]
|
|
if !clientExtValueOk || !glob.Glob(strings.ToLower(reqHexExt[1]), clientExtValue) {
|
|
return false
|
|
}
|
|
} else {
|
|
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
|
|
}
|
|
|
|
// getTrustedCerts is used to load all the trusted certificates from the backend, cached
|
|
|
|
func (b *backend) getTrustedCerts(ctx context.Context, storage logical.Storage, certName string) (pool *x509.CertPool, trusted []*ParsedCert, trustedNonCAs []*ParsedCert, conf *ocsp.VerifyConfig) {
|
|
if !b.trustedCacheDisabled.Load() {
|
|
if trusted, found := b.trustedCache.Get(certName); found {
|
|
return trusted.pool, trusted.trusted, trusted.trustedNonCAs, trusted.ocspConf
|
|
}
|
|
}
|
|
return b.loadTrustedCerts(ctx, storage, certName)
|
|
}
|
|
|
|
// 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, trustedCerts []*ParsedCert, trustedNonCAs []*ParsedCert, conf *ocsp.VerifyConfig) {
|
|
pool = x509.NewCertPool()
|
|
trustedCerts = 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, trustedCertPath)
|
|
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, trustedCertPath))
|
|
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
|
|
trustedCerts = append(trustedCerts, &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
|
|
}
|
|
}
|
|
|
|
if !b.trustedCacheDisabled.Load() {
|
|
b.trustedCache.Add(certName, &trusted{
|
|
pool: pool,
|
|
trusted: trustedCerts,
|
|
trustedNonCAs: trustedNonCAs,
|
|
ocspConf: conf,
|
|
})
|
|
}
|
|
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
|
|
}
|