Repository: tetratelabs/getenvoy
Pull request #281 (2021-06-16 03:35:12)

Refactors JSON schema and adds SHA256 verification of tarballs
This change allows us increased confidence that tarballs downloaded were
what we expected. Notably, `getenvoy use N.N.N` will fail now, if the
`sha256sum` doesn't match what's in the json key for that filename.

This refactors out Envoy-specific terminology from the JSON schema used
to validate envoy-versions.json. It is now called
release-versions-schema.json.

This adds a new key sha256sums in the schema and backfills data for the
existing versions.

The key name "sha256sums" comes from the `sha256sum` command. This is
SHA-256 instead of SHA-512 as it is a ubuquitous hash algo, for example,
used by default also in goreleaser.

This doesn't use a more generic term like "hash" because doing so would
cause unnecessary confusion on what hash it is (ex MD-5?).

The map "sha256sums" allows for easy indexing, and using the filename
instead of the URL allows less changes when rehosting the exact same
files.

Signed-off-by: Adrian Cole <adrian@tetrate.io>


Patch:
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index 712ff5e..3620c3c 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -17,7 +17,7 @@ project_name: getenvoy
 builds:
   - binary: getenvoy
     # Updates the version so that `getenvoy -v` prints out the release version instead of "dev"
-    ldflags: "-s -w -X github.com/tetratelabs/getenvoy/internal/version.GetEnvoy={{.Version}}"
+    ldflags: "-s -w -X github.com/tetratelabs/getenvoy/internal/version.getEnvoy={{.Version}}"
     main: .
     env:
       - CGO_ENABLED=0
