C2: a key pair the manager cannot read is made anew in its place (issue 1409)
mesh/delivery merged: source.main moved to novox/mesh-catalog@6181d9afb3e5
mesh/check checking 8291502cbbaf on 6181d9afb3e5
mesh/delivery merged: source.main moved to novox/mesh-catalog@6181d9afb3e5
mesh/check checking 8291502cbbaf on 6181d9afb3e5
Moved to its own account, the manager finds manager-key.json still owned by the tool runner's account, which the node-engine does not hand over; it skipped making a pair because the file existed, and every verb answered the read error. Now a pair it cannot read is replaced by a new one, renamed over it, and said in its journal; every use passes the public key per ask, so nothing depends on the old pair. Builds: design 39 bus-and-switch C2 Claude-Session: https://claude.ai/code/session_01NcbgfHb9d4T9nq5Q6WqdiJ
This commit is contained in:
@@ -60,7 +60,7 @@ func manager() (*Manager, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys, err := keypair(dir)
|
||||
keys, err := keypair(dir, say)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -131,28 +131,60 @@ func configLevels(node string) (map[string]string, error) {
|
||||
}
|
||||
|
||||
// keypair is this module's own, made once in its state directory; the private half never leaves it.
|
||||
// Written whole, then linked into place, so a second process reads the first's key and never half of it.
|
||||
func keypair(dir string) (KeyPair, error) {
|
||||
// Written whole, then linked into place, so a second process reads the first's key and never half of it. A pair it
|
||||
// finds and cannot read (another account's, left before the module ran as its own) is made anew in its place and said
|
||||
// in its journal (design 39 bus-and-switch C2): every use passes the public key per ask, so nothing depends on the old.
|
||||
func keypair(dir string, log func(string, ...any)) (KeyPair, error) {
|
||||
file := filepath.Join(dir, "manager-key.json")
|
||||
if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) {
|
||||
k, err := GenerateKeyPair()
|
||||
if err != nil {
|
||||
if err := writeKeyPair(dir, file, false); err != nil {
|
||||
return KeyPair{}, err
|
||||
}
|
||||
raw, _ := json.Marshal(k)
|
||||
tmp := filepath.Join(dir, fmt.Sprintf(".manager-key.%d.json", os.Getpid()))
|
||||
if err := os.WriteFile(tmp, raw, 0o600); err != nil {
|
||||
return KeyPair{}, err
|
||||
}
|
||||
_ = os.Link(tmp, file) // fails when another made it first, which is right
|
||||
_ = os.Remove(tmp)
|
||||
}
|
||||
k, err := readKeyPair(file)
|
||||
if err == nil {
|
||||
return k, nil
|
||||
}
|
||||
log("the key pair %s cannot be read (%v); a new one is made in its place", file, err)
|
||||
if err := writeKeyPair(dir, file, true); err != nil {
|
||||
return KeyPair{}, err
|
||||
}
|
||||
return readKeyPair(file)
|
||||
}
|
||||
|
||||
func readKeyPair(file string) (KeyPair, error) {
|
||||
raw, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return KeyPair{}, err
|
||||
}
|
||||
var k KeyPair
|
||||
return k, json.Unmarshal(raw, &k)
|
||||
if err := json.Unmarshal(raw, &k); err != nil {
|
||||
return KeyPair{}, err
|
||||
}
|
||||
if k.PublicKey == "" || k.PrivateKey == "" {
|
||||
return KeyPair{}, errors.New("it holds no whole key pair")
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
|
||||
// writeKeyPair writes a new pair whole beside file, then links it into place, or, replacing, renames it over the one
|
||||
// that cannot be read.
|
||||
func writeKeyPair(dir, file string, replace bool) error {
|
||||
k, err := GenerateKeyPair()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
raw, _ := json.Marshal(k)
|
||||
tmp := filepath.Join(dir, fmt.Sprintf(".manager-key.%d.json", os.Getpid()))
|
||||
if err := os.WriteFile(tmp, raw, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
if replace {
|
||||
return os.Rename(tmp, file)
|
||||
}
|
||||
_ = os.Link(tmp, file) // fails when another made it first, which is right
|
||||
_ = os.Remove(tmp)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- the long-running half ---------------------------------------------------------------------------
|
||||
|
||||
@@ -2,7 +2,10 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -41,3 +44,29 @@ func TestABoxOpensOnlyForItsRecipient(t *testing.T) {
|
||||
t.Fatal("a box opened for another key")
|
||||
}
|
||||
}
|
||||
|
||||
// design 39 bus-and-switch C2: a key pair the manager finds and cannot read (left by another account, here mode 000) is
|
||||
// replaced in its place by a new one, said in its journal; the new one is read back by the next start.
|
||||
func TestAKeyPairItCannotReadIsMadeAnew(t *testing.T) {
|
||||
if os.Geteuid() == 0 {
|
||||
t.Fatal("run as root, a file of mode 000 is still read: this test needs an ordinary account")
|
||||
}
|
||||
dir := t.TempDir()
|
||||
file := filepath.Join(dir, "manager-key.json")
|
||||
if err := os.WriteFile(file, []byte(`{"publicKey":"old","privateKey":"old"}`), 0o000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var said []string
|
||||
log := func(format string, a ...any) { said = append(said, fmt.Sprintf(format, a...)) }
|
||||
k, err := keypair(dir, log)
|
||||
if err != nil {
|
||||
t.Fatalf("a key pair it cannot read stops the manager: %v", err)
|
||||
}
|
||||
if k.PublicKey == "" || k.PublicKey == "old" || len(said) != 1 || !strings.Contains(said[0], "manager-key.json") {
|
||||
t.Fatalf("not made anew and said: key %q, journal %q", k.PublicKey, said)
|
||||
}
|
||||
again, err := keypair(dir, log)
|
||||
if err != nil || again != k || len(said) != 1 {
|
||||
t.Fatalf("the new pair is not the one read at the next start: %v, %v, %q", again == k, err, said)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user