Scert/backend/internal/nginxactivation/probe_test.go

227 lines
7.3 KiB
Go

package nginxactivation
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"io"
"log"
"math/big"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"time"
)
const testHostname = "management.example.test"
type testAuthority struct {
certificate *x509.Certificate
key *ecdsa.PrivateKey
roots *x509.CertPool
}
type testMaterial struct {
chain []byte
key []byte
pair tls.Certificate
}
func newAuthority(t *testing.T) testAuthority {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
template := &x509.Certificate{
SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "Scert test CA"},
NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(365 * 24 * time.Hour),
IsCA: true, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign,
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
t.Fatal(err)
}
certificate, err := x509.ParseCertificate(der)
if err != nil {
t.Fatal(err)
}
roots := x509.NewCertPool()
roots.AddCert(certificate)
return testAuthority{certificate: certificate, key: key, roots: roots}
}
func (a testAuthority) issue(t *testing.T, hostname string, before, after time.Time) testMaterial {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 120))
if err != nil {
t.Fatal(err)
}
template := &x509.Certificate{
SerialNumber: serial, DNSNames: []string{hostname}, NotBefore: before, NotAfter: after,
KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
der, err := x509.CreateCertificate(rand.Reader, template, a.certificate, &key.PublicKey, a.key)
if err != nil {
t.Fatal(err)
}
keyDER, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
t.Fatal(err)
}
material := testMaterial{
chain: pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}),
key: pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}),
}
material.chain = append(material.chain, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: a.certificate.Raw})...)
material.pair, err = tls.X509KeyPair(material.chain, material.key)
if err != nil {
t.Fatal(err)
}
return material
}
type testEndpoint struct {
Endpoint
active atomic.Pointer[tls.Certificate]
server *httptest.Server
}
func newTestEndpoint(t *testing.T, a testAuthority, material testMaterial, handler http.Handler) *testEndpoint {
t.Helper()
if handler == nil {
handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/readyz" || r.Method != http.MethodGet || r.TLS.ServerName != testHostname || !strings.HasPrefix(r.Host, testHostname+":") {
http.Error(w, "请求无效", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = io.WriteString(w, `{"service":"scert","status":"ready"}`)
})
}
e := &testEndpoint{}
e.active.Store(&material.pair)
e.server = httptest.NewUnstartedServer(handler)
e.server.Config.ErrorLog = log.New(io.Discard, "", 0)
e.server.TLS = &tls.Config{MinVersion: tls.VersionTLS12, GetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
return e.active.Load(), nil
}}
e.server.StartTLS()
t.Cleanup(e.server.Close)
e.Endpoint = Endpoint{Address: e.server.Listener.Addr().String(), Hostname: testHostname, Roots: a.roots}
return e
}
func TestEndpointStrictTLSAndReadiness(t *testing.T) {
a := newAuthority(t)
now := time.Now()
material := a.issue(t, testHostname, now.Add(-time.Hour), now.Add(24*time.Hour))
other := a.issue(t, testHostname, now.Add(-time.Hour), now.Add(48*time.Hour))
for _, test := range []struct {
name string
body string
status int
accepted bool
}{
{"ready", `{"service":"scert","status":"ready"}`, 200, true},
{"wrong-service", `{"service":"other","status":"ready"}`, 200, false},
{"not-ready", `{"service":"scert","status":"starting"}`, 200, false},
{"redirect", `{"service":"scert","status":"ready"}`, 308, false},
{"unavailable", `{"service":"scert","status":"ready"}`, 503, false},
{"trailing-data", `{"service":"scert","status":"ready"}{}`, 200, false},
{"large-body", strings.Repeat(" ", 4097), 200, false},
} {
t.Run(test.name, func(t *testing.T) {
e := newTestEndpoint(t, a, material, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "/redirected")
w.WriteHeader(test.status)
_, _ = io.WriteString(w, test.body)
}))
leaf, err := e.Check(context.Background(), material.pair.Certificate[0])
if (err == nil) != test.accepted {
t.Fatalf("accepted=%v, error=%v", test.accepted, err)
}
if test.accepted && !bytes.Equal(leaf, material.pair.Certificate[0]) {
t.Fatal("叶证书不匹配")
}
})
}
e := newTestEndpoint(t, a, material, nil)
t.Setenv("HTTPS_PROXY", "http://127.0.0.1:1")
if _, err := e.Check(context.Background(), nil); err != nil {
t.Fatal(err)
}
if _, err := e.Check(context.Background(), other.pair.Certificate[0]); err == nil {
t.Fatal("不应接受其他叶证书")
}
wrongHost := e.Endpoint
wrongHost.Hostname = "other.example.test"
if _, err := wrongHost.Check(context.Background(), nil); err == nil {
t.Fatal("不应接受错误主机")
}
wrongRoot := e.Endpoint
wrongRoot.Roots = x509.NewCertPool()
if _, err := wrongRoot.Check(context.Background(), nil); err == nil {
t.Fatal("不应接受不可信证书")
}
}
func TestEndpointWaitAndCancellation(t *testing.T) {
a := newAuthority(t)
now := time.Now()
old := a.issue(t, testHostname, now.Add(-time.Hour), now.Add(24*time.Hour))
newMaterial := a.issue(t, testHostname, now.Add(-time.Hour), now.Add(48*time.Hour))
e := newTestEndpoint(t, a, old, nil)
switched := make(chan struct{})
go func() {
time.Sleep(150 * time.Millisecond)
e.active.Store(&newMaterial.pair)
close(switched)
}()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := e.Wait(ctx, newMaterial.pair.Certificate[0]); err != nil {
t.Fatal(err)
}
<-switched
cancelled, stop := context.WithCancel(context.Background())
stop()
if err := e.Wait(cancelled, old.pair.Certificate[0]); !errors.Is(err, context.Canceled) {
t.Fatalf("取消未传播:%v", err)
}
if err := e.Wait(ctx, nil); err == nil {
t.Fatal("缺少预期证书应被拒绝")
}
}
func TestEndpointRejectsUnsafePolicies(t *testing.T) {
for _, address := range []string{"example.test:443", "192.0.2.1:443", "127.0.0.1:0", "127.0.0.1:65536", "127.0.0.1:+443", "127.0.0.1:0443", "[::ffff:127.0.0.1]:443", "127.0.0.1"} {
if err := (Endpoint{Address: address, Hostname: testHostname}).validate(); err == nil {
t.Fatalf("不应接受地址 %q", address)
}
}
for _, hostname := range []string{"*.example.test", "127.0.0.1", " management.example.test", "MANAGEMENT.example.test", "example.test/path"} {
if err := (Endpoint{Address: "127.0.0.1:443", Hostname: hostname}).validate(); err == nil {
t.Fatalf("不应接受域名 %q", hostname)
}
}
for _, address := range []string{"127.0.0.1:443", "[::1]:443"} {
if err := (Endpoint{Address: address, Hostname: testHostname}).validate(); err != nil {
t.Fatal(err)
}
}
}