diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/end.go b/modules/mesh-enrolment/cmd/mesh-enrolment/end.go index 47f045ad..3bf64a7b 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/end.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/end.go @@ -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) } diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/end_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/end_test.go index bd06efd4..1daccae9 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/end_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/end_test.go @@ -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 +} diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/enrolment.go b/modules/mesh-enrolment/cmd/mesh-enrolment/enrolment.go index a6f76d5a..06d342b8 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/enrolment.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/enrolment.go @@ -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. and 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 .. 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) } diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/holder.go b/modules/mesh-enrolment/cmd/mesh-enrolment/holder.go index 73b96e06..a74dcafe 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/holder.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/holder.go @@ -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 != "" { diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/integration_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/integration_test.go index f2fefcf2..c5adee20 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/integration_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/integration_test.go @@ -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) } diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/issue.go b/modules/mesh-enrolment/cmd/mesh-enrolment/issue.go index 8f24f725..7f0a5481 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/issue.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/issue.go @@ -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 { diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/issue_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/issue_test.go index 42a41569..1d567c32 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/issue_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/issue_test.go @@ -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) } } diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/kit_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/kit_test.go index deea20f6..d82cb107 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/kit_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/kit_test.go @@ -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":[]}` diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/main.go b/modules/mesh-enrolment/cmd/mesh-enrolment/main.go index ed8ec3b9..e7a62901 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/main.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/main.go @@ -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 } diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/names.go b/modules/mesh-enrolment/cmd/mesh-enrolment/names.go new file mode 100644 index 00000000..d5f5171b --- /dev/null +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/names.go @@ -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" +) diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/redeem.go b/modules/mesh-enrolment/cmd/mesh-enrolment/redeem.go index 7ee0b091..94376b25 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/redeem.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/redeem.go @@ -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., 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 diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/redeem_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/redeem_test.go index 6edebd44..e4858f20 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/redeem_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/redeem_test.go @@ -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 diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/state.go b/modules/mesh-enrolment/cmd/mesh-enrolment/state.go new file mode 100644 index 00000000..70a3c83a --- /dev/null +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/state.go @@ -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.` 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.
`: 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.` 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.
` 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.
` 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} +} diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/tokens_test.go b/modules/mesh-enrolment/cmd/mesh-enrolment/tokens_test.go index 5141f2af..7300b786 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/tokens_test.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/tokens_test.go @@ -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) } diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/tools.go b/modules/mesh-enrolment/cmd/mesh-enrolment/tools.go index 68a57e95..3d54ec78 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/tools.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/tools.go @@ -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 diff --git a/modules/mesh-enrolment/cmd/mesh-enrolment/vault.go b/modules/mesh-enrolment/cmd/mesh-enrolment/vault.go index 9d2dbe65..b989ed14 100644 --- a/modules/mesh-enrolment/cmd/mesh-enrolment/vault.go +++ b/modules/mesh-enrolment/cmd/mesh-enrolment/vault.go @@ -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 } diff --git a/modules/mesh-enrolment/go.mod b/modules/mesh-enrolment/go.mod index 419c1d3d..11e046a7 100644 --- a/modules/mesh-enrolment/go.mod +++ b/modules/mesh-enrolment/go.mod @@ -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 diff --git a/modules/mesh-enrolment/module.json b/modules/mesh-enrolment/module.json index b399b363..b3e92e18 100644 --- a/modules/mesh-enrolment/module.json +++ b/modules/mesh-enrolment/module.json @@ -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",