Ask a node's engine for a fresh setuid search at the terminal (hq issue 361)
After the operator removes by hand what the last search found, no apply says so and nothing searched again until the next day. node setuid-search <node> signs the ask as a hand-over is, under its own context, on a subject only the node's engine hears and only the controller may publish.
This commit is contained in:
+48
-11
@@ -74,9 +74,20 @@ func AskHandOver(ctx context.Context, conn *nats.Conn, signer Signer, node, path
|
||||
if err != nil {
|
||||
return HandOverAnswer{}, err
|
||||
}
|
||||
return askSigned(ctx, conn, broker.AskHandOverSubject(node), body, node, timeout, askWords{
|
||||
what: "a hand-over", nothing: "nothing was handed over", issue: "issue 356",
|
||||
unknown: "whether it was recorded is not known — the module's condition says whether the directory is " +
|
||||
"still used as found", record: "a record"})
|
||||
}
|
||||
|
||||
// askWords are what a signed ask's failures say of it.
|
||||
type askWords struct{ what, nothing, issue, unknown, record string }
|
||||
|
||||
// askSigned sends one signed ask on subject and reads the engine's answer.
|
||||
func askSigned(ctx context.Context, conn *nats.Conn, subject string, body []byte, node string, timeout time.Duration,
|
||||
w askWords) (HandOverAnswer, error) {
|
||||
asking, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
subject := broker.AskHandOverSubject(node)
|
||||
refused, stop := refusalsOf(conn, subject)
|
||||
defer stop()
|
||||
type replied struct {
|
||||
@@ -89,38 +100,64 @@ func AskHandOver(ctx context.Context, conn *nats.Conn, signer Signer, node, path
|
||||
done <- replied{msg, err}
|
||||
}()
|
||||
var reply *nats.Msg
|
||||
var err error
|
||||
select {
|
||||
case r := <-done:
|
||||
reply, err = r.msg, r.err
|
||||
case why := <-refused:
|
||||
cancel()
|
||||
return HandOverAnswer{}, fmt.Errorf("the bus refused the controller asking %s for a hand-over: %v", node, why)
|
||||
return HandOverAnswer{}, fmt.Errorf("the bus refused the controller asking %s for %s: %v", node, w.what, why)
|
||||
}
|
||||
switch {
|
||||
case errors.Is(err, nats.ErrNoResponders):
|
||||
return HandOverAnswer{}, fmt.Errorf("nothing on %s answers a hand-over: its node-engine is not running, is not "+
|
||||
"on the bus, or is older than this ask (novox/hq issue 356); nothing was handed over", node)
|
||||
return HandOverAnswer{}, fmt.Errorf("nothing on %s answers %s: its node-engine is not running, is not "+
|
||||
"on the bus, or is older than this ask (novox/hq %s); %s", node, w.what, w.issue, w.nothing)
|
||||
case errors.Is(err, context.DeadlineExceeded), errors.Is(err, nats.ErrTimeout):
|
||||
return HandOverAnswer{}, fmt.Errorf("%s did not answer the hand-over within %s; whether it was recorded is not "+
|
||||
"known — the module's condition says whether the directory is still used as found", node, timeout)
|
||||
return HandOverAnswer{}, fmt.Errorf("%s did not answer %s within %s; %s", node, w.what, timeout, w.unknown)
|
||||
case err != nil:
|
||||
return HandOverAnswer{}, err
|
||||
}
|
||||
var answer HandOverAnswer
|
||||
if err := json.Unmarshal(reply.Data, &answer); err != nil {
|
||||
return HandOverAnswer{}, fmt.Errorf("%s answered the hand-over with something unreadable: %w", node, err)
|
||||
return HandOverAnswer{}, fmt.Errorf("%s answered %s with something unreadable: %w", node, w.what, err)
|
||||
}
|
||||
if answer.Said == "" && answer.Refused == "" {
|
||||
return HandOverAnswer{}, fmt.Errorf("%s answered the hand-over with neither a record nor a refusal", node)
|
||||
return HandOverAnswer{}, fmt.Errorf("%s answered %s with neither %s nor a refusal", node, w.what, w.record)
|
||||
}
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
// SetuidSearchContext is prefixed to a setuid search ask's bytes before signing (novox/hq issue 361): never a
|
||||
// hand-over's signature, nor a declaration's. The engine holds the same words.
|
||||
const SetuidSearchContext = "novox-mesh setuid-search v1\n"
|
||||
|
||||
// AskSetuidSearch asks one machine's node-engine to throw its last search for setuid programs away and start a
|
||||
// full one (novox/hq issue 361): the ask a hand-over is, naming no path, signed under SetuidSearchContext.
|
||||
func AskSetuidSearch(ctx context.Context, conn *nats.Conn, signer Signer, node, by string,
|
||||
timeout time.Duration) (HandOverAnswer, error) {
|
||||
if conn == nil {
|
||||
return HandOverAnswer{}, errors.New("this controller is not on the bus")
|
||||
}
|
||||
body, err := signAsk(ctx, signer, SetuidSearchContext, HandOverAsk{Node: node, By: by})
|
||||
if err != nil {
|
||||
return HandOverAnswer{}, err
|
||||
}
|
||||
return askSigned(ctx, conn, broker.AskSetuidSearchSubject(node), body, node, timeout, askWords{
|
||||
what: "a setuid search", nothing: "no search was started", issue: "issue 361",
|
||||
unknown: "whether it started is not known — the controller's root-free verb says whether a search runs " +
|
||||
"there", record: "a start"})
|
||||
}
|
||||
|
||||
// SignHandOver fills the ask's expiry and nonce and signs it with the mesh's key, over HandOverContext and the
|
||||
// ask's bytes exactly as they travel.
|
||||
func SignHandOver(ctx context.Context, signer Signer, ask HandOverAsk) ([]byte, error) {
|
||||
return signAsk(ctx, signer, HandOverContext, ask)
|
||||
}
|
||||
|
||||
// signAsk fills an ask's expiry and nonce and signs it over prefix (its signing context) and its bytes.
|
||||
func signAsk(ctx context.Context, signer Signer, prefix string, ask HandOverAsk) ([]byte, error) {
|
||||
if signer == nil {
|
||||
return nil, errors.New("no signing key, so no hand-over can be asked")
|
||||
return nil, errors.New("no signing key, so nothing can be asked of an engine")
|
||||
}
|
||||
nonce := make([]byte, 16)
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
@@ -132,9 +169,9 @@ func SignHandOver(ctx context.Context, signer Signer, ask HandOverAsk) ([]byte,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signature, err := signer.Sign(ctx, append([]byte(HandOverContext), raw...))
|
||||
signature, err := signer.Sign(ctx, append([]byte(prefix), raw...))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot sign the hand-over: %w", err)
|
||||
return nil, fmt.Errorf("cannot sign the ask: %w", err)
|
||||
}
|
||||
return json.Marshal(SignedHandOver{Ask: raw, Signature: signature})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package link
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
"github.com/novox/mesh-controller/internal/testbus"
|
||||
)
|
||||
|
||||
// A fresh setuid search is asked of the machine on its own subject, signed under its own context — never a
|
||||
// hand-over's — naming no path, and the engine's answer comes back (novox/hq issue 361). The engine holds the
|
||||
// same subject and context (mesh-host internal/link, TestTheSetuidSearchAskKeepsItsSubjectAndContext).
|
||||
func TestASetuidSearchIsAskedOfTheMachineSignedUnderItsOwnContext(t *testing.T) {
|
||||
if broker.AskSetuidSearchSubject("laptop") != "mesh.node.laptop.ask.setuid-search" ||
|
||||
SetuidSearchContext != "novox-mesh setuid-search v1\n" {
|
||||
t.Fatalf("the subject %q, the context %q", broker.AskSetuidSearchSubject("laptop"), SetuidSearchContext)
|
||||
}
|
||||
conn, err := nats.Connect(testbus.URL(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(conn.Close)
|
||||
signer, public := testSigner(t)
|
||||
var heard HandOverAsk
|
||||
engine, err := conn.Subscribe(broker.AskSetuidSearchSubject("laptop"), func(msg *nats.Msg) {
|
||||
var signed SignedHandOver
|
||||
_ = json.Unmarshal(msg.Data, &signed)
|
||||
if ed25519.Verify(public, append([]byte(HandOverContext), signed.Ask...), signed.Signature) ||
|
||||
!ed25519.Verify(public, append([]byte(SetuidSearchContext), signed.Ask...), signed.Signature) {
|
||||
_ = msg.Respond([]byte(`{"refused":"not signed as a setuid search"}`))
|
||||
return
|
||||
}
|
||||
_ = json.Unmarshal(signed.Ask, &heard)
|
||||
body, _ := json.Marshal(HandOverAnswer{Said: "a new search starts, asked by " + heard.By})
|
||||
_ = msg.Respond(body)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = engine.Unsubscribe() })
|
||||
a, err := AskSetuidSearch(context.Background(), conn, signer, "laptop", "jo", 5*time.Second)
|
||||
if err != nil || a.Said != "a new search starts, asked by jo" {
|
||||
t.Fatalf("answered %+v, %v", a, err)
|
||||
}
|
||||
if heard.Node != "laptop" || heard.Path != "" || heard.Nonce == "" || heard.Expires.IsZero() {
|
||||
t.Fatalf("the engine heard %+v", heard)
|
||||
}
|
||||
if _, err := AskSetuidSearch(context.Background(), conn, signer, "anchor", "jo", 5*time.Second); err == nil ||
|
||||
!strings.Contains(err.Error(), "no search was started") {
|
||||
t.Fatalf("a machine with no engine listening: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user