open-vault/builtin/logical/pki/cert_util.go

1120 lines
32 KiB
Go
Raw Normal View History

package pki
import (
"bytes"
"crypto/ecdsa"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
2015-09-30 01:48:31 +00:00
"encoding/pem"
"fmt"
"net"
"regexp"
"strings"
"time"
"github.com/hashicorp/vault/helper/certutil"
"github.com/hashicorp/vault/helper/errutil"
"github.com/hashicorp/vault/helper/strutil"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
type certExtKeyUsage int
const (
serverExtKeyUsage certExtKeyUsage = 1 << iota
clientExtKeyUsage
codeSigningExtKeyUsage
emailProtectionExtKeyUsage
)
type creationBundle struct {
CommonName string
OU []string
Organization []string
DNSNames []string
EmailAddresses []string
IPAddresses []net.IP
IsCA bool
KeyType string
KeyBits int
SigningBundle *caInfoBundle
TTL time.Duration
KeyUsage x509.KeyUsage
ExtKeyUsage certExtKeyUsage
// Only used when signing a CA cert
UseCSRValues bool
// URLs to encode into the certificate
URLs *urlEntries
// The maximum path length to encode
MaxPathLength int
}
type caInfoBundle struct {
certutil.ParsedCertBundle
URLs *urlEntries
}
func (b *caInfoBundle) GetCAChain() []*certutil.CertBlock {
chain := []*certutil.CertBlock{}
// Include issuing CA in Chain, not including Root Authority
if (len(b.Certificate.AuthorityKeyId) > 0 &&
!bytes.Equal(b.Certificate.AuthorityKeyId, b.Certificate.SubjectKeyId)) ||
(len(b.Certificate.AuthorityKeyId) == 0 &&
!bytes.Equal(b.Certificate.RawIssuer, b.Certificate.RawSubject)) {
chain = append(chain, &certutil.CertBlock{
Certificate: b.Certificate,
Bytes: b.CertificateBytes,
})
if b.CAChain != nil && len(b.CAChain) > 0 {
chain = append(chain, b.CAChain...)
}
}
return chain
}
var (
hostnameRegex = regexp.MustCompile(`^(([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])$`)
oidExtensionBasicConstraints = []int{2, 5, 29, 19}
)
func oidInExtensions(oid asn1.ObjectIdentifier, extensions []pkix.Extension) bool {
for _, e := range extensions {
if e.Id.Equal(oid) {
return true
}
}
return false
}
func getFormat(data *framework.FieldData) string {
format := data.Get("format").(string)
switch format {
case "pem":
case "der":
case "pem_bundle":
default:
format = ""
}
return format
}
func validateKeyTypeLength(keyType string, keyBits int) *logical.Response {
switch keyType {
case "rsa":
switch keyBits {
case 2048:
case 4096:
case 8192:
default:
return logical.ErrorResponse(fmt.Sprintf(
"unsupported bit length for RSA key: %d", keyBits))
}
case "ec":
switch keyBits {
case 224:
case 256:
case 384:
case 521:
default:
return logical.ErrorResponse(fmt.Sprintf(
"unsupported bit length for EC key: %d", keyBits))
}
default:
return logical.ErrorResponse(fmt.Sprintf(
"unknown key type %s", keyType))
}
return nil
}
// 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) (*caInfoBundle, error) {
bundleEntry, err := req.Storage.Get("config/ca_bundle")
if err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to fetch local CA certificate/key: %v", err)}
}
if bundleEntry == nil {
return nil, errutil.UserError{Err: "backend must be configured with a CA certificate/key"}
}
var bundle certutil.CertBundle
if err := bundleEntry.DecodeJSON(&bundle); err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to decode local CA certificate/key: %v", err)}
}
parsedBundle, err := bundle.ToParsedCertBundle()
if err != nil {
return nil, errutil.InternalError{Err: err.Error()}
}
if parsedBundle.Certificate == nil {
return nil, errutil.InternalError{Err: "stored CA information not able to be parsed"}
}
caInfo := &caInfoBundle{*parsedBundle, nil}
entries, err := getURLs(req)
if err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to fetch URL information: %v", err)}
}
if entries == nil {
entries = &urlEntries{
IssuingCertificates: []string{},
CRLDistributionPoints: []string{},
OCSPServers: []string{},
}
}
caInfo.URLs = entries
return caInfo, nil
}
// 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
var certEntry *logical.StorageEntry
switch {
// Revoked goes first as otherwise ca/crl get hardcoded paths which fail if
// we actually want revocation info
case strings.HasPrefix(prefix, "revoked/"):
path = "revoked/" + strings.Replace(strings.ToLower(serial), ":", "-", -1)
case serial == "ca":
path = "ca"
case serial == "crl":
path = "crl"
default:
path = "certs/" + strings.Replace(strings.ToLower(serial), ":", "-", -1)
}
certEntry, err = req.Storage.Get(path)
if err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("error fetching certificate %s: %s", serial, err)}
}
if certEntry != nil {
if certEntry.Value == nil || len(certEntry.Value) == 0 {
return nil, errutil.InternalError{Err: fmt.Sprintf("returned certificate bytes for serial %s were empty", serial)}
}
return certEntry, nil
}
// No point checking these, no old/new style colons/hyphens
if path == "ca" || path == "crl" {
return nil, nil
}
// Save the desired path
desiredPath := path
// If we get here we need to check for old-style paths using colons
switch {
case strings.HasPrefix(prefix, "revoked/"):
path = "revoked/" + strings.Replace(strings.ToLower(serial), "-", ":", -1)
default:
path = "certs/" + strings.Replace(strings.ToLower(serial), "-", ":", -1)
}
certEntry, err = req.Storage.Get(path)
if err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("error fetching certificate %s: %s", serial, err)}
}
if certEntry == nil {
return nil, nil
}
if certEntry.Value == nil || len(certEntry.Value) == 0 {
return nil, errutil.InternalError{Err: fmt.Sprintf("returned certificate bytes for serial %s were empty", serial)}
}
certEntry.Key = desiredPath
if err = req.Storage.Put(certEntry); err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("error saving certificate with serial %s to new location", serial)}
}
if err = req.Storage.Delete(path); err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("error deleting certificate with serial %s from old location", serial)}
}
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 validateNames(req *logical.Request, names []string, role *roleEntry) string {
for _, name := range names {
sanitizedName := name
emailDomain := name
isEmail := false
isWildcard := false
// If it has an @, assume it is an email address and separate out the
// user from the hostname portion so that we can act on the hostname.
// Note that this matches behavior from the alt_names parameter. If it
// ends up being problematic for users, I guess that could be separated
// into dns_names and email_names in the future to be explicit, but I
// don't think this is likely.
if strings.Contains(name, "@") {
splitEmail := strings.Split(name, "@")
if len(splitEmail) != 2 {
return name
}
sanitizedName = splitEmail[1]
emailDomain = splitEmail[1]
isEmail = true
}
// If we have an asterisk as the first part of the domain name, mark it
// as wildcard and set the sanitized name to the remainder of the
// domain
if strings.HasPrefix(sanitizedName, "*.") {
sanitizedName = sanitizedName[2:]
isWildcard = true
}
// Email addresses using wildcard domain names do not make sense
if isEmail && isWildcard {
return name
}
// AllowAnyName is checked after this because EnforceHostnames still
// applies when allowing any name. Also, we check the sanitized name to
// ensure that we are not either checking a full email address or a
// wildcard prefix.
if role.EnforceHostnames {
if !hostnameRegex.MatchString(sanitizedName) {
return name
}
}
// Self-explanatory
if role.AllowAnyName {
continue
}
// The following blocks all work the same basic way:
// 1) If a role allows a certain class of base (localhost, token
// display name, role-configured domains), perform further tests
//
// 2) If there is a perfect match on either the name itself or it's an
// email address with a perfect match on the hostname portion, allow it
//
// 3) If subdomains are allowed, we check based on the sanitized name;
// note that if not a wildcard, will be equivalent to the email domain
// for email checks, and we already checked above for both a wildcard
// and email address being present in the same name
// 3a) First we check for a non-wildcard subdomain, as in <name>.<base>
// 3b) Then we check if it's a wildcard and the base domain is a match
//
// Variances are noted in-line
if role.AllowLocalhost {
if name == "localhost" ||
name == "localdomain" ||
(isEmail && emailDomain == "localhost") ||
(isEmail && emailDomain == "localdomain") {
continue
}
if role.AllowSubdomains {
// It is possible, if unlikely, to have a subdomain of "localhost"
if strings.HasSuffix(sanitizedName, ".localhost") ||
(isWildcard && sanitizedName == "localhost") {
continue
}
// A subdomain of "localdomain" is also not entirely uncommon
if strings.HasSuffix(sanitizedName, ".localdomain") ||
(isWildcard && sanitizedName == "localdomain") {
continue
}
}
}
if role.AllowTokenDisplayName {
if name == req.DisplayName {
continue
}
if role.AllowSubdomains {
if isEmail {
// If it's an email address, we need to parse the token
// display name in order to do a proper comparison of the
// subdomain
if strings.Contains(req.DisplayName, "@") {
splitDisplay := strings.Split(req.DisplayName, "@")
if len(splitDisplay) == 2 {
// Compare the sanitized name against the hostname
// portion of the email address in the roken
// display name
if strings.HasSuffix(sanitizedName, "."+splitDisplay[1]) {
continue
}
}
}
}
if strings.HasSuffix(sanitizedName, "."+req.DisplayName) ||
(isWildcard && sanitizedName == req.DisplayName) {
continue
}
}
}
if role.AllowedDomains != "" {
valid := false
for _, currDomain := range strings.Split(role.AllowedDomains, ",") {
// If there is, say, a trailing comma, ignore it
if currDomain == "" {
continue
}
// First, allow an exact match of the base domain if that role flag
// is enabled
if role.AllowBareDomains &&
(name == currDomain ||
(isEmail && emailDomain == currDomain)) {
valid = true
break
}
if role.AllowSubdomains {
if strings.HasSuffix(sanitizedName, "."+currDomain) ||
(isWildcard && sanitizedName == currDomain) {
valid = true
break
}
}
}
if valid {
continue
}
}
//panic(fmt.Sprintf("\nName is %s\nRole is\n%#v\n", name, role))
return name
}
return ""
}
func generateCert(b *backend,
role *roleEntry,
signingBundle *caInfoBundle,
isCA bool,
req *logical.Request,
data *framework.FieldData) (*certutil.ParsedCertBundle, error) {
if role.KeyType == "rsa" && role.KeyBits < 2048 {
return nil, errutil.UserError{Err: "RSA keys < 2048 bits are unsafe and not supported"}
}
creationBundle, err := generateCreationBundle(b, role, signingBundle, nil, req, data)
if err != nil {
return nil, err
}
if isCA {
creationBundle.IsCA = isCA
if signingBundle == nil {
// Generating a self-signed root certificate
entries, err := getURLs(req)
if err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to fetch URL information: %v", err)}
}
if entries == nil {
entries = &urlEntries{
IssuingCertificates: []string{},
CRLDistributionPoints: []string{},
OCSPServers: []string{},
}
}
creationBundle.URLs = entries
if role.MaxPathLength == nil {
creationBundle.MaxPathLength = -1
} else {
creationBundle.MaxPathLength = *role.MaxPathLength
}
}
}
parsedBundle, err := createCertificate(creationBundle)
if err != nil {
return nil, err
}
return parsedBundle, nil
}
// N.B.: This is only meant to be used for generating intermediate CAs.
// It skips some sanity checks.
func generateIntermediateCSR(b *backend,
role *roleEntry,
signingBundle *caInfoBundle,
req *logical.Request,
data *framework.FieldData) (*certutil.ParsedCSRBundle, error) {
creationBundle, err := generateCreationBundle(b, role, signingBundle, nil, req, data)
if err != nil {
return nil, err
}
parsedBundle, err := createCSR(creationBundle)
if err != nil {
return nil, err
}
return parsedBundle, nil
}
func signCert(b *backend,
role *roleEntry,
signingBundle *caInfoBundle,
isCA bool,
useCSRValues bool,
req *logical.Request,
data *framework.FieldData) (*certutil.ParsedCertBundle, error) {
csrString := data.Get("csr").(string)
2015-09-30 01:48:31 +00:00
if csrString == "" {
return nil, errutil.UserError{Err: fmt.Sprintf("\"csr\" is empty")}
2015-09-30 01:48:31 +00:00
}
pemBytes := []byte(csrString)
pemBlock, pemBytes := pem.Decode(pemBytes)
if pemBlock == nil {
return nil, errutil.UserError{Err: "csr contains no data"}
2015-09-30 01:48:31 +00:00
}
csr, err := x509.ParseCertificateRequest(pemBlock.Bytes)
if err != nil {
return nil, errutil.UserError{Err: "certificate request could not be parsed"}
2015-09-30 01:48:31 +00:00
}
switch role.KeyType {
case "rsa":
// Verify that the key matches the role type
if csr.PublicKeyAlgorithm != x509.RSA {
return nil, errutil.UserError{Err: fmt.Sprintf(
"role requires keys of type %s",
role.KeyType)}
}
pubKey, ok := csr.PublicKey.(*rsa.PublicKey)
if !ok {
return nil, errutil.UserError{Err: "could not parse CSR's public key"}
}
// Verify that the key is at least 2048 bits
if pubKey.N.BitLen() < 2048 {
return nil, errutil.UserError{Err: "RSA keys < 2048 bits are unsafe and not supported"}
}
// Verify that the bit size is at least the size specified in the role
if pubKey.N.BitLen() < role.KeyBits {
return nil, errutil.UserError{Err: fmt.Sprintf(
"role requires a minimum of a %d-bit key, but CSR's key is %d bits",
role.KeyBits,
pubKey.N.BitLen())}
}
case "ec":
// Verify that the key matches the role type
if csr.PublicKeyAlgorithm != x509.ECDSA {
return nil, errutil.UserError{Err: fmt.Sprintf(
"role requires keys of type %s",
role.KeyType)}
}
pubKey, ok := csr.PublicKey.(*ecdsa.PublicKey)
if !ok {
return nil, errutil.UserError{Err: "could not parse CSR's public key"}
}
// Verify that the bit size is at least the size specified in the role
if pubKey.Params().BitSize < role.KeyBits {
return nil, errutil.UserError{Err: fmt.Sprintf(
"role requires a minimum of a %d-bit key, but CSR's key is %d bits",
role.KeyBits,
pubKey.Params().BitSize)}
}
case "any":
// We only care about running RSA < 2048 bit checks, so if not RSA
// break out
if csr.PublicKeyAlgorithm != x509.RSA {
break
}
// Run RSA < 2048 bit checks
pubKey, ok := csr.PublicKey.(*rsa.PublicKey)
if !ok {
return nil, errutil.UserError{Err: "could not parse CSR's public key"}
}
if pubKey.N.BitLen() < 2048 {
return nil, errutil.UserError{Err: "RSA keys < 2048 bits are unsafe and not supported"}
}
}
creationBundle, err := generateCreationBundle(b, role, signingBundle, csr, req, data)
if err != nil {
return nil, err
}
creationBundle.IsCA = isCA
creationBundle.UseCSRValues = useCSRValues
parsedBundle, err := signCertificate(creationBundle, csr)
if err != nil {
return nil, err
}
return parsedBundle, nil
}
// generateCreationBundle is a shared function that reads parameters supplied
// from the various endpoints and generates a creationBundle with the
// parameters that can be used to issue or sign
func generateCreationBundle(b *backend,
role *roleEntry,
signingBundle *caInfoBundle,
csr *x509.CertificateRequest,
req *logical.Request,
data *framework.FieldData) (*creationBundle, error) {
var err error
var ok bool
// Read in names -- CN, DNS and email addresses
2015-11-16 15:10:03 +00:00
var cn string
dnsNames := []string{}
emailAddresses := []string{}
{
if csr != nil && role.UseCSRCommonName {
cn = csr.Subject.CommonName
2015-11-16 15:10:03 +00:00
}
if cn == "" {
cn = data.Get("common_name").(string)
if cn == "" {
return nil, errutil.UserError{Err: `the common_name field is required, or must be provided in a CSR with "use_csr_common_name" set to true`}
}
}
if csr != nil && role.UseCSRSANs {
dnsNames = csr.DNSNames
emailAddresses = csr.EmailAddresses
}
if !data.Get("exclude_cn_from_sans").(bool) {
if strings.Contains(cn, "@") {
// Note: emails are not disallowed if the role's email protection
// flag is false, because they may well be included for
// informational purposes; it is up to the verifying party to
// ensure that email addresses in a subject alternate name can be
// used for the purpose for which they are presented
emailAddresses = append(emailAddresses, cn)
} else {
dnsNames = append(dnsNames, cn)
}
}
if csr == nil || !role.UseCSRSANs {
cnAltRaw, ok := data.GetOk("alt_names")
if ok {
cnAlt := strutil.ParseDedupAndSortStrings(cnAltRaw.(string), ",")
for _, v := range cnAlt {
if strings.Contains(v, "@") {
emailAddresses = append(emailAddresses, v)
} else {
dnsNames = append(dnsNames, v)
}
}
2015-09-29 23:13:54 +00:00
}
}
// Check the CN. This ensures that the CN is checked even if it's
// excluded from SANs.
badName := validateNames(req, []string{cn}, role)
if len(badName) != 0 {
return nil, errutil.UserError{Err: fmt.Sprintf(
"common name %s not allowed by this role", badName)}
}
// Check for bad email and/or DNS names
badName = validateNames(req, dnsNames, role)
if len(badName) != 0 {
return nil, errutil.UserError{Err: fmt.Sprintf(
"subject alternate name %s not allowed by this role", badName)}
}
badName = validateNames(req, emailAddresses, role)
if len(badName) != 0 {
return nil, errutil.UserError{Err: fmt.Sprintf(
"email address %s not allowed by this role", badName)}
}
}
// Get and verify any IP SANs
ipAddresses := []net.IP{}
var ipAltInt interface{}
{
if csr != nil && role.UseCSRSANs {
if len(csr.IPAddresses) > 0 {
if !role.AllowIPSANs {
return nil, errutil.UserError{Err: fmt.Sprintf(
"IP Subject Alternative Names are not allowed in this role, but was provided some via CSR")}
}
ipAddresses = csr.IPAddresses
}
} else {
ipAltInt, ok = data.GetOk("ip_sans")
if ok {
ipAlt := ipAltInt.(string)
if len(ipAlt) != 0 {
if !role.AllowIPSANs {
return nil, errutil.UserError{Err: fmt.Sprintf(
"IP Subject Alternative Names are not allowed in this role, but was provided %s", ipAlt)}
}
for _, v := range strings.Split(ipAlt, ",") {
parsedIP := net.ParseIP(v)
if parsedIP == nil {
return nil, errutil.UserError{Err: fmt.Sprintf(
"the value '%s' is not a valid IP address", v)}
}
ipAddresses = append(ipAddresses, parsedIP)
}
2015-09-29 23:13:54 +00:00
}
}
}
}
// Set OU (organizationalUnit) values if specified in the role
ou := []string{}
{
if role.OU != "" {
ou = strutil.RemoveDuplicates(strutil.ParseStringSlice(role.OU, ","), false)
}
}
// Set O (organization) values if specified in the role
organization := []string{}
{
if role.Organization != "" {
organization = strutil.RemoveDuplicates(strutil.ParseStringSlice(role.Organization, ","), false)
}
}
// Get the TTL and very it against the max allowed
2015-09-29 23:13:54 +00:00
var ttlField string
var ttl time.Duration
var maxTTL time.Duration
var ttlFieldInt interface{}
{
ttlFieldInt, ok = data.GetOk("ttl")
if !ok {
ttlField = role.TTL
} else {
ttlField = ttlFieldInt.(string)
}
if len(ttlField) == 0 {
ttl = b.System().DefaultLeaseTTL()
} else {
ttl, err = time.ParseDuration(ttlField)
if err != nil {
return nil, errutil.UserError{Err: fmt.Sprintf(
"invalid requested ttl: %s", err)}
}
}
if len(role.MaxTTL) == 0 {
maxTTL = b.System().MaxLeaseTTL()
} else {
maxTTL, err = time.ParseDuration(role.MaxTTL)
if err != nil {
return nil, errutil.UserError{Err: fmt.Sprintf(
"invalid ttl: %s", err)}
}
}
if ttl > maxTTL {
// Don't error if they were using system defaults, only error if
// they specifically chose a bad TTL
if len(ttlField) == 0 {
ttl = maxTTL
} else {
return nil, errutil.UserError{Err: fmt.Sprintf(
"ttl is larger than maximum allowed (%d)", maxTTL/time.Second)}
}
}
// If it's not self-signed, verify that the issued certificate won't be
// valid past the lifetime of the CA certificate
if signingBundle != nil &&
time.Now().Add(ttl).After(signingBundle.Certificate.NotAfter) {
return nil, errutil.UserError{Err: fmt.Sprintf(
"cannot satisfy request, as TTL is beyond the expiration of the CA certificate")}
}
}
// Build up usages
var extUsage certExtKeyUsage
{
if role.ServerFlag {
extUsage = extUsage | serverExtKeyUsage
}
if role.ClientFlag {
extUsage = extUsage | clientExtKeyUsage
}
if role.CodeSigningFlag {
extUsage = extUsage | codeSigningExtKeyUsage
}
if role.EmailProtectionFlag {
extUsage = extUsage | emailProtectionExtKeyUsage
}
}
creationBundle := &creationBundle{
CommonName: cn,
OU: ou,
Organization: organization,
DNSNames: dnsNames,
EmailAddresses: emailAddresses,
IPAddresses: ipAddresses,
KeyType: role.KeyType,
KeyBits: role.KeyBits,
SigningBundle: signingBundle,
TTL: ttl,
2016-06-23 13:49:03 +00:00
KeyUsage: x509.KeyUsage(parseKeyUsages(role.KeyUsage)),
ExtKeyUsage: extUsage,
}
// Don't deal with URLs or max path length if it's self-signed, as these
// normally come from the signing bundle
if signingBundle == nil {
return creationBundle, nil
}
// This will have been read in from the getURLs function
creationBundle.URLs = signingBundle.URLs
// If the max path length in the role is not nil, it was specified at
// generation time with the max_path_length parameter; otherwise derive it
// from the signing certificate
if role.MaxPathLength != nil {
creationBundle.MaxPathLength = *role.MaxPathLength
} else {
switch {
case signingBundle.Certificate.MaxPathLen < 0:
creationBundle.MaxPathLength = -1
case signingBundle.Certificate.MaxPathLen == 0 &&
signingBundle.Certificate.MaxPathLenZero:
// The signing function will ensure that we do not issue a CA cert
creationBundle.MaxPathLength = 0
default:
// If this takes it to zero, we handle this case later if
// necessary
creationBundle.MaxPathLength = signingBundle.Certificate.MaxPathLen - 1
}
}
return creationBundle, nil
}
// addKeyUsages adds approrpiate key usages to the template given the creation
// information
func addKeyUsages(creationInfo *creationBundle, certTemplate *x509.Certificate) {
if creationInfo.IsCA {
certTemplate.KeyUsage = x509.KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign)
return
}
certTemplate.KeyUsage = creationInfo.KeyUsage
if creationInfo.ExtKeyUsage&serverExtKeyUsage != 0 {
certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, x509.ExtKeyUsageServerAuth)
}
if creationInfo.ExtKeyUsage&clientExtKeyUsage != 0 {
certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, x509.ExtKeyUsageClientAuth)
}
if creationInfo.ExtKeyUsage&codeSigningExtKeyUsage != 0 {
certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, x509.ExtKeyUsageCodeSigning)
}
if creationInfo.ExtKeyUsage&emailProtectionExtKeyUsage != 0 {
certTemplate.ExtKeyUsage = append(certTemplate.ExtKeyUsage, x509.ExtKeyUsageEmailProtection)
}
}
// Performs the heavy lifting of creating a certificate. Returns
// a fully-filled-in ParsedCertBundle.
func createCertificate(creationInfo *creationBundle) (*certutil.ParsedCertBundle, error) {
var err error
result := &certutil.ParsedCertBundle{}
serialNumber, err := certutil.GenerateSerialNumber()
if err != nil {
return nil, err
}
if err := certutil.GeneratePrivateKey(creationInfo.KeyType,
creationInfo.KeyBits,
result); err != nil {
return nil, err
}
subjKeyID, err := certutil.GetSubjKeyID(result.PrivateKey)
if err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("error getting subject key ID: %s", err)}
}
subject := pkix.Name{
CommonName: creationInfo.CommonName,
OrganizationalUnit: creationInfo.OU,
Organization: creationInfo.Organization,
}
certTemplate := &x509.Certificate{
SerialNumber: serialNumber,
Subject: subject,
NotBefore: time.Now().Add(-30 * time.Second),
NotAfter: time.Now().Add(creationInfo.TTL),
IsCA: false,
SubjectKeyId: subjKeyID,
DNSNames: creationInfo.DNSNames,
EmailAddresses: creationInfo.EmailAddresses,
IPAddresses: creationInfo.IPAddresses,
}
// Add this before calling addKeyUsages
if creationInfo.SigningBundle == nil {
certTemplate.IsCA = true
}
addKeyUsages(creationInfo, certTemplate)
certTemplate.IssuingCertificateURL = creationInfo.URLs.IssuingCertificates
certTemplate.CRLDistributionPoints = creationInfo.URLs.CRLDistributionPoints
certTemplate.OCSPServer = creationInfo.URLs.OCSPServers
var certBytes []byte
if creationInfo.SigningBundle != nil {
switch creationInfo.SigningBundle.PrivateKeyType {
case certutil.RSAPrivateKey:
certTemplate.SignatureAlgorithm = x509.SHA256WithRSA
case certutil.ECPrivateKey:
certTemplate.SignatureAlgorithm = x509.ECDSAWithSHA256
}
caCert := creationInfo.SigningBundle.Certificate
certBytes, err = x509.CreateCertificate(rand.Reader, certTemplate, caCert, result.PrivateKey.Public(), creationInfo.SigningBundle.PrivateKey)
} else {
// Creating a self-signed root
if creationInfo.MaxPathLength == 0 {
certTemplate.MaxPathLen = 0
certTemplate.MaxPathLenZero = true
} else {
certTemplate.MaxPathLen = creationInfo.MaxPathLength
}
switch creationInfo.KeyType {
case "rsa":
certTemplate.SignatureAlgorithm = x509.SHA256WithRSA
case "ec":
certTemplate.SignatureAlgorithm = x509.ECDSAWithSHA256
}
certTemplate.BasicConstraintsValid = true
certBytes, err = x509.CreateCertificate(rand.Reader, certTemplate, certTemplate, result.PrivateKey.Public(), result.PrivateKey)
}
if err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to create certificate: %s", err)}
}
result.CertificateBytes = certBytes
result.Certificate, err = x509.ParseCertificate(certBytes)
if err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to parse created certificate: %s", err)}
}
if creationInfo.SigningBundle != nil {
if len(creationInfo.SigningBundle.Certificate.AuthorityKeyId) > 0 &&
!bytes.Equal(creationInfo.SigningBundle.Certificate.AuthorityKeyId, creationInfo.SigningBundle.Certificate.SubjectKeyId) {
result.CAChain = []*certutil.CertBlock{
&certutil.CertBlock{
Certificate: creationInfo.SigningBundle.Certificate,
Bytes: creationInfo.SigningBundle.CertificateBytes,
},
}
result.CAChain = append(result.CAChain, creationInfo.SigningBundle.CAChain...)
}
}
return result, nil
}
// Creates a CSR. This is currently only meant for use when
// generating an intermediate certificate.
func createCSR(creationInfo *creationBundle) (*certutil.ParsedCSRBundle, error) {
var err error
result := &certutil.ParsedCSRBundle{}
if err := certutil.GeneratePrivateKey(creationInfo.KeyType,
creationInfo.KeyBits,
result); err != nil {
return nil, err
}
// Like many root CAs, other information is ignored
subject := pkix.Name{
CommonName: creationInfo.CommonName,
}
csrTemplate := &x509.CertificateRequest{
Subject: subject,
DNSNames: creationInfo.DNSNames,
EmailAddresses: creationInfo.EmailAddresses,
IPAddresses: creationInfo.IPAddresses,
}
switch creationInfo.KeyType {
case "rsa":
csrTemplate.SignatureAlgorithm = x509.SHA256WithRSA
case "ec":
csrTemplate.SignatureAlgorithm = x509.ECDSAWithSHA256
}
csr, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, result.PrivateKey)
if err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to create certificate: %s", err)}
}
result.CSRBytes = csr
result.CSR, err = x509.ParseCertificateRequest(csr)
if err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to parse created certificate: %s", err)}
}
return result, nil
}
// Performs the heavy lifting of generating a certificate from a CSR.
// Returns a ParsedCertBundle sans private keys.
func signCertificate(creationInfo *creationBundle,
csr *x509.CertificateRequest) (*certutil.ParsedCertBundle, error) {
switch {
case creationInfo == nil:
return nil, errutil.UserError{Err: "nil creation info given to signCertificate"}
case creationInfo.SigningBundle == nil:
return nil, errutil.UserError{Err: "nil signing bundle given to signCertificate"}
case csr == nil:
return nil, errutil.UserError{Err: "nil csr given to signCertificate"}
}
err := csr.CheckSignature()
if err != nil {
return nil, errutil.UserError{Err: "request signature invalid"}
}
result := &certutil.ParsedCertBundle{}
serialNumber, err := certutil.GenerateSerialNumber()
if err != nil {
return nil, err
}
marshaledKey, err := x509.MarshalPKIXPublicKey(csr.PublicKey)
if err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("error marshalling public key: %s", err)}
}
subjKeyID := sha1.Sum(marshaledKey)
subject := pkix.Name{
CommonName: creationInfo.CommonName,
OrganizationalUnit: creationInfo.OU,
Organization: creationInfo.Organization,
}
certTemplate := &x509.Certificate{
SerialNumber: serialNumber,
Subject: subject,
NotBefore: time.Now().Add(-30 * time.Second),
NotAfter: time.Now().Add(creationInfo.TTL),
SubjectKeyId: subjKeyID[:],
}
switch creationInfo.SigningBundle.PrivateKeyType {
case certutil.RSAPrivateKey:
certTemplate.SignatureAlgorithm = x509.SHA256WithRSA
case certutil.ECPrivateKey:
certTemplate.SignatureAlgorithm = x509.ECDSAWithSHA256
}
if creationInfo.UseCSRValues {
certTemplate.Subject = csr.Subject
certTemplate.DNSNames = csr.DNSNames
certTemplate.EmailAddresses = csr.EmailAddresses
certTemplate.IPAddresses = csr.IPAddresses
certTemplate.ExtraExtensions = csr.Extensions
} else {
certTemplate.DNSNames = creationInfo.DNSNames
certTemplate.EmailAddresses = creationInfo.EmailAddresses
certTemplate.IPAddresses = creationInfo.IPAddresses
}
addKeyUsages(creationInfo, certTemplate)
var certBytes []byte
caCert := creationInfo.SigningBundle.Certificate
certTemplate.IssuingCertificateURL = creationInfo.URLs.IssuingCertificates
certTemplate.CRLDistributionPoints = creationInfo.URLs.CRLDistributionPoints
certTemplate.OCSPServer = creationInfo.SigningBundle.URLs.OCSPServers
if creationInfo.IsCA {
certTemplate.BasicConstraintsValid = true
certTemplate.IsCA = true
if creationInfo.SigningBundle.Certificate.MaxPathLen == 0 &&
creationInfo.SigningBundle.Certificate.MaxPathLenZero {
return nil, errutil.UserError{Err: "signing certificate has a max path length of zero, and cannot issue further CA certificates"}
}
certTemplate.MaxPathLen = creationInfo.MaxPathLength
if certTemplate.MaxPathLen == 0 {
certTemplate.MaxPathLenZero = true
}
}
certBytes, err = x509.CreateCertificate(rand.Reader, certTemplate, caCert, csr.PublicKey, creationInfo.SigningBundle.PrivateKey)
if err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to create certificate: %s", err)}
}
result.CertificateBytes = certBytes
result.Certificate, err = x509.ParseCertificate(certBytes)
if err != nil {
return nil, errutil.InternalError{Err: fmt.Sprintf("unable to parse created certificate: %s", err)}
}
result.CAChain = creationInfo.SigningBundle.GetCAChain()
return result, nil
}