Files
mesh-catalog/modules/brother/cmd/brother/certcheck_test.go
T

161 lines
5.7 KiB
Go

package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"math/big"
"net"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/health"
)
// selfSigned is a self-signed certificate over the key.
func selfSigned(t *testing.T, key any, pub any) tls.Certificate {
t.Helper()
tpl := &x509.Certificate{SerialNumber: big.NewInt(time.Now().UnixNano()), Subject: pkix.Name{CommonName: "printer.test"},
NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(24 * time.Hour), DNSNames: []string{"printer.test"}}
der, err := x509.CreateCertificate(rand.Reader, tpl, tpl, pub, key)
if err != nil {
t.Fatal(err)
}
return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key}
}
// twoCertPrinter is a TLS server with an ECDSA and an RSA certificate, as the printer has; it answers its address
// and the two fingerprints.
func twoCertPrinter(t *testing.T) (addr, ecdsaFP, rsaFP string) {
t.Helper()
ek, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
rk, _ := rsa.GenerateKey(rand.Reader, 2048)
ec, rc := selfSigned(t, ek, &ek.PublicKey), selfSigned(t, rk, &rk.PublicKey)
ln, err := tls.Listen("tcp", "127.0.0.1:0", &tls.Config{Certificates: []tls.Certificate{ec, rc}})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { ln.Close() })
go func() {
for {
c, err := ln.Accept()
if err != nil {
return
}
go func() { c.(*tls.Conn).Handshake(); c.Close() }()
}
}()
return ln.Addr().String(), Fingerprint(ec.Certificate[0]), Fingerprint(rc.Certificate[0])
}
// 1510: both certificates pinned is healthy; a changed one is unhealthy and names the new fingerprint; none
// pinned names both.
func TestTheHealthComparesBothServedCertificatesWithThePins(t *testing.T) {
addr, e, r := twoCertPrinter(t)
ctx := context.Background()
if h := CheckCertificates(ctx, addr, e+", "+r); !h.Healthy || len(h.Served) != 2 || !h.Served[0].Pinned || !h.Served[1].Pinned {
t.Errorf("both pinned: %+v", h)
}
old := strings.Repeat("AB:", 31) + "AB"
h := CheckCertificates(ctx, addr, e+", "+old) // the RSA certificate changed
if h.Healthy || !strings.Contains(h.Why, r) || strings.Contains(h.Why, "ecdsa certificate is now") {
t.Errorf("rsa changed: %+v", h)
}
h = CheckCertificates(ctx, addr, old+", "+r) // the ECDSA certificate changed
if h.Healthy || !strings.Contains(h.Why, e) || strings.Contains(h.Why, "rsa certificate is now") {
t.Errorf("ecdsa changed: %+v", h)
}
h = CheckCertificates(ctx, addr, old)
if h.Healthy || !strings.Contains(h.Why, e) || !strings.Contains(h.Why, r) || !strings.Contains(h.Why, old) {
t.Errorf("none pinned: %+v", h)
}
}
// 1510: a printer that does not answer is not an unhealthy module: nothing can be compared, and the answer says so.
func TestAPrinterThatDoesNotAnswerIsNotUnhealthy(t *testing.T) {
ln, _ := net.Listen("tcp", "127.0.0.1:0")
addr := ln.Addr().String()
ln.Close()
h := CheckCertificates(context.Background(), addr, strings.Repeat("AB:", 31)+"AB")
if !h.Healthy || !strings.Contains(h.Why, "none was compared") {
t.Errorf("%+v", h)
}
}
// 1510: brother_health is a tool of the module, read from the settings at each call.
func TestBrotherHealthIsAToolThatReadsTheSettings(t *testing.T) {
addr, e, r := twoCertPrinter(t)
js := `{"printer":"` + addr + `","printer-certificate":"` + e + `, ` + r + `"}`
run := tool(t, newRig(t, js, "pw").tl, "brother_health").Run
got, err := run(map[string]any{})
if err != nil {
t.Fatal(err)
}
if h, ok := got.(CertHealth); !ok || !h.Healthy {
t.Errorf("answer = %+v", got)
}
bad := `{"printer":"` + addr + `","printer-certificate":"` + strings.Repeat("AB:", 31) + `AB"}`
got, _ = tool(t, newRig(t, bad, "pw").tl, "brother_health").Run(map[string]any{})
if h := got.(CertHealth); h.Healthy {
t.Errorf("a wrong pin is healthy: %+v", h)
}
}
// 1510: with the printer not given the answer is waits, read by the SDK as the engine reads it: state waits, each
// wait naming exactly one setting, a part and what is wanted; not an error and not unhealthy.
func TestHealthWaitsForTheSettingsTheOperatorHasYetToGive(t *testing.T) {
for js, want := range map[string][]string{`{}`: {"printer", "printer-certificate"}, `{"printer":"192.0.2.10"}`: {"printer-certificate"}} {
got, err := tool(t, newRig(t, js, "pw").tl, "brother_health").Run(map[string]any{})
if err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(got)
a, err := health.Read(raw)
if err != nil || a.State != health.StateWaits || len(a.Waits) != len(want) {
t.Fatalf("%s: read as %+v, %v", js, a, err)
}
for i, w := range a.Waits {
if w.Setting != want[i] || w.Check() != nil {
t.Errorf("%s: wait %d = %+v (%v)", js, i, w, w.Check())
}
}
}
}
// 1510: each handshake is bounded by 3 s and the whole check by 7 s, and a printer that accepts and never answers
// ends the check at the bound with the module healthy (nothing compared).
func TestTheCheckIsBoundedAndAHungPrinterEndsItAtTheBound(t *testing.T) {
if certDial != 3*time.Second || certTotal != 7*time.Second {
t.Fatalf("bounds = %s and %s, want 3 s and 7 s", certDial, certTotal)
}
ln, _ := net.Listen("tcp", "127.0.0.1:0")
defer ln.Close()
go func() {
for {
c, err := ln.Accept()
if err != nil {
return
}
defer c.Close() // held open, never answered
}
}()
oldD, oldT := certDial, certTotal
certDial, certTotal = 200*time.Millisecond, 500*time.Millisecond
t.Cleanup(func() { certDial, certTotal = oldD, oldT })
start := time.Now()
h := CheckCertificates(context.Background(), ln.Addr().String(), strings.Repeat("AB:", 31)+"AB")
if took := time.Since(start); took > 700*time.Millisecond {
t.Errorf("the check took %s", took)
}
if !h.Healthy || !strings.Contains(h.Why, "none was compared") {
t.Errorf("%+v", h)
}
}