Files
mesh-host/cmd/mesh-host/trust.go
T
jschoubben 5cfc4ff4a7 Trust the vault's key alone and write the grants part's user list alone
The controller ended by its unassignment and its lease is never handed, so
the engine's lease gate kept trusting the controller's key and the bus node
kept the controller's list as a fallback nobody writes (issue 1126, A6).
The engine now trusts no controller signature, its hand-over line names the
node seat's hand-over, and the user-list follower writes only the grants
part's list: a key unreadable or deleted keeps the list loaded, and with no
key heard nothing is written. Compositions in the node's bucket name the
file's place alone. The guard no longer asks for the controller's user.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 10:37:27 +01:00

294 lines
10 KiB
Go

package main
import (
"context"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/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 placement'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{} }
// noControllerKey is the controller's key as this engine trusts it: none. A link told it refuses every ask signed as
// the controller's, where a link told nothing would verify it with the membership's signer (link.HandOverCheck).
func noControllerKey() ed25519.PublicKey { return nil }
// 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() []delivery.Signer {
t.mu.Lock()
defer t.mu.Unlock()
var out []delivery.Signer
if t.vault != nil {
out = append(out, delivery.Signer{Name: delivery.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) (delivery.Signer, error) {
context, err := delivery.SignContext(kind)
if err != nil {
return delivery.Signer{}, err
}
return delivery.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[delivery.SignerVault] = a
}
return delivery.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 delivery.SignersKeyKey:
var pub ed25519.PublicKey
if !deleted && len(value) > 0 {
k, err := delivery.DecodeSignerKey(value)
if err == nil {
pub, err = k.PublicKey()
}
if err != nil {
say("the vault's signing key in " + delivery.SignersRead + " is not trusted: " + err.Error())
return
}
}
t.change(func() { t.vault = pub })
case delivery.SignersAuthorityKey:
pem := ""
if !deleted && len(value) > 0 {
a, err := delivery.DecodeSignerAuthority(value)
if err != nil {
say("the vault's authority in " + delivery.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, delivery.SignersBucket, ">")
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-placement 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 placement or haves
// cannot be read, keeps reading it.
func placedAllHave(ctx context.Context, session *link.Session, node string) (bool, string) {
key, err := delivery.PlacementNodeKey(node)
if err != nil {
return false, err.Error()
}
legacy, _ := delivery.LegacyPlacementKey(node)
// Its node key names every module placed (research 072 row 37); before the holder converted it, its legacy key does.
values := map[string][]byte{}
for _, k := range []string{key, legacy} {
if h, err := session.KVHistory(ctx, delivery.PlacementBucket, k); err == nil && len(h) > 0 && !h[len(h)-1].Deleted {
values[k] = h[len(h)-1].Value
}
}
p, err := placedModules(node, values)
if err != nil {
return false, err.Error()
}
return allHave(p, func(module string) (delivery.Have, bool, error) {
return session.ReadHaveErr(ctx, module)
})
}
// placedModules is the modules node's node key names (a placement of their names alone), or its legacy key's placement
// where it has no node key; an error when neither reads.
func placedModules(node string, values map[string][]byte) (delivery.Placement, error) {
key, _ := delivery.PlacementNodeKey(node)
if raw, ok := values[key]; ok {
n, err := delivery.DecodePlacementNode(raw)
if err != nil {
return delivery.Placement{}, err
}
p := delivery.Placement{Node: node}
for _, r := range n.Modules {
p.Modules = append(p.Modules, delivery.PlacedModule{Module: r.Module})
}
return p, nil
}
legacy, _ := delivery.LegacyPlacementKey(node)
raw, ok := values[legacy]
if !ok {
return delivery.Placement{}, errors.New("its placement is not read")
}
return delivery.DecodePlacement(raw)
}
// 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 delivery.Placement, has func(module string) (delivery.Have, bool, error)) (bool, string) {
if len(p.Modules) == 0 {
return false, "the seat mesh-placement 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 (delivery.ProvidedManifest), never from the kept declaration, so it keeps no read.
if h.Phase != delivery.PhaseRemoved && !strings.HasPrefix(h.FP, delivery.SourcePrefix2) && !delivery.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, delivery.SignVerb, map[string]any{"kind": delivery.KindNodeCertificate, "node": node})
if err != nil {
return err
}
var a delivery.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 := delivery.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
}