diff --git a/cmd/chat/main.go b/cmd/chat/main.go index cc7c35e..33985cb 100644 --- a/cmd/chat/main.go +++ b/cmd/chat/main.go @@ -27,11 +27,12 @@ import ( func main() { var ( - natsURL = flag.String("nats-url", "nats://127.0.0.1:4250", "NATS url") - ctrlURL = flag.String("ctrl-url", "http://127.0.0.1:8470", "membershipd control-plane url") - roomSub = flag.String("room", "proc.test.ticks", "room subject to subscribe to") - idFile = flag.String("id-file", "./local_files/chat.id", "identity file path") - demoEnc = flag.Bool("demo-encrypted", false, "run the encrypted forward-secrecy demo") + natsURL = flag.String("nats-url", "nats://127.0.0.1:4250", "NATS url") + ctrlURL = flag.String("ctrl-url", "http://127.0.0.1:8470", "membershipd control-plane url") + roomSub = flag.String("room", "proc.test.ticks", "room subject to subscribe to") + idFile = flag.String("id-file", "./local_files/chat.id", "identity file path") + demoEnc = flag.Bool("demo-encrypted", false, "run the encrypted forward-secrecy demo") + caFile = flag.String("ca", "", "path to the bus CA cert (ca.crt); set to connect with TLS + nkey to a secured bus") ) flag.Parse() @@ -39,19 +40,19 @@ func main() { log.SetPrefix("[chat] ") if *demoEnc { - runEncryptedDemo(*natsURL, *ctrlURL) + runEncryptedDemo(*natsURL, *ctrlURL, *caFile) return } - runSimple(*natsURL, *ctrlURL, *roomSub, *idFile) + runSimple(*natsURL, *ctrlURL, *roomSub, *idFile, *caFile) } // runSimple subscribes to a cleartext subject and prints messages live. -func runSimple(natsURL, ctrlURL, roomSub, idFile string) { +func runSimple(natsURL, ctrlURL, roomSub, idFile, caFile string) { id, err := client.LoadOrCreateIdentity(idFile) if err != nil { log.Fatalf("identity: %v", err) } - c, err := client.New(natsURL, ctrlURL, id) + c, err := client.Connect(natsURL, ctrlURL, id, caFile) if err != nil { log.Fatalf("connect: %v", err) } @@ -91,7 +92,7 @@ func shortID(id string) string { } // runEncryptedDemo proves E2E encryption + forward secrecy end-to-end. -func runEncryptedDemo(natsURL, ctrlURL string) { +func runEncryptedDemo(natsURL, ctrlURL, caFile string) { log.Printf("=== encrypted forward-secrecy demo ===") pass := true check := func(name string, ok bool) { @@ -109,10 +110,10 @@ func runEncryptedDemo(natsURL, ctrlURL string) { idB, err := newEphemeralIdentity() must(err, "generate B identity") - a, err := client.New(natsURL, ctrlURL, idA) + a, err := client.Connect(natsURL, ctrlURL, idA, caFile) must(err, "connect A") defer a.Close() - b, err := client.New(natsURL, ctrlURL, idB) + b, err := client.Connect(natsURL, ctrlURL, idB, caFile) must(err, "connect B") defer b.Close() diff --git a/cmd/worker/main.go b/cmd/worker/main.go index 5e72aa7..1f422be 100644 --- a/cmd/worker/main.go +++ b/cmd/worker/main.go @@ -23,6 +23,7 @@ func main() { ctrlURL = flag.String("ctrl-url", "http://127.0.0.1:8470", "membershipd control-plane url") roomSub = flag.String("room", "proc.test.ticks", "room subject to publish to") idFile = flag.String("id-file", "./local_files/worker.id", "identity file path") + caFile = flag.String("ca", "", "path to the bus CA cert (ca.crt); set to connect with TLS + nkey to a secured bus") ) flag.Parse() @@ -33,7 +34,7 @@ func main() { if err != nil { log.Fatalf("identity: %v", err) } - c, err := client.New(*natsURL, *ctrlURL, id) + c, err := client.Connect(*natsURL, *ctrlURL, id, *caFile) if err != nil { log.Fatalf("connect: %v", err) } diff --git a/mobile/unibus.go b/mobile/unibus.go index 3e4d79b..8956d80 100644 --- a/mobile/unibus.go +++ b/mobile/unibus.go @@ -44,14 +44,18 @@ func GenerateIdentity(path string) error { } // NewSession loads the identity at idPath and connects to the bus. natsURL is -// the data plane (for example nats://host:4250) and ctrlURL is the control -// plane HTTP endpoint (for example http://host:8470). -func NewSession(idPath, natsURL, ctrlURL string) (*Session, error) { +// the data plane (for example tls://host:4250) and ctrlURL is the control plane +// HTTP endpoint (for example http://host:8470). caPath is the path to the bus +// CA certificate (ca.crt) bundled with the app: when set, the session connects +// securely (TLS pinned to that CA + nkey authentication on the data plane), +// matching a bus running with auth + TLS. Pass an empty caPath to connect in +// plaintext to an unsecured (dev) bus. +func NewSession(idPath, natsURL, ctrlURL, caPath string) (*Session, error) { id, err := client.LoadOrCreateIdentity(idPath) if err != nil { return nil, err } - c, err := client.New(natsURL, ctrlURL, id) + c, err := client.Connect(natsURL, ctrlURL, id, caPath) if err != nil { return nil, err } diff --git a/pkg/client/client.go b/pkg/client/client.go index 8e3a878..4a7316d 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -79,6 +79,24 @@ func New(natsURL, ctrlURL string, id cs.Identity) (*Client, error) { return NewWithOptions(natsURL, ctrlURL, id, Options{}) } +// Connect is the single migration seam every peer (worker, chat, mobile, +// gateway) uses to pick its security posture from one input: the CA path. With +// a non-empty caPath it connects securely — TLS pinned to that CA plus nkey +// authentication on the data plane — matching a bus running with bus-auth +// enforce + bus-tls. With an empty caPath it falls back to the legacy plaintext, +// no-nkey connection for local dev against an unsecured bus. The control-plane +// HTTP requests are signed in both cases (that signing is unconditional). +func Connect(natsURL, ctrlURL string, id cs.Identity, caPath string) (*Client, error) { + if caPath == "" { + return New(natsURL, ctrlURL, id) + } + tlsCfg, err := busauth.LoadCATLSConfig(caPath) + if err != nil { + return nil, fmt.Errorf("client: load CA %q: %w", caPath, err) + } + return NewWithOptions(natsURL, ctrlURL, id, Options{UseNkey: true, TLS: tlsCfg}) +} + // NewWithOptions is New with explicit connection options (nkey auth, and, from // phase 0001d, TLS). It is the single place the data-plane connection is built, // so every peer (worker, chat, mobile, gateway) gets identical behavior by