Enrolment: the enrolment's names come from the node-enroller's and the vault's clients and seats, the assigner's seat and client, and the node-engine's client; the state's rules and the expired event (the seat's enrolment) are the holder's own; it invokes the seat mesh-module-assigner; the names it holds beside its own are checked against its manifest (D-11; ADR 0634 §1, §2, §9; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
@@ -7,7 +7,6 @@ import (
|
||||
"strings"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// Revoke ends an enrolment at any stage (ADR 0587 §5), by a person's call: at asked it withdraws the ask; at redeeming it also
|
||||
@@ -126,7 +125,7 @@ func (h *Holder) reconcileLocked() error {
|
||||
if err != nil || !found {
|
||||
continue
|
||||
}
|
||||
r, err := delivery.DecodeEnrolmentAddress(row.Value)
|
||||
r, err := decodeReservation(row.Value)
|
||||
// An address with no ask is reserved by an issue under way: its enrolment key is written next, then the ask.
|
||||
if err != nil || r.Ask == "" || open[r.Node] {
|
||||
continue
|
||||
@@ -164,11 +163,7 @@ func (h *Holder) endExpired(e Enrolment) error {
|
||||
if err := h.unplace(e, "expired"); err != nil {
|
||||
return fmt.Errorf("%s, whose enrolment expired, is not taken out of placement (tried again on the next pass): %w", e.Node, err)
|
||||
}
|
||||
body, err := delivery.EncodeEnrolmentExpiry(delivery.EnrolmentExpiry{Node: e.Node, IssuedID: delivery.EnrolmentIssuedID(e.Node, e.Issued)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := h.say(EventExpired, json.RawMessage(body), eventID(EventExpired, e)); err != nil {
|
||||
if err := h.say(EventExpired, expiredEvent(e), eventID(EventExpired, e)); err != nil {
|
||||
return fmt.Errorf("enrolment-expired of %s cannot be said (tried again on the next pass): %w", e.Node, err)
|
||||
}
|
||||
if err := h.Vault.Drop(e.Node); err != nil {
|
||||
@@ -177,7 +172,7 @@ func (h *Holder) endExpired(e Enrolment) error {
|
||||
if row, found, err := h.Store.Get(AddressKey(e.Address)); err != nil {
|
||||
return fmt.Errorf("the address key %s cannot be read (tried again on the next pass): %w", e.Address, err)
|
||||
} else if found {
|
||||
if r, err := delivery.DecodeEnrolmentAddress(row.Value); err == nil && r.Node == e.Node && r.Ask == e.Ask {
|
||||
if r, err := decodeReservation(row.Value); err == nil && r.Node == e.Node && r.Ask == e.Ask {
|
||||
if err := h.Store.Delete(AddressKey(e.Address)); err != nil {
|
||||
return fmt.Errorf("the address key %s is not deleted (tried again on the next pass): %w", e.Address, err)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,12 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -29,7 +33,7 @@ func revokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) {
|
||||
}
|
||||
// The node the yes placed, never confirmed, leaves placement on the enrolment's warrant before the end is said.
|
||||
if _, placed := r.rd["mesh-delivery-assigner.placement"]["ace.node"]; placed || len(r.added) == 0 ||
|
||||
!reflect.DeepEqual(r.added[len(r.added)-1].Change, delivery.PlacementProposalChange{Op: delivery.OpRemoveNode, Node: "ace"}) || r.added[len(r.added)-1].Warrant.Ask != "ask-ace" {
|
||||
!reflect.DeepEqual(r.added[len(r.added)-1].Change, meshmoduleassigner.ProposalChange{Op: meshmoduleassignerclient.OpRemoveNode, Node: "ace"}) || r.added[len(r.added)-1].Warrant.Ask != "ask-ace" {
|
||||
t.Errorf("revoked and still placed: %+v", r.added)
|
||||
}
|
||||
if len(r.keys()) != 0 || !slices.Equal(r.events(), []string{EventRevoked}) || len(r.acts) != 1 || !slices.Contains(r.vault.calls, "retire ace") {
|
||||
@@ -55,7 +59,7 @@ func revokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) {
|
||||
t.Error("expired and still placed")
|
||||
}
|
||||
last := r.said[len(r.said)-1]
|
||||
x, err := delivery.DecodeEnrolmentExpiry(last.Body)
|
||||
x, err := decodeExpired(last.Body)
|
||||
if err != nil || last.Event != EventExpired || x.Node != "novox" || !strings.HasPrefix(x.IssuedID, "enrolment-issued.novox.") ||
|
||||
!slices.Contains(r.vault.calls, "drop novox") || r.has(AddressKey("10.9.0.4")) {
|
||||
t.Errorf("said %s %s: %+v, %v; vault %v, keys %v", last.Event, last.Body, x, err, r.vault.calls, r.keys())
|
||||
@@ -87,10 +91,10 @@ func revokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) {
|
||||
t.Fatalf("%s: %v", c.name, err)
|
||||
}
|
||||
last := r.said[len(r.said)-1]
|
||||
x, err := delivery.DecodeEnrolmentExpiry(last.Body)
|
||||
x, err := decodeExpired(last.Body)
|
||||
_, placed := r.rd["mesh-delivery-assigner.placement"][c.node+".node"]
|
||||
removed := r.added[len(r.added)-1]
|
||||
if err != nil || x.IssuedID != delivery.EnrolmentIssuedID(c.node, issued) || placed || removed.Change.Op != delivery.OpRemoveNode ||
|
||||
if err != nil || x.IssuedID != meshnodeenrollerclient.IssuedID(c.node, issued) || placed || removed.Change.Op != meshmoduleassignerclient.OpRemoveNode ||
|
||||
removed.Change.Node != c.node || removed.Warrant.Ask != "ask-"+c.node || !slices.Contains(r.vault.calls, "drop "+c.node) || r.has(AddressKey("10.9.0.9")) {
|
||||
t.Errorf("%s: said %s (%v), placed %v, last placement call %+v, vault %v, keys %v", c.name, last.Body, err, placed, removed, r.vault.calls, r.keys())
|
||||
}
|
||||
@@ -105,7 +109,7 @@ func revokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) {
|
||||
}{
|
||||
{"after a confirm stopped before the enrolment key went", func(r *rig, node string) {
|
||||
e, _ := r.enrolment(node)
|
||||
e.Stage, e.KeysDigest = StageRedeeming, delivery.KeysDigest("pk", "ov", "se", "sv")
|
||||
e.Stage, e.KeysDigest = StageRedeeming, meshnodeenrollerclient.KeysDigest("pk", "ov", "se", "sv")
|
||||
_, _ = r.st.Put(NodeKey(node), e, 0)
|
||||
r.asker.answer("ask-"+node, "yes")
|
||||
r.st.failDelete[NodeKey(node)] = true // the confirm stops after the address key, before the enrolment key
|
||||
@@ -142,7 +146,7 @@ func revokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) {
|
||||
c.end(r, "joiner")
|
||||
removes := 0
|
||||
for _, a := range r.added {
|
||||
if a.Change.Op == delivery.OpRemoveNode {
|
||||
if a.Change.Op == meshmoduleassignerclient.OpRemoveNode {
|
||||
removes++
|
||||
}
|
||||
}
|
||||
@@ -151,7 +155,7 @@ func revokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) {
|
||||
}
|
||||
after := 0
|
||||
for _, a := range r.added {
|
||||
if a.Change.Op == delivery.OpRemoveNode {
|
||||
if a.Change.Op == meshmoduleassignerclient.OpRemoveNode {
|
||||
after++
|
||||
}
|
||||
}
|
||||
@@ -207,3 +211,15 @@ func theDropFollowsOnlyAnEndAndNothingHoldsTheToken(t *testing.T) {
|
||||
t.Errorf("history %s", raw)
|
||||
}
|
||||
}
|
||||
|
||||
// expired is what enrolment-expired says, read as the seat's contract has it: the enrolment, and the enrolment-issued event
|
||||
// its issue time names.
|
||||
type expired struct{ Node, IssuedID string }
|
||||
|
||||
func decodeExpired(raw []byte) (expired, error) {
|
||||
c, err := meshnodeenroller.DecodeEnrolmentsByNodeNodeValue(raw)
|
||||
if err != nil || c.Issued == nil {
|
||||
return expired{}, fmt.Errorf("enrolment-expired is not the seat's enrolment with its issue time: %v", err)
|
||||
}
|
||||
return expired{Node: c.Node, IssuedID: meshnodeenrollerclient.IssuedID(c.Node, *c.Issued)}, nil
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// mesh-enrolment: the holder of the seat mesh-enrolment (novox/hq ADR 0587 §1), held once for the mesh. A Go bundle served
|
||||
@@ -22,7 +22,7 @@ const (
|
||||
Module = "mesh-enrolment"
|
||||
Seat = meshnodeenroller.Seat
|
||||
// State is the holder's bucket of open enrolments; its keys are node.<node> and address.<address> (ADR 0587 §2).
|
||||
State = delivery.EnrolmentsState
|
||||
State = enrolmentsState
|
||||
)
|
||||
|
||||
// The seat's six verbs and five events (ADR 0587 §1).
|
||||
@@ -41,20 +41,14 @@ const (
|
||||
// to drop the token. A seat's event is heard as <seat>.<event>.
|
||||
var EndEvents = []string{EventEnrolled, EventRefused, EventRevoked, EventExpired}
|
||||
|
||||
// The stages of an open enrolment (ADR 0587 §2), as the seat's contract names them (delivery.Enrolment).
|
||||
// The stages of an open enrolment (ADR 0587 §2), as the seat's contract names them (Enrolment).
|
||||
const (
|
||||
// StageAsked: the operator's Approve is asked, no token exists.
|
||||
StageAsked = delivery.EnrolmentAsked
|
||||
StageAsked = stageAsked
|
||||
// StageLive: the Approve is given, the token exists and expires on the bus with the key.
|
||||
StageLive = delivery.EnrolmentLive
|
||||
StageLive = stageLive
|
||||
// StageRedeeming: the token is spent; the key has no expiry and ends only by confirm or revoke.
|
||||
StageRedeeming = delivery.EnrolmentRedeeming
|
||||
)
|
||||
|
||||
// Enrolment and Reservation are the seat's state contract (delivery.Enrolment, delivery.EnrolmentAddress).
|
||||
type (
|
||||
Enrolment = delivery.Enrolment
|
||||
Reservation = delivery.EnrolmentAddress
|
||||
StageRedeeming = stageRedeeming
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -62,11 +56,11 @@ const (
|
||||
)
|
||||
|
||||
// NodeKey and AddressKey are the two keys of an enrolment.
|
||||
func NodeKey(node string) string { return delivery.EnrolmentNodeKey(node) }
|
||||
func AddressKey(address string) string { return delivery.EnrolmentAddressKey(address) }
|
||||
func NodeKey(node string) string { return nodeKey(node) }
|
||||
func AddressKey(address string) string { return addressKey(address) }
|
||||
|
||||
// EnrolUser and NodeUser are the two bus users of a node: the one it joins with, and its own.
|
||||
func EnrolUser(node string) string { return delivery.EnrolmentUser(node) }
|
||||
func EnrolUser(node string) string { return meshnodeenrollerclient.User(node) }
|
||||
func NodeUser(node string) string { return nodePrefix + node }
|
||||
|
||||
var nodeName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}$`)
|
||||
@@ -79,15 +73,13 @@ func CheckNode(node string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeEnrolment(raw []byte) (Enrolment, error) { return delivery.DecodeEnrolment(raw) }
|
||||
|
||||
// Encode is a value of the bucket as the seat's contract writes it: an Enrolment or a Reservation checked and versioned.
|
||||
func Encode(v any) (json.RawMessage, error) {
|
||||
switch x := v.(type) {
|
||||
case Enrolment:
|
||||
return delivery.EncodeEnrolment(x)
|
||||
return encodeEnrolment(x)
|
||||
case Reservation:
|
||||
return delivery.EncodeEnrolmentAddress(x.Node, x.Ask, x.Issued)
|
||||
return encodeReservation(x.Node, x.Ask, x.Issued)
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
@@ -11,9 +11,12 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
mv "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
)
|
||||
|
||||
// Vault is the vault's side of enrolment (ADR 0587 §3, §5): the token and the node's account, and the signatures. The
|
||||
@@ -22,7 +25,7 @@ type Vault interface {
|
||||
// Token asks the vault to make the token and its hash for node, expiring at expires, and answers what the joining
|
||||
// machine must trust (the bus's address and fingerprint, the vault's signing key). No token is in the answer.
|
||||
// The token carries tunnel, the node's first tunnel built at issue (ADR 0587 §3, insight of 2026-11-02).
|
||||
Token(node string, expires time.Time, tunnel delivery.TokenTunnel) (mv.EnrolmentTokenAnswer, error)
|
||||
Token(node string, expires time.Time, tunnel meshnodeenrollerclient.TokenTunnel) (mv.EnrolmentTokenAnswer, error)
|
||||
// Sealed asks the vault for node's token sealed to the operator key, as the vault sealed it.
|
||||
Sealed(node string) (string, error)
|
||||
// Drop asks the vault to drop node's token and its hash; it answers only once both are gone.
|
||||
@@ -33,7 +36,7 @@ type Vault interface {
|
||||
Retire(node string) error
|
||||
// SignCertificate and SignReply sign through the vault's `sign`; the certificate's call carries every public key the node
|
||||
// presented, which the vault checks against the digest node-account was minted for.
|
||||
SignCertificate(node string, keys delivery.JoiningKeys) (mv.SignAnswer, error)
|
||||
SignCertificate(node string, keys meshmoduleassigner.ProposalChangeKeys) (mv.SignAnswer, error)
|
||||
SignReply(node string, reply []byte) (mv.SignAnswer, error)
|
||||
// Signer is the vault's public signing key in base64, which the redeem answer names for the node to check it against its token.
|
||||
Signer() (string, error)
|
||||
@@ -98,16 +101,16 @@ func (h *Holder) say(event string, body any, id string) error {
|
||||
|
||||
// AddNode is what the holder asks mesh-placement to add (ADR 0587 §7): the proposal's one change, a joining node with the
|
||||
// address the operator approved and the public halves of its keys, and the issue's warrant, which names this node and this
|
||||
// address (delivery.AddNodeWarrant).
|
||||
// address (meshmoduleassigner.Warrant).
|
||||
type AddNode struct {
|
||||
Why string
|
||||
Change delivery.PlacementProposalChange
|
||||
Warrant delivery.AddNodeWarrant
|
||||
Change meshmoduleassigner.ProposalChange
|
||||
Warrant meshmoduleassigner.Warrant
|
||||
}
|
||||
|
||||
// place adds the node into placement, with the address the operator approved and its tunnel key, on the issue's warrant
|
||||
// (ADR 0587 §7; the operator's decision of 2026-11-02 01:05), unless placement holds it at that address already.
|
||||
func (h *Holder) place(e Enrolment, placements map[string]delivery.Placement) error {
|
||||
func (h *Holder) place(e Enrolment, placements map[string]meshmoduleassigner.Placement) error {
|
||||
if p, ok := placements[e.Node]; ok && p.Decided.Address == e.Address {
|
||||
return nil
|
||||
}
|
||||
@@ -116,8 +119,8 @@ func (h *Holder) place(e Enrolment, placements map[string]delivery.Placement) er
|
||||
return err
|
||||
}
|
||||
return h.AddNode(AddNode{Why: "enrolment of " + e.Node + ": " + e.Why, Warrant: w,
|
||||
Change: delivery.PlacementProposalChange{Op: delivery.OpAddNode, Node: e.Node, Decided: &delivery.NodeDecided{Address: e.Address},
|
||||
Keys: &delivery.JoiningKeys{Overlay: e.OverlayKey}}})
|
||||
Change: meshmoduleassigner.ProposalChange{Op: meshmoduleassignerclient.OpAddNode, Node: e.Node, Decided: &meshmoduleassigner.NodeDecided{Address: e.Address},
|
||||
Keys: &meshmoduleassigner.ProposalChangeKeys{Overlay: e.OverlayKey}}})
|
||||
}
|
||||
|
||||
// unplace takes a node that never confirmed off placement again, on the same warrant, when its enrolment ends revoked, refused
|
||||
@@ -135,22 +138,22 @@ func (h *Holder) unplace(e Enrolment, why string) error {
|
||||
return err
|
||||
}
|
||||
return h.AddNode(AddNode{Why: "enrolment of " + e.Node + " ended unconfirmed: " + why, Warrant: w,
|
||||
Change: delivery.PlacementProposalChange{Op: delivery.OpRemoveNode, Node: e.Node}})
|
||||
Change: meshmoduleassigner.ProposalChange{Op: meshmoduleassignerclient.OpRemoveNode, Node: e.Node}})
|
||||
}
|
||||
|
||||
func (h *Holder) warrantOf(e Enrolment) (delivery.AddNodeWarrant, error) {
|
||||
func (h *Holder) warrantOf(e Enrolment) (meshmoduleassigner.Warrant, error) {
|
||||
if h.AddNode == nil || h.WarrantOf == nil {
|
||||
return delivery.AddNodeWarrant{}, errors.New("no route to mesh-placement")
|
||||
return meshmoduleassigner.Warrant{}, errors.New("no route to mesh-placement")
|
||||
}
|
||||
warrant, err := h.WarrantOf(e.Ask)
|
||||
if err != nil {
|
||||
return delivery.AddNodeWarrant{}, fmt.Errorf("the operator's warrant for %s cannot be read from the router: %w", e.Ask, err)
|
||||
return meshmoduleassigner.Warrant{}, fmt.Errorf("the operator's warrant for %s cannot be read from the router: %w", e.Ask, err)
|
||||
}
|
||||
return delivery.AddNodeWarrant{Node: e.Node, Address: e.Address, Ask: e.Ask, Warrant: warrant}, nil
|
||||
return meshmoduleassigner.Warrant{Node: e.Node, Address: e.Address, Ask: e.Ask, Warrant: warrant}, nil
|
||||
}
|
||||
|
||||
// checkTunnelKey says a tunnel key is a WireGuard public key, as `nox-mesh-host key` prints it: 32 bytes in standard base64
|
||||
// (as delivery.Enrolment requires it too); said here so issue names it among its findings.
|
||||
// (as Enrolment requires it too); said here so issue names it among its findings.
|
||||
func checkTunnelKey(k string) error {
|
||||
raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(k))
|
||||
if err != nil || len(raw) != 32 {
|
||||
@@ -189,7 +192,7 @@ func (h *Holder) openEnrolments() (map[string]Row, map[string]Enrolment, error)
|
||||
}
|
||||
|
||||
// placements reads every node's placement, assembled from its keys.
|
||||
func (h *Holder) placements() (map[string]delivery.Placement, error) {
|
||||
func (h *Holder) placements() (map[string]meshmoduleassigner.Placement, error) {
|
||||
keys, err := h.Read.Keys(PlacementRead)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("placement cannot be listed: %w", err)
|
||||
@@ -202,7 +205,7 @@ func (h *Holder) placements() (map[string]delivery.Placement, error) {
|
||||
held[k] = raw
|
||||
}
|
||||
}
|
||||
p, err := delivery.PlacementsOf(held)
|
||||
p, err := meshmoduleassignerclient.PlacementsOf(held)
|
||||
if len(p) == 0 && err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -215,8 +218,8 @@ func (h *Holder) placements() (map[string]delivery.Placement, error) {
|
||||
// that has not written one, the found tunnel's key kept in its placement), the hub's endpoint from its placement, and the
|
||||
// node's address on the private network. A mesh with no hub, or a hub with no key or no endpoint, has no tunnel to join
|
||||
// through, and that is said.
|
||||
func (h *Holder) firstTunnel(placements map[string]delivery.Placement, address, overlayKey string) (delivery.TokenTunnel, error) {
|
||||
var hub *delivery.Placement
|
||||
func (h *Holder) firstTunnel(placements map[string]meshmoduleassigner.Placement, address, overlayKey string) (meshnodeenrollerclient.TokenTunnel, error) {
|
||||
var hub *meshmoduleassigner.Placement
|
||||
for _, p := range placements {
|
||||
if p.Decided.Hub {
|
||||
p := p
|
||||
@@ -224,12 +227,12 @@ func (h *Holder) firstTunnel(placements map[string]delivery.Placement, address,
|
||||
}
|
||||
}
|
||||
if hub == nil {
|
||||
return delivery.TokenTunnel{}, errors.New("placement names no hub, so there is no tunnel to join through")
|
||||
return meshnodeenrollerclient.TokenTunnel{}, errors.New("placement names no hub, so there is no tunnel to join through")
|
||||
}
|
||||
key := ""
|
||||
if k, err := delivery.HaveNodeKey(hub.Node); err == nil {
|
||||
if k, err := nodeengineclient.HaveNodeKey(hub.Node); err == nil {
|
||||
if raw, found, err := h.Read.Get("node-engine.have", k); err == nil && found {
|
||||
if hn, err := delivery.DecodeHaveNode(raw); err == nil && hn.Record != nil {
|
||||
if hn, err := nodeengineclient.DecodeHaveNode(raw); err == nil && hn.Record != nil {
|
||||
key = hn.Record.TunnelKey
|
||||
}
|
||||
}
|
||||
@@ -246,12 +249,12 @@ func (h *Holder) firstTunnel(placements map[string]delivery.Placement, address,
|
||||
}
|
||||
prefix, rerr := privateRange(placements)
|
||||
if len(missing) > 0 {
|
||||
return delivery.TokenTunnel{}, fmt.Errorf("the hub %s has %s, so there is no tunnel to join through", hub.Node, strings.Join(missing, " and "))
|
||||
return meshnodeenrollerclient.TokenTunnel{}, fmt.Errorf("the hub %s has %s, so there is no tunnel to join through", hub.Node, strings.Join(missing, " and "))
|
||||
}
|
||||
if rerr != nil {
|
||||
return delivery.TokenTunnel{}, rerr
|
||||
return meshnodeenrollerclient.TokenTunnel{}, rerr
|
||||
}
|
||||
return delivery.TokenTunnel{Key: overlayKey, Address: address + "/32", Range: prefix.String(), HubKey: key, HubEndpoint: hub.Decided.Endpoint}, nil
|
||||
return meshnodeenrollerclient.TokenTunnel{Key: overlayKey, Address: address + "/32", Range: prefix.String(), HubKey: key, HubEndpoint: hub.Decided.Endpoint}, nil
|
||||
}
|
||||
|
||||
// hasRecord says the node already writes a record (a node of the mesh, ADR 0587 §4: "a node record ... holds it").
|
||||
@@ -261,7 +264,7 @@ func (h *Holder) hasRecord(node string) bool {
|
||||
return false
|
||||
}
|
||||
for _, k := range keys {
|
||||
if n, _, isNode, err := delivery.ParseHaveKey(k); err == nil && isNode && n == node {
|
||||
if n, _, isNode, err := nodeengineclient.ParseHaveKey(k); err == nil && isNode && n == node {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -271,7 +274,7 @@ func (h *Holder) hasRecord(node string) bool {
|
||||
// privateRange is the private network's range: the hub's tunnel range in its placement, as the controller's `token issue`
|
||||
// writes it into the token today (the adopted tunnel's range). A mesh whose hub's placement names none has no range a token
|
||||
// can carry, and that is said rather than guessed.
|
||||
func privateRange(placements map[string]delivery.Placement) (netip.Prefix, error) {
|
||||
func privateRange(placements map[string]meshmoduleassigner.Placement) (netip.Prefix, error) {
|
||||
for _, p := range placements {
|
||||
if t := p.Decided.Tunnel; p.Decided.Hub && t != nil && t.Range != "" {
|
||||
r, err := netip.ParsePrefix(t.Range)
|
||||
@@ -286,7 +289,7 @@ func privateRange(placements map[string]delivery.Placement) (netip.Prefix, error
|
||||
|
||||
// nextAddress is the lowest address of the private network that no placement key and no reservation holds (ADR 0587 §4),
|
||||
// after skip. The network is the hub's range; the hub's own address and the tunnel's are never given.
|
||||
func nextAddress(placements map[string]delivery.Placement, reserved map[string]bool, skip map[string]bool) (string, error) {
|
||||
func nextAddress(placements map[string]meshmoduleassigner.Placement, reserved map[string]bool, skip map[string]bool) (string, error) {
|
||||
taken := map[string]bool{}
|
||||
for _, p := range placements {
|
||||
if a := p.Decided.Address; a != "" {
|
||||
|
||||
@@ -23,10 +23,13 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
|
||||
mv "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/testbus"
|
||||
)
|
||||
|
||||
@@ -145,7 +148,7 @@ type standInVault struct {
|
||||
ctx context.Context
|
||||
logins jetstream.KeyValue
|
||||
mu sync.Mutex
|
||||
tokens map[string]delivery.Token
|
||||
tokens map[string]meshnodeenrollerclient.Token
|
||||
calls []string
|
||||
}
|
||||
|
||||
@@ -156,9 +159,9 @@ func (v *standInVault) note(c string) {
|
||||
}
|
||||
|
||||
// Token makes the token as the vault does: the bus it states, its signing key, a secret, and the first tunnel it is handed.
|
||||
func (v *standInVault) Token(node string, expires time.Time, tunnel delivery.TokenTunnel) (mv.EnrolmentTokenAnswer, error) {
|
||||
func (v *standInVault) Token(node string, expires time.Time, tunnel meshnodeenrollerclient.TokenTunnel) (mv.EnrolmentTokenAnswer, error) {
|
||||
v.mu.Lock()
|
||||
v.tokens[node] = delivery.Token{Version: 1, Node: node, Broker: "bus:4222", Fingerprint: "f", Signer: make([]byte, 32), Secret: "secret-" + node, Tunnel: &tunnel}
|
||||
v.tokens[node] = meshnodeenrollerclient.Token{Version: 1, Node: node, Broker: "bus:4222", Fingerprint: "f", Signer: make([]byte, 32), Secret: "secret-" + node, Tunnel: &tunnel}
|
||||
v.mu.Unlock()
|
||||
v.note("token " + node)
|
||||
return mv.EnrolmentTokenAnswer{Node: node, Broker: "bus:4222", Fingerprint: "f", Signer: "s"}, nil
|
||||
@@ -172,21 +175,21 @@ func (v *standInVault) Drop(node string) error {
|
||||
return nil
|
||||
}
|
||||
func (v *standInVault) Account(node, sealing, digest string) (mv.NodeAccountAnswer, error) {
|
||||
raw, err := delivery.EncodeNodeLogin(delivery.NodeLogin{Node: node, Login: delivery.FirstLogin(node), Since: time.Now().UTC()})
|
||||
raw, err := meshvaultclient.EncodeNodeLogin(mv.NodeLoginsByNodeValue{Node: node, Login: meshvaultclient.FirstLogin(node), Since: time.Now().UTC()})
|
||||
if err != nil {
|
||||
return mv.NodeAccountAnswer{}, err
|
||||
}
|
||||
if _, err := v.logins.Put(v.ctx, node, raw); err != nil {
|
||||
return mv.NodeAccountAnswer{}, err
|
||||
}
|
||||
return mv.NodeAccountAnswer{Node: node, Login: delivery.FirstLogin(node), Account: "account-" + node}, nil
|
||||
return mv.NodeAccountAnswer{Node: node, Login: meshvaultclient.FirstLogin(node), Account: "account-" + node}, nil
|
||||
}
|
||||
func (v *standInVault) Retire(node string) error { return nil }
|
||||
func (v *standInVault) SignCertificate(node string, _ delivery.JoiningKeys) (mv.SignAnswer, error) {
|
||||
return mv.SignAnswer{Kind: delivery.KindNodeCertificate, Node: node, Certificate: "PEM"}, nil
|
||||
func (v *standInVault) SignCertificate(node string, _ meshmoduleassigner.ProposalChangeKeys) (mv.SignAnswer, error) {
|
||||
return mv.SignAnswer{Kind: meshvaultclient.KindNodeCertificate, Node: node, Certificate: "PEM"}, nil
|
||||
}
|
||||
func (v *standInVault) SignReply(node string, reply []byte) (mv.SignAnswer, error) {
|
||||
return mv.SignAnswer{Kind: delivery.KindEnrolment, Node: node, Signature: "sig", Signer: "fp"}, nil
|
||||
return mv.SignAnswer{Kind: meshvaultclient.KindEnrolment, Node: node, Signature: "sig", Signer: "fp"}, nil
|
||||
}
|
||||
func (v *standInVault) Signer() (string, error) { return "vault-key", nil }
|
||||
|
||||
@@ -220,9 +223,9 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) {
|
||||
}
|
||||
return kv
|
||||
}
|
||||
enrolments := mk(delivery.EnrolmentsBucket, true)
|
||||
enrolments := mk(enrolmentsBucket, true)
|
||||
// No controller lease bucket at all: the controller ended by its unassignment (issue 1126).
|
||||
logins, placement := mk(delivery.NodeLoginsBucket, false), mk("mesh-delivery-assigner_placement", false)
|
||||
logins, placement := mk(nodeLoginsBucket, false), mk("mesh-delivery-assigner_placement", false)
|
||||
have := mk("node-engine_have", false)
|
||||
if _, err := placement.Put(ctx, "hub.node", []byte(hubPlacement)); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -249,7 +252,7 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) {
|
||||
events := func() []string { heardMu.Lock(); defer heardMu.Unlock(); return slices.Clone(heard) }
|
||||
|
||||
store := &busStore{ctx: ctx, js: js, kv: enrolments}
|
||||
vault := &standInVault{ctx: ctx, logins: logins, tokens: map[string]delivery.Token{}}
|
||||
vault := &standInVault{ctx: ctx, logins: logins, tokens: map[string]meshnodeenrollerclient.Token{}}
|
||||
asker := &fakeAsker{}
|
||||
h := &Holder{Store: store, Read: busReader{ctx, js}, Vault: vault, Asker: asker, AskLasts: time.Hour,
|
||||
Say: func(event string, body any, id string) error {
|
||||
@@ -259,7 +262,7 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) {
|
||||
},
|
||||
// mesh-placement, stood in: an add-node writes the node's key, a remove-node deletes it, each saying placement-changed.
|
||||
AddNode: func(a AddNode) error {
|
||||
if a.Change.Op == delivery.OpRemoveNode {
|
||||
if a.Change.Op == meshmoduleassignerclient.OpRemoveNode {
|
||||
if err := placement.Delete(ctx, a.Change.Node+".node"); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -371,7 +374,7 @@ func TestMeshEnrolmentWholeOnARealBus(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
nl, err := delivery.DecodeNodeLogin(e.Value())
|
||||
nl, err := meshvaultclient.DecodeNodeLogin(e.Value())
|
||||
if err != nil || nl.Login != ans.Login {
|
||||
t.Fatalf("node-logins says %+v, %v, and the answer %s", nl, err, ans.Login)
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// Event is what each of the seat's events carries (ADR 0587 §2): the key's whole content, which holds no secret and nothing
|
||||
@@ -63,10 +63,10 @@ func (h *Holder) Issue(caller, node, why, overlayKey string, lasts time.Duration
|
||||
findings = append(findings, err.Error())
|
||||
}
|
||||
if lasts == 0 {
|
||||
lasts = delivery.DefaultTokenLife
|
||||
lasts = meshnodeenrollerclient.DefaultTokenLife
|
||||
}
|
||||
if lasts < 0 || lasts > delivery.MostTokenLife {
|
||||
findings = append(findings, fmt.Sprintf("the expiry %s is outside zero to %s", lasts, delivery.MostTokenLife))
|
||||
if lasts < 0 || lasts > meshnodeenrollerclient.MostTokenLife {
|
||||
findings = append(findings, fmt.Sprintf("the expiry %s is outside zero to %s", lasts, meshnodeenrollerclient.MostTokenLife))
|
||||
}
|
||||
placements, perr := h.placements()
|
||||
if perr != nil {
|
||||
@@ -289,7 +289,7 @@ func (h *Holder) dropAddress(e Enrolment) error {
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
if r, err := delivery.DecodeEnrolmentAddress(row.Value); err == nil && r.Node != e.Node {
|
||||
if r, err := decodeReservation(row.Value); err == nil && r.Node != e.Node {
|
||||
return nil // another enrolment holds the address now
|
||||
}
|
||||
if err := h.Store.Delete(AddressKey(e.Address)); err != nil {
|
||||
|
||||
@@ -7,9 +7,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
)
|
||||
|
||||
var yes = oc.Option{ID: "yes"}
|
||||
@@ -103,11 +104,11 @@ func twoIssuesAtOnceHoldTwoAddressesAndAYesMakesTheToken(t *testing.T) {
|
||||
}
|
||||
// The address key names the ask and the issue, with no expiry of its own, so it outlives the enrolment key (ADR 0589 §6).
|
||||
if row, _, _ := r.st.Get(AddressKey(live.Address)); true {
|
||||
if res, err := delivery.DecodeEnrolmentAddress(row.Value); err != nil || res.Ask != e.Ask || !res.Issued.Equal(live.Issued) {
|
||||
if res, err := decodeReservation(row.Value); err != nil || res.Ask != e.Ask || !res.Issued.Equal(live.Issued) {
|
||||
t.Errorf("the address key %s: %+v, %v", row.Value, res, err)
|
||||
}
|
||||
}
|
||||
if got, want := r.vault.tunnels["ace"], (delivery.TokenTunnel{Key: tunnelKey, Address: live.Address + "/32", Range: "10.9.0.0/24",
|
||||
if got, want := r.vault.tunnels["ace"], (meshnodeenrollerclient.TokenTunnel{Key: tunnelKey, Address: live.Address + "/32", Range: "10.9.0.0/24",
|
||||
HubKey: "hub-tunnel-key", HubEndpoint: "192.0.2.1:51820"}); got != want {
|
||||
t.Errorf("the token's tunnel %+v, want %+v", got, want)
|
||||
}
|
||||
@@ -116,7 +117,7 @@ func twoIssuesAtOnceHoldTwoAddressesAndAYesMakesTheToken(t *testing.T) {
|
||||
t.Errorf("the token was made before placement held the node: %v", r.vault.calls)
|
||||
}
|
||||
for _, a := range r.added {
|
||||
if a.Change.Node == "ace" && (*a.Change.Keys != delivery.JoiningKeys{Overlay: tunnelKey} || a.Warrant.Ask != e.Ask || a.Change.Decided.Address != live.Address) {
|
||||
if a.Change.Node == "ace" && (*a.Change.Keys != meshmoduleassigner.ProposalChangeKeys{Overlay: tunnelKey} || a.Warrant.Ask != e.Ask || a.Change.Decided.Address != live.Address) {
|
||||
t.Errorf("the add-node at the yes %+v", a)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,12 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
|
||||
mv "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
)
|
||||
|
||||
// memStore is the holder's bucket in memory: revisions, and the expiry each key was written with.
|
||||
@@ -117,8 +120,8 @@ func (r memRead) Get(state, key string) ([]byte, bool, error) {
|
||||
type fakeVault struct {
|
||||
calls []string
|
||||
fail map[string]bool
|
||||
tunnels map[string]delivery.TokenTunnel
|
||||
cert delivery.JoiningKeys // the keys the certificate's call carried
|
||||
tunnels map[string]meshnodeenrollerclient.TokenTunnel
|
||||
cert meshmoduleassigner.ProposalChangeKeys // the keys the certificate's call carried
|
||||
}
|
||||
|
||||
func (v *fakeVault) do(what string) error {
|
||||
@@ -130,9 +133,9 @@ func (v *fakeVault) do(what string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *fakeVault) Token(node string, expires time.Time, tunnel delivery.TokenTunnel) (mv.EnrolmentTokenAnswer, error) {
|
||||
func (v *fakeVault) Token(node string, expires time.Time, tunnel meshnodeenrollerclient.TokenTunnel) (mv.EnrolmentTokenAnswer, error) {
|
||||
if v.tunnels == nil {
|
||||
v.tunnels = map[string]delivery.TokenTunnel{}
|
||||
v.tunnels = map[string]meshnodeenrollerclient.TokenTunnel{}
|
||||
}
|
||||
v.tunnels[node] = tunnel
|
||||
return mv.EnrolmentTokenAnswer{Node: node, Broker: "b", Fingerprint: "f", Signer: "s"}, v.do("token " + node)
|
||||
@@ -145,13 +148,13 @@ func (v *fakeVault) Account(node, sealing, digest string) (mv.NodeAccountAnswer,
|
||||
return mv.NodeAccountAnswer{Node: node, Login: "node." + node, Account: "account-" + node}, v.do("account " + node)
|
||||
}
|
||||
func (v *fakeVault) Retire(node string) error { return v.do("retire " + node) }
|
||||
func (v *fakeVault) SignCertificate(node string, k delivery.JoiningKeys) (mv.SignAnswer, error) {
|
||||
func (v *fakeVault) SignCertificate(node string, k meshmoduleassigner.ProposalChangeKeys) (mv.SignAnswer, error) {
|
||||
serving := k.Serving
|
||||
v.cert = k
|
||||
return mv.SignAnswer{Kind: delivery.KindNodeCertificate, Node: node, Certificate: "PEM-" + serving}, v.do("certificate " + node)
|
||||
return mv.SignAnswer{Kind: meshvaultclient.KindNodeCertificate, Node: node, Certificate: "PEM-" + serving}, v.do("certificate " + node)
|
||||
}
|
||||
func (v *fakeVault) SignReply(node string, reply []byte) (mv.SignAnswer, error) {
|
||||
return mv.SignAnswer{Kind: delivery.KindEnrolment, Node: node, Signature: "sig-" + delivery.DigestOf(reply)[7:15], Signer: "fp"}, v.do("sign " + node)
|
||||
return mv.SignAnswer{Kind: meshvaultclient.KindEnrolment, Node: node, Signature: "sig-" + meshvaultclient.DigestOf(reply)[7:15], Signer: "fp"}, v.do("sign " + node)
|
||||
}
|
||||
func (v *fakeVault) Signer() (string, error) { return "vault-signer", nil }
|
||||
|
||||
@@ -247,7 +250,7 @@ func newRig(t *testing.T) *rig {
|
||||
r.vault.calls = append(r.vault.calls, a.Change.Op+" "+a.Change.Node)
|
||||
if r.placeErr == nil { // placement writes or deletes the node as the assigner does on the warrant
|
||||
n := a.Change.Node
|
||||
if a.Change.Op == delivery.OpRemoveNode {
|
||||
if a.Change.Op == meshmoduleassignerclient.OpRemoveNode {
|
||||
delete(r.rd["mesh-delivery-assigner.placement"], n+".node")
|
||||
} else {
|
||||
r.rd["mesh-delivery-assigner.placement"][n+".node"] = `{"v":1,"node":"` + n + `","decided":{"address":"` + a.Change.Decided.Address + `"},"modules":[]}`
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
@@ -117,8 +116,8 @@ func (r followedReader) Get(state, key string) ([]byte, bool, error) {
|
||||
// PlacementRead is the assigner's placement as the enrolment reads it, and PlacementTool the assigner's read tool the
|
||||
// manifest invokes for its first view (ADR 0615, DLV-M6).
|
||||
const (
|
||||
PlacementRead = delivery.AssignerModule + "." + delivery.PlacementState
|
||||
PlacementTool = delivery.AssignerModule + ".assigner_read_placement"
|
||||
PlacementRead = assignerModule + "." + placementState
|
||||
PlacementTool = assignerModule + ".assigner_read_placement"
|
||||
)
|
||||
|
||||
// followed is the enrolment's pictures of the assigner's placement, started at once: its first view asked through
|
||||
@@ -126,7 +125,7 @@ const (
|
||||
func followed() *views.Reader {
|
||||
v := &views.Reader{Module: Module, Logf: func(f string, a ...any) { logf("["+Module+"] "+f, a...) }, Findings: views.OwnFindings{},
|
||||
Acts: meshhandactsclient.Recorder{Ask: stdio.Ask}}
|
||||
v.Follow(delivery.AssignerModule, delivery.PlacementState, PlacementTool)
|
||||
v.Follow(assignerModule, placementState, PlacementTool)
|
||||
v.Start(context.Background())
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package main
|
||||
|
||||
import "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller"
|
||||
|
||||
// The holders, states and buckets the enrolment names beside its own, taken from its manifest (module.json: its module, its
|
||||
// `reads`, its state, the seats it invokes); a unit row checks them (TestTheNamesAreTheManifests). No contract names a
|
||||
// holder (novox/hq ADR 0625 §4, ADR 0634 §1).
|
||||
const (
|
||||
vaultModule = "mesh-vault"
|
||||
assignerModule = "mesh-delivery-assigner"
|
||||
|
||||
// enrolmentsState is the holder's state of open enrolments, enrolmentsBucket its bucket.
|
||||
enrolmentsState = meshnodeenroller.StateEnrolments
|
||||
enrolmentsBucket = Module + "_" + enrolmentsState
|
||||
// signersRead is the vault's state signers as the enrolment's manifest reads it, signersKey the key holding its signing key.
|
||||
signersRead = vaultModule + ".signers"
|
||||
signersKey = "key"
|
||||
// nodeLoginsBucket is the vault's state node-logins (the holder reads it through the grants part, not directly).
|
||||
nodeLoginsBucket = vaultModule + "_node-logins"
|
||||
// placementState is the assigner's state placement, which the enrolment reads (reads: through its first view).
|
||||
placementState = "placement"
|
||||
)
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
)
|
||||
|
||||
// Redeem is the joining node-engine's one call (ADR 0587 §5, §6). It answers only a caller the runtime proves to be
|
||||
@@ -18,55 +19,55 @@ import (
|
||||
// of the keys presented,
|
||||
// waits for the bus node to load node.<node>, and answers the whole, signed by the vault. Called again with the same keys it
|
||||
// finishes what is not done and answers the same.
|
||||
func (h *Holder) Redeem(caller string, a delivery.Redeem) (delivery.RedeemAnswer, error) {
|
||||
func (h *Holder) Redeem(caller string, a meshnodeenrollerclient.Redeem) (meshnodeenrollerclient.RedeemAnswer, error) {
|
||||
if err := CheckNode(a.Node); err != nil {
|
||||
return delivery.RedeemAnswer{}, err
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, err
|
||||
}
|
||||
if caller != EnrolUser(a.Node) {
|
||||
return delivery.RedeemAnswer{}, fmt.Errorf("only %s may redeem the enrolment of %s, and the runtime proved the caller to be %q", EnrolUser(a.Node), a.Node, caller)
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("only %s may redeem the enrolment of %s, and the runtime proved the caller to be %q", EnrolUser(a.Node), a.Node, caller)
|
||||
}
|
||||
pub, err := base64.StdEncoding.DecodeString(a.PublicKey)
|
||||
proof, perr := a.ProofBytes()
|
||||
if err != nil || perr != nil || len(pub) != ed25519.PublicKeySize ||
|
||||
!ed25519.Verify(pub, delivery.RedeemProof(a.Node, a.PublicKey, a.OverlayKey, a.SealingKey, a.ServingKey), proof) {
|
||||
return delivery.RedeemAnswer{}, errors.New("the proof does not verify against the identity key presented: the enrolment is not spent")
|
||||
!ed25519.Verify(pub, meshnodeenrollerclient.RedeemProof(a.Node, a.PublicKey, a.OverlayKey, a.SealingKey, a.ServingKey), proof) {
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, errors.New("the proof does not verify against the identity key presented: the enrolment is not spent")
|
||||
}
|
||||
if a.SealingKey == "" {
|
||||
return delivery.RedeemAnswer{}, errors.New("no sealing key is presented, so the account cannot be sealed to the node: the enrolment is not spent")
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, errors.New("no sealing key is presented, so the account cannot be sealed to the node: the enrolment is not spent")
|
||||
}
|
||||
digest := a.KeysDigest()
|
||||
e, err := h.spend(a.Node, digest, a.OverlayKey)
|
||||
if err != nil {
|
||||
return delivery.RedeemAnswer{}, err
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, err
|
||||
}
|
||||
account, err := h.Vault.Account(e.Node, a.SealingKey, digest)
|
||||
if err != nil {
|
||||
return delivery.RedeemAnswer{}, fmt.Errorf("the vault did not give the account of %s (call redeem again with the same keys): %w", e.Node, err)
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("the vault did not give the account of %s (call redeem again with the same keys): %w", e.Node, err)
|
||||
}
|
||||
cert, err := h.Vault.SignCertificate(e.Node, delivery.JoiningKeys{Identity: a.PublicKey, Overlay: a.OverlayKey, Sealing: a.SealingKey, Serving: a.ServingKey})
|
||||
cert, err := h.Vault.SignCertificate(e.Node, meshmoduleassigner.ProposalChangeKeys{Identity: a.PublicKey, Overlay: a.OverlayKey, Sealing: a.SealingKey, Serving: a.ServingKey})
|
||||
if err != nil {
|
||||
return delivery.RedeemAnswer{}, fmt.Errorf("the vault did not sign the certificate of %s (call redeem again with the same keys): %w", e.Node, err)
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("the vault did not sign the certificate of %s (call redeem again with the same keys): %w", e.Node, err)
|
||||
}
|
||||
if h.Loaded != nil {
|
||||
loaded, err := h.Loaded(NodeUser(e.Node))
|
||||
if err != nil {
|
||||
return delivery.RedeemAnswer{}, fmt.Errorf("whether the bus node loaded %s cannot be read (call redeem again with the same keys): %w", NodeUser(e.Node), err)
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("whether the bus node loaded %s cannot be read (call redeem again with the same keys): %w", NodeUser(e.Node), err)
|
||||
}
|
||||
if !loaded {
|
||||
return delivery.RedeemAnswer{}, fmt.Errorf("the bus node has not loaded %s yet (call redeem again with the same keys)", NodeUser(e.Node))
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("the bus node has not loaded %s yet (call redeem again with the same keys)", NodeUser(e.Node))
|
||||
}
|
||||
}
|
||||
signer, err := h.Vault.Signer()
|
||||
if err != nil {
|
||||
return delivery.RedeemAnswer{}, fmt.Errorf("the vault's signing key cannot be named (call redeem again with the same keys): %w", err)
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("the vault's signing key cannot be named (call redeem again with the same keys): %w", err)
|
||||
}
|
||||
out := delivery.RedeemAnswer{Node: e.Node, Login: account.Login, Account: account.Account, Certificate: cert.Certificate, Signer: signer}
|
||||
out := meshnodeenrollerclient.RedeemAnswer{Node: e.Node, Login: account.Login, Account: account.Account, Certificate: cert.Certificate, Signer: signer}
|
||||
if out.Login == "" {
|
||||
out.Login = NodeUser(e.Node)
|
||||
}
|
||||
signed, err := h.Vault.SignReply(e.Node, out.SignedBytes())
|
||||
if err != nil {
|
||||
return delivery.RedeemAnswer{}, fmt.Errorf("the vault did not sign the answer (call redeem again with the same keys): %w", err)
|
||||
return meshnodeenrollerclient.RedeemAnswer{}, fmt.Errorf("the vault did not sign the answer (call redeem again with the same keys): %w", err)
|
||||
}
|
||||
out.Signature = signed.Signature
|
||||
return out, nil
|
||||
|
||||
@@ -7,20 +7,20 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
)
|
||||
|
||||
// redeemArgs are a node's redeem call with a proof signed by the key seeded, over the keys given.
|
||||
func redeemArgs(node string, seed byte, serving string) delivery.Redeem {
|
||||
func redeemArgs(node string, seed byte, serving string) meshnodeenrollerclient.Redeem {
|
||||
return redeemArgsWith(node, seed, serving, tunnelKey)
|
||||
}
|
||||
|
||||
// redeemArgsWith is redeemArgs presenting the tunnel key given.
|
||||
func redeemArgsWith(node string, seed byte, serving, overlay string) delivery.Redeem {
|
||||
func redeemArgsWith(node string, seed byte, serving, overlay string) meshnodeenrollerclient.Redeem {
|
||||
priv := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{seed}, ed25519.SeedSize))
|
||||
a := delivery.Redeem{Node: node, PublicKey: base64.StdEncoding.EncodeToString(priv.Public().(ed25519.PublicKey)),
|
||||
a := meshnodeenrollerclient.Redeem{Node: node, PublicKey: base64.StdEncoding.EncodeToString(priv.Public().(ed25519.PublicKey)),
|
||||
OverlayKey: overlay, SealingKey: "sealing-" + node, ServingKey: serving}
|
||||
a.Proof = base64.StdEncoding.EncodeToString(ed25519.Sign(priv, delivery.RedeemProof(node, a.PublicKey, a.OverlayKey, a.SealingKey, a.ServingKey)))
|
||||
a.Proof = base64.StdEncoding.EncodeToString(ed25519.Sign(priv, meshnodeenrollerclient.RedeemProof(node, a.PublicKey, a.OverlayKey, a.SealingKey, a.ServingKey)))
|
||||
return a
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func redeemRefusesWhatItMustAndSpendsTheTokenOnce(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
caller string
|
||||
args delivery.Redeem
|
||||
args meshnodeenrollerclient.Redeem
|
||||
setup func(r *rig)
|
||||
want string
|
||||
}{
|
||||
@@ -54,11 +54,11 @@ func redeemRefusesWhatItMustAndSpendsTheTokenOnce(t *testing.T) {
|
||||
_, _ = r.st.Put(NodeKey("ace"), e, 0)
|
||||
}, "waits for the operator's yes"},
|
||||
{"no enrolment", "enrol.novox", redeemArgs("novox", 3, "s"), nil, "no enrolment of novox is open"},
|
||||
{"another tunnel key than issued", "enrol.ace", func() delivery.Redeem {
|
||||
{"another tunnel key than issued", "enrol.ace", func() meshnodeenrollerclient.Redeem {
|
||||
priv := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{1}, ed25519.SeedSize))
|
||||
a := good
|
||||
a.OverlayKey = "another-tunnel-key"
|
||||
a.Proof = base64.StdEncoding.EncodeToString(ed25519.Sign(priv, delivery.RedeemProof("ace", a.PublicKey, a.OverlayKey, a.SealingKey, a.ServingKey)))
|
||||
a.Proof = base64.StdEncoding.EncodeToString(ed25519.Sign(priv, meshnodeenrollerclient.RedeemProof("ace", a.PublicKey, a.OverlayKey, a.SealingKey, a.ServingKey)))
|
||||
return a
|
||||
}(), nil, "not the one ace's enrolment was issued for"},
|
||||
{"not in placement yet", "enrol.ace", good, func(r *rig) { delete(r.rd["mesh-delivery-assigner.placement"], "ace.node") }, "placement does not hold ace"},
|
||||
@@ -95,7 +95,7 @@ func redeemRefusesWhatItMustAndSpendsTheTokenOnce(t *testing.T) {
|
||||
t.Errorf("placement %+v, answer %+v", r.added, ans)
|
||||
}
|
||||
// The certificate's call carries every key presented, which the vault checks against the account's digest.
|
||||
if r.vault.cert.Digest() != good.KeysDigest() {
|
||||
if meshnodeenrollerclient.KeysDigest(r.vault.cert.Identity, r.vault.cert.Overlay, r.vault.cert.Sealing, r.vault.cert.Serving) != good.KeysDigest() {
|
||||
t.Errorf("the certificate was asked with %+v", r.vault.cert)
|
||||
}
|
||||
r.loaded = false
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
)
|
||||
|
||||
// The holder's state: the two kinds of key of `enrolments`, whose forms are the seat's contract (meshnodeenroller:
|
||||
// EnrolmentsByNodeNodeValue, EnrolmentsByAddressAddressValue). The contract's decoding is strict by schema only; the rules
|
||||
// of a value are the holder's, who writes it, and are kept here once (novox/hq ADR 0587 §2, ADR 0634 §2).
|
||||
|
||||
// The stages of an open enrolment, the contract's `asked, live or redeeming`.
|
||||
const (
|
||||
stageAsked = "asked"
|
||||
stageLive = "live"
|
||||
stageRedeeming = "redeeming"
|
||||
)
|
||||
|
||||
// Enrolment is one key `node.<node>` of `enrolments`: the node's name, the private-network address chosen at issue, the
|
||||
// why, the caller as the runtime proved it, the ask's id, the stage, when it was issued and when the token expires, and
|
||||
// from redeem the digest of the public keys presented. No secret and nothing derived from the token.
|
||||
type Enrolment struct {
|
||||
V int `json:"v"`
|
||||
Node string `json:"node"`
|
||||
Address string `json:"address"`
|
||||
Why string `json:"why"`
|
||||
Caller string `json:"caller"`
|
||||
Ask string `json:"ask,omitempty"`
|
||||
Stage string `json:"stage"`
|
||||
Issued time.Time `json:"issued"`
|
||||
Expires time.Time `json:"expires"`
|
||||
KeysDigest string `json:"keys-digest,omitempty"`
|
||||
// OverlayKey is the machine's tunnel public key, base64 of 32 bytes, given at issue.
|
||||
OverlayKey string `json:"overlay-key"`
|
||||
}
|
||||
|
||||
// Reservation is one key `address.<address>`: the node the address is reserved for and, from when the issue's ask is
|
||||
// raised, that ask's id and the issue's time. Ask and Issued are both empty before the ask and both set from it.
|
||||
type Reservation struct {
|
||||
V int `json:"v"`
|
||||
Node string `json:"node"`
|
||||
Ask string `json:"ask,omitempty"`
|
||||
Issued time.Time `json:"issued,omitzero"`
|
||||
}
|
||||
|
||||
var digestForm = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
|
||||
|
||||
func nodeKey(node string) string { return meshnodeenroller.EnrolmentsByNodeNodeKey(node) }
|
||||
func addressKey(addr string) string { return meshnodeenroller.EnrolmentsByAddressAddressKey(addr) }
|
||||
func timeOf(t *time.Time) time.Time {
|
||||
if t == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return *t
|
||||
}
|
||||
|
||||
func (e Enrolment) check() error {
|
||||
if err := CheckNode(e.Node); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := netip.ParseAddr(e.Address); err != nil {
|
||||
return fmt.Errorf("the enrolment of %s names the address %q, which is not an address", e.Node, e.Address)
|
||||
}
|
||||
if strings.TrimSpace(e.Why) == "" || strings.TrimSpace(e.Caller) == "" {
|
||||
return fmt.Errorf("the enrolment of %s names no why or no caller", e.Node)
|
||||
}
|
||||
if k, err := base64.StdEncoding.DecodeString(e.OverlayKey); err != nil || len(k) != 32 {
|
||||
return fmt.Errorf("the enrolment of %s names no tunnel key of 32 bytes in base64", e.Node)
|
||||
}
|
||||
switch e.Stage {
|
||||
case stageAsked, stageLive:
|
||||
if e.KeysDigest != "" {
|
||||
return fmt.Errorf("the enrolment of %s at %s names presented keys, which only a redeem gives", e.Node, e.Stage)
|
||||
}
|
||||
case stageRedeeming:
|
||||
if !digestForm.MatchString(e.KeysDigest) {
|
||||
return fmt.Errorf("the enrolment's keys digest %q is not sha256:<64 lowercase hex digits>", e.KeysDigest)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("the enrolment of %s is at stage %q, not asked, live or redeeming", e.Node, e.Stage)
|
||||
}
|
||||
if e.Issued.IsZero() || !e.Expires.After(e.Issued) {
|
||||
return fmt.Errorf("the enrolment of %s names no issue or an expiry not after it", e.Node)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// encodeEnrolment is e as JSON at the layout version; it refuses what decodeEnrolment refuses.
|
||||
func encodeEnrolment(e Enrolment) ([]byte, error) {
|
||||
if err := e.check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.V = keys.Version
|
||||
return json.Marshal(e)
|
||||
}
|
||||
|
||||
// decodeEnrolment reads a key `node.<node>` through the contract's decoding; it refuses an unknown version, a node that is
|
||||
// not a name, an address that is not one, an unknown stage, presented keys before redeem or none at it, and an expiry
|
||||
// not after the issue.
|
||||
func decodeEnrolment(raw []byte) (Enrolment, error) {
|
||||
if err := keys.CheckVersion(raw); err != nil {
|
||||
return Enrolment{}, err
|
||||
}
|
||||
c, err := meshnodeenroller.DecodeEnrolmentsByNodeNodeValue(raw)
|
||||
if err != nil {
|
||||
return Enrolment{}, fmt.Errorf("the enrolment does not fit its shape: %w", err)
|
||||
}
|
||||
e := Enrolment{V: int(c.V), Node: c.Node, Address: c.Address, Why: c.Why, Caller: c.Caller, Ask: c.Ask, Stage: c.Stage,
|
||||
Issued: timeOf(c.Issued), Expires: timeOf(c.Expires), KeysDigest: c.KeysDigest, OverlayKey: c.OverlayKey}
|
||||
if err := e.check(); err != nil {
|
||||
return Enrolment{}, err
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func (r Reservation) check() error {
|
||||
if err := CheckNode(r.Node); err != nil {
|
||||
return err
|
||||
}
|
||||
if (strings.TrimSpace(r.Ask) == "") != r.Issued.IsZero() {
|
||||
return fmt.Errorf("the address reserved for %s names its ask without the issue's time, or the reverse", r.Node)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// encodeReservation is a key `address.<address>` reserving it for node: with no ask and a zero issued when it is reserved,
|
||||
// with the issue's ask and time once the ask is raised.
|
||||
func encodeReservation(node, ask string, issued time.Time) ([]byte, error) {
|
||||
r := Reservation{V: keys.Version, Node: node, Ask: ask, Issued: issued}
|
||||
if err := r.check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(r)
|
||||
}
|
||||
|
||||
// decodeReservation reads a key `address.<address>` through the contract's decoding; it refuses no node, and an ask without
|
||||
// the issue's time or the reverse.
|
||||
func decodeReservation(raw []byte) (Reservation, error) {
|
||||
if err := keys.CheckVersion(raw); err != nil {
|
||||
return Reservation{}, err
|
||||
}
|
||||
c, err := meshnodeenroller.DecodeEnrolmentsByAddressAddressValue(raw)
|
||||
if err != nil {
|
||||
return Reservation{}, fmt.Errorf("the reservation does not fit its shape: %w", err)
|
||||
}
|
||||
r := Reservation{V: int(c.V), Node: c.Node, Ask: c.Ask, Issued: timeOf(c.Issued)}
|
||||
if err := r.check(); err != nil {
|
||||
return Reservation{}, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// expiredEvent is the body of enrolment-expired, the contract's: the enrolment as it stood. The enrolment-issued event it
|
||||
// ends is named by the issue time (the client's IssuedID).
|
||||
func expiredEvent(e Enrolment) meshnodeenroller.EnrolmentExpiredEvent {
|
||||
issued := e.Issued
|
||||
return meshnodeenroller.EnrolmentExpiredEvent{Node: e.Node, Address: e.Address, Ask: e.Ask, Stage: stageLive, Issued: &issued, V: keys.Version}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
)
|
||||
|
||||
// seed writes an open enrolment of node at stage, with its address key, as the holder would have.
|
||||
@@ -17,7 +17,7 @@ func (r *rig) seed(node, address string, stage string) {
|
||||
e := Enrolment{Node: node, Address: address, Why: "a new machine", Caller: "person.operator", Ask: "ask-" + node,
|
||||
Stage: stage, Issued: r.now.Add(-time.Minute), Expires: r.now.Add(time.Hour), OverlayKey: tunnelKey}
|
||||
if stage == StageRedeeming {
|
||||
e.KeysDigest = delivery.KeysDigest("pk", "ov", "se", "sv")
|
||||
e.KeysDigest = meshnodeenrollerclient.KeysDigest("pk", "ov", "se", "sv")
|
||||
}
|
||||
_, _ = r.st.Put(NodeKey(node), e, 0)
|
||||
_, _ = r.st.Put(AddressKey(address), Reservation{Node: node, Ask: e.Ask, Issued: e.Issued}, 0)
|
||||
@@ -107,11 +107,19 @@ func theManifestSaysWhatTheCodeDoes(t *testing.T) {
|
||||
if !slices.Contains(m.Invokes, PlacementTool) || !slices.Equal(m.Tools, []string{"enrolment_first_views", "enrolment_first_view_again"}) {
|
||||
t.Errorf("invokes %v, tools %v: want %s and the first-view tools", m.Invokes, m.Tools, PlacementTool)
|
||||
}
|
||||
// The holders the holder names beside its own are the manifest's (D-11): the vault's signers it reads, the seats it invokes.
|
||||
if !slices.Contains(m.Reads, signersRead) || !slices.Contains(m.Invokes, placementAdd) || !slices.Contains(m.Invokes, vaultSign) ||
|
||||
!slices.Contains(m.Invokes, vaultToken) || !slices.Contains(m.Invokes, vaultAccount) {
|
||||
t.Errorf("reads %v, invokes %v: want %s, %s, %s, %s and %s", m.Reads, m.Invokes, signersRead, placementAdd, vaultSign, vaultToken, vaultAccount)
|
||||
}
|
||||
if enrolmentsBucket != m.Module+"_"+State || !strings.HasPrefix(PlacementTool, assignerModule+".") {
|
||||
t.Errorf("the names %s, %s are not the manifest's module %s and the assigner %s", enrolmentsBucket, PlacementTool, m.Module, assignerModule)
|
||||
}
|
||||
var heard []string
|
||||
for _, e := range EndEvents {
|
||||
heard = append(heard, Seat+"."+e)
|
||||
}
|
||||
heard = append(heard, "mesh-delivery-assigner.views.placement")
|
||||
heard = append(heard, assignerModule+".views."+placementState)
|
||||
if !slices.Equal(m.Consumes, heard) {
|
||||
t.Errorf("consumes %v, the end events are %v", m.Consumes, heard)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// Tool names: the seat's verbs, as the manifest claims them (ADR 0587 §1, §8).
|
||||
@@ -39,8 +39,8 @@ func flag(a map[string]any, k string) bool {
|
||||
return a[k] == true || strings.EqualFold(arg(a, k), "true")
|
||||
}
|
||||
|
||||
// lastsOf reads an expiry as a duration (delivery.TokenLife); none is the default.
|
||||
func lastsOf(s string) (time.Duration, error) { return delivery.TokenLife(s) }
|
||||
// lastsOf reads an expiry as a duration (meshnodeenrollerclient.TokenLife); none is the default.
|
||||
func lastsOf(s string) (time.Duration, error) { return meshnodeenrollerclient.TokenLife(s) }
|
||||
|
||||
// Tools are what the runtime serves.
|
||||
func Tools(h *Holder) []stdio.Tool {
|
||||
@@ -72,7 +72,7 @@ func Tools(h *Holder) []stdio.Tool {
|
||||
"overlay_key": str("its private-network key"), "sealing_key": str("its sealing key, base64"),
|
||||
"serving_key": str("its serving key"), "proof": str("the proof, base64")}, "node", "public_key", "proof"),
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
var args delivery.Redeem
|
||||
var args meshnodeenrollerclient.Redeem
|
||||
raw, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
|
||||
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
|
||||
mv "git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
|
||||
)
|
||||
|
||||
// Seats the holder calls (ADR 0587 §3, §5, §7). The vault's two verbs are the ones its manifest names this seat the only
|
||||
@@ -17,7 +19,7 @@ const (
|
||||
vaultToken = "seat:mesh-vault." + mv.VerbEnrolmentToken
|
||||
vaultAccount = "seat:mesh-vault." + mv.VerbNodeAccount
|
||||
vaultSign = "seat:mesh-vault." + mv.VerbSign
|
||||
placementAdd = "seat:mesh-placement.propose"
|
||||
placementAdd = "seat:" + meshmoduleassigner.Seat + "." + meshmoduleassigner.VerbPropose
|
||||
)
|
||||
|
||||
// Call asks a seat verb by its address (stdio.Ask through the runtime, or a test's).
|
||||
@@ -40,8 +42,8 @@ func answer[T any](raw json.RawMessage, err error) (T, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (v vaultOver) Token(node string, expires time.Time, t delivery.TokenTunnel) (mv.EnrolmentTokenAnswer, error) {
|
||||
// The whole first tunnel the token carries (delivery.TokenTunnel, catalogue's enrolment-token).
|
||||
func (v vaultOver) Token(node string, expires time.Time, t meshnodeenrollerclient.TokenTunnel) (mv.EnrolmentTokenAnswer, error) {
|
||||
// The whole first tunnel the token carries (meshnodeenrollerclient.TokenTunnel, catalogue's enrolment-token).
|
||||
tunnel := map[string]any{"hub_key": t.HubKey, "hub_endpoint": t.HubEndpoint, "address": t.Address, "key": t.Key, "range": t.Range}
|
||||
return answer[mv.EnrolmentTokenAnswer](v.call(vaultToken, map[string]any{"node": node,
|
||||
"expires": expires.UTC().Format(time.RFC3339), "tunnel": tunnel}))
|
||||
@@ -75,22 +77,22 @@ func (v vaultOver) Retire(node string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (v vaultOver) SignCertificate(node string, k delivery.JoiningKeys) (mv.SignAnswer, error) {
|
||||
return answer[mv.SignAnswer](v.call(vaultSign, map[string]any{"kind": delivery.KindNodeCertificate, "node": node,
|
||||
func (v vaultOver) SignCertificate(node string, k meshmoduleassigner.ProposalChangeKeys) (mv.SignAnswer, error) {
|
||||
return answer[mv.SignAnswer](v.call(vaultSign, map[string]any{"kind": meshvaultclient.KindNodeCertificate, "node": node,
|
||||
"public_key": k.Identity, "overlay_key": k.Overlay, "sealing_key": k.Sealing, "serving_key": k.Serving}))
|
||||
}
|
||||
|
||||
func (v vaultOver) SignReply(node string, reply []byte) (mv.SignAnswer, error) {
|
||||
return answer[mv.SignAnswer](v.call(vaultSign, map[string]any{"kind": delivery.KindEnrolment, "node": node,
|
||||
return answer[mv.SignAnswer](v.call(vaultSign, map[string]any{"kind": meshvaultclient.KindEnrolment, "node": node,
|
||||
"reply": base64.StdEncoding.EncodeToString(reply)}))
|
||||
}
|
||||
|
||||
func (v vaultOver) Signer() (string, error) {
|
||||
raw, found, err := v.read.Get(delivery.SignersRead, delivery.SignersKeyKey)
|
||||
raw, found, err := v.read.Get(signersRead, signersKey)
|
||||
if err != nil || !found {
|
||||
return "", fmt.Errorf("the vault's signing key is not readable in %s: %v", delivery.SignersRead, err)
|
||||
return "", fmt.Errorf("the vault's signing key is not readable in %s: %v", signersRead, err)
|
||||
}
|
||||
k, err := delivery.DecodeSignerKey(raw)
|
||||
k, err := meshvaultclient.DecodeSignerKey(raw)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -102,7 +104,7 @@ func addNodeOver(call Call) func(AddNode) error {
|
||||
return func(a AddNode) error {
|
||||
_, err := call(placementAdd, map[string]any{"why": a.Why, "changes": []any{a.Change}, "warrant": a.Warrant})
|
||||
if err != nil {
|
||||
return fmt.Errorf("mesh-placement did not take the node %s: %w", a.Change.Node, err)
|
||||
return fmt.Errorf("mesh-module-assigner did not take the node %s: %w", a.Change.Node, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,11 @@ module mesh-enrolment
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault v0.0.0-00010101000000-000000000000
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"invokes": [
|
||||
"seat:mesh-hand-acts.hand-act",
|
||||
"seat:mesh-hand-acts.warranted",
|
||||
"seat:mesh-placement.propose",
|
||||
"seat:mesh-module-assigner.propose",
|
||||
"seat:mesh-vault.enrolment-token",
|
||||
"seat:mesh-vault.node-account",
|
||||
"seat:mesh-vault.sign",
|
||||
|
||||
Reference in New Issue
Block a user