Files
coder/coderd/cryptokeys/ca.go
T

150 lines
5.0 KiB
Go

package cryptokeys
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"time"
"golang.org/x/xerrors"
)
const (
caCertPEMBlockType = "CERTIFICATE"
caKeyPEMBlockType = "EC PRIVATE KEY"
// clockSkewTolerance backdates the CA certificate's NotBefore and extends
// its NotAfter so that replicas with mildly skewed clocks still accept it.
clockSkewTolerance = time.Hour
)
// NATSCA is the decoded form of a single nats_ca crypto key row, produced by
// the generic crypto key cache (see idSecret). The CA signs the ephemeral leaf
// certificates that replicas use for NATS cluster mTLS.
//
// The active CA is served by a SigningKeycache.SigningKey call for the nats_ca
// feature; a specific historical CA (for verifying a peer leaf minted under an
// earlier CA during a rotation overlap) is served by VerifyingKey with that
// row's sequence.
type NATSCA struct {
// Sequence is the crypto_keys sequence of the row this CA came from.
Sequence int32
// Cert is the CA certificate used to sign or verify leaf certificates.
Cert *x509.Certificate
// Key is the CA private key, used to sign leaves.
Key crypto.Signer
}
// generateCASecret generates a new self-signed CA certificate and private key
// for signing NATS cluster leaf certificates, PEM-encoded into a single
// bundle for storage in the crypto_keys secret column.
//
// anchorTime is the key row's starts_at (which may be in the future for a
// rotated-in key). keyDuration is the rotator's key duration: the row stays the
// active signer for that long. The certificate stays valid for NATSCAOverlap
// past that window so that, once the next CA becomes the active signer, this CA
// is still valid while replicas' key caches refresh onto the new one. Leaves
// are separately clamped to expire before this NotAfter (see coderd/x/nats
// mintLeaf), so the overlap only needs to cover the cache-refresh transition.
func generateCASecret(anchorTime time.Time, keyDuration time.Duration) (string, error) {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return "", xerrors.Errorf("generate key: %w", err)
}
// 128-bit random serial per CA/Browser Forum conventions.
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return "", xerrors.Errorf("generate serial: %w", err)
}
template := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{
CommonName: "coder-nats-ca",
},
NotBefore: anchorTime.Add(-clockSkewTolerance),
NotAfter: anchorTime.Add(keyDuration + NATSCAOverlap),
KeyUsage: x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLenZero: true,
}
der, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
if err != nil {
return "", xerrors.Errorf("create certificate: %w", err)
}
keyDER, err := x509.MarshalECPrivateKey(key)
if err != nil {
return "", xerrors.Errorf("marshal private key: %w", err)
}
var secret []byte
secret = append(secret, pem.EncodeToMemory(&pem.Block{Type: caCertPEMBlockType, Bytes: der})...)
secret = append(secret, pem.EncodeToMemory(&pem.Block{Type: caKeyPEMBlockType, Bytes: keyDER})...)
return string(secret), nil
}
// parseCASecret parses a PEM bundle produced by generateCASecret back into
// the CA certificate and private key.
func parseCASecret(secret string) (*x509.Certificate, crypto.Signer, error) {
var (
cert *x509.Certificate
key *ecdsa.PrivateKey
)
rest := []byte(secret)
for {
var block *pem.Block
block, rest = pem.Decode(rest)
if block == nil {
break
}
switch block.Type {
case caCertPEMBlockType:
if cert != nil {
return nil, nil, xerrors.New("multiple certificates in CA secret")
}
var err error
cert, err = x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, nil, xerrors.Errorf("parse certificate: %w", err)
}
case caKeyPEMBlockType:
if key != nil {
return nil, nil, xerrors.New("multiple private keys in CA secret")
}
var err error
key, err = x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return nil, nil, xerrors.Errorf("parse private key: %w", err)
}
default:
return nil, nil, xerrors.Errorf("unexpected PEM block type: %q", block.Type)
}
}
if cert == nil {
return nil, nil, xerrors.New("no certificate in CA secret")
}
if key == nil {
return nil, nil, xerrors.New("no private key in CA secret")
}
if !key.PublicKey.Equal(cert.PublicKey) {
return nil, nil, xerrors.New("private key does not match certificate")
}
// Reject a structurally valid bundle whose certificate cannot act as a
// signing CA. Without this, a corrupted secret could yield a non-CA cert
// that silently becomes the active signer; leaves signed under it would
// then fail x509 verification on every replica.
if !cert.IsCA || !cert.BasicConstraintsValid || cert.KeyUsage&x509.KeyUsageCertSign == 0 {
return nil, nil, xerrors.New("certificate is not a valid signing CA")
}
return cert, key, nil
}