191 lines
6.5 KiB
Go
191 lines
6.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-host/internal/identity"
|
|
"github.com/novox/mesh-host/internal/link"
|
|
)
|
|
|
|
// Issue 786 (novox/hq ADR 0372): the engine states its own sealing key to the vault's node-key at start, retries
|
|
// while the vault does not answer, and states it again when the key changes; the same key is not stated twice.
|
|
type keyVault struct {
|
|
mu sync.Mutex
|
|
stated []string
|
|
fail int
|
|
verbs []string
|
|
// keyless is how many answers carry no vault key (a vault before ADR 0547 clause 9's, or not yet started); key the
|
|
// vault's key the others carry.
|
|
keyless int
|
|
key string
|
|
}
|
|
|
|
func (v *keyVault) ask(verb string, args any) (json.RawMessage, error) {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
v.verbs = append(v.verbs, verb)
|
|
if v.fail > 0 {
|
|
v.fail--
|
|
return nil, errors.New("the vault does not answer")
|
|
}
|
|
raw, _ := json.Marshal(args)
|
|
var a struct {
|
|
Key string `json:"key"`
|
|
}
|
|
_ = json.Unmarshal(raw, &a)
|
|
v.stated = append(v.stated, a.Key)
|
|
if v.key == "" || v.keyless > 0 {
|
|
v.keyless--
|
|
return json.RawMessage(`{"recorded":true}`), nil
|
|
}
|
|
answer, _ := json.Marshal(map[string]any{"recorded": true, "vault_key": v.key})
|
|
return answer, nil
|
|
}
|
|
|
|
func (v *keyVault) said() []string {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
return append([]string(nil), v.stated...)
|
|
}
|
|
|
|
func waitFor(t *testing.T, what string, ok func() bool) {
|
|
t.Helper()
|
|
for i := 0; i < 400; i++ {
|
|
if ok() {
|
|
return
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
t.Fatalf("timed out: %s", what)
|
|
}
|
|
|
|
func TestTheEngineStatesItsSealingKeyToTheVaultAtStartRetriesAndRestatesAChange(t *testing.T) {
|
|
first, _ := identity.GenerateSealingKey()
|
|
second, _ := identity.GenerateSealingKey()
|
|
var mu sync.Mutex
|
|
current := first
|
|
load := func() (identity.SealingKey, error) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
return current, nil
|
|
}
|
|
vault := &keyVault{fail: 2}
|
|
var lines []string
|
|
var lmu sync.Mutex
|
|
say := func(l string) { lmu.Lock(); lines = append(lines, l); lmu.Unlock() }
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
var hmu sync.Mutex
|
|
var answers []string
|
|
heard := func(a json.RawMessage) { hmu.Lock(); answers = append(answers, string(a)); hmu.Unlock() }
|
|
go keepNodeKeyStated(ctx, load, vault.ask, say, time.Millisecond, 5*time.Millisecond, &keyHearing{heard: heard})
|
|
|
|
waitFor(t, "the first key stated after two refusals", func() bool { return len(vault.said()) == 1 })
|
|
// Its answer is handed on, so the vault's own key in it is heard (ADR 0547 clause 9); refusals hand nothing.
|
|
waitFor(t, "the answer heard", func() bool { hmu.Lock(); defer hmu.Unlock(); return len(answers) == 1 })
|
|
if vault.said()[0] != first.Public {
|
|
t.Errorf("stated %q, want the engine's own key %q", vault.said()[0], first.Public)
|
|
}
|
|
vault.mu.Lock()
|
|
verbs := append([]string(nil), vault.verbs...)
|
|
vault.mu.Unlock()
|
|
for _, v := range verbs {
|
|
if v != "node-key" {
|
|
t.Errorf("asked %s", v)
|
|
}
|
|
}
|
|
// The same key again is not stated again.
|
|
time.Sleep(60 * time.Millisecond)
|
|
if n := len(vault.said()); n != 1 {
|
|
t.Errorf("the same key was stated %d times", n)
|
|
}
|
|
mu.Lock()
|
|
current = second
|
|
mu.Unlock()
|
|
waitFor(t, "the changed key stated", func() bool { return len(vault.said()) == 2 })
|
|
if vault.said()[1] != second.Public {
|
|
t.Errorf("stated %q after the change, want %q", vault.said()[1], second.Public)
|
|
}
|
|
lmu.Lock()
|
|
defer lmu.Unlock()
|
|
if len(lines) == 0 {
|
|
t.Error("the engine said nothing")
|
|
}
|
|
asksAgainUntilItHearsTheVaultsKey(t)
|
|
}
|
|
|
|
// ADR 0547 clause 9: the vault's key is heard though the engine's own key never changes. While none is held the key
|
|
// is stated again at each look, so a vault delivered after the engine (or answering no key yet) is heard; once one is
|
|
// held, again only at the slow interval, so a changed vault key is heard; never more.
|
|
func asksAgainUntilItHearsTheVaultsKey(t *testing.T) {
|
|
sealing, _ := identity.GenerateSealingKey()
|
|
load := func() (identity.SealingKey, error) { return sealing, nil }
|
|
pem := func(c string) string { return "-----BEGIN PUBLIC KEY-----\n" + c + "\n-----END PUBLIC KEY-----\n" }
|
|
for _, c := range []struct {
|
|
name string
|
|
keyless int
|
|
every time.Duration
|
|
change bool
|
|
wantAtLeast int
|
|
wantAtMost int
|
|
}{
|
|
{name: "a vault with no key yet is asked again until it answers one, then not", keyless: 3, wantAtLeast: 4, wantAtMost: 4},
|
|
{name: "a held key is asked again at the slow interval, and a changed one heard", every: 20 * time.Millisecond,
|
|
change: true, wantAtLeast: 3, wantAtMost: 1 << 20},
|
|
} {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
vault := &keyVault{keyless: c.keyless, key: pem("AAAA")}
|
|
h := &heardKey{}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
go keepNodeKeyStated(ctx, load, vault.ask, func(string) {}, time.Millisecond, 5*time.Millisecond,
|
|
&keyHearing{heard: h.heard, held: func() bool { return h.get() != "" }, look: time.Millisecond, every: c.every})
|
|
waitFor(t, "the vault's key heard", func() bool { return h.get() == pem("AAAA") })
|
|
if c.change {
|
|
vault.mu.Lock()
|
|
vault.key = pem("BBBB")
|
|
vault.mu.Unlock()
|
|
waitFor(t, "the changed vault key heard", func() bool { return h.get() == pem("BBBB") })
|
|
}
|
|
time.Sleep(60 * time.Millisecond)
|
|
n := len(vault.said())
|
|
if n < c.wantAtLeast || n > c.wantAtMost {
|
|
t.Errorf("node-key asked %d times, want %d to %d", n, c.wantAtLeast, c.wantAtMost)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The statement a host makes names this contract (issue 786), one above the vault's box. And the vault's key its
|
|
// answer gives is kept, told to the follower once when first heard and again only when changed; an answer without one
|
|
// (an older vault) or with no public key changes nothing (ADR 0547 clause 9).
|
|
func TestTheNodeKeyContractIsThirteen(t *testing.T) {
|
|
if link.NodeKeyContract != 13 {
|
|
t.Errorf("NodeKeyContract %d", link.NodeKeyContract)
|
|
}
|
|
h := &heardKey{}
|
|
kicks := 0
|
|
h.following(func() { kicks++ })
|
|
h.heard(json.RawMessage(`{"recorded":true}`))
|
|
h.heard(json.RawMessage(`{"recorded":true,"vault_key":"not a key"}`))
|
|
if h.get() != "" || kicks != 0 {
|
|
t.Fatalf("an answer with no key was kept: %q, %d kicks", h.get(), kicks)
|
|
}
|
|
pem := "-----BEGIN PUBLIC KEY-----\nAAAA\n-----END PUBLIC KEY-----\n"
|
|
answer, _ := json.Marshal(map[string]any{"recorded": true, "vault_key": pem})
|
|
h.heard(answer)
|
|
h.heard(answer)
|
|
if h.get() != pem || kicks != 1 {
|
|
t.Fatalf("key %q, %d kicks, want the key and one", h.get(), kicks)
|
|
}
|
|
h.following(func() { kicks += 10 })
|
|
if kicks != 11 {
|
|
t.Fatalf("a follower named after the key was heard was not told: %d", kicks)
|
|
}
|
|
}
|