internal/server/syncsvc/hostcert_test.go
Ref: Size: 6.0 KiB History
package syncsvc
import (
"bytes"
"crypto/ed25519"
"crypto/rand"
"errors"
"testing"
"github.com/a73x/eitri/internal/pb"
"github.com/a73x/eitri/internal/server/store"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
"google.golang.org/protobuf/proto"
)
// countingSigner is a hostCertSigner that records what it was asked to sign, so
// a test can assert BOTH the principal the control plane chose and how often it
// spent a signature.
type countingSigner struct {
calls int
principals []string
err error
}
func (c *countingSigner) SignHostCert(pub ssh.PublicKey, principal string) (string, error) {
c.calls++
c.principals = append(c.principals, principal)
if c.err != nil {
return "", c.err
}
return "cert-for:" + principal + ":" + string(ssh.MarshalAuthorizedKey(pub)), nil
}
// testPubKey returns a real ed25519 public key in authorized_keys form.
func testPubKey(t *testing.T) string {
t.Helper()
pub, _, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
sshPub, err := ssh.NewPublicKey(pub)
require.NoError(t, err)
return string(bytes.TrimSpace(ssh.MarshalAuthorizedKey(sshPub)))
}
// reportKey builds the report a host sends for a VM that has generated its host
// key and is waiting for the certificate.
func reportKey(vmID, pubkey string) *pb.Report {
return &pb.Report{Vms: []*pb.VMStatus{
{VmId: vmID, Phase: "creating", PowerState: "stopped", SshHostPubkey: pubkey},
}}
}
func TestReportedHostKeyIsCertifiedForTheNameOnTheRow(t *testing.T) {
f := setup(t)
signer := &countingSigner{}
f.svc.SetHostCertSigner(signer)
require.NoError(t, f.st.CreateVM(store.VM{ID: "vm1", HostID: f.host.ID, Name: "a",
ImageURL: "u", ImageSHA256: "s", VCPUs: 1, MemMB: 512, DiskGB: 5, PowerState: "running"}))
pubkey := testPubKey(t)
f.svc.applyReport(f.host.ID, reportKey("vm1", pubkey))
// The agent supplies a key, never a name: the principal comes from the row.
assert.Equal(t, []string{testTenant + ".a"}, signer.principals)
vm, err := f.st.GetVM("vm1")
require.NoError(t, err)
assert.Equal(t, pubkey, vm.SSHHostPubKey)
assert.Contains(t, vm.SSHHostCert, "cert-for:"+testTenant+".a:")
// Every host repeats every VM's key every tick, forever. Steady state must
// cost nothing.
for range 5 {
f.svc.applyReport(f.host.ID, reportKey("vm1", pubkey))
}
assert.Equal(t, 1, signer.calls, "an unchanged key must not be re-signed")
}
func TestReportedHostKeyForAnotherHostsVMIsRefused(t *testing.T) {
f := setup(t)
signer := &countingSigner{}
f.svc.SetHostCertSigner(signer)
tok, _ := f.st.CreateEnrollmentToken(testTenant)
other, err := f.st.RedeemEnrollmentToken(tok, store.EnrollFacts{Name: "other", OS: "linux", Arch: "amd64", Provisioner: "cloudhv"})
require.NoError(t, err)
require.NoError(t, f.st.CreateVM(store.VM{ID: "vm1", HostID: f.host.ID, Name: "a",
ImageURL: "u", ImageSHA256: "s", VCPUs: 1, MemMB: 512, DiskGB: 5, PowerState: "running"}))
f.svc.applyReport(other.ID, reportKey("vm1", testPubKey(t)))
assert.Equal(t, 0, signer.calls, "a host may only speak for the VMs it holds")
vm, err := f.st.GetVM("vm1")
require.NoError(t, err)
assert.Empty(t, vm.SSHHostPubKey)
assert.Empty(t, vm.SSHHostCert)
}
func TestTombstonedVMIsNotCertified(t *testing.T) {
f := setup(t)
signer := &countingSigner{}
f.svc.SetHostCertSigner(signer)
require.NoError(t, f.st.CreateVM(store.VM{ID: "vm1", HostID: f.host.ID, Name: "a",
ImageURL: "u", ImageSHA256: "s", VCPUs: 1, MemMB: 512, DiskGB: 5, PowerState: "running"}))
require.NoError(t, f.st.TombstoneVM("vm1"))
f.svc.applyReport(f.host.ID, reportKey("vm1", testPubKey(t)))
assert.Equal(t, 0, signer.calls)
}
func TestNoSignerWiredIgnoresReportedHostKeys(t *testing.T) {
f := setup(t)
require.NoError(t, f.st.CreateVM(store.VM{ID: "vm1", HostID: f.host.ID, Name: "a",
ImageURL: "u", ImageSHA256: "s", VCPUs: 1, MemMB: 512, DiskGB: 5, PowerState: "running"}))
f.svc.applyReport(f.host.ID, reportKey("vm1", testPubKey(t)))
vm, err := f.st.GetVM("vm1")
require.NoError(t, err)
assert.Empty(t, vm.SSHHostCert)
// And with no CA, no guest is held waiting for a certificate that will
// never come.
snap, err := f.svc.buildSnapshot(f.host.ID)
require.NoError(t, err)
require.Len(t, snap.Vms, 1)
assert.False(t, snap.Vms[0].HostCertRequired)
}
func TestAFailedSigningIsRetriedOnTheNextReport(t *testing.T) {
f := setup(t)
signer := &countingSigner{err: errors.New("CA unavailable")}
f.svc.SetHostCertSigner(signer)
require.NoError(t, f.st.CreateVM(store.VM{ID: "vm1", HostID: f.host.ID, Name: "a",
ImageURL: "u", ImageSHA256: "s", VCPUs: 1, MemMB: 512, DiskGB: 5, PowerState: "running"}))
pubkey := testPubKey(t)
f.svc.applyReport(f.host.ID, reportKey("vm1", pubkey))
signer.err = nil
f.svc.applyReport(f.host.ID, reportKey("vm1", pubkey))
assert.Equal(t, 2, signer.calls, "a rejected signature must not be remembered as done")
vm, err := f.st.GetVM("vm1")
require.NoError(t, err)
assert.NotEmpty(t, vm.SSHHostCert)
}
// TestSnapshotCarriesNoPrivateKeyMaterial is the blunt assertion: whatever else
// changes, a desired-state snapshot must never contain a private key. The
// control plane has none to send, and this is what would notice if it did.
func TestSnapshotCarriesNoPrivateKeyMaterial(t *testing.T) {
f := setup(t)
f.svc.SetHostCertSigner(&countingSigner{})
require.NoError(t, f.st.CreateVM(store.VM{ID: "vm1", HostID: f.host.ID, Name: "a",
ImageURL: "u", ImageSHA256: "s", VCPUs: 1, MemMB: 512, DiskGB: 5, PowerState: "running"}))
f.svc.applyReport(f.host.ID, reportKey("vm1", testPubKey(t)))
snap, err := f.svc.buildSnapshot(f.host.ID)
require.NoError(t, err)
require.Len(t, snap.Vms, 1)
assert.True(t, snap.Vms[0].HostCertRequired, "a fleet with a CA requires certified guests")
assert.NotEmpty(t, snap.Vms[0].SshHostCert, "the certificate goes down; the key never came up")
raw, err := proto.Marshal(snap)
require.NoError(t, err)
assert.False(t, bytes.Contains(raw, []byte("PRIVATE KEY")),
"no private key may appear anywhere in a snapshot")
}