diff --git a/internal/cmd/app.go b/internal/cmd/app.go
index 1339eaf..6d19093 100644
--- a/internal/cmd/app.go
+++ b/internal/cmd/app.go
@@ -47,7 +47,7 @@ func NewApp(o *globals.GlobalOpts) *cli.App {
    You may want to override ` + "`$ENVOY_VERSIONS_URL`" + ` to supply custom builds or
    otherwise control the source of Envoy binaries. When overriding, validate
    your JSON first: https://getenvoy.io/envoy-versions-schema.json`
-	app.Version = version.GetEnvoy
+	app.Version = string(version.GetEnvoy)
 	app.Flags = []cli.Flag{
 		&cli.StringFlag{
 			Name:        "home-dir",
diff --git a/internal/cmd/run.go b/internal/cmd/run.go
index 5549220..ebe96e9 100644
--- a/internal/cmd/run.go
+++ b/internal/cmd/run.go
@@ -33,7 +33,7 @@ import (
 
 // NewRunCmd create a command responsible for starting an Envoy process
 func NewRunCmd(o *globals.GlobalOpts) *cli.Command {
-	var envoyVersion string
+	var envoyVersion version.Version
 	cmd := &cli.Command{
 		Name:            "run",
 		Usage:           "Run Envoy with the given [arguments...] until interrupted",
@@ -111,7 +111,7 @@ state. On exit, these archive into ` + "`$GETENVOY_HOME/runs/$epochtime.tar.gz`"
 // initializeRunOpts allows us to default values when not overridden for tests.
 // The version parameter correlates with the globals.GlobalOpts EnvoyPath which is installed if needed.
 // Notably, this creates and sets a globals.GlobalOpts WorkingDirectory for Envoy, and any files that precede it.
-func initializeRunOpts(ctx context.Context, o *globals.GlobalOpts, p, v string) error {
+func initializeRunOpts(ctx context.Context, o *globals.GlobalOpts, p version.Platform, v version.Version) error {
 	runOpts := &o.RunOpts
 	if o.EnvoyPath == "" { // not overridden for tests
 		envoyPath, err := envoy.InstallIfNeeded(ctx, o, p, v)
diff --git a/internal/cmd/use.go b/internal/cmd/use.go
index 039ebed..db12a52 100644
--- a/internal/cmd/use.go
+++ b/internal/cmd/use.go
@@ -41,7 +41,7 @@ Example:
 $ getenvoy use %s`, envoy.CurrentVersionWorkingDirFile, envoy.CurrentVersionHomeDirFile, version.LastKnownEnvoy),
 		Before: validateVersionArg,
 		Action: func(c *cli.Context) error {
-			v := c.Args().First()
+			v := version.Version(c.Args().First())
 			if _, err := envoy.InstallIfNeeded(c.Context, o, globals.CurrentPlatform, v); err != nil {
 				return err
 			}
diff --git a/internal/cmd/versions.go b/internal/cmd/versions.go
index 5726304..54e4952 100644
--- a/internal/cmd/versions.go
+++ b/internal/cmd/versions.go
@@ -80,10 +80,8 @@ func NewVersionsCmd(o *globals.GlobalOpts) *cli.Command {
 }
 
 type versionReleaseDate struct {
-	// version ex "1.15.5"
-	version string
-	// releaseDate ex "2021-05-11"
-	releaseDate string
+	version     version.Version
+	releaseDate version.ReleaseDate
 }
 
 func getInstalledVersions(homeDir string) ([]versionReleaseDate, error) {
@@ -97,22 +95,25 @@ func getInstalledVersions(homeDir string) ([]versionReleaseDate, error) {
 
 	for _, f := range files {
 		if i, err := f.Info(); f.IsDir() && err == nil {
-			rows = append(rows, versionReleaseDate{f.Name(), i.ModTime().Format("2006-01-02")})
+			rows = append(rows, versionReleaseDate{
+				version.Version(f.Name()),
+				version.ReleaseDate(i.ModTime().Format("2006-01-02")),
+			})
 		}
 	}
 	return rows, nil
 }
 
 // addAvailableVersions adds remote Envoy versions valid for this platform to "rows", if they don't already exist
-func addAvailableVersions(rows *[]versionReleaseDate, remote map[string]version.EnvoyVersion, p string) error {
-	existingVersions := make(map[string]bool)
+func addAvailableVersions(rows *[]versionReleaseDate, remote map[version.Version]version.Release, p version.Platform) error {
+	existingVersions := make(map[version.Version]bool)
 	for _, v := range *rows { //nolint:gocritic
 		existingVersions[v.version] = true
 	}
 
 	for k, v := range remote {
 		if _, ok := v.Tarballs[p]; ok && !existingVersions[k] {
-			if _, err := time.Parse("2006-01-02", v.ReleaseDate); err != nil {
+			if _, err := time.Parse("2006-01-02", string(v.ReleaseDate)); err != nil {
 				return fmt.Errorf("invalid releaseDate of version %q for platform %q: %w", k, p, err)
 			}
 			*rows = append(*rows, versionReleaseDate{k, v.ReleaseDate})
diff --git a/internal/envoy/http.go b/internal/envoy/http.go
index 965fd1a..c3aa628 100644
--- a/internal/envoy/http.go
+++ b/internal/envoy/http.go
@@ -19,16 +19,18 @@ import (
 	"fmt"
 	"net/http"
 	"strings"
+
+	"github.com/tetratelabs/getenvoy/internal/version"
 )
 
 // httpGet adds the userAgent header to the request, so that we can tell what is a dev build vs release.
-func httpGet(ctx context.Context, url, platform, version string) (*http.Response, error) {
+func httpGet(ctx context.Context, url string, p version.Platform, v version.Version) (*http.Response, error) {
 	// #nosec -> url can be anywhere by design
 	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
 	if err != nil {
 		return nil, err
 	}
-	req.Header.Add("User-Agent", userAgent(platform, version))
+	req.Header.Add("User-Agent", userAgent(p, v))
 	return http.DefaultClient.Do(req)
 }
 
@@ -39,9 +41,9 @@ func httpGet(ctx context.Context, url, platform, version string) (*http.Response
 //
 // Note: Analytics may not work out-of-box. For example, Netlify does not support server-side analytics on 'User-Agent',
 // and even its 'Referer' analytics are limited to requests to HTML resources.
-func userAgent(platform, version string) string {
-	if !strings.HasPrefix(version, "v") || strings.Contains(version, "SNAPSHOT") {
+func userAgent(p version.Platform, v version.Version) string {
+	if !strings.HasPrefix(string(v), "v") || strings.Contains(string(v), "SNAPSHOT") {
 		return "getenvoy/dev"
 	}
-	return fmt.Sprintf("getenvoy/%s (%s)", version, platform)
+	return fmt.Sprintf("getenvoy/%s (%s)", v, p)
 }
diff --git a/internal/envoy/install.go b/internal/envoy/install.go
index 79b44f5..84719df 100644
--- a/internal/envoy/install.go
+++ b/internal/envoy/install.go
@@ -16,9 +16,13 @@ package envoy
 
 import (
 	"context"
+	"crypto/sha256"
 	"fmt"
+	"hash"
+	"io"
 	"net/http"
 	"os"
+	"path"
 	"path/filepath"
 	"time"
 
@@ -31,13 +35,13 @@ import (
 var binEnvoy = filepath.Join("bin", "envoy"+moreos.Exe)
 
 // InstallIfNeeded downloads an Envoy binary corresponding to the given version and returns a path to it or an error.
-func InstallIfNeeded(ctx context.Context, o *globals.GlobalOpts, p, v string) (string, error) {
-	installPath := filepath.Join(o.HomeDir, "versions", v)
+func InstallIfNeeded(ctx context.Context, o *globals.GlobalOpts, p version.Platform, v version.Version) (string, error) {
+	installPath := filepath.Join(o.HomeDir, "versions", string(v))
 	envoyPath := filepath.Join(installPath, binEnvoy)
 	_, err := os.Stat(envoyPath)
 	switch {
 	case os.IsNotExist(err):
-		var ev version.EnvoyVersions // Get version metadata for what we will install
+		var ev version.ReleaseVersions // Get version metadata for what we will install
 		ev, err = GetEnvoyVersions(ctx, o.EnvoyVersionsURL, p, v)
 		if err != nil {
 			return "", err
@@ -48,16 +52,22 @@ func InstallIfNeeded(ctx context.Context, o *globals.GlobalOpts, p, v string) (s
 			return "", fmt.Errorf("couldn't find version %q for platform %q", v, p)
 		}
 
+		tarball := version.Tarball(path.Base(string(tarballURL)))
+		sha256Sum := ev.SHA256Sums[tarball]
+		if len(sha256Sum) != 64 {
+			return "", fmt.Errorf("couldn't find sha256Sum of version %q for platform %q: %w", v, p, err)
+		}
+
 		var mtime time.Time // Create a directory for the version, preserving the release date as its mtime
-		if mtime, err = time.Parse("2006-01-02", ev.Versions[v].ReleaseDate); err != nil {
+		if mtime, err = time.Parse("2006-01-02", string(ev.Versions[v].ReleaseDate)); err != nil {
 			return "", fmt.Errorf("couldn't find releaseDate of version %q for platform %q: %w", v, p, err)
 		}
 		if err = os.MkdirAll(installPath, 0750); err != nil {
 			return "", fmt.Errorf("unable to create directory %q: %w", installPath, err)
 		}
 
-		fmt.Fprintln(o.Out, "downloading", tarballURL)                        //nolint
-		if err = untarEnvoy(ctx, installPath, tarballURL, p, v); err != nil { //nolint
+		fmt.Fprintln(o.Out, "downloading", tarballURL)                                   //nolint
+		if err = untarEnvoy(ctx, installPath, tarballURL, sha256Sum, p, v); err != nil { //nolint
 			return "", err
 		}
 		if err = os.Chtimes(installPath, mtime, mtime); err != nil { // overwrite the mtime to preserve it in the list
@@ -84,19 +94,42 @@ func verifyEnvoy(installPath string) (string, error) {
 	return envoyPath, nil
 }
 
-func untarEnvoy(ctx context.Context, dst, url, p, v string) error { // dst, src order like io.Copy
-	resp, err := httpGet(ctx, url, p, v)
+type digester struct {
+	r io.Reader
+	h hash.Hash
+	e error
+}
+
+func (d *digester) Read(p []byte) (n int, err error) {
+	n, err = d.r.Read(p)
+	if n > 0 {
+		_, d.e = d.h.Write(p[:n])
+	}
+	return
+}
+
+func untarEnvoy(ctx context.Context, dst string, src version.TarballURL, // dst, src order like io.Copy
+	sha256Sum version.SHA256Sum, p version.Platform, v version.Version) error {
+	res, err := httpGet(ctx, string(src), p, v)
 	if err != nil {
 		return err
 	}
-	defer resp.Body.Close() //nolint
+	defer res.Body.Close() //nolint
 
-	if resp.StatusCode != http.StatusOK {
-		return fmt.Errorf("received %v status code from %s", resp.StatusCode, url)
+	if res.StatusCode != http.StatusOK {
+		return fmt.Errorf("received %v status code from %s", res.StatusCode, src)
 	}
 
-	if err = tar.Untar(dst, resp.Body); err != nil {
-		return fmt.Errorf("error untarring %s: %w", url, err)
+	d := digester{res.Body, sha256.New(), nil}
+	if err = tar.Untar(dst, &d); err != nil {
+		return fmt.Errorf("error untarring %s: %w", src, err)
+	}
+	if d.e != nil {
+		return fmt.Errorf("error computing SHA-256 from %s: %w", src, d.e)
+	}
+	sum := version.SHA256Sum(fmt.Sprintf("%x", d.h.Sum(nil)))
+	if sum != sha256Sum {
+		return fmt.Errorf("expected SHA-256 sum %q, but have %q from %s", sha256Sum, sum, src)
 	}
 	return nil
 }
diff --git a/internal/envoy/version.go b/internal/envoy/version.go
index cb79b92..4624bb6 100644
--- a/internal/envoy/version.go
+++ b/internal/envoy/version.go
@@ -34,7 +34,7 @@ var (
 
 // GetHomeVersion returns the default version in the "homeDir" and path to to it (homeVersionFile). When "v" is empty,
 // homeVersionFile is not yet initialized.
-func GetHomeVersion(homeDir string) (v, homeVersionFile string, err error) {
+func GetHomeVersion(homeDir string) (v version.Version, homeVersionFile string, err error) {
 	v, homeVersionFile, err = getHomeVersion(homeDir)
 	if err == nil && v == "" { // no home version, yet
 		return
@@ -45,7 +45,7 @@ func GetHomeVersion(homeDir string) (v, homeVersionFile string, err error) {
 
 // WriteCurrentVersion writes the version to CurrentVersionWorkingDirFile or CurrentVersionHomeDirFile depending on
 // if the former is present.
-func WriteCurrentVersion(v, homeDir string) error {
+func WriteCurrentVersion(v version.Version, homeDir string) error {
 	if _, err := os.Stat(".envoy-version"); os.IsNotExist(err) {
 		return os.WriteFile(filepath.Join(homeDir, "version"), []byte(v), 0600)
 	} else if err != nil {
@@ -56,32 +56,36 @@ func WriteCurrentVersion(v, homeDir string) error {
 
 // CurrentVersion returns the first version in priority of VersionUsageList and its source or an error. The "source"
 // and error messages returned include unexpanded variables to clarify the intended context.
-func CurrentVersion(homeDir string) (v, source string, err error) {
+func CurrentVersion(homeDir string) (v version.Version, source string, err error) {
 	v, source, err = getCurrentVersion(homeDir)
 	err = verifyVersion(v, source, err)
 	return
 }
 
-func verifyVersion(v, source string, err error) error {
+func verifyVersion(v version.Version, source string, err error) error {
 	if err != nil {
 		return fmt.Errorf("couldn't read version from %s: %w", source, err)
 	}
-	if matched := globals.EnvoyVersionPattern.MatchString(v); !matched {
+	if matched := globals.EnvoyVersionPattern.MatchString(string(v)); !matched {
 		return fmt.Errorf("invalid version in %q: %q should look like %q", source, v, version.LastKnownEnvoy)
 	}
 	return nil
 }
 
-func getCurrentVersion(homeDir string) (v, source string, err error) {
+func getCurrentVersion(homeDir string) (v version.Version, source string, err error) {
 	// Priority 1: $ENVOY_VERSION
 	if ev, ok := os.LookupEnv("ENVOY_VERSION"); ok {
-		return ev, currentVersionVar, nil
+		v = version.Version(ev)
+		source = currentVersionVar
+		return
 	}
 
 	// Priority 2: $PWD/.envoy-version
 	data, err := os.ReadFile(".envoy-version")
 	if err == nil {
-		return strings.TrimSpace(string(data)), CurrentVersionWorkingDirFile, nil
+		v = version.Version(strings.TrimSpace(string(data)))
+		source = CurrentVersionWorkingDirFile
+		return
 	} else if !os.IsNotExist(err) {
 		return "", CurrentVersionWorkingDirFile, err
 	}
@@ -92,11 +96,11 @@ func getCurrentVersion(homeDir string) (v, source string, err error) {
 	return
 }
 
-func getHomeVersion(homeDir string) (v, homeVersionFile string, err error) {
+func getHomeVersion(homeDir string) (v version.Version, homeVersionFile string, err error) {
 	homeVersionFile = filepath.Join(homeDir, "version")
 	var data []byte
 	if data, err = os.ReadFile(homeVersionFile); err == nil {
-		v = strings.TrimSpace(string(data))
+		v = version.Version(strings.TrimSpace(string(data)))
 	} else if os.IsNotExist(err) {
 		err = nil // ok on file-not-found
 	}
diff --git a/internal/envoy/versions.go b/internal/envoy/versions.go
index 93be47b..e2f77cc 100644
--- a/internal/envoy/versions.go
+++ b/internal/envoy/versions.go
@@ -25,8 +25,8 @@ import (
 )
 
 // GetEnvoyVersions returns a version map from a remote URL. eg globals.DefaultEnvoyVersionsURL.
-func GetEnvoyVersions(ctx context.Context, envoyVersionsURL, p, v string) (version.EnvoyVersions, error) {
-	result := version.EnvoyVersions{}
+func GetEnvoyVersions(ctx context.Context, envoyVersionsURL string, p version.Platform, v version.Version) (version.ReleaseVersions, error) {
+	result := version.ReleaseVersions{}
 	// #nosec => This is by design, users can call out to wherever they like!
 	resp, err := httpGet(ctx, envoyVersionsURL, p, v)
 	if err != nil {
diff --git a/internal/globals/globals.go b/internal/globals/globals.go
index b821c7c..695cd36 100644
--- a/internal/globals/globals.go
+++ b/internal/globals/globals.go
@@ -18,6 +18,8 @@ import (
 	"io"
 	"regexp"
 	"runtime"
+
+	"github.com/tetratelabs/getenvoy/internal/version"
 )
 
 // RunOpts support invocations of "getenvoy run"
@@ -48,7 +50,7 @@ type GlobalOpts struct {
 	EnvoyVersionsURL string
 	// EnvoyVersion is the default version of Envoy to run. Defaults to the contents of "$HomeDir/versions/version".
 	// When that file is missing, it is generated from ".latestVersion" from the EnvoyVersionsURL.
-	EnvoyVersion string
+	EnvoyVersion version.Version
 	// HomeDir is an absolute path which most importantly contains "versions" installed from EnvoyVersionsURL. Defaults to DefaultHomeDir
 	HomeDir string
 	// Out is where status messages are written. Defaults to os.Stdout
@@ -66,5 +68,5 @@ var (
 	// EnvoyVersionPattern is used to validate versions and is the same pattern as envoy-versions-schema.json.
 	EnvoyVersionPattern = regexp.MustCompile(`^[1-9][0-9]*\.[0-9]+\.[0-9]+$`)
 	// CurrentPlatform is the platform of the current process. This is used as a key in EnvoyVersion.Tarballs.
-	CurrentPlatform = runtime.GOOS + "/" + runtime.GOARCH
+	CurrentPlatform = version.Platform(runtime.GOOS + "/" + runtime.GOARCH)
 )
diff --git a/internal/version/version.go b/internal/version/version.go
index 8b38a49..b75d4d0 100644
--- a/internal/version/version.go
+++ b/internal/version/version.go
@@ -12,40 +12,62 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+// Package version declares types for each string to keep strict coupling to the JSON schema
 package version
 
 import _ "embed" // We embed the Envoy version so that we can cache it in CI
 
+var getEnvoy = "dev"
+
 // GetEnvoy is the version of the CLI, used in help statements and HTTP requests via "User-Agent".
-// Override this via "-X github.com/tetratelabs/getenvoy/internal/version.GetEnvoy=XXX"
-var GetEnvoy = "dev"
+// Override this via "-X github.com/tetratelabs/getenvoy/internal/version.getEnvoy=XXX"
+var GetEnvoy = Version(getEnvoy)
+
+//go:embed last_known_envoy.txt
+var lastKnownEnvoy string
 
-// LastKnownEnvoy is the last known Envoy version, used to ensure help statements aren't out-of-date.
+// LastKnownEnvoy is the last known Envoy Version, used to ensure help statements aren't out-of-date.
 // This is derived from /site/envoy-versions.json, but not used directly because go:embed requires a file read from the
 // current directory tree.
 //
 // This is different than the "latestVersion" because this is built into the binary. For example, after the binary is
 // built, a more recent "latestVersion" can be used, even if the help statements only know about the one from compile
 // time.
-//go:embed last_known_envoy.txt
-var LastKnownEnvoy string
-
-// EnvoyVersions include metadata about the latest version and all available tarball URLs
-type EnvoyVersions struct {
-	// LatestVersion is the latest stable version. See https://github.com/envoyproxy/envoy/blob/main/RELEASES.md
-	LatestVersion string
-	// Versions are all stable versions of Envoy keyed on version number (without a 'v' prefix)
-	Versions map[string]EnvoyVersion
+var LastKnownEnvoy = Version(lastKnownEnvoy)
+
+// ReleaseVersions primarily maps Version to TarballURL and tracks the LatestVersion
+type ReleaseVersions struct {
+	// LatestVersion is the latest stable Version
+	LatestVersion Version `json:"latestVersion"`
+	// Versions maps a Version to its Release
+	Versions map[Version]Release `json:"versions"`
+	// SHA256Sums maps a Tarball to its SHA256Sum
+	SHA256Sums map[Tarball]SHA256Sum `json:"sha256sums"`
 }
 
-// EnvoyVersion is the release date and tarballs for this version, keyed on platform
-type EnvoyVersion struct {
-	// ReleaseDate is the date of the version tag https://github.com/envoyproxy/envoy/versions. Ex. "2021-04-16"
-	ReleaseDate string `json:"releaseDate"`
+// Version is a release version from https://github.com/envoyproxy/envoy/releases, without a 'v' prefix. Ex "1.18.3"
+type Version string
+
+// Platform encodes 'runtime.GOOS/runtime.GOARCH'. Ex "darwin/amd64"
+type Platform string
+
+// Tarball is the name of the tar.gz or tar.xz archive. Ex. "envoy-v1.18.3-linux-amd64.tar.xz"
+// Minimum contents are "${dist}/bin/envoy[.exe]" Ex. "envoy-v1.18.3-linux-amd64/bin/envoy"
+type Tarball string
+
+// TarballURL is the HTTPS URL to the Tarball. SHA256Sums must include its base name.
+type TarballURL string
+
+// SHA256Sum is a SHA-256 lower-hex hash. Ex. "1274f55b3022bc1331aed41089f189094e00729981fe132ce00aac6272ea0770"
+type SHA256Sum string
+
+// ReleaseDate is the publish date of the release Version. Ex. "2021-05-11"
+type ReleaseDate string
+
+// Release primarily maps available Tarballs for a Version
+type Release struct {
+	ReleaseDate ReleaseDate
 
-	// Tarballs is a map of platform to tarball URL
-	// platform is '$os/$arch' where $os is the supported operating system (runtime.GOOS) and $arch
-	// is the supported architecture (runtime.GOARCH). Ex "darwin/amd64"
-	// tarball URL is the URL of the tar.gz or tar.xz that bundles Envoy. Minimum contents are "$version/bin/envoy".
-	Tarballs map[string]string `json:"tarballs,omitempty"`
+	// Tarballs are the Tarballs available by Platform
+	Tarballs map[Platform]TarballURL `json:"tarballs,omitempty"`
 }
diff --git a/site/envoy-versions-schema.json b/site/envoy-versions-schema.json
index d43c88b..e502aed 100644
--- a/site/envoy-versions-schema.json
+++ b/site/envoy-versions-schema.json
@@ -1,65 +1,90 @@
 {
   "$schema": "https://json-schema.org/draft-07/schema#",
-  "id": "https://www.getenvoy.io/envoyVersions",
+  "id": "https://www.getenvoy.io/releaseVersions",
   "type": "object",
-  "title": "EnvoyVersions",
-  "description": "EnvoyVersions include metadata about the latest version and all available tarball URLs",
+  "title": "ReleaseVersions",
+  "description": "ReleaseVersions primarily maps Version to TarballURL and tracks the LatestVersion",
   "properties": {
     "latestVersion": {
       "title": "LatestVersion",
-      "description": "LatestVersion is the latest stable version. See https://github.com/envoyproxy/envoy/blob/main/RELEASES.md",
-      "type": "string",
-      "pattern": "^[1-9][0-9]*\\.[0-9]+\\.[0-9]+$"
+      "description": "LatestVersion is the latest stable Version",
+      "$ref": "#/definitions/Version"
     },
     "versions": {
       "type": "object",
       "title": "Versions",
-      "description": "Versions are all stable versions of Envoy keyed on version number (without a 'v' prefix)",
+      "description": "maps a Version to its Release",
       "minProperties": 1,
       "propertyNames": {
-        "pattern": "^[1-9][0-9]*\\.[0-9]+\\.[0-9]+$"
+        "$ref": "#/definitions/Version"
       },
       "additionalProperties": {
-        "$ref": "#/definitions/EnvoyVersion"
+        "$ref": "#/definitions/Release"
       }
+    },
+    "SHA256Sums": {
+      "type": "object",
+      "title": "SHA-256 sums",
+      "description": "maps a Tarball to its SHA256Sum",
+      "minProperties": 1,
+      "propertyNames": {
+        "$ref": "#/definitions/Tarball"
+      },
+      "additionalProperties": {
+        "$ref": "#/definitions/SHA256Sum"
+      },
+      "examples": [
+        {
+          "envoy-v1.18.3-windows-amd64.tar.xz": "1274f55b3022bc1331aed41089f189094e00729981fe132ce00aac6272ea0770"
+        }
+      ]
     }
   },
   "examples": [
     {
       "latestVersion": "1.18.3",
       "versions": {
-        "1.14.6": {
-          "releaseDate": "2021-04-16",
+        "1.15.5": {
+          "releaseDate": "2021-05-11",
           "tarballs": {
-            "darwin/amd64": "https://getenvoy.io/versions/1.14.6/envoy-1.14.6-darwin-x86_64.tar.gz",
-            "linux/amd64": "https://getenvoy.io/versions/1.14.6/envoy-1.14.6-linux-x86_64.tar.gz"
+            "darwin/amd64": "https://archive.tetratelabs.io/envoy/v1.15.5/envoy-v1.15.5-darwin-amd64.tar.xz",
+            "linux/amd64": "https://archive.tetratelabs.io/envoy/v1.15.5/envoy-v1.15.5-linux-amd64.tar.xz",
+            "linux/arm64": "https://archive.tetratelabs.io/envoy/v1.15.5/envoy-v1.15.5-linux-arm64.tar.xz"
           }
         },
         "1.18.3": {
           "releaseDate": "2021-05-11",
           "tarballs": {
-            "darwin/amd64": "https://getenvoy.io/versions/1.18.3/envoy-1.18.3-darwin-x86_64.tar.gz",
-            "linux/amd64": "https://getenvoy.io/versions/1.18.3/envoy-1.18.3-linux-x86_64.tar.gz",
+            "darwin/amd64": "https://archive.tetratelabs.io/envoy/v1.18.3/envoy-v1.18.3-darwin-amd64.tar.xz",
+            "linux/amd64": "https://archive.tetratelabs.io/envoy/v1.18.3/envoy-v1.18.3-linux-amd64.tar.xz",
+            "linux/arm64": "https://archive.tetratelabs.io/envoy/v1.18.3/envoy-v1.18.3-linux-arm64.tar.xz",
+            "windows/amd64": "https://archive.tetratelabs.io/envoy/v1.18.3/envoy-v1.18.3-windows-amd64.tar.xz"
           }
         }
+      },
+      "sha256sums": {
+        "envoy-v1.15.5-darwin-amd64.tar.xz": "8c6710c4380eb13c1748a44864af146a6bae75145a1a7dd590e3fcb61de71a84",
+        "envoy-v1.15.5-linux-amd64.tar.xz": "227c6166d4fb73e6d8ef8976549536a50fa9e5b0fdc6984190524d829a33b9b2",
+        "envoy-v1.15.5-linux-arm64.tar.xz": "dd9ae8dc76cd7704f22c51a7ec282c0af53e5facc16116be1cf9d396f792cef2",
+        "envoy-v1.18.3-darwin-amd64.tar.xz": "cb27be135f0d48167b756d17fa9117ed6295ee3f69e4db7900b94ddffef463e1",
+        "envoy-v1.18.3-linux-amd64.tar.xz": "10e1c6bc354018578f02c97358d3bbc9a26d4989cf752963611e69dac52656f4",
+        "envoy-v1.18.3-linux-arm64.tar.xz": "367d61bf246d56cc24949574977edefa4649ad2072c08d57a8bd63c44d991d25",
+        "envoy-v1.18.3-windows-amd64.tar.xz": "1274f55b3022bc1331aed41089f189094e00729981fe132ce00aac6272ea0770"
       }
     }
   ],
   "definitions": {
-    "EnvoyVersion": {
+    "Release": {
       "type": "object",
-      "title": "EnvoyVersion",
-      "description" : "EnvoyVersion is the release date and tarballs for this version, keyed on platform",
+      "title": "Release",
+      "description": "primarily maps available Tarballs for a Version",
       "properties": {
         "releaseDate": {
-          "title": "ReleaseDate",
-          "description": "ReleaseDate is the date of the version tag https://github.com/envoyproxy/envoy/versions",
-          "type": "string",
-          "format": "date"
+          "$ref": "#/definitions/ReleaseDate"
         },
         "tarballs": {
           "title": "Tarballs",
-          "description": "Tarballs available by platform",
+          "description": "Tarballs available by Platform",
           "type": "object",
           "minProperties": 1,
           "propertyNames": {
@@ -73,7 +98,7 @@
       },
       "required": [
         "releaseDate",
-        "downloads"
+        "tarballs"
       ]
     },
     "Platform": {
@@ -82,15 +107,56 @@
       "type": "string",
       "examples": [
         "darwin/amd64",
+        "darwin/arm64",
         "linux/amd64",
-        "linux/arm64"
+        "linux/arm64",
+        "windows/amd64"
+      ]
+    },
+    "ReleaseDate": {
+      "title": "ReleaseDate",
+      "description": "the publish date of the release Version",
+      "type": "string",
+      "format": "date",
+      "examples": [
+        "2021-05-11"
+      ]
+    },
+    "SHA256Sum": {
+      "type": "string",
+      "title": "SHA-256 sum",
+      "description": "SHA-256 lower-hex hash",
+      "pattern": "^[0-9a-f]{64}$",
+      "examples": [
+        "1274f55b3022bc1331aed41089f189094e00729981fe132ce00aac6272ea0770"
+      ]
+    },
+    "Tarball": {
+      "title": "Tarball Name",
+      "description": "Name of the tar.gz or tar.xz archive. Minimum contents are ${dist}/bin/${bin}[.exe]",
+      "type": "string",
+      "pattern": "^[a-zA-Z0-9-_.]+[.]tar[.][gx]z$",
+      "examples": [
+        "https://archive.tetratelabs.io/envoy/v1.18.3/envoy-v1.18.3-darwin-amd64.tar.xz"
       ]
     },
     "TarballURL": {
       "title": "Tarball URL",
-      "description": "URL of the tar.gz or tar.xz that bundles Envoy. Minimum contents are $version/bin/envoy",
+      "description": "HTTPS URL to the Tarball. SHA256Sums must include its base name",
       "type": "string",
-      "format": "uri"
+      "format": "uri",
+      "examples": [
+        "envoy-v1.18.3-darwin-amd64.tar.xz"
+      ]
+    },
+    "Version": {
+      "title": "Version",
+      "description": "a release version from https://github.com/${GITHUB_REPOSITORY}/releases, without a 'v' prefix.",
+      "type": "string",
+      "pattern": "^[1-9][0-9]*\\.[0-9]+\\.[0-9]+$",
+      "examples": [
+        "1.18.3"
+      ]
     }
   }
 }
diff --git a/site/envoy-versions.json b/site/envoy-versions.json
index 364aa40..b3b3bd4 100644
--- a/site/envoy-versions.json
+++ b/site/envoy-versions.json
@@ -57,5 +57,23 @@
         "linux/amd64": "https://dl.getenvoy.io/public/raw/files/getenvoy-envoy-1.18.3.p0.g98c1c9e-1p77.gb76c773-linux-glibc-release-x86_64.tar.xz"
       }
     }
+  },
+  "sha256sums": {
+    "getenvoy-envoy-1.12.7.p0.gdde2499-1p69.ga5345f6-darwin-release-x86_64.tar.xz": "1a43333cef9fa2e56451e196c0eaa2c67c26be42d85e1117202b982c31f23f3a",
+    "getenvoy-envoy-1.12.7.p0.gdde2499-1p69.ga5345f6-linux-glibc-release-x86_64.tar.xz": "cc5bd974a5a5f1505243ac89c4d580d45a5824a0591af8eeabd06a2b337c2dbb",
+    "getenvoy-envoy-1.13.7.p0.g56d148b-1p70.g486d4e2-darwin-release-x86_64.tar.xz": "c092b193a5f58a5c6ba291556320cd6b926126e73f77c8251b1617a5dac5a31c",
+    "getenvoy-envoy-1.13.7.p0.g56d148b-1p70.g486d4e2-linux-glibc-release-x86_64.tar.xz": "f8bf3eae1212ee13d4ce624c8e0a49b5202f7fb05d18dbefb1b228efcb583c50",
+    "getenvoy-envoy-1.14.7.p0.gbc15868-1p74.gbb8060d-darwin-release-x86_64.tar.xz": "6d95bb6b9c851716fddd7b5c53cc367872cc75aeb32d04b1dd4e300c875ba097",
+    "getenvoy-envoy-1.14.7.p0.gbc15868-1p74.gbb8060d-linux-glibc-release-x86_64.tar.xz": "b9fa2c9787d09c827f1a5f28433b7f476bbde677fa26ba469b48449b1515e1ba",
+    "getenvoy-envoy-1.15.5.p0.g623479e-1p74.gbb8060d-darwin-release-x86_64.tar.xz": "ce12c2d7c1619764d2f7908d82a974b0e345a3240e1b3b842837a7caed3ad948",
+    "getenvoy-envoy-1.15.5.p0.g623479e-1p74.gbb8060d-linux-glibc-release-x86_64.tar.xz": "2baab0ad6171639682a620bc1394646a5c2f63c89698bf4b316f29b5d1e24e0c",
+    "getenvoy-envoy-1.16.3.p0.gc4b7ab8-1p74.gbb8060d-darwin-release-x86_64.tar.xz": "24c810bf350ad7b462f34fe64400657c23ee5e8b7e9270c8ce93463192ebb622",
+    "getenvoy-envoy-1.16.3.p0.gc4b7ab8-1p74.gbb8060d-linux-glibc-release-x86_64.tar.xz": "9d00b59e9a698dbbe7679b1ef4cfad3100569736cc5088e58c4e6e910a522e2b",
+    "getenvoy-envoy-1.16.4.p0.gbf5d0eb-1p74.gbb8060d-darwin-release-x86_64.tar.xz": "6b68367962872ec984f91991d165f1eb1d368eaa22eba5a8e57734f98d67a263",
+    "getenvoy-envoy-1.16.4.p0.gbf5d0eb-1p74.gbb8060d-linux-glibc-release-x86_64.tar.xz": "ff3491d5a1f9687d947ec0f35b6096cdc27101984e4e520e1354fc5121fef856",
+    "getenvoy-envoy-1.17.3.p0.g46bf743-1p74.gbb8060d-darwin-release-x86_64.tar.xz": "823dfe638c8d6fcb9ec7c84e8707312b2bf9e481e32a48a5ae05a8fe9c361eb6",
+    "getenvoy-envoy-1.17.3.p0.g46bf743-1p74.gbb8060d-linux-glibc-release-x86_64.tar.xz": "299ffe0e5010cda230f20956c4f706319cdd6ca6279b578798f878ff129d86a0",
+    "getenvoy-envoy-1.18.3.p0.g98c1c9e-1p77.gb76c773-darwin-release-x86_64.tar.xz": "7c8f645da0347de155b0d598b6bf2df6dc2cfaf80e1b30b2d508e77f69c47ac4",
+    "getenvoy-envoy-1.18.3.p0.g98c1c9e-1p77.gb76c773-linux-glibc-release-x86_64.tar.xz": "a9788c23cc15ca51afb57a412d9ca1d5ea9ca975a23a13d20d5ca09f861b94d3"
   }
 }


Test patch:
diff --git a/e2e/getenvoy_use_test.go b/e2e/getenvoy_use_test.go
index 9955a76..30d98a4 100644
--- a/e2e/getenvoy_use_test.go
+++ b/e2e/getenvoy_use_test.go
@@ -34,27 +34,27 @@ func TestGetEnvoyUse(t *testing.T) {
 	defer removeHomeDir()
 
 	t.Run("not yet installed", func(t *testing.T) {
-		stdout, stderr, err := getEnvoyExec("--home-dir", homeDir, "use", version.LastKnownEnvoy)
+		stdout, stderr, err := getEnvoyExec("--home-dir", homeDir, "use", string(version.LastKnownEnvoy))
 
 		require.NoError(t, err)
 		require.Regexp(t, `^downloading https:.*tar.*z\n$`, stdout)
 		require.Empty(t, stderr)
 
 		// The binary was installed
-		envoyBin := filepath.Join(homeDir, "versions", version.LastKnownEnvoy, "bin", "envoy"+moreos.Exe)
+		envoyBin := filepath.Join(homeDir, "versions", string(version.LastKnownEnvoy), "bin", "envoy"+moreos.Exe)
 		require.FileExists(t, envoyBin)
 
 		// The current version was written
 		f, err := os.ReadFile(filepath.Join(homeDir, "version"))
 		require.NoError(t, err)
-		require.Equal(t, version.LastKnownEnvoy, string(f))
+		require.Equal(t, version.LastKnownEnvoy, version.Version(f))
 	})
 
 	t.Run("already installed", func(t *testing.T) {
-		stdout, stderr, err := getEnvoyExec("--home-dir", homeDir, "use", version.LastKnownEnvoy)
+		stdout, stderr, err := getEnvoyExec("--home-dir", homeDir, "use", string(version.LastKnownEnvoy))
 
 		require.NoError(t, err)
-		require.Equal(t, version.LastKnownEnvoy+" is already downloaded\n", stdout)
+		require.Equal(t, fmt.Sprintf("%s is already downloaded\n", version.LastKnownEnvoy), stdout)
 		require.Empty(t, stderr)
 	})
 }
diff --git a/internal/cmd/help_test.go b/internal/cmd/help_test.go
index a5b160c..dcac7ef 100644
--- a/internal/cmd/help_test.go
+++ b/internal/cmd/help_test.go
@@ -44,8 +44,8 @@ func TestGetEnvoyHelp(t *testing.T) {
 			}
 
 			bytes, err := os.ReadFile(filepath.Join("testdata", expected))
-			want := strings.ReplaceAll(string(bytes), "{VERSION}", version.GetEnvoy)
-			want = strings.ReplaceAll(want, "{ENVOY_VERSION}", version.LastKnownEnvoy)
+			want := strings.ReplaceAll(string(bytes), "{VERSION}", string(version.GetEnvoy))
+			want = strings.ReplaceAll(want, "{ENVOY_VERSION}", string(version.LastKnownEnvoy))
 
 			require.NoError(t, err)
 			require.Equal(t, want, stdout.String())
diff --git a/internal/cmd/use_test.go b/internal/cmd/use_test.go
index 7166c87..cf6241f 100644
--- a/internal/cmd/use_test.go
+++ b/internal/cmd/use_test.go
@@ -62,13 +62,13 @@ func TestGetEnvoyUse_InstallsAndWritesHomeVersion(t *testing.T) {
 	defer cleanup()
 
 	c, _, _ := newApp(o)
-	require.NoError(t, c.Run([]string{"getenvoy", "use", o.EnvoyVersion}))
+	require.NoError(t, c.Run([]string{"getenvoy", "use", string(o.EnvoyVersion)}))
 
 	// The binary was installed
-	require.FileExists(t, filepath.Join(o.HomeDir, "versions", o.EnvoyVersion, "bin", "envoy"))
+	require.FileExists(t, filepath.Join(o.HomeDir, "versions", string(o.EnvoyVersion), "bin", "envoy"))
 
 	// The current version was written
 	f, err := os.ReadFile(filepath.Join(o.HomeDir, "version"))
 	require.NoError(t, err)
-	require.Equal(t, o.EnvoyVersion, string(f))
+	require.Equal(t, o.EnvoyVersion, version.Version(f))
 }
diff --git a/internal/cmd/versions_cmd_test.go b/internal/cmd/versions_cmd_test.go
index 00b5d47..bc69f8e 100644
--- a/internal/cmd/versions_cmd_test.go
+++ b/internal/cmd/versions_cmd_test.go
@@ -140,7 +140,7 @@ func TestGetEnvoyVersions_All_RemoteIsCurrent(t *testing.T) {
 	o, cleanup := setupTest(t)
 	defer cleanup()
 
-	versionDir := filepath.Join(o.HomeDir, "versions", version.LastKnownEnvoy)
+	versionDir := filepath.Join(o.HomeDir, "versions", string(version.LastKnownEnvoy))
 	require.NoError(t, os.MkdirAll(versionDir, 0700))
 	morerequire.RequireSetMtime(t, versionDir, "2020-12-31")
 	require.NoError(t, os.WriteFile(filepath.Join(o.HomeDir, "version"), []byte(version.LastKnownEnvoy), 0600))
diff --git a/internal/cmd/versions_test.go b/internal/cmd/versions_test.go
index 35993ce..9275b4c 100644
--- a/internal/cmd/versions_test.go
+++ b/internal/cmd/versions_test.go
@@ -86,23 +86,23 @@ func TestGetInstalledVersions_SkipsFileInVersionsDir(t *testing.T) {
 }
 
 func TestAddAvailableVersions(t *testing.T) {
-	goodVersions := map[string]version.EnvoyVersion{
+	goodVersions := map[version.Version]version.Release{
 		"1.14.7": {
 			ReleaseDate: "2021-04-15",
-			Tarballs: map[string]string{
+			Tarballs: map[version.Platform]version.TarballURL{
 				"darwin/amd64": "https://getenvoy.io/versions/1.14.7/envoy-1.14.7-darwin-x86_64.tar.gz",
 				"linux/amd64":  "https://getenvoy.io/versions/1.14.7/envoy-1.14.7-linux-x86_64.tar.gz",
 			},
 		},
 		"1.17.3": {
 			ReleaseDate: "2021-05-11",
-			Tarballs: map[string]string{
+			Tarballs: map[version.Platform]version.TarballURL{
 				"linux/amd64": "https://getenvoy.io/versions/1.17.3/envoy-1.17.3-linux-x86_64.tar.gz",
 			},
 		},
 		"1.18.3": {
 			ReleaseDate: "2021-05-11",
-			Tarballs: map[string]string{
+			Tarballs: map[version.Platform]version.TarballURL{
 				"darwin/amd64": "https://getenvoy.io/versions/1.18.3/envoy-1.18.3-darwin-x86_64.tar.gz",
 				"linux/amd64":  "https://getenvoy.io/versions/1.18.3/envoy-1.18.3-linux-x86_64.tar.gz",
 			},
@@ -112,8 +112,8 @@ func TestAddAvailableVersions(t *testing.T) {
 	tests := []struct {
 		name     string
 		existing []versionReleaseDate
-		update   map[string]version.EnvoyVersion
-		platform string
+		update   map[version.Version]version.Release
+		platform version.Platform
 		expected []versionReleaseDate
 	}{
 		{
@@ -171,14 +171,14 @@ func TestAddAvailableVersions(t *testing.T) {
 func TestAddAvailableVersions_Validates(t *testing.T) {
 	tests := []struct {
 		name   string
-		update map[string]version.EnvoyVersion
+		update map[version.Version]version.Release
 	}{
 		{
 			name: "invalid releaseDate",
-			update: map[string]version.EnvoyVersion{
+			update: map[version.Version]version.Release{
 				"1.14.7": {
 					ReleaseDate: "ice cream",
-					Tarballs: map[string]string{
+					Tarballs: map[version.Platform]version.TarballURL{
 						"darwin/amd64": "https://getenvoy.io/versions/1.14.7/envoy-1.14.7-darwin-x86_64.tar.gz",
 					},
 				},
@@ -186,9 +186,9 @@ func TestAddAvailableVersions_Validates(t *testing.T) {
 		},
 		{
 			name: "missing releaseDate",
-			update: map[string]version.EnvoyVersion{
+			update: map[version.Version]version.Release{
 				"1.14.7": {
-					Tarballs: map[string]string{
+					Tarballs: map[version.Platform]version.TarballURL{
 						"darwin/amd64": "https://getenvoy.io/versions/1.14.7/envoy-1.14.7-darwin-x86_64.tar.gz",
 					},
 				},
diff --git a/internal/envoy/install_test.go b/internal/envoy/install_test.go
index 1d270c7..9f19e53 100644
--- a/internal/envoy/install_test.go
+++ b/internal/envoy/install_test.go
@@ -40,6 +40,8 @@ func TestUntarEnvoyError(t *testing.T) {
 	dst := filepath.Join(tempDir, "dst")
 	defer removeTempDir()
 
+	tarball, tarballSHA256sum := test.RequireFakeEnvoyTarGz(t, version.LastKnownEnvoy)
+
 	var realHandler func(w http.ResponseWriter, r *http.Request)
 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		if realHandler != nil {
@@ -50,9 +52,9 @@ func TestUntarEnvoyError(t *testing.T) {
 	}))
 	defer server.Close()
 
-	url := server.URL + "/file.tar.gz"
+	url := version.TarballURL(server.URL + "/file.tar.gz")
 	t.Run("error on incorrect URL", func(t *testing.T) {
-		err := untarEnvoy(ctx, dst, url, globals.CurrentPlatform, version.GetEnvoy)
+		err := untarEnvoy(ctx, dst, url, tarballSHA256sum, globals.CurrentPlatform, version.GetEnvoy)
 		require.EqualError(t, err, fmt.Sprintf(`received 404 status code from %s`, url))
 	})
 
@@ -60,7 +62,7 @@ func TestUntarEnvoyError(t *testing.T) {
 		w.WriteHeader(200)
 	}
 	t.Run("error on empty", func(t *testing.T) {
-		err := untarEnvoy(ctx, dst, url, globals.CurrentPlatform, version.GetEnvoy)
+		err := untarEnvoy(ctx, dst, url, tarballSHA256sum, globals.CurrentPlatform, version.GetEnvoy)
 		require.EqualError(t, err, fmt.Sprintf(`error untarring %s: EOF`, url))
 	})
 
@@ -69,19 +71,37 @@ func TestUntarEnvoyError(t *testing.T) {
 		w.Write([]byte("mary had a little lamb")) //nolint
 	}
 	t.Run("error on not a tar", func(t *testing.T) {
-		err := untarEnvoy(ctx, dst, url, globals.CurrentPlatform, version.GetEnvoy)
+		err := untarEnvoy(ctx, dst, url, tarballSHA256sum, globals.CurrentPlatform, version.GetEnvoy)
 		require.EqualError(t, err, fmt.Sprintf(`error untarring %s: gzip: invalid header`, url))
 	})
+
+	realHandler = func(w http.ResponseWriter, _ *http.Request) {
+		w.WriteHeader(200)
+		w.Write(tarball) //nolint
+	}
+	t.Run("error on wrong sha256sum a tar", func(t *testing.T) {
+		err := untarEnvoy(ctx, dst, url, "cafebabe", globals.CurrentPlatform, version.GetEnvoy)
+		require.EqualError(t, err, fmt.Sprintf(`expected SHA-256 sum "cafebabe", but have "%s" from %s`, tarballSHA256sum, url))
+	})
 }
 
 // TestUntarEnvoy doesn't test compression formats because that logic is in tar.Tar
 func TestUntarEnvoy(t *testing.T) {
-	o, cleanup := setupInstallTest(t)
-	defer cleanup()
+	tempDir, removeTempDir := morerequire.RequireNewTempDir(t)
+	defer removeTempDir()
+
+	tarball, tarballSHA256sum := test.RequireFakeEnvoyTarGz(t, version.LastKnownEnvoy)
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(200)
+		l, err := w.Write(tarball)
+		require.NoError(t, err)
+		require.Equal(t, len(tarball), l)
+	}))
+	defer server.Close()
 
-	err := untarEnvoy(o.ctx, o.tempDir, o.tarballURL, globals.CurrentPlatform, version.GetEnvoy)
+	err := untarEnvoy(context.Background(), tempDir, version.TarballURL(server.URL), tarballSHA256sum, globals.CurrentPlatform, version.GetEnvoy)
 	require.NoError(t, err)
-	require.FileExists(t, filepath.Join(o.tempDir, binEnvoy))
+	require.FileExists(t, filepath.Join(tempDir, binEnvoy))
 }
 
 func TestInstallIfNeeded_ErrorOnIncorrectURL(t *testing.T) {
@@ -99,18 +119,23 @@ func TestInstallIfNeeded_Validates(t *testing.T) {
 	o, cleanup := setupInstallTest(t)
 	defer cleanup()
 
-	tests := []struct{ name, goos, version, expectedErr string }{
+	tests := []struct {
+		name        string
+		p           version.Platform
+		v           version.Version
+		expectedErr string
+	}{
 		{
 			name:        "invalid version",
-			goos:        "darwin",
-			version:     `1.1.1`,
-			expectedErr: `couldn't find version "1.1.1" for platform "darwin"`,
+			p:           "darwin/amd64",
+			v:           `1.1.1`,
+			expectedErr: `couldn't find version "1.1.1" for platform "darwin/amd64"`,
 		},
 		{
 			name:        "unsupported OS",
-			goos:        "solaris",
-			version:     version.LastKnownEnvoy,
-			expectedErr: fmt.Sprintf(`couldn't find version %q for platform "solaris"`, version.LastKnownEnvoy),
+			p:           "solaris/amd64",
+			v:           version.LastKnownEnvoy,
+			expectedErr: fmt.Sprintf(`couldn't find version %q for platform "solaris/amd64"`, version.LastKnownEnvoy),
 		},
 	}
 
@@ -118,7 +143,7 @@ func TestInstallIfNeeded_Validates(t *testing.T) {
 		tc := tt
 		t.Run(tc.name, func(t *testing.T) {
 			o.Out = new(bytes.Buffer)
-			_, e := InstallIfNeeded(o.ctx, &o.GlobalOpts, tc.goos, tc.version)
+			_, e := InstallIfNeeded(o.ctx, &o.GlobalOpts, tc.p, tc.v)
 			require.EqualError(t, e, tc.expectedErr)
 			require.Empty(t, o.Out.(*bytes.Buffer))
 		})
@@ -139,7 +164,7 @@ func TestInstallIfNeeded(t *testing.T) {
 	versionDir := strings.Replace(envoyPath, binEnvoy, "", 1)
 	f, err := os.Stat(versionDir)
 	require.NoError(t, err)
-	require.Equal(t, f.ModTime().Format("2006-01-02"), test.FakeReleaseDate)
+	require.Equal(t, f.ModTime().Format("2006-01-02"), string(test.FakeReleaseDate))
 
 	require.Equal(t, fmt.Sprintln("downloading", o.tarballURL), out.String())
 }
@@ -184,7 +209,7 @@ func TestVerifyEnvoy(t *testing.T) {
 	tempDir, removeTempDir := morerequire.RequireNewTempDir(t)
 	defer removeTempDir()
 
-	envoyPath := filepath.Join(tempDir, "versions", version.LastKnownEnvoy)
+	envoyPath := filepath.Join(tempDir, "versions", string(version.LastKnownEnvoy))
 	require.NoError(t, os.MkdirAll(filepath.Join(envoyPath, "bin"), 0755))
 	t.Run("envoy binary doesn't exist", func(t *testing.T) {
 		EnvoyPath, e := verifyEnvoy(envoyPath)
@@ -211,7 +236,8 @@ func TestVerifyEnvoy(t *testing.T) {
 type installTest struct {
 	ctx context.Context
 	globals.GlobalOpts
-	tempDir, tarballURL string
+	tempDir    string
+	tarballURL version.TarballURL
 }
 
 func setupInstallTest(t *testing.T) (*installTest, func()) {
@@ -232,7 +258,7 @@ func setupInstallTest(t *testing.T) (*installTest, func()) {
 				EnvoyVersionsURL: versionsServer.URL + "/envoy-versions.json",
 				Out:              new(bytes.Buffer),
 				RunOpts: globals.RunOpts{
-					EnvoyPath: filepath.Join(tempDir, "versions", version.LastKnownEnvoy, binEnvoy),
+					EnvoyPath: filepath.Join(tempDir, "versions", string(version.LastKnownEnvoy), binEnvoy),
 				},
 			},
 		}, func() {
diff --git a/internal/envoy/version_test.go b/internal/envoy/version_test.go
index afe7fcf..f0b8939 100644
--- a/internal/envoy/version_test.go
+++ b/internal/envoy/version_test.go
@@ -86,7 +86,10 @@ func TestWriteCurrentVersion_HomeDir(t *testing.T) {
 	homeDir, removeHomeDir := morerequire.RequireNewTempDir(t)
 	defer removeHomeDir()
 
-	for _, tt := range []struct{ name, v string }{
+	for _, tt := range []struct {
+		name string
+		v    version.Version
+	}{
 		{"writes initial home version", "1.1.1"},
 		{"overwrites home version", "2.2.2"},
 	} {
@@ -116,13 +119,13 @@ func TestWriteCurrentVersion_OverwritesWorkingDirVersion(t *testing.T) {
 	require.NoError(t, WriteCurrentVersion("3.3.3", homeDir))
 	v, src, err := getCurrentVersion(homeDir)
 	require.NoError(t, err)
-	require.Equal(t, "3.3.3", v)
+	require.Equal(t, version.Version("3.3.3"), v)
 	require.Equal(t, CurrentVersionWorkingDirFile, src)
 
 	// didn't overwrite the home version
 	v, _, err = getHomeVersion(homeDir)
 	require.NoError(t, err)
-	require.Equal(t, "1.1.1", v)
+	require.Equal(t, version.Version("1.1.1"), v)
 }
 
 // TestCurrentVersion is intentionally written in priority order instead of via a matrix. This particularly helps with
@@ -134,7 +137,7 @@ func TestCurrentVersion(t *testing.T) {
 
 	t.Run("defaults to home version", func(t *testing.T) {
 		v, source, err := CurrentVersion(homeDir)
-		require.Equal(t, "1.1.1", v)
+		require.Equal(t, version.Version("1.1.1"), v)
 		require.Equal(t, CurrentVersionHomeDirFile, source)
 		require.NoError(t, err)
 	})
@@ -145,7 +148,7 @@ func TestCurrentVersion(t *testing.T) {
 
 	t.Run("prefers $PWD/.envoy-version over home version", func(t *testing.T) {
 		v, source, err := CurrentVersion(homeDir)
-		require.Equal(t, "2.2.2", v)
+		require.Equal(t, version.Version("2.2.2"), v)
 		require.Equal(t, CurrentVersionWorkingDirFile, source)
 		require.NoError(t, err)
 	})
@@ -155,7 +158,7 @@ func TestCurrentVersion(t *testing.T) {
 
 	t.Run("prefers $ENVOY_VERSION over $PWD/.envoy-version", func(t *testing.T) {
 		v, source, err := CurrentVersion(homeDir)
-		require.Equal(t, "3.3.3", v)
+		require.Equal(t, version.Version("3.3.3"), v)
 		require.Equal(t, currentVersionVar, source)
 		require.NoError(t, err)
 	})
diff --git a/internal/test/server.go b/internal/test/server.go
index 1649f11..fa28ad1 100644
--- a/internal/test/server.go
+++ b/internal/test/server.go
@@ -15,12 +15,14 @@
 package test
 
 import (
+	"crypto/sha256"
 	"encoding/json"
 	"fmt"
 	"io"
 	"net/http"
 	"net/http/httptest"
 	"os"
+	"path"
 	"path/filepath"
 	"runtime"
 	"strings"
@@ -36,7 +38,7 @@ import (
 
 const (
 	// FakeReleaseDate helps us make sure main code doesn't accidentally write the system time instead of the expected.
-	FakeReleaseDate = "2020-12-31"
+	FakeReleaseDate = version.ReleaseDate("2020-12-31")
 	// Even though currently binaries are compressed with "xz" more than "gz", using "gz" in tests allows us to re-use
 	// tar.TarGz instead of complicating internal utilities or adding dependencies only for tests.
 	archiveFormat = ".tar.gz"
@@ -44,33 +46,41 @@ const (
 )
 
 // RequireEnvoyVersionsTestServer serves "/envoy-versions.json", containing download links a fake Envoy archive.
-func RequireEnvoyVersionsTestServer(t *testing.T, v string) *httptest.Server {
+func RequireEnvoyVersionsTestServer(t *testing.T, v version.Version) *httptest.Server {
 	s := &server{t: t}
 	h := httptest.NewServer(s)
-	s.versions = version.EnvoyVersions{
+	s.versions = version.ReleaseVersions{
 		LatestVersion: v,
-		Versions: map[string]version.EnvoyVersion{ // hard-code date so that tests don't drift
-			v: {ReleaseDate: FakeReleaseDate, Tarballs: map[string]string{
-				"linux/" + runtime.GOARCH:  TarballURL(h.URL, "linux", runtime.GOARCH, v),
-				"darwin/" + runtime.GOARCH: TarballURL(h.URL, "darwin", runtime.GOARCH, v),
+		Versions: map[version.Version]version.Release{ // hard-code date so that tests don't drift
+			v: {ReleaseDate: FakeReleaseDate, Tarballs: map[version.Platform]version.TarballURL{
+				version.Platform("linux/" + runtime.GOARCH):   TarballURL(h.URL, "linux", runtime.GOARCH, v),
+				version.Platform("darwin/" + runtime.GOARCH):  TarballURL(h.URL, "darwin", runtime.GOARCH, v),
+				version.Platform("windows/" + runtime.GOARCH): TarballURL(h.URL, "windows", runtime.GOARCH, v),
 			}}},
+		SHA256Sums: map[version.Tarball]version.SHA256Sum{},
+	}
+	fakeEnvoyTarGz, sha256Sum := RequireFakeEnvoyTarGz(s.t, v)
+	s.fakeEnvoyTarGz = fakeEnvoyTarGz
+	for _, u := range s.versions.Versions[v].Tarballs {
+		s.versions.SHA256Sums[version.Tarball(path.Base(string(u)))] = sha256Sum
 	}
 	return h
 }
 
 // TarballURL gives the expected download URL for the given runtime.GOOS and Envoy version.
-func TarballURL(baseURL, goos, goarch, v string) string {
+func TarballURL(baseURL, goos, goarch string, v version.Version) version.TarballURL {
 	var arch = "x86_64"
 	if goarch != "arm64" {
 		arch = goarch
 	}
-	return fmt.Sprintf("%s%s%s/envoy-%s-%s-%s%s", baseURL, versionsPath, v, v, goos, arch, archiveFormat)
+	return version.TarballURL(fmt.Sprintf("%s%s%s/envoy-%s-%s-%s%s", baseURL, versionsPath, v, v, goos, arch, archiveFormat))
 }
 
 // server represents an HTTP server serving GetEnvoy versions.
 type server struct {
-	t        *testing.T
-	versions version.EnvoyVersions
+	t              *testing.T
+	versions       version.ReleaseVersions
+	fakeEnvoyTarGz []byte
 }
 
 func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -87,7 +97,7 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		require.Regexpf(s.t, `^1\.[1-9][0-9]\.[0-9]+$`, v, "unsupported version in uri %q", subpath)
 
 		w.WriteHeader(http.StatusOK)
-		_, err := w.Write(requireFakeEnvoyTarGz(s.t, v))
+		_, err := w.Write(s.fakeEnvoyTarGz)
 		require.NoError(s.t, err)
 	default:
 		w.WriteHeader(http.StatusNotFound)
@@ -100,12 +110,13 @@ func (s *server) getEnvoyVersions() []byte {
 	return data
 }
 
-func requireFakeEnvoyTarGz(t *testing.T, v string) []byte {
+// RequireFakeEnvoyTarGz makes a fake envoy.tar.gz
+func RequireFakeEnvoyTarGz(t *testing.T, v version.Version) ([]byte, version.SHA256Sum) {
 	tempDir, removeTempDir := morerequire.RequireNewTempDir(t)
 	defer removeTempDir()
 
 	// construct the platform directory based on the input version
-	installDir := filepath.Join(tempDir, v)
+	installDir := filepath.Join(tempDir, string(v))
 	require.NoError(t, os.MkdirAll(filepath.Join(installDir, "bin"), 0700)) //nolint:gosec
 	RequireFakeEnvoy(t, filepath.Join(installDir, "bin", "envoy"+moreos.Exe))
 
@@ -120,5 +131,5 @@ func requireFakeEnvoyTarGz(t *testing.T, v string) []byte {
 	defer f.Close() // nolint
 	b, err := io.ReadAll(f)
 	require.NoError(t, err)
-	return b
+	return b, version.SHA256Sum(fmt.Sprintf("%x", sha256.Sum256(b)))
 }
diff --git a/site/envoy-versions_test.go b/site/envoy-versions_test.go
index 7f7b1c6..26ad576 100644
--- a/site/envoy-versions_test.go
+++ b/site/envoy-versions_test.go
@@ -20,6 +20,7 @@ import (
 	"io"
 	"net/http"
 	"os"
+	"path"
 	"strings"
 	"testing"
 
@@ -51,19 +52,16 @@ func TestEnvoyVersionsJson(t *testing.T) {
 	data, err := os.ReadFile(envoyVersionsPath)
 	require.NoError(t, err)
 
-	evs := version.EnvoyVersions{}
+	evs := version.ReleaseVersions{}
 	err = json.Unmarshal(data, &evs)
 	require.NoErrorf(t, err, "error parsing json from %s", envoyVersionsPath)
-	require.Greaterf(t, len(evs.Versions), 2, "expected more than two versions")
+	require.NotZerof(t, len(evs.Versions), "expected at least one versions")
 
-	require.NotEmptyf(t, evs.LatestVersion, "latest version isn't in %s", envoyVersionsPath)
-	require.Containsf(t, evs.Versions, evs.LatestVersion, "latest version isn't in the version list of %s", envoyVersionsPath)
-	require.Equalf(t, evs.LatestVersion, version.LastKnownEnvoy, "version.LastKnownEnvoy doesn't match latest version in %s", envoyVersionsPath)
-
-	// Ensure there's an option besides the latest version
-	require.GreaterOrEqualf(t, len(evs.Versions), 2, "expected more than two versions")
-
-	type testCase struct{ version, platform, tarballURL string }
+	type testCase struct {
+		version    version.Version
+		platform   version.Platform
+		tarballURL version.TarballURL
+	}
 
 	var tests []testCase
 	for v, ev := range evs.Versions {
@@ -80,8 +78,13 @@ func TestEnvoyVersionsJson(t *testing.T) {
 		name := fmt.Sprintf("%s-%s", tc.version, tc.platform)
 		tarballURL := tc.tarballURL
 		t.Run(name, func(t *testing.T) {
-			require.Regexpf(t, "https://.*.tar.(gz|xz)", tarballURL, "expected an https tar.gz or xz %s", tarballURL)
-			res, err := http.Head(tarballURL)
+			require.Regexpf(t, "^https://.+/[a-zA-Z0-9-_.]+[.]tar[.][gx]z$", tarballURL, "expected an https tar.gz or xz %s", tarballURL)
+			tarball := version.Tarball(path.Base(string(tarballURL)))
+			require.Containsf(t, evs.SHA256Sums, tarball, "expected sha256Sums to include %s", tarball)
+			require.Regexpf(t, "^[0-9a-f]{64}$", evs.SHA256Sums[tarball], "unexpected SHA256Sum %s", tarballURL)
+
+			// We don't validate the SHA256Sum every time because it would consume a lot of bandwidth.
+			res, err := http.Head(string(tarballURL))
 			require.NoErrorf(t, err, "error from HEAD %s", tarballURL)
 			defer res.Body.Close() //nolint
 
@@ -92,8 +95,22 @@ func TestEnvoyVersionsJson(t *testing.T) {
 	}
 }
 
+func TestEnvoyVersionsJson_Latest(t *testing.T) {
+	data, err := os.ReadFile(envoyVersionsPath)
+	require.NoError(t, err)
+
+	evs := version.ReleaseVersions{}
+	err = json.Unmarshal(data, &evs)
+	require.NoErrorf(t, err, "error parsing json from %s", envoyVersionsPath)
+	require.NotZero(t, len(evs.Versions), "expected at least one versions")
+
+	require.NotEmptyf(t, evs.LatestVersion, "latest version isn't in %s", envoyVersionsPath)
+	require.Containsf(t, evs.Versions, evs.LatestVersion, "latest version isn't in the version list of %s", envoyVersionsPath)
+	require.Equalf(t, evs.LatestVersion, version.LastKnownEnvoy, "version.LastKnownEnvoy doesn't match latest version in %s", envoyVersionsPath)
+}
+
 // getEnvoyReleases returns release metadata we can use to verify ours
-func getEnvoyReleaseDates() (map[string]string, error) {
+func getEnvoyReleaseDates() (map[version.Version]version.ReleaseDate, error) {
 	url := "https://api.github.com/repos/envoyproxy/envoy/releases?per_page=100"
 	resp, err := http.Get(url)
 	if err != nil {
@@ -114,13 +131,13 @@ func getEnvoyReleaseDates() (map[string]string, error) {
 		return nil, fmt.Errorf("error unmarshalling GitHub Releases: %w", err)
 	}
 
-	m := map[string]string{}
+	m := map[version.Version]version.ReleaseDate{}
 	for _, r := range releases { //nolint:gocritic
 		if r.Draft || r.PreRelease {
 			continue
 		}
 		// clean inputs "v1.15.4" -> "2021-05-11T19:11:09Z" into "1.15.4" -> "2021-05-11"
-		m[strings.TrimPrefix(r.Name, "v")] = r.PublishedAt[0:10]
+		m[version.Version(strings.TrimPrefix(r.Name, "v"))] = version.ReleaseDate(r.PublishedAt[0:10])
 	}
 	return m, nil
 }