The engine dials as enrol.<node> on the pinned certificate, calls redeem with the public halves of its keys and a proof over them (the SDK's RedeemProof, which binds the node and the keys and never the token's secret), asks again while the holder says to, and verifies the vault's answer against the key its token carries before it keeps anything: an answer signed by another key, naming another signer or node, or altered is refused and nothing is kept. A signed one is kept (membership, login, keys), then the node dials as the login it was given, reads its key of node-logins, and calls confirm; nothing is applied until its placement names a module. When the bus does not serve the seat to the token (the controller still holds its lease) the controller's enrolment subject is used as before. The token's shape is mesh-sdk's now: identity keeps its names as aliases. Tests: one added, paid by merging three identity token tests into one (net two fewer). (issue 774, 774/S64) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
105 lines
4.0 KiB
Go
105 lines
4.0 KiB
Go
package link
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/ed25519"
|
|
"encoding/base64"
|
|
"errors"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// signedBy is an answer for ace, signed under the enrolment context by the key seeded, naming that key as its signer.
|
|
func signedBy(t *testing.T, seed byte) (delivery.RedeemAnswer, ed25519.PublicKey) {
|
|
t.Helper()
|
|
priv := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{seed}, ed25519.SeedSize))
|
|
pub := priv.Public().(ed25519.PublicKey)
|
|
a := delivery.RedeemAnswer{Node: "ace", Login: "node.ace", Account: `{"sealed":"box"}`, Certificate: "PEM", Signer: base64.StdEncoding.EncodeToString(pub)}
|
|
ctx, err := delivery.SignContext(delivery.KindEnrolment)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a.Signature = base64.StdEncoding.EncodeToString(delivery.SignMessage(priv, ctx, a.SignedBytes()))
|
|
return a, pub
|
|
}
|
|
|
|
type standInConfirming struct {
|
|
steps *[]string
|
|
nl delivery.NodeLogin
|
|
}
|
|
|
|
func (c standInConfirming) Login(context.Context) (delivery.NodeLogin, error) {
|
|
*c.steps = append(*c.steps, "read node-logins")
|
|
return c.nl, nil
|
|
}
|
|
func (c standInConfirming) Confirm(context.Context) error {
|
|
*c.steps = append(*c.steps, "confirm")
|
|
return nil
|
|
}
|
|
func (c standInConfirming) Close() {}
|
|
|
|
// 774/S64/AC1 and AC2 (ADR 0587 §6): an answer not signed by the key the token carries (or naming another) is refused and
|
|
// nothing is kept or dialled; a signed one is kept, then the engine dials as the login it names, reads its key of node-logins,
|
|
// and confirms; a key naming another login than the answer's is not confirmed.
|
|
func TestTheEngineKeepsOnlyAnAnswerSignedByTheKeyItsTokenCarries(t *testing.T) {
|
|
ctx := context.Background()
|
|
good, pub := signedBy(t, 1)
|
|
forged, _ := signedBy(t, 2)
|
|
badNode := good
|
|
badNode.Node = "g14"
|
|
for name, c := range map[string]struct {
|
|
answer delivery.RedeemAnswer
|
|
want string
|
|
}{
|
|
"signed by another key": {forged, "signing key other than the one the token carries"},
|
|
"another node": {badNode, `is for "g14"`},
|
|
"altered after signing": {func() delivery.RedeemAnswer { a := good; a.Account = "other"; return a }(), "not signed by the key the token carries"},
|
|
} {
|
|
var steps []string
|
|
j := Joining{Node: "ace", Signer: pub,
|
|
Redeem: func(context.Context) (delivery.RedeemAnswer, error) { return c.answer, nil },
|
|
Open: func(delivery.RedeemAnswer) (string, error) { steps = append(steps, "open"); return "pw", nil },
|
|
Keep: func(delivery.RedeemAnswer, string) error { steps = append(steps, "keep"); return nil },
|
|
Dial: func(context.Context, string, string) (Confirming, error) {
|
|
steps = append(steps, "dial")
|
|
return nil, errors.New("must not dial")
|
|
}}
|
|
if _, err := j.Run(ctx); err == nil || !strings.Contains(err.Error(), c.want) || len(steps) != 0 {
|
|
t.Errorf("%s: %v, steps %v", name, err, steps)
|
|
}
|
|
}
|
|
var steps []string
|
|
login := "node.ace"
|
|
j := Joining{Node: "ace", Signer: pub,
|
|
Redeem: func(context.Context) (delivery.RedeemAnswer, error) {
|
|
steps = append(steps, "redeem")
|
|
return good, nil
|
|
},
|
|
Open: func(delivery.RedeemAnswer) (string, error) { steps = append(steps, "open"); return "pw", nil },
|
|
Keep: func(a delivery.RedeemAnswer, pw string) error {
|
|
steps = append(steps, "keep "+a.Login+" "+pw)
|
|
return nil
|
|
},
|
|
Dial: func(_ context.Context, l, pw string) (Confirming, error) {
|
|
steps = append(steps, "dial "+l+" "+pw)
|
|
return standInConfirming{steps: &steps, nl: delivery.NodeLogin{Node: "ace", Login: login}}, nil
|
|
}}
|
|
if _, err := j.Run(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if want := []string{"redeem", "open", "keep node.ace pw", "dial node.ace pw", "read node-logins", "confirm"}; !slices.Equal(steps, want) {
|
|
t.Errorf("steps %v, want %v", steps, want)
|
|
}
|
|
steps, login = nil, "node.ace--b"
|
|
if _, err := j.Run(ctx); err == nil || slices.Contains(steps, "confirm") {
|
|
t.Errorf("a key naming another login was confirmed: %v, %v", err, steps)
|
|
}
|
|
if CalledSeatSubject("mesh-enrolment", "redeem", "enrol.ace") != "mesh.seat.mesh-enrolment.call.redeem.enrol~ace" {
|
|
t.Error("the call's subject does not name its caller as the composer grants it")
|
|
}
|
|
}
|