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
210 lines
6.5 KiB
Go
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)
|
|
}
|
|
}
|