Nodes compose: the catalogue's placement files and their check, mesh-config reading placement, the vault's own and pair credentials, the forge's commit trees (issue 1126)
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 851e8782d569 on da29f77b1081 | novox/mesh-catalog#505 in group feat/1126-nodes-compose
mesh/delivery closed: a newer head of the same pull request
mesh/check checking 851e8782d569 on da29f77b1081 | novox/mesh-catalog#505 in group feat/1126-nodes-compose
One commit for the branch feat/1126-nodes-compose of mesh-catalog: its 15 commits squashed so its history does not carry two compiled programs committed by mistake and removed later, mesh-config (5.5 MB) and checks/placement (6.2 MB) (issue 1577). The tree equals the former head 2db56067. The operator allowed this rewrite of the feature branch on 2026-10-26.
This commit is contained in:
@@ -5,6 +5,8 @@ package main
|
||||
// key (or a first one) raises `secret-changed` for the machine's core secrets, the same key says nothing.
|
||||
|
||||
import (
|
||||
"git.novox.be/novox/mesh-sdk/go/seal"
|
||||
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -100,14 +102,17 @@ authorization {
|
||||
users = [
|
||||
{ user: "novox.mesh-vault", password: "pw", permissions: {
|
||||
publish: { deny: [">"] }
|
||||
subscribe: { allow: ["mesh.seat.mesh-vault.call.node-key.*"] }
|
||||
subscribe: { allow: ["mesh.seat.mesh-vault.call.node-key.*", "mesh.seat.mesh-vault.call.own.*"] }
|
||||
allow_responses: true } }
|
||||
{ user: "node.ace", password: "pw", permissions: {
|
||||
publish: { allow: ["mesh.seat.mesh-vault.call.node-key.node_ace"] }
|
||||
publish: { allow: ["mesh.seat.mesh-vault.call.node-key.node_ace", "mesh.seat.mesh-vault.call.own.node_ace"] }
|
||||
subscribe: { allow: ["_INBOX.node.ace.>"] } } }
|
||||
{ user: "node.g14", password: "pw", permissions: {
|
||||
publish: { allow: ["mesh.seat.mesh-vault.call.node-key.node_g14"] }
|
||||
publish: { allow: ["mesh.seat.mesh-vault.call.node-key.node_g14", "mesh.seat.mesh-vault.call.own.node_g14"] }
|
||||
subscribe: { allow: ["_INBOX.node.g14.>"] } } }
|
||||
{ user: "controller", password: "pw", permissions: {
|
||||
publish: { allow: ["mesh.seat.mesh-vault.call.own.controller"] }
|
||||
subscribe: { allow: ["_INBOX.controller.>"] } } }
|
||||
]
|
||||
}
|
||||
`
|
||||
@@ -135,39 +140,47 @@ func TestOnARealBusANodeEngineStatesOnlyItsOwnKey(t *testing.T) {
|
||||
t.Cleanup(s.Shutdown)
|
||||
|
||||
v := newVault(t, nil)
|
||||
tool := toolNamed(t, definitions(&Env{Vault: v.Vault}), Seat+".node-key")
|
||||
tools := map[string]toolDef{"node-key": toolNamed(t, definitions(&Env{Vault: v.Vault}), Seat+".node-key"),
|
||||
"own": toolNamed(t, definitions(&Env{Vault: v.Vault}), Seat+".own")}
|
||||
vaultConn := connectAs(t, s.ClientURL(), "novox.mesh-vault")
|
||||
if _, err := vaultConn.Subscribe("mesh.seat.mesh-vault.call.node-key.*", func(m *nats.Msg) {
|
||||
serve := func(m *nats.Msg) {
|
||||
parts := strings.Split(m.Subject, ".")
|
||||
var args map[string]any
|
||||
_ = json.Unmarshal(m.Data, &args)
|
||||
answer := busAnswer{}
|
||||
if got, err := tool.runAs(untoken(parts[len(parts)-1]), args); err != nil {
|
||||
if got, err := tools[parts[len(parts)-2]].runAs(untoken(parts[len(parts)-1]), args); err != nil {
|
||||
answer.Error = err.Error()
|
||||
} else {
|
||||
answer.Result, _ = json.Marshal(got)
|
||||
}
|
||||
raw, _ := json.Marshal(answer)
|
||||
_ = m.Respond(raw)
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, verb := range []string{"node-key", "own"} {
|
||||
if _, err := vaultConn.Subscribe("mesh.seat.mesh-vault.call."+verb+".*", serve); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
_ = vaultConn.Flush()
|
||||
|
||||
say := func(user, subject, key string) (string, error) {
|
||||
raw, _ := json.Marshal(map[string]string{"key": key})
|
||||
call := func(user, subject string, args any) (json.RawMessage, error) {
|
||||
raw, _ := json.Marshal(args)
|
||||
msg, err := connectAs(t, s.ClientURL(), user).Request(subject, raw, 3*time.Second)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
var a busAnswer
|
||||
_ = json.Unmarshal(msg.Data, &a)
|
||||
if a.Error != "" {
|
||||
return a.Error, errString(a.Error)
|
||||
return nil, errString(a.Error)
|
||||
}
|
||||
return "", nil
|
||||
return a.Result, nil
|
||||
}
|
||||
aceKey, _ := aSealingKey(t)
|
||||
say := func(user, subject, key string) (string, error) {
|
||||
_, err := call(user, subject, map[string]string{"key": key})
|
||||
return "", err
|
||||
}
|
||||
aceKey, acePriv := aSealingKey(t)
|
||||
g14Key, _ := aSealingKey(t)
|
||||
if _, err := say("node.ace", "mesh.seat.mesh-vault.call.node-key.node_ace", aceKey); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -188,4 +201,32 @@ func TestOnARealBusANodeEngineStatesOnlyItsOwnKey(t *testing.T) {
|
||||
if got, held, _ := v.Store.NodeKey("g14"); !held || got != g14Key {
|
||||
t.Errorf("g14's key is %q, %v", got, held)
|
||||
}
|
||||
|
||||
// Issue 1126 (ADR 0547 §2, ADR 0550 §5): `own` on the same bus. node.ace asks its own and gets its module's secret
|
||||
// sealed to the key it stated, opening with its key alone; g14 cannot ask in ace's name, and asking in its own gets
|
||||
// none of ace's; the controller is refused, as no node-engine.
|
||||
if _, err := moveIn(t, v, "node.ace", MoveOf{Node: "ace", Module: "web", Name: "password", Origin: "made", Generation: "g-1",
|
||||
SealingKey: aceKey}, "ace-web-pw"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, err := call("node.ace", "mesh.seat.mesh-vault.call.own.node_ace", map[string]any{})
|
||||
var own OwnAnswer
|
||||
if err != nil || json.Unmarshal(raw, &own) != nil || own.Node != "ace" || len(own.Own) != 1 {
|
||||
t.Fatalf("ace's own over the bus: %s, %v", raw, err)
|
||||
}
|
||||
if value, err := seal.Open(own.Own[0].Sealed, acePriv); err != nil || string(value) != "ace-web-pw" {
|
||||
t.Errorf("ace's secret does not open with ace's key: %q, %v", value, err)
|
||||
}
|
||||
if strings.Contains(string(raw), "ace-web-pw") {
|
||||
t.Error("ace's secret crossed the bus in the clear")
|
||||
}
|
||||
if _, err := call("node.g14", "mesh.seat.mesh-vault.call.own.node_ace", map[string]any{}); err == nil {
|
||||
t.Error("node.g14 asked ace's own in ace's name")
|
||||
}
|
||||
if raw, err := call("node.g14", "mesh.seat.mesh-vault.call.own.node_g14", map[string]any{}); err != nil || strings.Contains(string(raw), "web") {
|
||||
t.Errorf("g14 asking its own was answered %s, %v", raw, err)
|
||||
}
|
||||
if _, err := call("controller", "mesh.seat.mesh-vault.call.own.controller", map[string]any{}); err == nil || !strings.Contains(err.Error(), "not a node-engine") {
|
||||
t.Errorf("the controller asking own: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
package main
|
||||
|
||||
// A machine's node-engine composes its own modules (novox/hq ADR 0547 §2, issue 1126), and asks the vault for their own
|
||||
// secrets sealed to its own key (ADR 0349 §11, ADR 0550 §5): the verb `own`, answered to the node-engine of a machine
|
||||
// alone, about that machine alone (the machine is the caller's, as the bus proved it, never an argument). Each value is
|
||||
// sealed to the sealing key the machine's engine stated (`node-key`), so what crosses the bus opens on that machine only,
|
||||
// and the composition carries it sealed (no composition carries a secret in the clear). A module handed to the
|
||||
// provisioner is left out: its client fills its secrets from `get` (ADR 0349 §17).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/seal"
|
||||
)
|
||||
|
||||
// NodeOwnSecret is one own secret of a module on the asking machine, sealed to the machine's key.
|
||||
type NodeOwnSecret struct {
|
||||
Module string `json:"module"`
|
||||
Name string `json:"name"`
|
||||
Origin string `json:"origin"`
|
||||
Generation int `json:"generation"`
|
||||
Sealed seal.Box `json:"sealed"`
|
||||
}
|
||||
|
||||
// OwnAnswer is what `own` answers: the machine, the sealing key every value is sealed to (raw X25519, base64, as its
|
||||
// engine stated it), each own secret of its modules, and each pair credential it asked for that the vault holds.
|
||||
type OwnAnswer struct {
|
||||
Node string `json:"node"`
|
||||
Key string `json:"key"`
|
||||
Own []NodeOwnSecret `json:"own"`
|
||||
Credentials []NodeCredential `json:"credentials,omitempty"`
|
||||
Missing []CredentialAsked `json:"missing,omitempty"`
|
||||
}
|
||||
|
||||
// CredentialAsked names one pair credential by its ends, as a node composing its own asks for it (mesh-sdk
|
||||
// compose.CredentialKey): Role is `consumer` when the asking machine consumes it, `provider` when it grants it.
|
||||
type CredentialAsked struct {
|
||||
Role string `json:"role"`
|
||||
Provision string `json:"provision"`
|
||||
Consumer string `json:"consumer"`
|
||||
ConsumerModule string `json:"consumer_module"`
|
||||
Provider string `json:"provider"`
|
||||
Local string `json:"local,omitempty"`
|
||||
}
|
||||
|
||||
// NodeCredential is one pair credential answered, sealed to the asking machine's key, with the name the vault keeps it
|
||||
// under (`pair/...` per pair, `provision/<provider>/<provision>/<node>/<module>` issued for one holder, or the provision's
|
||||
// one shared value `provision/<provider>/<provision>`).
|
||||
type NodeCredential struct {
|
||||
CredentialAsked
|
||||
Name string `json:"name"`
|
||||
Generation int `json:"generation"`
|
||||
Sealed seal.Box `json:"sealed"`
|
||||
}
|
||||
|
||||
// credentialNames are the names a pair credential may be kept under, the most particular first.
|
||||
func credentialNames(c CredentialAsked) []string {
|
||||
pair := "pair/" + c.Provider + "/" + c.Provision + "/" + c.Consumer + "/" + c.ConsumerModule
|
||||
if c.Local != "" {
|
||||
pair += "/" + c.Local
|
||||
}
|
||||
return []string{pair, "provision/" + c.Provider + "/" + c.Provision + "/" + c.Consumer + "/" + c.ConsumerModule,
|
||||
"provision/" + c.Provider + "/" + c.Provision}
|
||||
}
|
||||
|
||||
// credentialRefusal says why node may not have c, or "": a consumer's credential to the machine its consuming module
|
||||
// stands on, a provider's to the providing machine, and nothing to a machine that is neither.
|
||||
func (v *Vault) credentialRefusal(node string, c CredentialAsked) string {
|
||||
for _, part := range []string{c.Provision, c.Consumer, c.ConsumerModule, c.Provider} {
|
||||
if !namePart.MatchString(part) {
|
||||
return fmt.Sprintf("%q is not a name the mesh writes", part)
|
||||
}
|
||||
}
|
||||
if c.Local != "" && !namePart.MatchString(c.Local) {
|
||||
return fmt.Sprintf("%q is not a name the mesh writes", c.Local)
|
||||
}
|
||||
switch c.Role {
|
||||
case "consumer":
|
||||
if c.Consumer != node {
|
||||
return fmt.Sprintf("%s asks as a consumer on %s, and is not that machine", node, c.Consumer)
|
||||
}
|
||||
if d, ok := v.Askers.(Declared); ok && d.Read != nil {
|
||||
if raw, err := standing(d.Read)(node + "." + c.ConsumerModule); err == nil && raw == nil {
|
||||
return fmt.Sprintf("%s is not on %s: neither assigned there nor required by a module there", c.ConsumerModule, node)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
case "provider":
|
||||
if c.Provider != node {
|
||||
return fmt.Sprintf("%s asks as the provider %s, and is not that machine", node, c.Provider)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("the role %q is not consumer or provider", c.Role)
|
||||
}
|
||||
|
||||
// composeOrigin is the vault's origin of a value as the composition reads it: made by the mesh, or given (accepted at a
|
||||
// terminal, typed at a desk, issued by a provider's software).
|
||||
func composeOrigin(origin string) string {
|
||||
if origin == "" || origin == OriginMade {
|
||||
return OriginMade
|
||||
}
|
||||
return OriginAccepted
|
||||
}
|
||||
|
||||
// OwnOfNode answers the node-engine of a machine the own secrets of its modules, each sealed to the machine's stated key.
|
||||
func (v *Vault) OwnOfNode(caller string, asked ...CredentialAsked) (OwnAnswer, error) {
|
||||
node, ok := nodeEngineOf(caller)
|
||||
if !ok {
|
||||
if caller == "" || caller == stdio.Unproven {
|
||||
caller = "an unproven caller"
|
||||
}
|
||||
return OwnAnswer{}, fmt.Errorf("own answers a machine's node-engine alone, about its own machine; %s is not a node-engine", caller)
|
||||
}
|
||||
key, had, err := v.Store.NodeKey(node)
|
||||
if err != nil {
|
||||
return OwnAnswer{}, err
|
||||
}
|
||||
if !had {
|
||||
return OwnAnswer{}, fmt.Errorf("the vault holds no secret named own/%s yet: %s's node-engine has stated no sealing key "+
|
||||
"(node-key), so there is no key of its own to seal to", node, node)
|
||||
}
|
||||
to, err := publicPEM(key)
|
||||
if err != nil {
|
||||
return OwnAnswer{}, fmt.Errorf("%s's sealing key as recorded is not usable: %w", node, err)
|
||||
}
|
||||
all, err := v.Store.List()
|
||||
if err != nil {
|
||||
return OwnAnswer{}, err
|
||||
}
|
||||
out := OwnAnswer{Node: node, Key: key}
|
||||
for _, r := range all {
|
||||
parts := strings.Split(r.Name, "/")
|
||||
if len(parts) != 4 || parts[0] != "own" || parts[1] != node {
|
||||
continue
|
||||
}
|
||||
if v.handed(node, parts[2]) {
|
||||
continue
|
||||
}
|
||||
value, err := seal.Open(r.AtRest, v.Own.PrivateKey)
|
||||
if err != nil {
|
||||
return OwnAnswer{}, fmt.Errorf("%s does not open with the vault's key: %w", r.Name, err)
|
||||
}
|
||||
sealed, err := seal.Seal(value, to)
|
||||
if err != nil {
|
||||
return OwnAnswer{}, err
|
||||
}
|
||||
if err := v.recordHanding(Handing{To: caller, Name: r.Name, Generation: r.Generation, Outcome: "handed"}); err != nil {
|
||||
return OwnAnswer{}, fmt.Errorf("%s is not handed out: who was handed it could not be recorded: %w", r.Name, err)
|
||||
}
|
||||
out.Own = append(out.Own, NodeOwnSecret{Module: parts[2], Name: parts[3], Origin: composeOrigin(r.Origin),
|
||||
Generation: r.Generation, Sealed: sealed})
|
||||
}
|
||||
sort.Slice(out.Own, func(i, j int) bool {
|
||||
if out.Own[i].Module != out.Own[j].Module {
|
||||
return out.Own[i].Module < out.Own[j].Module
|
||||
}
|
||||
return out.Own[i].Name < out.Own[j].Name
|
||||
})
|
||||
for _, c := range asked {
|
||||
if why := v.credentialRefusal(node, c); why != "" {
|
||||
return OwnAnswer{}, fmt.Errorf("%s may not ask for the credential of %s from %s to %s on %s: %s", caller,
|
||||
c.Provision, c.Provider, c.ConsumerModule, c.Consumer, why)
|
||||
}
|
||||
found := false
|
||||
for _, name := range credentialNames(c) {
|
||||
r, held, err := v.Store.Read(name)
|
||||
if err != nil {
|
||||
return OwnAnswer{}, err
|
||||
}
|
||||
if !held {
|
||||
continue
|
||||
}
|
||||
value, err := seal.Open(r.AtRest, v.Own.PrivateKey)
|
||||
if err != nil {
|
||||
return OwnAnswer{}, fmt.Errorf("%s does not open with the vault's key: %w", name, err)
|
||||
}
|
||||
sealed, err := seal.Seal(value, to)
|
||||
if err != nil {
|
||||
return OwnAnswer{}, err
|
||||
}
|
||||
if err := v.recordHanding(Handing{To: caller, Name: name, Generation: r.Generation, Outcome: "handed"}); err != nil {
|
||||
return OwnAnswer{}, fmt.Errorf("%s is not handed out: who was handed it could not be recorded: %w", name, err)
|
||||
}
|
||||
out.Credentials = append(out.Credentials, NodeCredential{CredentialAsked: c, Name: name, Generation: r.Generation, Sealed: sealed})
|
||||
found = true
|
||||
break
|
||||
}
|
||||
if !found {
|
||||
out.Missing = append(out.Missing, c)
|
||||
}
|
||||
}
|
||||
v.log("handed %s's node-engine the %d own secrets of its modules and %d credentials (%d not held), sealed to its key",
|
||||
node, len(out.Own), len(out.Credentials), len(out.Missing))
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// handed says the plan of module on node hands it to the provisioner, so its secrets are filled from `get`, not composed.
|
||||
// Where the plans cannot be read, nothing counts as handed: the composer leaves out a handed module's secrets itself.
|
||||
func (v *Vault) handed(node, module string) bool {
|
||||
d, ok := v.Askers.(Declared)
|
||||
if !ok || d.Read == nil {
|
||||
return false
|
||||
}
|
||||
raw, err := standing(d.Read)(node + "." + module)
|
||||
if err != nil || raw == nil {
|
||||
return false
|
||||
}
|
||||
var a plannedSecrets
|
||||
return json.Unmarshal(raw, &a) == nil && a.Provisioning != nil
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/seal"
|
||||
)
|
||||
|
||||
// Issue 1126 (ADR 0547 §2, ADR 0550 §5): `own` answers a machine's node-engine the own secrets of its modules, sealed to
|
||||
// the key that machine's engine stated, about that machine alone; another caller, another machine's secrets and a
|
||||
// machine with no stated key get nothing.
|
||||
func TestOwnAnswersAMachineItsModulesSecretsSealedToItsKey(t *testing.T) {
|
||||
v := newVault(t, nil)
|
||||
anchorPub, anchorPriv := aSealingKey(t)
|
||||
laptopPub, _ := aSealingKey(t)
|
||||
for _, m := range []struct {
|
||||
caller string
|
||||
of MoveOf
|
||||
value string
|
||||
}{
|
||||
{"node.anchor", MoveOf{Node: "anchor", Module: "web", Name: "password", Origin: "made", Generation: "g-1", SealingKey: anchorPub}, "anchor-web-pw"},
|
||||
{"node.anchor", MoveOf{Node: "anchor", Module: "web", Name: "api-key", Origin: "accepted", Generation: "g-1", SealingKey: anchorPub}, "anchor-web-key"},
|
||||
{"node.laptop", MoveOf{Node: "laptop", Module: "web", Name: "password", Origin: "made", Generation: "g-1", SealingKey: laptopPub}, "laptop-web-pw"},
|
||||
} {
|
||||
if _, err := moveIn(t, v, m.caller, m.of, m.value); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
got, err := v.OwnOfNode("node.anchor")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Node != "anchor" || got.Key != anchorPub || len(got.Own) != 2 {
|
||||
t.Fatalf("anchor was answered %+v", got)
|
||||
}
|
||||
want := map[string][2]string{"api-key": {"accepted", "anchor-web-key"}, "password": {"made", "anchor-web-pw"}}
|
||||
for _, s := range got.Own {
|
||||
w, ok := want[s.Name]
|
||||
if !ok || s.Module != "web" || s.Origin != w[0] || s.Generation < 1 {
|
||||
t.Errorf("anchor's %s: %+v", s.Name, s)
|
||||
continue
|
||||
}
|
||||
value, err := seal.Open(s.Sealed, anchorPriv)
|
||||
if err != nil || string(value) != w[1] {
|
||||
t.Errorf("anchor's %s does not open to its value with anchor's key: %q, %v", s.Name, value, err)
|
||||
}
|
||||
}
|
||||
for caller, says := range map[string]string{
|
||||
"controller": "is not a node-engine",
|
||||
"anchor.web": "is not a node-engine",
|
||||
"": "an unproven caller",
|
||||
"node.unknown": "no secret named own/unknown yet",
|
||||
} {
|
||||
if _, err := v.OwnOfNode(caller); err == nil || !strings.Contains(err.Error(), says) {
|
||||
t.Errorf("%q: %v, want a refusal saying %q", caller, err, says)
|
||||
}
|
||||
}
|
||||
if laptop, err := v.OwnOfNode("node.laptop"); err != nil || len(laptop.Own) != 1 || laptop.Own[0].Name != "password" {
|
||||
t.Errorf("laptop was answered %+v, %v; want its one secret alone", laptop, err)
|
||||
}
|
||||
|
||||
// A pair credential: laptop's web consumes anchor's postgres. Each end is answered the one value sealed to its own key;
|
||||
// a credential the vault holds none of is named missing; a machine at neither end is refused.
|
||||
if _, err := moveIn(t, v, "node.laptop", MoveOf{Provider: "anchor", Provision: "postgres", Consumer: "laptop",
|
||||
ConsumerModule: "web", Origin: "made", Generation: "g-1", SealingKey: laptopPub}, "pg-pair"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pair := CredentialAsked{Provision: "postgres", Consumer: "laptop", ConsumerModule: "web", Provider: "anchor"}
|
||||
asConsumer, asProvider := pair, pair
|
||||
asConsumer.Role, asProvider.Role = "consumer", "provider"
|
||||
none := CredentialAsked{Role: "consumer", Provision: "redis", Consumer: "laptop", ConsumerModule: "web", Provider: "anchor"}
|
||||
laptop, err := v.OwnOfNode("node.laptop", asConsumer, none)
|
||||
if err != nil || len(laptop.Credentials) != 1 || len(laptop.Missing) != 1 || laptop.Missing[0] != none {
|
||||
t.Fatalf("laptop as consumer: %+v, %v", laptop, err)
|
||||
}
|
||||
if !strings.HasPrefix(laptop.Credentials[0].Name, "pair/anchor/postgres/laptop/web") {
|
||||
t.Errorf("laptop's credential is kept as %s", laptop.Credentials[0].Name)
|
||||
}
|
||||
anchor, err := v.OwnOfNode("node.anchor", asProvider)
|
||||
if err != nil || len(anchor.Credentials) != 1 {
|
||||
t.Fatalf("anchor as provider: %+v, %v", anchor, err)
|
||||
}
|
||||
if value, err := seal.Open(anchor.Credentials[0].Sealed, anchorPriv); err != nil || string(value) != "pg-pair" {
|
||||
t.Errorf("anchor's copy does not open to the value with its key: %q, %v", value, err)
|
||||
}
|
||||
for who, c := range map[string]CredentialAsked{"node.anchor": asConsumer, "node.laptop": asProvider} {
|
||||
if _, err := v.OwnOfNode(who, c); err == nil || !strings.Contains(err.Error(), "is not that machine") {
|
||||
t.Errorf("%s asking %+v: %v", who, c, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -251,6 +251,34 @@ func definitions(env *Env) []toolDef {
|
||||
// clause 9, adopt.go): a public half, never the private one.
|
||||
return map[string]any{"recorded": true, "vault_key": v.Own.PublicKey}, nil
|
||||
}},
|
||||
{Name: Seat + ".own",
|
||||
Description: "The own secrets of this machine's modules, each sealed to the machine's sealing key (novox/hq ADR 0547 §2, " +
|
||||
"ADR 0550 §5, issue 1126): from the node-engine of the machine alone, about its own machine (the bus proves which), " +
|
||||
"for the compositions it makes itself. Answers the machine, the key each value is sealed to, and per module and " +
|
||||
"name the origin, the generation and the sealed value; a module handed to the provisioner is left out. With credentials, " +
|
||||
"also each pair credential the machine consumes or grants, sealed the same way. Never a value in the clear.",
|
||||
Input: map[string]any{"type": "object", "properties": map[string]any{
|
||||
"credentials": map[string]any{"type": "array", "description": "the pair credentials the machine's composition " +
|
||||
"reads, each {role: consumer|provider, provision, consumer, consumer_module, provider, local}: answered " +
|
||||
"sealed to the machine's key when the machine is that consumer's or that provider, and named missing when " +
|
||||
"the vault holds none", "items": map[string]any{"type": "object"}}}},
|
||||
run: func(caller string, a map[string]any) (any, error) {
|
||||
v, err := env.ready()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var asked []CredentialAsked
|
||||
if raw, ok := a["credentials"]; ok && raw != nil {
|
||||
b, err := json.Marshal(raw)
|
||||
if err == nil {
|
||||
err = json.Unmarshal(b, &asked)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("credentials is a list of {role, provision, consumer, consumer_module, provider, local}: %w", err)
|
||||
}
|
||||
}
|
||||
return v.OwnOfNode(caller, asked...)
|
||||
}},
|
||||
{Name: Seat + ".move-asks",
|
||||
Description: "Copy rows of the controller's record of asks at a desk into the vault's, as they are and once " +
|
||||
"(design 57's V3): from the controller alone, at most 500 at a time. No value is in any of them.",
|
||||
|
||||
@@ -177,7 +177,8 @@
|
||||
"rotate",
|
||||
"move-in",
|
||||
"node-key",
|
||||
"move-asks"
|
||||
"move-asks",
|
||||
"own"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user