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 }