a73x

internal/server/sshca/sshca_test.go

Ref:   Size: 5.4 KiB   History

package sshca

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

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

// testKEK stands in for the config's key_encryption_key; otherKEK is a
// different one of the same size, for the plane that comes back with the wrong
// key.
var (
	testKEK  = bytes.Repeat([]byte{0x2b}, seal.KEKSize)
	otherKEK = bytes.Repeat([]byte{0x7f}, seal.KEKSize)
)

func TestLoadOrCreate_CreatesWith0600(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "ca")

	signer, err := LoadOrCreate(path, testKEK)
	if err != nil {
		t.Fatalf("LoadOrCreate: %v", err)
	}
	if signer == nil || signer.PublicKey() == nil {
		t.Fatal("LoadOrCreate returned a nil signer")
	}

	fi, err := os.Stat(path)
	if err != nil {
		t.Fatalf("stat created key: %v", err)
	}
	if perm := fi.Mode().Perm(); perm != 0o600 {
		t.Fatalf("key file perms = %o, want 0600", perm)
	}
}

func TestLoadOrCreate_ReloadStable(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "ca")

	first, err := LoadOrCreate(path, testKEK)
	if err != nil {
		t.Fatalf("LoadOrCreate (create): %v", err)
	}
	second, err := LoadOrCreate(path, testKEK)
	if err != nil {
		t.Fatalf("LoadOrCreate (reload): %v", err)
	}

	a := ssh.MarshalAuthorizedKey(first.PublicKey())
	b := ssh.MarshalAuthorizedKey(second.PublicKey())
	if !bytes.Equal(a, b) {
		t.Fatalf("public key changed across reload:\n first:  %s second: %s", a, b)
	}
}

func TestNew_AccessorsAndAuthorizedKey(t *testing.T) {
	dir := t.TempDir()
	caPath := filepath.Join(dir, "ca")
	hostPath := filepath.Join(dir, "host")

	ca, err := New(caPath, hostPath, testKEK)
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	if ca.HostCA() == nil {
		t.Fatal("HostCA() is nil")
	}
	if ca.HostKey() == nil {
		t.Fatal("HostKey() is nil")
	}

	// The host CA and gate host key must be distinct key material.
	if bytes.Equal(ssh.MarshalAuthorizedKey(ca.HostCA().PublicKey()),
		ssh.MarshalAuthorizedKey(ca.HostKey().PublicKey())) {
		t.Fatal("HostCA and HostKey share the same public key")
	}

	authLine := ca.HostCAAuthorizedKey()
	if len(authLine) == 0 {
		t.Fatal("HostCAAuthorizedKey() is empty")
	}
	// It must be a parseable authorized_keys line matching the host CA.
	pub, _, _, _, err := ssh.ParseAuthorizedKey(authLine)
	if err != nil {
		t.Fatalf("ParseAuthorizedKey(HostCAAuthorizedKey()): %v", err)
	}
	if !bytes.Equal(ssh.MarshalAuthorizedKey(pub),
		ssh.MarshalAuthorizedKey(ca.HostCA().PublicKey())) {
		t.Fatal("HostCAAuthorizedKey() does not match HostCA public key")
	}
}

func TestGenerateHostKey_PEMParsesToSigner(t *testing.T) {
	pemBytes, signer, err := GenerateHostKey()
	if err != nil {
		t.Fatalf("GenerateHostKey: %v", err)
	}
	if signer == nil || signer.PublicKey() == nil {
		t.Fatal("GenerateHostKey returned a nil signer")
	}
	// The PEM must round-trip to the SAME public key so a guest that loads it as
	// /etc/ssh/ssh_host_ed25519_key presents the key the cert was signed for.
	parsed, err := ssh.ParsePrivateKey(pemBytes)
	if err != nil {
		t.Fatalf("ParsePrivateKey(GenerateHostKey PEM): %v", err)
	}
	if !bytes.Equal(ssh.MarshalAuthorizedKey(parsed.PublicKey()),
		ssh.MarshalAuthorizedKey(signer.PublicKey())) {
		t.Fatal("PEM public key does not match the returned signer")
	}
}

func TestAuthorizedKeyLineIsCanonical(t *testing.T) {
	_, signer, err := GenerateHostKey()
	if err != nil {
		t.Fatal(err)
	}
	line := AuthorizedKeyLine(signer.PublicKey())
	if strings.ContainsAny(line, "\n\r") {
		t.Fatalf("line must have no newline: %q", line)
	}
	// Same key marshaled with a trailing newline trims to the same canonical line.
	withComment := string(ssh.MarshalAuthorizedKey(signer.PublicKey()))
	if AuthorizedKeyLine(signer.PublicKey()) != strings.TrimSpace(withComment) {
		t.Fatal("AuthorizedKeyLine must equal the trimmed marshaled key")
	}
}

func TestSignHostCert_SignedByCAAndScopedToPrincipal(t *testing.T) {
	ca, err := LoadOrCreate(filepath.Join(t.TempDir(), "ca"), testKEK)
	if err != nil {
		t.Fatalf("LoadOrCreate CA: %v", err)
	}
	_, host, err := GenerateHostKey()
	if err != nil {
		t.Fatalf("GenerateHostKey: %v", err)
	}

	now := time.Unix(1_700_000_000, 0)
	cert, err := SignHostCert(ca, host.PublicKey(), []string{"gate.example.com"}, "eitri-gate", now, HostCertTTL)
	if err != nil {
		t.Fatalf("SignHostCert: %v", err)
	}
	if cert.CertType != ssh.HostCert {
		t.Fatalf("CertType = %d, want HostCert", cert.CertType)
	}
	if got := cert.ValidPrincipals; len(got) != 1 || got[0] != "gate.example.com" {
		t.Fatalf("ValidPrincipals = %v, want [gate.example.com]", got)
	}
	if cert.ValidBefore-cert.ValidAfter != uint64(HostCertTTL.Seconds()) {
		t.Fatalf("validity window = %d, want %d", cert.ValidBefore-cert.ValidAfter, uint64(HostCertTTL.Seconds()))
	}

	// A client trusting the CA as a host authority must accept the cert for its
	// principal — this is the `@cert-authority` verification path.
	checker := &ssh.CertChecker{
		Clock:           func() time.Time { return now.Add(time.Hour) },
		IsHostAuthority: func(k ssh.PublicKey, _ string) bool { return bytes.Equal(k.Marshal(), ca.PublicKey().Marshal()) },
	}
	if err := checker.CheckHostKey("gate.example.com:22", nil, cert); err != nil {
		t.Fatalf("CheckHostKey (trusted CA, matching principal): %v", err)
	}
	// A different hostname must NOT be accepted (principal scoping holds).
	if err := checker.CheckHostKey("other.example.com:22", nil, cert); err == nil {
		t.Fatal("CheckHostKey accepted a hostname not in the cert principals")
	}
}