142 lines
6.9 KiB
Go
142 lines
6.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/overlay"
|
|
|
|
"github.com/novox/mesh-host/internal/identity"
|
|
)
|
|
|
|
// Issue 1126 (ADR 0555 §3): the mesh's own certificate a composition carries is kept for the machine's record, and the
|
|
// record's keys read it; a composition carrying none changes nothing. And (L3 s6, s7; ADR 0550 §2) the record says what
|
|
// the node trusts a mesh signature from, and the node keeps the certificate the vault signed for it.
|
|
func TestTheAppliedAuthorityIsKeptForTheRecord(t *testing.T) {
|
|
state := filepath.Join(t.TempDir(), "state", "state.json")
|
|
res := func(id, content string) json.RawMessage {
|
|
raw, _ := json.Marshal(map[string]any{"id": id, "type": "file", "path": "/etc/x", "content": content})
|
|
return raw
|
|
}
|
|
if err := keepAuthority(state, delivery.Composed{Resources: []json.RawMessage{res("web.conf", "x")}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(authorityPath(state)); !os.IsNotExist(err) {
|
|
t.Fatalf("a composition with no authority kept one: %v", err)
|
|
}
|
|
if err := keepAuthority(state, delivery.Composed{Resources: []json.RawMessage{res("ca-trust.certificate-authority", "CA-1")}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := recordKeys(state, nil)(); got.Authority != "CA-1" {
|
|
t.Errorf("the record's keys read the authority %q", got.Authority)
|
|
}
|
|
|
|
// L3 s6: before the vault's signers are read the node trusts the controller's key alone; once read, a message the
|
|
// vault signed is accepted and the record names both; once the lease is handed away the controller's key is refused
|
|
// and the record names the vault's alone. Each change tells the record's writer.
|
|
controllerPub, controller, _ := ed25519.GenerateKey(nil)
|
|
vaultPub, vault, _ := ed25519.GenerateKey(nil)
|
|
trust := newMeshTrust(controllerPub)
|
|
told := 0
|
|
trust.onChange(func() { told++ })
|
|
keys := recordKeys(state, trust.Trusts)
|
|
ctrlFP, vaultFP := delivery.SignerFingerprint(controllerPub), delivery.SignerFingerprint(vaultPub)
|
|
if got := keys().Trusts; !delivery.TrustsKey(got, delivery.SignerController, ctrlFP) || delivery.TrustsKey(got, delivery.SignerVault, vaultFP) {
|
|
t.Errorf("before the signers are read the record trusts %v", got)
|
|
}
|
|
enrol, _ := delivery.SignContext(delivery.KindEnrolment)
|
|
msg := []byte("a reply")
|
|
if _, err := trust.Verify(delivery.KindEnrolment, msg, delivery.SignMessage(vault, enrol, msg)); err == nil {
|
|
t.Error("a vault-signed message is accepted before the vault's key is read")
|
|
}
|
|
vaultCA := testAuthority(t, vaultPub, vault)
|
|
keyValue, _ := json.Marshal(delivery.NewSignerKey(vaultPub, time.Now()))
|
|
fp, _ := delivery.AuthorityFingerprint(vaultCA)
|
|
authValue, _ := json.Marshal(delivery.SignerAuthority{Certificate: vaultCA, Fingerprint: fp, Made: time.Now()})
|
|
trust.heardSigner(delivery.SignersKeyKey, keyValue, false, func(string) {})
|
|
trust.heardSigner(delivery.SignersAuthorityKey, authValue, false, func(string) {})
|
|
if s, err := trust.Verify(delivery.KindEnrolment, msg, delivery.SignMessage(vault, enrol, msg)); err != nil || s.Name != delivery.SignerVault {
|
|
t.Errorf("a vault-signed message on a node that read signers: %+v, %v", s, err)
|
|
}
|
|
if got := keys().Trusts; !delivery.TrustsKey(got, delivery.SignerController, ctrlFP) || !delivery.TrustsKey(got, delivery.SignerVault, vaultFP) ||
|
|
trust.VaultAuthority() != vaultCA || told != 2 {
|
|
t.Errorf("after the signers are read the record trusts %v (told %d)", got, told)
|
|
}
|
|
handed, _ := json.Marshal(buscompose.LeaseHolder{HandedTo: "mesh-vault", HandedBy: "person.operator"})
|
|
trust.heardLease(handed, true, nil)
|
|
if _, err := trust.Verify(delivery.KindEnrolment, msg, delivery.SignMessage(controller, enrol, msg)); err == nil || trust.Controller() != nil {
|
|
t.Error("after the lease is handed away the controller's key is still trusted")
|
|
}
|
|
if got := keys().Trusts; delivery.TrustsKey(got, delivery.SignerController, ctrlFP) || !delivery.TrustsKey(got, delivery.SignerVault, vaultFP) || told != 3 {
|
|
t.Errorf("after the lease is handed away the record trusts %v (told %d)", got, told)
|
|
}
|
|
trust.heardLease(handed, true, nil)
|
|
if told != 3 {
|
|
t.Error("a lease heard again unchanged wrote the record again")
|
|
}
|
|
// A signers' key naming another fingerprint is not trusted.
|
|
bad, _ := json.Marshal(delivery.SignerKey{Key: base64.StdEncoding.EncodeToString(controllerPub), Fingerprint: vaultFP})
|
|
trust.heardSigner(delivery.SignersKeyKey, bad, false, func(string) {})
|
|
if !delivery.TrustsKey(keys().Trusts, delivery.SignerVault, vaultFP) {
|
|
t.Error("a signers' key that does not check replaced the vault's")
|
|
}
|
|
|
|
// L3 s7: the certificate the vault signs for this node's internal name and serving key is kept where the record reads
|
|
// it, and the record is written again; one signed by another authority is refused and nothing is kept.
|
|
serving, err := identity.GenerateServingKey()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := identity.WriteServingKey(identity.ServingKeyPath(state), serving); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
servingPub, _ := base64.StdEncoding.DecodeString(serving.Public)
|
|
signedBy := func(ca string, key ed25519.PrivateKey) string {
|
|
parent, _ := delivery.ParseCertificate(ca)
|
|
der, err := x509.CreateCertificate(nil, delivery.NodeCertificateTemplate(overlay.InternalName("n1"), servingPub, time.Now().Add(-time.Hour)),
|
|
parent, ed25519.PublicKey(servingPub), key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}))
|
|
}
|
|
var asked []any
|
|
answer := signedBy(vaultCA, vault)
|
|
ask := func(_ context.Context, verb string, args any) (json.RawMessage, error) {
|
|
asked = append(asked, verb, args)
|
|
return json.Marshal(delivery.SignAnswer{Kind: delivery.KindNodeCertificate, Node: "n1", Certificate: answer})
|
|
}
|
|
written := 0
|
|
if err := certifyThisNode(context.Background(), state, "n1", trust, ask, func() { written++ }); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := recordKeys(state, nil)(); got.Certificate != answer || written != 1 || len(asked) != 2 || asked[0] != delivery.SignVerb {
|
|
t.Errorf("the vault's certificate: kept %v, written %d, asked %v", got.Certificate == answer, written, asked)
|
|
}
|
|
answer = signedBy(testAuthority(t, controllerPub, controller), controller)
|
|
if err := certifyThisNode(context.Background(), state, "n1", trust, ask, func() { written++ }); err == nil || written != 1 {
|
|
t.Errorf("a certificate of another authority was kept: %v", err)
|
|
}
|
|
}
|
|
|
|
// testAuthority is a self-signed authority certificate for key, as the vault makes its own.
|
|
func testAuthority(t *testing.T, pub ed25519.PublicKey, key ed25519.PrivateKey) string {
|
|
t.Helper()
|
|
tpl := delivery.AuthorityTemplate(pub, time.Now().Add(-time.Hour))
|
|
der, err := x509.CreateCertificate(nil, tpl, tpl, pub, key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}))
|
|
}
|