test(upgrade): exercise the real signed checksum flow, not a bypass
Some checks failed
CI / Test (push) Failing after 12m50s
CI / Build (push) Successful in 1m35s
CI / Build-1 (push) Successful in 1m58s
CI / Build-2 (push) Successful in 1m33s
CI / Build-3 (push) Successful in 1m33s
CI / Build-4 (push) Successful in 1m33s
CI / Build-5 (push) Successful in 1m34s
CI / Lint (push) Failing after 2m30s
CI / Coverage (push) Successful in 2m47s
CI / Vet (push) Successful in 1m59s

Supersedes the previous "disable signature verification" stop-gap. The two
checksum tests now run verifyChecksum with signature verification ENABLED using a
per-test ed25519 keypair (withReleasePubKey) and a matching checksums.txt.sig
served over the exact body — so they cover the real production path end to end
instead of skipping it. Adds verifyChecksum-level coverage for the cases that
actually protect a self-update: a checksums file signed by the wrong key is
rejected, a missing .sig is rejected, and verifyChecksumOnly (--allow-unsigned)
still passes on the checksum alone. No production code change.
This commit is contained in:
Deivid Soto 2026-06-04 08:47:24 +02:00
parent 86f03ba787
commit 3f22d698da

View file

@ -4,7 +4,10 @@ import (
"archive/tar" "archive/tar"
"compress/gzip" "compress/gzip"
"context" "context"
"crypto/ed25519"
"crypto/rand"
"crypto/sha256" "crypto/sha256"
"encoding/base64"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"net/http" "net/http"
@ -673,22 +676,35 @@ func TestDownloadWithHTTPTest(t *testing.T) {
}) })
} }
// signedChecksumsHandler serves checksums.txt (body) AND a matching
// checksums.txt.sig (base64 ed25519 signature over the exact body) so a test can
// drive verifyChecksum's real signature+checksum path with a controlled key.
func signedChecksumsHandler(body []byte, priv ed25519.PrivateKey) http.HandlerFunc {
sig := base64.StdEncoding.EncodeToString(ed25519.Sign(priv, body))
return func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "checksums.txt.sig") {
fmt.Fprintln(w, sig)
return
}
_, _ = w.Write(body)
}
}
func TestVerifyChecksumWithHTTPTest(t *testing.T) { func TestVerifyChecksumWithHTTPTest(t *testing.T) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
t.Skip("tar.gz test only on unix") t.Skip("tar.gz test only on unix")
} }
// This test predates release signing and exercises the checksum-MATCHING // Drive verifyChecksum's REAL flow: signature verification ENABLED with a test
// logic only. With the baked release pubkey set, verifyChecksum now requires a // keypair, then the SHA256 match. Each server serves a valid checksums.txt.sig
// valid checksums.txt.sig and fails at signature decode before reaching the // over the exact checksums body so the signature step passes and the
// SHA256 comparison these cases assert. Disable signature verification here // checksum-matching assertions are actually reached.
// (empty pubkey → loadReleasePubKey returns nil → step skipped); the signature pub, priv, err := ed25519.GenerateKey(rand.Reader)
// path has dedicated coverage in signature_test.go. Pattern mirrors that file. if err != nil {
prevPubKey := releasePubKeyBase64 t.Fatalf("generate keypair: %v", err)
releasePubKeyBase64 = "" }
t.Cleanup(func() { releasePubKeyBase64 = prevPubKey }) withReleasePubKey(t, base64.StdEncoding.EncodeToString(pub))
// Create a fake archive file
dir := t.TempDir() dir := t.TempDir()
archiveContent := []byte("archive-content-for-checksum-test") archiveContent := []byte("archive-content-for-checksum-test")
archivePath := filepath.Join(dir, "test-archive.tar.gz") archivePath := filepath.Join(dir, "test-archive.tar.gz")
@ -696,43 +712,31 @@ func TestVerifyChecksumWithHTTPTest(t *testing.T) {
h := sha256.Sum256(archiveContent) h := sha256.Sum256(archiveContent)
correctHash := hex.EncodeToString(h[:]) correctHash := hex.EncodeToString(h[:])
// archiveName() uses runtime.GOOS/GOARCH.
// The function builds the archive name using archiveName(), which uses runtime.GOOS/GOARCH.
expectedArchiveName := archiveName("1.0.0") expectedArchiveName := archiveName("1.0.0")
t.Run("matching checksum", func(t *testing.T) { serve := func(t *testing.T, h http.Handler) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(h)
fmt.Fprintf(w, "0000000000000000000000000000000000000000000000000000000000000000 other_file.tar.gz\n") t.Cleanup(srv.Close)
fmt.Fprintf(w, "%s %s\n", correctHash, expectedArchiveName) restore := swapHTTPClient(&http.Client{Transport: &rewriteTransport{url: srv.URL}})
})) t.Cleanup(restore)
defer srv.Close() }
restore := swapHTTPClient(&http.Client{ t.Run("matching checksum (valid signature)", func(t *testing.T) {
Transport: &rewriteTransport{url: srv.URL}, body := []byte(fmt.Sprintf("0000000000000000000000000000000000000000000000000000000000000000 other_file.tar.gz\n%s %s\n", correctHash, expectedArchiveName))
}) serve(t, signedChecksumsHandler(body, priv))
defer restore() if err := verifyChecksum(context.Background(), "1.0.0", archivePath); err != nil {
err := verifyChecksum(context.Background(), "1.0.0", archivePath)
if err != nil {
t.Errorf("verifyChecksum() = %v, want nil", err) t.Errorf("verifyChecksum() = %v, want nil", err)
} }
}) })
t.Run("mismatched checksum", func(t *testing.T) { t.Run("mismatched checksum", func(t *testing.T) {
wrongHash := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" wrongHash := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body := []byte(fmt.Sprintf("%s %s\n", wrongHash, expectedArchiveName))
fmt.Fprintf(w, "%s %s\n", wrongHash, expectedArchiveName) serve(t, signedChecksumsHandler(body, priv))
}))
defer srv.Close()
restore := swapHTTPClient(&http.Client{
Transport: &rewriteTransport{url: srv.URL},
})
defer restore()
err := verifyChecksum(context.Background(), "1.0.0", archivePath) err := verifyChecksum(context.Background(), "1.0.0", archivePath)
if err == nil { if err == nil {
t.Error("verifyChecksum() with wrong hash should return error") t.Fatal("verifyChecksum() with wrong hash should return error")
} }
if !strings.Contains(err.Error(), "SHA256 mismatch") { if !strings.Contains(err.Error(), "SHA256 mismatch") {
t.Errorf("verifyChecksum() error = %q, want to contain 'SHA256 mismatch'", err) t.Errorf("verifyChecksum() error = %q, want to contain 'SHA256 mismatch'", err)
@ -740,39 +744,74 @@ func TestVerifyChecksumWithHTTPTest(t *testing.T) {
}) })
t.Run("archive not in checksums", func(t *testing.T) { t.Run("archive not in checksums", func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body := []byte("abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 some_other_file.tar.gz\n")
fmt.Fprintf(w, "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890 some_other_file.tar.gz\n") serve(t, signedChecksumsHandler(body, priv))
}))
defer srv.Close()
restore := swapHTTPClient(&http.Client{
Transport: &rewriteTransport{url: srv.URL},
})
defer restore()
err := verifyChecksum(context.Background(), "1.0.0", archivePath) err := verifyChecksum(context.Background(), "1.0.0", archivePath)
if err == nil { if err == nil {
t.Error("verifyChecksum() with missing entry should return error") t.Fatal("verifyChecksum() with missing entry should return error")
} }
if !strings.Contains(err.Error(), "no checksum found") { if !strings.Contains(err.Error(), "no checksum found") {
t.Errorf("verifyChecksum() error = %q, want to contain 'no checksum found'", err) t.Errorf("verifyChecksum() error = %q, want to contain 'no checksum found'", err)
} }
}) })
t.Run("checksums server error", func(t *testing.T) { t.Run("bad signature rejected", func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, otherPriv, _ := ed25519.GenerateKey(rand.Reader)
w.WriteHeader(500) body := []byte(fmt.Sprintf("%s %s\n", correctHash, expectedArchiveName))
})) // Correct checksums, but signed by the WRONG key → must be rejected BEFORE
defer srv.Close() // any hash is trusted (the whole point of signing the checksums file).
serve(t, signedChecksumsHandler(body, otherPriv))
restore := swapHTTPClient(&http.Client{
Transport: &rewriteTransport{url: srv.URL},
})
defer restore()
err := verifyChecksum(context.Background(), "1.0.0", archivePath) err := verifyChecksum(context.Background(), "1.0.0", archivePath)
if err == nil { if err == nil {
t.Error("verifyChecksum() with server error should return error") t.Fatal("verifyChecksum() with bad signature should return error")
}
if !strings.Contains(err.Error(), "verify signature") {
t.Errorf("verifyChecksum() error = %q, want to contain 'verify signature'", err)
}
})
t.Run("missing signature rejected", func(t *testing.T) {
body := []byte(fmt.Sprintf("%s %s\n", correctHash, expectedArchiveName))
// 404 on .sig while a pubkey is configured → can't verify → hard fail.
serve(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "checksums.txt.sig") {
http.NotFound(w, r)
return
}
_, _ = w.Write(body)
}))
err := verifyChecksum(context.Background(), "1.0.0", archivePath)
if err == nil {
t.Fatal("verifyChecksum() with missing signature should return error")
}
if !strings.Contains(err.Error(), "verify signature") {
t.Errorf("verifyChecksum() error = %q, want to contain 'verify signature'", err)
}
})
t.Run("allow-unsigned skips signature", func(t *testing.T) {
body := []byte(fmt.Sprintf("%s %s\n", correctHash, expectedArchiveName))
// No .sig served — verifyChecksumOnly (the --allow-unsigned path) must still
// succeed on the checksum alone.
serve(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "checksums.txt.sig") {
http.NotFound(w, r)
return
}
_, _ = w.Write(body)
}))
if err := verifyChecksumOnly(context.Background(), "1.0.0", archivePath); err != nil {
t.Errorf("verifyChecksumOnly() = %v, want nil (signature skipped)", err)
}
})
t.Run("checksums server error", func(t *testing.T) {
serve(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
}))
err := verifyChecksum(context.Background(), "1.0.0", archivePath)
if err == nil {
t.Fatal("verifyChecksum() with server error should return error")
} }
if !strings.Contains(err.Error(), "HTTP 500") { if !strings.Contains(err.Error(), "HTTP 500") {
t.Errorf("verifyChecksum() error = %q, want to contain 'HTTP 500'", err) t.Errorf("verifyChecksum() error = %q, want to contain 'HTTP 500'", err)
@ -780,18 +819,9 @@ func TestVerifyChecksumWithHTTPTest(t *testing.T) {
}) })
t.Run("nonexistent archive file", func(t *testing.T) { t.Run("nonexistent archive file", func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body := []byte(fmt.Sprintf("%s %s\n", correctHash, expectedArchiveName))
fmt.Fprintf(w, "%s %s\n", correctHash, expectedArchiveName) serve(t, signedChecksumsHandler(body, priv))
})) if err := verifyChecksum(context.Background(), "1.0.0", "/nonexistent-archive-path"); err == nil {
defer srv.Close()
restore := swapHTTPClient(&http.Client{
Transport: &rewriteTransport{url: srv.URL},
})
defer restore()
err := verifyChecksum(context.Background(), "1.0.0", "/nonexistent-archive-path")
if err == nil {
t.Error("verifyChecksum() with nonexistent archive should return error") t.Error("verifyChecksum() with nonexistent archive should return error")
} }
}) })
@ -802,12 +832,12 @@ func TestVerifyChecksumCaseInsensitive(t *testing.T) {
t.Skip("tar.gz test only on unix") t.Skip("tar.gz test only on unix")
} }
// Predates release signing; tests checksum matching only. Disable signature // Real flow: signature ENABLED with a test key + a valid .sig over the body.
// verification (see TestVerifyChecksumWithHTTPTest) so it reaches the SHA256 pub, priv, err := ed25519.GenerateKey(rand.Reader)
// comparison instead of failing on the absent .sig. if err != nil {
prevPubKey := releasePubKeyBase64 t.Fatalf("generate keypair: %v", err)
releasePubKeyBase64 = "" }
t.Cleanup(func() { releasePubKeyBase64 = prevPubKey }) withReleasePubKey(t, base64.StdEncoding.EncodeToString(pub))
dir := t.TempDir() dir := t.TempDir()
archiveContent := []byte("case-insensitive-hash-test") archiveContent := []byte("case-insensitive-hash-test")
@ -818,10 +848,9 @@ func TestVerifyChecksumCaseInsensitive(t *testing.T) {
// Use uppercase hash in checksums.txt — verifyChecksum uses EqualFold // Use uppercase hash in checksums.txt — verifyChecksum uses EqualFold
upperHash := strings.ToUpper(hex.EncodeToString(h[:])) upperHash := strings.ToUpper(hex.EncodeToString(h[:]))
expectedArchiveName := archiveName("1.0.0") expectedArchiveName := archiveName("1.0.0")
body := []byte(fmt.Sprintf("%s %s\n", upperHash, expectedArchiveName))
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(signedChecksumsHandler(body, priv))
fmt.Fprintf(w, "%s %s\n", upperHash, expectedArchiveName)
}))
defer srv.Close() defer srv.Close()
restore := swapHTTPClient(&http.Client{ restore := swapHTTPClient(&http.Client{
@ -829,8 +858,7 @@ func TestVerifyChecksumCaseInsensitive(t *testing.T) {
}) })
defer restore() defer restore()
err := verifyChecksum(context.Background(), "1.0.0", archivePath) if err := verifyChecksum(context.Background(), "1.0.0", archivePath); err != nil {
if err != nil {
t.Errorf("verifyChecksum() with uppercase hash = %v, want nil", err) t.Errorf("verifyChecksum() with uppercase hash = %v, want nil", err)
} }
} }