// Package mtls builds mutual-TLS configurations for the one inter-service link // that crosses an untrusted network: the reverse bot-link between a remote // Telegram bot and the gateway (pkg/proto/botlink/v1). Both peers present a // certificate signed by a shared private CA and verify the other against it, so the // gateway accepts only our bot and the bot trusts only our gateway. Every other // inter-service hop stays on the trusted internal network and uses plaintext // (docs/ARCHITECTURE.md ยง12). package mtls import ( "crypto/tls" "crypto/x509" "fmt" "os" ) // ServerConfig builds a TLS config for the gateway's bot-link listener. It loads // the server certificate from certFile/keyFile, trusts client certificates signed // by the CA in caFile, and requires every client to present a valid one. func ServerConfig(certFile, keyFile, caFile string) (*tls.Config, error) { cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return nil, fmt.Errorf("mtls: load server keypair: %w", err) } pool, err := loadCAPool(caFile) if err != nil { return nil, err } return &tls.Config{ Certificates: []tls.Certificate{cert}, ClientCAs: pool, ClientAuth: tls.RequireAndVerifyClientCert, MinVersion: tls.VersionTLS13, }, nil } // ClientConfig builds a TLS config for the bot dialing the gateway. It presents the // client certificate from certFile/keyFile, verifies the gateway's certificate // against the CA in caFile, and pins the expected serverName. func ClientConfig(certFile, keyFile, caFile, serverName string) (*tls.Config, error) { cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { return nil, fmt.Errorf("mtls: load client keypair: %w", err) } pool, err := loadCAPool(caFile) if err != nil { return nil, err } return &tls.Config{ Certificates: []tls.Certificate{cert}, RootCAs: pool, ServerName: serverName, MinVersion: tls.VersionTLS13, }, nil } // loadCAPool reads a PEM bundle and returns a certificate pool trusting it. func loadCAPool(caFile string) (*x509.CertPool, error) { pem, err := os.ReadFile(caFile) if err != nil { return nil, fmt.Errorf("mtls: read CA %s: %w", caFile, err) } pool := x509.NewCertPool() if !pool.AppendCertsFromPEM(pem) { return nil, fmt.Errorf("mtls: CA %s contains no certificates", caFile) } return pool, nil }