Files
go-synapse-backupper/cmd/synapse-backupper/keygen_test.go
T

445 lines
12 KiB
Go

package main
import (
"bytes"
"encoding/pem"
"os"
"path/filepath"
"testing"
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/keymanager"
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/mlkem768"
"git.tswf.io/infra/go-synapse-backupper/pkg/adapters/crypto/x25519"
"git.tswf.io/infra/go-synapse-backupper/pkg/domain/crypto"
)
func makeRegistry(t *testing.T) crypto.Registry {
t.Helper()
reg := crypto.NewRegistry()
if err := reg.Register(0x0006, func() crypto.KEM { return mlkem768.New() }); err != nil {
t.Fatalf("register mlkem768: %v", err)
}
if err := reg.Register(0x0007, func() crypto.KEM { return x25519.New() }); err != nil {
t.Fatalf("register x25519: %v", err)
}
return reg
}
func TestKeygenCmdFlags(t *testing.T) {
cmd := newKeygenCmd()
// Verify flags exist and have correct defaults.
if cmd.Flag("type") == nil {
t.Fatal("missing --type flag")
}
if cmd.Flag("type").DefValue != "both" {
t.Errorf("--type default = %q, want both", cmd.Flag("type").DefValue)
}
if cmd.Flag("out-prefix") == nil {
t.Fatal("missing --out-prefix flag")
}
if cmd.Flag("out-prefix").DefValue != "" {
t.Errorf("--out-prefix default = %q, want empty", cmd.Flag("out-prefix").DefValue)
}
if cmd.Flag("force") == nil {
t.Fatal("missing --force flag")
}
if cmd.Flag("force").DefValue != "false" {
t.Errorf("--force default = %q, want false", cmd.Flag("force").DefValue)
}
}
func TestKeygenCmdInvalidType(t *testing.T) {
cmd := newKeygenCmd()
cmd.SetArgs([]string{"--type", "invalid", "--out-prefix", filepath.Join(t.TempDir(), "keys")})
err := cmd.Execute()
if err == nil {
t.Fatal("expected error for invalid --type")
}
}
func TestKeygenCmdBoth(t *testing.T) {
reg := makeRegistry(t)
dir := t.TempDir()
prefix := filepath.Join(dir, "keys")
cmd := newKeygenCmdWithDeps(reg)
cmd.SetArgs([]string{"--type", "both", "--out-prefix", prefix})
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute failed: %v", err)
}
// Verify all 4 files exist.
files := []string{
prefix + ".pq.pub.pem",
prefix + ".pq.priv.pem",
prefix + ".classical.pub.pem",
prefix + ".classical.priv.pem",
}
for _, f := range files {
if _, err := os.Stat(f); err != nil {
t.Errorf("expected file %s to exist: %v", f, err)
}
}
}
func TestKeygenCmdPEMTypes(t *testing.T) {
reg := makeRegistry(t)
dir := t.TempDir()
prefix := filepath.Join(dir, "keys")
cmd := newKeygenCmdWithDeps(reg)
cmd.SetArgs([]string{"--type", "both", "--out-prefix", prefix})
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute failed: %v", err)
}
tests := []struct {
path string
wantType string
wantKeyLen int
}{
{prefix + ".pq.pub.pem", "ML-KEM-768 PUBLIC KEY", 1184},
{prefix + ".pq.priv.pem", "ML-KEM-768 PRIVATE KEY", 64},
{prefix + ".classical.pub.pem", "X25519 PUBLIC KEY", 32},
{prefix + ".classical.priv.pem", "X25519 PRIVATE KEY", 32},
}
for _, tt := range tests {
data, err := os.ReadFile(tt.path)
if err != nil {
t.Fatalf("read %s: %v", tt.path, err)
}
block, _ := pem.Decode(data)
if block == nil {
t.Fatalf("failed to decode PEM from %s", tt.path)
}
if block.Type != tt.wantType {
t.Errorf("%s PEM type = %q, want %q", tt.path, block.Type, tt.wantType)
}
if len(block.Bytes) != tt.wantKeyLen {
t.Errorf("%s key len = %d, want %d", tt.path, len(block.Bytes), tt.wantKeyLen)
}
}
}
func TestKeygenCmdRoundTrip(t *testing.T) {
reg := makeRegistry(t)
dir := t.TempDir()
prefix := filepath.Join(dir, "keys")
cmd := newKeygenCmdWithDeps(reg)
cmd.SetArgs([]string{"--type", "both", "--out-prefix", prefix})
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute failed: %v", err)
}
km := keymanager.NewKeyManager(reg)
schemes := []struct {
pubPath string
privPath string
schemeID uint16
pubLen int
privLen int
}{
{prefix + ".pq.pub.pem", prefix + ".pq.priv.pem", 0x0006, 1184, 64},
{prefix + ".classical.pub.pem", prefix + ".classical.priv.pem", 0x0007, 32, 32},
}
for _, s := range schemes {
pub, err := km.LoadPub(s.pubPath, s.schemeID)
if err != nil {
t.Fatalf("LoadPub %s: %v", s.pubPath, err)
}
priv, err := km.LoadPriv(s.privPath, s.schemeID)
if err != nil {
t.Fatalf("LoadPriv %s: %v", s.privPath, err)
}
if pub.SchemeID() != s.schemeID {
t.Errorf("pub.SchemeID() = 0x%04x, want 0x%04x", pub.SchemeID(), s.schemeID)
}
if priv.SchemeID() != s.schemeID {
t.Errorf("priv.SchemeID() = 0x%04x, want 0x%04x", priv.SchemeID(), s.schemeID)
}
if len(pub.Raw()) != s.pubLen {
t.Errorf("pub.Raw() len = %d, want %d", len(pub.Raw()), s.pubLen)
}
if len(priv.Raw()) != s.privLen {
t.Errorf("priv.Raw() len = %d, want %d", len(priv.Raw()), s.privLen)
}
if len(pub.KeyID()) != 8 {
t.Errorf("pub.KeyID() len = %d, want 8", len(pub.KeyID()))
}
if len(priv.KeyID()) != 8 {
t.Errorf("priv.KeyID() len = %d, want 8", len(priv.KeyID()))
}
// Verify the raw bytes round-trip correctly by checking the PEM
// contents match what LoadPub/LoadPriv return.
pubPEM, err := os.ReadFile(s.pubPath)
if err != nil {
t.Fatalf("read pub PEM: %v", err)
}
pubBlock, _ := pem.Decode(pubPEM)
if pubBlock == nil {
t.Fatal("failed to decode pub PEM")
}
if !bytes.Equal(pub.Raw(), pubBlock.Bytes) {
t.Errorf("pub.Raw() does not match PEM bytes")
}
privPEM, err := os.ReadFile(s.privPath)
if err != nil {
t.Fatalf("read priv PEM: %v", err)
}
privBlock, _ := pem.Decode(privPEM)
if privBlock == nil {
t.Fatal("failed to decode priv PEM")
}
if !bytes.Equal(priv.Raw(), privBlock.Bytes) {
t.Errorf("priv.Raw() does not match PEM bytes")
}
}
}
func TestKeygenCmdPQOnly(t *testing.T) {
reg := makeRegistry(t)
dir := t.TempDir()
prefix := filepath.Join(dir, "keys")
cmd := newKeygenCmdWithDeps(reg)
cmd.SetArgs([]string{"--type", "pq", "--out-prefix", prefix})
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute failed: %v", err)
}
if _, err := os.Stat(prefix + ".pq.pub.pem"); err != nil {
t.Errorf("expected pq.pub.pem to exist: %v", err)
}
if _, err := os.Stat(prefix + ".pq.priv.pem"); err != nil {
t.Errorf("expected pq.priv.pem to exist: %v", err)
}
if _, err := os.Stat(prefix + ".classical.pub.pem"); !os.IsNotExist(err) {
t.Errorf("expected classical.pub.pem to NOT exist")
}
if _, err := os.Stat(prefix + ".classical.priv.pem"); !os.IsNotExist(err) {
t.Errorf("expected classical.priv.pem to NOT exist")
}
}
func TestKeygenCmdClassicalOnly(t *testing.T) {
reg := makeRegistry(t)
dir := t.TempDir()
prefix := filepath.Join(dir, "keys")
cmd := newKeygenCmdWithDeps(reg)
cmd.SetArgs([]string{"--type", "classical", "--out-prefix", prefix})
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute failed: %v", err)
}
if _, err := os.Stat(prefix + ".classical.pub.pem"); err != nil {
t.Errorf("expected classical.pub.pem to exist: %v", err)
}
if _, err := os.Stat(prefix + ".classical.priv.pem"); err != nil {
t.Errorf("expected classical.priv.pem to exist: %v", err)
}
if _, err := os.Stat(prefix + ".pq.pub.pem"); !os.IsNotExist(err) {
t.Errorf("expected pq.pub.pem to NOT exist")
}
if _, err := os.Stat(prefix + ".pq.priv.pem"); !os.IsNotExist(err) {
t.Errorf("expected pq.priv.pem to NOT exist")
}
}
func TestKeygenCmdPermissions(t *testing.T) {
reg := makeRegistry(t)
dir := t.TempDir()
prefix := filepath.Join(dir, "keys")
cmd := newKeygenCmdWithDeps(reg)
cmd.SetArgs([]string{"--type", "both", "--out-prefix", prefix})
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute failed: %v", err)
}
pubFiles := []string{prefix + ".pq.pub.pem", prefix + ".classical.pub.pem"}
for _, f := range pubFiles {
info, err := os.Stat(f)
if err != nil {
t.Fatalf("stat %s: %v", f, err)
}
mode := info.Mode().Perm()
if mode != 0o644 {
t.Errorf("%s permissions = 0%o, want 0644", f, mode)
}
}
privFiles := []string{prefix + ".pq.priv.pem", prefix + ".classical.priv.pem"}
for _, f := range privFiles {
info, err := os.Stat(f)
if err != nil {
t.Fatalf("stat %s: %v", f, err)
}
mode := info.Mode().Perm()
if mode != 0o600 {
t.Errorf("%s permissions = 0%o, want 0600", f, mode)
}
}
}
func TestKeygenCmdNoOverwrite(t *testing.T) {
reg := makeRegistry(t)
dir := t.TempDir()
prefix := filepath.Join(dir, "keys")
// Create an existing file.
existing := prefix + ".pq.pub.pem"
if err := os.WriteFile(existing, []byte("existing"), 0o644); err != nil {
t.Fatalf("write existing file: %v", err)
}
cmd := newKeygenCmdWithDeps(reg)
cmd.SetArgs([]string{"--type", "both", "--out-prefix", prefix})
err := cmd.Execute()
if err == nil {
t.Fatal("expected error when file exists without --force")
}
// Verify existing file was not overwritten.
data, err := os.ReadFile(existing)
if err != nil {
t.Fatalf("read existing file: %v", err)
}
if !bytes.Equal(data, []byte("existing")) {
t.Error("existing file was overwritten without --force")
}
}
func TestKeygenCmdForceOverwrite(t *testing.T) {
reg := makeRegistry(t)
dir := t.TempDir()
prefix := filepath.Join(dir, "keys")
// Create an existing file.
existing := prefix + ".pq.pub.pem"
if err := os.WriteFile(existing, []byte("existing"), 0o644); err != nil {
t.Fatalf("write existing file: %v", err)
}
cmd := newKeygenCmdWithDeps(reg)
cmd.SetArgs([]string{"--type", "both", "--out-prefix", prefix, "--force"})
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute failed: %v", err)
}
// Verify file was overwritten with valid PEM.
data, err := os.ReadFile(existing)
if err != nil {
t.Fatalf("read overwritten file: %v", err)
}
block, _ := pem.Decode(data)
if block == nil {
t.Fatal("overwritten file is not valid PEM")
}
if block.Type != "ML-KEM-768 PUBLIC KEY" {
t.Errorf("overwritten PEM type = %q, want ML-KEM-768 PUBLIC KEY", block.Type)
}
}
func TestKeygenCmdKeyIDConsistency(t *testing.T) {
reg := makeRegistry(t)
dir := t.TempDir()
prefix := filepath.Join(dir, "keys")
cmd := newKeygenCmdWithDeps(reg)
cmd.SetArgs([]string{"--type", "both", "--out-prefix", prefix})
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute failed: %v", err)
}
km := keymanager.NewKeyManager(reg)
schemes := []struct {
pubPath string
privPath string
schemeID uint16
pubLen int
privLen int
}{
{prefix + ".pq.pub.pem", prefix + ".pq.priv.pem", 0x0006, 1184, 64},
{prefix + ".classical.pub.pem", prefix + ".classical.priv.pem", 0x0007, 32, 32},
}
for _, s := range schemes {
pub, err := km.LoadPub(s.pubPath, s.schemeID)
if err != nil {
t.Fatalf("LoadPub %s: %v", s.pubPath, err)
}
priv, err := km.LoadPriv(s.privPath, s.schemeID)
if err != nil {
t.Fatalf("LoadPriv %s: %v", s.privPath, err)
}
if len(pub.Raw()) != s.pubLen {
t.Errorf("pub %s raw len = %d, want %d", s.pubPath, len(pub.Raw()), s.pubLen)
}
if len(priv.Raw()) != s.privLen {
t.Errorf("priv %s raw len = %d, want %d", s.privPath, len(priv.Raw()), s.privLen)
}
// KeyID must be present and 8 bytes for both pub and priv.
if len(pub.KeyID()) != 8 {
t.Errorf("pub %s KeyID len = %d, want 8", s.pubPath, len(pub.KeyID()))
}
if len(priv.KeyID()) != 8 {
t.Errorf("priv %s KeyID len = %d, want 8", s.privPath, len(priv.KeyID()))
}
}
}
func TestKeygenCmdEmptyPrefix(t *testing.T) {
reg := makeRegistry(t)
dir := t.TempDir()
cmd := newKeygenCmdWithDeps(reg)
cmd.SetArgs([]string{"--type", "pq"})
cmd.SetOut(nil)
cmd.SetErr(nil)
// Change working directory to temp dir so empty prefix creates files there.
origWd, _ := os.Getwd()
if err := os.Chdir(dir); err != nil {
t.Fatalf("Chdir(%q): %v", dir, err)
}
defer func() {
if err := os.Chdir(origWd); err != nil {
t.Fatalf("Chdir(%q): %v", origWd, err)
}
}()
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute failed: %v", err)
}
if _, err := os.Stat(filepath.Join(dir, ".pq.pub.pem")); err != nil {
t.Errorf("expected .pq.pub.pem to exist: %v", err)
}
if _, err := os.Stat(filepath.Join(dir, ".pq.priv.pem")); err != nil {
t.Errorf("expected .pq.priv.pem to exist: %v", err)
}
}