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")
}
}