116 lines
4.4 KiB
Go
116 lines
4.4 KiB
Go
package link
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
)
|
|
|
|
// The hand-over's ask and answer hold these field names; the engine's side holds the same list (mesh-host
|
|
// internal/link, TestTheHandOverAskAndAnswerKeepTheirFieldNames).
|
|
func TestTheHandOverAskAndAnswerKeepTheirFieldNames(t *testing.T) {
|
|
body, _ := json.Marshal(HandOverAsk{Node: "laptop", Path: "/srv/notes", By: "jo through mesh-cli on anchor",
|
|
Expires: time.Now(), Nonce: "n"})
|
|
if got := keysIn(t, body); got != "by expires node nonce path" {
|
|
t.Fatalf("the ask's fields are %q", got)
|
|
}
|
|
body, _ = json.Marshal(SignedHandOver{Ask: []byte("{}"), Signature: []byte("s")})
|
|
if got := keysIn(t, body); got != "ask signature" {
|
|
t.Fatalf("the signed ask's fields are %q", got)
|
|
}
|
|
body, _ = json.Marshal(HandOverAnswer{Said: "s", Refused: "r"})
|
|
if got := keysIn(t, body); got != "refused said" {
|
|
t.Fatalf("the answer's fields are %q", got)
|
|
}
|
|
if broker.AskHandOverSubject("laptop") != "mesh.node.laptop.ask.hand-over" {
|
|
t.Fatalf("the subject is %q", broker.AskHandOverSubject("laptop"))
|
|
}
|
|
if HandOverContext != "novox-mesh hand-over v1\n" {
|
|
t.Fatalf("the signing context is %q; the engine holds the same words", HandOverContext)
|
|
}
|
|
}
|
|
|
|
// keySigner signs with one key, as the controller's identity does.
|
|
type keySigner struct{ key ed25519.PrivateKey }
|
|
|
|
func (k keySigner) Sign(_ context.Context, message []byte) ([]byte, error) {
|
|
return ed25519.Sign(k.key, message), nil
|
|
}
|
|
|
|
func testSigner(t *testing.T) (keySigner, ed25519.PublicKey) {
|
|
t.Helper()
|
|
public, private, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return keySigner{private}, public
|
|
}
|
|
|
|
// **The ask is signed over the context and its bytes, with a fresh nonce and a short expiry** (review of issue
|
|
// 356): the signature verifies over HandOverContext and the ask's bytes, and not over the bytes alone — so it is
|
|
// never a declaration's — and two asks never share a nonce.
|
|
func TestAHandOverIsSignedWithAContextANonceAndAnExpiry(t *testing.T) {
|
|
signer, public := testSigner(t)
|
|
body, err := SignHandOver(context.Background(), signer, HandOverAsk{Node: "laptop", Path: "/srv/notes", By: "jo"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var signed SignedHandOver
|
|
if err := json.Unmarshal(body, &signed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !ed25519.Verify(public, append([]byte(HandOverContext), signed.Ask...), signed.Signature) {
|
|
t.Fatal("the signature does not verify over the context and the ask")
|
|
}
|
|
if ed25519.Verify(public, signed.Ask, signed.Signature) {
|
|
t.Fatal("the signature verifies over the ask's bytes alone, as a declaration's would")
|
|
}
|
|
var ask HandOverAsk
|
|
if err := json.Unmarshal(signed.Ask, &ask); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ask.Node != "laptop" || ask.Path != "/srv/notes" || ask.By != "jo" || len(ask.Nonce) != 32 {
|
|
t.Fatalf("signed %+v", ask)
|
|
}
|
|
if left := time.Until(ask.Expires); left <= 0 || left > HandOverGood {
|
|
t.Fatalf("the ask is good for %s", left)
|
|
}
|
|
again, _ := SignHandOver(context.Background(), signer, HandOverAsk{Node: "laptop", Path: "/srv/notes", By: "jo"})
|
|
var other SignedHandOver
|
|
_ = json.Unmarshal(again, &other)
|
|
var second HandOverAsk
|
|
_ = json.Unmarshal(other.Ask, &second)
|
|
if second.Nonce == ask.Nonce {
|
|
t.Fatal("two asks share a nonce")
|
|
}
|
|
if _, err := SignHandOver(context.Background(), nil, HandOverAsk{}); err == nil {
|
|
t.Fatal("an ask was made with no key")
|
|
}
|
|
}
|
|
|
|
// The ask reaches the machine's engine on its own subject and its answer comes back whole: what it recorded, or
|
|
// its refusal as the engine worded it. A machine with no engine listening is said as not answering, and an
|
|
// answer that is neither is refused rather than read as a record.
|
|
|
|
// A machine's grant hears its own hand-over ask and nobody else's, and may answer it: the one request a node is
|
|
// asked (novox/hq issue 356).
|
|
func TestAMachineHearsItsOwnHandOverAskAndMayAnswerIt(t *testing.T) {
|
|
perms, err := broker.PermissionsFor(broker.Principal{Kind: broker.KindNode, Node: "laptop", PasswordHash: "x"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var hears, other bool
|
|
for _, s := range perms.Subscribe {
|
|
hears = hears || s == "mesh.node.laptop.ask.hand-over"
|
|
other = other || s == "mesh.node.anchor.ask.hand-over"
|
|
}
|
|
if !hears || other || !perms.AllowResponses {
|
|
t.Fatalf("a machine's grant: hears its own %v, another's %v, answers %v (%v)", hears, other, perms.AllowResponses,
|
|
perms.Subscribe)
|
|
}
|
|
}
|