Every signed ask is verified with the controller's key the engine trusts at that ask, so after the lease write none signed by it is taken (issue 774, ADR 0563 §7, ADR 0550 §2; rev-l3 round 1)
This commit is contained in:
@@ -1112,6 +1112,8 @@ func runLink(ctx context.Context, opts options) error {
|
||||
Membership: membership,
|
||||
// And the public halves and certificate its record says (novox/hq ADR 0555 §3), read at each facts report.
|
||||
Keys: recordKeys(opts.state, trust.Trusts),
|
||||
// And the controller's key, read per signed ask: none once its lease is handed away (novox/hq ADR 0563 §7).
|
||||
ControllerKey: trust.Controller,
|
||||
// And the first have this build writes is what proves it to the launcher (ADR 0539 §9, ADR 0227 §8).
|
||||
HaveWritten: proof.haveWritten,
|
||||
// And the operator's hand-over of a directory used as found, carried by the controller from the
|
||||
|
||||
@@ -76,9 +76,13 @@ const MaxHandOverBy = 200
|
||||
// HandOverCheck is what an ask is held to: the mesh's key, this node's name, the clock, and the nonces taken.
|
||||
type HandOverCheck struct {
|
||||
Signer ed25519.PublicKey
|
||||
Node string
|
||||
Now func() time.Time
|
||||
Nonces *Nonces
|
||||
// SignerNow, when set, is the controller's key as the engine trusts it at this ask, read per ask in place of Signer:
|
||||
// nil once the controller's lease is handed away, which refuses every ask signed by its key (novox/hq ADR 0563 §7,
|
||||
// ADR 0550 §2).
|
||||
SignerNow func() ed25519.PublicKey
|
||||
Node string
|
||||
Now func() time.Time
|
||||
Nonces *Nonces
|
||||
}
|
||||
|
||||
// Nonces are the nonces of asks taken, each kept until its ask expires: an ask is taken once. Kept in memory —
|
||||
@@ -157,8 +161,14 @@ func believedBytesWithin(check HandOverCheck, context string, withPath bool, bod
|
||||
if err := json.Unmarshal(body, &signed); err != nil {
|
||||
return HandOverAsk{}, nil, "the ask is not a signed ask: " + err.Error()
|
||||
}
|
||||
if len(check.Signer) != ed25519.PublicKeySize ||
|
||||
!ed25519.Verify(check.Signer, append([]byte(context), signed.Ask...), signed.Signature) {
|
||||
signer := check.Signer
|
||||
if check.SignerNow != nil {
|
||||
if signer = check.SignerNow(); signer == nil {
|
||||
return HandOverAsk{}, nil, "the controller's lease is handed away, and no ask signed by its key is taken"
|
||||
}
|
||||
}
|
||||
if len(signer) != ed25519.PublicKeySize ||
|
||||
!ed25519.Verify(signer, append([]byte(context), signed.Ask...), signed.Signature) {
|
||||
return HandOverAsk{}, nil, "the ask is not signed by the mesh"
|
||||
}
|
||||
var ask HandOverAsk
|
||||
|
||||
@@ -148,6 +148,21 @@ func TestAHandOverAskIsBelievedOnlySignedForThisNodeUnexpiredAndOnce(t *testing.
|
||||
if strings.Join(given, "; ") != "/srv/notes by jo through mesh-cli on anchor; /refused by jo through mesh-cli on anchor" {
|
||||
t.Fatalf("the handler was given %v", given)
|
||||
}
|
||||
// ADR 0563 §7, ADR 0550 §2: after the lease write the engine answers no ask signed by the controller's key. The key
|
||||
// is read per ask from what the engine trusts now; nil (the lease handed away) refuses, and nothing is handed over.
|
||||
trusted := m.Signer
|
||||
following := HandOverCheck{Signer: m.Signer, SignerNow: func() ed25519.PublicKey { return trusted }, Node: m.Node,
|
||||
Now: func() time.Time { return now }, Nonces: &Nonces{}}
|
||||
before := len(given)
|
||||
trusted = nil
|
||||
late := signedAsk(t, key, with(func(a *HandOverAsk) { a.Expires = now.Add(time.Minute); a.Nonce = strings.Repeat("cd", 16) }))
|
||||
if a := AnswerHandOver(following, handle, late); !strings.Contains(a.Refused, "lease is handed away") || len(given) != before {
|
||||
t.Fatalf("after the lease write an ask signed by the controller was answered %+v", a)
|
||||
}
|
||||
trusted = m.Signer
|
||||
if a := AnswerHandOver(following, handle, late); a.Refused != "" || len(given) != before+1 {
|
||||
t.Fatalf("while the controller holds the lease its ask was answered %+v", a)
|
||||
}
|
||||
// A nonce is forgotten once its ask has expired, so the set does not grow for ever.
|
||||
now = now.Add(HandOverGood + time.Second)
|
||||
check.Nonces.take("ff", now.Add(time.Second), now)
|
||||
@@ -177,17 +192,23 @@ func TestAHandOverIsAnsweredOnlyWhenItCarriesAReply(t *testing.T) {
|
||||
// handingLink is a quiet link that hears hand-overs, keeping the handler it was given.
|
||||
type handingLink struct {
|
||||
*quietLink
|
||||
given chan HandOverFunc
|
||||
given chan HandOverFunc
|
||||
signer chan func() ed25519.PublicKey
|
||||
}
|
||||
|
||||
func (l handingLink) HearHandOvers(h HandOverFunc) { l.given <- h }
|
||||
|
||||
// FollowSigner keeps what the link is told to verify the controller's asks with.
|
||||
func (l handingLink) FollowSigner(f func() ed25519.PublicKey) { l.signer <- f }
|
||||
|
||||
// **The link is given the queue's hand-over** when it is served: what the controller asks reaches the engine's
|
||||
// judgement, and nothing else.
|
||||
func TestALinkThatHearsHandOversIsGivenTheQueues(t *testing.T) {
|
||||
m, _ := aMember(t)
|
||||
l := handingLink{quietLink: newQuietLink(), given: make(chan HandOverFunc, 1)}
|
||||
l := handingLink{quietLink: newQuietLink(), given: make(chan HandOverFunc, 1), signer: make(chan func() ed25519.PublicKey, 1)}
|
||||
q := &Session{Membership: m, Timeout: time.Second}
|
||||
// The link verifies the controller's asks with the key the engine trusts now (ADR 0563 §7).
|
||||
q.ControllerKey = func() ed25519.PublicKey { return nil }
|
||||
q.HandOver = func(path, by string) (string, error) { return "the queue's: " + path + " by " + by, nil }
|
||||
ctx, stop := context.WithCancel(context.Background())
|
||||
served := make(chan error, 1)
|
||||
@@ -200,6 +221,14 @@ func TestALinkThatHearsHandOversIsGivenTheQueues(t *testing.T) {
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("the link was never given the queue's hand-over")
|
||||
}
|
||||
select {
|
||||
case f := <-l.signer:
|
||||
if f() != nil {
|
||||
t.Fatal("the link was given another key than the queue's")
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("the link was never given the key the engine trusts now")
|
||||
}
|
||||
stop()
|
||||
<-served
|
||||
// 774 (ADR 0563 §1): a call of the node seat reaches its verb with its caller as the subject proves it, unproven on
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
@@ -36,11 +37,14 @@ func natsURL(address string) string {
|
||||
|
||||
// natsLink is this node's connection as a JetStream subscription.
|
||||
type natsLink struct {
|
||||
conn *nats.Conn
|
||||
js nats.JetStreamContext
|
||||
asking *nats.Subscription
|
||||
asked chan struct{}
|
||||
handing *nats.Subscription
|
||||
// mu guards signerNow (FollowSigner).
|
||||
mu sync.Mutex
|
||||
signerNow func() ed25519.PublicKey
|
||||
conn *nats.Conn
|
||||
js nats.JetStreamContext
|
||||
asking *nats.Subscription
|
||||
asked chan struct{}
|
||||
handing *nats.Subscription
|
||||
// searching hears the controller's terminal asking for a fresh setuid search (novox/hq issue 361).
|
||||
searching *nats.Subscription
|
||||
// judging hears the controller asking for a read-only judgement of this node's directories (issue 358).
|
||||
@@ -145,6 +149,21 @@ func (l *natsLink) hearAsks() {
|
||||
}
|
||||
}
|
||||
|
||||
// FollowSigner has every signed ask heard on this link verified with the key the engine trusts at that ask, the
|
||||
// controller's while it holds its lease and none from its hand-over (novox/hq ADR 0563 §7, ADR 0550 §2).
|
||||
func (l *natsLink) FollowSigner(f func() ed25519.PublicKey) {
|
||||
l.mu.Lock()
|
||||
l.signerNow = f
|
||||
l.mu.Unlock()
|
||||
}
|
||||
|
||||
// check is how a signed ask is believed on this link: the static membership signer until FollowSigner names the trust.
|
||||
func (l *natsLink) check() HandOverCheck {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
return HandOverCheck{Signer: l.signer, SignerNow: l.signerNow, Node: l.node}
|
||||
}
|
||||
|
||||
// HearHandOvers listens for the controller asking this node to hand a directory used as found to the mesh
|
||||
// (novox/hq issue 356): a request on this machine's own subject, answered once on the reply it carried. The
|
||||
// ask is verified with the mesh's key before anything is read out of it (AnswerHandOver), because a responder
|
||||
@@ -155,7 +174,7 @@ func (l *natsLink) HearHandOvers(handle HandOverFunc) {
|
||||
if handle == nil {
|
||||
return
|
||||
}
|
||||
check := HandOverCheck{Signer: l.signer, Node: l.node}
|
||||
check := l.check()
|
||||
handing, err := l.conn.Subscribe(AskHandOverSubject(l.node), func(msg *nats.Msg) {
|
||||
if body, answer := HandOverReply(check, handle, msg.Reply, msg.Data); answer {
|
||||
_ = msg.Respond(body)
|
||||
@@ -174,7 +193,7 @@ func (l *natsLink) HearSetuidSearches(handle SetuidSearchFunc) {
|
||||
if handle == nil {
|
||||
return
|
||||
}
|
||||
check := HandOverCheck{Signer: l.signer, Node: l.node}
|
||||
check := l.check()
|
||||
searching, err := l.conn.Subscribe(AskSetuidSearchSubject(l.node), func(msg *nats.Msg) {
|
||||
if body, answer := SetuidSearchReply(check, handle, msg.Reply, msg.Data); answer {
|
||||
_ = msg.Respond(body)
|
||||
@@ -193,7 +212,7 @@ func (l *natsLink) HearDirectoryJudgements(handle JudgeDirectoriesFunc) {
|
||||
if handle == nil {
|
||||
return
|
||||
}
|
||||
check := HandOverCheck{Signer: l.signer, Node: l.node}
|
||||
check := l.check()
|
||||
judging, err := l.conn.Subscribe(AskJudgeDirectoriesSubject(l.node), func(msg *nats.Msg) {
|
||||
if body, answer := JudgeDirectoriesReply(check, handle, msg.Reply, msg.Data); answer {
|
||||
_ = msg.Respond(body)
|
||||
@@ -211,7 +230,7 @@ func (l *natsLink) HearAccessActs(handle AccessActFunc) {
|
||||
if handle == nil {
|
||||
return
|
||||
}
|
||||
check := HandOverCheck{Signer: l.signer, Node: l.node}
|
||||
check := l.check()
|
||||
acting, err := l.conn.Subscribe(AskAccessActSubject(l.node), func(msg *nats.Msg) {
|
||||
if body, answer := AccessActReply(check, handle, msg.Reply, msg.Data); answer {
|
||||
_ = msg.Respond(body)
|
||||
@@ -229,7 +248,7 @@ func (l *natsLink) HearMoveSecrets(handle MoveSecretFunc) {
|
||||
if handle == nil {
|
||||
return
|
||||
}
|
||||
check := HandOverCheck{Signer: l.signer, Node: l.node}
|
||||
check := l.check()
|
||||
moving, err := l.conn.Subscribe(AskMoveSecretSubject(l.node), func(msg *nats.Msg) {
|
||||
if body, answer := MoveSecretReply(check, handle, msg.Reply, msg.Data); answer {
|
||||
_ = msg.Respond(body)
|
||||
|
||||
@@ -187,6 +187,12 @@ func serve(ctx context.Context, link Link, m Membership, queue *Session, say Ann
|
||||
}
|
||||
// And the controller asking this node to hand a directory used as found to the mesh (novox/hq issue
|
||||
// 356), answered on the link the question came over.
|
||||
// Every signed ask is verified with the key trusted at that ask, none after the lease write (ADR 0563 §7).
|
||||
if f, ok := link.(interface {
|
||||
FollowSigner(func() ed25519.PublicKey)
|
||||
}); ok && queue.ControllerKey != nil {
|
||||
f.FollowSigner(queue.ControllerKey)
|
||||
}
|
||||
if h, ok := link.(HandOvers); ok {
|
||||
h.HearHandOvers(queue.HandOver)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package link
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -48,6 +49,9 @@ type Session struct {
|
||||
// MoveSecret moves one value sealed to this machine into the vault, asked by the controller during the move of
|
||||
// the secrets (design 57's V3, issue 676). Nil answers every such ask with a refusal.
|
||||
MoveSecret MoveSecretFunc
|
||||
// ControllerKey is the controller's key as the engine trusts it now: nil once its lease is handed away (novox/hq
|
||||
// ADR 0563 §7). Each link verifies every signed ask with it, read per ask; nil ControllerKey keeps the membership's.
|
||||
ControllerKey func() ed25519.PublicKey
|
||||
// Acts is the node seat node-engine this engine holds (novox/hq ADR 0563): served on every link, and its asks to the
|
||||
// operator asked and heard over it. Nil serves no seat.
|
||||
Acts *HandActs
|
||||
|
||||
Reference in New Issue
Block a user