open-vault/builtin/logical/pki/path_config_ca.go
2015-11-19 09:51:17 -05:00

596 lines
16 KiB
Go

package pki
import (
"fmt"
"reflect"
"strings"
"time"
"github.com/fatih/structs"
"github.com/hashicorp/vault/helper/certutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
var rootAndSignSchema = map[string]*framework.FieldSchema{
"common_name": &framework.FieldSchema{
Type: framework.TypeString,
Description: `The requested common name; if you want more than
one, specify the alternative names in the
alt_names map`,
},
"alt_names": &framework.FieldSchema{
Type: framework.TypeString,
Description: `The requested Subject Alternative Names, if any,
in a comma-delimited list`,
},
"ip_sans": &framework.FieldSchema{
Type: framework.TypeString,
Description: `The requested IP SANs, if any, in a
common-delimited list`,
},
"ttl": &framework.FieldSchema{
Type: framework.TypeString,
Description: `The requested Time To Live for the certificate;
sets the expiration date. If not specified
the role default, backend default, or system
default TTL is used, in that order. Cannot
be larger than the mount max TTL. Note:
this only has an effect when generating
a CA cert or signing a CA cert, not when
creating a CSR for an intermediate CA.`,
},
}
var generateSchema = map[string]*framework.FieldSchema{
"exported": &framework.FieldSchema{
Type: framework.TypeString,
Description: `Must be "internal" or "exported".
If set to "exported", the generated private
key will be returned. This is your *only*
chance to retrieve the private key!`,
},
"key_bits": &framework.FieldSchema{
Type: framework.TypeInt,
Default: 2048,
Description: `The number of bits to use for the private key.`,
},
}
func pathConfigCA(b *backend) *framework.Path {
return &framework.Path{
Pattern: "config/ca",
Fields: map[string]*framework.FieldSchema{
"pem_bundle": &framework.FieldSchema{
Type: framework.TypeString,
Description: `DEPRECATED: use "config/ca/set" instead.`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.WriteOperation: b.pathCASetWrite,
},
HelpSynopsis: pathConfigCASetHelpSyn,
HelpDescription: pathConfigCASetHelpDesc,
}
}
func pathSetCA(b *backend) *framework.Path {
return &framework.Path{
Pattern: "config/ca/set",
Fields: map[string]*framework.FieldSchema{
"pem_bundle": &framework.FieldSchema{
Type: framework.TypeString,
Description: `PEM-format, concatenated unencrypted
secret key and certificate, or, if a
CSR was generated with the "generate"
endpoint, just the signed certificate.`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.WriteOperation: b.pathCASetWrite,
},
HelpSynopsis: pathConfigCASetHelpSyn,
HelpDescription: pathConfigCASetHelpDesc,
}
}
func pathGenerateRootCA(b *backend) *framework.Path {
ret := &framework.Path{
Pattern: "config/ca/generate/root/" + framework.GenericNameRegex("exported"),
Fields: rootAndSignSchema,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.WriteOperation: b.pathCAGenerateRoot,
},
HelpSynopsis: pathConfigCAGenerateHelpSyn,
HelpDescription: pathConfigCAGenerateHelpDesc,
}
ret.Fields["pki_address"] = &framework.FieldSchema{
Type: framework.TypeString,
Description: `The base URL of the PKI mount, e.g.
"https://vault.example.com/v1/root_pki".
For HA setups, the given host name
should be the address that can always
be used to contact the leader, as this is
is used for generating the CA/CRL URLs in
the certificate.`,
}
for k, v := range generateSchema {
ret.Fields[k] = v
}
return ret
}
func pathGenerateIntermediateCA(b *backend) *framework.Path {
ret := &framework.Path{
Pattern: "config/ca/generate/intermediate/" + framework.GenericNameRegex("exported"),
Fields: generateSchema,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.WriteOperation: b.pathCAGenerateIntermediate,
},
HelpSynopsis: pathConfigCAGenerateHelpSyn,
HelpDescription: pathConfigCAGenerateHelpDesc,
}
return ret
}
func pathSignIntermediateCA(b *backend) *framework.Path {
ret := &framework.Path{
Pattern: "config/ca/sign",
Fields: rootAndSignSchema,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.WriteOperation: b.pathCASignWrite,
},
HelpSynopsis: pathConfigCASignHelpSyn,
HelpDescription: pathConfigCASignHelpDesc,
}
ret.Fields["pki_address"] = &framework.FieldSchema{
Type: framework.TypeString,
Description: `The base URL of the *destination*
PKI mount, e.g.
"https://vault.example.com/v1/intermediate_pki".
For HA setups, the given host name
should be the address that can always
be used to contact the leader, as this is
is used for generating the CA/CRL URLs in
the certificate.`,
}
ret.Fields["csr"] = &framework.FieldSchema{
Type: framework.TypeString,
Description: `PEM-format CSR to be signed.`,
}
return ret
}
func (b *backend) pathCAGenerateRoot(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
var err error
exported := data.Get("exported").(string)
switch exported {
case "exported":
case "internal":
default:
return logical.ErrorResponse(fmt.Sprintf(
"The \"exported\" path parameter must be \"internal\" or \"exported\"")), nil
}
pkiAddress := strings.ToLower(data.Get("pki_address").(string))
switch {
case len(pkiAddress) == 0:
return logical.ErrorResponse(fmt.Sprintf(
"\"pki_address\" cannot be empty")), nil
case !strings.HasPrefix(pkiAddress, "http"):
return logical.ErrorResponse(fmt.Sprintf(
"\"pki_address\" must be a URL")), nil
case !strings.Contains(pkiAddress, "/v1/"):
return logical.ErrorResponse(fmt.Sprintf(
"\"pki_address\" needs to be the path to the PKI mount, not the base Vault path")), nil
case !strings.Contains(pkiAddress, "/v1/"+req.MountPoint[:len(req.MountPoint)-1]):
return logical.ErrorResponse(fmt.Sprintf(
"\"pki_address\" needs to be the path to this mount")), nil
}
if strings.HasSuffix(pkiAddress, "/") {
pkiAddress = pkiAddress[:len(pkiAddress)-1]
}
role := &roleEntry{
TTL: data.Get("ttl").(string),
KeyType: "rsa",
KeyBits: data.Get("key_bits").(int),
AllowLocalhost: true,
AllowAnyName: true,
EnforceHostnames: false,
}
switch role.KeyBits {
case 1024:
case 2048:
case 4096:
default:
return logical.ErrorResponse(fmt.Sprintf(
"\"key_bits\" must be 1024, 2048, or 4096")), nil
}
var resp *logical.Response
parsedBundle, err := generateCert(b, role, nil, pkiAddress, req, data)
if err != nil {
switch err.(type) {
case certutil.UserError:
return logical.ErrorResponse(err.Error()), nil
case certutil.InternalError:
return nil, err
}
}
cb, err := parsedBundle.ToCertBundle()
if err != nil {
return nil, fmt.Errorf("Error converting raw cert bundle to cert bundle: %s", err)
}
resp = &logical.Response{
Data: map[string]interface{}{
"serial_number": cb.SerialNumber,
"certificate": cb.Certificate,
"issuing_ca": cb.IssuingCA,
"expiration": int64(parsedBundle.Certificate.NotAfter.Unix()),
},
}
if exported == "exported" {
resp.Data["private_key"] = cb.PrivateKey
resp.Data["private_key_type"] = cb.PrivateKeyType
}
entry, err := logical.StorageEntryJSON("config/ca_bundle", cb)
if err != nil {
return nil, err
}
err = req.Storage.Put(entry)
if err != nil {
return nil, err
}
// For ease of later use, also store just the certificate at a known
// location, plus a blank CRL
entry.Key = "ca"
entry.Value = parsedBundle.CertificateBytes
err = req.Storage.Put(entry)
if err != nil {
return nil, err
}
entry.Key = "crl"
entry.Value = []byte{}
err = req.Storage.Put(entry)
if err != nil {
return nil, err
}
return resp, nil
}
func (b *backend) pathCAGenerateIntermediate(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
var err error
exported := data.Get("exported").(string)
switch exported {
case "exported":
case "internal":
default:
return logical.ErrorResponse(fmt.Sprintf(
"The \"exported\" path parameter must be \"internal\" or \"exported\"")), nil
}
role := &roleEntry{
KeyType: "rsa",
KeyBits: data.Get("key_bits").(int),
AllowLocalhost: true,
AllowAnyName: true,
EnforceHostnames: false,
}
switch role.KeyBits {
case 0:
role.KeyBits = 2048
case 1024:
case 2048:
case 4096:
default:
return logical.ErrorResponse(fmt.Sprintf(
"\"key_bits\" must be 1024, 2048, or 4096")), nil
}
var resp *logical.Response
parsedBundle, err := generateCSR(b, role, nil, req, data)
if err != nil {
switch err.(type) {
case certutil.UserError:
return logical.ErrorResponse(err.Error()), nil
case certutil.InternalError:
return nil, err
}
}
csrb, err := parsedBundle.ToCSRBundle()
if err != nil {
return nil, fmt.Errorf("Error converting raw CSR bundle to CSR bundle: %s", err)
}
resp = &logical.Response{
Data: map[string]interface{}{
"csr": csrb.CSR,
},
}
if exported == "exported" {
resp.Data["private_key"] = csrb.PrivateKey
resp.Data["private_key_type"] = csrb.PrivateKeyType
}
cb := &certutil.CertBundle{
PrivateKey: csrb.PrivateKey,
PrivateKeyType: csrb.PrivateKeyType,
}
entry, err := logical.StorageEntryJSON("config/ca_bundle", cb)
if err != nil {
return nil, err
}
err = req.Storage.Put(entry)
if err != nil {
return nil, err
}
return resp, nil
}
func (b *backend) pathCASignWrite(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
var err error
pkiAddress := strings.ToLower(data.Get("pki_address").(string))
switch {
case len(pkiAddress) == 0:
return logical.ErrorResponse(fmt.Sprintf(
"\"pki_address\" cannot be empty")), nil
case !strings.HasPrefix(pkiAddress, "http"):
return logical.ErrorResponse(fmt.Sprintf(
"\"pki_address\" must be a URL")), nil
case !strings.Contains(pkiAddress, "/v1/"):
return logical.ErrorResponse(fmt.Sprintf(
"\"pki_address\" needs to be the path to the PKI mount, not the base Vault path")), nil
case strings.Contains(pkiAddress, "/v1/"+req.MountPoint):
return logical.ErrorResponse(fmt.Sprintf(
"\"pki_address\" needs to be the path to the destination mount, not the signing mount")), nil
}
if strings.HasSuffix(pkiAddress, "/") {
pkiAddress = pkiAddress[:len(pkiAddress)-1]
}
role := &roleEntry{
TTL: data.Get("ttl").(string),
AllowLocalhost: true,
AllowAnyName: true,
EnforceHostnames: false,
}
var caErr error
signingBundle, caErr := fetchCAInfo(req)
switch caErr.(type) {
case certutil.UserError:
return nil, certutil.UserError{Err: fmt.Sprintf(
"could not fetch the CA certificate (was one set?): %s", caErr)}
case certutil.InternalError:
return nil, certutil.InternalError{Err: fmt.Sprintf(
"error fetching CA certificate: %s", caErr)}
}
parsedBundle, err := signCert(b, role, signingBundle, pkiAddress, req, data)
if err != nil {
switch err.(type) {
case certutil.UserError:
return logical.ErrorResponse(err.Error()), nil
case certutil.InternalError:
return nil, err
}
}
cb, err := parsedBundle.ToCertBundle()
if err != nil {
return nil, fmt.Errorf("Error converting raw cert bundle to cert bundle: %s", err)
}
resp := b.Secret(SecretCertsType).Response(
structs.New(cb).Map(),
map[string]interface{}{
"serial_number": cb.SerialNumber,
})
resp.Secret.TTL = parsedBundle.Certificate.NotAfter.Sub(time.Now())
err = req.Storage.Put(&logical.StorageEntry{
Key: "certs/" + cb.SerialNumber,
Value: parsedBundle.CertificateBytes,
})
if err != nil {
return nil, fmt.Errorf("Unable to store certificate locally")
}
return resp, nil
}
func (b *backend) pathCASetWrite(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
pemBundle := d.Get("pem_bundle").(string)
parsedBundle, err := certutil.ParsePEMBundle(pemBundle)
if err != nil {
switch err.(type) {
case certutil.InternalError:
return nil, err
default:
return logical.ErrorResponse(err.Error()), nil
}
}
// Handle the case of a self-signed certificate
if parsedBundle.Certificate == nil && parsedBundle.IssuingCA != nil {
parsedBundle.Certificate = parsedBundle.IssuingCA
parsedBundle.CertificateBytes = parsedBundle.IssuingCABytes
}
cb := &certutil.CertBundle{}
entry, err := req.Storage.Get("config/ca_bundle")
if err != nil {
return nil, err
}
if entry != nil {
err = entry.DecodeJSON(cb)
if err != nil {
return nil, err
}
// If we have a stored private key and did not get one, attempt to
// correlate the two -- this could be due to a CSR being signed
// for a generated CA cert and the resulting cert now being uploaded
if len(cb.PrivateKey) != 0 &&
cb.PrivateKeyType != "" &&
parsedBundle.PrivateKeyType == certutil.UnknownPrivateKey &&
(parsedBundle.PrivateKeyBytes == nil || len(parsedBundle.PrivateKeyBytes) == 0) {
parsedCB, err := cb.ToParsedCertBundle()
if err != nil {
return nil, err
}
if parsedCB.PrivateKey == nil {
return nil, fmt.Errorf("Encountered nil private key from saved key")
}
// If true, the stored private key corresponds to the cert's
// public key, so fill it in
//panic(fmt.Sprintf("\nparsedCB.PrivateKey.Public().: %#v\nparsedBundle.Certificate.PublicKey"))
if reflect.DeepEqual(parsedCB.PrivateKey.Public(), parsedBundle.Certificate.PublicKey) {
parsedBundle.PrivateKey = parsedCB.PrivateKey
parsedBundle.PrivateKeyType = parsedCB.PrivateKeyType
parsedBundle.PrivateKeyBytes = parsedCB.PrivateKeyBytes
}
}
}
if parsedBundle.PrivateKey == nil ||
parsedBundle.PrivateKeyBytes == nil ||
len(parsedBundle.PrivateKeyBytes) == 0 {
return logical.ErrorResponse("No private key given and no matching key stored"), nil
}
// TODO?: CRLs can only be generated with RSA keys right now, in the
// Go standard library. The plubming is here to support non-RSA keys
// if the library gets support
if parsedBundle.PrivateKeyType != certutil.RSAPrivateKey {
return logical.ErrorResponse("Currently, only RSA keys are supported for the CA certificate"), nil
}
if !parsedBundle.Certificate.IsCA {
return logical.ErrorResponse("The given certificate is not marked for CA use and cannot be used with this backend"), nil
}
cb, err = parsedBundle.ToCertBundle()
if err != nil {
return nil, fmt.Errorf("Error converting raw values into cert bundle: %s", err)
}
entry, err = logical.StorageEntryJSON("config/ca_bundle", cb)
if err != nil {
return nil, err
}
err = req.Storage.Put(entry)
if err != nil {
return nil, err
}
// For ease of later use, also store just the certificate at a known
// location, plus a blank CRL
entry.Key = "ca"
entry.Value = parsedBundle.CertificateBytes
err = req.Storage.Put(entry)
if err != nil {
return nil, err
}
entry.Key = "crl"
entry.Value = []byte{}
err = req.Storage.Put(entry)
if err != nil {
return nil, err
}
return nil, nil
}
const pathConfigCASetHelpSyn = `
Set the CA certificate and private key used for generated credentials.
`
const pathConfigCASetHelpDesc = `
This sets the CA information used for credentials generated by this
by this mount. This must be a PEM-format, concatenated unencrypted
secret key and certificate.
For security reasons, the secret key cannot be retrieved later.
`
const pathConfigCAGenerateHelpSyn = `
Generate a new CA certificate and private key used for signing.
`
const pathConfigCAGenerateHelpDesc = `
This path generates a CA certificate and private key to be used for
credentials generated by this mount. The path can either
end in "internal" or "exported"; this controls whether the
unencrypted private key is exported after generation. This will
be your only chance to export the private key; for security reasons
it cannot be read or exported later.
If the "type" option is set to "self-signed", the generated
certificate will be a self-signed root CA. Otherwise, this mount
will act as an intermediate CA; a CSR will be returned, to be signed
by your chosen CA (which could be another mount of this backend).
Note that the CRL path will be set to this mount's CRL path; if you
need further customization it is recommended that you create a CSR
separately and get it signed. Either way, use the "config/ca/set"
endpoint to load the signed certificate into Vault.
`
const pathConfigCASignHelpSyn = `
Generate a signed CA certificate from a CSR.
`
const pathConfigCASignHelpDesc = `
This path generates a CA certificate to be used for credentials
generated by the certificate's destination mount.
Use the "config/ca/set" endpoint to load the signed certificate
into Vault another Vault mount.
`