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`, composed into its container from the
node's settings by a placeholder that names a seat, `${seat:mesh-store:5432}`,
and read on top of the sealed value by the store, the broker, the management API
and the bus connection. The answer is empty when the mesh has nothing to add,
so what genesis wrote stands until the node says otherwise.
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. 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
now come from one derivation, with 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)
|
|
}
|
|
}
|