Files
mesh-host/cmd/mesh-host/trust.go
T
jschoubben 8717ec76d4 The node-engine follows the module-assignment seat: assignments, not placement; the seat's packages move to mesh-contracts and mesh-delivery (issue 1966)
The state, events and types of the seat mesh-module-assigner say assignments; the legacy node key form is gone from the
reads; the dead seat-verb list of the retired name is deleted. The placement guard and the placed-file sense stay.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 22:08:40 +01:00

290 lines
10 KiB
Go

package main
import (
"context"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"github.com/novox/mesh-host/internal/buckets"
"os"
"path/filepath"
"strings"
"sync"
"time"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
"github.com/novox/mesh-host/internal/identity"
"github.com/novox/mesh-host/internal/link"
)
// **What this engine trusts a mesh signature from** (novox/hq ADR 0550 §2; design 47's L3 item d; issue 1126): the
// vault's key alone, read from the vault's state `signers`, which only the vault may write: the bus grants are its
// anchor, as the wants' and the assignment's. The controller's key, which the node was enrolled with (its membership's
// signer), is trusted never: the controller ended by its unassignment (the operator, 2026-11-02) and its lease is never
// handed, so no gate waits on it. What it trusts now the node states in its record (NodeRecord.Trusts).
type meshTrust struct {
mu sync.Mutex
vault ed25519.PublicKey
authority string
// changed is told when what it trusts changed, so the record is written again.
changed func()
}
// newMeshTrust trusts nothing until the vault's key is read.
func newMeshTrust() *meshTrust { return &meshTrust{} }
// onChange names who is told when what it trusts changed.
func (t *meshTrust) onChange(f func()) {
t.mu.Lock()
t.changed = f
t.mu.Unlock()
}
// Signers are the keys a mesh signature is verified against now: the vault's once read, and none before.
func (t *meshTrust) Signers() []meshvaultclient.Signer {
t.mu.Lock()
defer t.mu.Unlock()
var out []meshvaultclient.Signer
if t.vault != nil {
out = append(out, meshvaultclient.Signer{Name: meshvaultclient.SignerVault, Key: t.vault})
}
return out
}
// Verify is the signer that signed msg as kind, or why none it trusts did.
func (t *meshTrust) Verify(kind string, msg, sig []byte) (meshvaultclient.Signer, error) {
context, err := meshvaultclient.SignContext(kind)
if err != nil {
return meshvaultclient.Signer{}, err
}
return meshvaultclient.VerifyBy(t.Signers(), context, msg, sig)
}
// VaultAuthority is the vault's authority certificate as read from `signers`, or "".
func (t *meshTrust) VaultAuthority() string {
if t == nil {
return ""
}
t.mu.Lock()
defer t.mu.Unlock()
return t.authority
}
// Trusts is what the record says it trusts: each signer's key and the vault's authority, by fingerprint.
func (t *meshTrust) Trusts() []string {
if t == nil {
return nil
}
authorities := map[string]string{}
if a := t.VaultAuthority(); a != "" {
authorities[meshvaultclient.SignerVault] = a
}
return meshvaultclient.Trusts(t.Signers(), authorities)
}
// change applies f under the lock and tells who follows when Trusts differs after it.
func (t *meshTrust) change(f func()) {
before := strings.Join(t.Trusts(), "\n")
t.mu.Lock()
f()
changed := t.changed
t.mu.Unlock()
if strings.Join(t.Trusts(), "\n") != before && changed != nil {
changed()
}
}
// heardSigner takes one key of the vault's `signers`: its key or its authority, refused (and said) when it does not
// check; a deleted key is no longer trusted.
func (t *meshTrust) heardSigner(key string, value []byte, deleted bool, say link.Announce) {
switch key {
case meshvault.SignersByKeyKey():
var pub ed25519.PublicKey
if !deleted && len(value) > 0 {
k, err := meshvaultclient.DecodeSignerKey(value)
if err == nil {
pub, err = meshvaultclient.SignerPublicKey(k)
}
if err != nil {
say("the vault's signing key in " + buckets.SignersRead + " is not trusted: " + err.Error())
return
}
}
t.change(func() { t.vault = pub })
case meshvault.SignersByAuthorityKey():
pem := ""
if !deleted && len(value) > 0 {
a, err := meshvaultclient.DecodeSignerAuthority(value)
if err != nil {
say("the vault's authority in " + buckets.SignersRead + " is not trusted: " + err.Error())
return
}
pem = a.Certificate
}
t.change(func() { t.authority = pem })
}
}
// trustFollowAgain bounds the wait before a watch that could not be made is tried again on the same link: the vault may
// not serve `sign` yet, and until it does this node is not granted the reads.
var trustFollowAgain = 2 * time.Minute
// followTrust follows the vault's `signers` on every link, as watches (never a poll), for as
// long as ctx lasts. A watch that cannot be made is said once per reason and tried again.
func followTrust(ctx context.Context, session *link.Session, trust *meshTrust, say link.Announce) {
said := ""
sayOnce := func(line string) {
if line != said {
said = line
if line != "" {
say(line)
}
}
}
for ctx.Err() == nil {
linked, cancel, err := session.WhenLinked(ctx)
if err != nil {
return
}
watching, stop := context.WithCancel(linked)
signers, serr := session.KVWatch(watching, buckets.Signers, ">")
if serr != nil {
stop()
sayOnce(fmt.Sprintf("what this node trusts a mesh signature from is not followed yet (the vault's signers: %v): "+
"it trusts what it last read, and looks again in %s", serr, trustFollowAgain))
} else {
sayOnce("")
for u := range signers {
if !u.Caught {
trust.heardSigner(u.Key, u.Value, u.Deleted, say)
}
}
}
stop()
select {
case <-linked.Done():
case <-time.After(trustFollowAgain):
}
cancel()
}
}
// placedAllHave says whether every module the seat mesh-module-assigner places on node has a have of its own, read from the
// bus: then the kept declaration is read no more (keptRetired). A node the seat does not place, or whose assignment or haves
// cannot be read, keeps reading it.
func placedAllHave(ctx context.Context, session *link.Session, node string) (bool, string) {
key, err := meshmoduleassignerclient.NodeKey(node)
if err != nil {
return false, err.Error()
}
// Its node key names every module placed (research 072 row 37).
values := map[string][]byte{}
if h, err := session.KVHistory(ctx, buckets.Assignments, key); err == nil && len(h) > 0 && !h[len(h)-1].Deleted {
values[key] = h[len(h)-1].Value
}
p, err := placedModules(node, values)
if err != nil {
return false, err.Error()
}
return allHave(p, func(module string) (nodeengine.HaveByNodeModuleValue, bool, error) {
return session.ReadHaveErr(ctx, module)
})
}
// placedModules is the modules node's node key names (an assignment of their names alone); an error when it does not
// read.
func placedModules(node string, values map[string][]byte) (meshmoduleassigner.Assignments, error) {
key, _ := meshmoduleassignerclient.NodeKey(node)
raw, ok := values[key]
if !ok {
return meshmoduleassigner.Assignments{}, errors.New("its assignment is not read")
}
n, err := meshmoduleassignerclient.DecodeNode(raw)
if err != nil {
return meshmoduleassigner.Assignments{}, err
}
p := meshmoduleassigner.Assignments{Node: node}
for _, r := range n.Modules {
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: r.Module})
}
return p, nil
}
// allHave says whether has finds, for every module p places, a have that names a fingerprint of the new form, and why
// not. A module whose have names only a build of the old form runs inside the node's own composition, and the kept
// declaration is the one record of what runs of it: retired then, the node's own composed it as nothing and refused to
// remove what runs (issue 774, 2026-10-30 19:43: systemd-resolved on shanks, nats and restic on novox, each node's own
// apply refused from then on).
func allHave(p meshmoduleassigner.Assignments, has func(module string) (nodeengine.HaveByNodeModuleValue, bool, error)) (bool, string) {
if len(p.Modules) == 0 {
return false, "the seat mesh-module-assigner places no module here"
}
for _, m := range p.Modules {
h, ok, err := has(m.Module)
if err != nil {
return false, "the have of " + m.Module + " is not read: " + err.Error()
}
if !ok {
return false, m.Module + " has no have yet"
}
// A module the mesh provides (mesh-wireguard) never has a fingerprint: the node's own composes it from the manifest
// mesh-sdk provides (overlay.ProvidedManifest), never from the kept declaration, so it keeps no read.
if h.Phase != nodeengineclient.PhaseRemoved && !strings.HasPrefix(h.Fp, meshcatalogclient.SourcePrefix2) && !overlay.ProvidedByMesh(m.Module) {
return false, m.Module + " runs at a build of the old form (" + h.Build + "), which only the kept declaration records"
}
}
return true, fmt.Sprintf("each of the %d modules placed here has a have of its own", len(p.Modules))
}
// certifyThisNode asks the vault's `sign` for this node's own certificate (kind node-certificate, ADR 0550 §2), checks
// it is for this node's internal name and serving key and signed by the authority the answer names, which must be the
// vault's this node trusts, keeps it where the record reads it, and writes the record again (written).
func certifyThisNode(ctx context.Context, state, node string, trust *meshTrust,
ask func(ctx context.Context, verb string, args any) (json.RawMessage, error), written func()) error {
serving, err := identity.LoadServingKey(identity.ServingKeyPath(state))
if err != nil {
return err
}
key, err := base64.StdEncoding.DecodeString(serving.Public)
if err != nil || len(key) != ed25519.PublicKeySize {
return fmt.Errorf("this node's serving key is not an ed25519 key")
}
raw, err := ask(ctx, meshvault.VerbSign, map[string]any{"kind": meshvaultclient.KindNodeCertificate, "node": node})
if err != nil {
return err
}
var a meshvault.SignAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return fmt.Errorf("the vault's answer is not a signing: %w", err)
}
trusted := trust.VaultAuthority()
if trusted == "" {
return fmt.Errorf("this node has read no authority of the vault's to check the certificate against")
}
if err := meshvaultclient.CheckNodeCertificate(a.Certificate, []string{trusted}, overlay.InternalName(node),
ed25519.PublicKey(key), time.Now()); err != nil {
return err
}
path := identity.CertificatePath(state)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
if err := os.WriteFile(path, []byte(a.Certificate), 0o644); err != nil {
return err
}
if written != nil {
written()
}
return nil
}