mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-11-04 10:11:18 +01:00 
			
		
		
		
	Updates https://github.com/tailscale/corp/issues/28569 Signed-off-by: Andrew Lytvynov <awly@tailscale.com>
		
			
				
	
	
		
			340 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			340 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright (c) Tailscale Inc & AUTHORS
 | 
						|
// SPDX-License-Identifier: BSD-3-Clause
 | 
						|
 | 
						|
package prober
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"context"
 | 
						|
	"crypto/ecdsa"
 | 
						|
	"crypto/rand"
 | 
						|
	"crypto/rsa"
 | 
						|
	"crypto/tls"
 | 
						|
	"crypto/x509"
 | 
						|
	"crypto/x509/pkix"
 | 
						|
	"encoding/pem"
 | 
						|
	"math/big"
 | 
						|
	"net"
 | 
						|
	"net/http"
 | 
						|
	"net/http/httptest"
 | 
						|
	"strings"
 | 
						|
	"testing"
 | 
						|
	"time"
 | 
						|
)
 | 
						|
 | 
						|
var leafCert = x509.Certificate{
 | 
						|
	SerialNumber:       big.NewInt(10001),
 | 
						|
	Subject:            pkix.Name{CommonName: "tlsprobe.test"},
 | 
						|
	SignatureAlgorithm: x509.SHA256WithRSA,
 | 
						|
	PublicKeyAlgorithm: x509.RSA,
 | 
						|
	Version:            3,
 | 
						|
	IPAddresses:        []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
 | 
						|
	NotBefore:          time.Now().Add(-5 * time.Minute),
 | 
						|
	NotAfter:           time.Now().Add(60 * 24 * time.Hour),
 | 
						|
	SubjectKeyId:       []byte{1, 2, 3},
 | 
						|
	AuthorityKeyId:     []byte{1, 2, 3, 4, 5}, // issuerCert below
 | 
						|
	ExtKeyUsage:        []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
 | 
						|
	KeyUsage:           x509.KeyUsageDigitalSignature,
 | 
						|
}
 | 
						|
 | 
						|
var issuerCertTpl = x509.Certificate{
 | 
						|
	SerialNumber:       big.NewInt(10002),
 | 
						|
	Subject:            pkix.Name{CommonName: "tlsprobe.ca.test"},
 | 
						|
	SignatureAlgorithm: x509.SHA256WithRSA,
 | 
						|
	PublicKeyAlgorithm: x509.RSA,
 | 
						|
	Version:            3,
 | 
						|
	IPAddresses:        []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
 | 
						|
	NotBefore:          time.Now().Add(-5 * time.Minute),
 | 
						|
	NotAfter:           time.Now().Add(60 * 24 * time.Hour),
 | 
						|
	SubjectKeyId:       []byte{1, 2, 3, 4, 5},
 | 
						|
	ExtKeyUsage:        []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
 | 
						|
	KeyUsage:           x509.KeyUsageDigitalSignature,
 | 
						|
}
 | 
						|
 | 
						|
func simpleCert() (tls.Certificate, error) {
 | 
						|
	certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
 | 
						|
	if err != nil {
 | 
						|
		return tls.Certificate{}, err
 | 
						|
	}
 | 
						|
	certPrivKeyPEM := new(bytes.Buffer)
 | 
						|
	pem.Encode(certPrivKeyPEM, &pem.Block{
 | 
						|
		Type:  "RSA PRIVATE KEY",
 | 
						|
		Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey),
 | 
						|
	})
 | 
						|
	certBytes, err := x509.CreateCertificate(rand.Reader, &leafCert, &leafCert, &certPrivKey.PublicKey, certPrivKey)
 | 
						|
	if err != nil {
 | 
						|
		return tls.Certificate{}, err
 | 
						|
	}
 | 
						|
	certPEM := new(bytes.Buffer)
 | 
						|
	pem.Encode(certPEM, &pem.Block{
 | 
						|
		Type:  "CERTIFICATE",
 | 
						|
		Bytes: certBytes,
 | 
						|
	})
 | 
						|
	return tls.X509KeyPair(certPEM.Bytes(), certPrivKeyPEM.Bytes())
 | 
						|
}
 | 
						|
 | 
						|
