An operator ran `mesh-host reconcile` on an adopted control-node with twelve modules assigned. It applied the bundle the host carries — the genesis declaration, foundation only, converged: recreated the store, failed on the broker's held port, wrote the converged base filter and started its service, and stopped at the first failing action. The filter closed the machine for forty-five minutes. The host reported the node adopted in every report, the declaration said converged, and nothing compared the two; nothing was printed before acting (hq issue 104). The host now records the node's mode — from every declaration the mesh sends, and at genesis from what the operator said — and refuses, at the point of application, a declaration that says the other mode, naming both and the act that changes it. Only a declaration the link delivers, signed, changes the mode: that is how `converge` and `adopt` arrive, so the flip still works and nothing else can do it. Genesis marks the bundle consumed, with the digest of what it applied, so `reconcile` holds a node the mesh has spoken to against what the mesh last said and never the bundle, and refuses the carried bytes when they are not what genesis applied. A file is refused when it is not what the mesh last said: a declaration carries no sequence and no issued-at, so the host cannot tell older from newer, and says so. Both commands print what they would change — a hold, a removal, an action named as one — before touching anything, and --dry-run is that list and nothing more.
128 lines
4.4 KiB
Go
128 lines
4.4 KiB
Go
package store
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// What the mesh last told this node to be, kept so it can go on being it.
|
|
//
|
|
// novox/hq ADR 0004: a disconnected node keeps reconciling against its own store, so it holds its
|
|
// machine in the last state it was told. A laptop shut for a week comes back and reconciles; it
|
|
// does not come back and ask what it is.
|
|
//
|
|
// That needs the declaration itself. The record of what was *applied* is not enough to re-apply:
|
|
// it holds an id, a type and a target, which is what removal needs and not what creation needs.
|
|
// So the declaration is kept whole.
|
|
//
|
|
// **Kept signed, and verified again on every load.** The signature is not decoration here: this
|
|
// file is on a machine, and a node that read it back unverified would apply whatever was in it.
|
|
// Anyone able to write it already has root — but the check costs nothing, and it means the file
|
|
// is trusted for the same reason the message was, rather than for being local.
|
|
|
|
// DeclaredName is where it lives, beside the state.
|
|
const DeclaredName = "declared.json"
|
|
|
|
// DeclaredPath is where the last declaration lives, given where the state lives.
|
|
func DeclaredPath(statePath string) string {
|
|
return filepath.Join(filepath.Dir(statePath), DeclaredName)
|
|
}
|
|
|
|
// Declared is the last thing the mesh said, and the signature it came with.
|
|
type Declared struct {
|
|
Declaration []byte `json:"declaration"`
|
|
Signature []byte `json:"signature"`
|
|
}
|
|
|
|
// ErrNothingDeclared means the mesh has never told this node anything.
|
|
//
|
|
// An ordinary state, not a fault: a node that has enrolled and not yet been sent a declaration
|
|
// has nothing to reconcile against, and that is different from having lost it.
|
|
var ErrNothingDeclared = errors.New("the mesh has not told this node anything yet")
|
|
|
|
// SaveDeclared keeps what the mesh said, so a disconnected node can go on obeying it.
|
|
func SaveDeclared(path string, d Declared) error {
|
|
if len(d.Declaration) == 0 {
|
|
return errors.New("refusing to keep an empty declaration")
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
return err
|
|
}
|
|
raw, err := json.Marshal(d)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tmp, err := os.CreateTemp(filepath.Dir(path), ".declared-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer os.Remove(tmp.Name())
|
|
if err := tmp.Chmod(0o600); err != nil {
|
|
tmp.Close()
|
|
return err
|
|
}
|
|
if _, err := tmp.Write(raw); err != nil {
|
|
tmp.Close()
|
|
return err
|
|
}
|
|
if err := tmp.Sync(); err != nil {
|
|
tmp.Close()
|
|
return err
|
|
}
|
|
if err := tmp.Close(); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmp.Name(), path)
|
|
}
|
|
|
|
// ReadDeclared reads what was kept without proving it is the mesh's.
|
|
//
|
|
// For naming and comparing only — which declaration this node was last told, and what mode it
|
|
// said — never for applying. A declaration to apply goes through LoadDeclared, which verifies.
|
|
func ReadDeclared(path string) (Declared, error) {
|
|
raw, err := os.ReadFile(path)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return Declared{}, ErrNothingDeclared
|
|
}
|
|
if err != nil {
|
|
return Declared{}, fmt.Errorf("this node was told something and cannot read it back: %w", err)
|
|
}
|
|
var d Declared
|
|
if err := json.Unmarshal(raw, &d); err != nil {
|
|
return Declared{}, fmt.Errorf("what this node was told is unreadable at %s: %w", path, err)
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// LoadDeclared reads it back and proves it is still the mesh's.
|
|
//
|
|
// Verified against the signing key this node holds, which came from its token. A declaration on
|
|
// disk that does not verify is refused rather than applied: either the file was changed, or this
|
|
// node now believes a different mesh — and applying it either way would be applying something
|
|
// nobody in this mesh said.
|
|
func LoadDeclared(path string, signer ed25519.PublicKey) ([]byte, error) {
|
|
raw, err := os.ReadFile(path)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil, ErrNothingDeclared
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("this node was told something and cannot read it back: %w", err)
|
|
}
|
|
|
|
var d Declared
|
|
if err := json.Unmarshal(raw, &d); err != nil {
|
|
return nil, fmt.Errorf("what this node was told is unreadable at %s: %w", path, err)
|
|
}
|
|
if !ed25519.Verify(signer, d.Declaration, d.Signature) {
|
|
return nil, fmt.Errorf(
|
|
"what this node kept at %s is not signed by the mesh it joined. It will not be "+
|
|
"applied — either the file was changed, or this node's signing key was", path)
|
|
}
|
|
return d.Declaration, nil
|
|
}
|