143 lines
4.8 KiB
Go
143 lines
4.8 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-host/internal/identity"
|
|
)
|
|
|
|
// This machine's sealing key, stated to the vault in the engine's own name (novox/hq ADR 0372, issue 786). The vault
|
|
// records it from the machine's own account alone and, when it is a first or a changed key, says the machine's core
|
|
// secrets changed so the controller asks for them again sealed to it. Without this a machine that never moved a
|
|
// secret in (adopted after feature 57) has no key at the vault. Nothing but the public half crosses.
|
|
|
|
const (
|
|
// nodeKeyRetryFrom and nodeKeyRetryUpTo bound the wait between tries while the vault does not answer: it starts
|
|
// after the bus and may not serve the verb yet.
|
|
nodeKeyRetryFrom = 5 * time.Second
|
|
nodeKeyRetryUpTo = 5 * time.Minute
|
|
// nodeKeyLook is how often the key file is read to see whether it changed.
|
|
nodeKeyLook = 30 * time.Second
|
|
)
|
|
|
|
// heardKey is the vault's own public key as its answer to node-key gave it (ADR 0547 clause 9, issue 1945): the key the
|
|
// bus node hands the loaded list's hashes sealed to. One for the process, as the key stated is; kick is the follower of
|
|
// the user list on the link open now, told when the key is first heard or changes.
|
|
type heardKey struct {
|
|
mu sync.Mutex
|
|
pem string
|
|
kick func()
|
|
}
|
|
|
|
// vaultKey is the vault's key this engine heard.
|
|
var vaultKey = &heardKey{}
|
|
|
|
func (h *heardKey) get() string {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
return h.pem
|
|
}
|
|
|
|
// heard keeps the key a node-key answer gave, and tells the follower when it is a first or changed one. An answer
|
|
// with no key (a vault before this one) changes nothing.
|
|
func (h *heardKey) heard(answer json.RawMessage) {
|
|
var a struct {
|
|
VaultKey string `json:"vault_key"`
|
|
}
|
|
if json.Unmarshal(answer, &a) != nil || !strings.Contains(a.VaultKey, "PUBLIC KEY") {
|
|
return
|
|
}
|
|
h.mu.Lock()
|
|
changed := a.VaultKey != h.pem
|
|
h.pem = a.VaultKey
|
|
kick := h.kick
|
|
h.mu.Unlock()
|
|
if changed && kick != nil {
|
|
kick()
|
|
}
|
|
}
|
|
|
|
// following names the follower told when the key changes, and tells it at once when a key is known.
|
|
func (h *heardKey) following(kick func()) {
|
|
h.mu.Lock()
|
|
h.kick = kick
|
|
known := h.pem != ""
|
|
h.mu.Unlock()
|
|
if known && kick != nil {
|
|
kick()
|
|
}
|
|
}
|
|
|
|
// nodeKeyReask is how often the key is stated again though it did not change, so a changed vault key is heard: the
|
|
// vault takes the same key again and changes nothing (ADR 0547 clause 9).
|
|
const nodeKeyReask = 10 * time.Minute
|
|
|
|
// keyHearing is what the keeper does with the vault's own key in node-key's answers (ADR 0547 clause 9): heard takes
|
|
// it from an answer; held says whether one is held, and while none is the key is stated again at every look, so a vault
|
|
// delivered after this engine is heard within a look; every is how often it is stated again all the same, so a changed
|
|
// vault key is heard.
|
|
type keyHearing struct {
|
|
heard func(json.RawMessage)
|
|
held func() bool
|
|
// look is how often it is stated again while no vault key is held; every how often once one is.
|
|
look, every time.Duration
|
|
}
|
|
|
|
// keepNodeKeyStated states the sealing key load answers to the vault at start, retrying with a growing wait from
|
|
// retryFrom to retryUpTo while it does not answer, and states it again whenever the key changes. It says one line
|
|
// when a key is stated and one when a try fails for another reason than the last, and returns when ctx ends. With
|
|
// hearing, each answer goes to it, and the key is stated again while no vault key is held and at its interval.
|
|
func keepNodeKeyStated(ctx context.Context, load func() (identity.SealingKey, error), ask vaultAsk, say func(string),
|
|
retryFrom, retryUpTo time.Duration, hearing *keyHearing) {
|
|
stated, lastFailure := "", ""
|
|
var askedAt time.Time
|
|
wait := retryFrom
|
|
fail := func(why string) {
|
|
if why != lastFailure {
|
|
say("mesh-host: the sealing key was not stated to the vault, trying again: " + why)
|
|
lastFailure = why
|
|
}
|
|
wait = min(wait*2, retryUpTo)
|
|
}
|
|
look := min(nodeKeyLook, retryFrom)
|
|
for {
|
|
delay := look
|
|
key, err := load()
|
|
again := false
|
|
if hearing != nil && stated != "" {
|
|
since := time.Since(askedAt)
|
|
held := hearing.held == nil || hearing.held()
|
|
again = (!held && since >= hearing.look) || (held && hearing.every > 0 && since >= hearing.every)
|
|
}
|
|
switch {
|
|
case err != nil:
|
|
fail(err.Error())
|
|
delay = wait
|
|
case key.Public != stated || again:
|
|
if answer, err := ask("node-key", map[string]string{"key": key.Public}); err != nil {
|
|
fail(err.Error())
|
|
delay = wait
|
|
} else {
|
|
askedAt = time.Now()
|
|
if hearing != nil && hearing.heard != nil {
|
|
hearing.heard(answer)
|
|
}
|
|
if key.Public != stated {
|
|
say(fmt.Sprintf("mesh-host: this machine's sealing key %s is stated to the vault", key.Public))
|
|
}
|
|
stated, lastFailure, wait = key.Public, "", retryFrom
|
|
}
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-time.After(delay):
|
|
}
|
|
}
|
|
}
|