// 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, } }