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:
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user