package main import ( "crypto/ed25519" "crypto/rand" "crypto/sha256" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/hex" "math/big" "net" "os" "path/filepath" "strings" "testing" "time" "github.com/novox/mesh-controller/internal/broker" ) // Where a builder publishes. // // Preferably from the mesh: a builder that is a module requires an artifact store, and the mesh // writes it a binding saying which machine answers and on what port. Reading it means the address // is not a setting somebody keeps in step by hand. func binding(t *testing.T, body string) string { t.Helper() path := filepath.Join(t.TempDir(), "artifact-store.json") if err := os.WriteFile(path, []byte(body), 0o644); err != nil { t.Fatal(err) } return path } func TestTheMeshSaysWhereToPublish(t *testing.T) { t.Setenv("MESH_BINDING", binding(t, `{"binding":1,"provision":"artifact-store", "from":"anchor","at":"anchor.internal","serves":{"port":5000,"scheme":"http"}}`)) where, err := whereToPublish() if err != nil { t.Fatal(err) } if where != "anchor.internal:5000" { t.Fatalf("got %q", where) } } func TestABindingWithNoAddressIsRefused(t *testing.T) { // The provider is not on the private network, so there is no name to reach it by. Falling // back to anything would publish to a store on the wrong machine and be found out much later. t.Setenv("MESH_BINDING", binding(t, `{"binding":1,"from":"anchor","serves":{"port":5000}}`)) _, err := whereToPublish() if err == nil { t.Fatal("a binding with nowhere to reach was accepted") } if !strings.Contains(err.Error(), "anchor") { t.Fatalf("the failure does not name the machine: %v", err) } } func TestABindingWithNoPortIsRefused(t *testing.T) { t.Setenv("MESH_BINDING", binding(t, `{"binding":1,"from":"a","at":"a.internal","serves":{}}`)) if _, err := whereToPublish(); err == nil { t.Fatal("a binding saying nothing about a port was accepted") } } func TestTheVariableStillWorksForABuilderRunByAPerson(t *testing.T) { // Which is how this started and how it is still run while being developed. t.Setenv("MESH_BINDING", "") t.Setenv("MESH_REGISTRY", "127.0.0.1:5000") where, err := whereToPublish() if err != nil { t.Fatal(err) } if where != "127.0.0.1:5000" { t.Fatalf("got %q", where) } } func TestNeitherIsRefusedRatherThanGuessed(t *testing.T) { t.Setenv("MESH_BINDING", "") t.Setenv("MESH_REGISTRY", "") if _, err := whereToPublish(); err == nil { t.Fatal("a builder with nowhere to publish reported somewhere") } } func TestTheCredentialComesFromAFileTheMeshSealed(t *testing.T) { // A builder that is a module is given its credential the way every module is: sealed to the // machine and written by the host. An environment variable instead would put the one copy // that matters through a terminal and a process listing. path := filepath.Join(t.TempDir(), "broker") if err := os.WriteFile(path, []byte("amqps://a-builder:secret@broker.internal:5671/\n"), 0o600); err != nil { t.Fatal(err) } t.Setenv("MESH_BROKER_FILE", path) t.Setenv("MESH_BROKER_AMQP", "amqp://should-not-be-used@nowhere/") got, err := brokerFrom() if err != nil { t.Fatal(err) } if got.URL != "amqps://a-builder:secret@broker.internal:5671/" { t.Fatalf("got %q", got.URL) } } // The credential the mesh seals carries what to check the broker's certificate against, because a // mesh's broker presents a certificate of the mesh's own and no public trust store has it. A URL // alone can only reach a broker somebody else vouches for. func TestTheSealedCredentialCarriesWhatVerifiesTheBroker(t *testing.T) { path := filepath.Join(t.TempDir(), "broker") if err := os.WriteFile(path, []byte( `{"url":"amqps://a-builder:secret@broker.internal:5671/","fingerprint":"abc123"}`), 0o600); err != nil { t.Fatal(err) } t.Setenv("MESH_BROKER_FILE", path) t.Setenv("MESH_BROKER_AMQP", "") got, err := brokerFrom() if err != nil { t.Fatal(err) } if got.URL != "amqps://a-builder:secret@broker.internal:5671/" { t.Fatalf("the url was lost: %q", got.URL) } if got.Fingerprint != "abc123" { t.Fatal("the builder was given nothing to check the broker against, so it can only " + "connect to a broker some public authority vouches for") } } func TestAnEmptyCredentialFileIsRefused(t *testing.T) { // Otherwise the builder connects as nobody and is refused, with the reason three layers away. path := filepath.Join(t.TempDir(), "broker") if err := os.WriteFile(path, []byte("\n"), 0o600); err != nil { t.Fatal(err) } t.Setenv("MESH_BROKER_FILE", path) t.Setenv("MESH_BROKER_AMQP", "") if _, err := brokerFrom(); err == nil { t.Fatal("an empty credential was accepted") } } func TestABuilderWithNoCredentialAtAllSaysSo(t *testing.T) { t.Setenv("MESH_BROKER_FILE", "") t.Setenv("MESH_BROKER_AMQP", "") if _, err := brokerFrom(); err == nil { t.Fatal("a builder with no broker reported one") } } // The pin is compared in the spelling the mesh writes it. // // A bare digest against a written fingerprint never matches, and the failure is indistinguishable // from being pointed at the wrong broker — which is the one thing this check exists to report // truthfully. It cost a lab run. func TestThePinIsComparedInTheSpellingTheMeshWritesIt(t *testing.T) { certificate, key := aServerCertificate(t) der := certificate.Certificate[0] sum := sha256.Sum256(der) written := "sha256:" + hex.EncodeToString(sum[:]) listener, err := tls.Listen("tcp", "127.0.0.1:0", &tls.Config{ Certificates: []tls.Certificate{certificate}, MinVersion: tls.VersionTLS12, }) if err != nil { t.Fatal(err) } defer listener.Close() go func() { for { conn, err := listener.Accept() if err != nil { return } _ = conn.(*tls.Conn).Handshake() conn.Close() } }() _ = key // The pin the mesh wrote must be accepted. if err := handshakeWith(listener.Addr().String(), written); err != nil { t.Fatalf("the broker this builder was told about was refused: %v", err) } // And a different one refused, or the check reports nothing. other := "sha256:" + strings.Repeat("ab", 32) if err := handshakeWith(listener.Addr().String(), other); err == nil { t.Fatal("a broker this builder was not told about was accepted") } } // handshakeWith runs the pin check the builder dials with against an address. func handshakeWith(address, pin string) error { conn, err := tls.Dial("tcp", address, broker.PinnedToFingerprint(pin)) if err != nil { return err } return conn.Close() } func aServerCertificate(t *testing.T) (tls.Certificate, ed25519.PrivateKey) { t.Helper() public, private, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } template := &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "a broker"}, NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour), IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, } der, err := x509.CreateCertificate(rand.Reader, template, template, public, private) if err != nil { t.Fatal(err) } return tls.Certificate{Certificate: [][]byte{der}, PrivateKey: private}, private }