A few things:

* Add comments to every non-obvious (e.g. not basic read/write handler type) function
* Remove revoked/ endpoint, at least for now
* Add configurable CRL lifetime
* Cleanup
* Address some comments from code review

Commit contents (C)2015 Akamai Technologies, Inc. <opensource@akamai.com>
This commit is contained in:
Jeff Mitchell 2015-06-19 12:48:18 -04:00
parent 23ba605068
commit 435aefc072
11 changed files with 396 additions and 315 deletions

View File

@ -2,6 +2,8 @@ package pki
import (
"strings"
"sync"
"time"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
@ -21,7 +23,6 @@ func Backend() *framework.Backend {
PathsSpecial: &logical.Paths{
Root: []string{
"config/*",
"revoked/*",
"revoke/*",
"crl/rotate",
},
@ -37,13 +38,13 @@ func Backend() *framework.Backend {
Paths: []*framework.Path{
pathRoles(&b),
pathConfigCA(&b),
pathConfigCRL(&b),
pathIssue(&b),
pathRotateCRL(&b),
pathFetchCA(&b),
pathFetchCRL(&b),
pathFetchCRLViaCertPath(&b),
pathFetchValid(&b),
pathFetchRevoked(&b),
pathRevoke(&b),
},
@ -52,11 +53,17 @@ func Backend() *framework.Backend {
},
}
b.crlLifetime = time.Hour * 72
b.revokeStorageLock = &sync.Mutex{}
return b.Backend
}
type backend struct {
*framework.Backend
crlLifetime time.Duration
revokeStorageLock *sync.Mutex
}
const backendHelp = `

View File

@ -1,7 +1,6 @@
package pki
import (
"crypto"
"crypto/x509"
"encoding/pem"
"fmt"
@ -22,77 +21,7 @@ var (
stepCount = 0
)
func checkCertsAndPrivateKey(keyType string, usage certUsage, validity time.Duration, certBundle *certutil.CertBundle) (cert, ca *x509.Certificate, privKey crypto.Signer, err error) {
var pemBlock *pem.Block
pemBlock, _ = pem.Decode([]byte(certBundle.Certificate))
if pemBlock == nil {
return nil, nil, nil, fmt.Errorf("No PEM data found for cert")
}
cert, err = x509.ParseCertificate(pemBlock.Bytes)
if err != nil {
return nil, nil, nil, err
}
pemBlock, _ = pem.Decode([]byte(certBundle.IssuingCA))
if pemBlock == nil {
return nil, nil, nil, fmt.Errorf("No PEM data found for issuing CA")
}
ca, err = x509.ParseCertificate(pemBlock.Bytes)
if err != nil {
return nil, nil, nil, err
}
pemBlock, _ = pem.Decode([]byte(certBundle.PrivateKey))
if pemBlock == nil {
return nil, nil, nil, fmt.Errorf("No PEM data found for private key")
}
switch keyType {
case "rsa":
privKey, err = x509.ParsePKCS1PrivateKey(pemBlock.Bytes)
if err != nil {
return nil, nil, nil, err
}
case "ec":
privKey, err = x509.ParseECPrivateKey(pemBlock.Bytes)
if err != nil {
return nil, nil, nil, fmt.Errorf("Unable to decode EC private key: %s; value was %s", err, certBundle.PrivateKey)
}
default:
return nil, nil, nil, fmt.Errorf("Unknown private key type %s", keyType)
}
// There should only be one usage type, because only one is requested
// in the tests
if len(cert.ExtKeyUsage) != 1 {
return cert, nil, nil, fmt.Errorf("Got wrong size key usage in generated cert")
}
switch usage {
case serverUsage:
if cert.ExtKeyUsage[0] != x509.ExtKeyUsageServerAuth {
return cert, nil, nil, fmt.Errorf("Bad key usage")
}
case clientUsage:
if cert.ExtKeyUsage[0] != x509.ExtKeyUsageClientAuth {
return cert, nil, nil, fmt.Errorf("Bad key usage")
}
case codeSigningUsage:
if cert.ExtKeyUsage[0] != x509.ExtKeyUsageCodeSigning {
return cert, nil, nil, fmt.Errorf("Bad key usage")
}
}
if math.Abs(float64(time.Now().Unix()-cert.NotBefore.Unix())) > 10 {
return cert, nil, nil, fmt.Errorf("Validity period starts out of range")
}
if math.Abs(float64(time.Now().Add(validity).Unix()-cert.NotAfter.Unix())) > 10 {
return cert, nil, nil, fmt.Errorf("Validity period too large")
}
return
}
// Performs basic tests on CA functionality
func TestBackend_basic(t *testing.T) {
b := Backend()
@ -108,6 +37,8 @@ func TestBackend_basic(t *testing.T) {
logicaltest.Test(t, testCase)
}
// Generates and tests steps that walk through the various possibilities
// of role flags to ensure that they are properly restricted
func TestBackend_roles(t *testing.T) {
b := Backend()
@ -129,6 +60,65 @@ func TestBackend_roles(t *testing.T) {
logicaltest.Test(t, testCase)
}
// Performs some validity checking on the returned bundles
func checkCertsAndPrivateKey(keyType string, usage certUsage, validity time.Duration, certBundle *certutil.CertBundle) (*certutil.ParsedCertBundle, error) {
parsedCertBundle, err := certBundle.ToParsedCertBundle()
if err != nil {
return nil, fmt.Errorf("Error parsing cert bundle: %s", err)
}
switch {
case parsedCertBundle.Certificate == nil:
return nil, fmt.Errorf("Did not find a certificate in the cert bundle")
case parsedCertBundle.IssuingCA == nil:
return nil, fmt.Errorf("Did not find a CA in the cert bundle")
case parsedCertBundle.PrivateKey == nil:
return nil, fmt.Errorf("Did not find a private key in the cert bundle")
case parsedCertBundle.PrivateKeyType == certutil.UnknownPrivateKey:
return nil, fmt.Errorf("Could not figure out type of private key")
}
switch {
case parsedCertBundle.PrivateKeyType == certutil.RSAPrivateKey && keyType != "rsa":
fallthrough
case parsedCertBundle.PrivateKeyType == certutil.ECPrivateKey && keyType != "ec":
return nil, fmt.Errorf("Given key type does not match type found in bundle")
}
cert := parsedCertBundle.Certificate
// There should only be one usage type, because only one is requested
// in the tests
if len(cert.ExtKeyUsage) != 1 {
return nil, fmt.Errorf("Got wrong size key usage in generated cert")
}
switch usage {
case serverUsage:
if cert.ExtKeyUsage[0] != x509.ExtKeyUsageServerAuth {
return nil, fmt.Errorf("Bad key usage")
}
case clientUsage:
if cert.ExtKeyUsage[0] != x509.ExtKeyUsageClientAuth {
return nil, fmt.Errorf("Bad key usage")
}
case codeSigningUsage:
if cert.ExtKeyUsage[0] != x509.ExtKeyUsageCodeSigning {
return nil, fmt.Errorf("Bad key usage")
}
}
if math.Abs(float64(time.Now().Unix()-cert.NotBefore.Unix())) > 10 {
return nil, fmt.Errorf("Validity period starts out of range")
}
if math.Abs(float64(time.Now().Add(validity).Unix()-cert.NotAfter.Unix())) > 10 {
return nil, fmt.Errorf("Validity period too large")
}
return parsedCertBundle, nil
}
// Generates steps to test out CA configuration -- certificates + CRL expiry,
// and ensure that the certificates are readable after storing them
func generateCASteps(t *testing.T) []logicaltest.TestStep {
ret := []logicaltest.TestStep{
logicaltest.TestStep{
@ -139,6 +129,14 @@ func generateCASteps(t *testing.T) []logicaltest.TestStep {
},
},
logicaltest.TestStep{
Operation: logical.WriteOperation,
Path: "config/crl",
Data: map[string]interface{}{
"expiry": "16h",
},
},
// Ensure we can fetch it back via unauthenticated means, in various formats
logicaltest.TestStep{
Operation: logical.ReadOperation,
@ -187,11 +185,23 @@ func generateCASteps(t *testing.T) []logicaltest.TestStep {
return nil
},
},
logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: "config/crl",
Check: func(resp *logical.Response) error {
if resp.Data["expiry"].(string) != "16h" {
return fmt.Errorf("CRL lifetimes do not match (got %s)", resp.Data["expiry"].(string))
}
return nil
},
},
}
return ret
}
// Generates steps to test out various role permutations
func generateRoleSteps(t *testing.T) []logicaltest.TestStep {
roleVals := roleEntry{
LeaseMax: "12h",
@ -215,6 +225,7 @@ func generateRoleSteps(t *testing.T) []logicaltest.TestStep {
return fmt.Errorf("Expected an error, but did not seem to get one")
}
// Adds tests with the currently configured issue/role information
addTests := func(testCheck logicaltest.TestCheckFunc) {
//fmt.Printf("role vals: %#v\n", roleVals)
//fmt.Printf("issue vals: %#v\n", issueTestStep)
@ -232,6 +243,8 @@ func generateRoleSteps(t *testing.T) []logicaltest.TestStep {
ret = append(ret, issueTestStep)
}
// Returns a TestCheckFunc that performs various validity checks on the
// returned certificate information, mostly within checkCertsAndPrivateKey
getCnCheck := func(name, keyType string, usage certUsage, validity time.Duration) logicaltest.TestCheckFunc {
var certBundle certutil.CertBundle
return func(resp *logical.Response) error {
@ -239,10 +252,11 @@ func generateRoleSteps(t *testing.T) []logicaltest.TestStep {
if err != nil {
return err
}
cert, _, _, err := checkCertsAndPrivateKey(keyType, usage, validity, &certBundle)
parsedCertBundle, err := checkCertsAndPrivateKey(keyType, usage, validity, &certBundle)
if err != nil {
return fmt.Errorf("Error checking generated certificate: %s", err)
}
cert := parsedCertBundle.Certificate
if cert.Subject.CommonName != name {
return fmt.Errorf("Error: returned certificate has CN of %s but %s was requested", cert.Subject.CommonName, name)
}
@ -256,6 +270,7 @@ func generateRoleSteps(t *testing.T) []logicaltest.TestStep {
}
}
// Common names to test with the various role flags toggled
var commonNames struct {
Localhost bool `structs:"localhost"`
BaseDomain bool `structs:"foo.example.com"`
@ -265,6 +280,11 @@ func generateRoleSteps(t *testing.T) []logicaltest.TestStep {
AnyHost bool `structs:"porkslap.beer"`
}
// Adds a series of tests based on the current selection of
// allowed common names; contains some (seeded) randomness
//
// This allows for a variety of common names to be tested in various
// combinations with allowed toggles of the role
addCnTests := func() {
cnMap := structs.New(commonNames).Map()
// For the number of tests being run, this is known to hit all

View File

@ -38,51 +38,39 @@ type certCreationBundle struct {
Usage certUsage
}
func getCertBundle(s logical.Storage, path string) (*certutil.CertBundle, error) {
bundle, err := s.Get(path)
// Fetches the CA info. Unlike other certificates, the CA info is stored
// in the backend as a CertBundle, because we are storing its private key
func fetchCAInfo(req *logical.Request) (*certutil.ParsedCertBundle, error) {
bundleEntry, err := req.Storage.Get("config/ca_bundle")
if err != nil {
return nil, err
return nil, certutil.InternalError{Err: fmt.Sprintf("Unable to fetch local CA certificate/key: %s", err)}
}
if bundle == nil {
return nil, nil
if bundleEntry == nil {
return nil, certutil.UserError{Err: fmt.Sprintf("Backend must be configured with a CA certificate/key")}
}
var result certutil.CertBundle
if err := bundle.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
func fetchCAInfo(req *logical.Request) (*certutil.ParsedCertBundle, *x509.Certificate, error, error) {
bundle, err := getCertBundle(req.Storage, "config/ca_bundle")
if err != nil {
return nil, nil, nil, fmt.Errorf("Unable to fetch local CA certificate/key: %s", err)
}
if bundle == nil {
return nil, nil, fmt.Errorf("Backend must be configured with a CA certificate/key"), nil
var bundle certutil.CertBundle
if err := bundleEntry.DecodeJSON(&bundle); err != nil {
return nil, certutil.InternalError{Err: fmt.Sprintf("Unable to decode local CA certificate/key: %s", err)}
}
parsedBundle, err := bundle.ToParsedCertBundle()
if err != nil {
return nil, nil, nil, err
return nil, certutil.InternalError{Err: err.Error()}
}
certificates, err := x509.ParseCertificates(parsedBundle.CertificateBytes)
switch {
case err != nil:
return nil, nil, nil, err
case len(certificates) != 1:
return nil, nil, nil, fmt.Errorf("Length of CA certificate bundle is wrong")
if parsedBundle.Certificate == nil {
return nil, certutil.InternalError{Err: "Stored CA information not able to be parsed"}
}
return parsedBundle, certificates[0], nil, nil
return parsedBundle, nil
}
func fetchCertBySerial(req *logical.Request, prefix, serial string) (certEntry *logical.StorageEntry, userError, internalError error) {
// Allows fetching certificates from the backend; it handles the slightly
// separate pathing for CA, CRL, and revoked certificates.
func fetchCertBySerial(req *logical.Request, prefix, serial string) (*logical.StorageEntry, error) {
var path string
var err error
switch {
case serial == "ca":
path = "ca"
@ -94,20 +82,22 @@ func fetchCertBySerial(req *logical.Request, prefix, serial string) (certEntry *
path = "certs/" + strings.Replace(strings.ToLower(serial), "-", ":", -1)
}
certEntry, err = req.Storage.Get(path)
certEntry, err := req.Storage.Get(path)
if err != nil || certEntry == nil {
return nil, fmt.Errorf("Certificate with serial number %s not found (if it has been revoked, the revoked/ endpoint must be used)", serial), nil
return nil, certutil.InternalError{Err: fmt.Sprintf("Certificate with serial number %s not found", serial)}
}
if len(certEntry.Value) == 0 {
return nil, nil, fmt.Errorf("Returned certificate bytes for serial %s were empty", serial)
if certEntry.Value == nil || len(certEntry.Value) == 0 {
return nil, certutil.InternalError{Err: fmt.Sprintf("Returned certificate bytes for serial %s were empty", serial)}
}
return
return certEntry, nil
}
// Given a set of requested names for a certificate, verifies that all of them
// match the various toggles set in the role for controlling issuance.
// If one does not pass, it is returned in the string argument.
func validateCommonNames(req *logical.Request, commonNames []string, role *roleEntry) (string, error) {
// TODO: handle wildcards
hostnameRegex, err := regexp.Compile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`)
if err != nil {
return "", fmt.Errorf("Error compiling hostname regex: %s", err)
@ -168,28 +158,30 @@ func validateCommonNames(req *logical.Request, commonNames []string, role *roleE
return "", nil
}
func createCertificate(creationInfo *certCreationBundle) (parsedBundle *certutil.ParsedCertBundle, userErr, intErr error) {
// Performs the heavy lifting of creating a certificate. Returns
// a fully-filled-in ParsedCertBundle.
func createCertificate(creationInfo *certCreationBundle) (*certutil.ParsedCertBundle, error) {
var clientPrivKey crypto.Signer
var err error
parsedBundle = &certutil.ParsedCertBundle{}
result := &certutil.ParsedCertBundle{}
var serialNumber *big.Int
serialNumber, err = rand.Int(rand.Reader, (&big.Int{}).Exp(big.NewInt(2), big.NewInt(159), nil))
if err != nil {
return nil, nil, fmt.Errorf("Error getting random serial number")
return nil, certutil.InternalError{Err: fmt.Sprintf("Error getting random serial number")}
}
switch creationInfo.KeyType {
case "rsa":
parsedBundle.PrivateKeyType = certutil.RSAPrivateKey
result.PrivateKeyType = certutil.RSAPrivateKey
clientPrivKey, err = rsa.GenerateKey(rand.Reader, creationInfo.KeyBits)
if err != nil {
return nil, nil, fmt.Errorf("Error generating RSA private key")
return nil, certutil.InternalError{Err: fmt.Sprintf("Error generating RSA private key")}
}
parsedBundle.PrivateKey = clientPrivKey
parsedBundle.PrivateKeyBytes = x509.MarshalPKCS1PrivateKey(clientPrivKey.(*rsa.PrivateKey))
result.PrivateKey = clientPrivKey
result.PrivateKeyBytes = x509.MarshalPKCS1PrivateKey(clientPrivKey.(*rsa.PrivateKey))
case "ec":
parsedBundle.PrivateKeyType = certutil.ECPrivateKey
result.PrivateKeyType = certutil.ECPrivateKey
var curve elliptic.Curve
switch creationInfo.KeyBits {
case 224:
@ -201,24 +193,24 @@ func createCertificate(creationInfo *certCreationBundle) (parsedBundle *certutil
case 521:
curve = elliptic.P521()
default:
return nil, fmt.Errorf("Unsupported bit length for EC key: %d", creationInfo.KeyBits), nil
return nil, certutil.UserError{Err: fmt.Sprintf("Unsupported bit length for EC key: %d", creationInfo.KeyBits)}
}
clientPrivKey, err = ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
return nil, nil, fmt.Errorf("Error generating EC private key")
return nil, certutil.InternalError{Err: fmt.Sprintf("Error generating EC private key")}
}
parsedBundle.PrivateKey = clientPrivKey
parsedBundle.PrivateKeyBytes, err = x509.MarshalECPrivateKey(clientPrivKey.(*ecdsa.PrivateKey))
result.PrivateKey = clientPrivKey
result.PrivateKeyBytes, err = x509.MarshalECPrivateKey(clientPrivKey.(*ecdsa.PrivateKey))
if err != nil {
return nil, nil, fmt.Errorf("Error marshalling EC private key")
return nil, certutil.InternalError{Err: fmt.Sprintf("Error marshalling EC private key")}
}
default:
return nil, fmt.Errorf("Unknown key type: %s", creationInfo.KeyType), nil
return nil, certutil.UserError{Err: fmt.Sprintf("Unknown key type: %s", creationInfo.KeyType)}
}
subjKeyID, err := certutil.GetSubjKeyID(parsedBundle.PrivateKey)
subjKeyID, err := certutil.GetSubjKeyID(result.PrivateKey)
if err != nil {
return nil, nil, fmt.Errorf("Error getting subject key ID: %s", err)
return nil, certutil.InternalError{Err: fmt.Sprintf("Error getting subject key ID: %s", err)}
}
subject := pkix.Name{
@ -262,17 +254,17 @@ func createCertificate(creationInfo *certCreationBundle) (parsedBundle *certutil
cert, err := x509.CreateCertificate(rand.Reader, certTemplate, creationInfo.CACert, clientPrivKey.Public(), creationInfo.SigningBundle.PrivateKey)
if err != nil {
return nil, nil, fmt.Errorf("Unable to create certificate: %s", err)
return nil, certutil.InternalError{Err: fmt.Sprintf("Unable to create certificate: %s", err)}
}
parsedBundle.CertificateBytes = cert
parsedBundle.Certificate, err = x509.ParseCertificate(cert)
result.CertificateBytes = cert
result.Certificate, err = x509.ParseCertificate(cert)
if err != nil {
return nil, nil, fmt.Errorf("Unable to parse created certificate: %s", err)
return nil, certutil.InternalError{Err: fmt.Sprintf("Unable to parse created certificate: %s", err)}
}
parsedBundle.IssuingCABytes = creationInfo.SigningBundle.CertificateBytes
parsedBundle.IssuingCA = creationInfo.SigningBundle.Certificate
result.IssuingCABytes = creationInfo.SigningBundle.CertificateBytes
result.IssuingCA = creationInfo.SigningBundle.Certificate
return
return result, nil
}

View File

@ -5,9 +5,9 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"sync"
"time"
"github.com/hashicorp/vault/helper/certutil"
"github.com/hashicorp/vault/logical"
)
@ -16,46 +16,45 @@ type revocationInfo struct {
RevocationTime int64 `json:"revocation_time"`
}
var (
crlLifetime = time.Hour * 72
revokeStorageLock = &sync.Mutex{}
)
func revokeCert(req *logical.Request, serial string) (*logical.Response, error) {
// Revokes a cert, and tries to be smart about error recovery
func revokeCert(b *backend, req *logical.Request, serial string) (*logical.Response, error) {
alreadyRevoked := false
var err error
var revInfo revocationInfo
revInfo := revocationInfo{}
certEntry, userErr, intErr := fetchCertBySerial(req, "revoked/", serial)
certEntry, err := fetchCertBySerial(req, "revoked/", serial)
// Don't check error because it's expected that it may fail here;
// just check for existence
if certEntry != nil {
// Verify that it is also deleted from certs/
// in case of partial failure from an earlier run.
certEntry, _, _ = fetchCertBySerial(req, "certs/", serial)
if certEntry != nil {
alreadyRevoked = true
revEntry, err := req.Storage.Get("revoked/" + serial)
if err != nil {
return nil, fmt.Errorf("Error getting existing revocation info")
}
err = revEntry.DecodeJSON(&revInfo)
if err != nil {
return nil, fmt.Errorf("Error decoding existing revocation info")
}
} else {
certEntry, _ = fetchCertBySerial(req, "certs/", serial)
if certEntry == nil {
// Everything seems sane, so don't rebuild the CRL
return nil, nil
}
// Still exists in certs/; set the revocation info, below it will
// be removed from certs/ and the CRL rotated
alreadyRevoked = true
revEntry, err := req.Storage.Get("revoked/" + serial)
if revEntry == nil || err != nil {
return nil, fmt.Errorf("Error getting existing revocation info")
}
err = revEntry.DecodeJSON(&revInfo)
if err != nil {
return nil, fmt.Errorf("Error decoding existing revocation info")
}
}
if !alreadyRevoked {
certEntry, userErr, intErr = fetchCertBySerial(req, "certs/", serial)
switch {
case userErr != nil:
return logical.ErrorResponse(userErr.Error()), nil
case intErr != nil:
return nil, intErr
certEntry, err = fetchCertBySerial(req, "certs/", serial)
switch err.(type) {
case certutil.UserError:
return logical.ErrorResponse(err.Error()), nil
case certutil.InternalError:
return nil, err
}
cert, err := x509.ParseCertificate(certEntry.Value)
@ -85,12 +84,12 @@ func revokeCert(req *logical.Request, serial string) (*logical.Response, error)
}
userErr, intErr = buildCRL(req)
switch {
case userErr != nil:
return logical.ErrorResponse(fmt.Sprintf("Error during CRL building: %s", userErr)), nil
case intErr != nil:
return nil, fmt.Errorf("Error encountered during CRL building: %s", intErr)
crlErr := buildCRL(b, req)
switch crlErr.(type) {
case certutil.UserError:
return logical.ErrorResponse(fmt.Sprintf("Error during CRL building: %s", crlErr)), nil
case certutil.InternalError:
return nil, fmt.Errorf("Error encountered during CRL building: %s", crlErr)
}
err = req.Storage.Delete("certs/" + serial)
@ -106,10 +105,15 @@ func revokeCert(req *logical.Request, serial string) (*logical.Response, error)
}, nil
}
func buildCRL(req *logical.Request) (error, error) {
// Builds a CRL by going through the list of revoked certificates and building
// a new CRL with the stored revocation times and serial numbers.
//
// If a certificate has already expired, it will be removed entirely rather than
// become part of the new CRL.
func buildCRL(b *backend, req *logical.Request) error {
revokedSerials, err := req.Storage.List("revoked/")
if err != nil {
return nil, fmt.Errorf("Error fetching list of revoked certs: %s", err)
return certutil.InternalError{Err: fmt.Sprintf("Error fetching list of revoked certs: %s", err)}
}
revokedCerts := []pkix.RevokedCertificate{}
@ -117,32 +121,32 @@ func buildCRL(req *logical.Request) (error, error) {
for _, serial := range revokedSerials {
revokedEntry, err := req.Storage.Get("revoked/" + serial)
if err != nil {
return nil, fmt.Errorf("Unable to fetch revoked cert with serial %s: %s", serial, err)
return certutil.InternalError{Err: fmt.Sprintf("Unable to fetch revoked cert with serial %s: %s", serial, err)}
}
if revokedEntry == nil {
return nil, fmt.Errorf("Revoked certificate entry for serial %s is nil", serial)
return certutil.InternalError{Err: fmt.Sprintf("Revoked certificate entry for serial %s is nil", serial)}
}
if revokedEntry.Value == nil || len(revokedEntry.Value) == 0 {
// TODO: In this case, remove it and continue? How likely is this to
// happen? Alternately, could skip it entirely, or could implement a
// delete function so that there is a way to remove these
return nil, fmt.Errorf("Found revoked serial but actual certificate is empty")
return certutil.InternalError{Err: fmt.Sprintf("Found revoked serial but actual certificate is empty")}
}
err = revokedEntry.DecodeJSON(&revInfo)
if err != nil {
return nil, fmt.Errorf("Error decoding revocation entry for serial %s: %s", serial, err)
return certutil.InternalError{Err: fmt.Sprintf("Error decoding revocation entry for serial %s: %s", serial, err)}
}
revokedCert, err := x509.ParseCertificate(revInfo.CertificateBytes)
if err != nil {
return nil, fmt.Errorf("Unable to parse stored revoked certificate with serial %s: %s", serial, err)
return certutil.InternalError{Err: fmt.Sprintf("Unable to parse stored revoked certificate with serial %s: %s", serial, err)}
}
if revokedCert.NotAfter.Before(time.Now()) {
err = req.Storage.Delete(serial)
if err != nil {
return nil, fmt.Errorf("Unable to delete revoked, expired certificate with serial %s: %s", serial, err)
return certutil.InternalError{Err: fmt.Sprintf("Unable to delete revoked, expired certificate with serial %s: %s", serial, err)}
}
continue
}
@ -153,18 +157,30 @@ func buildCRL(req *logical.Request) (error, error) {
})
}
signingBundle, caCert, userErr, intErr := fetchCAInfo(req)
switch {
case userErr != nil:
return fmt.Errorf("Could not fetch the CA certificate: %s", userErr), nil
case intErr != nil:
return nil, fmt.Errorf("Error fetching CA certificate: %s", intErr)
signingBundle, caErr := fetchCAInfo(req)
switch caErr.(type) {
case certutil.UserError:
return certutil.UserError{Err: fmt.Sprintf("Could not fetch the CA certificate: %s", caErr)}
case certutil.InternalError:
return certutil.InternalError{Err: fmt.Sprintf("Error fetching CA certificate: %s", caErr)}
}
// TODO: Make expiry configurable
crlBytes, err := caCert.CreateCRL(rand.Reader, signingBundle.PrivateKey, revokedCerts, time.Now(), time.Now().Add(crlLifetime))
crlLifetime := b.crlLifetime
crlInfo, err := b.CRL(req.Storage)
if err != nil {
return nil, fmt.Errorf("Error creating new CRL: %s", err)
return certutil.InternalError{Err: fmt.Sprintf("Error fetching CRL config information: %s", err)}
}
if crlInfo != nil {
crlDur, err := time.ParseDuration(crlInfo.Expiry)
if err != nil {
return certutil.InternalError{Err: fmt.Sprintf("Error parsing CRL duration of %s", crlInfo.Expiry)}
}
crlLifetime = crlDur
}
crlBytes, err := signingBundle.Certificate.CreateCRL(rand.Reader, signingBundle.PrivateKey, revokedCerts, time.Now(), time.Now().Add(crlLifetime))
if err != nil {
return certutil.InternalError{Err: fmt.Sprintf("Error creating new CRL: %s", err)}
}
err = req.Storage.Put(&logical.StorageEntry{
@ -172,8 +188,8 @@ func buildCRL(req *logical.Request) (error, error) {
Value: crlBytes,
})
if err != nil {
return nil, fmt.Errorf("Error storing CRL: %s", err)
return certutil.InternalError{Err: fmt.Sprintf("Error storing CRL: %s", err)}
}
return nil, nil
return nil
}

View File

@ -0,0 +1,102 @@
package pki
import (
"fmt"
"time"
"github.com/fatih/structs"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
// CRLConfig holds basic CRL configuration information
type crlConfig struct {
Expiry string `json:"expiry" mapstructure:"expiry" structs:"expiry"`
}
func pathConfigCRL(b *backend) *framework.Path {
return &framework.Path{
Pattern: "config/crl",
Fields: map[string]*framework.FieldSchema{
"expiry": &framework.FieldSchema{
Type: framework.TypeString,
Description: `The amount of time the generated CRL should be
valid; defaults to 72 hours`,
Default: "72h",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathCRLRead,
logical.WriteOperation: b.pathCRLWrite,
},
HelpSynopsis: pathConfigCRLHelpSyn,
HelpDescription: pathConfigCRLHelpDesc,
}
}
func (b *backend) CRL(s logical.Storage) (*crlConfig, error) {
entry, err := s.Get("config/crl")
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result crlConfig
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
func (b *backend) pathCRLRead(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
config, err := b.CRL(req.Storage)
if err != nil {
return nil, err
}
if config == nil {
return nil, nil
}
return &logical.Response{
Data: structs.New(config).Map(),
}, nil
}
func (b *backend) pathCRLWrite(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
expiry := d.Get("expiry").(string)
_, err := time.ParseDuration(expiry)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("Given expiry could not be decoded: %s", err)), nil
}
config := &crlConfig{
Expiry: expiry,
}
entry, err := logical.StorageEntryJSON("config/crl", config)
if err != nil {
return nil, err
}
err = req.Storage.Put(entry)
if err != nil {
return nil, err
}
return nil, nil
}
const pathConfigCRLHelpSyn = `
Configure the CRL expiration.
`
const pathConfigCRLHelpDesc = `
This endpoint allows configuration of the CRL lifetime.
`

View File

@ -3,12 +3,13 @@ package pki
import (
"encoding/pem"
"fmt"
"strings"
"github.com/hashicorp/vault/helper/certutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
// Returns the CA in raw format
func pathFetchCA(b *backend) *framework.Path {
return &framework.Path{
Pattern: `ca(/pem)?`,
@ -22,6 +23,7 @@ func pathFetchCA(b *backend) *framework.Path {
}
}
// Returns the CRL in raw format
func pathFetchCRL(b *backend) *framework.Path {
return &framework.Path{
Pattern: `crl(/pem)?`,
@ -35,6 +37,8 @@ func pathFetchCRL(b *backend) *framework.Path {
}
}
// Returns any valid (non-revoked) cert. Since "ca" fits the pattern, this path
// also handles returning the CA cert in a non-raw format.
func pathFetchValid(b *backend) *framework.Path {
return &framework.Path{
Pattern: `cert/(?P<serial>[0-9A-Fa-f-:]+)`,
@ -55,6 +59,7 @@ hyphen-separated octal`,
}
}
// This returns the CRL in a non-raw format
func pathFetchCRLViaCertPath(b *backend) *framework.Path {
return &framework.Path{
Pattern: `cert/crl`,
@ -68,36 +73,22 @@ func pathFetchCRLViaCertPath(b *backend) *framework.Path {
}
}
func pathFetchRevoked(b *backend) *framework.Path {
return &framework.Path{
Pattern: `revoked/(?P<serial>[0-9A-Fa-f-:]+)`,
Fields: map[string]*framework.FieldSchema{
"serial": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Certificate serial number, in colon- or hyphen-separated octal",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathFetchRead,
},
HelpSynopsis: pathFetchHelpSyn,
HelpDescription: pathFetchHelpDesc,
}
}
func (b *backend) pathFetchRead(req *logical.Request, data *framework.FieldData) (response *logical.Response, retErr error) {
var serial string
var pemType string
var contentType string
var certEntry *logical.StorageEntry
var userErr, intErr error
var funcErr error
var certificate []byte
response = &logical.Response{
Data: map[string]interface{}{},
}
// Some of these need to return raw and some non-raw;
// this is basically handled by setting contentType or not.
// Errors don't cause an immediate exit, because the raw
// paths still need to return raw output.
switch {
case req.Path == "ca" || req.Path == "ca/pem":
serial = "ca"
@ -123,39 +114,27 @@ func (b *backend) pathFetchRead(req *logical.Request, data *framework.FieldData)
goto reply
}
_, _, userErr, intErr = fetchCAInfo(req)
switch {
case userErr != nil:
response = logical.ErrorResponse(fmt.Sprintf("%s", userErr))
_, funcErr = fetchCAInfo(req)
switch funcErr.(type) {
case certutil.UserError:
response = logical.ErrorResponse(fmt.Sprintf("%s", funcErr))
goto reply
case intErr != nil:
retErr = intErr
case certutil.InternalError:
retErr = funcErr
goto reply
}
certEntry, userErr, intErr = fetchCertBySerial(req, req.Path, serial)
switch {
case userErr != nil:
response = logical.ErrorResponse(userErr.Error())
certEntry, funcErr = fetchCertBySerial(req, req.Path, serial)
switch funcErr.(type) {
case certutil.UserError:
response = logical.ErrorResponse(funcErr.Error())
goto reply
case intErr != nil:
retErr = intErr
case certutil.InternalError:
retErr = funcErr
goto reply
}
switch {
case strings.HasPrefix(req.Path, "revoked/"):
var revInfo revocationInfo
err := certEntry.DecodeJSON(&revInfo)
if err != nil {
retErr = fmt.Errorf("Error decoding revocation entry for serial %s: %s", serial, err)
goto reply
}
certificate = revInfo.CertificateBytes
response.Data["revocation_time"] = revInfo.RevocationTime
default:
certificate = certEntry.Value
}
certificate = certEntry.Value
if len(pemType) != 0 {
block := pem.Block{
@ -188,11 +167,11 @@ reply:
}
const pathFetchHelpSyn = `
Fetch a CA, CRL, valid or revoked certificate.
Fetch a CA, CRL, or non-revoked certificate.
`
const pathFetchHelpDesc = `
This allows certificates to be fetched. If using the fetch/ prefix any valid certificate can be fetched; if using the revoked/ prefix, which requires a root token, revoked certificates can also be fetched.
This allows certificates to be fetched. If using the fetch/ prefix any non-revoked certificate can be fetched.
Using "ca" or "crl" as the value fetches the appropriate information in DER encoding. Add "/pem" to either to get PEM encoding.
`

View File

@ -7,6 +7,7 @@ import (
"time"
"github.com/fatih/structs"
"github.com/hashicorp/vault/helper/certutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
@ -123,15 +124,15 @@ func (b *backend) pathIssueCert(
return nil, fmt.Errorf("Error validating name %s: %s", badName, err)
}
signingBundle, caCert, userErr, intErr := fetchCAInfo(req)
switch {
case userErr != nil:
return logical.ErrorResponse(fmt.Sprintf("Could not fetch the CA certificate: %s", userErr)), nil
case intErr != nil:
return nil, fmt.Errorf("Error fetching CA certificate: %s", intErr)
signingBundle, caErr := fetchCAInfo(req)
switch caErr.(type) {
case certutil.UserError:
return logical.ErrorResponse(fmt.Sprintf("Could not fetch the CA certificate: %s", caErr)), nil
case certutil.InternalError:
return nil, fmt.Errorf("Error fetching CA certificate: %s", caErr)
}
if time.Now().Add(lease).After(caCert.NotAfter) {
if time.Now().Add(lease).After(signingBundle.Certificate.NotAfter) {
return logical.ErrorResponse(fmt.Sprintf("Cannot satisfy request, as maximum lease is beyond the expiration of the CA certificate")), nil
}
@ -148,7 +149,7 @@ func (b *backend) pathIssueCert(
creationBundle := &certCreationBundle{
SigningBundle: signingBundle,
CACert: caCert,
CACert: signingBundle.Certificate,
CommonNames: commonNames,
IPSANs: ipSANs,
KeyType: role.KeyType,
@ -157,12 +158,12 @@ func (b *backend) pathIssueCert(
Usage: usage,
}
parsedBundle, userErr, intErr := createCertificate(creationBundle)
switch {
case userErr != nil:
return logical.ErrorResponse(userErr.Error()), nil
case intErr != nil:
return nil, intErr
parsedBundle, err := createCertificate(creationBundle)
switch err.(type) {
case certutil.UserError:
return logical.ErrorResponse(err.Error()), nil
case certutil.InternalError:
return nil, err
}
cb, err := parsedBundle.ToCertBundle()

View File

@ -3,6 +3,7 @@ package pki
import (
"fmt"
"github.com/hashicorp/vault/helper/certutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
@ -46,29 +47,29 @@ func (b *backend) pathRevokeWrite(req *logical.Request, data *framework.FieldDat
return logical.ErrorResponse("The serial number must be provided"), nil
}
revokeStorageLock.Lock()
defer revokeStorageLock.Unlock()
b.revokeStorageLock.Lock()
defer b.revokeStorageLock.Unlock()
return revokeCert(req, serial)
return revokeCert(b, req, serial)
}
func (b *backend) pathRotateCRLRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
revokeStorageLock.Lock()
defer revokeStorageLock.Unlock()
b.revokeStorageLock.Lock()
defer b.revokeStorageLock.Unlock()
userErr, intErr := buildCRL(req)
switch {
case userErr != nil:
return logical.ErrorResponse(fmt.Sprintf("Error during CRL building: %s", userErr)), nil
case intErr != nil:
return nil, fmt.Errorf("Error encountered during CRL building: %s", intErr)
crlErr := buildCRL(b, req)
switch crlErr.(type) {
case certutil.UserError:
return logical.ErrorResponse(fmt.Sprintf("Error during CRL building: %s", crlErr)), nil
case certutil.InternalError:
return nil, fmt.Errorf("Error encountered during CRL building: %s", crlErr)
default:
return &logical.Response{
Data: map[string]interface{}{
"success": true,
},
}, nil
}
return &logical.Response{
Data: map[string]interface{}{
"success": true,
},
}, nil
}
const pathRevokeHelpSyn = `

View File

@ -52,8 +52,8 @@ func (b *backend) secretCredsRevoke(
serial := strings.Replace(strings.ToLower(serialInt.(string)), "-", ":", -1)
revokeStorageLock.Lock()
defer revokeStorageLock.Unlock()
b.revokeStorageLock.Lock()
defer b.revokeStorageLock.Unlock()
return revokeCert(req, serial)
return revokeCert(b, req, serial)
}

View File

@ -41,21 +41,21 @@ const (
// UserError represents an error generated due to invalid user input
type UserError struct {
s string
Err string
}
func (e UserError) Error() string {
return e.s
return e.Err
}
// InternalError represents an error generated internally,
// presumably not due to invalid user input
type InternalError struct {
s string
Err string
}
func (e InternalError) Error() string {
return e.s
return e.Err
}
// CertBundle contains a key type, a PEM-encoded private key,

View File

@ -443,44 +443,6 @@ If you get stuck at any time, simply run `vault help pki` or with a subpath for
</dd>
</dl>
### /pki/revoked/
#### GET
<dl class="api">
<dt>Description</dt>
<dd>
Retrieves a revoked certificate and its revocation time. The serial
number must be in either hyphen-separated or colon-separated octal format.
<br /><br />This is a root-protected endpoint.
</dd>
<dt>Method</dt>
<dd>GET</dd>
<dt>URL</dt>
<dd>`/pki/revoked/<serial>`</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"data": {
"revocation_time": 1433269787,
"certificate": "-----BEGIN CERTIFICATE-----\nMIIGmDCCBYCgAwIBAgIHBzEB3fTzhTANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UE\n..."
}
}
...
```
</dd>
</dl>
### /pki/roles/
#### POST
@ -665,7 +627,8 @@ If you get stuck at any time, simply run `vault help pki` or with a subpath for
<dl class="api">
<dt>Description</dt>
<dd>
Deletes the role definition.
Deletes the role definition. Deleting a role does <b>not</b> revoke
certificates previously issued under this role.
</dd>
<dt>Method</dt>