vault/builtin/credential/cert/path_login.go
2016-03-07 14:59:00 -05:00

313 lines
8.6 KiB
Go

package cert
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"strings"
"github.com/hashicorp/vault/helper/certutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
// 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",
Fields: map[string]*framework.FieldSchema{},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathLogin,
},
}
}
func (b *backend) pathLogin(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
var matched *ParsedCert
if verifyResp, resp, err := b.verifyCredentials(req); err != nil {
return nil, err
} else if resp != nil {
return resp, nil
} else {
matched = verifyResp
}
if matched == nil {
return nil, nil
}
ttl := matched.Entry.TTL
if ttl == 0 {
ttl = b.System().DefaultLeaseTTL()
}
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)
// Generate a response
resp := &logical.Response{
Auth: &logical.Auth{
InternalData: map[string]interface{}{
"subject_key_id": skid,
"authority_key_id": akid,
},
Policies: matched.Entry.Policies,
DisplayName: matched.Entry.DisplayName,
Metadata: map[string]string{
"cert_name": matched.Entry.Name,
"common_name": clientCerts[0].Subject.CommonName,
"subject_key_id": certutil.GetOctalFormatted(clientCerts[0].SubjectKeyId, ":"),
"authority_key_id": certutil.GetOctalFormatted(clientCerts[0].AuthorityKeyId, ":"),
},
LeaseOptions: logical.LeaseOptions{
Renewable: true,
TTL: ttl,
},
},
}
return resp, nil
}
func (b *backend) pathLoginRenew(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
config, err := b.Config(req.Storage)
if err != nil {
return nil, err
}
if !config.DisableBinding {
var matched *ParsedCert
if verifyResp, resp, err := b.verifyCredentials(req); 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 logical.ErrorResponse("client identity during renewal not matching client identity used during login"), nil
}
}
// Get the cert and use its TTL
cert, err := b.Cert(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
}
return framework.LeaseExtend(cert.TTL, 0, b.System())(req, d)
}
func (b *backend) verifyCredentials(req *logical.Request) (*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
// Load the trusted certificates
roots, trusted, trustedNonCAs := b.loadTrustedCerts(req.Storage)
// If trustedNonCAs is not empty it means that client had registered a non-CA cert
// with the backend.
if len(trustedNonCAs) != 0 {
policy := b.matchNonCAPolicy(connState.PeerCertificates[0], trustedNonCAs)
if policy != nil {
return policy, nil, nil
}
}
// Validate the connection state is trusted
trustedChains, err := validateConnState(roots, connState)
if err != nil {
return nil, nil, err
}
// If no trusted chain was found, client is not authenticated
if len(trustedChains) == 0 {
return nil, logical.ErrorResponse("invalid certificate or no client certificate supplied"), nil
}
validChain := b.checkForValidChain(req.Storage, trustedChains)
if !validChain {
return nil, logical.ErrorResponse(
"no chain containing non-revoked certificates could be found for this login certificate",
), nil
}
// Match the trusted chain with the policy
return b.matchPolicy(trustedChains, trusted), nil, nil
}
// matchNonCAPolicy is used to match the client cert with the registered non-CA
// policies to establish client identity.
func (b *backend) matchNonCAPolicy(clientCert *x509.Certificate, trustedNonCAs []*ParsedCert) *ParsedCert {
for _, trustedNonCA := range trustedNonCAs {
tCert := trustedNonCA.Certificates[0]
if tCert.SerialNumber.Cmp(clientCert.SerialNumber) == 0 && bytes.Equal(tCert.AuthorityKeyId, clientCert.AuthorityKeyId) {
return trustedNonCA
}
}
return nil
}
// matchPolicy is used to match the associated policy with the certificate that
// was used to establish the client identity.
func (b *backend) matchPolicy(chains [][]*x509.Certificate, trusted []*ParsedCert) *ParsedCert {
// There is probably a better way to do this...
for _, chain := range chains {
for _, trust := range trusted {
for _, tCert := range trust.Certificates {
for _, cCert := range chain {
if tCert.Equal(cCert) {
return trust
}
}
}
}
}
return nil
}
// loadTrustedCerts is used to load all the trusted certificates from the backend
func (b *backend) loadTrustedCerts(store logical.Storage) (pool *x509.CertPool, trusted []*ParsedCert, trustedNonCAs []*ParsedCert) {
pool = x509.NewCertPool()
names, err := store.List("cert/")
if err != nil {
b.Logger().Printf("[ERR] cert: failed to list trusted certs: %v", err)
return
}
for _, name := range names {
entry, err := b.Cert(store, strings.TrimPrefix(name, "cert/"))
if err != nil {
b.Logger().Printf("[ERR] cert: failed to load trusted certs '%s': %v", name, err)
continue
}
parsed := parsePEM([]byte(entry.Certificate))
if len(parsed) == 0 {
b.Logger().Printf("[ERR] cert: failed to parse certificate for '%s'", name)
continue
}
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,
})
}
}
return
}
func (b *backend) checkForValidChain(store logical.Storage, chains [][]*x509.Certificate) bool {
var badChain bool
for _, chain := range chains {
badChain = false
for _, cert := range chain {
badCRLs := b.findSerialInCRLs(cert.SerialNumber)
if len(badCRLs) != 0 {
badChain = true
break
}
}
if !badChain {
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) {
opts := x509.VerifyOptions{
Roots: roots,
Intermediates: x509.NewCertPool(),
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
certs := cs.PeerCertificates
if len(certs) == 0 {
return nil, nil
}
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
}