internal/agent/syncclient/hostcert_test.go
Ref: Size: 2.9 KiB History
package syncclient
import (
"os"
"testing"
"time"
"github.com/a73x/eitri/internal/server/sshca"
"github.com/a73x/eitri/internal/server/store"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
)
// gateSigner is the control plane's half of the exchange: it signs whatever
// public key a host reports, for whatever principal the control plane chose.
type gateSigner struct{ ca ssh.Signer }
func (g gateSigner) SignHostCert(pub ssh.PublicKey, principal string) (string, error) {
cert, err := sshca.SignHostCert(g.ca, pub, []string{principal}, principal, time.Now(), sshca.HostCertTTL)
if err != nil {
return "", err
}
return string(ssh.MarshalAuthorizedKey(cert)), nil
}
// TestGuestHostKeyRoundTrip drives the whole exchange over a real QUIC sync
// connection: the host generates a key, reports the public half, the control
// plane signs it for the name on the VM's row, and the certificate comes back
// down. The private half is never asked for and never sent.
func TestGuestHostKeyRoundTrip(t *testing.T) {
h := newServerHarness(t)
_, caSigner, err := sshca.GenerateHostKey()
require.NoError(t, err)
h.svc.SetHostCertSigner(gateSigner{ca: caSigner})
hostID, cred := h.enroll()
c := newClient(t, h.addr, h.fp, hostID, cred)
require.NoError(t, h.st.CreateVM(store.VM{ID: "vm1", HostID: hostID, Name: "a",
ImageURL: "u", ImageSHA256: "s", VCPUs: 1, MemMB: 512, DiskGB: 5, PowerState: "stopped"}))
go c.Run(t.Context())
var vm store.VM
require.Eventually(t, func() bool {
vm, err = h.st.GetVM("vm1")
return err == nil && vm.SSHHostCert != ""
}, 10*time.Second, 100*time.Millisecond, "the control plane should certify the key its host reported")
// The certificate is for the key that stayed on the host, under the name
// the control plane derived from the row.
priv, err := os.ReadFile(c.St.HostKeyPath("vm1"))
require.NoError(t, err)
hostSigner, err := ssh.ParsePrivateKey(priv)
require.NoError(t, err)
pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(vm.SSHHostCert))
require.NoError(t, err)
cert, ok := pk.(*ssh.Certificate)
require.True(t, ok)
assert.Equal(t, uint32(ssh.HostCert), cert.CertType)
assert.Equal(t, []string{testTenant + ".a"}, cert.ValidPrincipals)
assert.Equal(t, hostSigner.PublicKey().Marshal(), cert.Key.Marshal(),
"the certificate must be for the key this host holds")
// The stored public key is the reported one, and nothing resembling a
// private key was persisted alongside it.
assert.Equal(t, string(ssh.MarshalAuthorizedKey(hostSigner.PublicKey())), vm.SSHHostPubKey+"\n")
assert.NotContains(t, vm.SSHHostPubKey, "PRIVATE")
// Once certified, the VM completes its create — it was held at the gate
// until the certificate arrived.
require.Eventually(t, func() bool {
v, err := h.st.GetVM("vm1")
return err == nil && v.Status == "ready"
}, 10*time.Second, 100*time.Millisecond, "the guest should boot once its certificate lands")
}