Add capability to use the CSR's common name (by default for CA CSRs if

no common_name parameter is given, role-controlled for non-CA CSRs).

Fix logic around the CA/CRL endpoints. Now settable when generating a
self-signed root or setting a CA cert into the backend; if not set,
these values are not set in issued certs. Not required when signing an
intermediate cert (and in fact it was wrong to do so in the first
place).
This commit is contained in:
Jeff Mitchell 2015-10-09 13:45:17 -04:00
parent 54fccb2ff4
commit 7c5a174493
4 changed files with 149 additions and 89 deletions

View file

@ -38,17 +38,26 @@ type certCreationBundle struct {
DNSNames []string
EmailAddresses []string
IPAddresses []net.IP
PKIAddress string
IsCA bool
KeyType string
KeyBits int
SigningBundle *certutil.ParsedCertBundle
SigningBundle *caInfoBundle
TTL time.Duration
Usage certUsage
// This is only used when generating a self-signed root;
// otherwise the address in the caInfoBundle is used, if set
PKIAddress string
}
type caInfoBundle struct {
certutil.ParsedCertBundle
PKIAddress string
}
// 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) {
func fetchCAInfo(req *logical.Request) (*caInfoBundle, error) {
bundleEntry, err := req.Storage.Get("config/ca_bundle")
if err != nil {
return nil, certutil.InternalError{Err: fmt.Sprintf("Unable to fetch local CA certificate/key: %s", err)}
@ -71,7 +80,17 @@ func fetchCAInfo(req *logical.Request) (*certutil.ParsedCertBundle, error) {
return nil, certutil.InternalError{Err: "Stored CA information not able to be parsed"}
}
return parsedBundle, nil
caInfo := &caInfoBundle{*parsedBundle, ""}
pkiAddress, err := req.Storage.Get("config/pki_address")
if err != nil {
return nil, certutil.InternalError{Err: fmt.Sprintf("Unable to fetch local PKI Address: %s", err)}
}
if pkiAddress != nil {
caInfo.PKIAddress = string(pkiAddress.Value)
}
return caInfo, nil
}
// Allows fetching certificates from the backend; it handles the slightly
@ -222,16 +241,22 @@ func validateNames(req *logical.Request, names []string, role *roleEntry) (strin
func generateCert(b *backend,
role *roleEntry,
signingBundle *certutil.ParsedCertBundle,
signingBundle *caInfoBundle,
isCA bool,
pkiAddress string,
req *logical.Request,
data *framework.FieldData) (*certutil.ParsedCertBundle, error) {
creationBundle, err := generateCreationBundle(b, role, signingBundle, pkiAddress, req, data)
creationBundle, err := generateCreationBundle(b, role, signingBundle, nil, req, data)
if err != nil {
return nil, err
}
if isCA {
creationBundle.IsCA = isCA
creationBundle.PKIAddress = pkiAddress
}
parsedBundle, err := createCertificate(creationBundle)
if err != nil {
return nil, err
@ -244,7 +269,7 @@ func generateCert(b *backend,
// It skips some sanity checks.
func generateCSR(b *backend,
role *roleEntry,
signingBundle *certutil.ParsedCertBundle,
signingBundle *caInfoBundle,
req *logical.Request,
data *framework.FieldData) (*certutil.ParsedCSRBundle, error) {
@ -266,8 +291,8 @@ func generateCSR(b *backend,
func signCert(b *backend,
role *roleEntry,
signingBundle *certutil.ParsedCertBundle,
pkiAddress string,
signingBundle *caInfoBundle,
isCA bool,
req *logical.Request,
data *framework.FieldData) (*certutil.ParsedCertBundle, error) {
@ -287,11 +312,13 @@ func signCert(b *backend,
return nil, certutil.UserError{Err: "certificate request could not be parsed"}
}
creationBundle, err := generateCreationBundle(b, role, signingBundle, pkiAddress, req, data)
creationBundle, err := generateCreationBundle(b, role, signingBundle, csr, req, data)
if err != nil {
return nil, err
}
creationBundle.IsCA = isCA
parsedBundle, err := signCertificate(creationBundle, csr)
if err != nil {
return nil, err
@ -302,8 +329,8 @@ func signCert(b *backend,
func generateCreationBundle(b *backend,
role *roleEntry,
signingBundle *certutil.ParsedCertBundle,
pkiAddress string,
signingBundle *caInfoBundle,
csr *x509.CertificateRequest,
req *logical.Request,
data *framework.FieldData) (*certCreationBundle, error) {
var err error
@ -311,7 +338,15 @@ func generateCreationBundle(b *backend,
// Get the common name(s)
cn := data.Get("common_name").(string)
if len(cn) == 0 {
return nil, certutil.UserError{Err: "The common_name field is required"}
if csr != nil {
if role.UseCSRCommonName {
cn = csr.Subject.CommonName
} else {
return nil, certutil.UserError{Err: `The common_name field must be supplied when "use_csr_common_name" is not specified in the role`}
}
} else {
return nil, certutil.UserError{Err: "The common_name field is required"}
}
}
dnsNames := []string{}
@ -442,7 +477,6 @@ func generateCreationBundle(b *backend,
IPAddresses: ipAddresses,
KeyType: role.KeyType,
KeyBits: role.KeyBits,
PKIAddress: pkiAddress,
SigningBundle: signingBundle,
TTL: ttl,
Usage: usage,
@ -554,20 +588,30 @@ func createCertificate(creationInfo *certCreationBundle) (*certutil.ParsedCertBu
subject.Province = caCert.Subject.Province
subject.StreetAddress = caCert.Subject.StreetAddress
subject.PostalCode = caCert.Subject.PostalCode
certTemplate.CRLDistributionPoints = caCert.CRLDistributionPoints
if creationInfo.SigningBundle.PKIAddress != "" {
certTemplate.CRLDistributionPoints = []string{
creationInfo.SigningBundle.PKIAddress + "/crl",
}
certTemplate.IssuingCertificateURL = []string{
creationInfo.SigningBundle.PKIAddress + "/ca",
}
}
certBytes, err = x509.CreateCertificate(rand.Reader, certTemplate, caCert, clientPrivKey.Public(), creationInfo.SigningBundle.PrivateKey)
} else {
// Creating a self-signed root
switch creationInfo.KeyType {
case "rsa":
certTemplate.SignatureAlgorithm = x509.SHA256WithRSA
case "ec":
certTemplate.SignatureAlgorithm = x509.ECDSAWithSHA256
}
certTemplate.CRLDistributionPoints = []string{
creationInfo.PKIAddress + "/crl",
}
certTemplate.IssuingCertificateURL = []string{
creationInfo.PKIAddress + "/ca",
if creationInfo.PKIAddress != "" {
certTemplate.CRLDistributionPoints = []string{
creationInfo.PKIAddress + "/crl",
}
certTemplate.IssuingCertificateURL = []string{
creationInfo.PKIAddress + "/ca",
}
}
certTemplate.IsCA = true
certTemplate.KeyUsage = x509.KeyUsage(certTemplate.KeyUsage | x509.KeyUsageCertSign | x509.KeyUsageCRLSign)
@ -752,17 +796,19 @@ func signCertificate(creationInfo *certCreationBundle,
subject.StreetAddress = caCert.Subject.StreetAddress
subject.PostalCode = caCert.Subject.PostalCode
certTemplate.IssuingCertificateURL = caCert.IssuingCertificateURL
if creationInfo.PKIAddress != "" {
if creationInfo.SigningBundle.PKIAddress != "" {
certTemplate.CRLDistributionPoints = []string{
creationInfo.PKIAddress + "/crl",
creationInfo.SigningBundle.PKIAddress + "/crl",
}
certTemplate.IssuingCertificateURL = []string{
creationInfo.SigningBundle.PKIAddress + "/ca",
}
}
if creationInfo.IsCA {
certTemplate.IsCA = true
certTemplate.KeyUsage = x509.KeyUsage(certTemplate.KeyUsage | x509.KeyUsageCertSign | x509.KeyUsageCRLSign)
certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, x509.ExtKeyUsageOCSPSigning)
} else {
certTemplate.CRLDistributionPoints = caCert.CRLDistributionPoints
}
certBytes, err = x509.CreateCertificate(rand.Reader, certTemplate, caCert, csr.PublicKey, creationInfo.SigningBundle.PrivateKey)

View file

@ -16,8 +16,10 @@ 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`,
one, specify the alternative names in the alt_names
map. If not specified when signing, the common
name will be taken from the CSR; other names
must still be specified in alt_names or ip_sans.`,
},
"alt_names": &framework.FieldSchema{
@ -107,6 +109,19 @@ secret key and certificate, or, if a
CSR was generated with the "generate"
endpoint, just the signed certificate.`,
},
"pki_address": &framework.FieldSchema{
Type: framework.TypeString,
Description: `The base URL of the PKI mount, e.g.
"https://vault.example.com/v1/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. If empty, no CA/CRL
information will be encoded into
certificates.`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
@ -140,7 +155,9 @@ 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.`,
the certificate. If empty, no CA/CRL
information will be encoded into
certificates.`,
}
for k, v := range generateSchema {
@ -188,18 +205,6 @@ func pathSignIntermediateCA(b *backend) *framework.Path {
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.`,
@ -237,17 +242,11 @@ func (b *backend) pathCAGenerateRoot(
pkiAddress := strings.ToLower(data.Get("pki_address").(string))
switch {
case len(pkiAddress) == 0:
return logical.ErrorResponse(
`"pki_address" cannot be empty`,
), nil
break
case !strings.HasPrefix(pkiAddress, "http"):
return logical.ErrorResponse(
`"pki_address" must be a URL`,
), nil
case !strings.Contains(pkiAddress, "/v1/"):
return logical.ErrorResponse(
`"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(
`"pki_address" needs to be the path to this mount"`,
@ -290,7 +289,7 @@ func (b *backend) pathCAGenerateRoot(
}
var resp *logical.Response
parsedBundle, err := generateCert(b, role, nil, pkiAddress, req, data)
parsedBundle, err := generateCert(b, role, nil, true, pkiAddress, req, data)
if err != nil {
switch err.(type) {
case certutil.UserError:
@ -338,8 +337,15 @@ func (b *backend) pathCAGenerateRoot(
return nil, err
}
entry.Key = "config/pki_address"
entry.Value = []byte(pkiAddress)
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
// location, plus a fresh CRL
entry.Key = "ca"
entry.Value = parsedBundle.CertificateBytes
err = req.Storage.Put(entry)
@ -347,9 +353,7 @@ func (b *backend) pathCAGenerateRoot(
return nil, err
}
entry.Key = "crl"
entry.Value = []byte{}
err = req.Storage.Put(entry)
err = buildCRL(b, req)
if err != nil {
return nil, err
}
@ -480,29 +484,6 @@ func (b *backend) pathCASignIntermediate(
), nil
}
pkiAddress := strings.ToLower(data.Get("pki_address").(string))
switch {
case len(pkiAddress) == 0:
return logical.ErrorResponse(
`"pki_address" cannot be empty`,
), nil
case !strings.HasPrefix(pkiAddress, "http"):
return logical.ErrorResponse(
`"pki_address" must be a URL`,
), nil
case !strings.Contains(pkiAddress, "/v1/"):
return logical.ErrorResponse(
`"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(
`"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,
@ -510,6 +491,10 @@ func (b *backend) pathCASignIntermediate(
EnforceHostnames: false,
}
if cn := data.Get("common_name").(string); len(cn) == 0 {
role.UseCSRCommonName = true
}
var caErr error
signingBundle, caErr := fetchCAInfo(req)
switch caErr.(type) {
@ -521,7 +506,7 @@ func (b *backend) pathCASignIntermediate(
"error fetching CA certificate: %s", caErr)}
}
parsedBundle, err := signCert(b, role, signingBundle, pkiAddress, req, data)
parsedBundle, err := signCert(b, role, signingBundle, true, req, data)
if err != nil {
switch err.(type) {
case certutil.UserError:
@ -568,8 +553,8 @@ func (b *backend) pathCASignIntermediate(
}
func (b *backend) pathCASetWrite(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
pemBundle := d.Get("pem_bundle").(string)
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
pemBundle := data.Get("pem_bundle").(string)
parsedBundle, err := certutil.ParsePEMBundle(pemBundle)
if err != nil {
@ -587,6 +572,23 @@ func (b *backend) pathCASetWrite(
parsedBundle.CertificateBytes = parsedBundle.IssuingCABytes
}
pkiAddress := strings.ToLower(data.Get("pki_address").(string))
switch {
case len(pkiAddress) == 0:
break
case !strings.HasPrefix(pkiAddress, "http"):
return logical.ErrorResponse(
`"pki_address" must be a URL`,
), nil
case !strings.Contains(pkiAddress, "/v1/"+req.MountPoint[:len(req.MountPoint)-1]):
return logical.ErrorResponse(
`"pki_address" needs to be the path to this mount"`,
), nil
}
if strings.HasSuffix(pkiAddress, "/") {
pkiAddress = pkiAddress[:len(pkiAddress)-1]
}
cb := &certutil.CertBundle{}
entry, err := req.Storage.Get("config/ca_bundle")
if err != nil {
@ -646,8 +648,15 @@ func (b *backend) pathCASetWrite(
return nil, err
}
entry.Key = "config/pki_address"
entry.Value = []byte(pkiAddress)
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
// location, plus a fresh CRL
entry.Key = "ca"
entry.Value = parsedBundle.CertificateBytes
err = req.Storage.Put(entry)
@ -655,14 +664,9 @@ func (b *backend) pathCASetWrite(
return nil, err
}
entry.Key = "crl"
entry.Value = []byte{}
err = req.Storage.Put(entry)
if err != nil {
return nil, err
}
err = buildCRL(b, req)
return nil, nil
return nil, err
}
const pathConfigCASetHelpSyn = `

View file

@ -120,9 +120,9 @@ func (b *backend) pathIssueSignCert(
var parsedBundle *certutil.ParsedCertBundle
if useCSR {
parsedBundle, err = signCert(b, role, signingBundle, "", req, data)
parsedBundle, err = signCert(b, role, signingBundle, false, req, data)
} else {
parsedBundle, err = generateCert(b, role, signingBundle, "", req, data)
parsedBundle, err = generateCert(b, role, signingBundle, false, "", req, data)
}
if err != nil {
switch err.(type) {

View file

@ -139,6 +139,14 @@ and "ec" are the only valid values.`,
certainly want to change this if you adjust
the key_type.`,
},
"use_csr_common_name": &framework.FieldSchema{
Type: framework.TypeBool,
Description: `If set, when used with a signing profile,
the common name in the CSR will be used. This
does *not* include any requested Subject Alternative
Names.`,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
@ -253,6 +261,7 @@ func (b *backend) pathRoleCreate(
EmailProtectionFlag: data.Get("email_protection_flag").(bool),
KeyType: data.Get("key_type").(string),
KeyBits: data.Get("key_bits").(int),
UseCSRCommonName: data.Get("use_csr_common_name").(bool),
}
var maxTTL time.Duration
@ -342,6 +351,7 @@ type roleEntry struct {
ClientFlag bool `json:"client_flag" structs:"client_flag" mapstructure:"client_flag"`
CodeSigningFlag bool `json:"code_signing_flag" structs:"code_signing_flag" mapstructure:"code_signing_flag"`
EmailProtectionFlag bool `json:"email_protection_flag" structs:"email_protection_flag" mapstructure:"email_protection_flag"`
UseCSRCommonName bool `json:"use_csr_common_name" structs:"use_csr_common_name" mapstructure:"use_csr_common_name"`
KeyType string `json:"key_type" structs:"key_type" mapstructure:"key_type"`
KeyBits int `json:"key_bits" structs:"key_bits" mapstructure:"key_bits"`
}