// Package link is how a node reaches the mesh: one outbound connection to the broker, and // nothing listening on this machine. // // novox/hq ADR 0004: the node checks the broker's certificate against the fingerprint in its // token *before sending anything*. That is trust on first use with the first use moved out of // band — the token travelled by a person, so its authenticity comes from the channel it took // rather than from anything this machine can check afterwards. package link import ( "crypto/sha256" "crypto/tls" "crypto/x509" "encoding/hex" "errors" "fmt" "net" "strings" "time" ) // ErrWrongCertificate is what a node gets when the broker is not the one its token described. // // Its own error because it means something specific and alarming: either the mesh's broker was // replaced, or this node is being pointed at something else. It is not a connection problem and // must not be retried as one. var ErrWrongCertificate = errors.New("the broker presented a certificate this token does not pin") // Fingerprint is what a pin looks like: sha256 over the certificate as it arrives on the wire. func Fingerprint(der []byte) string { sum := sha256.Sum256(der) return "sha256:" + hex.EncodeToString(sum[:]) } // PinnedConfig is a TLS configuration that trusts exactly one certificate. // // InsecureSkipVerify is true and that is not a weakening — it is the point. The mesh's broker at // bootstrap has a self-signed certificate and is reached at an address rather than a name, so // there is no authority to check it against and no name to match. Chain and hostname verification // are replaced with something stricter: this exact certificate, or nothing. // // The check runs in VerifyPeerCertificate, which TLS calls before the handshake completes — so a // wrong broker is refused before this node sends anything, which is what ADR 0004 requires. func PinnedConfig(pin string) (*tls.Config, error) { pin = strings.TrimSpace(pin) if !strings.HasPrefix(pin, "sha256:") || len(pin) != len("sha256:")+64 { return nil, fmt.Errorf( "%q is not a certificate fingerprint: it is sha256: followed by 64 hex characters", pin) } if _, err := hex.DecodeString(pin[len("sha256:"):]); err != nil { return nil, fmt.Errorf("%q is not a certificate fingerprint: %w", pin, err) } return &tls.Config{ InsecureSkipVerify: true, //nolint:gosec // replaced by the pin below, which is stricter MinVersion: tls.VersionTLS12, VerifyPeerCertificate: func(raw [][]byte, _ [][]*x509.Certificate) error { if len(raw) == 0 { return fmt.Errorf("%w: it presented none", ErrWrongCertificate) } // The leaf, which is what the pin is of. A chain is irrelevant here: nothing is // being traced to an authority, so an intermediate matching would prove nothing. got := Fingerprint(raw[0]) if got != pin { return fmt.Errorf( "%w\n expected %s\n got %s\nEither this mesh's broker was replaced, "+ "or this node is being pointed at something else. This is not a "+ "connection problem and retrying will not help", ErrWrongCertificate, pin, got) } return nil }, }, nil } // Dial opens a TLS connection to the broker, refusing anything but the pinned certificate. func Dial(address, pin string, timeout time.Duration) (*tls.Conn, error) { config, err := PinnedConfig(pin) if err != nil { return nil, err } dialer := &net.Dialer{Timeout: timeout} conn, err := tls.DialWithDialer(dialer, "tcp", address, config) if err != nil { if errors.Is(err, ErrWrongCertificate) { return nil, err } return nil, fmt.Errorf("cannot reach the broker at %s: %w", address, err) } return conn, nil }