173 lines
5.3 KiB
Go
173 lines
5.3 KiB
Go
|
/*
|
||
|
Copyright 2020 VMware, Inc.
|
||
|
SPDX-License-Identifier: Apache-2.0
|
||
|
*/
|
||
|
|
||
|
// Package certauthority implements a simple x509 certificate authority suitable for use in an aggregated API service.
|
||
|
package certauthority
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"crypto"
|
||
|
"crypto/ecdsa"
|
||
|
"crypto/elliptic"
|
||
|
"crypto/rand"
|
||
|
"crypto/tls"
|
||
|
"crypto/x509"
|
||
|
"crypto/x509/pkix"
|
||
|
"encoding/pem"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"math/big"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
// CA holds the state for a simple x509 certificate authority suitable for use in an aggregated API service.
|
||
|
type CA struct {
|
||
|
// secure random number generators for various steps (usually crypto/rand.Reader, but broken out here for tests).
|
||
|
serialRNG io.Reader
|
||
|
keygenRNG io.Reader
|
||
|
signingRNG io.Reader
|
||
|
|
||
|
// clock tells the current time (usually time.Now(), but broken out here for tests).
|
||
|
clock func() time.Time
|
||
|
|
||
|
// signer is the private key for the current CA.
|
||
|
signer crypto.Signer
|
||
|
|
||
|
// caCert is the DER-encoded certificate for the current CA.
|
||
|
caCertBytes []byte
|
||
|
}
|
||
|
|
||
|
// Option to pass when calling New.
|
||
|
type Option func(*CA) error
|
||
|
|
||
|
func New(subject pkix.Name, opts ...Option) (*CA, error) {
|
||
|
// Initialize the result by starting with some defaults and applying any provided options.
|
||
|
ca := CA{
|
||
|
serialRNG: rand.Reader,
|
||
|
keygenRNG: rand.Reader,
|
||
|
signingRNG: rand.Reader,
|
||
|
clock: time.Now,
|
||
|
}
|
||
|
for _, opt := range opts {
|
||
|
if err := opt(&ca); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Generate a random serial for the CA
|
||
|
serialNumber, err := randomSerial(ca.serialRNG)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("could not generate CA serial: %w", err)
|
||
|
}
|
||
|
|
||
|
// Generate a new P256 keypair.
|
||
|
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), ca.keygenRNG)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("could not generate CA private key: %w", err)
|
||
|
}
|
||
|
ca.signer = privateKey
|
||
|
|
||
|
// Make a CA certificate valid for 100 years and backdated by one minute.
|
||
|
now := ca.clock()
|
||
|
notBefore := now.Add(-1 * time.Minute)
|
||
|
notAfter := now.Add(24 * time.Hour * 365 * 100)
|
||
|
|
||
|
// Create CA cert template
|
||
|
caTemplate := x509.Certificate{
|
||
|
SerialNumber: serialNumber,
|
||
|
Subject: subject,
|
||
|
NotBefore: notBefore,
|
||
|
NotAfter: notAfter,
|
||
|
IsCA: true,
|
||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
|
||
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||
|
BasicConstraintsValid: true,
|
||
|
}
|
||
|
|
||
|
// Self-sign the CA to get the DER certificate.
|
||
|
caCertBytes, err := x509.CreateCertificate(ca.signingRNG, &caTemplate, &caTemplate, &privateKey.PublicKey, privateKey)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("could not issue CA certificate: %w", err)
|
||
|
}
|
||
|
ca.caCertBytes = caCertBytes
|
||
|
return &ca, nil
|
||
|
}
|
||
|
|
||
|
// WriteBundle writes the current CA signing bundle in concatenated PEM format.
|
||
|
func (c *CA) WriteBundle(out io.Writer) error {
|
||
|
if err := pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: c.caCertBytes}); err != nil {
|
||
|
return fmt.Errorf("could not encode CA certificate to PEM: %w", err)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Bundle returns the current CA signing bundle in concatenated PEM format.
|
||
|
func (c *CA) Bundle() ([]byte, error) {
|
||
|
var out bytes.Buffer
|
||
|
err := c.WriteBundle(&out)
|
||
|
return out.Bytes(), err
|
||
|
}
|
||
|
|
||
|
// Issue a new server certificate for the given identity and duration.
|
||
|
func (c *CA) Issue(subject pkix.Name, dnsNames []string, ttl time.Duration) (*tls.Certificate, error) {
|
||
|
// Choose a random 128 bit serial number.
|
||
|
serialNumber, err := randomSerial(c.serialRNG)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("could not generate serial number for certificate: %w", err)
|
||
|
}
|
||
|
|
||
|
// Generate a new P256 keypair.
|
||
|
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), c.keygenRNG)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("could not generate private key: %w", err)
|
||
|
}
|
||
|
|
||
|
// Make a CA caCert valid for the requested TTL and backdated by one minute.
|
||
|
now := c.clock()
|
||
|
notBefore := now.Add(-1 * time.Minute)
|
||
|
notAfter := now.Add(ttl)
|
||
|
|
||
|
// Parse the DER encoded certificate to get an x509.Certificate.
|
||
|
caCert, err := x509.ParseCertificate(c.caCertBytes)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("could not parse CA certificate: %w", err)
|
||
|
}
|
||
|
|
||
|
// Sign a cert, getting back the DER-encoded certificate bytes.
|
||
|
template := x509.Certificate{
|
||
|
SerialNumber: serialNumber,
|
||
|
Subject: subject,
|
||
|
NotBefore: notBefore,
|
||
|
NotAfter: notAfter,
|
||
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||
|
BasicConstraintsValid: true,
|
||
|
IsCA: false,
|
||
|
DNSNames: dnsNames,
|
||
|
}
|
||
|
certBytes, err := x509.CreateCertificate(rand.Reader, &template, caCert, &privateKey.PublicKey, c.signer)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("could not sign certificate: %w", err)
|
||
|
}
|
||
|
|
||
|
// Parse the DER encoded certificate back out into an *x509.Certificate.
|
||
|
newCert, err := x509.ParseCertificate(certBytes)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("could not parse certificate: %w", err)
|
||
|
}
|
||
|
|
||
|
// Return the new certificate.
|
||
|
return &tls.Certificate{
|
||
|
Certificate: [][]byte{certBytes},
|
||
|
Leaf: newCert,
|
||
|
PrivateKey: privateKey,
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
// randomSerial generates a random 128 bit serial number.
|
||
|
func randomSerial(rng io.Reader) (*big.Int, error) {
|
||
|
return rand.Int(rng, new(big.Int).Lsh(big.NewInt(1), 128))
|
||
|
}
|