Files
mesh-controller/internal/broker/broker_test.go
T
jschoubben e07b56ce43 An address is read from the node's settings where it is used, never recorded with a port
Three readers did not follow a moved foundation port (novox/hq 04-ISSUES/102),
and each took the control-node down in its own way: the control plane's own
store and broker connections, sealed at genesis with the port inside; and every
build the mesh ever recorded, kept as `<registry>:<port>/<module>/<artifact>@…`.

The control plane cannot open its own sealed connections to move a port, and it
cannot bind the store as a consumer would — a binding mints a credential. So its
settings get a third twin, `NAME_PORT`, read on top of the sealed value by the
store, the broker, the management API and the bus connection, and filled into
its container by a placeholder that names a seat, `${seat:mesh-store:5432}`,
from the node's given or mesh-assigned ports — never the manifest's number, and
empty when the mesh has nothing to add, so what genesis wrote stands. A value
that is still a placeholder is nothing said, aloud: the manifest naming it lands
in the next commit, once every control plane that composes it knows it.

A build is now recorded by digest and path — `artifact-store://<module>/<artifact>@…`
— and the store's address is composed in where a reference is used: the
declaration, the trust file, the bases a build is handed, a replay to the
catalogue. Over the network as `<node>.internal:<port>`; on the store's own node
before any network exists — every genesis push before its "network" step — by
loopback. A reference recorded before this, with an address, is re-routed the
same way when the mesh built it. The trust file and every provider's address
come from one derivation: the node's given port, over the mesh's assignment,
over the manifest's number.

novox/hq 04-ISSUES/102
2026-09-23 23:49:31 +02:00

210 lines
6.5 KiB
Go

