364 lines
9.0 KiB
Go
364 lines
9.0 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package tlsutil
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/sha256"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"net"
|
|
"time"
|
|
)
|
|
|
|
// GenerateSerialNumber returns random bigint generated with crypto/rand
|
|
func GenerateSerialNumber() (*big.Int, error) {
|
|
l := new(big.Int).Lsh(big.NewInt(1), 128)
|
|
s, err := rand.Int(rand.Reader, l)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// GeneratePrivateKey generates a new ecdsa private key
|
|
func GeneratePrivateKey() (crypto.Signer, string, error) {
|
|
curve := elliptic.P256()
|
|
|
|
pk, err := ecdsa.GenerateKey(curve, rand.Reader)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("error generating ECDSA private key: %s", err)
|
|
}
|
|
|
|
bs, err := x509.MarshalECPrivateKey(pk)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("error marshaling ECDSA private key: %s", err)
|
|
}
|
|
|
|
pemBlock, err := pemEncodeKey(bs, "EC PRIVATE KEY")
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
return pk, pemBlock, nil
|
|
}
|
|
|
|
func pemEncodeKey(key []byte, blockType string) (string, error) {
|
|
var buf bytes.Buffer
|
|
|
|
if err := pem.Encode(&buf, &pem.Block{Type: blockType, Bytes: key}); err != nil {
|
|
return "", fmt.Errorf("error encoding private key: %s", err)
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
type CAOpts struct {
|
|
Signer crypto.Signer
|
|
Serial *big.Int
|
|
Days int
|
|
PermittedDNSDomains []string
|
|
Country string
|
|
PostalCode string
|
|
Province string
|
|
Locality string
|
|
StreetAddress string
|
|
Organization string
|
|
OrganizationalUnit string
|
|
Name string
|
|
}
|
|
|
|
type CertOpts struct {
|
|
Signer crypto.Signer
|
|
CA string
|
|
Serial *big.Int
|
|
Name string
|
|
Days int
|
|
DNSNames []string
|
|
IPAddresses []net.IP
|
|
ExtKeyUsage []x509.ExtKeyUsage
|
|
}
|
|
|
|
// IsNotCustom checks whether any of CAOpts parameters have been populated with
|
|
// non-default values.
|
|
func (c *CAOpts) IsNotCustom() bool {
|
|
return c.Country == "" &&
|
|
c.PostalCode == "" &&
|
|
c.Province == "" &&
|
|
c.Locality == "" &&
|
|
c.StreetAddress == "" &&
|
|
c.Organization == "" &&
|
|
c.OrganizationalUnit == "" &&
|
|
c.Name == ""
|
|
}
|
|
|
|
// GenerateCA generates a new CA for agent TLS (not to be confused with Connect TLS)
|
|
func GenerateCA(opts CAOpts) (string, string, error) {
|
|
var (
|
|
id []byte
|
|
pk string
|
|
err error
|
|
signer = opts.Signer
|
|
sn = opts.Serial
|
|
)
|
|
if signer == nil {
|
|
var err error
|
|
signer, pk, err = GeneratePrivateKey()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
}
|
|
|
|
id, err = keyID(signer.Public())
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
if sn == nil {
|
|
var err error
|
|
sn, err = GenerateSerialNumber()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
}
|
|
|
|
if opts.IsNotCustom() {
|
|
opts.Name = fmt.Sprintf("Nomad Agent CA %d", sn)
|
|
if opts.Days == 0 {
|
|
opts.Days = 1825
|
|
}
|
|
opts.Country = "US"
|
|
opts.PostalCode = "94105"
|
|
opts.Province = "CA"
|
|
opts.Locality = "San Francisco"
|
|
opts.StreetAddress = "101 Second Street"
|
|
opts.Organization = "HashiCorp Inc."
|
|
opts.OrganizationalUnit = "Nomad"
|
|
} else {
|
|
if opts.Name == "" {
|
|
return "", "", errors.New("common name value not provided")
|
|
} else {
|
|
opts.Name = fmt.Sprintf("%s %d", opts.Name, sn)
|
|
}
|
|
if opts.Country == "" {
|
|
return "", "", errors.New("country value not provided")
|
|
}
|
|
|
|
if opts.Organization == "" {
|
|
return "", "", errors.New("organization value not provided")
|
|
}
|
|
|
|
if opts.OrganizationalUnit == "" {
|
|
return "", "", errors.New("organizational unit value not provided")
|
|
}
|
|
}
|
|
|
|
// Create the CA cert
|
|
template := x509.Certificate{
|
|
SerialNumber: sn,
|
|
Subject: pkix.Name{
|
|
Country: []string{opts.Country},
|
|
PostalCode: []string{opts.PostalCode},
|
|
Province: []string{opts.Province},
|
|
Locality: []string{opts.Locality},
|
|
StreetAddress: []string{opts.StreetAddress},
|
|
Organization: []string{opts.Organization},
|
|
OrganizationalUnit: []string{opts.OrganizationalUnit},
|
|
CommonName: opts.Name,
|
|
},
|
|
BasicConstraintsValid: true,
|
|
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature,
|
|
IsCA: true,
|
|
NotAfter: time.Now().AddDate(0, 0, opts.Days),
|
|
NotBefore: time.Now(),
|
|
AuthorityKeyId: id,
|
|
SubjectKeyId: id,
|
|
}
|
|
|
|
if len(opts.PermittedDNSDomains) > 0 {
|
|
template.PermittedDNSDomainsCritical = true
|
|
template.PermittedDNSDomains = opts.PermittedDNSDomains
|
|
}
|
|
bs, err := x509.CreateCertificate(
|
|
rand.Reader, &template, &template, signer.Public(), signer)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("error generating CA certificate: %s", err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs})
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("error encoding private key: %s", err)
|
|
}
|
|
|
|
return buf.String(), pk, nil
|
|
}
|
|
|
|
// GenerateCert generates a new certificate for agent TLS (not to be confused with Connect TLS)
|
|
func GenerateCert(opts CertOpts) (string, string, error) {
|
|
parent, err := parseCert(opts.CA)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
signee, pk, err := GeneratePrivateKey()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
id, err := keyID(signee.Public())
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
sn := opts.Serial
|
|
if sn == nil {
|
|
var err error
|
|
sn, err = GenerateSerialNumber()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
}
|
|
|
|
template := x509.Certificate{
|
|
SerialNumber: sn,
|
|
Subject: pkix.Name{CommonName: opts.Name},
|
|
BasicConstraintsValid: true,
|
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
|
ExtKeyUsage: opts.ExtKeyUsage,
|
|
IsCA: false,
|
|
NotAfter: time.Now().AddDate(0, 0, opts.Days),
|
|
NotBefore: time.Now(),
|
|
SubjectKeyId: id,
|
|
DNSNames: opts.DNSNames,
|
|
IPAddresses: opts.IPAddresses,
|
|
}
|
|
|
|
bs, err := x509.CreateCertificate(rand.Reader, &template, parent, signee.Public(), opts.Signer)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs})
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("error encoding private key: %s", err)
|
|
}
|
|
|
|
return buf.String(), pk, nil
|
|
}
|
|
|
|
// KeyId returns a x509 KeyId from the given signing key.
|
|
func keyID(raw interface{}) ([]byte, error) {
|
|
switch raw.(type) {
|
|
case *ecdsa.PublicKey:
|
|
case *rsa.PublicKey:
|
|
default:
|
|
return nil, fmt.Errorf("invalid key type: %T", raw)
|
|
}
|
|
|
|
// This is not standard; RFC allows any unique identifier as long as they
|
|
// match in subject/authority chains but suggests specific hashing of DER
|
|
// bytes of public key including DER tags.
|
|
bs, err := x509.MarshalPKIXPublicKey(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// String formatted
|
|
kID := sha256.Sum256(bs)
|
|
return kID[:], nil
|
|
}
|
|
|
|
// ParseCert parses the x509 certificate from a PEM-encoded value.
|
|
func ParseCert(pemValue string) (*x509.Certificate, error) {
|
|
// The _ result below is not an error but the remaining PEM bytes.
|
|
block, _ := pem.Decode([]byte(pemValue))
|
|
if block == nil {
|
|
return nil, fmt.Errorf("no PEM-encoded data found")
|
|
}
|
|
|
|
if block.Type != "CERTIFICATE" {
|
|
return nil, fmt.Errorf("first PEM-block should be CERTIFICATE type")
|
|
}
|
|
|
|
return x509.ParseCertificate(block.Bytes)
|
|
}
|
|
|
|
func parseCert(pemValue string) (*x509.Certificate, error) {
|
|
// The _ result below is not an error but the remaining PEM bytes.
|
|
block, _ := pem.Decode([]byte(pemValue))
|
|
if block == nil {
|
|
return nil, fmt.Errorf("no PEM-encoded data found")
|
|
}
|
|
|
|
if block.Type != "CERTIFICATE" {
|
|
return nil, fmt.Errorf("first PEM-block should be CERTIFICATE type")
|
|
}
|
|
|
|
return x509.ParseCertificate(block.Bytes)
|
|
}
|
|
|
|
// ParseSigner parses a crypto.Signer from a PEM-encoded key. The private key
|
|
// is expected to be the first block in the PEM value.
|
|
func ParseSigner(pemValue string) (crypto.Signer, error) {
|
|
// The _ result below is not an error but the remaining PEM bytes.
|
|
block, _ := pem.Decode([]byte(pemValue))
|
|
if block == nil {
|
|
return nil, fmt.Errorf("no PEM-encoded data found")
|
|
}
|
|
|
|
switch block.Type {
|
|
case "EC PRIVATE KEY":
|
|
return x509.ParseECPrivateKey(block.Bytes)
|
|
|
|
case "RSA PRIVATE KEY":
|
|
return x509.ParsePKCS1PrivateKey(block.Bytes)
|
|
|
|
case "PRIVATE KEY":
|
|
signer, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pk, ok := signer.(crypto.Signer)
|
|
if !ok {
|
|
return nil, fmt.Errorf("private key is not a valid format")
|
|
}
|
|
|
|
return pk, nil
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unknown PEM block type for signing key: %s", block.Type)
|
|
}
|
|
}
|
|
|
|
func Verify(caString, certString, dns string) error {
|
|
roots := x509.NewCertPool()
|
|
ok := roots.AppendCertsFromPEM([]byte(caString))
|
|
if !ok {
|
|
return fmt.Errorf("failed to parse root certificate")
|
|
}
|
|
|
|
cert, err := parseCert(certString)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse certificate")
|
|
}
|
|
|
|
opts := x509.VerifyOptions{
|
|
DNSName: fmt.Sprint(dns),
|
|
Roots: roots,
|
|
}
|
|
|
|
_, err = cert.Verify(opts)
|
|
return err
|
|
}
|