313 lines
10 KiB
Go
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")
|
|
}
|
|
})
|
|
}
|