318 lines
10 KiB
Go
318 lines
10 KiB
Go
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) (bool, error) {
|
|
_, ok, err := session.ReadHaveErr(ctx, module)
|
|
return ok, err
|
|
})
|
|
}
|
|
|
|
// allHave says whether has finds a have for every module p places, and why not.
|
|
func allHave(p delivery.Placement, has func(module string) (bool, error)) (bool, string) {
|
|
if len(p.Modules) == 0 {
|
|
return false, "the seat mesh-placement places no module here"
|
|
}
|
|
for _, m := range p.Modules {
|
|
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"
|
|
}
|
|
}
|
|
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
|
|
}
|