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).
This commit is contained in:
1 parent
e1f7e6f529
commit
e9fe10f0f7
12 files changed
+1198
-465
No files matched your search
@@ -0,0 +1,65 @@
|
||||
// pin.go: certificate PINNING for the peer link, as opposed to CA trust.
|
||||
//
|
||||
// There's no certificate authority that can issue for a Tailscale/LAN IP,
|
||||
// so the peer doesn't ask "was this signed by someone I trust?" — it asks
|
||||
// "is this the exact certificate my invite code told me to expect?". That
|
||||
// makes InsecureSkipVerify safe here: it turns off the check that doesn't
|
||||
// apply (hostname/CA validation) and VerifyPeerCertificate replaces it
|
||||
// with the one that does.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func certMatchesFingerprint(rawCert []byte, wantHex string) bool {
|
||||
sum := sha256.Sum256(rawCert)
|
||||
return hex.EncodeToString(sum[:]) == strings.ToLower(strings.TrimSpace(wantHex))
|
||||
}
|
||||
|
||||
func validFingerprint(s string) bool {
|
||||
s = strings.TrimSpace(s)
|
||||
if len(s) != sha256.Size*2 {
|
||||
return false
|
||||
}
|
||||
_, err := hex.DecodeString(s)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// pinnedClientTLSConfig builds a client TLS config that accepts exactly
|
||||
// one certificate: the one whose SHA-256 fingerprint matches. Nothing
|
||||
// else about the certificate (hostname, expiry chain, issuer) is checked.
|
||||
func pinnedClientTLSConfig(fingerprint string) (*tls.Config, error) {
|
||||
if !validFingerprint(fingerprint) {
|
||||
return nil, fmt.Errorf("fingerprint %q doesn't look like a SHA-256 hex digest (want %d hex chars)", fingerprint, sha256.Size*2)
|
||||
}
|
||||
want := strings.ToLower(strings.TrimSpace(fingerprint))
|
||||
return &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
MinVersion: tls.VersionTLS13,
|
||||
VerifyPeerCertificate: func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
|
||||
if len(rawCerts) == 0 {
|
||||
return fmt.Errorf("the server didn't present a certificate")
|
||||
}
|
||||
if !certMatchesFingerprint(rawCerts[0], want) {
|
||||
return fmt.Errorf("the server's certificate doesn't match the pinned fingerprint — wrong hub, or its certificate was regenerated (you'd need a fresh invite code)")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// hubServerTLSConfig is the hub side: just present the certificate, no
|
||||
// client-certificate verification (the peer proves itself at the
|
||||
// application layer with the HMAC challenge/response, see auth.go).
|
||||
func hubServerTLSConfig(cert tls.Certificate) *tls.Config {
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
MinVersion: tls.VersionTLS13,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user