tailscale/prober/tls_test.go

237 lines
6.7 KiB
Go

// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package prober
import (
"bytes"
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"golang.org/x/crypto/ocsp"
)
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(), 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",
},
{
"valid duration but no OCSP",
func() *x509.Certificate { return &leafCert },
"no OCSP server presented in leaf cert for CN=tlsprobe.test",
},
} {
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 ocspServer struct {
issuer *x509.Certificate
responderCert *x509.Certificate
template *ocsp.Response
priv crypto.Signer
}
func (s *ocspServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if s.template == nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
resp, err := ocsp.CreateResponse(s.issuer, s.responderCert, *s.template, s.priv)
if err != nil {
panic(err)
}
w.Write(resp)
}
func TestOCSP(t *testing.T) {
issuerKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
t.Fatal(err)
}
issuerBytes, err := x509.CreateCertificate(rand.Reader, &issuerCertTpl, &issuerCertTpl, &issuerKey.PublicKey, issuerKey)
if err != nil {
t.Fatal(err)
}
issuerCert, err := x509.ParseCertificate(issuerBytes)
if err != nil {
t.Fatal(err)
}
responderKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
t.Fatal(err)
}
// issuer cert template re-used here, but with a different key
responderBytes, err := x509.CreateCertificate(rand.Reader, &issuerCertTpl, &issuerCertTpl, &responderKey.PublicKey, responderKey)
if err != nil {
t.Fatal(err)
}
responderCert, err := x509.ParseCertificate(responderBytes)
if err != nil {
t.Fatal(err)
}
handler := &ocspServer{
issuer: issuerCert,
responderCert: responderCert,
priv: issuerKey,
}
srv := httptest.NewUnstartedServer(handler)
srv.Start()
defer srv.Close()
cert := leafCert
cert.OCSPServer = append(cert.OCSPServer, srv.URL)
key, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
t.Fatal(err)
}
certBytes, err := x509.CreateCertificate(rand.Reader, &cert, issuerCert, &key.PublicKey, issuerKey)
if err != nil {
t.Fatal(err)
}
parsed, err := x509.ParseCertificate(certBytes)
if err != nil {
t.Fatal(err)
}
for _, tt := range []struct {
name string
resp *ocsp.Response
wantErr string
}{
{"good response", &ocsp.Response{Status: ocsp.Good}, ""},
{"unknown response", &ocsp.Response{Status: ocsp.Unknown}, "unknown OCSP verification status for CN=tlsprobe.test"},
{"revoked response", &ocsp.Response{Status: ocsp.Revoked}, "cert for CN=tlsprobe.test has been revoked"},
{"error 500 from ocsp", nil, "non-200 status code from OCSP"},
} {
t.Run(tt.name, func(t *testing.T) {
handler.template = tt.resp
if handler.template != nil {
handler.template.SerialNumber = big.NewInt(1337)
}
cs := &tls.ConnectionState{PeerCertificates: []*x509.Certificate{parsed, issuerCert}}
err := validateConnState(context.Background(), cs)
if err == nil && tt.wantErr == "" {
return
}
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
t.Errorf("unexpected error %q; want %q", err, tt.wantErr)
}
})
}
}