Node-engine: logins, signers, enrolment tokens and redeems, and the grants part's sealed user list come from the vault and node-enroller clients and the grants state package; no file imports go/delivery (ADR 0634 §1, §4; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 15:47:59 +01:00
parent 4bfa94dfef
commit 13e2fe14aa
22 changed files with 248 additions and 219 deletions
+20 -20
View File
@@ -7,6 +7,7 @@ import (
"encoding/base64"
"encoding/json"
"encoding/pem"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
engcomp "github.com/novox/mesh-host/internal/compositions"
"os"
"path/filepath"
@@ -16,7 +17,6 @@ import (
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/identity"
)
@@ -52,38 +52,38 @@ func TestTheAppliedAuthorityIsKeptForTheRecord(t *testing.T) {
told := 0
trust.onChange(func() { told++ })
keys := recordKeys(state, trust.Trusts)
ctrlFP, vaultFP := delivery.SignerFingerprint(controllerPub), delivery.SignerFingerprint(vaultPub)
enrol, _ := delivery.SignContext(delivery.KindEnrolment)
ctrlFP, vaultFP := meshvaultclient.SignerFingerprint(controllerPub), meshvaultclient.SignerFingerprint(vaultPub)
enrol, _ := meshvaultclient.SignContext(meshvaultclient.KindEnrolment)
msg := []byte("a reply")
if got := keys().Trusts; len(got) != 0 {
t.Errorf("before the signers are read the record trusts %v", got)
}
if _, err := trust.Verify(delivery.KindEnrolment, msg, delivery.SignMessage(controller, enrol, msg)); err == nil {
if _, err := trust.Verify(meshvaultclient.KindEnrolment, msg, meshvaultclient.SignMessage(controller, enrol, msg)); err == nil {
t.Error("a controller-signed message is accepted")
}
if _, err := trust.Verify(delivery.KindEnrolment, msg, delivery.SignMessage(vault, enrol, msg)); err == nil {
if _, err := trust.Verify(meshvaultclient.KindEnrolment, msg, meshvaultclient.SignMessage(vault, enrol, msg)); err == nil {
t.Error("a vault-signed message is accepted before the vault's key is read")
}
vaultCA := testAuthority(t, vaultPub, vault)
keyValue, _ := json.Marshal(delivery.NewSignerKey(vaultPub, time.Now()))
fp, _ := delivery.AuthorityFingerprint(vaultCA)
authValue, _ := json.Marshal(delivery.SignerAuthority{Certificate: vaultCA, Fingerprint: fp, Made: time.Now()})
trust.heardSigner(delivery.SignersKeyKey, keyValue, false, func(string) {})
trust.heardSigner(delivery.SignersAuthorityKey, authValue, false, func(string) {})
if s, err := trust.Verify(delivery.KindEnrolment, msg, delivery.SignMessage(vault, enrol, msg)); err != nil || s.Name != delivery.SignerVault {
keyValue, _ := json.Marshal(meshvaultclient.NewSignerKey(vaultPub, time.Now()))
fp, _ := meshvaultclient.AuthorityFingerprint(vaultCA)
authValue, _ := json.Marshal(meshvault.SignersByAuthorityValue{Certificate: vaultCA, Fingerprint: fp, Made: time.Now()})
trust.heardSigner(meshvault.SignersByKeyKey(), keyValue, false, func(string) {})
trust.heardSigner(meshvault.SignersByAuthorityKey(), authValue, false, func(string) {})
if s, err := trust.Verify(meshvaultclient.KindEnrolment, msg, meshvaultclient.SignMessage(vault, enrol, msg)); err != nil || s.Name != meshvaultclient.SignerVault {
t.Errorf("a vault-signed message on a node that read signers: %+v, %v", s, err)
}
if _, err := trust.Verify(delivery.KindEnrolment, msg, delivery.SignMessage(controller, enrol, msg)); err == nil {
if _, err := trust.Verify(meshvaultclient.KindEnrolment, msg, meshvaultclient.SignMessage(controller, enrol, msg)); err == nil {
t.Error("a controller-signed message is accepted once the vault's key is read")
}
if got := keys().Trusts; strings.Contains(strings.Join(got, " "), ctrlFP) || !delivery.TrustsKey(got, delivery.SignerVault, vaultFP) ||
if got := keys().Trusts; strings.Contains(strings.Join(got, " "), ctrlFP) || !meshvaultclient.TrustsKey(got, meshvaultclient.SignerVault, vaultFP) ||
trust.VaultAuthority() != vaultCA || told != 2 {
t.Errorf("after the signers are read the record trusts %v (told %d)", got, told)
}
// A signers' key naming another fingerprint is not trusted.
bad, _ := json.Marshal(delivery.SignerKey{Key: base64.StdEncoding.EncodeToString(controllerPub), Fingerprint: vaultFP})
trust.heardSigner(delivery.SignersKeyKey, bad, false, func(string) {})
if !delivery.TrustsKey(keys().Trusts, delivery.SignerVault, vaultFP) {
bad, _ := json.Marshal(meshvault.SignersByKeyValue{Key: base64.StdEncoding.EncodeToString(controllerPub), Fingerprint: vaultFP})
trust.heardSigner(meshvault.SignersByKeyKey(), bad, false, func(string) {})
if !meshvaultclient.TrustsKey(keys().Trusts, meshvaultclient.SignerVault, vaultFP) {
t.Error("a signers' key that does not check replaced the vault's")
}
@@ -98,8 +98,8 @@ func TestTheAppliedAuthorityIsKeptForTheRecord(t *testing.T) {
}
servingPub, _ := base64.StdEncoding.DecodeString(serving.Public)
signedBy := func(ca string, key ed25519.PrivateKey) string {
parent, _ := delivery.ParseCertificate(ca)
der, err := x509.CreateCertificate(nil, delivery.NodeCertificateTemplate(overlay.InternalName("n1"), servingPub, time.Now().Add(-time.Hour)),
parent, _ := meshvaultclient.ParseCertificate(ca)
der, err := x509.CreateCertificate(nil, meshvaultclient.NodeCertificateTemplate(overlay.InternalName("n1"), servingPub, time.Now().Add(-time.Hour)),
parent, ed25519.PublicKey(servingPub), key)
if err != nil {
t.Fatal(err)
@@ -110,7 +110,7 @@ func TestTheAppliedAuthorityIsKeptForTheRecord(t *testing.T) {
answer := signedBy(vaultCA, vault)
ask := func(_ context.Context, verb string, args any) (json.RawMessage, error) {
asked = append(asked, verb, args)
return json.Marshal(meshvault.SignAnswer{Kind: delivery.KindNodeCertificate, Node: "n1", Certificate: answer})
return json.Marshal(meshvault.SignAnswer{Kind: meshvaultclient.KindNodeCertificate, Node: "n1", Certificate: answer})
}
written := 0
if err := certifyThisNode(context.Background(), state, "n1", trust, ask, func() { written++ }); err != nil {
@@ -160,7 +160,7 @@ func TestTheAppliedAuthorityIsKeptForTheRecord(t *testing.T) {
// testAuthority is a self-signed authority certificate for key, as the vault makes its own.
func testAuthority(t *testing.T, pub ed25519.PublicKey, key ed25519.PrivateKey) string {
t.Helper()
tpl := delivery.AuthorityTemplate(pub, time.Now().Add(-time.Hour))
tpl := meshvaultclient.AuthorityTemplate(pub, time.Now().Add(-time.Hour))
der, err := x509.CreateCertificate(nil, tpl, tpl, pub, key)
if err != nil {
t.Fatal(err)
+6 -7
View File
@@ -6,10 +6,9 @@ import (
"encoding/json"
"errors"
"fmt"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshnodeenrollerclient"
"os"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/identity"
"github.com/novox/mesh-host/internal/link"
)
@@ -21,7 +20,7 @@ import (
func enrolThroughSeat(ctx context.Context, opts options, token identity.Token, node string, mine identity.Identity,
sealing identity.SealingKey, serving identity.ServingKey, reported map[string]any, found *link.Tunnel, identityPath string) error {
req := delivery.Redeem{Node: node, PublicKey: base64.StdEncoding.EncodeToString(mine.Public), OverlayKey: mine.Overlay.Public,
req := meshnodeenrollerclient.Redeem{Node: node, PublicKey: base64.StdEncoding.EncodeToString(mine.Public), OverlayKey: mine.Overlay.Public,
SealingKey: sealing.Public, ServingKey: serving.Public, Profile: reported}
if found != nil {
var err error
@@ -29,18 +28,18 @@ func enrolThroughSeat(ctx context.Context, opts options, token identity.Token, n
return err
}
}
req.Proof = base64.StdEncoding.EncodeToString(mine.Sign(delivery.RedeemProof(node, req.PublicKey, req.OverlayKey, req.SealingKey, req.ServingKey)))
req.Proof = base64.StdEncoding.EncodeToString(mine.Sign(meshnodeenrollerclient.RedeemProof(node, req.PublicKey, req.OverlayKey, req.SealingKey, req.ServingKey)))
to := link.Approach{Address: token.Broker, Fingerprint: token.Fingerprint, Transport: link.OnNATS}
membership := link.Membership{Node: node, Broker: token.Broker, Fingerprint: token.Fingerprint, Transport: link.OnNATS}
j := link.Joining{Node: node, Signer: token.SignerKey(),
Redeem: func(ctx context.Context) (delivery.RedeemAnswer, error) {
Redeem: func(ctx context.Context) (meshnodeenrollerclient.RedeemAnswer, error) {
return link.Redeem(ctx, to, node, token.Secret, req, opts.timeout)
},
Open: func(a delivery.RedeemAnswer) (string, error) {
Open: func(a meshnodeenrollerclient.RedeemAnswer) (string, error) {
value, err := sealing.Open(a.Account)
return string(value), err
},
Keep: func(a delivery.RedeemAnswer, password string) error {
Keep: func(a meshnodeenrollerclient.RedeemAnswer, password string) error {
// The mesh's name for this node wins over what the machine called itself, as on the controller's path.
mine.Node = a.Node
mine.Membership = identity.Membership{Broker: token.Broker, Fingerprint: token.Fingerprint, Signer: token.Signer,
+7 -6
View File
@@ -4,12 +4,13 @@ import (
"context"
"encoding/json"
"fmt"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
"github.com/novox/mesh-host/internal/buckets"
"os"
"path/filepath"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/identity"
"github.com/novox/mesh-host/internal/link"
)
@@ -48,7 +49,7 @@ var loginsFollowAgain = 2 * time.Minute
// followLogins follows this node's key of the vault's node-logins on every link, as a watch (never a poll), for as long as ctx
// lasts, and hands each value to hear. A watch that cannot be made is said once and tried again.
func followLogins(ctx context.Context, session *link.Session, node string, hear func(delivery.NodeLogin), say link.Announce) {
func followLogins(ctx context.Context, session *link.Session, node string, hear func(meshvault.NodeLoginsByNodeValue), say link.Announce) {
said := ""
for ctx.Err() == nil {
linked, cancel, err := session.WhenLinked(ctx)
@@ -56,7 +57,7 @@ func followLogins(ctx context.Context, session *link.Session, node string, hear
return
}
watching, stop := context.WithCancel(linked)
updates, werr := session.KVWatch(watching, delivery.NodeLoginsBucket, node)
updates, werr := session.KVWatch(watching, buckets.NodeLogins, node)
if werr != nil {
stop()
if line := "this node's login is not followed yet (" + werr.Error() + "): it dials as it last did, and looks again in " + loginsFollowAgain.String(); line != said {
@@ -69,9 +70,9 @@ func followLogins(ctx context.Context, session *link.Session, node string, hear
if u.Caught || u.Deleted || len(u.Value) == 0 {
continue
}
nl, err := delivery.DecodeNodeLogin(u.Value)
nl, err := meshvaultclient.DecodeNodeLogin(u.Value)
if err != nil {
say("the key " + node + " of " + delivery.NodeLoginsRead + " is not read: " + err.Error())
say("the key " + node + " of " + buckets.NodeLoginsRead + " is not read: " + err.Error())
continue
}
hear(nl)
+2 -2
View File
@@ -16,6 +16,7 @@ import (
"errors"
"flag"
"fmt"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
"io"
"net"
"os"
@@ -30,7 +31,6 @@ import (
"time"
"git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/accounts"
"github.com/novox/mesh-host/internal/agent"
"github.com/novox/mesh-host/internal/apply"
@@ -1075,7 +1075,7 @@ func runLink(ctx context.Context, opts options) error {
}, say)
// And the login it dials as, which the vault's node-logins names for this node (novox/hq ADR 0583 §1).
rotator := newRotator(queue, opts.state, opts.timeout, say)
go followLogins(aside, queue, mine.Node, func(nl delivery.NodeLogin) {
go followLogins(aside, queue, mine.Node, func(nl meshvault.NodeLoginsByNodeValue) {
if line := membership.Logins.Heard(nl); line != "" {
say(line)
}
+20 -20
View File
@@ -9,6 +9,7 @@ import (
"fmt"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
@@ -21,7 +22,6 @@ import (
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/identity"
"github.com/novox/mesh-host/internal/link"
@@ -51,23 +51,23 @@ func (t *meshTrust) onChange(f func()) {
}
// Signers are the keys a mesh signature is verified against now: the vault's once read, and none before.
func (t *meshTrust) Signers() []delivery.Signer {
func (t *meshTrust) Signers() []meshvaultclient.Signer {
t.mu.Lock()
defer t.mu.Unlock()
var out []delivery.Signer
var out []meshvaultclient.Signer
if t.vault != nil {
out = append(out, delivery.Signer{Name: delivery.SignerVault, Key: t.vault})
out = append(out, meshvaultclient.Signer{Name: meshvaultclient.SignerVault, Key: t.vault})
}
return out
}
// Verify is the signer that signed msg as kind, or why none it trusts did.
func (t *meshTrust) Verify(kind string, msg, sig []byte) (delivery.Signer, error) {
context, err := delivery.SignContext(kind)
func (t *meshTrust) Verify(kind string, msg, sig []byte) (meshvaultclient.Signer, error) {
context, err := meshvaultclient.SignContext(kind)
if err != nil {
return delivery.Signer{}, err
return meshvaultclient.Signer{}, err
}
return delivery.VerifyBy(t.Signers(), context, msg, sig)
return meshvaultclient.VerifyBy(t.Signers(), context, msg, sig)
}
// VaultAuthority is the vault's authority certificate as read from `signers`, or "".
@@ -87,9 +87,9 @@ func (t *meshTrust) Trusts() []string {
}
authorities := map[string]string{}
if a := t.VaultAuthority(); a != "" {
authorities[delivery.SignerVault] = a
authorities[meshvaultclient.SignerVault] = a
}
return delivery.Trusts(t.Signers(), authorities)
return meshvaultclient.Trusts(t.Signers(), authorities)
}
// change applies f under the lock and tells who follows when Trusts differs after it.
@@ -108,25 +108,25 @@ func (t *meshTrust) change(f func()) {
// check; a deleted key is no longer trusted.
func (t *meshTrust) heardSigner(key string, value []byte, deleted bool, say link.Announce) {
switch key {
case delivery.SignersKeyKey:
case meshvault.SignersByKeyKey():
var pub ed25519.PublicKey
if !deleted && len(value) > 0 {
k, err := delivery.DecodeSignerKey(value)
k, err := meshvaultclient.DecodeSignerKey(value)
if err == nil {
pub, err = k.PublicKey()
pub, err = meshvaultclient.SignerPublicKey(k)
}
if err != nil {
say("the vault's signing key in " + delivery.SignersRead + " is not trusted: " + err.Error())
say("the vault's signing key in " + buckets.SignersRead + " is not trusted: " + err.Error())
return
}
}
t.change(func() { t.vault = pub })
case delivery.SignersAuthorityKey:
case meshvault.SignersByAuthorityKey():
pem := ""
if !deleted && len(value) > 0 {
a, err := delivery.DecodeSignerAuthority(value)
a, err := meshvaultclient.DecodeSignerAuthority(value)
if err != nil {
say("the vault's authority in " + delivery.SignersRead + " is not trusted: " + err.Error())
say("the vault's authority in " + buckets.SignersRead + " is not trusted: " + err.Error())
return
}
pem = a.Certificate
@@ -157,7 +157,7 @@ func followTrust(ctx context.Context, session *link.Session, trust *meshTrust, s
return
}
watching, stop := context.WithCancel(linked)
signers, serr := session.KVWatch(watching, delivery.SignersBucket, ">")
signers, serr := session.KVWatch(watching, buckets.Signers, ">")
if serr != nil {
stop()
sayOnce(fmt.Sprintf("what this node trusts a mesh signature from is not followed yet (the vault's signers: %v): "+
@@ -266,7 +266,7 @@ func certifyThisNode(ctx context.Context, state, node string, trust *meshTrust,
if err != nil || len(key) != ed25519.PublicKeySize {
return fmt.Errorf("this node's serving key is not an ed25519 key")
}
raw, err := ask(ctx, meshvault.VerbSign, map[string]any{"kind": delivery.KindNodeCertificate, "node": node})
raw, err := ask(ctx, meshvault.VerbSign, map[string]any{"kind": meshvaultclient.KindNodeCertificate, "node": node})
if err != nil {
return err
}
@@ -278,7 +278,7 @@ func certifyThisNode(ctx context.Context, state, node string, trust *meshTrust,
if trusted == "" {
return fmt.Errorf("this node has read no authority of the vault's to check the certificate against")
}
if err := delivery.CheckNodeCertificate(a.Certificate, []string{trusted}, overlay.InternalName(node),
if err := meshvaultclient.CheckNodeCertificate(a.Certificate, []string{trusted}, overlay.InternalName(node),
ed25519.PublicKey(key), time.Now()); err != nil {
return err
}
+2 -2
View File
@@ -5,12 +5,12 @@ import (
"encoding/json"
"errors"
"fmt"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
engcomp "github.com/novox/mesh-host/internal/compositions"
"os"
"path/filepath"
"git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/seal"
"github.com/novox/mesh-host/internal/apply"
"github.com/novox/mesh-host/internal/identity"
@@ -44,7 +44,7 @@ func userListFollower(opts options, session *link.Session, running func() []engc
},
// From take-over, the grants part's list (ADR 0547 clauses 7, 8): its one key watched, its object read by name and
// opened with this machine's sealing key, the key's public half said in the have so the grants part seals to it.
Grants: bucketWatch{session, delivery.GrantsUserListBucket},
Grants: bucketWatch{session, grantsstate.GrantsUserListBucket},
Fetch: func(ctx context.Context, name string) ([]byte, error) {
return session.GrantsObject(ctx, name, grantsListLimit)
},