mesh-vault signs: its signing key and authority made once with their operator copies, the verb sign for a node's own certificate from bus state, the event signed, the state signers and the tools that read and reassert them (issue 774, ADR 0550 §2)

This commit is contained in:
2026-10-28 21:54:36 +01:00
parent bd5f170af0
commit c00005c141
7 changed files with 745 additions and 9 deletions
+7 -1
View File
@@ -54,7 +54,7 @@ func build() *Env {
env.Vault = &Vault{Store: store, Own: own, Askers: Declared{Read: assigned.Read, Confirm: readAssignment, Store: store},
Assignments: Mirror{Keys: assignmentKeys, Read: readAssignment}, Emit: emit, Ask: stdio.Ask,
Now: time.Now, Log: say, Accounts: busAccounts{}, Haves: readHave, LeaseRead: readLease, WantRead: readWant,
HaveKeys: func() ([]string, error) { return stdio.State(HaveRead).Keys() }, Placement: placement}
HaveKeys: func() ([]string, error) { return stdio.State(HaveRead).Keys() }, Placement: placement, Signers: busSigners{}}
return env
}
@@ -261,6 +261,12 @@ func main() {
}
env.pass()
if env.Vault != nil {
// The signing key and authority once the operator key is taken, and `signers` when it differs (ADR 0550 §2).
if err := env.Vault.SigningPass(); err != nil {
env.Vault.signingSaid(err)
} else {
env.Vault.signingSaid(nil)
}
env.Vault.HashOwed()
env.Vault.MintOwed()
env.Vault.RetirePass()
@@ -0,0 +1,463 @@
package main
// The vault signs (novox/hq ADR 0550 §2; issue 774, design 47's L3 item d). It makes an ed25519 signing key and an x509
// authority once, keeps each as its own secret with its operator copy (ADR 0113 §22), publishes their public halves in its
// state `signers`, and signs with them on its verb `sign`: a node's certificate for that node's node-engine alone, built
// from bus state (the name from the node's placement, the serving key from its record), never from bytes the caller chose.
// Each signature is said as `signed` and kept in the record of hand-outs, never a key. The controller's key is never
// copied here.
import (
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/overlay"
)
// The vault's own signing secrets: each an ed25519 seed in base64 (never PEM, which the vault takes as a private key
// someone sent it and refuses).
const (
SigningKeySecret = "signing/key"
AuthorityKeySecret = "signing/authority-key"
// signedOutcome marks a signature in the record of hand-outs.
signedOutcome = "signed"
)
// signingSecret says name is one of the vault's signing secrets, which no asker is ever handed.
func signingSecret(name string) bool { return name == SigningKeySecret || name == AuthorityKeySecret }
// SignersStore is the vault's state `signers` (delivery.SignersBucket): a key's value, or nil; and a write.
type SignersStore interface {
Get(key string) ([]byte, error)
Put(key string, value []byte) error
}
// busSigners is SignersStore over the vault's own state through its runtime.
type busSigners struct{}
func (busSigners) Get(key string) ([]byte, error) {
e, err := stdio.State(delivery.SignersState).Get(key)
if err != nil || e == nil {
return nil, err
}
return e.Value, nil
}
func (busSigners) Put(key string, value []byte) error {
_, err := stdio.State(delivery.SignersState).Put(key, json.RawMessage(value))
return err
}
// signingKeys are the vault's two keys, opened, and the authority's certificate.
type signingKeys struct {
key ed25519.PrivateKey
keyMade time.Time
authority ed25519.PrivateKey
authorityMade time.Time
certificate string
parsed *x509.Certificate
}
// EnsureSigning makes the signing key and the authority once, when the store holds neither, and answers whether it made
// them. **Never made again over what is held** (as OwnKey refuses a new key beside secrets): one held without the other,
// or none held while `signers` already publishes a key, is a lost key, refused loudly with nothing made, because every
// node trusts what was published and a new key would be one nobody trusts.
func (v *Vault) EnsureSigning() (bool, error) {
_, keyHeld, err := v.Store.Read(SigningKeySecret)
if err != nil {
return false, err
}
_, authHeld, err := v.Store.Read(AuthorityKeySecret)
if err != nil {
return false, err
}
switch {
case keyHeld && authHeld:
return false, nil
case keyHeld != authHeld:
missing := AuthorityKeySecret
if authHeld {
missing = SigningKeySecret
}
return false, fmt.Errorf("THE VAULT'S %s IS MISSING BESIDE THE OTHER SIGNING SECRET: nothing is made, since a new one "+
"would be trusted by no node; put it back from its operator copy (novox/hq ADR 0113 §21, ADR 0550 §2)", missing)
}
if v.Signers != nil {
if raw, err := v.Signers.Get(delivery.SignersKeyKey); err != nil {
return false, fmt.Errorf("the vault's state %s could not be read, so no signing key is made: %w", delivery.SignersRead, err)
} else if raw != nil {
return false, fmt.Errorf("THE VAULT'S SIGNING KEYS ARE MISSING WHILE %s PUBLISHES ONE: nothing is made, since every "+
"node trusts the published key; put them back from their operator copies (novox/hq ADR 0113 §21)", delivery.SignersRead)
}
}
for _, name := range []string{SigningKeySecret, AuthorityKeySecret} {
_, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return false, err
}
if _, err := v.keepWith(name, base64.StdEncoding.EncodeToString(priv.Seed()), "the vault", "", OriginMade,
keepOptions{neverReplace: true, quiet: true}); err != nil {
return false, fmt.Errorf("the vault's %s is not made: %w", name, err)
}
}
v.log("made the vault's signing key and authority, each kept with its operator copy (ADR 0550 §2)")
return true, nil
}
// openSeed is a signing secret opened: its private key and when it was made.
func (v *Vault) openSeed(name string) (ed25519.PrivateKey, time.Time, error) {
r, held, err := v.Store.Read(name)
if err != nil {
return nil, time.Time{}, err
}
if !held {
return nil, time.Time{}, fmt.Errorf("the vault holds no %s yet: it is made once the operator key is taken", name)
}
value, err := openAtRest(r, v)
if err != nil {
return nil, time.Time{}, fmt.Errorf("%s does not open with the vault's key: %w", name, err)
}
seed, err := base64.StdEncoding.DecodeString(value)
if err != nil || len(seed) != ed25519.SeedSize {
return nil, time.Time{}, fmt.Errorf("%s is not an ed25519 seed", name)
}
return ed25519.NewKeyFromSeed(seed), r.MadeAt, nil
}
// keys opens both signing secrets and derives the authority's certificate from its key and when it was made, so it is the
// same certificate at every start.
func (v *Vault) keys() (signingKeys, error) {
key, keyMade, err := v.openSeed(SigningKeySecret)
if err != nil {
return signingKeys{}, err
}
auth, authMade, err := v.openSeed(AuthorityKeySecret)
if err != nil {
return signingKeys{}, err
}
pub := auth.Public().(ed25519.PublicKey)
tmpl := delivery.AuthorityTemplate(pub, authMade)
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, pub, auth)
if err != nil {
return signingKeys{}, fmt.Errorf("the vault's authority certificate could not be made: %w", err)
}
parsed, err := x509.ParseCertificate(der)
if err != nil {
return signingKeys{}, err
}
return signingKeys{key: key, keyMade: keyMade, authority: auth, authorityMade: authMade, parsed: parsed,
certificate: string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}))}, nil
}
// SignersValues are the two keys of `signers` as the vault holds them now.
func (v *Vault) SignersValues() (delivery.SignerKey, delivery.SignerAuthority, error) {
k, err := v.keys()
if err != nil {
return delivery.SignerKey{}, delivery.SignerAuthority{}, err
}
fp, err := delivery.AuthorityFingerprint(k.certificate)
if err != nil {
return delivery.SignerKey{}, delivery.SignerAuthority{}, err
}
return delivery.NewSignerKey(k.key.Public().(ed25519.PublicKey), k.keyMade),
delivery.SignerAuthority{Certificate: k.certificate, Fingerprint: fp, Made: k.authorityMade.UTC()}, nil
}
// PublishSigners writes `signers` from the keys the vault holds, each key only when its value differs (or always with
// force, signers_reassert), and answers which keys it wrote.
func (v *Vault) PublishSigners(force bool) ([]string, error) {
if v.Signers == nil {
return nil, errors.New("the vault has no state " + delivery.SignersRead + " to write")
}
key, auth, err := v.SignersValues()
if err != nil {
return nil, err
}
var written []string
for name, value := range map[string]any{delivery.SignersKeyKey: key, delivery.SignersAuthorityKey: auth} {
raw, err := json.Marshal(value)
if err != nil {
return written, err
}
if !force {
held, err := v.Signers.Get(name)
if err != nil {
return written, fmt.Errorf("%s %s could not be read: %w", delivery.SignersRead, name, err)
}
if same(held, raw) {
continue
}
}
if err := v.Signers.Put(name, raw); err != nil {
return written, fmt.Errorf("%s %s could not be written: %w", delivery.SignersRead, name, err)
}
written = append(written, name)
}
if len(written) > 0 {
v.log("wrote %s %s (ADR 0550 §2)", delivery.SignersRead, strings.Join(written, " and "))
}
return written, nil
}
// same says two JSON values are the same value.
func same(a, b []byte) bool {
if a == nil || b == nil {
return false
}
var x, y any
if json.Unmarshal(a, &x) != nil || json.Unmarshal(b, &y) != nil {
return false
}
ra, _ := json.Marshal(x)
rb, _ := json.Marshal(y)
return string(ra) == string(rb)
}
// SigningPass makes the keys once and publishes `signers` when it differs: each minute pass, after the operator key.
func (v *Vault) SigningPass() error {
if _, err := v.EnsureSigning(); err != nil {
return err
}
if v.Signers == nil {
return nil
}
_, err := v.PublishSigners(false)
return err
}
// SignArgs are `sign`'s arguments: the kind, and the kind's fields.
type SignArgs struct {
Kind string
Node string
Reply string
}
// Sign answers caller's `sign`. The caller is the one the runtime proved; who may ask which kind is the vault's rule,
// never an argument: a node certificate for a machine's node-engine alone, about its own machine; an enrolment for no
// caller until adoption's record names one (ADR 0561 §5); the authority for the vault itself, never asked.
func (v *Vault) Sign(caller string, a SignArgs) (delivery.SignAnswer, error) {
if caller == "" || caller == stdio.Unproven || !busUser.MatchString(caller) {
return delivery.SignAnswer{}, errors.New("the vault signs only for a caller the runtime proved")
}
switch a.Kind {
case delivery.KindNodeCertificate:
return v.signNodeCertificate(caller, a.Node)
case delivery.KindEnrolment:
return delivery.SignAnswer{}, fmt.Errorf("%s may not ask for an enrolment's signature: no caller is named for it until "+
"adoption's record names the seat that adopts (novox/hq ADR 0561 §5)", caller)
case delivery.KindAuthority:
return delivery.SignAnswer{}, fmt.Errorf("the authority is the vault's own, signed for itself; %s may not ask it", caller)
}
return delivery.SignAnswer{}, fmt.Errorf("%q is not a kind the vault signs (%s)", a.Kind, strings.Join(delivery.SignKinds, ", "))
}
// signNodeCertificate signs the certificate of the calling machine's internal name for the serving key its record states.
func (v *Vault) signNodeCertificate(caller, asked string) (delivery.SignAnswer, error) {
node, ok := nodeEngineOf(caller)
if !ok {
return delivery.SignAnswer{}, fmt.Errorf("a node certificate is signed for a machine's node-engine alone, about its own "+
"machine; %s is not a node-engine", caller)
}
if asked != "" && asked != node {
return delivery.SignAnswer{}, fmt.Errorf("%s asks for the certificate of %s, and is %s's node-engine: a machine asks for its own alone", caller, asked, node)
}
name, err := v.internalName(node)
if err != nil {
return delivery.SignAnswer{}, err
}
serving, err := v.servingKey(node)
if err != nil {
return delivery.SignAnswer{}, err
}
k, err := v.keys()
if err != nil {
return delivery.SignAnswer{}, err
}
der, err := x509.CreateCertificate(rand.Reader, delivery.NodeCertificateTemplate(name, serving, k.authorityMade),
k.parsed, serving, k.authority)
if err != nil {
return delivery.SignAnswer{}, fmt.Errorf("the certificate of %s could not be signed: %w", name, err)
}
fp, _ := delivery.AuthorityFingerprint(k.certificate)
answer := delivery.SignAnswer{Kind: delivery.KindNodeCertificate, Node: node, Authority: k.certificate,
Certificate: string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})), Signer: fp, Digest: delivery.DigestOf(der)}
if err := v.saySigned(caller, AuthorityKeySecret, answer); err != nil {
return delivery.SignAnswer{}, err
}
return answer, nil
}
// internalName is node's name inside the mesh: only a node with an address on the private network in its placement has
// one (a certificate for a name nothing resolves is one nothing can check).
func (v *Vault) internalName(node string) (string, error) {
if v.Placement.Read == nil {
return "", errors.New("the vault reads no placement, so it cannot tell a node's internal name")
}
key, err := delivery.PlacementKey(node)
if err != nil {
return "", err
}
raw, err := v.Placement.Read(key)
if err != nil {
return "", fmt.Errorf(notYetPrefix+"the placement of %s could not be read: %w", node, err)
}
if raw == nil {
return "", fmt.Errorf("%s has no placement in the catalogue, so it has no name inside the mesh", node)
}
p, err := delivery.DecodePlacement(raw)
if err != nil {
return "", fmt.Errorf("the placement of %s is not readable: %w", node, err)
}
if p.Decided.Address == "" {
return "", fmt.Errorf("%s is not on the private network (its placement names no address), so it has no name inside the mesh to be certified for", node)
}
return overlay.InternalName(node), nil
}
// servingKey is the serving key node's record states, `<node>.node` of the have bucket, which that node alone writes.
func (v *Vault) servingKey(node string) (ed25519.PublicKey, error) {
if v.Haves == nil {
return nil, errors.New("the vault cannot read the have bucket (" + HaveRead + "), so it cannot read a node's record")
}
key, err := delivery.HaveNodeKey(node)
if err != nil {
return nil, err
}
raw, err := v.Haves(key)
if err != nil {
return nil, fmt.Errorf(notYetPrefix+"%s %s could not be read: %w", HaveRead, key, err)
}
if raw == nil {
return nil, fmt.Errorf("%s has written no record (%s %s), so it states no serving key", node, HaveRead, key)
}
h, err := delivery.DecodeHaveNode(raw)
if err != nil {
return nil, fmt.Errorf("the record of %s is not readable: %w", node, err)
}
if h.Record == nil || h.Record.ServingKey == "" {
return nil, fmt.Errorf("%s's record states no serving key, so there is no key to certify", node)
}
pub, err := base64.StdEncoding.DecodeString(h.Record.ServingKey)
if err != nil || len(pub) != ed25519.PublicKeySize {
return nil, fmt.Errorf("%s's serving key is not an ed25519 public key in base64", node)
}
return ed25519.PublicKey(pub), nil
}
// saySigned keeps the signature in the record of hand-outs and says `signed`: who, which kind, which node, the digest and
// the signer's fingerprint. Never a key. A signature whose record cannot be written is not answered.
func (v *Vault) saySigned(caller, with string, a delivery.SignAnswer) error {
at := v.Now().UTC()
if err := v.recordHanding(Handing{To: caller, Name: with, Outcome: signedOutcome,
Why: a.Kind + " " + a.Node + " " + a.Digest}); err != nil {
return fmt.Errorf("the signature is not answered: its record could not be written: %w", err)
}
if v.Emit == nil {
return nil
}
raw, _ := json.Marshal(delivery.Signed{Caller: caller, Kind: a.Kind, Node: a.Node, Digest: a.Digest, Signer: a.Signer, At: at})
var body map[string]any
_ = json.Unmarshal(raw, &body)
if err := v.Emit(delivery.SignedEvent, body); err != nil {
v.log("signed %s for %s, but %s could not be said: %v", a.Kind, caller, delivery.SignedEvent, err)
}
return nil
}
// SignedRow is one signature as vault_signed lists it.
type SignedRow struct {
At time.Time `json:"at"`
Caller string `json:"caller"`
With string `json:"with"`
Kind string `json:"kind"`
Node string `json:"node,omitempty"`
Digest string `json:"digest"`
}
// Signatures are the last signatures the vault made, newest first, at most limit.
func (v *Vault) Signatures(limit int) ([]SignedRow, error) {
all, err := v.Store.Handed(10000)
if err != nil {
return nil, err
}
out := []SignedRow{}
for _, h := range all {
if h.Outcome != signedOutcome || len(out) >= limit {
continue
}
f := strings.Fields(h.Why)
row := SignedRow{At: h.At, Caller: h.To, With: h.Name}
if len(f) == 3 {
row.Kind, row.Node, row.Digest = f[0], f[1], f[2]
}
out = append(out, row)
}
return out, nil
}
// SigningState is what vault_status and vault_signers say of the signing secrets: whether each is held, its operator
// copy, and the fingerprints held against those published.
func (v *Vault) SigningState() map[string]any {
out := map[string]any{}
for _, name := range []string{SigningKeySecret, AuthorityKeySecret} {
r, held, err := v.Store.Read(name)
row := map[string]any{"held": held}
if err != nil {
row["error"] = err.Error()
}
if held {
row["generation"], row["made"], row["operator_copy"] = r.Generation, r.MadeAt, r.Operator.Sealed != ""
}
out[name] = row
}
key, auth, err := v.SignersValues()
if err != nil {
out["error"] = err.Error()
return out
}
out["key_fingerprint"], out["authority_fingerprint"] = key.Fingerprint, auth.Fingerprint
if v.Signers == nil {
return out
}
published := map[string]any{}
match := true
for name, want := range map[string]string{delivery.SignersKeyKey: key.Fingerprint, delivery.SignersAuthorityKey: auth.Fingerprint} {
raw, err := v.Signers.Get(name)
var got struct {
Fingerprint string `json:"fingerprint"`
}
if err != nil || raw == nil || json.Unmarshal(raw, &got) != nil {
published[name], match = nil, false
continue
}
published[name] = got.Fingerprint
match = match && got.Fingerprint == want
}
out["published"], out["published_matches"] = published, match
return out
}
// signingSaid says why the signing pass did not finish, once, and again when it changes.
func (v *Vault) signingSaid(err error) {
line := ""
if err != nil {
line = err.Error()
}
v.mu.Lock()
changed := line != v.signingWhy
v.signingWhy = line
v.mu.Unlock()
if changed && line != "" {
v.log("the vault signs nothing yet: %s", line)
}
}
@@ -0,0 +1,196 @@
package main
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/json"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/overlay"
"git.novox.be/novox/mesh-sdk/go/seal"
)
// signersState is the vault's state `signers` in memory, counting writes.
type signersState struct {
keys map[string][]byte
writes int
}
func (s *signersState) Get(key string) ([]byte, error) { return s.keys[key], nil }
func (s *signersState) Put(key string, value []byte) error {
s.keys[key] = append([]byte(nil), value...)
s.writes++
return nil
}
// L3 s2 to s5 (novox/hq ADR 0550 §2): the vault makes its signing key and authority once, with their operator copies and
// no other holder; publishes their public halves only when they differ; signs a node's certificate for that node's
// node-engine alone, from its placement's name and its record's serving key; and no answer, event or record carries a
// key.
func TestTheVaultSigns(t *testing.T) {
v := newVault(t, nil)
state := &signersState{keys: map[string][]byte{}}
v.Signers = state
servingPub, _, _ := ed25519.GenerateKey(rand.Reader)
placements := map[string]delivery.Placement{
"ace": {Node: "ace", Decided: delivery.NodeDecided{Address: "10.77.0.3"}},
"g14": {Node: "g14", Decided: delivery.NodeDecided{Address: "10.77.0.4"}},
"plain": {Node: "plain"},
}
v.Placement = PlacementReader{Read: func(key string) ([]byte, error) {
node, err := delivery.ParsePlacementKey(key)
if err != nil {
return nil, err
}
p, ok := placements[node]
if !ok {
return nil, nil
}
return delivery.EncodePlacement(p)
}}
records := map[string]string{"ace": base64.StdEncoding.EncodeToString(servingPub), "plain": base64.StdEncoding.EncodeToString(servingPub)}
v.Haves = func(key string) ([]byte, error) {
node, _, _ := strings.Cut(key, ".")
serving, ok := records[node]
if !ok {
return nil, nil
}
return delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: node, ServingKey: serving}})
}
// s2: made once, kept with their operator copies; a second pass makes nothing new and writes nothing again (s5).
if err := v.SigningPass(); err != nil {
t.Fatal(err)
}
var seeds []string
for _, name := range []string{SigningKeySecret, AuthorityKeySecret} {
r, held, err := v.Store.Read(name)
if err != nil || !held || r.Generation != 1 {
t.Fatalf("%s: held %v at generation %d, %v", name, held, r.Generation, err)
}
value, _ := seal.Open(r.AtRest, v.Own.PrivateKey)
if got := openOperator(t, v.operatorPublic, v.operatorPrivate, r.Operator.Sealed); got != value {
t.Errorf("%s: the operator copy does not open to the value held", name)
}
seeds = append(seeds, value)
}
key1, auth1, err := v.SignersValues()
if err != nil {
t.Fatal(err)
}
writes := state.writes
if writes != 2 {
t.Errorf("signers written %d times at the first pass, want its two keys", writes)
}
if err := v.SigningPass(); err != nil {
t.Fatal(err)
}
key2, auth2, _ := v.SignersValues()
if key2 != key1 || auth2 != auth1 || state.writes != writes {
t.Errorf("a second pass changed the keys or wrote signers again (%d writes)", state.writes)
}
if _, err := delivery.DecodeSignerKey(state.keys[delivery.SignersKeyKey]); err != nil {
t.Errorf("signers' key: %v", err)
}
authority, err := delivery.DecodeSignerAuthority(state.keys[delivery.SignersAuthorityKey])
if err != nil {
t.Fatalf("signers' authority: %v", err)
}
// No other holder: `get` hands neither, even to an asker a rule names for it.
asker, _ := seal.GenerateKeyPair()
v.Askers = AllowList{"node.ace": {SigningKeySecret, AuthorityKeySecret}}
for _, name := range []string{SigningKeySecret, AuthorityKeySecret} {
if _, err := v.Get("node.ace", name, asker.PublicKey); err == nil {
t.Errorf("get handed %s", name)
}
}
v.Askers = nil
if forced, err := v.PublishSigners(true); err != nil || len(forced) != 2 {
t.Errorf("signers_reassert wrote %v, %v", forced, err)
}
// s3, s4: who may ask which kind, and the certificate built from bus state.
for _, row := range []struct {
name, caller, kind, node, refused string
}{
{"a module is not a node-engine", "ace.mesh-grants", delivery.KindNodeCertificate, "", "not a node-engine"},
{"another node's certificate", "node.g14", delivery.KindNodeCertificate, "ace", "its own alone"},
{"a node not on the private network", "node.plain", delivery.KindNodeCertificate, "", "not on the private network"},
{"a node that states no serving key", "node.g14", delivery.KindNodeCertificate, "", "no record"},
{"an enrolment has no caller yet", "node.ace", delivery.KindEnrolment, "", "ADR 0561 §5"},
{"the authority is the vault's own", "node.ace", delivery.KindAuthority, "", "the vault's own"},
{"an unproven caller", "", delivery.KindNodeCertificate, "", "proved"},
{"the node's own certificate", "node.ace", delivery.KindNodeCertificate, "ace", ""},
} {
v.bus.raw = nil
got, err := v.Sign(row.caller, SignArgs{Kind: row.kind, Node: row.node})
if row.refused != "" {
if err == nil || !strings.Contains(err.Error(), row.refused) {
t.Errorf("%s: %v, want refused saying %q", row.name, err, row.refused)
}
if len(v.bus.raw) != 0 {
t.Errorf("%s: refused, and said %v", row.name, v.bus.raw)
}
continue
}
if err != nil {
t.Fatalf("%s: %v", row.name, err)
}
if err := delivery.CheckNodeCertificate(got.Certificate, []string{authority.Certificate}, overlay.InternalName("ace"),
servingPub, authority.Made.Add(1)); err != nil {
t.Errorf("%s: the certificate does not check against signers' authority: %v", row.name, err)
}
if got.Signer != authority.Fingerprint || got.Digest == "" {
t.Errorf("%s: signer %s, digest %q", row.name, got.Signer, got.Digest)
}
// Said once as signed, kept in the record, and no key in any of it.
answer, _ := json.Marshal(got)
said := strings.Join(v.bus.raw, "\n")
if !strings.Contains(said, `"event":"signed"`) || !strings.Contains(said, got.Digest) {
t.Errorf("%s: signed not said: %s", row.name, said)
}
rows, _ := v.Signatures(10)
if len(rows) != 1 || rows[0].Caller != "node.ace" || rows[0].Kind != delivery.KindNodeCertificate || rows[0].Digest != got.Digest {
t.Errorf("%s: vault_signed lists %+v", row.name, rows)
}
status, _ := json.Marshal(v.SigningState())
for _, seed := range seeds {
raw, _ := base64.StdEncoding.DecodeString(seed)
priv := ed25519.NewKeyFromSeed(raw)
for _, text := range []string{string(answer), said, string(status), string(state.keys[delivery.SignersKeyKey]),
string(state.keys[delivery.SignersAuthorityKey])} {
if strings.Contains(text, seed) || strings.Contains(text, base64.StdEncoding.EncodeToString(priv)) {
t.Errorf("%s: a signing key's private half is in what the vault says: %s", row.name, text)
}
}
}
}
// A serving key changed in the record is the one certified next, never the one before.
other, _, _ := ed25519.GenerateKey(rand.Reader)
records["ace"] = base64.StdEncoding.EncodeToString(other)
got, err := v.Sign("node.ace", SignArgs{Kind: delivery.KindNodeCertificate})
if err != nil || delivery.CheckNodeCertificate(got.Certificate, []string{authority.Certificate}, overlay.InternalName("ace"),
servingPub, authority.Made.Add(1)) == nil {
t.Errorf("a certificate for a serving key the record no longer states: %v", err)
}
// s2: one secret lost beside the other, or both lost while signers publishes a key, makes nothing.
lost := newVault(t, nil)
lost.Signers = &signersState{keys: map[string][]byte{delivery.SignersKeyKey: state.keys[delivery.SignersKeyKey]}}
if made, err := lost.EnsureSigning(); made || err == nil || !strings.Contains(err.Error(), "MISSING") {
t.Errorf("keys made while signers publishes one: %v, %v", made, err)
}
half := newVault(t, nil)
if _, err := half.Put(SigningKeySecret, seeds[0], "the test"); err != nil {
t.Fatal(err)
}
if made, err := half.EnsureSigning(); made || err == nil || !strings.Contains(err.Error(), "MISSING") {
t.Errorf("an authority made beside a held signing key: %v, %v", made, err)
}
if _, held, _ := half.Store.Read(AuthorityKeySecret); held {
t.Error("the authority was made all the same")
}
}
+52 -1
View File
@@ -573,6 +573,57 @@ func definitions(env *Env) []toolDef {
}
return map[string]any{"deleted": consumer, "freed_bytes": -1}, nil
}},
{Name: Seat + ".sign",
Description: "Sign with the vault's key or authority (novox/hq ADR 0550 §2). Kind node-certificate: the certificate " +
"of the calling machine's internal name for the serving key its record states, built from bus state alone and " +
"asked by that machine's node-engine only (the bus proves which); kind enrolment: no caller is named until " +
"adoption's record (ADR 0561 §5). Answers the certificate, the authority, the signer's fingerprint and the " +
"digest, and says signed. Never a key.",
Input: map[string]any{"kind": str("node-certificate or enrolment"),
"node": str("the machine, for a node certificate: the caller's own (left out, the caller's)"),
"reply": str("the enrolment reply's bytes in base64, for an enrolment")},
run: func(caller string, a map[string]any) (any, error) {
v, err := env.ready()
if err != nil {
return nil, err
}
return v.Sign(caller, SignArgs{Kind: arg(a, "kind"), Node: arg(a, "node"), Reply: arg(a, "reply")})
}},
{Name: "vault_signers",
Description: "The vault's signing key and authority (novox/hq ADR 0550 §2): whether each is held with its operator " +
"copy, the fingerprints the vault holds, what its state signers publishes, and whether they match. Never a key.",
run: func(string, map[string]any) (any, error) {
v, err := env.ready()
if err != nil {
return nil, err
}
return v.SigningState(), nil
}},
{Name: "vault_signed",
Description: "The last signatures the vault made, newest first: when, the caller the bus proved, the kind, the node " +
"and the digest of what was signed. Never a key.",
Input: map[string]any{"limit": map[string]any{"type": "number", "description": "how many (default 50)"}},
run: func(_ string, a map[string]any) (any, error) {
v, err := env.ready()
if err != nil {
return nil, err
}
return v.Signatures(limitOf(a))
}},
{Name: "signers_reassert",
Description: "Write the vault's state signers again from the keys it holds, whether or not it differs (novox/hq " +
"ADR 0550 §2): the by-hand path for a state lost or doubted. Makes no key.",
run: func(string, map[string]any) (any, error) {
v, err := env.ready()
if err != nil {
return nil, err
}
written, err := v.PublishSigners(true)
if err != nil {
return nil, err
}
return map[string]any{"written": written}, nil
}},
{Name: "vault_health",
Description: "The vault's runtime health, as the node-engine asks it (novox/hq ADR 0366): unhealthy once " +
"re-sealing the operator copies has failed on three passes in a row, saying what failed and why; " +
@@ -594,7 +645,7 @@ func definitions(env *Env) []toolDef {
key, _ := env.Vault.Store.OperatorKey()
return map[string]any{"ready": true, "operator_key": key, "copies_on_an_earlier_key": left,
"operator_key_refused": env.keyRefused(), "handed_rolled": env.Vault.Store.Rolled(),
"minting": env.Vault.MintState(), "retiring": env.Vault.RetireState(), "ok": left == 0 && env.keyRefused() == "" && key != ""}, nil
"minting": env.Vault.MintState(), "retiring": env.Vault.RetireState(), "signing": env.Vault.SigningState(), "ok": left == 0 && env.keyRefused() == "" && key != ""}, nil
}},
}
}
@@ -66,6 +66,11 @@ type Vault struct {
LeaseRead func() ([]byte, bool, error)
// minting is the switch on the lease and what the vault knows of the wanted bus accounts (mint.go).
minting minting
// Signers is the vault's state `signers`, where it publishes its signing halves (signing.go, ADR 0550 §2): nil writes
// none.
Signers SignersStore
// signingWhy is why the last signing pass did not finish, under mu (signing.go).
signingWhy string
// callsMu guards pending: the keys of the calls open now (intake.go), never written anywhere.
callsMu sync.Mutex
pending map[string]*callKey
@@ -295,6 +300,10 @@ func (v *Vault) Get(caller, name, publicKey string) (Answer, error) {
if err := validName(name); err != nil {
return Answer{}, err
}
// The vault's signing secrets are handed to nobody: it signs with them itself (ADR 0550 §2).
if signingSecret(name) {
return Answer{}, fmt.Errorf("%s is the vault's own signing secret and is handed to nobody: ask the verb sign", name)
}
// A value issued for one holder is that holder's alone, whatever else would allow it (issue 749).
if why := notTheHolder(caller, name); why != "" {
v.log("refused %s to %s: %s", name, caller, why)
@@ -145,9 +145,9 @@ func TestNoBucketHoldsASecret(t *testing.T) {
if err := json.Unmarshal(raw, &manifest); err != nil {
t.Fatal(err)
}
if s, _ := json.Marshal(manifest["state"]); string(s) != `[{"history":2,"name":"bus-accounts"}]` {
t.Errorf("the manifest declares buckets %s: a secret is in no bucket, sealed or not (ADR 0201 §13), and the one "+
"bucket is the accounts' hashes (ADR 0547 clause 9)", s)
if s, _ := json.Marshal(manifest["state"]); string(s) != `[{"history":2,"name":"bus-accounts"},"signers"]` {
t.Errorf("the manifest declares buckets %s: a secret is in no bucket, sealed or not (ADR 0201 §13), and the "+
"buckets are the accounts' hashes (ADR 0547 clause 9) and the signing keys' public halves (ADR 0550 §2)", s)
}
if r, _ := json.Marshal(manifest["reads"]); string(r) != `["mesh-controller.assignments","node-engine.have","mesh-grants.accounts","mesh-controller.lease","mesh-delivery-indexer.placement"]` {
t.Errorf("the manifest reads %s: the vault reads the controller's assignments, the haves and the grants part's "+
@@ -171,6 +171,11 @@ func TestNoBucketHoldsASecret(t *testing.T) {
code = []byte(strings.Replace(string(code), "stdio.State(BusAccountsState).Keys(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(BusAccountsState).Delete(", "", 1))
}
// The signing keys' public halves, in signing.go alone: one read and one write of its own bucket (ADR 0550 §2).
if f == "signing.go" {
code = []byte(strings.Replace(string(code), "stdio.State(delivery.SignersState).Get(", "", 1))
code = []byte(strings.Replace(string(code), "stdio.State(delivery.SignersState).Put(", "", 1))
}
// The haves, in main.go alone: one read, one watch and their keys (own_drift), never a write; one watch of the
// assignments, which the vault's rules read from memory once it caught up (issue 1126); and one watch of the wanted
// accounts, the grants part's, which hold no secret (ADR 0561 §2).
+10 -4
View File
@@ -17,7 +17,8 @@
"secret-changed",
"account-hashed",
"own-replaced",
"account-issued"
"account-issued",
"signed"
],
"reads": [
"mesh-controller.assignments",
@@ -30,7 +31,8 @@
{
"name": "bus-accounts",
"history": 2
}
},
"signers"
],
"receives": {
"secret": "${dir:grants}/mesh.json"
@@ -171,7 +173,10 @@
"vault_status",
"vault_health",
"own_drift",
"own_take_switch"
"own_take_switch",
"vault_signers",
"vault_signed",
"signers_reassert"
],
"claims": [
{
@@ -187,7 +192,8 @@
"node-key",
"move-asks",
"own",
"issue-account"
"issue-account",
"sign"
]
}
]