2023-06-07 23:07:43 +00:00
|
|
|
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
2020-09-16 14:19:51 +00:00
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
2020-07-13 19:30:16 +00:00
|
|
|
|
|
|
|
// 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"
|
2020-10-27 23:33:08 +00:00
|
|
|
"net"
|
2020-07-13 19:30:16 +00:00
|
|
|
"time"
|
2021-03-15 16:24:07 +00:00
|
|
|
|
|
|
|
"go.pinniped.dev/internal/constable"
|
2020-07-13 19:30:16 +00:00
|
|
|
)
|
|
|
|
|
2020-08-24 19:01:07 +00:00
|
|
|
// certBackdate is the amount of time before time.Now() that will be used to set
|
2021-09-21 13:19:50 +00:00
|
|
|
// a certificate's NotBefore field. We use the same hard coded and unconfigurable
|
|
|
|
// backdate value as used by the Kubernetes controller manager certificate signer:
|
|
|
|
// https://github.com/kubernetes/kubernetes/blob/68d646a101005e95379d84160adf01d146bdd149/pkg/controller/certificates/signer/signer.go#L199
|
|
|
|
const certBackdate = 5 * time.Minute
|
2020-08-24 19:01:07 +00:00
|
|
|
|
2020-07-27 12:50:59 +00:00
|
|
|
type env struct {
|
2020-07-13 19:30:16 +00:00
|
|
|
// 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
|
|
|
|
|
2020-07-27 12:50:59 +00:00
|
|
|
// parse function to parse an ASN.1 byte slice into an x509 struct (normally x509.ParseCertificate)
|
|
|
|
parseCert func([]byte) (*x509.Certificate, error)
|
|
|
|
}
|
2020-07-13 19:30:16 +00:00
|
|
|
|
2020-07-27 12:50:59 +00:00
|
|
|
// CA holds the state for a simple x509 certificate authority suitable for use in an aggregated API service.
|
|
|
|
type CA struct {
|
2021-03-02 01:02:08 +00:00
|
|
|
// caCertBytes is the DER-encoded certificate for the current CA.
|
2020-07-13 19:30:16 +00:00
|
|
|
caCertBytes []byte
|
|
|
|
|
2020-07-27 12:50:59 +00:00
|
|
|
// signer is the private key for the current CA.
|
|
|
|
signer crypto.Signer
|
|
|
|
|
2021-03-02 01:02:08 +00:00
|
|
|
// privateKey is the same private key represented by signer, but in a format which allows export.
|
|
|
|
// It is only set by New, not by Load, since Load can handle various types of PrivateKey but New
|
|
|
|
// only needs to create keys of type ecdsa.PrivateKey.
|
|
|
|
privateKey *ecdsa.PrivateKey
|
|
|
|
|
2020-07-27 12:50:59 +00:00
|
|
|
// env is our reference to the outside world (clocks and random number generation).
|
|
|
|
env env
|
|
|
|
}
|
2020-07-13 19:30:16 +00:00
|
|
|
|
2020-07-27 12:50:59 +00:00
|
|
|
// secureEnv is the "real" environment using secure RNGs and the real system clock.
|
|
|
|
func secureEnv() env {
|
|
|
|
return env{
|
2020-07-13 19:30:16 +00:00
|
|
|
serialRNG: rand.Reader,
|
|
|
|
keygenRNG: rand.Reader,
|
|
|
|
signingRNG: rand.Reader,
|
|
|
|
clock: time.Now,
|
2020-07-27 12:50:59 +00:00
|
|
|
parseCert: x509.ParseCertificate,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-27 13:18:37 +00:00
|
|
|
// ErrInvalidCACertificate is returned when the contents of the loaded CA certificate do not meet our assumptions.
|
2021-03-15 16:24:07 +00:00
|
|
|
const ErrInvalidCACertificate = constable.Error("invalid CA certificate")
|
2020-07-27 13:18:37 +00:00
|
|
|
|
2020-07-27 12:50:59 +00:00
|
|
|
// Load a certificate authority from an existing certificate and private key (in PEM format).
|
2020-08-19 18:21:07 +00:00
|
|
|
func Load(certPEM string, keyPEM string) (*CA, error) {
|
|
|
|
cert, err := tls.X509KeyPair([]byte(certPEM), []byte(keyPEM))
|
2020-07-27 12:50:59 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not load CA: %w", err)
|
2020-07-13 19:30:16 +00:00
|
|
|
}
|
2020-07-27 12:50:59 +00:00
|
|
|
if certCount := len(cert.Certificate); certCount != 1 {
|
2020-07-27 13:18:37 +00:00
|
|
|
return nil, fmt.Errorf("%w: expected a single certificate, found %d certificates", ErrInvalidCACertificate, certCount)
|
2020-07-13 19:30:16 +00:00
|
|
|
}
|
2021-03-15 16:24:07 +00:00
|
|
|
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("failed to parse key pair as x509 cert: %w", err)
|
|
|
|
}
|
|
|
|
if !x509Cert.IsCA {
|
|
|
|
return nil, fmt.Errorf("%w: passed in key pair is not a CA", ErrInvalidCACertificate)
|
|
|
|
}
|
2020-07-27 12:50:59 +00:00
|
|
|
return &CA{
|
|
|
|
caCertBytes: cert.Certificate[0],
|
|
|
|
signer: cert.PrivateKey.(crypto.Signer),
|
|
|
|
env: secureEnv(),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2021-03-13 00:09:16 +00:00
|
|
|
// New generates a fresh certificate authority with the given Common Name and TTL.
|
|
|
|
func New(commonName string, ttl time.Duration) (*CA, error) {
|
|
|
|
return newInternal(commonName, ttl, secureEnv())
|
2020-08-27 19:59:47 +00:00
|
|
|
}
|
2020-07-13 19:30:16 +00:00
|
|
|
|
2020-07-27 12:50:59 +00:00
|
|
|
// newInternal is the internal guts of New, broken out for easier testing.
|
2021-03-13 00:09:16 +00:00
|
|
|
func newInternal(commonName string, ttl time.Duration, env env) (*CA, error) {
|
2020-07-27 12:50:59 +00:00
|
|
|
ca := CA{env: env}
|
2020-07-13 19:30:16 +00:00
|
|
|
// Generate a random serial for the CA
|
2020-07-27 12:50:59 +00:00
|
|
|
serialNumber, err := randomSerial(env.serialRNG)
|
2020-07-13 19:30:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not generate CA serial: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate a new P256 keypair.
|
2021-03-02 01:02:08 +00:00
|
|
|
ca.privateKey, err = ecdsa.GenerateKey(elliptic.P256(), env.keygenRNG)
|
2020-07-13 19:30:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not generate CA private key: %w", err)
|
|
|
|
}
|
2021-03-02 01:02:08 +00:00
|
|
|
ca.signer = ca.privateKey
|
2020-07-13 19:30:16 +00:00
|
|
|
|
2020-08-27 19:59:47 +00:00
|
|
|
// Make a CA certificate valid for some ttl and backdated by some amount.
|
2020-07-27 12:50:59 +00:00
|
|
|
now := env.clock()
|
2020-08-24 19:01:07 +00:00
|
|
|
notBefore := now.Add(-certBackdate)
|
2020-08-27 19:59:47 +00:00
|
|
|
notAfter := now.Add(ttl)
|
2020-07-13 19:30:16 +00:00
|
|
|
|
|
|
|
// Create CA cert template
|
|
|
|
caTemplate := x509.Certificate{
|
|
|
|
SerialNumber: serialNumber,
|
2021-03-13 00:09:16 +00:00
|
|
|
Subject: pkix.Name{CommonName: commonName},
|
2020-07-13 19:30:16 +00:00
|
|
|
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.
|
2021-03-02 01:02:08 +00:00
|
|
|
caCertBytes, err := x509.CreateCertificate(env.signingRNG, &caTemplate, &caTemplate, &ca.privateKey.PublicKey, ca.privateKey)
|
2020-07-13 19:30:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not issue CA certificate: %w", err)
|
|
|
|
}
|
|
|
|
ca.caCertBytes = caCertBytes
|
|
|
|
return &ca, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Bundle returns the current CA signing bundle in concatenated PEM format.
|
2020-07-27 12:50:59 +00:00
|
|
|
func (c *CA) Bundle() []byte {
|
|
|
|
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.caCertBytes})
|
2020-07-13 19:30:16 +00:00
|
|
|
}
|
|
|
|
|
2021-03-02 01:02:08 +00:00
|
|
|
// PrivateKeyToPEM returns the current CA private key in PEM format, if this CA was constructed by New.
|
|
|
|
func (c *CA) PrivateKeyToPEM() ([]byte, error) {
|
|
|
|
if c.privateKey == nil {
|
|
|
|
return nil, fmt.Errorf("no private key data (did you try to use this after Load?)")
|
|
|
|
}
|
|
|
|
derKey, err := x509.MarshalECPrivateKey(c.privateKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: derKey}), nil
|
|
|
|
}
|
|
|
|
|
2020-12-02 20:33:07 +00:00
|
|
|
// Pool returns the current CA signing bundle as a *x509.CertPool.
|
|
|
|
func (c *CA) Pool() *x509.CertPool {
|
|
|
|
pool := x509.NewCertPool()
|
|
|
|
pool.AppendCertsFromPEM(c.Bundle())
|
|
|
|
return pool
|
|
|
|
}
|
|
|
|
|
2021-03-13 00:09:16 +00:00
|
|
|
// IssueClientCert issues a new client certificate with username and groups included in the Kube-style
|
|
|
|
// certificate subject for the given identity and duration.
|
|
|
|
func (c *CA) IssueClientCert(username string, groups []string, ttl time.Duration) (*tls.Certificate, error) {
|
|
|
|
return c.issueCert(x509.ExtKeyUsageClientAuth, pkix.Name{CommonName: username, Organization: groups}, nil, nil, ttl)
|
|
|
|
}
|
|
|
|
|
|
|
|
// IssueServerCert issues a new server certificate for the given identity and duration.
|
|
|
|
// The dnsNames and ips are each optional, but at least one of them should be specified.
|
|
|
|
func (c *CA) IssueServerCert(dnsNames []string, ips []net.IP, ttl time.Duration) (*tls.Certificate, error) {
|
|
|
|
return c.issueCert(x509.ExtKeyUsageServerAuth, pkix.Name{}, dnsNames, ips, ttl)
|
|
|
|
}
|
|
|
|
|
2023-06-07 23:07:43 +00:00
|
|
|
// IssueClientCertPEM is similar to IssueClientCert, but returns the new cert as a pair of PEM-formatted byte slices
|
2021-03-13 00:09:16 +00:00
|
|
|
// for the certificate and private key.
|
|
|
|
func (c *CA) IssueClientCertPEM(username string, groups []string, ttl time.Duration) ([]byte, []byte, error) {
|
|
|
|
return toPEM(c.IssueClientCert(username, groups, ttl))
|
|
|
|
}
|
|
|
|
|
2023-06-07 23:07:43 +00:00
|
|
|
// IssueServerCertPEM is similar to IssueServerCert, but returns the new cert as a pair of PEM-formatted byte slices
|
2021-03-13 00:09:16 +00:00
|
|
|
// for the certificate and private key.
|
|
|
|
func (c *CA) IssueServerCertPEM(dnsNames []string, ips []net.IP, ttl time.Duration) ([]byte, []byte, error) {
|
|
|
|
return toPEM(c.IssueServerCert(dnsNames, ips, ttl))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *CA) issueCert(extKeyUsage x509.ExtKeyUsage, subject pkix.Name, dnsNames []string, ips []net.IP, ttl time.Duration) (*tls.Certificate, error) {
|
2020-07-13 19:30:16 +00:00
|
|
|
// Choose a random 128 bit serial number.
|
2020-07-27 12:50:59 +00:00
|
|
|
serialNumber, err := randomSerial(c.env.serialRNG)
|
2020-07-13 19:30:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not generate serial number for certificate: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate a new P256 keypair.
|
2020-07-27 12:50:59 +00:00
|
|
|
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), c.env.keygenRNG)
|
2020-07-13 19:30:16 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not generate private key: %w", err)
|
|
|
|
}
|
|
|
|
|
2020-08-24 19:01:07 +00:00
|
|
|
// Make a CA caCert valid for the requested TTL and backdated by some amount.
|
2020-07-27 12:50:59 +00:00
|
|
|
now := c.env.clock()
|
2020-08-24 19:01:07 +00:00
|
|
|
notBefore := now.Add(-certBackdate)
|
2020-07-13 19:30:16 +00:00
|
|
|
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{
|
2021-03-13 00:09:16 +00:00
|
|
|
SerialNumber: serialNumber,
|
|
|
|
Subject: subject,
|
|
|
|
NotBefore: notBefore,
|
|
|
|
NotAfter: notAfter,
|
|
|
|
ExtKeyUsage: []x509.ExtKeyUsage{extKeyUsage},
|
2020-07-13 19:30:16 +00:00
|
|
|
BasicConstraintsValid: true,
|
|
|
|
IsCA: false,
|
|
|
|
DNSNames: dnsNames,
|
2020-10-27 23:33:08 +00:00
|
|
|
IPAddresses: ips,
|
2020-07-13 19:30:16 +00:00
|
|
|
}
|
|
|
|
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.
|
2020-07-27 12:50:59 +00:00
|
|
|
newCert, err := c.env.parseCert(certBytes)
|
2020-07-13 19:30:16 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-07-27 12:50:59 +00:00
|
|
|
func toPEM(cert *tls.Certificate, err error) ([]byte, []byte, error) {
|
2021-03-13 00:09:16 +00:00
|
|
|
// If the wrapped IssueServerCert() returned an error, pass it back.
|
2020-07-27 12:50:59 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
2020-08-11 01:53:53 +00:00
|
|
|
certPEM, keyPEM, err := ToPEM(cert)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return certPEM, keyPEM, nil
|
|
|
|
}
|
|
|
|
|
2023-06-07 23:07:43 +00:00
|
|
|
// ToPEM encodes a tls.Certificate into a private key PEM and a cert chain PEM.
|
2020-08-11 01:53:53 +00:00
|
|
|
func ToPEM(cert *tls.Certificate) ([]byte, []byte, error) {
|
2020-07-27 12:50:59 +00:00
|
|
|
// Encode the certificate(s) to PEM.
|
|
|
|
certPEMBlocks := make([][]byte, 0, len(cert.Certificate))
|
|
|
|
for _, c := range cert.Certificate {
|
|
|
|
certPEMBlocks = append(certPEMBlocks, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c}))
|
|
|
|
}
|
|
|
|
certPEM := bytes.Join(certPEMBlocks, nil)
|
|
|
|
|
|
|
|
// Encode the private key to PEM, which means we first need to convert to PKCS8 (DER).
|
|
|
|
privateKeyPKCS8, err := x509.MarshalPKCS8PrivateKey(cert.PrivateKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("failed to marshal private key into PKCS8: %w", err)
|
|
|
|
}
|
|
|
|
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateKeyPKCS8})
|
|
|
|
|
|
|
|
return certPEM, keyPEM, nil
|
|
|
|
}
|
|
|
|
|
2020-07-13 19:30:16 +00:00
|
|
|
// 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))
|
|
|
|
}
|