package station_test import ( "bytes" "context" "crypto" "crypto/hmac" "crypto/rand" "crypto/rsa" "crypto/sha1" "crypto/sha256" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/binary" jsonv2 "encoding/json/v2" "encoding/pem" "errors" "io/fs" "math/big" "net" "os" "path/filepath" "testing" "time" "gitea.neitzel.de/konrad/funknetz/internal/gcm64" "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, "protocol": "v3", "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() { // Run's error is observed through State, which is the station seam. _ = 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 := jsonv2.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) } name, ok := station.MachineArch() if !ok { t.Fatal("machine arch") } if got["machineArch"] != 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 := gcm64.Open(key, gcm64.IV(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 := gcm64.Seal(key, gcm64.IV(iv, 0, counter), plain, aad[:]) if err != nil { t.Fatal(err) } wire := append([]byte{}, aad[:]...) wire = append(wire, tag...) return append(wire, ct...) } 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")); !errors.Is(err, fs.ErrNotExist) { 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, "protocol": "v3", "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() { // Run's error is observed through State, which is the station seam. _ = 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() { // Run's error is observed through State, which is the station seam. _ = 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() { // Run's error is observed through State, which is the station seam. _ = 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) // The station records this handshake failure; its State is the result. _ = 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) // The station records this handshake failure; its State is the result. _ = 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) }