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:
parent
54fccb2ff4
commit
7c5a174493
|
@ -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)
|
||||
|
|
|
@ -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 = `
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue