Files
mesh-catalog/modules/mesh-vault/cmd/mesh-vault/signing_test.go
T

207 lines
8.9 KiB
Go

package main
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/json"
engineclient "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
nodeseat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
vaultclient "git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
assignerclient "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
placing "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"strings"
"testing"
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/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]placing.Placement{
"ace": {Node: "ace", Decided: placing.NodeDecided{Address: "10.77.0.3"}},
"g14": {Node: "g14", Decided: placing.NodeDecided{Address: "10.77.0.4"}},
"plain": {Node: "plain"},
}
// Research 072 row 37: ace and plain in their keys per module and node, g14 still in the form before.
v.Placement = PlacementReader{Read: func(key string) ([]byte, error) {
node, _, kind, err := assignerclient.ParseKey(key)
if err != nil {
return nil, err
}
p, ok := placements[node]
if !ok || (node == "g14") != (kind == assignerclient.LegacyKind) {
return nil, nil
}
if kind == assignerclient.LegacyKind {
return assignerclient.EncodePlacement(p)
}
values, err := assignerclient.PlacementValues(p)
return values[key], err
}}
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 engineclient.EncodeHaveNode(nodeseat.HaveByNodeNodeValue{Record: &nodeseat.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 := vaultclient.DecodeSignerKey(state.keys[meshvault.SignersByKeyKey()]); err != nil {
t.Errorf("signers' key: %v", err)
}
authority, err := vaultclient.DecodeSignerAuthority(state.keys[meshvault.SignersByAuthorityKey()])
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", vaultclient.KindNodeCertificate, "", "not a node-engine"},
{"another node's certificate", "node.g14", vaultclient.KindNodeCertificate, "ace", "its own alone"},
{"a node not on the private network", "node.plain", vaultclient.KindNodeCertificate, "", "not on the private network"},
{"a node that states no serving key", "node.g14", vaultclient.KindNodeCertificate, "", "no record"},
{"an enrolment has no caller yet", "node.ace", vaultclient.KindEnrolment, "", "ADR 0561 §5"},
{"the authority is the vault's own", "node.ace", vaultclient.KindAuthority, "", "the vault's own"},
{"an unproven caller", "", vaultclient.KindNodeCertificate, "", "proved"},
{"the node's own certificate", "node.ace", vaultclient.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 := vaultclient.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 != vaultclient.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[meshvault.SignersByKeyKey()]),
string(state.keys[meshvault.SignersByAuthorityKey()])} {
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: vaultclient.KindNodeCertificate})
if err != nil || vaultclient.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{meshvault.SignersByKeyKey(): state.keys[meshvault.SignersByKeyKey()]}}
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")
}
}