The co-op link was authenticated (HMAC token, never sent over the wire) but not encrypted. The hub now generates a self-signed cert on first run; the peer pins its exact fingerprint (no CA involved — there isn't one for a Tailscale/LAN address), delivered via a single invite-code paste that also carries the token, replacing today's separate IP+token copy. The peer link moves to its own TLS-only port (peer_listen, 47823) so the plain overlay/panel port (47822, OBS-facing) never needs to be exposed alongside it — today, opening the overlay port to a remote partner also exposes /deaths and the panel to anyone. Mandatory pinning, no insecure fallback: a half-configured peer (some but not all of hub/token/fingerprint, or a broken invite) fails loudly at startup rather than connecting unpinned. An unconfigured peer still runs fine as a local-only overlay, same as before. New: tlscert.go (cert generation/persistence), pin.go (fingerprint pinning), invite.go (invite-code encode/decode, host auto-detection), each with tests. main.go/config.go/duo.go/ws.go carry the wiring for this — the dual listener, new config keys, and the TLS-aware WebSocket dial — and were rewritten in English in the process, per the project's new English-only code convention (see CLAUDE.md).
85 lines
2.1 KiB
Go
85 lines
2.1 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
)
|
|
|
|
func TestLoadOrCreateHubCert_GeneratesAndPersists(t *testing.T) {
|
|
testExeDir = t.TempDir()
|
|
defer func() { testExeDir = "" }()
|
|
|
|
cert, fp, generated, err := loadOrCreateHubCert()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !generated {
|
|
t.Fatal("expected a fresh cert to be reported as generated")
|
|
}
|
|
if len(fp) != 64 {
|
|
t.Fatalf("fingerprint should be a 64-char hex SHA-256, got %d chars: %q", len(fp), fp)
|
|
}
|
|
if cert.Leaf == nil {
|
|
t.Fatal("expected cert.Leaf to be populated")
|
|
}
|
|
if _, err := os.Stat(hubCertPath()); err != nil {
|
|
t.Fatalf("cert file wasn't persisted: %v", err)
|
|
}
|
|
if _, err := os.Stat(hubKeyPath()); err != nil {
|
|
t.Fatalf("key file wasn't persisted: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadOrCreateHubCert_ReusesSameFingerprint(t *testing.T) {
|
|
testExeDir = t.TempDir()
|
|
defer func() { testExeDir = "" }()
|
|
|
|
_, fp1, gen1, err := loadOrCreateHubCert()
|
|
if err != nil || !gen1 {
|
|
t.Fatalf("first call should generate: gen=%v err=%v", gen1, err)
|
|
}
|
|
_, fp2, gen2, err := loadOrCreateHubCert()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error on reload: %v", err)
|
|
}
|
|
if gen2 {
|
|
t.Fatal("second call should reuse the persisted cert, not generate a new one")
|
|
}
|
|
if fp1 != fp2 {
|
|
t.Fatalf("fingerprint changed across reload: %q vs %q", fp1, fp2)
|
|
}
|
|
}
|
|
|
|
func TestLoadOrCreateHubCert_DifferentInstallationsDiffer(t *testing.T) {
|
|
testExeDir = t.TempDir()
|
|
_, fpA, _, err := loadOrCreateHubCert()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
testExeDir = t.TempDir()
|
|
_, fpB, _, err := loadOrCreateHubCert()
|
|
testExeDir = ""
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if fpA == fpB {
|
|
t.Fatal("two installations generated the same certificate fingerprint")
|
|
}
|
|
}
|
|
|
|
func TestLoadOrCreateHubCert_PartialPairFails(t *testing.T) {
|
|
testExeDir = t.TempDir()
|
|
defer func() { testExeDir = "" }()
|
|
|
|
if err := writePEM(hubCertPath(), "CERTIFICATE", []byte("not a real cert")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// key file intentionally left missing
|
|
|
|
if _, _, _, err := loadOrCreateHubCert(); err == nil {
|
|
t.Fatal("expected an error for a partial/broken cert+key pair")
|
|
}
|
|
}
|