package test import ( "RenewCMS/internal/infrastructure/update/releasesign" "bytes" "crypto/sha256" "encoding/hex" "os" "path/filepath" "strings" "testing" ) const placeholderPublicKey = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" type signingKeyPair struct { privatePath string publicPath string } func runReleaseSign(args ...string) (int, string) { var stdout, stderr bytes.Buffer code := releasesign.Run(args, &stdout, &stderr) return code, stdout.String() + stderr.String() } func newSigningKeyPair(t *testing.T, dir string) signingKeyPair { t.Helper() pair := signingKeyPair{privatePath: filepath.Join(dir, "release.key"), publicPath: filepath.Join(dir, "release.pub")} if code, output := runReleaseSign("keygen", pair.privatePath, "--pub-out", pair.publicPath); code != 0 { t.Fatalf("keygen failed: %s", output) } return pair } func writeChecksums(t *testing.T, dir string) string { t.Helper() path := filepath.Join(dir, "checksums.txt") if err := os.WriteFile(path, []byte("abc renewcms_v1_linux_amd64\ndef renewcms_v1_darwin_arm64\n"), 0o600); err != nil { t.Fatal(err) } return path } func TestReleaseSigningRoundTrip(t *testing.T) { t.Run("Release signing round trip with a throwaway key", func(t *testing.T) { dir := t.TempDir() pair := newSigningKeyPair(t, dir) file := writeChecksums(t, dir) if code, output := runReleaseSign("sign", file, "--key", pair.privatePath, "--pub", pair.publicPath); code != 0 { t.Fatalf("sign failed: %s", output) } if code, output := runReleaseSign("verify", file, "--pub", pair.publicPath); code != 0 { t.Fatalf("verify failed: %s", output) } content, _ := os.ReadFile(file) content[0] ^= 0x01 if err := os.WriteFile(file, content, 0o600); err != nil { t.Fatal(err) } code, output := runReleaseSign("verify", file, "--pub", pair.publicPath) if code == 0 || !strings.Contains(output, "signature does not match") { t.Errorf("a changed file must fail verify with a clear message: %d %q", code, output) } }) t.Run("verify with a different public key fails", func(t *testing.T) { dir := t.TempDir() pair := newSigningKeyPair(t, dir) other := newSigningKeyPair(t, t.TempDir()) file := writeChecksums(t, dir) runReleaseSign("sign", file, "--key", pair.privatePath, "--pub", pair.publicPath) if code, _ := runReleaseSign("verify", file, "--pub", other.publicPath); code == 0 { t.Error("verify with another public key must fail") } }) t.Run("sign takes the private key from RELEASE_SIGNING_KEY", func(t *testing.T) { dir := t.TempDir() pair := newSigningKeyPair(t, dir) file := writeChecksums(t, dir) t.Setenv("RELEASE_SIGNING_KEY", pair.privatePath) if code, output := runReleaseSign("sign", file, "--pub", pair.publicPath); code != 0 { t.Fatalf("sign failed: %s", output) } if code, output := runReleaseSign("verify", file, "--pub", pair.publicPath); code != 0 { t.Fatalf("verify failed: %s", output) } }) t.Run("sign without any key says so", func(t *testing.T) { dir := t.TempDir() pair := newSigningKeyPair(t, dir) t.Setenv("RELEASE_SIGNING_KEY", "") code, output := runReleaseSign("sign", writeChecksums(t, dir), "--pub", pair.publicPath) if code == 0 || !strings.Contains(output, "RELEASE_SIGNING_KEY") { t.Errorf("code = %d output = %q", code, output) } }) t.Run("sign with a missing key file names the path", func(t *testing.T) { dir := t.TempDir() pair := newSigningKeyPair(t, dir) missing := filepath.Join(dir, "missing.key") code, output := runReleaseSign("sign", writeChecksums(t, dir), "--key", missing, "--pub", pair.publicPath) if code == 0 || !strings.Contains(output, missing) { t.Errorf("code = %d output = %q", code, output) } }) } func TestReleaseSignKeygen(t *testing.T) { t.Run("keygen refuses a path inside the repository", func(t *testing.T) { repoRoot, err := filepath.Abs("..") if err != nil { t.Fatal(err) } inside := filepath.Join(repoRoot, "tmp", "releasesign-test.key") t.Cleanup(func() { os.Remove(inside) }) code, output := runReleaseSign("keygen", inside) if code == 0 || !strings.Contains(output, "inside the repository") { t.Errorf("code = %d output = %q", code, output) } if _, err := os.Stat(inside); err == nil { t.Error("nothing may be written inside the repository") } }) t.Run("keygen never overwrites an existing key", func(t *testing.T) { dir := t.TempDir() pair := newSigningKeyPair(t, dir) before, _ := os.ReadFile(pair.privatePath) code, _ := runReleaseSign("keygen", pair.privatePath) after, _ := os.ReadFile(pair.privatePath) if code == 0 || !bytes.Equal(before, after) { t.Errorf("code = %d, key changed = %v", code, !bytes.Equal(before, after)) } }) t.Run("keygen prints the public key and where to commit it", func(t *testing.T) { dir := t.TempDir() code, output := runReleaseSign("keygen", filepath.Join(dir, "k")) if code != 0 || !strings.Contains(output, "internal/infrastructure/update/release.pub") { t.Errorf("code = %d output = %q", code, output) } }) } func TestReleaseSignPlaceholderKey(t *testing.T) { t.Run("The placeholder public key is refused", func(t *testing.T) { dir := t.TempDir() pair := newSigningKeyPair(t, dir) placeholder := filepath.Join(dir, "placeholder.pub") if err := os.WriteFile(placeholder, []byte(placeholderPublicKey+"\n"), 0o600); err != nil { t.Fatal(err) } file := writeChecksums(t, dir) signCode, signOutput := runReleaseSign("sign", file, "--key", pair.privatePath, "--pub", placeholder) verifyCode, verifyOutput := runReleaseSign("verify", file, "--pub", placeholder) for _, output := range []string{signOutput, verifyOutput} { if !strings.Contains(output, "placeholder public key has not been replaced") { t.Errorf("output = %q", output) } } if signCode == 0 || verifyCode == 0 { t.Errorf("sign = %d verify = %d", signCode, verifyCode) } if _, err := os.Stat(file + ".sig"); err == nil { t.Error("no .sig may be written") } }) t.Run("a malformed public key is refused", func(t *testing.T) { dir := t.TempDir() bad := filepath.Join(dir, "bad.pub") _ = os.WriteFile(bad, []byte("not base64!"), 0o600) if code, _ := runReleaseSign("verify", writeChecksums(t, dir), "--pub", bad); code == 0 { t.Error("a malformed key must fail") } }) } func TestReleaseSignKeyMismatch(t *testing.T) { t.Run("sign refuses a private key that does not match the public key", func(t *testing.T) { dir := t.TempDir() signer := newSigningKeyPair(t, dir) other := newSigningKeyPair(t, t.TempDir()) file := writeChecksums(t, dir) code, output := runReleaseSign("sign", file, "--key", signer.privatePath, "--pub", other.publicPath) if code == 0 || !strings.Contains(output, "does not match") { t.Errorf("code = %d output = %q", code, output) } if _, err := os.Stat(file + ".sig"); err == nil { t.Error("no .sig may be written") } }) } func TestReleaseSignUsage(t *testing.T) { for name, args := range map[string][]string{ "no command": nil, "unknown command": {"explode"}, "missing file": {"verify"}, "unknown flag": {"verify", "x", "--nope", "1"}, } { t.Run(name, func(t *testing.T) { if code, _ := runReleaseSign(args...); code == 0 { t.Error("expected a non-zero exit") } }) } } func TestReleaseSignChecksums(t *testing.T) { dir := t.TempDir() for name, content := range map[string]string{ "renewcms_v2.5.0_linux_amd64": "linux", "renewcms_v2.5.0_windows_amd64.exe": "windows", "renewcms_v2.4.0_linux_amd64": "old", } { if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o600); err != nil { t.Fatal(err) } } t.Run("writes the SHA-256 of the matching binaries only", func(t *testing.T) { code, output := runReleaseSign("checksums", dir, "--match", "renewcms_v2.5.0_*") if code != 0 { t.Fatalf("checksums failed: %s", output) } content, err := os.ReadFile(filepath.Join(dir, "checksums.txt")) if err != nil { t.Fatal(err) } lines := strings.Split(strings.TrimSpace(string(content)), "\n") if len(lines) != 2 || !strings.HasSuffix(lines[0], " renewcms_v2.5.0_linux_amd64") || !strings.HasSuffix(lines[1], " renewcms_v2.5.0_windows_amd64.exe") { t.Fatalf("checksums.txt = %q", content) } sum := sha256.Sum256([]byte("linux")) if want := hex.EncodeToString(sum[:]) + " renewcms_v2.5.0_linux_amd64"; lines[0] != want { t.Errorf("line = %q, want %q", lines[0], want) } }) t.Run("a second run does not list checksums.txt or its signature", func(t *testing.T) { runReleaseSign("checksums", dir, "--match", "*") content, _ := os.ReadFile(filepath.Join(dir, "checksums.txt")) if strings.Contains(string(content), "checksums.txt") { t.Errorf("checksums.txt lists itself: %q", content) } }) t.Run("no match is an error", func(t *testing.T) { if code, _ := runReleaseSign("checksums", dir, "--match", "nothing_*"); code == 0 { t.Error("expected a non-zero exit") } }) } func TestReleaseSignFailsOnUnwritableOutputs(t *testing.T) { t.Run("keygen reports a public key that cannot be written", func(t *testing.T) { dir := t.TempDir() code, _ := runReleaseSign("keygen", filepath.Join(dir, "release.key"), "--pub-out", filepath.Join(dir, "missing", "release.pub")) if code == 0 { t.Fatal("keygen exited 0 although the public key could not be written") } }) t.Run("keygen reports a trailing flag with no value without panicking", func(t *testing.T) { code, _ := runReleaseSign("keygen", filepath.Join(t.TempDir(), "release.key"), "--pub-out") if code == 0 { t.Fatal("keygen exited 0 for a flag with no value") } }) t.Run("sign reports a signature that cannot be written", func(t *testing.T) { dir := t.TempDir() pair := newSigningKeyPair(t, dir) file := writeChecksums(t, dir) if err := os.Mkdir(file+".sig", 0o700); err != nil { t.Fatal(err) } if code, _ := runReleaseSign("sign", file, "--key", pair.privatePath, "--pub", pair.publicPath); code == 0 { t.Fatal("sign exited 0 although the signature could not be written") } }) t.Run("checksums reports a checksums file that cannot be written", func(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "a.bin"), []byte("a"), 0o600); err != nil { t.Fatal(err) } if err := os.Mkdir(filepath.Join(dir, "checksums.txt"), 0o700); err != nil { t.Fatal(err) } if code, _ := runReleaseSign("checksums", dir, "--match", "*.bin"); code == 0 { t.Fatal("checksums exited 0 although checksums.txt could not be written") } }) }