mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 08:11:32 +01:00 
			
		
		
		
	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>
		
			
				
	
	
		
			188 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			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
 | |
| }
 |