internal/cli/mint_test.go
Ref: Size: 6.1 KiB History
package cli
import (
"crypto/ed25519"
"crypto/rand"
"encoding/pem"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/a73x/eitri/internal/guest"
"golang.org/x/crypto/ssh"
)
// newCA writes an unencrypted ed25519 CA private key and returns its path
// and public key.
func newCA(t *testing.T) (string, ssh.PublicKey) {
t.Helper()
dir := t.TempDir()
caPath := filepath.Join(dir, "ca")
if err := EnsureKeypair(caPath); err != nil {
t.Fatal(err)
}
pub, err := os.ReadFile(caPath + ".pub")
if err != nil {
t.Fatal(err)
}
k, _, _, _, err := ssh.ParseAuthorizedKey(pub)
if err != nil {
t.Fatal(err)
}
return caPath, k
}
func TestEnsureKeypairGeneratesOnceAndParses(t *testing.T) {
key := filepath.Join(t.TempDir(), "id_ed25519")
if err := EnsureKeypair(key); err != nil {
t.Fatal(err)
}
priv1, err := os.ReadFile(key)
if err != nil {
t.Fatal(err)
}
if _, err := ssh.ParsePrivateKey(priv1); err != nil {
t.Fatalf("generated key unparsable: %v", err)
}
if fi, _ := os.Stat(key); fi.Mode().Perm() != 0o600 {
t.Errorf("key mode = %v", fi.Mode().Perm())
}
// Second call must not touch the existing key.
if err := EnsureKeypair(key); err != nil {
t.Fatal(err)
}
priv2, _ := os.ReadFile(key)
if string(priv1) != string(priv2) {
t.Error("EnsureKeypair overwrote an existing key")
}
}
func TestMintCertShape(t *testing.T) {
caPath, caPub := newCA(t)
key := filepath.Join(t.TempDir(), "id_ed25519")
if err := EnsureKeypair(key); err != nil {
t.Fatal(err)
}
if err := MintCert(caPath, key, "tester@box"); err != nil {
t.Fatal(err)
}
raw, err := os.ReadFile(key + "-cert.pub")
if err != nil {
t.Fatal(err)
}
k, _, _, _, err := ssh.ParseAuthorizedKey(raw)
if err != nil {
t.Fatal(err)
}
cert, ok := k.(*ssh.Certificate)
if !ok {
t.Fatalf("not a certificate: %T", k)
}
if cert.CertType != ssh.UserCert || cert.KeyId != "tester@box" {
t.Errorf("type/keyid: %v %q", cert.CertType, cert.KeyId)
}
if len(cert.ValidPrincipals) != 1 || cert.ValidPrincipals[0] != guest.LoginUser {
t.Errorf("principals: %v", cert.ValidPrincipals)
}
now := time.Now().Unix()
if int64(cert.ValidAfter) > now || int64(cert.ValidBefore) < now+25*60 || int64(cert.ValidBefore) > now+35*60 {
t.Errorf("validity window: after=%d before=%d now=%d", cert.ValidAfter, cert.ValidBefore, now)
}
for _, ext := range []string{"permit-X11-forwarding", "permit-agent-forwarding", "permit-port-forwarding", "permit-pty", "permit-user-rc"} {
if _, ok := cert.Permissions.Extensions[ext]; !ok {
t.Errorf("missing extension %s (pty allocation breaks without defaults)", ext)
}
}
// Signed by OUR CA.
checker := ssh.CertChecker{IsUserAuthority: func(a ssh.PublicKey) bool {
return strings.TrimSpace(string(ssh.MarshalAuthorizedKey(a))) ==
strings.TrimSpace(string(ssh.MarshalAuthorizedKey(caPub)))
}}
if err := checker.CheckCert(guest.LoginUser, cert); err != nil {
t.Errorf("cert not accepted by its own CA: %v", err)
}
}
func TestMintCertMissingCA(t *testing.T) {
key := filepath.Join(t.TempDir(), "id_ed25519")
if err := EnsureKeypair(key); err != nil {
t.Fatal(err)
}
err := MintCert(filepath.Join(t.TempDir(), "nope"), key, "x")
if err == nil || !strings.Contains(err.Error(), "eitri init") {
t.Fatalf("want actionable no-CA error, got %v", err)
}
}
func TestMintCertDerivesPubFromPrivateKey(t *testing.T) {
caPath, _ := newCA(t)
key := filepath.Join(t.TempDir(), "id_ed25519")
if err := EnsureKeypair(key); err != nil {
t.Fatal(err)
}
// A stale or missing .pub must not matter: the mint derives from the key.
if err := os.Remove(key + ".pub"); err != nil {
t.Fatal(err)
}
if err := MintCert(caPath, key, "x"); err != nil {
t.Fatalf("mint with missing .pub: %v", err)
}
raw, err := os.ReadFile(key + "-cert.pub")
if err != nil {
t.Fatal(err)
}
k, _, _, _, err := ssh.ParseAuthorizedKey(raw)
if err != nil {
t.Fatal(err)
}
cert := k.(*ssh.Certificate)
priv, _ := os.ReadFile(key)
signer, _ := ssh.ParsePrivateKey(priv)
if string(cert.Key.Marshal()) != string(signer.PublicKey().Marshal()) {
t.Error("cert key does not match the private key on disk")
}
}
func TestMintCertEncryptedCANonInteractive(t *testing.T) {
dir := t.TempDir()
caPath := filepath.Join(dir, "ca")
_, priv, err := ed25519GenerateForTest()
if err != nil {
t.Fatal(err)
}
block, err := ssh.MarshalPrivateKeyWithPassphrase(priv, "", []byte("secret"))
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(caPath, pem.EncodeToMemory(block), 0o600); err != nil {
t.Fatal(err)
}
key := filepath.Join(dir, "id_ed25519")
if err := EnsureKeypair(key); err != nil {
t.Fatal(err)
}
// Non-interactive stdin (go test): must fail fast with a helpful error,
// never hang on a passphrase prompt.
err = MintCert(caPath, key, "x")
if err == nil || !strings.Contains(err.Error(), "passphrase") {
t.Fatalf("want passphrase-protected error, got %v", err)
}
}
func TestMintCertEncryptedUserKeyUsesEmbeddedPublicKey(t *testing.T) {
caPath, _ := newCA(t)
dir := t.TempDir()
key := filepath.Join(dir, "id_ed25519")
pub, priv, err := ed25519GenerateForTest()
if err != nil {
t.Fatal(err)
}
// Passphrase-encrypted USER key: minting must still work — the openssh
// format embeds the public key unencrypted, and only the public half is
// needed (ssh prompts for the passphrase itself at connect time).
block, err := ssh.MarshalPrivateKeyWithPassphrase(priv, "", []byte("secret"))
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(key, pem.EncodeToMemory(block), 0o600); err != nil {
t.Fatal(err)
}
if err := MintCert(caPath, key, "x"); err != nil {
t.Fatalf("mint with encrypted user key: %v", err)
}
raw, err := os.ReadFile(key + "-cert.pub")
if err != nil {
t.Fatal(err)
}
k, _, _, _, err := ssh.ParseAuthorizedKey(raw)
if err != nil {
t.Fatal(err)
}
sshPub, err := ssh.NewPublicKey(pub)
if err != nil {
t.Fatal(err)
}
if string(k.(*ssh.Certificate).Key.Marshal()) != string(sshPub.Marshal()) {
t.Error("cert key does not match the encrypted private key's public half")
}
}
func ed25519GenerateForTest() (ed25519.PublicKey, ed25519.PrivateKey, error) {
return ed25519.GenerateKey(rand.Reader)
}