diff --git a/modules/mesh-vault/cmd/mesh-vault/main.go b/modules/mesh-vault/cmd/mesh-vault/main.go index a86c5eef..af3c48c1 100644 --- a/modules/mesh-vault/cmd/mesh-vault/main.go +++ b/modules/mesh-vault/cmd/mesh-vault/main.go @@ -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() diff --git a/modules/mesh-vault/cmd/mesh-vault/signing.go b/modules/mesh-vault/cmd/mesh-vault/signing.go new file mode 100644 index 00000000..c895a65e --- /dev/null +++ b/modules/mesh-vault/cmd/mesh-vault/signing.go @@ -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` 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) + } +} diff --git a/modules/mesh-vault/cmd/mesh-vault/signing_test.go b/modules/mesh-vault/cmd/mesh-vault/signing_test.go new file mode 100644 index 00000000..68ef9f6d --- /dev/null +++ b/modules/mesh-vault/cmd/mesh-vault/signing_test.go @@ -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") + } +} diff --git a/modules/mesh-vault/cmd/mesh-vault/tools.go b/modules/mesh-vault/cmd/mesh-vault/tools.go index 5fc05dc7..9a027cee 100644 --- a/modules/mesh-vault/cmd/mesh-vault/tools.go +++ b/modules/mesh-vault/cmd/mesh-vault/tools.go @@ -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 }}, } } diff --git a/modules/mesh-vault/cmd/mesh-vault/vault.go b/modules/mesh-vault/cmd/mesh-vault/vault.go index 37e9560c..d52ac302 100644 --- a/modules/mesh-vault/cmd/mesh-vault/vault.go +++ b/modules/mesh-vault/cmd/mesh-vault/vault.go @@ -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) diff --git a/modules/mesh-vault/cmd/mesh-vault/vault_test.go b/modules/mesh-vault/cmd/mesh-vault/vault_test.go index 8bd1018d..c3f38969 100644 --- a/modules/mesh-vault/cmd/mesh-vault/vault_test.go +++ b/modules/mesh-vault/cmd/mesh-vault/vault_test.go @@ -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). diff --git a/modules/mesh-vault/module.json b/modules/mesh-vault/module.json index f20e2cb4..5fd98c62 100644 --- a/modules/mesh-vault/module.json +++ b/modules/mesh-vault/module.json @@ -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" ] } ]