430 lines
13 KiB
Go
430 lines
13 KiB
Go
// Package certutil contains helper functions that are mostly used
|
|
// with the PKI backend but can be generally useful. Functionality
|
|
// includes helpers for converting a certificate/private key bundle
|
|
// between DER and PEM, printing certificate serial numbers, and more.
|
|
//
|
|
// Functionality specific to the PKI backend includes some types
|
|
// and helper methods to make requesting certificates from the
|
|
// backend easy.
|
|
package certutil
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// Secret is used to attempt to unmarshal a Vault secret
|
|
// JSON response, as a convenience
|
|
type Secret struct {
|
|
Data map[string]interface{} `json:"data"`
|
|
}
|
|
|
|
// The type of of the Private Key referenced in CertBundle
|
|
// and ParsedCertBundle. This uses colloquial names rather than
|
|
// official names, to eliminate confusion
|
|
type PrivateKeyType int
|
|
|
|
const (
|
|
UnknownPrivateKey = iota
|
|
RSAPrivateKey
|
|
ECPrivateKey
|
|
)
|
|
|
|
// TLSUsage controls whether the intended usage of a *tls.Config
|
|
// returned from ParsedCertBundle.GetTLSConfig is for server use,
|
|
// client use, or both, which affects which values are set
|
|
type TLSUsage int
|
|
|
|
const (
|
|
TLSUnknown TLSUsage = 0
|
|
TLSServer TLSUsage = 1 << iota
|
|
TLSClient
|
|
)
|
|
|
|
// UserError represents an error generated due to invalid user input
|
|
type UserError struct {
|
|
Err string
|
|
}
|
|
|
|
func (e UserError) Error() string {
|
|
return e.Err
|
|
}
|
|
|
|
// InternalError represents an error generated internally,
|
|
// presumably not due to invalid user input
|
|
type InternalError struct {
|
|
Err string
|
|
}
|
|
|
|
func (e InternalError) Error() string {
|
|
return e.Err
|
|
}
|
|
|
|
// CertBundle contains a key type, a PEM-encoded private key,
|
|
// a PEM-encoded certificate, and a string-encoded serial number,
|
|
// returned from a successful Issue request
|
|
type CertBundle struct {
|
|
PrivateKeyType string `json:"private_key_type" structs:"private_key_type" mapstructure:"private_key_type"`
|
|
Certificate string `json:"certificate" structs:"certificate" mapstructure:"certificate"`
|
|
IssuingCA string `json:"issuing_ca" structs:"issuing_ca" mapstructure:"issuing_ca"`
|
|
PrivateKey string `json:"private_key" structs:"private_key" mapstructure:"private_key"`
|
|
SerialNumber string `json:"serial_number" structs:"serial_number" mapstructure:"serial_number"`
|
|
}
|
|
|
|
// ParsedCertBundle contains a key type, a DER-encoded private key,
|
|
// and a DER-encoded certificate
|
|
type ParsedCertBundle struct {
|
|
PrivateKeyType int
|
|
PrivateKeyBytes []byte
|
|
PrivateKey crypto.Signer
|
|
IssuingCABytes []byte
|
|
IssuingCA *x509.Certificate
|
|
CertificateBytes []byte
|
|
Certificate *x509.Certificate
|
|
}
|
|
|
|
// CSRBundle contains a key type, a PEM-encoded private key,
|
|
// and a PEM-encoded CSR
|
|
type CSRBundle struct {
|
|
PrivateKeyType string `json:"private_key_type" structs:"private_key_type" mapstructure:"private_key_type"`
|
|
CSR string `json:"csr" structs:"csr" mapstructure:"csr"`
|
|
PrivateKey string `json:"private_key" structs:"private_key" mapstructure:"private_key"`
|
|
}
|
|
|
|
// ParsedCSRBundle contains a key type, a DER-encoded private key,
|
|
// and a DER-encoded certificate request
|
|
type ParsedCSRBundle struct {
|
|
PrivateKeyType int
|
|
PrivateKeyBytes []byte
|
|
PrivateKey crypto.Signer
|
|
CSRBytes []byte
|
|
CSR *x509.CertificateRequest
|
|
}
|
|
|
|
// ToParsedCertBundle converts a string-based certificate bundle
|
|
// to a byte-based raw certificate bundle
|
|
func (c *CertBundle) ToParsedCertBundle() (*ParsedCertBundle, error) {
|
|
result := &ParsedCertBundle{}
|
|
var err error
|
|
var pemBlock *pem.Block
|
|
|
|
if len(c.PrivateKey) > 0 {
|
|
pemBlock, _ = pem.Decode([]byte(c.PrivateKey))
|
|
if pemBlock == nil {
|
|
return nil, UserError{"Error decoding private key from cert bundle"}
|
|
}
|
|
result.PrivateKeyBytes = pemBlock.Bytes
|
|
|
|
switch c.PrivateKeyType {
|
|
case "ec":
|
|
result.PrivateKeyType = ECPrivateKey
|
|
case "rsa":
|
|
result.PrivateKeyType = RSAPrivateKey
|
|
default:
|
|
// Try to figure it out and correct
|
|
if _, err := x509.ParseECPrivateKey(pemBlock.Bytes); err == nil {
|
|
result.PrivateKeyType = ECPrivateKey
|
|
c.PrivateKeyType = "ec"
|
|
} else if _, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes); err == nil {
|
|
result.PrivateKeyType = RSAPrivateKey
|
|
c.PrivateKeyType = "rsa"
|
|
} else {
|
|
return nil, UserError{fmt.Sprintf("Unknown private key type in bundle: %s", c.PrivateKeyType)}
|
|
}
|
|
}
|
|
|
|
result.PrivateKey, err = result.getSigner()
|
|
if err != nil {
|
|
return nil, UserError{fmt.Sprintf("Error getting signer: %s", err)}
|
|
}
|
|
}
|
|
|
|
if len(c.Certificate) > 0 {
|
|
pemBlock, _ = pem.Decode([]byte(c.Certificate))
|
|
if pemBlock == nil {
|
|
return nil, UserError{"Error decoding certificate from cert bundle"}
|
|
}
|
|
result.CertificateBytes = pemBlock.Bytes
|
|
result.Certificate, err = x509.ParseCertificate(result.CertificateBytes)
|
|
if err != nil {
|
|
return nil, UserError{"Error encountered parsing certificate bytes from raw bundle"}
|
|
}
|
|
}
|
|
|
|
if len(c.IssuingCA) > 0 {
|
|
pemBlock, _ = pem.Decode([]byte(c.IssuingCA))
|
|
if pemBlock == nil {
|
|
return nil, UserError{"Error decoding issuing CA from cert bundle"}
|
|
}
|
|
result.IssuingCABytes = pemBlock.Bytes
|
|
result.IssuingCA, err = x509.ParseCertificate(result.IssuingCABytes)
|
|
if err != nil {
|
|
return nil, UserError{fmt.Sprintf("Error parsing CA certificate: %s", err)}
|
|
}
|
|
}
|
|
|
|
if len(c.SerialNumber) == 0 && len(c.Certificate) > 0 {
|
|
c.SerialNumber = GetOctalFormatted(result.Certificate.SerialNumber.Bytes(), ":")
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// ToCertBundle converts a byte-based raw DER certificate bundle
|
|
// to a PEM-based string certificate bundle
|
|
func (p *ParsedCertBundle) ToCertBundle() (*CertBundle, error) {
|
|
result := &CertBundle{}
|
|
block := pem.Block{
|
|
Type: "CERTIFICATE",
|
|
}
|
|
|
|
if p.Certificate != nil {
|
|
result.SerialNumber = strings.TrimSpace(GetOctalFormatted(p.Certificate.SerialNumber.Bytes(), ":"))
|
|
}
|
|
|
|
if p.CertificateBytes != nil && len(p.CertificateBytes) > 0 {
|
|
block.Bytes = p.CertificateBytes
|
|
result.Certificate = strings.TrimSpace(string(pem.EncodeToMemory(&block)))
|
|
}
|
|
|
|
if p.IssuingCABytes != nil && len(p.IssuingCABytes) > 0 {
|
|
block.Bytes = p.IssuingCABytes
|
|
result.IssuingCA = strings.TrimSpace(string(pem.EncodeToMemory(&block)))
|
|
}
|
|
|
|
if p.PrivateKeyBytes != nil && len(p.PrivateKeyBytes) > 0 {
|
|
block.Bytes = p.PrivateKeyBytes
|
|
switch p.PrivateKeyType {
|
|
case RSAPrivateKey:
|
|
result.PrivateKeyType = "rsa"
|
|
block.Type = "RSA PRIVATE KEY"
|
|
case ECPrivateKey:
|
|
result.PrivateKeyType = "ec"
|
|
block.Type = "EC PRIVATE KEY"
|
|
default:
|
|
return nil, InternalError{"Could not determine private key type when creating block"}
|
|
}
|
|
result.PrivateKey = strings.TrimSpace(string(pem.EncodeToMemory(&block)))
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// GetSigner returns a crypto.Signer corresponding to the private key
|
|
// contained in this ParsedCertBundle. The Signer contains a Public() function
|
|
// for getting the corresponding public. The Signer can also be
|
|
// type-converted to private keys
|
|
func (p *ParsedCertBundle) getSigner() (crypto.Signer, error) {
|
|
var signer crypto.Signer
|
|
var err error
|
|
|
|
if p.PrivateKeyBytes == nil || len(p.PrivateKeyBytes) == 0 {
|
|
return nil, UserError{"Given parsed cert bundle does not have private key information"}
|
|
}
|
|
|
|
switch p.PrivateKeyType {
|
|
case ECPrivateKey:
|
|
signer, err = x509.ParseECPrivateKey(p.PrivateKeyBytes)
|
|
if err != nil {
|
|
return nil, UserError{fmt.Sprintf("Unable to parse CA's private EC key: %s", err)}
|
|
}
|
|
|
|
case RSAPrivateKey:
|
|
signer, err = x509.ParsePKCS1PrivateKey(p.PrivateKeyBytes)
|
|
if err != nil {
|
|
return nil, UserError{fmt.Sprintf("Unable to parse CA's private RSA key: %s", err)}
|
|
}
|
|
|
|
default:
|
|
return nil, UserError{"Unable to determine type of private key; only RSA and EC are supported"}
|
|
}
|
|
return signer, nil
|
|
}
|
|
|
|
// ToParsedCSRBundle converts a string-based CSR bundle
|
|
// to a byte-based raw CSR bundle
|
|
func (c *CSRBundle) ToParsedCSRBundle() (*ParsedCSRBundle, error) {
|
|
result := &ParsedCSRBundle{}
|
|
var err error
|
|
var pemBlock *pem.Block
|
|
|
|
if len(c.PrivateKey) > 0 {
|
|
pemBlock, _ = pem.Decode([]byte(c.PrivateKey))
|
|
if pemBlock == nil {
|
|
return nil, UserError{"Error decoding private key from cert bundle"}
|
|
}
|
|
result.PrivateKeyBytes = pemBlock.Bytes
|
|
|
|
switch c.PrivateKeyType {
|
|
case "ec":
|
|
result.PrivateKeyType = ECPrivateKey
|
|
case "rsa":
|
|
result.PrivateKeyType = RSAPrivateKey
|
|
default:
|
|
// Try to figure it out and correct
|
|
if _, err := x509.ParseECPrivateKey(pemBlock.Bytes); err == nil {
|
|
result.PrivateKeyType = ECPrivateKey
|
|
c.PrivateKeyType = "ec"
|
|
} else if _, err := x509.ParsePKCS1PrivateKey(pemBlock.Bytes); err == nil {
|
|
result.PrivateKeyType = RSAPrivateKey
|
|
c.PrivateKeyType = "rsa"
|
|
} else {
|
|
return nil, UserError{fmt.Sprintf("Unknown private key type in bundle: %s", c.PrivateKeyType)}
|
|
}
|
|
}
|
|
|
|
result.PrivateKey, err = result.getSigner()
|
|
if err != nil {
|
|
return nil, UserError{fmt.Sprintf("Error getting signer: %s", err)}
|
|
}
|
|
}
|
|
|
|
if len(c.CSR) > 0 {
|
|
pemBlock, _ = pem.Decode([]byte(c.CSR))
|
|
if pemBlock == nil {
|
|
return nil, UserError{"Error decoding certificate from cert bundle"}
|
|
}
|
|
result.CSRBytes = pemBlock.Bytes
|
|
result.CSR, err = x509.ParseCertificateRequest(result.CSRBytes)
|
|
if err != nil {
|
|
return nil, UserError{"Error encountered parsing certificate bytes from raw bundle"}
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// ToCSRBundle converts a byte-based raw DER certificate bundle
|
|
// to a PEM-based string certificate bundle
|
|
func (p *ParsedCSRBundle) ToCSRBundle() (*CSRBundle, error) {
|
|
result := &CSRBundle{}
|
|
block := pem.Block{
|
|
Type: "CERTIFICATE REQUEST",
|
|
}
|
|
|
|
if p.CSRBytes != nil && len(p.CSRBytes) > 0 {
|
|
block.Bytes = p.CSRBytes
|
|
result.CSR = strings.TrimSpace(string(pem.EncodeToMemory(&block)))
|
|
}
|
|
|
|
if p.PrivateKeyBytes != nil && len(p.PrivateKeyBytes) > 0 {
|
|
block.Bytes = p.PrivateKeyBytes
|
|
switch p.PrivateKeyType {
|
|
case RSAPrivateKey:
|
|
result.PrivateKeyType = "rsa"
|
|
block.Type = "RSA PRIVATE KEY"
|
|
case ECPrivateKey:
|
|
result.PrivateKeyType = "ec"
|
|
block.Type = "EC PRIVATE KEY"
|
|
default:
|
|
return nil, InternalError{"Could not determine private key type when creating block"}
|
|
}
|
|
result.PrivateKey = strings.TrimSpace(string(pem.EncodeToMemory(&block)))
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// GetSigner returns a crypto.Signer corresponding to the private key
|
|
// contained in this ParsedCSRBundle. The Signer contains a Public() function
|
|
// for getting the corresponding public. The Signer can also be
|
|
// type-converted to private keys
|
|
func (p *ParsedCSRBundle) getSigner() (crypto.Signer, error) {
|
|
var signer crypto.Signer
|
|
var err error
|
|
|
|
if p.PrivateKeyBytes == nil || len(p.PrivateKeyBytes) == 0 {
|
|
return nil, UserError{"Given parsed cert bundle does not have private key information"}
|
|
}
|
|
|
|
switch p.PrivateKeyType {
|
|
case ECPrivateKey:
|
|
signer, err = x509.ParseECPrivateKey(p.PrivateKeyBytes)
|
|
if err != nil {
|
|
return nil, UserError{fmt.Sprintf("Unable to parse CA's private EC key: %s", err)}
|
|
}
|
|
|
|
case RSAPrivateKey:
|
|
signer, err = x509.ParsePKCS1PrivateKey(p.PrivateKeyBytes)
|
|
if err != nil {
|
|
return nil, UserError{fmt.Sprintf("Unable to parse CA's private RSA key: %s", err)}
|
|
}
|
|
|
|
default:
|
|
return nil, UserError{"Unable to determine type of private key; only RSA and EC are supported"}
|
|
}
|
|
return signer, nil
|
|
}
|
|
|
|
// GetTLSConfig returns a TLS config generally suitable for client
|
|
// authentiation. The returned TLS config can be modified slightly
|
|
// to be made suitable for a server requiring client authentication;
|
|
// specifically, you should set the value of ClientAuth in the returned
|
|
// config to match your needs.
|
|
func (p *ParsedCertBundle) GetTLSConfig(usage TLSUsage) (*tls.Config, error) {
|
|
tlsCert := tls.Certificate{
|
|
Certificate: [][]byte{},
|
|
}
|
|
|
|
tlsConfig := &tls.Config{
|
|
NextProtos: []string{"http/1.1"},
|
|
}
|
|
|
|
if p.Certificate != nil {
|
|
tlsCert.Leaf = p.Certificate
|
|
}
|
|
|
|
if p.PrivateKey != nil {
|
|
tlsCert.PrivateKey = p.PrivateKey
|
|
}
|
|
|
|
if p.CertificateBytes != nil && len(p.CertificateBytes) > 0 {
|
|
tlsCert.Certificate = append(tlsCert.Certificate, p.CertificateBytes)
|
|
}
|
|
|
|
if p.IssuingCABytes != nil && len(p.IssuingCABytes) > 0 {
|
|
tlsCert.Certificate = append(tlsCert.Certificate, p.IssuingCABytes)
|
|
|
|
// Technically we only need one cert, but this doesn't duplicate code
|
|
certBundle, err := p.ToCertBundle()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Error converting parsed bundle to string bundle when getting TLS config: %s", err)
|
|
}
|
|
|
|
caPool := x509.NewCertPool()
|
|
ok := caPool.AppendCertsFromPEM([]byte(certBundle.IssuingCA))
|
|
if !ok {
|
|
return nil, fmt.Errorf("Could not append CA certificate")
|
|
}
|
|
|
|
if usage&TLSServer > 0 {
|
|
tlsConfig.ClientCAs = caPool
|
|
tlsConfig.ClientAuth = tls.VerifyClientCertIfGiven
|
|
}
|
|
if usage&TLSClient > 0 {
|
|
tlsConfig.RootCAs = caPool
|
|
}
|
|
}
|
|
|
|
if tlsCert.Certificate != nil && len(tlsCert.Certificate) > 0 {
|
|
tlsConfig.Certificates = []tls.Certificate{tlsCert}
|
|
tlsConfig.BuildNameToCertificate()
|
|
}
|
|
|
|
return tlsConfig, nil
|
|
}
|
|
|
|
// IssueData is a structure that is suitable for marshaling into a request;
|
|
// either via JSON, or into a map[string]interface{} via the structs package
|
|
type IssueData struct {
|
|
TTL string `json:"ttl" structs:"ttl" mapstructure:"ttl"`
|
|
CommonName string `json:"common_name" structs:"common_name" mapstructure:"common_name"`
|
|
AltNames string `json:"alt_names" structs:"alt_names" mapstructure:"alt_names"`
|
|
IPSANs string `json:"ip_sans" structs:"ip_sans" mapstructure:"ip_sans"`
|
|
CSR string `json:"csr" structs:"csr" mapstructure:"csr"`
|
|
}
|