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") } }