| 1 | package publicrelease |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/ed25519" |
| 6 | "crypto/sha256" |
| 7 | "encoding/base64" |
| 8 | "encoding/hex" |
| 9 | "fmt" |
| 10 | "net/http" |
| 11 | "net/http/httptest" |
| 12 | "strings" |
| 13 | "testing" |
| 14 | ) |
| 15 | |
| 16 | func testFeed(t *testing.T, sign bool, assets map[string][]byte) (Feed, string) { |
| 17 | t.Helper() |
| 18 | pub, priv, _ := ed25519.GenerateKey(nil) |
| 19 | var sums strings.Builder |
| 20 | for name, body := range assets { |
| 21 | s := sha256.Sum256(body) |
| 22 | fmt.Fprintf(&sums, "%s %s\n", hex.EncodeToString(s[:]), name) |
| 23 | } |
| 24 | sig := ed25519.Sign(priv, []byte(sums.String())) |
| 25 | mux := http.NewServeMux() |
| 26 | mux.HandleFunc("/latest.txt", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "v0.18.0") }) |
| 27 | mux.HandleFunc("/dl/v0.18.0/SHA256SUMS.txt", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, sums.String()) }) |
| 28 | mux.HandleFunc("/dl/v0.18.0/SHA256SUMS.txt.sig", func(w http.ResponseWriter, r *http.Request) { |
| 29 | if sign { |
| 30 | fmt.Fprintln(w, base64.StdEncoding.EncodeToString(sig)) |
| 31 | } else { |
| 32 | fmt.Fprintln(w, base64.StdEncoding.EncodeToString(make([]byte, ed25519.SignatureSize))) |
| 33 | } |
| 34 | }) |
| 35 | for name, body := range assets { |
| 36 | body := body |
| 37 | mux.HandleFunc("/dl/v0.18.0/"+name, func(w http.ResponseWriter, r *http.Request) { w.Write(body) }) |
| 38 | } |
| 39 | ts := httptest.NewServer(mux) |
| 40 | t.Cleanup(ts.Close) |
| 41 | return Feed{LatestURL: ts.URL + "/latest.txt", DownloadBase: ts.URL + "/dl", PubKey: base64.StdEncoding.EncodeToString(pub)}, ts.URL |
| 42 | } |
| 43 | |
| 44 | func TestFeedLatestAndVerifiedAsset(t *testing.T) { |
| 45 | body := []byte("ELF arm64") |
| 46 | f, _ := testFeed(t, true, map[string][]byte{"carlos-linux-arm64": body, "carlos-linux-amd64": []byte("ELF amd64")}) |
| 47 | ctx := context.Background() |
| 48 | tag, err := f.Latest(ctx) |
| 49 | if err != nil || tag != "v0.18.0" { |
| 50 | t.Fatalf("latest = %q %v", tag, err) |
| 51 | } |
| 52 | data, sha, err := f.Asset(ctx, tag, AssetName("linux-arm64"), 1<<20) |
| 53 | if err != nil || string(data) != string(body) { |
| 54 | t.Fatalf("asset: %v", err) |
| 55 | } |
| 56 | s := sha256.Sum256(body) |
| 57 | if sha != hex.EncodeToString(s[:]) { |
| 58 | t.Fatalf("sha = %s", sha) |
| 59 | } |
| 60 | if _, _, err := f.Asset(ctx, tag, "carlos-windows-amd64.zip", 1<<20); err == nil { |
| 61 | t.Fatal("an asset absent from the sums was accepted") |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | func TestFeedRefusesABadSignatureAndATamperedAsset(t *testing.T) { |
| 66 | body := []byte("ELF") |
| 67 | f, _ := testFeed(t, false, map[string][]byte{"carlos-linux-arm64": body}) |
| 68 | if _, err := f.Sums(context.Background(), "v0.18.0"); err == nil || !strings.Contains(err.Error(), "signature") { |
| 69 | t.Fatalf("bad signature: %v", err) |
| 70 | } |
| 71 | // Signed sums, but the served asset differs from the sum. |
| 72 | f2, _ := testFeed(t, true, map[string][]byte{"carlos-linux-arm64": body}) |
| 73 | // Simulate tampering by asking for a limit smaller than the asset: the |
| 74 | // truncated download cannot match its sum. |
| 75 | if _, _, err := f2.Asset(context.Background(), "v0.18.0", "carlos-linux-arm64", 1); err == nil { |
| 76 | t.Fatal("a truncated asset matched its sum") |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | func TestFeedLatestRequiresATag(t *testing.T) { |
| 81 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "main") })) |
| 82 | defer ts.Close() |
| 83 | f := Feed{LatestURL: ts.URL, DownloadBase: ts.URL, PubKey: ""} |
| 84 | if _, err := f.Latest(context.Background()); err == nil { |
| 85 | t.Fatal("accepted a non-tag latest") |
| 86 | } |
| 87 | } |
| 88 | |