Files
funknetz/station/protocol3_test.go
T
konradandCursor a405048f23 Write a missing signing request and give up on a handshake that never returns.
A certificate already on disk still has to be renewable, and a reflector that never finishes TLS must not hold the session past the login timers.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-07 14:36:28 +02:00

1053 lines
30 KiB
Go

package station_test
import (
"bytes"
"context"
"crypto"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/binary"
"encoding/json"
"encoding/pem"
"errors"
"math/big"
"net"
"os"
"path/filepath"
"testing"
"time"
jsonv2 "encoding/json/v2"
"golang.org/x/sys/unix"
"gitea.neitzel.de/konrad/funknetz/internal/opus"
"gitea.neitzel.de/konrad/funknetz/station"
)
func configWithDir(t testing.TB, host string, port int, callsign, authKey string, talkGroup int, certDir, logPath string) string {
t.Helper()
if logPath == "" {
logPath = "/tmp/funknetz-test.log"
}
body, err := jsonv2.Marshal(map[string]any{
"host": host,
"port": port,
"callsign": callsign,
"authKey": authKey,
"talkGroup": talkGroup,
"certificateDir": certDir,
"enableLogging": false,
"logFile": logPath,
})
if err != nil {
t.Fatal(err)
}
return string(body)
}
type testPKI struct {
caPEM []byte
caCert *x509.Certificate
caKey *rsa.PrivateKey
server tls.Certificate
sig []byte
}
func newPKI(t *testing.T, dns string, ip net.IP) *testPKI {
t.Helper()
caKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
caTmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "Test CA"},
IsCA: true,
BasicConstraintsValid: true,
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
}
caDER, err := x509.CreateCertificate(rand.Reader, caTmpl, caTmpl, &caKey.PublicKey, caKey)
if err != nil {
t.Fatal(err)
}
caCert, err := x509.ParseCertificate(caDER)
if err != nil {
t.Fatal(err)
}
caPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caDER})
srvKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
srvTmpl := &x509.Certificate{
SerialNumber: big.NewInt(2),
Subject: pkix.Name{CommonName: dns},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
if dns != "" {
srvTmpl.DNSNames = []string{dns}
}
if ip != nil {
srvTmpl.IPAddresses = []net.IP{ip}
}
srvDER, err := x509.CreateCertificate(rand.Reader, srvTmpl, caCert, &srvKey.PublicKey, caKey)
if err != nil {
t.Fatal(err)
}
srvPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: srvDER})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(srvKey)})
cert, err := tls.X509KeyPair(srvPEM, keyPEM)
if err != nil {
t.Fatal(err)
}
sum := sha256.Sum256(caPEM)
sig, err := rsa.SignPKCS1v15(rand.Reader, caKey, crypto.SHA256, sum[:])
if err != nil {
t.Fatal(err)
}
return &testPKI{caPEM: caPEM, caCert: caCert, caKey: caKey, server: cert, sig: sig}
}
func (p *testPKI) info() []byte {
sum := sha256.Sum256(p.caPEM)
b := appendU16(nil, uint16(len(p.caPEM)))
return appendBytes(b, sum[:])
}
func (p *testPKI) bundle(sig []byte) []byte {
b := appendStr(nil, string(p.caPEM))
b = appendBytes(b, sig)
return appendStr(b, string(p.caPEM))
}
func (p *testPKI) serverConfig() *tls.Config {
return &tls.Config{
Certificates: []tls.Certificate{p.server},
MinVersion: tls.VersionTLS12,
ClientAuth: tls.RequestClientCert,
}
}
func signClient(t *testing.T, p *testPKI, key *rsa.PrivateKey, cn string) []byte {
t.Helper()
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(3),
Subject: pkix.Name{CommonName: cn},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, p.caCert, &key.PublicKey, p.caKey)
if err != nil {
t.Fatal(err)
}
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
}
func writeKey(t *testing.T, path string, key *rsa.PrivateKey) {
t.Helper()
body := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
if err := os.WriteFile(path, body, 0o600); err != nil {
t.Fatal(err)
}
}
type v3 struct {
st *station.Station
clock *fakeClock
refl *reflector
pki *testPKI
certDir string
cancel context.CancelFunc
id uint16
iv []byte
key []byte
reg []byte
}
func startV3(t *testing.T, certDir string) *v3 {
t.Helper()
refl := newReflector(t)
if certDir == "" {
certDir = filepath.Join(t.TempDir(), "certs")
}
path := writeConfigFile(t, configWithDir(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20, certDir, ""))
clock := newFakeClock()
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(t.Context())
t.Cleanup(cancel)
go func() { _ = st.Run(ctx) }()
pki := newPKI(t, "", net.ParseIP("127.0.0.1"))
return &v3{st: st, clock: clock, refl: refl, pki: pki, certDir: certDir, cancel: cancel}
}
func (v *v3) throughBundle(t *testing.T) {
t.Helper()
v.refl.accept(t)
typ, body := v.refl.readFrame(t)
if typ != 5 || !versionIs(body, 3, 0) {
t.Fatalf("version type %d body %v", typ, body)
}
v.refl.writeFrame(t, 19, v.pki.info())
typ, _ = v.refl.readFrame(t)
if typ != 20 {
t.Fatalf("type %d, want a CA bundle request", typ)
}
info, err := os.Stat(v.certDir)
if err != nil {
t.Fatal(err)
}
if info.Mode().Perm() != 0o700 {
t.Fatalf("directory mode %o, want 0700", info.Mode().Perm())
}
v.refl.writeFrame(t, 21, v.pki.bundle(v.pki.sig))
typ, _ = v.refl.readFrame(t)
if typ != 14 {
t.Fatalf("type %d, want start encryption", typ)
}
}
func (v *v3) tls(t *testing.T, cfg *tls.Config) {
t.Helper()
v.refl.writeFrame(t, 15, nil)
if err := v.refl.handshake(t, cfg); err != nil {
t.Fatal(err)
}
}
func (v *v3) hmac(t *testing.T) {
t.Helper()
v.refl.writeFrame(t, 16, nil)
typ, body := v.refl.readFrame(t)
if typ != 17 {
t.Fatalf("type %d, want the signing request", typ)
}
checkCSR(t, body, v.certDir)
challenge := make([]byte, 20)
for i := range challenge {
challenge[i] = byte(i + 1)
}
v.refl.writeFrame(t, 10, appendBytes(nil, challenge))
typ, body = v.refl.readFrame(t)
if typ != 11 {
t.Fatalf("type %d, want auth response", typ)
}
call, rest := takeStr(t, body)
digest, _ := takeBytes(t, rest)
if call != "DL1ABC" {
t.Fatalf("callsign %q", call)
}
mac := hmacSum([]byte("key"), challenge)
if !bytes.Equal(digest, mac) {
t.Fatal("auth digest does not match the auth key")
}
v.refl.writeFrame(t, 12, nil)
}
func (v *v3) serverAndKey(t *testing.T, codecs []string) {
t.Helper()
v.refl.writeFrame(t, 100, serverInfo(0x1234, []string{"db0abc"}, codecs))
typ, body := v.refl.readFrame(t)
if typ != 111 {
t.Fatalf("type %d, want node information", typ)
}
iv, rest := takeBytes(t, body)
key, rest := takeBytes(t, rest)
raw, rest := takeStr(t, rest)
if len(iv) != 6 || len(key) != 16 || len(rest) != 0 {
t.Fatalf("iv %d key %d rest %d", len(iv), len(key), len(rest))
}
var got map[string]string
if err := json.Unmarshal([]byte(raw), &got); err != nil {
t.Fatal(err)
}
if got["sw"] != station.SoftwareName || got["swVer"] != station.SoftwareVersion || got["projVer"] != station.SoftwareVersion {
t.Fatalf("node json %#v", got)
}
var u unix.Utsname
if err := unix.Uname(&u); err != nil {
t.Fatal(err)
}
name := u.Machine[:]
if i := bytes.IndexByte(name, 0); i >= 0 {
name = name[:i]
}
if got["machineArch"] != string(name) {
t.Fatalf("machineArch %q, want %q", got["machineArch"], name)
}
v.iv = iv
v.key = key
v.id = 0x1234
if _, _, ok := tryFrame(v.refl); ok {
t.Fatal("selected the talk group before the UDP heartbeat")
}
v.refl.writeFrame(t, 114, nil)
reg := v.refl.readRawUDP(t)
plain := openReg(t, v.key, v.iv, v.id, reg)
if binary.BigEndian.Uint16(plain) != 1 {
t.Fatalf("registration type %d", binary.BigEndian.Uint16(plain))
}
v.reg = reg
if got := v.st.State(); got.Connection == "Connected" || got.Protocol != "" {
t.Fatalf("State = %+v, want not connected before the heartbeat", got)
}
}
func (v *v3) beat(t *testing.T) {
t.Helper()
v.refl.writeRawUDP(t, sealToStation(t, v.key, v.iv, 0, appendU16(nil, 1)))
typ, body := v.refl.readFrame(t)
if typ != 106 {
t.Fatalf("type %d, want select talk group", typ)
}
tg, _ := takeU32(t, body)
if tg != 20 {
t.Fatalf("talk group %d", tg)
}
got, err := v.st.Wait(t.Context(), func(s station.State) bool { return s.Connection == "Connected" })
if err != nil {
t.Fatal(err)
}
if got.Protocol != "3.0" || got.Callsign != "DL1ABC" || got.TalkGroup != 20 {
t.Fatalf("State = %+v", got)
}
}
func checkCSR(t *testing.T, body []byte, dir string) {
t.Helper()
raw, rest := takeStr(t, body)
if len(rest) != 0 {
t.Fatalf("%d trailing bytes", len(rest))
}
block, _ := pem.Decode([]byte(raw))
if block == nil || block.Type != "CERTIFICATE REQUEST" {
t.Fatal("signing request is not a PEM request")
}
req, err := x509.ParseCertificateRequest(block.Bytes)
if err != nil {
t.Fatal(err)
}
if err := req.CheckSignature(); err != nil {
t.Fatal(err)
}
if req.Subject.CommonName != "DL1ABC" || len(req.EmailAddresses) != 0 || len(req.DNSNames) != 0 {
t.Fatalf("subject %+v emails %v", req.Subject, req.EmailAddresses)
}
var sawBC, sawKU, sawEKU bool
for _, ext := range req.Extensions {
switch {
case ext.Id.Equal(asn1.ObjectIdentifier{2, 5, 29, 19}):
sawBC = ext.Critical
case ext.Id.Equal(asn1.ObjectIdentifier{2, 5, 29, 15}):
sawKU = ext.Critical
var bits asn1.BitString
if _, err := asn1.Unmarshal(ext.Value, &bits); err != nil {
t.Fatal(err)
}
if bits.BitLength < 5 || bits.Bytes[0]&0xA8 != 0xA8 {
t.Fatalf("key usage %x", bits.Bytes)
}
case ext.Id.Equal(asn1.ObjectIdentifier{2, 5, 29, 37}):
sawEKU = true
}
}
if !sawBC || !sawKU || !sawEKU {
t.Fatalf("extensions basic %v key %v eku %v", sawBC, sawKU, sawEKU)
}
info, err := os.Stat(filepath.Join(dir, "key.pem"))
if err != nil {
t.Fatal(err)
}
if info.Mode().Perm() != 0o600 {
t.Fatalf("key mode %o, want 0600", info.Mode().Perm())
}
keyPEM, err := os.ReadFile(filepath.Join(dir, "key.pem"))
if err != nil {
t.Fatal(err)
}
kblock, _ := pem.Decode(keyPEM)
key, err := x509.ParsePKCS1PrivateKey(kblock.Bytes)
if err != nil {
t.Fatal(err)
}
if key.N.BitLen() != 2048 {
t.Fatalf("key bits %d", key.N.BitLen())
}
}
func openReg(t *testing.T, key, iv []byte, id uint16, pkt []byte) []byte {
t.Helper()
if len(pkt) < 14 {
t.Fatalf("datagram %d", len(pkt))
}
return openGCM(t, key, iv, id, 0, pkt[:6], pkt[6:14], pkt[14:])
}
func openGCM(t *testing.T, key, iv []byte, id uint16, counter uint32, aad, tag, ct []byte) []byte {
t.Helper()
plain, err := openTruncTest(key, nonceFor(iv, id, counter), tag, ct, aad)
if err != nil {
t.Fatal(err)
}
return plain
}
func sealToStation(t *testing.T, key, iv []byte, counter uint32, plain []byte) []byte {
t.Helper()
var aad [4]byte
binary.BigEndian.PutUint32(aad[:], counter)
tag, ct, err := sealTruncTest(key, nonceFor(iv, 0, counter), plain, aad[:])
if err != nil {
t.Fatal(err)
}
wire := append([]byte{}, aad[:]...)
wire = append(wire, tag...)
return append(wire, ct...)
}
func nonceFor(iv []byte, id uint16, counter uint32) []byte {
nonce := make([]byte, 12)
copy(nonce, iv)
binary.BigEndian.PutUint16(nonce[6:8], id)
binary.BigEndian.PutUint32(nonce[8:12], counter)
return nonce
}
func sealTruncTest(key, nonce, plain, aad []byte) (tag, ct []byte, err error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, nil, err
}
out := gcm.Seal(nil, nonce, plain, aad)
return append([]byte(nil), out[len(plain):len(plain)+8]...), append([]byte(nil), out[:len(plain)]...), nil
}
func openTruncTest(key, nonce, tag, ct, aad []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
stream := gcm.Seal(nil, nonce, make([]byte, len(ct)), aad)
plain := make([]byte, len(ct))
for i := range plain {
plain[i] = ct[i] ^ stream[i]
}
check := gcm.Seal(nil, nonce, plain, aad)
if !bytes.Equal(check[len(plain):len(plain)+8], tag) {
return nil, errors.New("tag")
}
return plain, nil
}
func openFromStation(t *testing.T, key, iv []byte, id uint16, pkt []byte) (uint32, []byte) {
t.Helper()
if len(pkt) < 12 {
t.Fatalf("datagram %d", len(pkt))
}
counter := binary.BigEndian.Uint32(pkt[:4])
return counter, openGCM(t, key, iv, id, counter, pkt[:4], pkt[4:12], pkt[12:])
}
func TestCABundleIsStoredWhenTheSignatureChecks(t *testing.T) {
v := startV3(t, "")
v.throughBundle(t)
got, err := os.ReadFile(filepath.Join(v.certDir, "ca.pem"))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, v.pki.caPEM) {
t.Fatal("stored bundle does not match the signed PEM")
}
}
func TestBadCASignatureRetriesOn3(t *testing.T) {
v := startV3(t, "")
v.refl.accept(t)
v.refl.readFrame(t)
v.refl.writeFrame(t, 19, v.pki.info())
typ, _ := v.refl.readFrame(t)
if typ != 20 {
t.Fatalf("type %d, want a bundle request", typ)
}
sig := append([]byte{}, v.pki.sig...)
sig[0] ^= 0xff
v.refl.writeFrame(t, 21, v.pki.bundle(sig))
got, err := v.st.Wait(context.Background(), func(s station.State) bool { return s.Detail == "CA signature rejected" })
if err != nil {
t.Fatal(err)
}
if got.Connection != "Disconnected" {
t.Fatalf("Connection %s", got.Connection)
}
if _, err := os.Stat(filepath.Join(v.certDir, "ca.pem")); !os.IsNotExist(err) {
t.Fatalf("ca.pem err = %v, want absent", err)
}
v.clock.BlockUntilWaiting(t)
v.refl.ln.(*net.TCPListener).SetDeadline(time.Now().Add(150 * time.Millisecond))
if conn, err := v.refl.ln.Accept(); err == nil {
conn.Close()
t.Fatal("retried before 2 seconds")
}
v.clock.Advance(2 * time.Second)
v.refl.accept(t)
typ, body := v.refl.readFrame(t)
if typ != 5 || !versionIs(body, 3, 0) {
t.Fatalf("retry type %d body %v", typ, body)
}
if v.st.State().Detail != "CA signature rejected" {
t.Fatalf("Detail = %q", v.st.State().Detail)
}
}
func TestExistingCABundleIsKept(t *testing.T) {
v := startV3(t, "")
if err := os.Mkdir(v.certDir, 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(v.certDir, "ca.pem"), v.pki.caPEM, 0o644); err != nil {
t.Fatal(err)
}
v.refl.accept(t)
v.refl.readFrame(t)
v.refl.writeFrame(t, 19, v.pki.info())
typ, _ := v.refl.readFrame(t)
if typ != 14 {
t.Fatalf("type %d, want start encryption and no bundle request", typ)
}
}
func TestCADigestMismatchIsLoggedAndKept(t *testing.T) {
logPath := filepath.Join(t.TempDir(), "funknetz.log")
refl := newReflector(t)
certDir := filepath.Join(t.TempDir(), "certs")
if err := os.Mkdir(certDir, 0o700); err != nil {
t.Fatal(err)
}
old := []byte("not the announced bundle\n")
if err := os.WriteFile(filepath.Join(certDir, "ca.pem"), old, 0o644); err != nil {
t.Fatal(err)
}
body, err := jsonv2.Marshal(map[string]any{
"host": "127.0.0.1", "port": refl.port, "callsign": "DL1ABC", "authKey": "key",
"talkGroup": 20, "certificateDir": certDir, "enableLogging": true, "logFile": logPath,
})
if err != nil {
t.Fatal(err)
}
path := writeConfigFile(t, string(body))
clock := newFakeClock()
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
go func() { _ = st.Run(ctx) }()
pki := newPKI(t, "", net.ParseIP("127.0.0.1"))
refl.accept(t)
refl.readFrame(t)
refl.writeFrame(t, 19, pki.info())
typ, _ := refl.readFrame(t)
if typ != 14 {
t.Fatalf("type %d, want start encryption", typ)
}
got, err := os.ReadFile(filepath.Join(certDir, "ca.pem"))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, old) {
t.Fatal("replaced the stored bundle")
}
logBody, err := os.ReadFile(logPath)
if err != nil {
t.Fatal(err)
}
if !bytes.Contains(logBody, []byte("CA digest differs")) {
t.Fatalf("log = %q", logBody)
}
}
func TestCannotCreateCertificateDirStops(t *testing.T) {
refl := newReflector(t)
certDir := filepath.Join(t.TempDir(), "blocked")
if err := os.WriteFile(certDir, []byte("x"), 0o600); err != nil {
t.Fatal(err)
}
path := writeConfigFile(t, configWithDir(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20, certDir, ""))
clock := newFakeClock()
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
go func() { _ = st.Run(ctx) }()
pki := newPKI(t, "", net.ParseIP("127.0.0.1"))
refl.accept(t)
refl.readFrame(t)
refl.writeFrame(t, 19, pki.info())
got, err := st.Wait(ctx, func(s station.State) bool {
return len(s.Detail) >= len("Cannot create ") && s.Detail[:len("Cannot create ")] == "Cannot create "
})
if err != nil {
t.Fatal(err)
}
if got.Detail != "Cannot create "+certDir || got.Connection != "Disconnected" {
t.Fatalf("State = %+v", got)
}
clock.Advance(2 * time.Second)
refl.ln.(*net.TCPListener).SetDeadline(time.Now().Add(150 * time.Millisecond))
if conn, err := refl.ln.Accept(); err == nil {
conn.Close()
t.Fatal("opened a 2.0 session")
}
}
func TestRelativeCertificateDirStaysRelative(t *testing.T) {
refl := newReflector(t)
dir := t.TempDir()
path := filepath.Join(dir, "funknetz.json")
body := configWithDir(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20, "./funknetz", "")
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
before := body
clock := newFakeClock()
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
go func() { _ = st.Run(ctx) }()
pki := newPKI(t, "", net.ParseIP("127.0.0.1"))
refl.accept(t)
refl.readFrame(t)
refl.writeFrame(t, 19, pki.info())
typ, _ := refl.readFrame(t)
if typ != 20 {
t.Fatalf("type %d, want a bundle request", typ)
}
info, err := os.Stat(filepath.Join(dir, "funknetz"))
if err != nil {
t.Fatal(err)
}
if !info.IsDir() || info.Mode().Perm() != 0o700 {
t.Fatalf("mode %o", info.Mode().Perm())
}
after, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if string(after) != before {
t.Fatalf("file = %s, want the relative path kept", after)
}
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(cwd, "funknetz")); err == nil {
t.Fatal("created funknetz in the working directory")
}
}
func TestTLSUsesTheCallsignCertificate(t *testing.T) {
v := startV3(t, "")
if err := os.Mkdir(v.certDir, 0o700); err != nil {
t.Fatal(err)
}
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
writeKey(t, filepath.Join(v.certDir, "key.pem"), key)
if err := os.WriteFile(filepath.Join(v.certDir, "cert.pem"), signClient(t, v.pki, key, "DL1ABC"), 0o644); err != nil {
t.Fatal(err)
}
v.throughBundle(t)
v.tls(t, v.pki.serverConfig())
peer := v.refl.tls.ConnectionState().PeerCertificates
if len(peer) != 1 || peer[0].Subject.CommonName != "DL1ABC" {
t.Fatalf("presented %#v", peer)
}
v.refl.writeFrame(t, 12, nil)
typ, body := v.refl.readFrame(t)
if typ != 17 {
t.Fatalf("type %d, want the signing request written because none was stored", typ)
}
checkCSR(t, body, v.certDir)
v.serverAndKey(t, []string{"OPUS"})
v.beat(t)
}
func TestCertificateProblemsStop(t *testing.T) {
cases := []struct {
name string
prep func(t *testing.T, dir string, p *testPKI)
want string
}{
{name: "callsign", want: "Certificate callsign does not match", prep: func(t *testing.T, dir string, p *testPKI) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
writeKey(t, filepath.Join(dir, "key.pem"), key)
if err := os.WriteFile(filepath.Join(dir, "cert.pem"), signClient(t, p, key, "DB0ZZZ"), 0o644); err != nil {
t.Fatal(err)
}
}},
{name: "missing key", want: "Certificate key is missing", prep: func(t *testing.T, dir string, p *testPKI) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "cert.pem"), signClient(t, p, key, "DL1ABC"), 0o644); err != nil {
t.Fatal(err)
}
}},
{name: "mismatch", want: "Certificate does not match the key", prep: func(t *testing.T, dir string, p *testPKI) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
other, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
writeKey(t, filepath.Join(dir, "key.pem"), other)
if err := os.WriteFile(filepath.Join(dir, "cert.pem"), signClient(t, p, key, "DL1ABC"), 0o644); err != nil {
t.Fatal(err)
}
}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
v := startV3(t, "")
if err := os.Mkdir(v.certDir, 0o700); err != nil {
t.Fatal(err)
}
tc.prep(t, v.certDir, v.pki)
v.throughBundle(t)
v.refl.writeFrame(t, 15, nil)
got, err := v.st.Wait(context.Background(), func(s station.State) bool { return s.Detail == tc.want })
if err != nil {
t.Fatal(err)
}
if got.Connection != "Disconnected" {
t.Fatalf("Connection %s", got.Connection)
}
v.clock.Advance(2 * time.Second)
v.refl.ln.(*net.TCPListener).SetDeadline(time.Now().Add(150 * time.Millisecond))
if conn, err := v.refl.ln.Accept(); err == nil {
conn.Close()
t.Fatal("retried")
}
})
}
}
func TestHostMismatchRetriesOn3(t *testing.T) {
v := startV3(t, "")
v.pki = newPKI(t, "example.com", nil)
v.throughBundle(t)
v.refl.writeFrame(t, 15, nil)
_ = v.refl.handshake(t, v.pki.serverConfig())
got, err := v.st.Wait(context.Background(), func(s station.State) bool {
return s.Detail == "Reflector certificate does not match the host"
})
if err != nil {
t.Fatal(err)
}
if got.Connection != "Disconnected" {
t.Fatalf("Connection %s", got.Connection)
}
v.clock.BlockUntilWaiting(t)
v.clock.Advance(2 * time.Second)
v.refl.accept(t)
typ, body := v.refl.readFrame(t)
if typ != 5 || !versionIs(body, 3, 0) {
t.Fatalf("retry type %d body %v", typ, body)
}
if v.st.State().Detail != "Reflector certificate does not match the host" {
t.Fatalf("Detail = %q", v.st.State().Detail)
}
}
func TestMissingSigningRequestIsWritten(t *testing.T) {
v := startV3(t, "")
if err := os.Mkdir(v.certDir, 0o700); err != nil {
t.Fatal(err)
}
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
writeKey(t, filepath.Join(v.certDir, "key.pem"), key)
if err := os.WriteFile(filepath.Join(v.certDir, "cert.pem"), signClient(t, v.pki, key, "DL1ABC"), 0o644); err != nil {
t.Fatal(err)
}
v.throughBundle(t)
v.tls(t, v.pki.serverConfig())
v.refl.writeFrame(t, 16, nil)
typ, body := v.refl.readFrame(t)
if typ != 17 {
t.Fatalf("type %d, want the signing request", typ)
}
checkCSR(t, body, v.certDir)
}
func TestHandshakeThatDoesNotFinishRetriesOn3(t *testing.T) {
v := startV3(t, "")
v.throughBundle(t)
gen := v.clock.Generation()
v.refl.writeFrame(t, 15, nil)
waitClock(t, v.clock, gen)
v.clock.Advance(15 * time.Second)
if _, err := v.st.Wait(context.Background(), func(s station.State) bool {
return s.Detail == "TLS handshake failed"
}); err != nil {
t.Fatal(err)
}
v.clock.BlockUntilWaiting(t)
v.clock.Advance(2 * time.Second)
v.refl.accept(t)
typ, body := v.refl.readFrame(t)
if typ != 5 || !versionIs(body, 3, 0) {
t.Fatalf("retry type %d body %v, want 3.0", typ, body)
}
if v.st.State().Detail != "TLS handshake failed" {
t.Fatalf("Detail = %q", v.st.State().Detail)
}
}
func TestTLSHandshakeFailureRetriesOn3(t *testing.T) {
v := startV3(t, "")
v.throughBundle(t)
cfg := v.pki.serverConfig()
cfg.MinVersion = tls.VersionTLS10
cfg.MaxVersion = tls.VersionTLS11
v.refl.writeFrame(t, 15, nil)
_ = v.refl.handshake(t, cfg)
if _, err := v.st.Wait(context.Background(), func(s station.State) bool { return s.Detail == "TLS handshake failed" }); err != nil {
t.Fatal(err)
}
v.clock.BlockUntilWaiting(t)
v.clock.Advance(2 * time.Second)
v.refl.accept(t)
typ, body := v.refl.readFrame(t)
if typ != 5 || !versionIs(body, 3, 0) {
t.Fatalf("retry type %d body %v", typ, body)
}
}
func TestSigningRequestAndAuthKey(t *testing.T) {
v := startV3(t, "")
v.throughBundle(t)
v.tls(t, v.pki.serverConfig())
v.hmac(t)
v.serverAndKey(t, []string{"OPUS"})
v.beat(t)
}
func TestReflectorCertificateIsSavedAndReconnects(t *testing.T) {
v := startV3(t, "")
v.throughBundle(t)
v.tls(t, v.pki.serverConfig())
v.refl.writeFrame(t, 16, nil)
typ, _ := v.refl.readFrame(t)
if typ != 17 {
t.Fatalf("type %d, want the signing request", typ)
}
pemCert := signClient(t, v.pki, mustKey(t), "DL1ABC")
v.refl.writeFrame(t, 18, appendStr(nil, string(pemCert)))
v.refl.accept(t)
typ, body := v.refl.readFrame(t)
if typ != 5 || !versionIs(body, 3, 0) {
t.Fatalf("reconnect type %d body %v", typ, body)
}
got, err := os.ReadFile(filepath.Join(v.certDir, "cert.pem"))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, pemCert) {
t.Fatal("certificate file does not match the reflector PEM")
}
}
func TestCertificateAfterLoginReconnects(t *testing.T) {
v := startV3(t, "")
v.throughBundle(t)
v.tls(t, v.pki.serverConfig())
v.hmac(t)
v.serverAndKey(t, []string{"OPUS"})
v.beat(t)
pemCert := signClient(t, v.pki, mustKey(t), "DL1ABC")
v.refl.writeFrame(t, 18, appendStr(nil, string(pemCert)))
v.refl.accept(t)
typ, body := v.refl.readFrame(t)
if typ != 5 || !versionIs(body, 3, 0) {
t.Fatalf("reconnect type %d body %v", typ, body)
}
}
func TestNewerSigningRequestIsSentAfterLogin(t *testing.T) {
v := startV3(t, "")
if err := os.Mkdir(v.certDir, 0o700); err != nil {
t.Fatal(err)
}
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
writeKey(t, filepath.Join(v.certDir, "key.pem"), key)
if err := os.WriteFile(filepath.Join(v.certDir, "cert.pem"), signClient(t, v.pki, key, "DL1ABC"), 0o644); err != nil {
t.Fatal(err)
}
csr := []byte("-----BEGIN CERTIFICATE REQUEST-----\nMIIB\n-----END CERTIFICATE REQUEST-----\n")
if err := os.WriteFile(filepath.Join(v.certDir, "csr.pem"), csr, 0o644); err != nil {
t.Fatal(err)
}
future := time.Now().Add(time.Hour)
if err := os.Chtimes(filepath.Join(v.certDir, "csr.pem"), future, future); err != nil {
t.Fatal(err)
}
v.throughBundle(t)
v.tls(t, v.pki.serverConfig())
v.refl.writeFrame(t, 12, nil)
typ, body := v.refl.readFrame(t)
if typ != 17 {
t.Fatalf("type %d, want the newer signing request", typ)
}
raw, _ := takeStr(t, body)
if raw != string(csr) {
t.Fatalf("csr = %q", raw)
}
}
func TestMissingOpusOn3Stops(t *testing.T) {
v := startV3(t, "")
v.throughBundle(t)
v.tls(t, v.pki.serverConfig())
v.hmac(t)
v.refl.writeFrame(t, 100, serverInfo(1, nil, []string{"GSM"}))
if _, err := v.st.Wait(context.Background(), func(s station.State) bool {
return s.Detail == "The reflector did not offer OPUS."
}); err != nil {
t.Fatal(err)
}
v.clock.Advance(2 * time.Second)
v.refl.ln.(*net.TCPListener).SetDeadline(time.Now().Add(150 * time.Millisecond))
if conn, err := v.refl.ln.Accept(); err == nil {
conn.Close()
t.Fatal("retried")
}
}
func TestEncryptedTalkGroup(t *testing.T) {
v := startV3(t, "")
v.throughBundle(t)
v.tls(t, v.pki.serverConfig())
v.hmac(t)
v.serverAndKey(t, []string{"OPUS"})
first := append([]byte{}, v.reg...)
v.clock.BlockUntilWaiting(t)
v.clock.Advance(10 * time.Second)
typ, _ := v.refl.readFrame(t)
if typ != 1 {
t.Fatalf("type %d, want a TCP heartbeat", typ)
}
gen := v.clock.Generation()
v.refl.writeFrame(t, 1, nil)
waitClock(t, v.clock, gen)
v.clock.Advance(5 * time.Second)
again := v.refl.readRawUDP(t)
if !bytes.Equal(first, again) {
t.Fatal("registration datagram changed while the heartbeat was missing")
}
v.beat(t)
v.st.HoldButton()
v.st.WriteMicrophone(make([]int16, opus.FrameSamples))
counter, plain := openFromStation(t, v.key, v.iv, v.id, v.refl.readRawUDP(t))
if counter != 1 || binary.BigEndian.Uint16(plain[:2]) != 101 {
t.Fatalf("audio counter %d type %d", counter, binary.BigEndian.Uint16(plain[:2]))
}
packet, _ := takeBytes(t, plain[2:])
dec, err := opus.NewDecoder()
if err != nil {
t.Fatal(err)
}
defer dec.Close()
if pcm, err := dec.Decode(packet); err != nil || len(pcm) != opus.FrameSamples {
t.Fatalf("decode %v len %d", err, len(pcm))
}
v.st.ReleaseButton()
counter, plain = openFromStation(t, v.key, v.iv, v.id, v.refl.readRawUDP(t))
if binary.BigEndian.Uint16(plain[:2]) != 102 {
t.Fatalf("flush type %d counter %d", binary.BigEndian.Uint16(plain[:2]), counter)
}
enc, err := opus.NewEncoder()
if err != nil {
t.Fatal(err)
}
defer enc.Close()
pkt, err := enc.Encode(make([]int16, opus.FrameSamples))
if err != nil {
t.Fatal(err)
}
good := sealToStation(t, v.key, v.iv, 5, append(appendU16(nil, 101), appendBytes(nil, pkt)...))
bad := append([]byte{}, good...)
bad[len(bad)-1] ^= 0xff
v.refl.writeRawUDP(t, bad)
v.refl.writeRawUDP(t, sealToStation(t, v.key, v.iv, 0, append(appendU16(nil, 101), appendBytes(nil, pkt)...)))
v.refl.writeRawUDP(t, good)
if _, err := v.st.WaitPlayback(context.Background(), opus.FrameSamples); err != nil {
t.Fatal(err)
}
if extra := v.st.TakePlayback(); len(extra) != 0 {
t.Fatalf("played %d extra samples", len(extra))
}
}
func mustKey(t *testing.T) *rsa.PrivateKey {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
return key
}
func hmacSum(key, msg []byte) []byte {
mac := hmac.New(sha1.New, key)
mac.Write(msg)
return mac.Sum(nil)
}