291 lines
10 KiB
Go
291 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-contracts/identity-and-access/go/seats/meshvault"
|
|
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
|
|
"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{} }
|
|
|
|
// 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, meshvault.VerbSign, map[string]any{"kind": delivery.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 := 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
|
|
}
|