Add CRLSets endpoints; write method is done. Add verification logic to

login path. Change certs "ttl" field to be a string to match common
backend behavior.
This commit is contained in:
Jeff Mitchell 2015-10-05 21:10:20 -04:00
parent 62eef4e711
commit be1a2266cc
5 changed files with 264 additions and 29 deletions

View file

@ -17,6 +17,7 @@ func Backend() *framework.Backend {
PathsSpecial: &logical.Paths{
Root: []string{
"certs/*",
"crlsets/*",
},
Unauthenticated: []string{
@ -27,6 +28,7 @@ func Backend() *framework.Backend {
Paths: append([]*framework.Path{
pathLogin(&b),
pathCerts(&b),
pathCRLSets(&b),
}),
AuthRenew: b.pathLoginRenew,

View file

@ -11,6 +11,19 @@ import (
logicaltest "github.com/hashicorp/vault/logical/testing"
)
func testFactory(t *testing.T) logical.Backend {
b, err := Factory(&logical.BackendConfig{
System: &logical.StaticSystemView{
DefaultLeaseTTLVal: 300 * time.Second,
MaxLeaseTTLVal: 1800 * time.Second,
},
})
if err != nil {
t.Fatal("error: %s", err)
}
return b
}
// Test a client trusted by a CA
func TestBackend_basic_CA(t *testing.T) {
connState := testConnState(t, "../../../test/key/ourdomain.cer",
@ -19,17 +32,12 @@ func TestBackend_basic_CA(t *testing.T) {
if err != nil {
t.Fatalf("err: %v", err)
}
b, err := Factory(&logical.BackendConfig{
System: &logical.StaticSystemView{
DefaultLeaseTTLVal: 300 * time.Second,
MaxLeaseTTLVal: 1800 * time.Second,
},
})
logicaltest.Test(t, logicaltest.TestCase{
Backend: b,
Backend: testFactory(t),
Steps: []logicaltest.TestStep{
testAccStepCert(t, "web", ca, "foo"),
testAccStepLogin(t, connState),
testAccStepCertLease(t, "web", ca, "foo"),
testAccStepCertTTL(t, "web", ca, "foo"),
testAccStepLogin(t, connState),
testAccStepCertNoLease(t, "web", ca, "foo"),
@ -47,7 +55,7 @@ func TestBackend_basic_singleCert(t *testing.T) {
t.Fatalf("err: %v", err)
}
logicaltest.Test(t, logicaltest.TestCase{
Backend: Backend(),
Backend: testFactory(t),
Steps: []logicaltest.TestStep{
testAccStepCert(t, "web", ca, "foo"),
testAccStepLogin(t, connState),
@ -60,7 +68,7 @@ func TestBackend_untrusted(t *testing.T) {
connState := testConnState(t, "../../../test/unsigned/cert.pem",
"../../../test/unsigned/key.pem")
logicaltest.Test(t, logicaltest.TestCase{
Backend: Backend(),
Backend: testFactory(t),
Steps: []logicaltest.TestStep{
testAccStepLoginInvalid(t, connState),
},
@ -131,6 +139,20 @@ func testAccStepCert(
}
}
func testAccStepCertLease(
t *testing.T, name string, cert []byte, policies string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.WriteOperation,
Path: "certs/" + name,
Data: map[string]interface{}{
"certificate": string(cert),
"policies": policies,
"display_name": name,
"lease": 1000,
},
}
}
func testAccStepCertTTL(
t *testing.T, name string, cert []byte, policies string) logicaltest.TestStep {
return logicaltest.TestStep{
@ -140,7 +162,7 @@ func testAccStepCertTTL(
"certificate": string(cert),
"policies": policies,
"display_name": name,
"ttl": 1000,
"ttl": "1000s",
},
}
}

View file

@ -1,6 +1,7 @@
package cert
import (
"fmt"
"strings"
"time"
@ -41,9 +42,9 @@ seconds. Defaults to system/backend default TTL.`,
},
"ttl": &framework.FieldSchema{
Type: framework.TypeInt,
Description: `TTL time in seconds. Defaults to system/backend
default TTL time.`,
Type: framework.TypeString,
Description: `TTL for tokens issued by this backend.
Defaults to system/backend default TTL time.`,
},
},
@ -103,7 +104,7 @@ func (b *backend) pathCertRead(
"certificate": cert.Certificate,
"display_name": cert.DisplayName,
"policies": strings.Join(cert.Policies, ","),
"ttl": duration,
"ttl": duration / time.Second,
},
}, nil
}
@ -131,24 +132,35 @@ func (b *backend) pathCertWrite(
return logical.ErrorResponse("failed to parse certificate"), nil
}
// Parse the lease duration or default to backend/system default
leaseDur := time.Duration(0)
leaseSec := d.Get("ttl").(int)
if leaseSec == 0 {
leaseSec = d.Get("lease").(int)
}
if leaseSec > 0 {
leaseDur = time.Duration(leaseSec) * time.Second
}
// Store it
entry, err := logical.StorageEntryJSON("cert/"+name, &CertEntry{
certEntry := &CertEntry{
Name: name,
Certificate: certificate,
DisplayName: displayName,
Policies: policies,
TTL: leaseDur,
})
}
// Parse the lease duration or default to backend/system default
var err error
maxTTL := b.System().MaxLeaseTTL()
ttlStr := d.Get("ttl").(string)
var ttl time.Duration
if len(ttlStr) == 0 {
ttl = time.Second * time.Duration(d.Get("lease").(int))
} else {
ttl, err = time.ParseDuration(ttlStr)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("Failed to parse ttl of %d", ttlStr)), nil
}
}
if ttl > maxTTL {
return logical.ErrorResponse(fmt.Sprintf("Given TTL of %d seconds greater than current mount/system default of %d seconds", ttl/time.Second, maxTTL/time.Second)), nil
}
if ttl > time.Duration(0) {
certEntry.TTL = ttl
}
// Store it
entry, err := logical.StorageEntryJSON("cert/"+name, certEntry)
if err != nil {
return nil, err
}

View file

@ -0,0 +1,169 @@
package cert
import (
"crypto/x509"
"fmt"
"math/big"
"strings"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathCRLSets(b *backend) *framework.Path {
return &framework.Path{
Pattern: "crlsets/" + framework.GenericNameRegex("name"),
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "The name of the certificate",
},
"crl": &framework.FieldSchema{
Type: framework.TypeString,
Description: `The public certificate that should be trusted.
Must be x509 PEM encoded.`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.DeleteOperation: b.pathCRLSetDelete,
logical.ReadOperation: b.pathCRLSetRead,
logical.WriteOperation: b.pathCRLSetWrite,
},
HelpSynopsis: pathCRLSetsHelpSyn,
HelpDescription: pathCRLSetsHelpDesc,
}
}
func (b *backend) pathCRLSetDelete(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
// FIXME: There should be a path, or a value to this path, to clear out
// an individual serial in case of an index out-of-sync issue
err := req.Storage.Delete("cert/" + strings.ToLower(d.Get("name").(string)))
if err != nil {
return nil, err
}
return nil, nil
}
func (b *backend) pathCRLSetRead(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := strings.ToLower(d.Get("name").(string))
if name == "" {
return logical.ErrorResponse(`"name" parameter cannot be empty`), nil
}
return nil, nil
/*
return &logical.Response{
Data: map[string]interface{}{
"certificate": cert.Certificate,
"display_name": cert.DisplayName,
"policies": strings.Join(cert.Policies, ","),
"ttl": duration / time.Second,
},
}, nil
*/
}
func (b *backend) pathCRLSetWrite(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := strings.ToLower(d.Get("name").(string))
crl := d.Get("crl").(string)
certList, err := x509.ParseCRL([]byte(crl))
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("failed to parse CRL: %v", err)), nil
}
if certList == nil {
return logical.ErrorResponse("parsed CRL is nil"), nil
}
entry, err := logical.StorageEntryJSON("crlsets/set/"+name, crl)
if err != nil {
return nil, err
}
if err = req.Storage.Put(entry); err != nil {
return nil, err
}
// Clear out old entries if this is replacing a previously-set CRL.
// In practice this is what the index is for; it lets us store
// the certs individually for storage efficiency but ensure we can
// clean up properly. So use it to clean up.
// N.B.: This is best-effort. The worst thing that can happen is
// some wasted storage
b.cleanIndex(req.Storage, name)
crlSetIndex := []*big.Int{}
for _, revokedCert := range certList.TBSCertList.RevokedCertificates {
crlSetIndex = append(crlSetIndex, revokedCert.SerialNumber)
}
entry, err = logical.StorageEntryJSON("crlsets/index/"+name, crlSetIndex)
if err != nil {
return nil, err
}
if err = req.Storage.Put(entry); err != nil {
return nil, err
}
for _, revokedSerial := range crlSetIndex {
entry, err = logical.StorageEntryJSON("crlsets/serial/"+revokedSerial.String(),
&RevokedSerial{
CRLSet: name,
})
if err != nil {
return nil, err
}
if err = req.Storage.Put(entry); err != nil {
return nil, err
}
}
return nil, nil
}
func (b *backend) cleanIndex(storage logical.Storage, name string) {
entry, err := storage.Get("crlsets/index/" + name)
if err != nil {
return
}
if entry == nil {
return
}
crlSetIndex := []*big.Int{}
err = entry.DecodeJSON(&crlSetIndex)
if err != nil {
goto destroyIndex
}
for _, serial := range crlSetIndex {
storage.Delete("crlsets/serial/" + serial.String())
}
destroyIndex:
storage.Delete("crlsets/index/" + name)
return
}
type RevokedSerial struct {
CRLSet string `json:"crlset"`
}
//FIXME
const pathCRLSetsHelpSyn = `
Manage trusted certificates used for authentication.
`
const pathCRLSetsHelpDesc = `
This endpoint allows you to create, read, update, and delete trusted certificates
that are allowed to authenticate.
Deleting a certificate will not revoke auth for prior authenticated connections.
To do this, do a revoke on "login". If you don't need to revoke login immediately,
then the next renew will cause the lease to expire.
`

View file

@ -5,6 +5,7 @@ import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"strings"
"github.com/hashicorp/vault/logical"
@ -49,6 +50,14 @@ func (b *backend) pathLogin(
return logical.ErrorResponse("invalid certificate or no client certificate supplied"), nil
}
crlSetMatch, err := b.checkRevocation(req.Storage, trustedChains)
if err != nil {
return nil, fmt.Errorf("error checking revocation: %v", err)
}
if crlSetMatch != "" {
return logical.ErrorResponse(fmt.Sprintf("certificate in the chain has been revoked by set %s", crlSetMatch)), nil
}
// Match the trusted chain with the policy
matched := b.matchPolicy(trustedChains, trusted)
if matched == nil {
@ -128,6 +137,27 @@ func (b *backend) loadTrustedCerts(store logical.Storage) (pool *x509.CertPool,
return
}
func (b *backend) checkRevocation(store logical.Storage, chains [][]*x509.Certificate) (string, error) {
var revokedSerial RevokedSerial
var badCRLSet string
for _, chain := range chains {
for _, cert := range chain {
entry, err := store.Get("crlsets/serial/" + cert.SerialNumber.String())
if err != nil {
return "", fmt.Errorf("error looking up revoked serials: %v", err)
}
if entry != nil {
err := entry.DecodeJSON(&revokedSerial)
if err != nil {
return "", fmt.Errorf("error decoding revoked serial entry: %v", err)
}
badCRLSet = revokedSerial.CRLSet
}
}
}
return badCRLSet, nil
}
// parsePEM parses a PEM encoded x509 certificate
func parsePEM(raw []byte) (certs []*x509.Certificate) {
for len(raw) > 0 {
@ -136,7 +166,7 @@ func parsePEM(raw []byte) (certs []*x509.Certificate) {
if block == nil {
break
}
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
if (block.Type != "CERTIFICATE" && block.Type != "TRUSTED CERTIFICATE") || len(block.Headers) != 0 {
continue
}