9338c22c53
* Rename files to match test suite and existing pattern * Factor out issuer loading into a dedicated function - Add a little more checks/validation when loading the a PKI issuer - Factor out the issuer loading into a dedicated function - Leverage existing health check code to parse issuer certificates * Read parent issuer once instead of reloading it for every child - Read in our parent issuer once instead of running it for every child we want to compare against - Provides clearer error message that we have failed reading from which path to the end user * PR Feedback - Rename a variable for clarity - Use readIssuer in the validation of the parent issuer within pkiIssuer - Add some missing return 1 statements in error handlers that had been missed
291 lines
8.1 KiB
Go
291 lines
8.1 KiB
Go
package command
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/posener/complete"
|
|
)
|
|
|
|
type PKIReIssueCACommand struct {
|
|
*BaseCommand
|
|
|
|
flagConfig string
|
|
flagReturnIndicator string
|
|
flagDefaultDisabled bool
|
|
flagList bool
|
|
|
|
flagKeyStorageSource string
|
|
flagNewIssuerName string
|
|
}
|
|
|
|
func (c *PKIReIssueCACommand) Synopsis() string {
|
|
return "Uses a parent certificate and a template certificate to create a new issuer on a child mount"
|
|
}
|
|
|
|
func (c *PKIReIssueCACommand) Help() string {
|
|
helpText := `
|
|
Usage: vault pki reissue PARENT TEMPLATE CHILD_MOUNT options
|
|
`
|
|
return strings.TrimSpace(helpText)
|
|
}
|
|
|
|
func (c *PKIReIssueCACommand) Flags() *FlagSets {
|
|
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
|
|
f := set.NewFlagSet("Command Options")
|
|
|
|
f.StringVar(&StringVar{
|
|
Name: "type",
|
|
Target: &c.flagKeyStorageSource,
|
|
Default: "internal",
|
|
EnvVar: "",
|
|
Usage: `Options are “existing” - to use an existing key inside vault, “internal” - to generate a new key inside vault, or “kms” - to link to an external key. Exported keys are not available through this API.`,
|
|
Completion: complete.PredictSet("internal", "existing", "kms"),
|
|
})
|
|
|
|
f.StringVar(&StringVar{
|
|
Name: "issuer_name",
|
|
Target: &c.flagNewIssuerName,
|
|
Default: "",
|
|
EnvVar: "",
|
|
Usage: `If present, the newly created issuer will be given this name`,
|
|
})
|
|
|
|
return set
|
|
}
|
|
|
|
func (c *PKIReIssueCACommand) Run(args []string) int {
|
|
// Parse Args
|
|
f := c.Flags()
|
|
if err := f.Parse(args); err != nil {
|
|
c.UI.Error(err.Error())
|
|
return 1
|
|
}
|
|
args = f.Args()
|
|
|
|
if len(args) < 3 {
|
|
c.UI.Error("Not enough arguments: expected parent issuer and child-mount location and some key_value argument")
|
|
return 1
|
|
}
|
|
|
|
stdin := (io.Reader)(os.Stdin)
|
|
userData, err := parseArgsData(stdin, args[3:])
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("Failed to parse K=V data: %s", err))
|
|
return 1
|
|
}
|
|
|
|
// Check We Have a Client
|
|
client, err := c.Client()
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("Failed to obtain client: %v", err))
|
|
return 1
|
|
}
|
|
|
|
parentIssuer := sanitizePath(args[0]) // /pki/issuer/default
|
|
templateIssuer := sanitizePath(args[1])
|
|
intermediateMount := sanitizePath(args[2])
|
|
|
|
templateIssuerBundle, err := readIssuer(client, templateIssuer)
|
|
if err != nil {
|
|
c.UI.Error(fmt.Sprintf("Error fetching template certificate %v : %v", templateIssuer, err))
|
|
return 1
|
|
}
|
|
certificate := templateIssuerBundle.certificate
|
|
|
|
useExistingKey := c.flagKeyStorageSource == "existing"
|
|
keyRef := ""
|
|
if useExistingKey {
|
|
keyRef = templateIssuerBundle.keyId
|
|
|
|
if keyRef == "" {
|
|
c.UI.Error(fmt.Sprintf("Template issuer %s did not have a key id field set in response which is required", templateIssuer))
|
|
return 1
|
|
}
|
|
}
|
|
|
|
templateData, err := parseTemplateCertificate(*certificate, useExistingKey, keyRef)
|
|
data := updateTemplateWithData(templateData, userData)
|
|
|
|
return pkiIssue(c.BaseCommand, parentIssuer, intermediateMount, c.flagNewIssuerName, c.flagKeyStorageSource, data)
|
|
}
|
|
|
|
func updateTemplateWithData(template map[string]interface{}, changes map[string]interface{}) map[string]interface{} {
|
|
data := map[string]interface{}{}
|
|
|
|
for key, value := range template {
|
|
data[key] = value
|
|
}
|
|
|
|
// ttl and not_after set the same thing. Delete template ttl if using not_after:
|
|
if _, ok := changes["not_after"]; ok {
|
|
delete(data, "ttl")
|
|
}
|
|
|
|
// If we are updating the key_type, do not set key_bits
|
|
if _, ok := changes["key_type"]; ok && changes["key_type"] != template["key_type"] {
|
|
delete(data, "key_bits")
|
|
}
|
|
|
|
for key, value := range changes {
|
|
data[key] = value
|
|
}
|
|
|
|
return data
|
|
}
|
|
|
|
func parseTemplateCertificate(certificate x509.Certificate, useExistingKey bool, keyRef string) (templateData map[string]interface{}, err error) {
|
|
// Generate Certificate Signing Parameters
|
|
templateData = map[string]interface{}{
|
|
"common_name": certificate.Subject.CommonName,
|
|
"alt_names": makeAltNamesCommaSeparatedString(certificate.DNSNames, certificate.EmailAddresses),
|
|
"ip_sans": makeIpAddressCommaSeparatedString(certificate.IPAddresses),
|
|
"uri_sans": makeUriCommaSeparatedString(certificate.URIs),
|
|
// other_sans (string: "") - Specifies custom OID/UTF8-string SANs. These must match values specified on the role in allowed_other_sans (see role creation for allowed_other_sans globbing rules). The format is the same as OpenSSL: <oid>;<type>:<value> where the only current valid type is UTF8. This can be a comma-delimited list or a JSON string slice.
|
|
// Punting on Other_SANs, shouldn't really be on CAs
|
|
"signature_bits": findSignatureBits(certificate.SignatureAlgorithm),
|
|
"exclude_cn_from_sans": determineExcludeCnFromSans(certificate),
|
|
"ou": certificate.Subject.OrganizationalUnit,
|
|
"organization": certificate.Subject.Organization,
|
|
"country": certificate.Subject.Country,
|
|
"locality": certificate.Subject.Locality,
|
|
"province": certificate.Subject.Province,
|
|
"street_address": certificate.Subject.StreetAddress,
|
|
"postal_code": certificate.Subject.PostalCode,
|
|
"serial_number": certificate.Subject.SerialNumber,
|
|
"ttl": (certificate.NotAfter.Sub(certificate.NotBefore)).String(),
|
|
"max_path_length": certificate.MaxPathLen,
|
|
"permitted_dns_domains": strings.Join(certificate.PermittedDNSDomains, ","),
|
|
"use_pss": isPSS(certificate.SignatureAlgorithm),
|
|
}
|
|
|
|
if useExistingKey {
|
|
templateData["skid"] = hex.EncodeToString(certificate.SubjectKeyId) // TODO: Double Check this with someone
|
|
if keyRef == "" {
|
|
return nil, fmt.Errorf("unable to create certificate template for existing key without a key_id")
|
|
}
|
|
templateData["key_ref"] = keyRef
|
|
} else {
|
|
templateData["key_type"] = getKeyType(certificate.PublicKeyAlgorithm.String())
|
|
templateData["key_bits"] = findBitLength(certificate.PublicKey)
|
|
}
|
|
|
|
return templateData, nil
|
|
}
|
|
|
|
func isPSS(algorithm x509.SignatureAlgorithm) bool {
|
|
switch algorithm {
|
|
case x509.SHA384WithRSAPSS, x509.SHA512WithRSAPSS, x509.SHA256WithRSAPSS:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func makeAltNamesCommaSeparatedString(names []string, emails []string) string {
|
|
return strings.Join(names, ",") + "," + strings.Join(emails, ",")
|
|
}
|
|
|
|
func makeUriCommaSeparatedString(uris []*url.URL) string {
|
|
stringAddresses := make([]string, len(uris))
|
|
for i, uri := range uris {
|
|
stringAddresses[i] = uri.String()
|
|
}
|
|
return strings.Join(stringAddresses, ",")
|
|
}
|
|
|
|
func makeIpAddressCommaSeparatedString(addresses []net.IP) string {
|
|
stringAddresses := make([]string, len(addresses))
|
|
for i, address := range addresses {
|
|
stringAddresses[i] = address.String()
|
|
}
|
|
return strings.Join(stringAddresses, ",")
|
|
}
|
|
|
|
func determineExcludeCnFromSans(certificate x509.Certificate) bool {
|
|
cn := certificate.Subject.CommonName
|
|
if cn == "" {
|
|
return false
|
|
}
|
|
|
|
emails := certificate.EmailAddresses
|
|
for _, email := range emails {
|
|
if email == cn {
|
|
return false
|
|
}
|
|
}
|
|
|
|
dnses := certificate.DNSNames
|
|
for _, dns := range dnses {
|
|
if dns == cn {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func findBitLength(publicKey any) int {
|
|
if publicKey == nil {
|
|
return 0
|
|
}
|
|
switch pub := publicKey.(type) {
|
|
case *rsa.PublicKey:
|
|
return pub.N.BitLen()
|
|
case *ecdsa.PublicKey:
|
|
switch pub.Curve {
|
|
case elliptic.P224():
|
|
return 224
|
|
case elliptic.P256():
|
|
return 256
|
|
case elliptic.P384():
|
|
return 384
|
|
case elliptic.P521():
|
|
return 521
|
|
default:
|
|
return 0
|
|
}
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func findSignatureBits(algo x509.SignatureAlgorithm) int {
|
|
switch algo {
|
|
case x509.MD2WithRSA, x509.MD5WithRSA, x509.SHA1WithRSA, x509.DSAWithSHA1, x509.ECDSAWithSHA1:
|
|
return -1
|
|
case x509.SHA256WithRSA, x509.DSAWithSHA256, x509.ECDSAWithSHA256, x509.SHA256WithRSAPSS:
|
|
return 256
|
|
case x509.SHA384WithRSA, x509.ECDSAWithSHA384, x509.SHA384WithRSAPSS:
|
|
return 384
|
|
case x509.SHA512WithRSA, x509.SHA512WithRSAPSS, x509.ECDSAWithSHA512:
|
|
return 512
|
|
case x509.PureEd25519:
|
|
return 0
|
|
default:
|
|
return -1
|
|
}
|
|
}
|
|
|
|
func getKeyType(goKeyType string) string {
|
|
switch goKeyType {
|
|
case "RSA":
|
|
return "rsa"
|
|
case "ECDSA":
|
|
return "ec"
|
|
case "Ed25519":
|
|
return "ed25519"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|