func TestTLSConnection(t *testing.T) {
 | 
						|
	crt, err := simpleCert()
 | 
						|
	if err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
	srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
 | 
						|
	srv.TLS = &tls.Config{Certificates: []tls.Certificate{crt}}
 | 
						|
	srv.StartTLS()
 | 
						|
	defer srv.Close()
 | 
						|
 | 
						|
	err = probeTLS(context.Background(), &tls.Config{ServerName: "fail.example.com"}, srv.Listener.Addr().String())
 | 
						|
	// The specific error message here is platform-specific ("certificate is not trusted"
 | 
						|
	// on macOS and "certificate signed by unknown authority" on Linux), so only check
 | 
						|
	// that it contains the word 'certificate'.
 | 
						|
	if err == nil || !strings.Contains(err.Error(), "certificate") {
 | 
						|
		t.Errorf("unexpected error: %q", err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestCertExpiration(t *testing.T) {
 | 
						|
	for _, tt := range []struct {
 | 
						|
		name    string
 | 
						|
		cert    func() *x509.Certificate
 | 
						|
		wantErr string
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			"cert not valid yet",
 | 
						|
			func() *x509.Certificate {
 | 
						|
				c := leafCert
 | 
						|
				c.NotBefore = time.Now().Add(time.Hour)
 | 
						|
				return &c
 | 
						|
			},
 | 
						|
			"one of the certs has NotBefore in the future",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			"cert expiring soon",
 | 
						|
			func() *x509.Certificate {
 | 
						|
				c := leafCert
 | 
						|
				c.NotAfter = time.Now().Add(time.Hour)
 | 
						|
				return &c
 | 
						|
			},
 | 
						|
			"one of the certs expires in",
 | 
						|
		},
 | 
						|
	} {
 | 
						|
		t.Run(tt.name, func(t *testing.T) {
 | 
						|
			cs := &tls.ConnectionState{PeerCertificates: []*x509.Certificate{tt.cert()}}
 | 
						|
			err := validateConnState(context.Background(), cs)
 | 
						|
			if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
 | 
						|
				t.Errorf("unexpected error %q; want %q", err, tt.wantErr)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
type CRLServer struct {
 | 
						|
	crlBytes []byte
 | 
						|
}
 | 
						|
 | 
						|
func (s *CRLServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
						|
	if s.crlBytes == nil {
 | 
						|
		w.WriteHeader(http.StatusInternalServerError)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	w.Header().Set("Content-Type", "application/pkix-crl")
 | 
						|
	w.WriteHeader(http.StatusOK)
 | 
						|
	w.Write(s.crlBytes)
 | 
						|
}
 | 
						|
 | 
						|
// someECDSAKey{1,2,3} are different EC private keys in PEM format
 | 
						|
// as generated by:
 | 
						|
//
 | 
						|
//	openssl ecparam -name prime256v1 -genkey -noout -out -
 | 
						|
//
 | 
						|
// They're used in tests to avoid burning CPU at test time to just
 | 
						|
// to make some arbitrary test keys.
 | 
						|
const (
 | 
						|
	someECDSAKey1 = `
 | 
						|
-----BEGIN EC PRIVATE KEY-----
 | 
						|
MHcCAQEEIDKggO47Si0/JgqF0q9m0HfQ92lbERWsBaKS5YihtuheoAoGCCqGSM49
 | 
						|
AwEHoUQDQgAE/JtNZkfFmAGQJHW5Xgz0Eoyi9MKVxl77sXjIFDMX233QDIWPEM/B
 | 
						|
vmNMvdFkuYBjwbq6H+SNf1NXRNladEGU/Q==
 | 
						|
-----END EC PRIVATE KEY-----
 | 
						|
`
 | 
						|
	someECDSAKey2 = `
 | 
						|
-----BEGIN EC PRIVATE KEY-----
 | 
						|
MHcCAQEEIPIJhRf4MpzLil1ZKcRqMx+jPeJXw96KtYYzV2AcgBzgoAoGCCqGSM49
 | 
						|
AwEHoUQDQgAEhA9CSWFmUvdvXMzyt+as+6f+0luydHU1x/gEksVByYIgYxahaGts
 | 
						|
xbSKj6F2WgAN/ok1gFLqhH3UWMNVthM1wA==
 | 
						|
-----END EC PRIVATE KEY-----
 | 
						|
`
 | 
						|
	someECDSAKey3 = `
 | 
						|
-----BEGIN EC PRIVATE KEY-----
 | 
						|
MHcCAQEEIKgZ1OJjK2St9O0i52N1K+IgSiu2/NSMk9Yt2+kDMHd7oAoGCCqGSM49
 | 
						|
AwEHoUQDQgAExFp80etkjy/AEUtSgJjXRA39jTU7eiEmCGRREewFQhwcEscBEfrg
 | 
						|
6NN31r9YlEs+hZ8gXE1L3Deu6jn5jW3pig==
 | 
						|
-----END EC PRIVATE KEY-----
 | 
						|
`
 | 
						|
)
 | 
						|
 | 
						|
// parseECKey parses an EC private key from a PEM-encoded string.
 | 
						|
func parseECKey(t *testing.T, pemPriv string) *ecdsa.PrivateKey {
 | 
						|
	t.Helper()
 | 
						|
	block, _ := pem.Decode([]byte(pemPriv))
 | 
						|
	if block == nil {
 | 
						|
		t.Fatal("failed to decode PEM")
 | 
						|
	}
 | 
						|
	key, err := x509.ParseECPrivateKey(block.Bytes)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatalf("failed to parse EC key: %v", err)
 | 
						|
	}
 | 
						|
	return key
 | 
						|
}
 | 
						|
 | 
						|
func TestCRL(t *testing.T) {
 | 
						|
	// Generate CA key and self-signed CA cert
 | 
						|
	caKey := parseECKey(t, someECDSAKey1)
 | 
						|
 | 
						|
	caTpl := issuerCertTpl
 | 
						|
	caTpl.BasicConstraintsValid = true
 | 
						|
	caTpl.IsCA = true
 | 
						|
	caTpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature
 | 
						|
	caTpl.SignatureAlgorithm = x509.ECDSAWithSHA256
 | 
						|
	caBytes, err := x509.CreateCertificate(rand.Reader, &caTpl, &caTpl, &caKey.PublicKey, caKey)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
	caCert, err := x509.ParseCertificate(caBytes)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
 | 
						|
	// Issue a leaf cert signed by the CA
 | 
						|
	leaf := leafCert
 | 
						|
	leaf.SerialNumber = big.NewInt(20001)
 | 
						|
	leaf.SignatureAlgorithm = x509.ECDSAWithSHA256
 | 
						|
	leaf.Issuer = caCert.Subject
 | 
						|
	leafKey := parseECKey(t, someECDSAKey2)
 | 
						|
	leafBytes, err := x509.CreateCertificate(rand.Reader, &leaf, caCert, &leafKey.PublicKey, caKey)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
	leafCertParsed, err := x509.ParseCertificate(leafBytes)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
 | 
						|
	// Catch no CRL set by Let's Encrypt date.
 | 
						|
	noCRLCert := leafCert
 | 
						|
	noCRLCert.SerialNumber = big.NewInt(20002)
 | 
						|
	noCRLCert.CRLDistributionPoints = []string{}
 | 
						|
	noCRLCert.NotBefore = time.Unix(letsEncryptStartedStaplingCRL, 0).Add(-48 * time.Hour)
 | 
						|
	noCRLCert.Issuer = caCert.Subject
 | 
						|
	noCRLCert.SignatureAlgorithm = x509.ECDSAWithSHA256
 | 
						|
	noCRLCertKey := parseECKey(t, someECDSAKey3)
 | 
						|
	noCRLStapledBytes, err := x509.CreateCertificate(rand.Reader, &noCRLCert, caCert, &noCRLCertKey.PublicKey, caKey)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
	noCRLStapledParsed, err := x509.ParseCertificate(noCRLStapledBytes)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
 | 
						|
	crlServer := &CRLServer{crlBytes: nil}
 | 
						|
	srv := httptest.NewServer(crlServer)
 | 
						|
	defer srv.Close()
 | 
						|
 | 
						|
	// Create a CRL that revokes the leaf cert using x509.CreateRevocationList
 | 
						|
	now := time.Now()
 | 
						|
	revoked := []x509.RevocationListEntry{{
 | 
						|
		SerialNumber:   leaf.SerialNumber,
 | 
						|
		RevocationTime: now,
 | 
						|
		ReasonCode:     1, // Key compromise
 | 
						|
	}}
 | 
						|
	rl := x509.RevocationList{
 | 
						|
		SignatureAlgorithm:        caCert.SignatureAlgorithm,
 | 
						|
		Issuer:                    caCert.Subject,
 | 
						|
		ThisUpdate:                now,
 | 
						|
		NextUpdate:                now.Add(24 * time.Hour),
 | 
						|
		RevokedCertificateEntries: revoked,
 | 
						|
		Number:                    big.NewInt(1),
 | 
						|
	}
 | 
						|
	rlBytes, err := x509.CreateRevocationList(rand.Reader, &rl, caCert, caKey)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
 | 
						|
	emptyRlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{Number: big.NewInt(2)}, caCert, caKey)
 | 
						|
	if err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tt := range []struct {
 | 
						|
		name     string
 | 
						|
		cert     *x509.Certificate
 | 
						|
		crlBytes []byte
 | 
						|
		issuer   pkix.Name
 | 
						|
		wantErr  string
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			"ValidCert",
 | 
						|
			leafCertParsed,
 | 
						|
			emptyRlBytes,
 | 
						|
			caCert.Issuer,
 | 
						|
			"",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			"RevokedCert",
 | 
						|
			leafCertParsed,
 | 
						|
			rlBytes,
 | 
						|
			caCert.Issuer,
 | 
						|
			"has been revoked on",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			"EmptyCRL",
 | 
						|
			leafCertParsed,
 | 
						|
			emptyRlBytes,
 | 
						|
			caCert.Issuer,
 | 
						|
			"",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			"NoCRLLetsEncrypt",
 | 
						|
			leafCertParsed,
 | 
						|
			nil,
 | 
						|
			pkix.Name{CommonName: "tlsprobe.test", Organization: []string{"Let's Encrypt"}},
 | 
						|
			"no CRL server presented in leaf cert for",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			"NoCRLOtherCA",
 | 
						|
			leafCertParsed,
 | 
						|
			nil,
 | 
						|
			caCert.Issuer,
 | 
						|
			"",
 | 
						|
		},
 | 
						|
		{
 | 
						|
			"NotBeforeCRLStaplingDate",
 | 
						|
			noCRLStapledParsed,
 | 
						|
			nil,
 | 
						|
			caCert.Issuer,
 | 
						|
			"",
 | 
						|
		},
 | 
						|
	} {
 | 
						|
		t.Run(tt.name, func(t *testing.T) {
 | 
						|
			tt.cert.Issuer = tt.issuer
 | 
						|
			cs := &tls.ConnectionState{PeerCertificates: []*x509.Certificate{tt.cert, caCert}}
 | 
						|
			if tt.crlBytes != nil {
 | 
						|
				crlServer.crlBytes = tt.crlBytes
 | 
						|
				tt.cert.CRLDistributionPoints = []string{srv.URL}
 | 
						|
			} else {
 | 
						|
				crlServer.crlBytes = nil
 | 
						|
				tt.cert.CRLDistributionPoints = []string{}
 | 
						|
			}
 | 
						|
			err := validateConnState(context.Background(), cs)
 | 
						|
 | 
						|
			if err == nil && tt.wantErr == "" {
 | 
						|
				return
 | 
						|
			}
 | 
						|
 | 
						|
			if err == nil || tt.wantErr == "" || !strings.Contains(err.Error(), tt.wantErr) {
 | 
						|
				t.Errorf("unexpected error %q; want %q", err, tt.wantErr)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 |