Files
deathwatch/tlscert_test.go
emmatherock e9fe10f0f7 Add TLS + certificate pinning for the peer link
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).
2026-09-17 21:51:22 -03:00

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")
}
}