|
|
@@ -1,12 +1,14 @@
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
-// Package tlstest contains code to help test Tailscale's client proxy support.
|
|
|
+// 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"
|
|
|
@@ -19,32 +21,47 @@ import (
|
|
|
"time"
|
|
|
)
|
|
|
|
|
|
-// Some baked-in ECDSA keys to speed up tests, not having to burn CPU to
|
|
|
-// generate them each time. We only make the certs (which have expiry times)
|
|
|
-// at runtime.
|
|
|
+// TestRootCA returns a self-signed ECDSA root CA certificate (as PEM) for
|
|
|
+// testing purposes.
|
|
|
//
|
|
|
-// They were made with:
|
|
|
+// Typical use in a test is like:
|
|
|
//
|
|
|
-// openssl ecparam -name prime256v1 -genkey -noout -out root-ca.key
|
|
|
+// 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 (
|
|
|
- //go:embed testdata/root-ca.key
|
|
|
- rootCAKeyPEM []byte
|
|
|
-
|
|
|
- // TestProxyServerKey is the PEM private key for [TestProxyServerCert].
|
|
|
- //
|
|
|
- //go:embed testdata/proxy.tstest.key
|
|
|
- TestProxyServerKey []byte
|
|
|
-
|
|
|
- // TestControlPlaneKey is the PEM private key for [TestControlPlaneCert].
|
|
|
- //
|
|
|
- //go:embed testdata/controlplane.tstest.key
|
|
|
- TestControlPlaneKey []byte
|
|
|
+ mu sync.Mutex
|
|
|
+ privateKeys = make(map[string][]byte) // domain -> private key PEM
|
|
|
)
|
|
|
|
|
|
-// TestRootCA returns a self-signed ECDSA root CA certificate (as PEM) for
|
|
|
-// testing purposes.
|
|
|
-func TestRootCA() []byte {
|
|
|
- return bytes.Clone(testRootCAOncer())
|
|
|
+// 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 {
|
|
|
@@ -81,7 +98,7 @@ func pemCert(der []byte) []byte {
|
|
|
}
|
|
|
|
|
|
var rootCAKey = sync.OnceValue(func() *ecdsa.PrivateKey {
|
|
|
- return mustParsePEM(rootCAKeyPEM, x509.ParseECPrivateKey)
|
|
|
+ return mustParsePEM(privateKey(caDomain), x509.ParseECPrivateKey)
|
|
|
})
|
|
|
|
|
|
func mustParsePEM[T any](pemBytes []byte, parse func([]byte) (T, error)) T {
|
|
|
@@ -96,16 +113,27 @@ func mustParsePEM[T any](pemBytes []byte, parse func([]byte) (T, error)) T {
|
|
|
return v
|
|
|
}
|
|
|
|
|
|
-// KeyPair is a simple struct to hold a certificate and its private key.
|
|
|
-type KeyPair struct {
|
|
|
- Domain string
|
|
|
- KeyPEM []byte // PEM-encoded private key
|
|
|
-}
|
|
|
+// 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 (p KeyPair) ServerTLSConfig() *tls.Config {
|
|
|
- cert, err := tls.X509KeyPair(p.CertPEM(), p.KeyPEM)
|
|
|
+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())
|
|
|
}
|
|
|
@@ -114,24 +142,16 @@ func (p KeyPair) ServerTLSConfig() *tls.Config {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// ProxyServerKeyPair is a KeyPair for a test control plane server
|
|
|
-// with domain name "proxy.tstest".
|
|
|
-var ProxyServerKeyPair = KeyPair{
|
|
|
- Domain: "proxy.tstest",
|
|
|
- KeyPEM: TestProxyServerKey,
|
|
|
-}
|
|
|
-
|
|
|
-// ControlPlaneKeyPair is a KeyPair for a test control plane server
|
|
|
-// with domain name "controlplane.tstest".
|
|
|
-var ControlPlaneKeyPair = KeyPair{
|
|
|
- Domain: "controlplane.tstest",
|
|
|
- KeyPEM: TestControlPlaneKey,
|
|
|
+// KeyPEM returns a PEM-encoded private key for the domain.
|
|
|
+func (d Domain) KeyPEM() []byte {
|
|
|
+ return privateKey(string(d))
|
|
|
}
|
|
|
|
|
|
-func (p KeyPair) CertPEM() []byte {
|
|
|
+// CertPEM returns a PEM-encoded certificate for the domain.
|
|
|
+func (d Domain) CertPEM() []byte {
|
|
|
caCert := mustParsePEM(TestRootCA(), x509.ParseCertificate)
|
|
|
- caPriv := mustParsePEM(rootCAKeyPEM, x509.ParseECPrivateKey)
|
|
|
- leafKey := mustParsePEM(p.KeyPEM, x509.ParseECPrivateKey)
|
|
|
+ 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 {
|
|
|
@@ -141,14 +161,14 @@ func (p KeyPair) CertPEM() []byte {
|
|
|
now := time.Now().Add(-time.Hour)
|
|
|
tpl := &x509.Certificate{
|
|
|
SerialNumber: serial,
|
|
|
- Subject: pkix.Name{CommonName: p.Domain},
|
|
|
+ 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{p.Domain},
|
|
|
+ DNSNames: []string{string(d)},
|
|
|
}
|
|
|
|
|
|
der, err := x509.CreateCertificate(rand.Reader, tpl, caCert, &leafKey.PublicKey, caPriv)
|