Files
RenewCMS/test/releasesign_test.go
T

313 lines
10 KiB
Go

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