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:
parent
62eef4e711
commit
be1a2266cc
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
169
builtin/credential/cert/path_crlsets.go
Normal file
169
builtin/credential/cert/path_crlsets.go
Normal 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.
|
||||
`
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue