a73x

internal/server/sshca/sealed_test.go

Ref:   Size: 6.2 KiB   History

package sshca

import (
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/a73x/eitri/internal/server/seal"
	"golang.org/x/crypto/ssh"
)

// read returns a key file's contents as a string.
func read(t *testing.T, path string) string {
	t.Helper()
	b, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read %s: %v", path, err)
	}
	return string(b)
}

// plaintextKey writes an unsealed key file — the shape a plane that predates
// sealing has on disk — and returns its path and public key.
func plaintextKey(t *testing.T, dir string) (path string, pub string) {
	t.Helper()
	pemBytes, signer, err := GenerateHostKey()
	if err != nil {
		t.Fatalf("GenerateHostKey: %v", err)
	}
	path = filepath.Join(dir, "ca")
	if err := os.WriteFile(path, pemBytes, 0o600); err != nil {
		t.Fatalf("write plaintext key: %v", err)
	}
	return path, AuthorizedKeyLine(signer.PublicKey())
}

// TestFirstBootWritesASealedKey: a key generated today is never written in the
// clear, not even briefly.
func TestFirstBootWritesASealedKey(t *testing.T) {
	path := filepath.Join(t.TempDir(), "ca")
	signer, err := LoadOrCreate(path, testKEK)
	if err != nil {
		t.Fatalf("LoadOrCreate: %v", err)
	}

	stored := read(t, path)
	if !seal.IsSealed(stored) {
		t.Fatalf("key file is not sealed: %q", stored)
	}
	if strings.Contains(stored, "PRIVATE KEY") {
		t.Fatal("key file contains a PEM")
	}

	// And it is the key that was returned: reopening yields the same public half.
	reopened, err := LoadOrCreate(path, testKEK)
	if err != nil {
		t.Fatalf("reload: %v", err)
	}
	if AuthorizedKeyLine(reopened.PublicKey()) != AuthorizedKeyLine(signer.PublicKey()) {
		t.Fatal("the sealed key reopened as a different key")
	}
}

// TestPlaintextKeyIsSealedInPlace is the upgrade path: a plane whose host CA
// predates sealing seals it on the boot that first has a key, keeps the same
// identity, and does not do it again.
func TestPlaintextKeyIsSealedInPlace(t *testing.T) {
	dir := t.TempDir()
	path, pub := plaintextKey(t, dir)

	signer, err := LoadOrCreate(path, testKEK)
	if err != nil {
		t.Fatalf("LoadOrCreate: %v", err)
	}
	// The identity is unchanged — this is the whole point. A different key here
	// would invalidate every @cert-authority pin in the fleet.
	if got := AuthorizedKeyLine(signer.PublicKey()); got != pub {
		t.Fatalf("identity changed on seal:\n before %s\n after  %s", pub, got)
	}

	sealed := read(t, path)
	if !seal.IsSealed(sealed) {
		t.Fatalf("key file was not sealed in place: %q", sealed)
	}
	if fi, err := os.Stat(path); err != nil {
		t.Fatalf("stat: %v", err)
	} else if perm := fi.Mode().Perm(); perm != 0o600 {
		t.Fatalf("sealed key perms = %o, want 0600", perm)
	}

	// Idempotent: the second boot opens what the first wrote and rewrites nothing.
	again, err := LoadOrCreate(path, testKEK)
	if err != nil {
		t.Fatalf("second boot: %v", err)
	}
	if AuthorizedKeyLine(again.PublicKey()) != pub {
		t.Fatal("identity changed on the second boot")
	}
	if read(t, path) != sealed {
		t.Fatal("the second boot rewrote an already-sealed key")
	}

	// Nothing is left behind in the data directory.
	entries, err := os.ReadDir(dir)
	if err != nil {
		t.Fatalf("readdir: %v", err)
	}
	if len(entries) != 1 {
		names := make([]string, 0, len(entries))
		for _, e := range entries {
			names = append(names, e.Name())
		}
		t.Fatalf("sealing left temp files behind: %v", names)
	}
}

// TestWrongKEKRefusesAndKeepsTheKey is the one that matters most: a plane that
// comes back with the wrong key must stop, not mint a new identity. A
// regenerated host CA would present every client with an impostor.
func TestWrongKEKRefusesAndKeepsTheKey(t *testing.T) {
	path := filepath.Join(t.TempDir(), "ca")
	if _, err := LoadOrCreate(path, testKEK); err != nil {
		t.Fatalf("seed: %v", err)
	}
	sealed := read(t, path)

	for _, tc := range []struct {
		name string
		kek  []byte
	}{
		{"wrong key", otherKEK},
		{"no key at all", nil},
		{"short key", testKEK[:16]},
	} {
		t.Run(tc.name, func(t *testing.T) {
			signer, err := LoadOrCreate(path, tc.kek)
			if err == nil {
				t.Fatal("a sealed key opened with the wrong KEK")
			}
			if signer != nil {
				t.Fatal("a signer was returned despite the failure")
			}
			if !strings.Contains(err.Error(), path) {
				t.Errorf("the error must name the file, got %v", err)
			}
			if !strings.Contains(err.Error(), "key_encryption_key") {
				t.Errorf("the error must name the config key, got %v", err)
			}
			if strings.Contains(err.Error(), "PRIVATE KEY") {
				t.Errorf("the error echoes key material: %v", err)
			}
			if read(t, path) != sealed {
				t.Fatal("the key file was rewritten — a host CA must never be regenerated over an unreadable one")
			}
		})
	}

	// And the right key still opens it afterwards: nothing was consumed.
	if _, err := LoadOrCreate(path, testKEK); err != nil {
		t.Fatalf("the key must survive a failed open: %v", err)
	}
}

// TestUnparseableKeyIsReportedNotSealed: a corrupt file is a corrupt file. It is
// reported as one, and not rewritten as sealed nonsense that would hide what
// happened.
func TestUnparseableKeyIsReportedNotSealed(t *testing.T) {
	path := filepath.Join(t.TempDir(), "ca")
	if err := os.WriteFile(path, []byte("not a key at all\n"), 0o600); err != nil {
		t.Fatalf("write: %v", err)
	}
	if _, err := LoadOrCreate(path, testKEK); err == nil {
		t.Fatal("garbage was accepted as a key")
	}
	if got := read(t, path); got != "not a key at all\n" {
		t.Fatalf("the file was rewritten: %q", got)
	}
}

// TestNewSealsBothKeys: the host CA and the gate host key both rest sealed, and
// remain distinct keys.
func TestNewSealsBothKeys(t *testing.T) {
	dir := t.TempDir()
	caPath, hostPath := filepath.Join(dir, "ca"), filepath.Join(dir, "host")

	ca, err := New(caPath, hostPath, testKEK)
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	for _, p := range []string{caPath, hostPath} {
		if stored := read(t, p); !seal.IsSealed(stored) {
			t.Errorf("%s is not sealed: %q", p, stored)
		}
	}
	if string(ssh.MarshalAuthorizedKey(ca.HostCA().PublicKey())) ==
		string(ssh.MarshalAuthorizedKey(ca.HostKey().PublicKey())) {
		t.Fatal("HostCA and HostKey share the same public key")
	}

	// A wrong KEK stops the whole gate rather than half-loading it.
	if _, err := New(caPath, hostPath, otherKEK); err == nil {
		t.Fatal("New accepted the wrong KEK")
	}
}