feat: signed releases, release feed, notification (read-only)
This commit is contained in:
1 parent
a49eec909c
commit
834493a4f5
49 files changed
+4191
-18
No files matched your search
@@ -0,0 +1,312 @@
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user