Merge pull request #1127 from hashicorp/iss1000-cert-renewal

Cert: renewal enhancements
This commit is contained in:
Vishal Nayak 2016-02-24 21:05:12 -05:00
commit f40cfbeabd
8 changed files with 205 additions and 55 deletions

View file

@ -28,6 +28,7 @@ func Backend() *backend {
},
Paths: append([]*framework.Path{
pathConfig(&b),
pathLogin(&b),
pathCerts(&b),
pathCRLs(&b),

View file

@ -0,0 +1,63 @@
package cert
import (
"fmt"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathConfig(b *backend) *framework.Path {
return &framework.Path{
Pattern: "config",
Fields: map[string]*framework.FieldSchema{
"disable_binding": &framework.FieldSchema{
Type: framework.TypeBool,
Default: false,
Description: `If set, during renewal, skips the matching of presented client identity with the client identity used during login. Defaults to false.`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathConfigWrite,
},
}
}
func (b *backend) pathConfigWrite(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
disableBinding := data.Get("disable_binding").(bool)
entry, err := logical.StorageEntryJSON("config", config{
DisableBinding: disableBinding,
})
if err != nil {
return nil, err
}
if err := req.Storage.Put(entry); err != nil {
return nil, err
}
return nil, nil
}
// Config returns the configuration for this backend.
func (b *backend) Config(s logical.Storage) (*config, error) {
entry, err := s.Get("config")
if err != nil {
return nil, err
}
// Returning a default configuration if an entry is not found
var result config
if entry != nil {
if err := entry.DecodeJSON(&result); err != nil {
return nil, fmt.Errorf("error reading configuration: %s", err)
}
}
return &result, nil
}
type config struct {
DisableBinding bool `json:"disable_binding"`
}

View file

@ -3,6 +3,7 @@ package cert
import (
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"strings"
@ -29,35 +30,16 @@ func pathLogin(b *backend) *framework.Path {
func (b *backend) pathLogin(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
// Get the connection state
if req.Connection == nil || req.Connection.ConnState == nil {
return logical.ErrorResponse("tls connection required"), nil
}
connState := req.Connection.ConnState
// Load the trusted certificates
roots, trusted := b.loadTrustedCerts(req.Storage)
// Validate the connection state is trusted
trustedChains, err := validateConnState(roots, connState)
if err != nil {
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 no trusted chain was found, client is not authenticated
if len(trustedChains) == 0 {
return logical.ErrorResponse("invalid certificate or no client certificate supplied"), nil
}
validChain := b.checkForValidChain(req.Storage, trustedChains)
if !validChain {
return logical.ErrorResponse(
"no chain containing non-revoked certificates could be found for this login certificate",
), nil
}
// Match the trusted chain with the policy
matched := b.matchPolicy(trustedChains, trusted)
if matched == nil {
return nil, nil
}
@ -67,14 +49,25 @@ func (b *backend) pathLogin(
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": connState.PeerCertificates[0].Subject.CommonName,
"common_name": clientCerts[0].Subject.CommonName,
},
LeaseOptions: logical.LeaseOptions{
Renewable: true,
@ -85,6 +78,86 @@ func (b *backend) pathLogin(
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 := b.loadTrustedCerts(req.Storage)
// 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
}
// 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 {
@ -204,18 +277,3 @@ func validateConnState(roots *x509.CertPool, cs *tls.ConnectionState) ([][]*x509
}
return chains, nil
}
func (b *backend) pathLoginRenew(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
// 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)
}

View file

@ -83,7 +83,7 @@ func pathRoles(b *backend) *framework.Path {
Description: `
[Optional for both types]
Comma separated list of CIDR blocks for which the role is applicable for.
CIDR blocks can belong to more than one role. Defaults to zero-address (0.0.0.0/0)`,
CIDR blocks can belong to more than one role.`,
},
"exclude_cidr_list": &framework.FieldSchema{
Type: framework.TypeString,

View file

@ -309,7 +309,7 @@ func (m *ExpirationManager) Renew(leaseID string, increment time.Duration) (*log
// RenewToken is used to renew a token which does not need to
// invoke a logical backend.
func (m *ExpirationManager) RenewToken(source string, token string,
func (m *ExpirationManager) RenewToken(req *logical.Request, source string, token string,
increment time.Duration) (*logical.Auth, error) {
defer metrics.MeasureSince([]string{"expire", "renew-token"}, time.Now())
// Compute the Lease ID
@ -327,7 +327,7 @@ func (m *ExpirationManager) RenewToken(source string, token string,
}
// Attempt to renew the auth entry
resp, err := m.renewAuthEntry(le, increment)
resp, err := m.renewAuthEntry(req, le, increment)
if err != nil {
return nil, err
}
@ -550,14 +550,15 @@ func (m *ExpirationManager) renewEntry(le *leaseEntry, increment time.Duration)
}
// renewAuthEntry is used to attempt renew of an auth entry
func (m *ExpirationManager) renewAuthEntry(le *leaseEntry, increment time.Duration) (*logical.Response, error) {
func (m *ExpirationManager) renewAuthEntry(req *logical.Request, le *leaseEntry, increment time.Duration) (*logical.Response, error) {
auth := *le.Auth
auth.IssueTime = le.IssueTime
auth.Increment = increment
auth.ClientToken = ""
req := logical.RenewAuthRequest(le.Path, &auth, nil)
resp, err := m.router.Route(req)
authReq := logical.RenewAuthRequest(le.Path, &auth, nil)
authReq.Connection = req.Connection
resp, err := m.router.Route(authReq)
if err != nil {
return nil, fmt.Errorf("failed to renew entry: %v", err)
}

View file

@ -222,9 +222,6 @@ func (r *Router) routeCommon(req *logical.Request, existenceCheck bool) (*logica
}
}
// Determine if this path is an unauthenticated path before we modify it
loginPath := r.LoginPath(req.Path)
// Adjust the path to exclude the routing prefix
original := req.Path
req.Path = strings.TrimPrefix(req.Path, mount)
@ -248,11 +245,8 @@ func (r *Router) routeCommon(req *logical.Request, existenceCheck bool) (*logica
req.ClientToken = re.SaltID(req.ClientToken)
}
// If the request is not a login path, then clear the connection
// Cache the pointer to the original connection object
originalConn := req.Connection
if !loginPath {
req.Connection = nil
}
// Reset the request before returning
defer func() {

View file

@ -895,7 +895,7 @@ func (ts *TokenStore) handleRenew(
}
// Renew the token and its children
auth, err := ts.expiration.RenewToken(te.Path, te.ID, increment)
auth, err := ts.expiration.RenewToken(req, te.Path, te.ID, increment)
if err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}

View file

@ -375,3 +375,36 @@ of the header should be "X-Vault-Token" and the value should be the token.
</dd>
</dl>
### /auth/cert/config
#### POST
<dl class="api">
<dt>Description</dt>
<dd>
Configuration options for the backend.
</dd>
<dt>Method</dt>
<dd>POST</dd>
<dt>URL</dt>
<dd>`/auth/cert/config`</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">disable_binding</span>
<span class="param-flags">optional</span>
If set, during renewal, skips the matching of presented client identity with the client identity used during login. Defaults to false.
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>
A `204` response code.
</dd>
</dl>