package main import ( "context" "crypto/ed25519" "encoding/base64" "encoding/json" "fmt" "os" "path/filepath" "strings" "sync" "time" "git.novox.be/novox/mesh-sdk/go/buscompose" "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). Two signers: the // controller's key, which the node was enrolled with (its membership's signer), and the vault's key, 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 // lead's design limit, 2026-10-28). The controller's key is trusted until its lease is handed away, heard on the lease key // (buscompose.LeaseHandedAway) and never on a clock; from then the vault's alone. What it trusts now the node states in its // record (NodeRecord.Trusts), so the gate before the lease write reads every node's from the bus. type meshTrust struct { mu sync.Mutex controller ed25519.PublicKey handedAway bool vault ed25519.PublicKey authority string // changed is told when what it trusts changed, so the record is written again. changed func() } // newMeshTrust trusts the controller's key alone until the vault's is read. func newMeshTrust(controller ed25519.PublicKey) *meshTrust { t := &meshTrust{} if len(controller) == ed25519.PublicKeySize { t.controller = controller } return t } // 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() } // Controller is the controller's key while the controller holds its lease, and nil once the lease is handed away. func (t *meshTrust) Controller() ed25519.PublicKey { if t == nil { return nil } t.mu.Lock() defer t.mu.Unlock() if t.handedAway { return nil } return t.controller } // HandedAway says the controller's lease was handed away, as heard on the lease key. func (t *meshTrust) HandedAway() bool { if t == nil { return false } t.mu.Lock() defer t.mu.Unlock() return t.handedAway } // Signers are the keys a mesh signature is verified against now: the controller's while it holds the lease, and the // vault's once read. func (t *meshTrust) Signers() []delivery.Signer { t.mu.Lock() defer t.mu.Unlock() var out []delivery.Signer if t.controller != nil && !t.handedAway { out = append(out, delivery.Signer{Name: delivery.SignerController, Key: t.controller}) } 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 }) } } // heardLease takes the controller's lease key as read: once handed away to a holder that is no controller, the // controller's key is trusted no more. A lease that cannot be read changes nothing. func (t *meshTrust) heardLease(raw []byte, found bool, readErr error) { if readErr != nil { return } handed, _ := buscompose.LeaseHandedAway(raw, found, nil) t.change(func() { t.handedAway = handed }) } // 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` and the controller's lease 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, ">") lease, lerr := session.KVWatch(watching, buscompose.LeaseBucket, buscompose.ControllerLeaseKey) if serr != nil || lerr != nil { stop() sayOnce(fmt.Sprintf("what this node trusts a mesh signature from is not followed yet (the vault's signers: %v; "+ "the controller's lease: %v): it trusts what it last read, and looks again in %s", serr, lerr, trustFollowAgain)) signers, lease = nil, nil } else { sayOnce("") } for signers != nil || lease != nil { select { case u, ok := <-signers: if !ok { signers = nil continue } if !u.Caught { trust.heardSigner(u.Key, u.Value, u.Deleted, say) } case u, ok := <-lease: if !ok { lease = nil continue } if !u.Caught { trust.heardLease(u.Value, !u.Deleted && len(u.Value) > 0, nil) } } } 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.PlacementKey(node) if err != nil { return false, err.Error() } h, err := session.KVHistory(ctx, delivery.PlacementBucket, key) if err != nil || len(h) == 0 || h[len(h)-1].Deleted { return false, "its placement is not read" } p, err := delivery.DecodePlacement(h[len(h)-1].Value) if err != nil { return false, err.Error() } return allHave(p, func(module string) (delivery.Have, bool, error) { return session.ReadHaveErr(ctx, module) }) } // 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 }