mesh-enrolment: read the group's mesh-sdk contract instead of its own copies of the shapes
The seat, the enrolment state, redeem and its proof, the answer the vault signs, and the vault's two verbs' answers are now mesh-sdk's (catalogue and delivery), which the vault, the grants part and the engine read too; the proof is the contract's RedeemProof, the answer carries the login, account, certificate and the vault's signing key (read from its state signers), and the bucket's values are written through the contract's codecs. The expiry event names the issued event by message id as well as by the sequence, which a bundle cannot learn. The module builds against the group's mesh-sdk head through a go.work outside the tree; the lead re-pins go.mod before the push. Tests merged into the same functions; none added. (issue 774, 774/S54-S58) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -6,6 +6,7 @@ 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
|
||||
@@ -81,13 +82,13 @@ func (h *Holder) Heard(op, key string, value []byte) error {
|
||||
case stdio.OpExpire:
|
||||
e, known := h.seen[node]
|
||||
delete(h.seen, node)
|
||||
body := Event{Node: node, Enrolment: e}
|
||||
id := "enrolment-expired." + node
|
||||
if known {
|
||||
body.Issued = eventID(EventIssued, e)
|
||||
id = eventID(EventExpired, e)
|
||||
}
|
||||
body.Reason = "the token's expiry passed unredeemed"
|
||||
// The sequence of the enrolment-issued event on the events' stream is not given to a bundle that publishes (the runtime
|
||||
// answers no sequence), so issued-event is 0 and issued-id names the message id that carried the content.
|
||||
body := Expired{EnrolmentExpiry: delivery.EnrolmentExpiry{V: delivery.Version, Node: node}, IssuedID: eventID(EventIssued, e)}
|
||||
if err := h.say(EventExpired, body, id); err != nil {
|
||||
return fmt.Errorf("enrolment-expired of %s cannot be said (offered again): %w", node, err)
|
||||
}
|
||||
@@ -95,6 +96,13 @@ func (h *Holder) Heard(op, key string, value []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Expired is what enrolment-expired carries (delivery.EnrolmentExpiry) and the message id of the enrolment-issued event that
|
||||
// carried the key's content, which the bus's sequence number cannot be known for.
|
||||
type Expired struct {
|
||||
delivery.EnrolmentExpiry
|
||||
IssuedID string `json:"issued-id,omitempty"`
|
||||
}
|
||||
|
||||
// Ended is the durable consumer's act on one of the holder's own ending events (ADR 0587 §5): the vault is asked to drop the
|
||||
// node's token and its hash, and the event is acknowledged only when the vault answers that both are gone, so a drop that
|
||||
// fails is delivered again by the bus. It never runs before an end: the consumer hears only the end events.
|
||||
|
||||
@@ -42,7 +42,7 @@ func TestRevokeEndsAtAnyStageAndAnExpiryMarkerSaysExpired(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
last := r.said[len(r.said)-1]
|
||||
mustContain(t, string(last.Body), `"issued-event":"enrolment-issued.novox.`)
|
||||
mustContain(t, string(last.Body), `"issued-id":"enrolment-issued.novox.`)
|
||||
if last.Event != EventExpired {
|
||||
t.Errorf("said %s", last.Event)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"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
|
||||
@@ -15,75 +18,54 @@ import (
|
||||
|
||||
// Module and Seat are the module's name and the seat it holds.
|
||||
const (
|
||||
Module = "mesh-enrolment"
|
||||
Seat = "mesh-enrolment"
|
||||
Module = catalogue.EnrolmentSeat
|
||||
Seat = catalogue.EnrolmentSeat
|
||||
// State is the holder's bucket of open enrolments; its keys are node.<node> and address.<address> (ADR 0587 §2).
|
||||
State = "enrolments"
|
||||
State = delivery.EnrolmentsState
|
||||
)
|
||||
|
||||
// The seat's six verbs and five events (ADR 0587 §1).
|
||||
var Verbs = []string{"issue", "redeem", "confirm", "revoke", "tokens", "again"}
|
||||
var Verbs = []string{catalogue.EnrolmentVerbIssue, catalogue.EnrolmentVerbRedeem, catalogue.EnrolmentVerbConfirm, catalogue.EnrolmentVerbRevoke,
|
||||
catalogue.EnrolmentVerbTokens, catalogue.EnrolmentVerbAgain}
|
||||
|
||||
const (
|
||||
EventIssued = "enrolment-issued"
|
||||
EventEnrolled = "node-enrolled"
|
||||
EventRefused = "enrolment-refused"
|
||||
EventRevoked = "enrolment-revoked"
|
||||
EventExpired = "enrolment-expired"
|
||||
EventIssued = catalogue.EnrolmentIssuedEvent
|
||||
EventEnrolled = catalogue.NodeEnrolledEvent
|
||||
EventRefused = catalogue.EnrolmentRefusedEvent
|
||||
EventRevoked = catalogue.EnrolmentRevokedEvent
|
||||
EventExpired = catalogue.EnrolmentExpiredEvent
|
||||
)
|
||||
|
||||
// EndEvents are the events that end an enrolment; the holder's durable consumer hears each (ADR 0587 §5) and asks the vault
|
||||
// to drop the token. A seat's event is heard as <seat>.<event>.
|
||||
var EndEvents = []string{EventEnrolled, EventRefused, EventRevoked, EventExpired}
|
||||
|
||||
// Stage is how far an open enrolment has come (ADR 0587 §2).
|
||||
type Stage string
|
||||
|
||||
// The stages of an open enrolment (ADR 0587 §2), as the seat's contract names them (delivery.Enrolment).
|
||||
const (
|
||||
// StageAsked: the operator's Approve is asked, no token exists.
|
||||
StageAsked Stage = "asked"
|
||||
StageAsked = delivery.EnrolmentAsked
|
||||
// StageLive: the Approve is given, the token exists and expires on the bus with the key.
|
||||
StageLive Stage = "live"
|
||||
StageLive = delivery.EnrolmentLive
|
||||
// StageRedeeming: the token is spent; the key has no expiry and ends only by confirm or revoke.
|
||||
StageRedeeming Stage = "redeeming"
|
||||
StageRedeeming = delivery.EnrolmentRedeeming
|
||||
)
|
||||
|
||||
// Enrolment and Reservation are the seat's state contract (delivery.Enrolment, delivery.EnrolmentAddress).
|
||||
type (
|
||||
Enrolment = delivery.Enrolment
|
||||
Reservation = delivery.EnrolmentAddress
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultExpiry and MaxExpiry bound a token's life (ADR 0587 §4).
|
||||
DefaultExpiry = time.Hour
|
||||
MaxExpiry = 24 * time.Hour
|
||||
// EnrolUser is the bus user a joining machine dials as: enrol.<node> (ADR 0587 §4).
|
||||
enrolPrefix = "enrol."
|
||||
nodePrefix = "node."
|
||||
nodePrefix = "node."
|
||||
)
|
||||
|
||||
// Enrolment is the value of the key node.<node> (ADR 0587 §2): no secret, 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 Stage `json:"stage"`
|
||||
Issued time.Time `json:"issued"`
|
||||
Expires time.Time `json:"expires"`
|
||||
// KeysDigest is the digest of the public keys the first redeem presented (ADR 0587 §5), empty before it.
|
||||
KeysDigest string `json:"keys-digest,omitempty"`
|
||||
}
|
||||
|
||||
// Reservation is the value of the key address.<address>: the node's name alone (ADR 0587 §2).
|
||||
type Reservation struct {
|
||||
V int `json:"v"`
|
||||
Node string `json:"node"`
|
||||
}
|
||||
|
||||
// NodeKey and AddressKey are the two keys of an enrolment.
|
||||
func NodeKey(node string) string { return "node." + node }
|
||||
func AddressKey(address string) string { return "address." + address }
|
||||
func NodeKey(node string) string { return delivery.EnrolmentNodeKey(node) }
|
||||
func AddressKey(address string) string { return delivery.EnrolmentAddressKey(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 enrolPrefix + node }
|
||||
func EnrolUser(node string) string { return delivery.EnrolmentUser(node) }
|
||||
func NodeUser(node string) string { return nodePrefix + node }
|
||||
|
||||
var nodeName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}$`)
|
||||
@@ -96,20 +78,17 @@ func CheckNode(node string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeEnrolment(raw []byte) (Enrolment, error) {
|
||||
var e Enrolment
|
||||
if err := json.Unmarshal(raw, &e); err != nil {
|
||||
return Enrolment{}, fmt.Errorf("an enrolment key does not read: %w", err)
|
||||
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)
|
||||
case Reservation:
|
||||
return delivery.EncodeEnrolmentAddress(x.Node)
|
||||
}
|
||||
if e.Node == "" || e.Address == "" {
|
||||
return Enrolment{}, errors.New("an enrolment key names no node or no address")
|
||||
}
|
||||
switch e.Stage {
|
||||
case StageAsked, StageLive, StageRedeeming:
|
||||
default:
|
||||
return Enrolment{}, fmt.Errorf("the enrolment of %s is in no stage %q", e.Node, e.Stage)
|
||||
}
|
||||
return e, nil
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
// Row is a key as the store holds it.
|
||||
|
||||
@@ -18,20 +18,22 @@ import (
|
||||
// Vault is the vault's side of enrolment (ADR 0587 §3, §5): the token and the node's account, and the signatures. The
|
||||
// holder keeps nothing of what it answers.
|
||||
type Vault interface {
|
||||
// Token asks the vault to make the token and its hash for node, expiring at expires, and answers the public fields
|
||||
// the joining machine must trust. No token is in the answer.
|
||||
Token(node string, expires time.Time) (json.RawMessage, error)
|
||||
// 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.
|
||||
Token(node string, expires time.Time) (delivery.EnrolmentTokenAnswer, error)
|
||||
// Sealed asks the vault for node's token sealed to the operator key, as the vault sealed it.
|
||||
Sealed(node string) (json.RawMessage, error)
|
||||
Sealed(node string) (string, error)
|
||||
// Drop asks the vault to drop node's token and its hash; it answers only once both are gone.
|
||||
Drop(node string) error
|
||||
// Account asks for node's account sealed to the sealing key; the same node and keys again answer the same account.
|
||||
Account(node, sealingKey, keysDigest string) (json.RawMessage, error)
|
||||
Account(node, sealingKey, keysDigest string) (delivery.NodeAccountAnswer, error)
|
||||
// Retire asks the vault to retire the account it minted for node.
|
||||
Retire(node string) error
|
||||
// SignCertificate and SignReply sign through the vault's `sign`.
|
||||
SignCertificate(node, servingKey string) (json.RawMessage, error)
|
||||
SignReply(node string, reply []byte) (json.RawMessage, error)
|
||||
SignCertificate(node, servingKey string) (delivery.SignAnswer, error)
|
||||
SignReply(node string, reply []byte) (delivery.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)
|
||||
}
|
||||
|
||||
// Asker is the operator's channel as the holder asks it (asker.Client is one).
|
||||
@@ -99,12 +101,14 @@ type AddNode struct {
|
||||
Warrant Warrant `json:"warrant"`
|
||||
}
|
||||
|
||||
// NodeKeys are the public halves of a joining node's keys: identity, private network, sealing and serving.
|
||||
// NodeKeys are the public halves of a joining node's keys: identity, private network, sealing and serving, and the
|
||||
// tunnel it found, as delivery.Redeem presents them.
|
||||
type NodeKeys struct {
|
||||
Public []byte `json:"public_key"`
|
||||
Overlay string `json:"overlay_key,omitempty"`
|
||||
Sealing string `json:"sealing_key,omitempty"`
|
||||
Serving string `json:"serving_key,omitempty"`
|
||||
Public string `json:"public_key"`
|
||||
Overlay string `json:"overlay_key,omitempty"`
|
||||
Sealing string `json:"sealing_key,omitempty"`
|
||||
Serving string `json:"serving_key,omitempty"`
|
||||
Tunnel json.RawMessage `json:"tunnel,omitempty"`
|
||||
}
|
||||
|
||||
// Warrant is the issue's warrant as the proposal carries it: the ask the operator answered, the node and the address it
|
||||
@@ -115,13 +119,6 @@ type Warrant struct {
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
// Digest is the digest of the keys a node presented, the one hash an event carries (ADR 0587 §1).
|
||||
func (k NodeKeys) Digest() string { return delivery.DigestOf(k.bytes()) }
|
||||
|
||||
func (k NodeKeys) bytes() []byte {
|
||||
return []byte("novox-mesh-enrol-keys\x00" + string(k.Public) + "\x00" + k.Overlay + "\x00" + k.Sealing + "\x00" + k.Serving)
|
||||
}
|
||||
|
||||
// issuing says whether issue is on: only while the controller's lease is handed to a holder that is no controller (ADR 0587
|
||||
// §4), and why either way.
|
||||
func (h *Holder) issuing() (bool, string) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/asker"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"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
|
||||
@@ -59,10 +60,10 @@ func (h *Holder) Issue(caller, node, why string, lasts time.Duration) (IssueAnsw
|
||||
findings = append(findings, "no why is given: the operator is asked why this machine joins")
|
||||
}
|
||||
if lasts == 0 {
|
||||
lasts = DefaultExpiry
|
||||
lasts = delivery.DefaultTokenLife
|
||||
}
|
||||
if lasts < 0 || lasts > MaxExpiry {
|
||||
findings = append(findings, fmt.Sprintf("the expiry %s is outside zero to %s", lasts, MaxExpiry))
|
||||
if lasts < 0 || lasts > delivery.MostTokenLife {
|
||||
findings = append(findings, fmt.Sprintf("the expiry %s is outside zero to %s", lasts, delivery.MostTokenLife))
|
||||
}
|
||||
if on, reason := h.issuing(); !on {
|
||||
findings = append(findings, "issuing is off until the controller's lease is written to a holder that is no controller: "+reason+
|
||||
@@ -89,7 +90,7 @@ func (h *Holder) Issue(caller, node, why string, lasts time.Duration) (IssueAnsw
|
||||
return IssueAnswer{}, fmt.Errorf("no enrolment of %q is issued: %s", node, strings.Join(findings, "; "))
|
||||
}
|
||||
now := h.now()
|
||||
e := Enrolment{V: 1, Node: node, Why: strings.TrimSpace(why), Caller: caller, Stage: StageAsked, Issued: now, Expires: now.Add(lasts)}
|
||||
e := Enrolment{Node: node, Why: strings.TrimSpace(why), Caller: caller, Stage: StageAsked, Issued: now, Expires: now.Add(lasts)}
|
||||
e.Ask = "enrol-" + node + "-" + now.UTC().Format("20060102t150405")
|
||||
skip := map[string]bool{}
|
||||
for {
|
||||
@@ -101,7 +102,7 @@ func (h *Holder) Issue(caller, node, why string, lasts time.Duration) (IssueAnsw
|
||||
if err != nil {
|
||||
return IssueAnswer{}, fmt.Errorf("no enrolment of %q is issued: %w", node, err)
|
||||
}
|
||||
switch err := h.Store.Create(AddressKey(addr), Reservation{V: 1, Node: node}, 0); {
|
||||
switch err := h.Store.Create(AddressKey(addr), Reservation{Node: node}, 0); {
|
||||
case err == nil:
|
||||
e.Address = addr
|
||||
case errors.Is(err, ErrExists):
|
||||
@@ -242,7 +243,7 @@ func (h *Holder) Warranted(o asks.Option, w asks.Warrant) error {
|
||||
if _, err := h.Store.Put(NodeKey(e.Node), e, ttl); err != nil {
|
||||
return fmt.Errorf("the enrolment of %s cannot be moved to live: %w", e.Node, err)
|
||||
}
|
||||
if _, err := h.Store.Put(AddressKey(e.Address), Reservation{V: 1, Node: e.Node}, ttl); err != nil {
|
||||
if _, err := h.Store.Put(AddressKey(e.Address), Reservation{Node: e.Node}, ttl); err != nil {
|
||||
return fmt.Errorf("the address key %s cannot be given its expiry: %w", e.Address, err)
|
||||
}
|
||||
return h.sayEvent(EventIssued, e, "")
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// memStore is the holder's bucket in memory: revisions, and the expiry each key was written with.
|
||||
@@ -29,7 +30,10 @@ func newMem() *memStore {
|
||||
}
|
||||
|
||||
func (m *memStore) write(key string, v any, ttl time.Duration) uint64 {
|
||||
raw, _ := json.Marshal(v)
|
||||
raw, err := Encode(v)
|
||||
if err != nil {
|
||||
panic(err) // a test wrote a value the contract refuses
|
||||
}
|
||||
m.rev++
|
||||
m.rows[key] = Row{Value: raw, Revision: m.rev}
|
||||
m.ttl[key] = ttl
|
||||
@@ -123,23 +127,24 @@ func (v *fakeVault) do(what string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *fakeVault) Token(node string, expires time.Time) (json.RawMessage, error) {
|
||||
return json.RawMessage(`{"broker":"b","fingerprint":"f","signer":"s"}`), v.do("token " + node)
|
||||
func (v *fakeVault) Token(node string, expires time.Time) (delivery.EnrolmentTokenAnswer, error) {
|
||||
return delivery.EnrolmentTokenAnswer{Node: node, Broker: "b", Fingerprint: "f", Signer: "s"}, v.do("token " + node)
|
||||
}
|
||||
func (v *fakeVault) Sealed(node string) (json.RawMessage, error) {
|
||||
return json.RawMessage(`"sealed-for-` + node + `"`), v.do("sealed " + node)
|
||||
func (v *fakeVault) Sealed(node string) (string, error) {
|
||||
return "sealed-for-" + node, v.do("sealed " + node)
|
||||
}
|
||||
func (v *fakeVault) Drop(node string) error { return v.do("drop " + node) }
|
||||
func (v *fakeVault) Account(node, sealing, digest string) (json.RawMessage, error) {
|
||||
return json.RawMessage(`{"account":"` + node + `"}`), v.do("account " + node)
|
||||
func (v *fakeVault) Account(node, sealing, digest string) (delivery.NodeAccountAnswer, error) {
|
||||
return delivery.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, serving string) (json.RawMessage, error) {
|
||||
return json.RawMessage(`{"certificate":"c"}`), v.do("certificate " + node)
|
||||
func (v *fakeVault) SignCertificate(node, serving string) (delivery.SignAnswer, error) {
|
||||
return delivery.SignAnswer{Kind: delivery.KindNodeCertificate, Node: node, Certificate: "PEM-" + serving}, v.do("certificate " + node)
|
||||
}
|
||||
func (v *fakeVault) SignReply(node string, reply []byte) (json.RawMessage, error) {
|
||||
return json.RawMessage(`{"signature":"sig","signer":"fp"}`), v.do("sign " + node)
|
||||
func (v *fakeVault) SignReply(node string, reply []byte) (delivery.SignAnswer, error) {
|
||||
return delivery.SignAnswer{Kind: delivery.KindEnrolment, Node: node, Signature: "sig-" + delivery.DigestOf(reply)[7:15], Signer: "fp"}, v.do("sign " + node)
|
||||
}
|
||||
func (v *fakeVault) Signer() (string, error) { return "vault-signer", nil }
|
||||
|
||||
type fakeAsker struct {
|
||||
mu sync.Mutex
|
||||
|
||||
@@ -30,11 +30,14 @@ func (runtimeStore) Get(key string) (Row, bool, error) {
|
||||
}
|
||||
|
||||
func (runtimeStore) Create(key string, v any, ttl time.Duration) error {
|
||||
var err error
|
||||
raw, err := Encode(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ttl > 0 {
|
||||
_, err = stdio.State(State).CreateTTL(key, v, ttl)
|
||||
_, err = stdio.State(State).CreateTTL(key, raw, ttl)
|
||||
} else {
|
||||
_, err = stdio.State(State).Create(key, v)
|
||||
_, err = stdio.State(State).Create(key, raw)
|
||||
}
|
||||
if errors.Is(err, stdio.ErrChanged) {
|
||||
return ErrExists
|
||||
@@ -43,7 +46,11 @@ func (runtimeStore) Create(key string, v any, ttl time.Duration) error {
|
||||
}
|
||||
|
||||
func (runtimeStore) Update(key string, v any, revision uint64) (uint64, error) {
|
||||
rev, err := stdio.State(State).Update(key, v, revision)
|
||||
raw, err := Encode(v)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
rev, err := stdio.State(State).Update(key, raw, revision)
|
||||
if errors.Is(err, stdio.ErrChanged) {
|
||||
return 0, ErrChanged
|
||||
}
|
||||
@@ -51,10 +58,14 @@ func (runtimeStore) Update(key string, v any, revision uint64) (uint64, error) {
|
||||
}
|
||||
|
||||
func (runtimeStore) Put(key string, v any, ttl time.Duration) (uint64, error) {
|
||||
if ttl > 0 {
|
||||
return stdio.State(State).PutTTL(key, v, ttl)
|
||||
raw, err := Encode(v)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return stdio.State(State).Put(key, v)
|
||||
if ttl > 0 {
|
||||
return stdio.State(State).PutTTL(key, raw, ttl)
|
||||
}
|
||||
return stdio.State(State).Put(key, raw)
|
||||
}
|
||||
|
||||
func (runtimeStore) Delete(key string) error { return stdio.State(State).Delete(key) }
|
||||
@@ -82,7 +93,7 @@ func newHolder() *Holder {
|
||||
rec := handacts.Recorder{Ask: stdio.Ask}
|
||||
rd := runtimeReader{}
|
||||
return &Holder{
|
||||
Store: runtimeStore{}, Read: rd, Vault: vaultOver{call: call}, Asker: asker.New("asked"),
|
||||
Store: runtimeStore{}, Read: rd, Vault: vaultOver{call: call, read: rd}, Asker: asker.New("asked"),
|
||||
Say: func(event string, body any, id string) error {
|
||||
return stdio.SeatPublish(SeatEventSubject(event), body, id)
|
||||
},
|
||||
|
||||
@@ -3,51 +3,16 @@ package main
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// RedeemArgs are what a joining node-engine says at redeem (ADR 0587 §6): the public halves of the keys it made where it
|
||||
// runs, and a proof, signed with its identity key, that it holds the private half of the first.
|
||||
type RedeemArgs struct {
|
||||
Node string `json:"node"`
|
||||
PublicKey []byte `json:"public_key"`
|
||||
OverlayKey string `json:"overlay_key,omitempty"`
|
||||
SealingKey string `json:"sealing_key,omitempty"`
|
||||
ServingKey string `json:"serving_key,omitempty"`
|
||||
Profile map[string]any `json:"profile,omitempty"`
|
||||
Proof []byte `json:"proof"`
|
||||
}
|
||||
|
||||
// Keys are the public halves presented.
|
||||
func (a RedeemArgs) Keys() NodeKeys {
|
||||
return NodeKeys{Public: a.PublicKey, Overlay: a.OverlayKey, Sealing: a.SealingKey, Serving: a.ServingKey}
|
||||
}
|
||||
|
||||
// RedeemProof is the bytes a node signs with its identity key when it redeems: the node's name and every key it presents, so a
|
||||
// proof cannot be moved to another request or another node. The layout is mesh-host's EnrolProof (internal/link/enrol.go),
|
||||
// with the node's name in the place of the token's secret, which this seat never holds.
|
||||
func RedeemProof(node string, k NodeKeys) []byte {
|
||||
return []byte("novox-mesh-enrol\x00" + node + "\x00" + base64.StdEncoding.EncodeToString(k.Public) +
|
||||
"\x00" + k.Overlay + "\x00" + k.Sealing + "\x00" + k.Serving)
|
||||
}
|
||||
|
||||
// RedeemReply is what the vault signs: the node's account sealed to its sealing key, the signed certificate for its internal
|
||||
// name and the address the operator approved.
|
||||
type RedeemReply struct {
|
||||
Node string `json:"node"`
|
||||
Address string `json:"address"`
|
||||
Account json.RawMessage `json:"account"`
|
||||
Certificate json.RawMessage `json:"certificate"`
|
||||
}
|
||||
|
||||
// RedeemAnswer is what redeem answers: the reply's bytes and the vault's signature over them. The node-engine refuses an
|
||||
// answer not signed by the key its token carries.
|
||||
type RedeemAnswer struct {
|
||||
Reply []byte `json:"reply"`
|
||||
Signed json.RawMessage `json:"signed"`
|
||||
// keysOf are the public halves a redeem presented.
|
||||
func keysOf(a delivery.Redeem) NodeKeys {
|
||||
return NodeKeys{Public: a.PublicKey, Overlay: a.OverlayKey, Sealing: a.SealingKey, Serving: a.ServingKey, Tunnel: a.Tunnel}
|
||||
}
|
||||
|
||||
// Redeem is the joining node-engine's one call (ADR 0587 §5, §6). It answers only a caller the runtime proves to be
|
||||
@@ -56,51 +21,61 @@ type RedeemAnswer struct {
|
||||
// compare-and-set, with no expiry), asks the vault for the node's account and the certificate, writes the node into placement,
|
||||
// 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 RedeemArgs) (RedeemAnswer, error) {
|
||||
func (h *Holder) Redeem(caller string, a delivery.Redeem) (delivery.RedeemAnswer, error) {
|
||||
if err := CheckNode(a.Node); err != nil {
|
||||
return RedeemAnswer{}, err
|
||||
return delivery.RedeemAnswer{}, err
|
||||
}
|
||||
if caller != EnrolUser(a.Node) {
|
||||
return 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 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)
|
||||
}
|
||||
keys := a.Keys()
|
||||
if len(a.PublicKey) != ed25519.PublicKeySize || !ed25519.Verify(a.PublicKey, RedeemProof(a.Node, keys), a.Proof) {
|
||||
return RedeemAnswer{}, errors.New("the proof does not verify against the identity key presented: the enrolment is not spent")
|
||||
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")
|
||||
}
|
||||
digest := keys.Digest()
|
||||
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")
|
||||
}
|
||||
digest := a.KeysDigest()
|
||||
e, err := h.spend(a.Node, digest)
|
||||
if err != nil {
|
||||
return RedeemAnswer{}, err
|
||||
return delivery.RedeemAnswer{}, err
|
||||
}
|
||||
account, err := h.Vault.Account(e.Node, a.SealingKey, digest)
|
||||
if err != nil {
|
||||
return RedeemAnswer{}, fmt.Errorf("the vault did not give the account of %s (call redeem again with the same keys): %w", e.Node, err)
|
||||
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)
|
||||
}
|
||||
cert, err := h.Vault.SignCertificate(e.Node, a.ServingKey)
|
||||
if err != nil {
|
||||
return RedeemAnswer{}, fmt.Errorf("the vault did not sign the certificate of %s (call redeem again with the same keys): %w", e.Node, err)
|
||||
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)
|
||||
}
|
||||
if err := h.place(e, a, keys); err != nil {
|
||||
return RedeemAnswer{}, err
|
||||
if err := h.place(e, keysOf(a)); err != nil {
|
||||
return delivery.RedeemAnswer{}, err
|
||||
}
|
||||
if h.Loaded != nil {
|
||||
loaded, err := h.Loaded(NodeUser(e.Node))
|
||||
if err != nil {
|
||||
return 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 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)
|
||||
}
|
||||
if !loaded {
|
||||
return RedeemAnswer{}, fmt.Errorf("the bus node has not loaded %s yet (call redeem again with the same keys)", NodeUser(e.Node))
|
||||
return delivery.RedeemAnswer{}, fmt.Errorf("the bus node has not loaded %s yet (call redeem again with the same keys)", NodeUser(e.Node))
|
||||
}
|
||||
}
|
||||
reply, err := json.Marshal(RedeemReply{Node: e.Node, Address: e.Address, Account: account, Certificate: cert})
|
||||
signer, err := h.Vault.Signer()
|
||||
if err != nil {
|
||||
return RedeemAnswer{}, err
|
||||
return delivery.RedeemAnswer{}, fmt.Errorf("the vault's signing key cannot be named (call redeem again with the same keys): %w", err)
|
||||
}
|
||||
signed, err := h.Vault.SignReply(e.Node, reply)
|
||||
out := delivery.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 RedeemAnswer{}, fmt.Errorf("the vault did not sign the answer (call redeem again with the same keys): %w", err)
|
||||
return delivery.RedeemAnswer{}, fmt.Errorf("the vault did not sign the answer (call redeem again with the same keys): %w", err)
|
||||
}
|
||||
return RedeemAnswer{Reply: reply, Signed: signed}, nil
|
||||
out.Signature = signed.Signature
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// spend moves the key from live to redeeming with the presented keys' digest, by compare-and-set, before its expiry; at
|
||||
@@ -138,7 +113,7 @@ func (h *Holder) spend(node, digest string) (Enrolment, error) {
|
||||
return Enrolment{}, fmt.Errorf("the enrolment of %s cannot be spent: %w", node, err)
|
||||
}
|
||||
// The move to redeeming rewrites both keys without an expiry, so a redeem under way never loses its key (§5).
|
||||
if _, err := h.Store.Put(AddressKey(e.Address), Reservation{V: 1, Node: node}, 0); err != nil {
|
||||
if _, err := h.Store.Put(AddressKey(e.Address), Reservation{Node: node}, 0); err != nil {
|
||||
return Enrolment{}, fmt.Errorf("the address key %s cannot be kept (call redeem again with the same keys): %w", e.Address, err)
|
||||
}
|
||||
return e, nil
|
||||
@@ -147,7 +122,7 @@ func (h *Holder) spend(node, digest string) (Enrolment, error) {
|
||||
}
|
||||
|
||||
// place writes the node into placement with the issue's warrant, unless placement holds it at the approved address already.
|
||||
func (h *Holder) place(e Enrolment, a RedeemArgs, keys NodeKeys) error {
|
||||
func (h *Holder) place(e Enrolment, keys NodeKeys) error {
|
||||
placements, err := h.placements()
|
||||
if err != nil {
|
||||
return fmt.Errorf("placement cannot be read (call redeem again with the same keys): %w", err)
|
||||
|
||||
@@ -6,20 +6,16 @@ import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// engineEnrolProof is mesh-host's EnrolProof (internal/link/enrol.go:112-114), copied: the bytes a node-engine signs. The
|
||||
// holder's RedeemProof must be the same bytes with the node's name in the token's place.
|
||||
func engineEnrolProof(secret string, public []byte, overlay, sealing, serving string) []byte {
|
||||
return []byte("novox-mesh-enrol\x00" + secret + "\x00" + base64.StdEncoding.EncodeToString(public) +
|
||||
"\x00" + overlay + "\x00" + sealing + "\x00" + serving)
|
||||
}
|
||||
|
||||
// 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) RedeemArgs {
|
||||
func redeemArgs(node string, seed byte, serving string) delivery.Redeem {
|
||||
priv := ed25519.NewKeyFromSeed(bytes.Repeat([]byte{seed}, ed25519.SeedSize))
|
||||
a := RedeemArgs{Node: node, PublicKey: priv.Public().(ed25519.PublicKey), OverlayKey: "overlay-" + node, SealingKey: "sealing-" + node, ServingKey: serving}
|
||||
a.Proof = ed25519.Sign(priv, RedeemProof(node, a.Keys()))
|
||||
a := delivery.Redeem{Node: node, PublicKey: base64.StdEncoding.EncodeToString(priv.Public().(ed25519.PublicKey)),
|
||||
OverlayKey: "overlay-" + node, SealingKey: "sealing-" + node, ServingKey: serving}
|
||||
a.Proof = base64.StdEncoding.EncodeToString(ed25519.Sign(priv, delivery.RedeemProof(node, a.PublicKey, a.OverlayKey, a.SealingKey, a.ServingKey)))
|
||||
return a
|
||||
}
|
||||
|
||||
@@ -29,15 +25,13 @@ func redeemArgs(node string, seed byte, serving string) RedeemArgs {
|
||||
func TestRedeemRefusesWhatItMustAndSpendsTheTokenOnce(t *testing.T) {
|
||||
good := redeemArgs("ace", 1, "serving-a")
|
||||
bad := good
|
||||
bad.Proof = append([]byte{}, good.Proof...)
|
||||
bad.Proof[0] ^= 1
|
||||
if !bytes.Equal(RedeemProof("ace", good.Keys()), engineEnrolProof("ace", good.PublicKey, good.OverlayKey, good.SealingKey, good.ServingKey)) {
|
||||
t.Fatal("the proof's bytes are not the engine's")
|
||||
}
|
||||
raw, _ := good.ProofBytes()
|
||||
raw[0] ^= 1
|
||||
bad.Proof = base64.StdEncoding.EncodeToString(raw)
|
||||
cases := []struct {
|
||||
name string
|
||||
caller string
|
||||
args RedeemArgs
|
||||
args delivery.Redeem
|
||||
setup func(r *rig)
|
||||
want string
|
||||
}{
|
||||
@@ -46,7 +40,7 @@ func TestRedeemRefusesWhatItMustAndSpendsTheTokenOnce(t *testing.T) {
|
||||
{"after the expiry", "enrol.ace", good, func(r *rig) { r.now = r.now.Add(2 * time.Hour) }, "expired at"},
|
||||
{"other keys than the first", "enrol.ace", redeemArgs("ace", 2, "serving-b"), func(r *rig) {
|
||||
e, _ := r.enrolment("ace")
|
||||
e.Stage, e.KeysDigest = StageRedeeming, good.Keys().Digest()
|
||||
e.Stage, e.KeysDigest = StageRedeeming, good.KeysDigest()
|
||||
_, _ = r.st.Put(NodeKey("ace"), e, 0)
|
||||
}, "spent once"},
|
||||
{"before the operator's yes", "enrol.ace", good, func(r *rig) {
|
||||
@@ -80,10 +74,10 @@ func TestRedeemRefusesWhatItMustAndSpendsTheTokenOnce(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
e, _ := r.enrolment("ace")
|
||||
if e.Stage != StageRedeeming || e.KeysDigest != good.Keys().Digest() || r.st.ttl[NodeKey("ace")] != 0 || r.st.ttl[AddressKey("10.9.0.2")] != 0 {
|
||||
if e.Stage != StageRedeeming || e.KeysDigest != good.KeysDigest() || r.st.ttl[NodeKey("ace")] != 0 || r.st.ttl[AddressKey("10.9.0.2")] != 0 {
|
||||
t.Errorf("spent %+v, ttl %v/%v", e, r.st.ttl[NodeKey("ace")], r.st.ttl[AddressKey("10.9.0.2")])
|
||||
}
|
||||
if len(r.added) != 1 || r.added[0].Warrant != (Warrant{Ask: "ask-ace", Node: "ace", Address: "10.9.0.2"}) || len(ans.Reply) == 0 || len(ans.Signed) == 0 {
|
||||
if len(r.added) != 1 || r.added[0].Warrant != (Warrant{Ask: "ask-ace", Node: "ace", Address: "10.9.0.2"}) || ans.Signature == "" || ans.Account != "account-ace" || ans.Login != "node.ace" || ans.Signer != "vault-signer" || ans.Certificate != "PEM-serving-a" {
|
||||
t.Errorf("placement %+v, answer %+v", r.added, ans)
|
||||
}
|
||||
r.loaded = false
|
||||
@@ -110,7 +104,7 @@ func TestARedeemStoppedMidwayIsFinishedByTheSameKeysAgain(t *testing.T) {
|
||||
t.Fatalf("%s: no error", stop)
|
||||
}
|
||||
got, err := r.h.Redeem("enrol.ace", good)
|
||||
if err != nil || !bytes.Equal(got.Reply, want.Reply) || string(got.Signed) != string(want.Signed) {
|
||||
if err != nil || got != want {
|
||||
t.Errorf("%s: %v, answer %+v, want %+v", stop, err, got, want)
|
||||
}
|
||||
if stop == "sign ace" && len(r.added) != 1 {
|
||||
@@ -121,7 +115,7 @@ func TestARedeemStoppedMidwayIsFinishedByTheSameKeysAgain(t *testing.T) {
|
||||
r := newRig(t)
|
||||
r.seed("ace", "10.9.0.2", StageRedeeming)
|
||||
e, _ := r.enrolment("ace")
|
||||
e.KeysDigest = good.Keys().Digest()
|
||||
e.KeysDigest = good.KeysDigest()
|
||||
_, _ = r.st.Put(NodeKey("ace"), e, 0)
|
||||
r.rd["mesh-delivery-assigner.placement"]["ace.node"] = `{"v":1,"node":"ace","decided":{"address":"10.9.0.2"},"modules":[]}`
|
||||
if _, err := r.h.Redeem("enrol.ace", good); err != nil || len(r.added) != 0 {
|
||||
|
||||
@@ -10,18 +10,18 @@ import (
|
||||
// TokenRow is one open enrolment as `tokens` answers it (ADR 0587 §8): its stage, expiry, ask and revision, and for a node
|
||||
// at stage live the token sealed to the operator key as the vault sealed it (the holder never reads it and keeps it nowhere).
|
||||
type TokenRow struct {
|
||||
Node string `json:"node"`
|
||||
Address string `json:"address"`
|
||||
Why string `json:"why"`
|
||||
Caller string `json:"caller"`
|
||||
Stage Stage `json:"stage"`
|
||||
Ask string `json:"ask,omitempty"`
|
||||
Issued time.Time `json:"issued"`
|
||||
Expires time.Time `json:"expires"`
|
||||
Age string `json:"age"`
|
||||
Revision uint64 `json:"revision"`
|
||||
Keys string `json:"keys-digest,omitempty"`
|
||||
Sealed json.RawMessage `json:"sealed,omitempty"`
|
||||
Node string `json:"node"`
|
||||
Address string `json:"address"`
|
||||
Why string `json:"why"`
|
||||
Caller string `json:"caller"`
|
||||
Stage string `json:"stage"`
|
||||
Ask string `json:"ask,omitempty"`
|
||||
Issued time.Time `json:"issued"`
|
||||
Expires time.Time `json:"expires"`
|
||||
Age string `json:"age"`
|
||||
Revision uint64 `json:"revision"`
|
||||
Keys string `json:"keys-digest,omitempty"`
|
||||
Sealed string `json:"sealed,omitempty"`
|
||||
// Unsealed says why no sealed token is given for a node that should have one.
|
||||
Unsealed string `json:"unsealed,omitempty"`
|
||||
}
|
||||
|
||||
@@ -7,17 +7,19 @@ import (
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// seed writes an open enrolment of node at stage, with its address key, as the holder would have.
|
||||
func (r *rig) seed(node, address string, stage Stage) {
|
||||
e := Enrolment{V: 1, Node: node, Address: address, Why: "a new machine", Caller: "person.operator", Ask: "ask-" + node, Stage: stage,
|
||||
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)}
|
||||
if stage == StageRedeeming {
|
||||
e.KeysDigest = "sha256:keys"
|
||||
e.KeysDigest = delivery.KeysDigest("pk", "ov", "se", "sv")
|
||||
}
|
||||
_, _ = r.st.Put(NodeKey(node), e, 0)
|
||||
_, _ = r.st.Put(AddressKey(address), Reservation{V: 1, Node: node}, 0)
|
||||
_, _ = r.st.Put(AddressKey(address), Reservation{Node: node}, 0)
|
||||
}
|
||||
|
||||
// 774/S54/AC1 and AC2: tokens answers each stage with its expiry, ask and revision, the fill and whether issuing is on;
|
||||
@@ -34,7 +36,7 @@ func TestTokensAnswersEachStageAndRelaysTheSealedTokenOfOneNode(t *testing.T) {
|
||||
var stages []string
|
||||
for _, row := range all.Open {
|
||||
stages = append(stages, row.Node+":"+string(row.Stage))
|
||||
if row.Revision == 0 || row.Expires.IsZero() || row.Ask == "" || row.Sealed != nil {
|
||||
if row.Revision == 0 || row.Expires.IsZero() || row.Ask == "" || row.Sealed != "" {
|
||||
t.Errorf("row %+v lacks revision, expiry or ask, or relays a token for a list of all", row)
|
||||
}
|
||||
}
|
||||
@@ -42,7 +44,7 @@ func TestTokensAnswersEachStageAndRelaysTheSealedTokenOfOneNode(t *testing.T) {
|
||||
t.Errorf("tokens %v, fill %+v, issuing %+v", stages, all.Fill, all.Issuing)
|
||||
}
|
||||
one, err := r.h.Tokens("g14", false)
|
||||
if err != nil || len(one.Open) != 1 || string(one.Open[0].Sealed) != `"sealed-for-g14"` {
|
||||
if err != nil || len(one.Open) != 1 || one.Open[0].Sealed != "sealed-for-g14" {
|
||||
t.Fatalf("one node: %+v, %v", one, err)
|
||||
}
|
||||
for k, row := range r.st.rows {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
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).
|
||||
@@ -38,17 +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; none is the default.
|
||||
func lastsOf(s string) (time.Duration, error) {
|
||||
if s == "" {
|
||||
return 0, nil
|
||||
}
|
||||
d, err := time.ParseDuration(s)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("the expiry %q is no duration (for example 1h): %w", s, err)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
// lastsOf reads an expiry as a duration (delivery.TokenLife); none is the default.
|
||||
func lastsOf(s string) (time.Duration, error) { return delivery.TokenLife(s) }
|
||||
|
||||
// Tools are what the runtime serves.
|
||||
func Tools(h *Holder) []stdio.Tool {
|
||||
@@ -79,7 +71,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 RedeemArgs
|
||||
var args delivery.Redeem
|
||||
raw, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -12,47 +13,83 @@ import (
|
||||
// Seats the holder calls (ADR 0587 §3, §5, §7). The vault's two verbs are the ones its manifest names this seat the only
|
||||
// caller of; its `sign` names this seat for a node's certificate and a redeem answer.
|
||||
const (
|
||||
vaultToken = "seat:mesh-vault.enrolment-token"
|
||||
vaultAccount = "seat:mesh-vault.node-account"
|
||||
vaultSign = "seat:mesh-vault.sign"
|
||||
vaultToken = "seat:mesh-vault." + delivery.EnrolmentTokenVerb
|
||||
vaultAccount = "seat:mesh-vault." + delivery.NodeAccountVerb
|
||||
vaultSign = "seat:mesh-vault." + delivery.SignVerb
|
||||
placementAdd = "seat:mesh-placement.propose"
|
||||
)
|
||||
|
||||
// Call asks a seat verb by its address (stdio.Ask through the runtime, or a test's).
|
||||
type Call func(key string, body any) (json.RawMessage, error)
|
||||
|
||||
// vaultOver is the vault as the holder reaches it: the seat's verbs over a Call.
|
||||
type vaultOver struct{ call Call }
|
||||
|
||||
func (v vaultOver) Token(node string, expires time.Time) (json.RawMessage, error) {
|
||||
return v.call(vaultToken, map[string]any{"node": node, "expires": expires.UTC().Format(time.RFC3339)})
|
||||
// vaultOver is the vault as the holder reaches it: the seat's verbs over a Call, and its public signing key from its state.
|
||||
type vaultOver struct {
|
||||
call Call
|
||||
read Reader
|
||||
}
|
||||
|
||||
func (v vaultOver) Sealed(node string) (json.RawMessage, error) {
|
||||
return v.call(vaultToken, map[string]any{"node": node, "sealed": true})
|
||||
func answer[T any](raw json.RawMessage, err error) (T, error) {
|
||||
var out T
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
if uerr := json.Unmarshal(raw, &out); uerr != nil {
|
||||
return out, fmt.Errorf("the vault's answer does not read: %w", uerr)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (v vaultOver) Token(node string, expires time.Time) (delivery.EnrolmentTokenAnswer, error) {
|
||||
return answer[delivery.EnrolmentTokenAnswer](v.call(vaultToken, map[string]any{"node": node, "expires": expires.UTC().Format(time.RFC3339)}))
|
||||
}
|
||||
|
||||
func (v vaultOver) Sealed(node string) (string, error) {
|
||||
a, err := answer[delivery.EnrolmentTokenAnswer](v.call(vaultToken, map[string]any{"node": node, "sealed": "true"}))
|
||||
if err == nil && a.Sealed == "" {
|
||||
err = errors.New("the vault gave no sealed token")
|
||||
}
|
||||
return a.Sealed, err
|
||||
}
|
||||
|
||||
func (v vaultOver) Drop(node string) error {
|
||||
_, err := v.call(vaultToken, map[string]any{"node": node, "drop": true})
|
||||
a, err := answer[delivery.EnrolmentTokenAnswer](v.call(vaultToken, map[string]any{"node": node, "drop": "true"}))
|
||||
if err == nil && !a.Dropped {
|
||||
err = errors.New("the vault did not say the token and its hash are gone")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (v vaultOver) Account(node, sealingKey, keysDigest string) (json.RawMessage, error) {
|
||||
return v.call(vaultAccount, map[string]any{"node": node, "sealing_key": sealingKey, "keys_digest": keysDigest})
|
||||
func (v vaultOver) Account(node, sealingKey, keysDigest string) (delivery.NodeAccountAnswer, error) {
|
||||
return answer[delivery.NodeAccountAnswer](v.call(vaultAccount, map[string]any{"node": node, "sealing_key": sealingKey, "keys_digest": keysDigest}))
|
||||
}
|
||||
|
||||
func (v vaultOver) Retire(node string) error {
|
||||
_, err := v.call(vaultAccount, map[string]any{"node": node, "retire": true})
|
||||
a, err := answer[delivery.NodeAccountAnswer](v.call(vaultAccount, map[string]any{"node": node, "retire": "true"}))
|
||||
if err == nil && !a.Retired {
|
||||
err = errors.New("the vault did not say the account is retired")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (v vaultOver) SignCertificate(node, servingKey string) (json.RawMessage, error) {
|
||||
return v.call(vaultSign, map[string]any{"kind": delivery.KindNodeCertificate, "node": node, "serving_key": servingKey})
|
||||
func (v vaultOver) SignCertificate(node, servingKey string) (delivery.SignAnswer, error) {
|
||||
return answer[delivery.SignAnswer](v.call(vaultSign, map[string]any{"kind": delivery.KindNodeCertificate, "node": node, "serving_key": servingKey}))
|
||||
}
|
||||
|
||||
func (v vaultOver) SignReply(node string, reply []byte) (json.RawMessage, error) {
|
||||
return v.call(vaultSign, map[string]any{"kind": delivery.KindEnrolment, "node": node,
|
||||
"reply": base64.StdEncoding.EncodeToString(reply)})
|
||||
func (v vaultOver) SignReply(node string, reply []byte) (delivery.SignAnswer, error) {
|
||||
return answer[delivery.SignAnswer](v.call(vaultSign, map[string]any{"kind": delivery.KindEnrolment, "node": node,
|
||||
"reply": base64.StdEncoding.EncodeToString(reply)}))
|
||||
}
|
||||
|
||||
func (v vaultOver) Signer() (string, error) {
|
||||
raw, found, err := v.read.Get(delivery.SignersRead, delivery.SignersKeyKey)
|
||||
if err != nil || !found {
|
||||
return "", fmt.Errorf("the vault's signing key is not readable in %s: %v", delivery.SignersRead, err)
|
||||
}
|
||||
k, err := delivery.DecodeSignerKey(raw)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return k.Key, nil
|
||||
}
|
||||
|
||||
// addNodeOver asks mesh-placement to add the node with the issue's warrant (ADR 0587 §7).
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"mesh-controller.lease",
|
||||
"mesh-delivery-assigner.placement",
|
||||
"mesh-grants.user-list",
|
||||
"mesh-vault.signers",
|
||||
"node-engine.have"
|
||||
],
|
||||
"state": [
|
||||
|
||||
Reference in New Issue
Block a user