tailscale/tstest/tlstest/tlstest.go
Brad Fitzpatrick a64ca7a5b4 tstest/tlstest: simplify, don't even bake in any keys
I earlier thought this saved a second of CPU even on a fast machine,
but I think when I was previously measuring, I still had a 4096 bit
RSA key being generated in the code I was measuring.

Measuring again for this, it's plenty fast.

Prep for using this package more, for derp, etc.

Updates #16315

Change-Id: I4c9008efa9aa88a3d65409d6ffd7b3807f4d75e9
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2025-06-19 16:12:32 -07:00

188 lines
5.1 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package tlstest contains code to help test Tailscale's TLS support without
// depending on real WebPKI roots or certificates during tests.
package tlstest
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
_ "embed"
"encoding/pem"
"fmt"
"math/big"
"sync"
"time"
)
// TestRootCA returns a self-signed ECDSA root CA certificate (as PEM) for
// testing purposes.
//
// Typical use in a test is like:
//
// bakedroots.ResetForTest(t, tlstest.TestRootCA())
func TestRootCA() []byte {
return bytes.Clone(testRootCAOncer())
}
// cache for [privateKey], so it always returns the same key for a given domain.
var (
mu sync.Mutex
privateKeys = make(map[string][]byte) // domain -> private key PEM
)
// caDomain is a fake domain name to repreesnt the private key for the root CA.
const caDomain = "_root"
// privateKey returns a PEM-encoded test ECDSA private key for the given domain.
func privateKey(domain string) (pemBytes []byte) {
mu.Lock()
defer mu.Unlock()
if pemBytes, ok := privateKeys[domain]; ok {
return bytes.Clone(pemBytes)
}
defer func() { privateKeys[domain] = bytes.Clone(pemBytes) }()
k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(fmt.Sprintf("failed to generate ECDSA key for %q: %v", domain, err))
}
der, err := x509.MarshalECPrivateKey(k)
if err != nil {
panic(fmt.Sprintf("failed to marshal ECDSA key for %q: %v", domain, err))
}
var buf bytes.Buffer
if err := pem.Encode(&buf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: der}); err != nil {
panic(fmt.Sprintf("failed to encode PEM: %v", err))
}
return buf.Bytes()
}
var testRootCAOncer = sync.OnceValue(func() []byte {
key := rootCAKey()
now := time.Now().Add(-time.Hour)
tpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: "Tailscale Unit Test ECDSA Root",
Organization: []string{"Tailscale Test Org"},
},
NotBefore: now,
NotAfter: now.AddDate(5, 0, 0),
IsCA: true,
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
SubjectKeyId: mustSKID(&key.PublicKey),
}
der, err := x509.CreateCertificate(rand.Reader, tpl, tpl, &key.PublicKey, key)
if err != nil {
panic(err)
}
return pemCert(der)
})
func pemCert(der []byte) []byte {
var buf bytes.Buffer
if err := pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: der}); err != nil {
panic(fmt.Sprintf("failed to encode PEM: %v", err))
}
return buf.Bytes()
}
var rootCAKey = sync.OnceValue(func() *ecdsa.PrivateKey {
return mustParsePEM(privateKey(caDomain), x509.ParseECPrivateKey)
})
func mustParsePEM[T any](pemBytes []byte, parse func([]byte) (T, error)) T {
block, rest := pem.Decode(pemBytes)
if block == nil || len(rest) > 0 {
panic("invalid PEM")
}
v, err := parse(block.Bytes)
if err != nil {
panic(fmt.Sprintf("invalid PEM: %v", err))
}
return v
}
// Domain is a fake domain name used in TLS tests.
//
// They don't have real DNS records. Tests are expected to fake DNS
// lookups and dials for these domains.
type Domain string
// ProxyServer is a domain name for a hypothetical proxy server.
const (
ProxyServer = Domain("proxy.tstest")
// ControlPlane is a domain name for a test control plane server.
ControlPlane = Domain("controlplane.tstest")
// Derper is a domain name for a test DERP server.
Derper = Domain("derp.tstest")
)
// ServerTLSConfig returns a TLS configuration suitable for a server
// using the KeyPair's certificate and private key.
func (d Domain) ServerTLSConfig() *tls.Config {
cert, err := tls.X509KeyPair(d.CertPEM(), privateKey(string(d)))
if err != nil {
panic("invalid TLS key pair: " + err.Error())
}
return &tls.Config{
Certificates: []tls.Certificate{cert},
}
}
// KeyPEM returns a PEM-encoded private key for the domain.
func (d Domain) KeyPEM() []byte {
return privateKey(string(d))
}
// CertPEM returns a PEM-encoded certificate for the domain.
func (d Domain) CertPEM() []byte {
caCert := mustParsePEM(TestRootCA(), x509.ParseCertificate)
caPriv := mustParsePEM(privateKey(caDomain), x509.ParseECPrivateKey)
leafKey := mustParsePEM(d.KeyPEM(), x509.ParseECPrivateKey)
serial, err := rand.Int(rand.Reader, big.NewInt(0).Lsh(big.NewInt(1), 128))
if err != nil {
panic(err)
}
now := time.Now().Add(-time.Hour)
tpl := &x509.Certificate{
SerialNumber: serial,
Subject: pkix.Name{CommonName: string(d)},
NotBefore: now,
NotAfter: now.AddDate(2, 0, 0),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
DNSNames: []string{string(d)},
}
der, err := x509.CreateCertificate(rand.Reader, tpl, caCert, &leafKey.PublicKey, caPriv)
if err != nil {
panic(err)
}
return pemCert(der)
}
func mustSKID(pub *ecdsa.PublicKey) []byte {
skid, err := x509.MarshalPKIXPublicKey(pub)
if err != nil {
panic(err)
}
return skid[:20] // same as x509 library
}