From 2786ae2ddebeae8d1361b281044f18b98a7f431a Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sun, 7 Jun 2026 12:44:13 +0200 Subject: [PATCH] feat(busauth,client): pin the bus CA over TLS busauth.LoadCATLSConfig turns a ca.crt path into a *tls.Config trusting only that private CA (clients must pin it; the system roots would reject a self-signed server cert). busauth.ServerTLSConfig loads the server keypair. client.Options gains TLS; NewWithOptions calls nats.Secure when set, so the data-plane connection is encrypted and the server pinned. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/busauth/tls.go | 37 +++++++++++++++++++++++++++++++++++++ pkg/client/client.go | 8 ++++++++ 2 files changed, 45 insertions(+) create mode 100644 pkg/busauth/tls.go diff --git a/pkg/busauth/tls.go b/pkg/busauth/tls.go new file mode 100644 index 0000000..47e4fcf --- /dev/null +++ b/pkg/busauth/tls.go @@ -0,0 +1,37 @@ +package busauth + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "os" +) + +// LoadCATLSConfig builds a *tls.Config that trusts ONLY the given CA certificate +// (PEM file), for a bus client pinning the project's self-signed CA. Because the +// bus uses a private CA rather than a public one, clients must pin it explicitly; +// trusting the system roots would reject the server cert. This is the single +// helper every client (Go peers, the mobile binding, the gateway) uses to turn a +// ca.crt path into a connection config. +func LoadCATLSConfig(caPEMPath string) (*tls.Config, error) { + pem, err := os.ReadFile(caPEMPath) + if err != nil { + return nil, fmt.Errorf("busauth: read CA %q: %w", caPEMPath, err) + } + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(pem) { + return nil, fmt.Errorf("busauth: CA %q contains no valid PEM certificate", caPEMPath) + } + return &tls.Config{RootCAs: pool, MinVersion: tls.VersionTLS12}, nil +} + +// ServerTLSConfig loads the bus NATS server's certificate and private key (PEM +// files) into a *tls.Config to present to clients. The private key never leaves +// the host; only the CA cert travels to clients. +func ServerTLSConfig(certPEMPath, keyPEMPath string) (*tls.Config, error) { + cert, err := tls.LoadX509KeyPair(certPEMPath, keyPEMPath) + if err != nil { + return nil, fmt.Errorf("busauth: load server keypair: %w", err) + } + return &tls.Config{Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12}, nil +} diff --git a/pkg/client/client.go b/pkg/client/client.go index 3d84db2..8e3a878 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -16,6 +16,7 @@ import ( "bytes" "context" "crypto/rand" + "crypto/tls" "encoding/base64" "encoding/hex" "encoding/json" @@ -66,6 +67,10 @@ type Options struct { // with an nkey to a server that does not advertise nkey auth ("nkeys not // supported by the server"), so this is opt-in rather than always-on. UseNkey bool + // TLS, when non-nil, secures the NATS connection and pins the server to this + // config's RootCAs (the bus's self-signed CA). Build it with + // busauth.LoadCATLSConfig(caPath). Nil keeps the connection plaintext. + TLS *tls.Config } // New connects to NATS and records the control-plane URL with default Options @@ -87,6 +92,9 @@ func NewWithOptions(natsURL, ctrlURL string, id cs.Identity, opts Options) (*Cli } natsOpts = append(natsOpts, nats.Nkey(nkeyPub, nkeySign)) } + if opts.TLS != nil { + natsOpts = append(natsOpts, nats.Secure(opts.TLS)) + } nc, err := nats.Connect(natsURL, natsOpts...) if err != nil { return nil, fmt.Errorf("client: connect nats %q: %w", natsURL, err)