The node-engine applies only a composition signed by the controller or the operator, and when the controller cannot compose its own fix the operator composes, signs and writes one at the terminal (ADR 0539 clauses 3 and 10).
146 lines
5.4 KiB
Go
146 lines
5.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/compose"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/overlay"
|
|
)
|
|
|
|
// aMesh is the Input of mesh-controller's aMesh, as mesh-sdk's compose tests make it (compose/fixture_test.go): two
|
|
// machines on the private network, the module the mesh provides for itself assigned to both. The manifest is the one
|
|
// the controller provides (overlay.Manifest), read back as the catalogue reads it.
|
|
func aMesh(t *testing.T) compose.Input {
|
|
t.Helper()
|
|
var m catalogue.Manifest
|
|
b, _ := json.Marshal(overlay.Manifest())
|
|
if err := json.Unmarshal(b, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
in := compose.Input{Catalogue: map[string]catalogue.Manifest{overlay.Name: m}}
|
|
for i, name := range []string{"anchor", "laptop"} {
|
|
pub, _, _ := ed25519.GenerateKey(rand.Reader)
|
|
in.Nodes = append(in.Nodes, compose.Node{Name: name,
|
|
Place: compose.Place{Key: base64.StdEncoding.EncodeToString(pub), Endpoint: name + ".example:51820", Site: "here", Hub: name == "anchor", Address: []string{"10.77.0.1", "10.77.0.2"}[i]},
|
|
Assigned: []string{overlay.Name}, Capabilities: map[string]bool{"container-runtime": true, "wireguard": true, "systemd": true}})
|
|
}
|
|
return in
|
|
}
|
|
|
|
func writeKey(t *testing.T, dir string) ed25519.PublicKey {
|
|
t.Helper()
|
|
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
|
|
der, err := x509.MarshalPKCS8PrivateKey(priv)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "operator.key"), pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return pub
|
|
}
|
|
|
|
// 1943/AC5: mesh-compose, given the kept inputs, a node, a module and its fingerprint, prints that module's
|
|
// composition for that node signed by the operator's key and its digest: its resources are the controller's for that
|
|
// module on that node (compose.Compose), and it verifies against the operator's key. Without --write it writes nothing.
|
|
func TestMeshComposeGivesTheControllersResourcesSignedByTheOperator(t *testing.T) {
|
|
dir := t.TempDir()
|
|
in := aMesh(t)
|
|
raw, err := json.Marshal(in)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
input := filepath.Join(dir, "input.json")
|
|
if err := os.WriteFile(input, raw, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
operator := writeKey(t, dir)
|
|
fp := delivery.SourcePrefix2 + strings.Repeat("a", 64)
|
|
|
|
var out bytes.Buffer
|
|
if err := run([]string{"-input", input, "-node", "laptop", "-module", overlay.Name, "-fp", fp,
|
|
"-key", filepath.Join(dir, "operator.key")}, &out); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var printed struct {
|
|
Key string `json:"key"`
|
|
Bucket string `json:"bucket"`
|
|
Digest string `json:"digest"`
|
|
Composition json.RawMessage `json:"composition"`
|
|
Written bool `json:"written"`
|
|
}
|
|
if err := json.Unmarshal(out.Bytes(), &printed); err != nil {
|
|
t.Fatalf("%s: %v", out.String(), err)
|
|
}
|
|
c, err := delivery.DecodeComposed(printed.Composition)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if digest, err := delivery.VerifyComposed(c, []ed25519.PublicKey{operator}); err != nil || digest != printed.Digest {
|
|
t.Errorf("the composition verifies %q, %v against the operator's key; printed %s", digest, err, printed.Digest)
|
|
}
|
|
wantKey, _ := delivery.ComposedKey("laptop", overlay.Name, fp)
|
|
if printed.Key != wantKey || printed.Bucket != "composed-laptop" || printed.Written {
|
|
t.Errorf("printed key %s in %s, written %v", printed.Key, printed.Bucket, printed.Written)
|
|
}
|
|
|
|
d, _, err := compose.Compose(in, "laptop")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var want, got []string
|
|
for _, r := range d.Resources {
|
|
want = append(want, r["id"].(string))
|
|
}
|
|
for _, r := range c.Resources {
|
|
var head struct{ ID string }
|
|
_ = json.Unmarshal(r, &head)
|
|
got = append(got, head.ID)
|
|
}
|
|
sort.Strings(want)
|
|
sort.Strings(got)
|
|
if len(got) == 0 || strings.Join(got, " ") != strings.Join(want, " ") {
|
|
t.Errorf("mesh-compose gives %v, the controller's composition %v", got, want)
|
|
}
|
|
}
|
|
|
|
// 1943/AC5: mesh-compose refuses what it cannot compose, naming why: no key, a fingerprint not of the new form, a
|
|
// module the node does not run, a node the mesh does not have.
|
|
func TestMeshComposeRefuses(t *testing.T) {
|
|
dir := t.TempDir()
|
|
raw, _ := json.Marshal(aMesh(t))
|
|
input := filepath.Join(dir, "input.json")
|
|
_ = os.WriteFile(input, raw, 0o600)
|
|
writeKey(t, dir)
|
|
key := filepath.Join(dir, "operator.key")
|
|
fp := delivery.SourcePrefix2 + strings.Repeat("a", 64)
|
|
for _, c := range []struct {
|
|
args []string
|
|
why string
|
|
}{
|
|
{[]string{"-input", input, "-node", "laptop", "-module", overlay.Name, "-fp", fp}, "-key"},
|
|
{[]string{"-input", input, "-node", "laptop", "-module", overlay.Name, "-fp", "src2:1", "-key", key}, "fingerprint"},
|
|
{[]string{"-input", input, "-node", "laptop", "-module", "mesh-mail", "-fp", fp, "-key", key}, "does not run mesh-mail"},
|
|
{[]string{"-input", input, "-node", "nowhere", "-module", overlay.Name, "-fp", fp, "-key", key}, "nowhere"},
|
|
{[]string{"-input", input, "-node", "laptop", "-module", overlay.Name, "-fp", fp, "-key", key, "-write"}, "-creds"},
|
|
} {
|
|
var out bytes.Buffer
|
|
if err := run(c.args, &out); err == nil || !strings.Contains(err.Error(), c.why) {
|
|
t.Errorf("mesh-compose %v = %v, want a refusal saying %q", c.args, err, c.why)
|
|
}
|
|
}
|
|
}
|