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
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:
parent
86f03ba787
commit
3f22d698da
1 changed files with 110 additions and 82 deletions
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue