Files
vault/builtin/credential/cert/path_login.go
hashicorp-copywrite[bot] 0b12cdcfd1 [COMPLIANCE] License changes (#22290)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License.

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* add missing license headers

* Update copyright file headers to BUS-1.1

* Fix test that expected exact offset on hcl file

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Sarah Thompson <sthompson@hashicorp.com>
Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
2023-08-10 18:14:03 -07:00

746 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/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))
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
}
// 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
}