package broker
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/pem"
"errors"
"math/big"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// writeCertificate puts a real self-signed certificate on disk and returns its path and the
// DER bytes, which is what a TLS client would see on the wire.
func writeCertificate(t *testing.T) (string, []byte) {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "mesh-broker"},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(24 * time.Hour),
}
der, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
t.Fatal(err)
}
path := filepath.Join(t.TempDir(), "tls.crt")
if err := os.WriteFile(path, pem.EncodeToMemory(
&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o644); err != nil {
t.Fatal(err)
}
return path, der
}
func TestTheFingerprintIsOverWhatAClientSees(t *testing.T) {
// A node computes this from the certificate the broker presents, which is DER on the wire.
// Hashing the PEM text instead would mean the same certificate, re-wrapped with different
// line endings, produced a different pin — and every token issued around that moment would
// send a node to something it refuses to talk to.
path, der := writeCertificate(t)
got, err := FingerprintOf(path)
if err != nil {
t.Fatal(err)
}
sum := sha256.Sum256(der)
want := "sha256:" + hex.EncodeToString(sum[:])
if got != want {
t.Errorf("fingerprint is %s, and a client computing it from the wire gets %s", got, want)
}
}
func TestReWrappingTheSameCertificateDoesNotChangeThePin(t *testing.T) {
// The property the test above protects, stated directly: same certificate, different file
// formatting, same pin.
path, der := writeCertificate(t)
first, err := FingerprintOf(path)
if err != nil {
t.Fatal(err)
}
rewrapped := filepath.Join(t.TempDir(), "same.crt")
body := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
if err := os.WriteFile(rewrapped, append([]byte("\n\n"), body...), 0o644); err != nil {
t.Fatal(err)
}
second, err := FingerprintOf(rewrapped)
if err != nil {
t.Fatal(err)
}
if first != second {
t.Errorf("the same certificate produced two pins:\n %s\n %s", first, second)
}
}
func TestAPrivateKeyIsNotACertificate(t *testing.T) {
// The mistake somebody makes once: pointing this at tls.key. Left unchecked it would produce
// a confident pin over the wrong file, and every node would refuse the broker.
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
der, err := x509.MarshalECPrivateKey(key)
if err != nil {
t.Fatal(err)
}
path := filepath.Join(t.TempDir(), "tls.key")
if err := os.WriteFile(path, pem.EncodeToMemory(
&pem.Block{Type: "EC PRIVATE KEY", Bytes: der}), 0o600); err != nil {
t.Fatal(err)
}
_, err = FingerprintOf(path)
if err == nil {
t.Fatal("a private key was fingerprinted as if it were a certificate")
}
if !strings.Contains(err.Error(), "private key") {
t.Errorf("the error does not say what the file actually is: %v", err)
}
}
func TestAMalformedCertificateIsRefusedRatherThanHashed(t *testing.T) {
// Bytes wrapped in the right PEM header are not a certificate. Hashing them would produce a
// pin that matches nothing, and the failure would arrive on a node instead of here.
path := filepath.Join(t.TempDir(), "broken.crt")
if err := os.WriteFile(path, pem.EncodeToMemory(
&pem.Block{Type: "CERTIFICATE", Bytes: []byte("not a certificate")}), 0o644); err != nil {
t.Fatal(err)
}
if _, err := FingerprintOf(path); err == nil {
t.Fatal("malformed bytes were accepted as a certificate")
}
}
func TestNoBrokerIsAStateAndNotAFailure(t *testing.T) {
t.Setenv(AddressVar, "")
t.Setenv(CertificateVar, "")
if _, err := FromEnvironment(); !errors.Is(err, ErrNotConfigured) {
t.Fatalf("expected ErrNotConfigured, got %v", err)
}
}
func TestAnAddressWithoutACertificateIsRefused(t *testing.T) {
// Worse than neither: a token with somewhere to connect and nothing to check would have a
// node trust whatever answers at that address.
//
// Asserted on which refusal fired. Without the check this still fails a line later, trying to
// read a certificate at the empty path — so a test asking only "was there an error" passes
// with the guard deleted. It was written that way first and confirmed to defend nothing.
t.Setenv(AddressVar, "192.0.2.10:5671")
t.Setenv(CertificateVar, "")
_, err := FromEnvironment()
if err == nil || errors.Is(err, ErrNotConfigured) {
t.Fatalf("an address with no certificate was accepted: %v", err)
}
if !strings.Contains(err.Error(), "must be set together") {
t.Errorf("refused for the wrong reason: %v", err)
}
}
func TestACertificateWithoutAnAddressIsRefused(t *testing.T) {
path, _ := writeCertificate(t)
t.Setenv(AddressVar, "")
t.Setenv(CertificateVar, path)
_, err := FromEnvironment()
if err == nil || errors.Is(err, ErrNotConfigured) {
t.Fatalf("a certificate with no address was accepted: %v", err)
}
if !strings.Contains(err.Error(), "must be set together") {
t.Errorf("refused for the wrong reason: %v", err)
}
}
func TestBothTogetherGiveABroker(t *testing.T) {
path, _ := writeCertificate(t)
t.Setenv(AddressVar, "192.0.2.10:5671")
t.Setenv(CertificateVar, path)
known, err := FromEnvironment()
if err != nil {
t.Fatal(err)
}
if known.Address != "192.0.2.10:5671" || !strings.HasPrefix(known.Fingerprint, "sha256:") {
t.Errorf("got %+v", known)
}
}
// The node moved the bus, and the address a token carries follows (novox/hq 04-ISSUES/102).
func TestTheAddressPortFollowsThePortTwin(t *testing.T) {
t.Setenv(AddressVar, "broker.example:5671")
t.Setenv(AddressVar+"_FILE", "")
path, _ := writeCertificate(t)
t.Setenv(CertificateVar, path)
t.Setenv(AddressVar+"_PORT", "5679")
b, err := FromEnvironment()
if err != nil {
t.Fatal(err)
}
if b.Address != "broker.example:5679" {
t.Fatalf("the address is %q; the node put the bus on 5679", b.Address)
}
}
func TestTheManagementPortFollowsThePortTwin(t *testing.T) {
t.Setenv(ManagementVar, "http://guest:guest@127.0.0.1:15672")
t.Setenv(ManagementVar+"_FILE", "")
t.Setenv(ManagementVar+"_PORT", "15673")
m, err := ManagementFromEnvironment()
if err != nil {
t.Fatal(err)
}
if m.base.Host != "127.0.0.1:15673" {
t.Fatalf("the management API is at %q; the node put it on 15673", m.base.Host)
}
}