From 04106d15d5ad73db6e670e84340ac1f9be200a23 Mon Sep 17 00:00:00 2001 From: garethgeorge Date: Mon, 15 Jan 2024 14:02:07 -0800 Subject: [PATCH] feat: verify gpg signature when downloading and installing restic binary --- internal/resticinstaller/resticinstaller.go | 137 ++++++++++---- internal/resticinstaller/verify.go | 188 ++++++++++++++++++++ scripts/latest-restic-version.sh | 6 + 3 files changed, 298 insertions(+), 33 deletions(-) create mode 100644 internal/resticinstaller/verify.go create mode 100755 scripts/latest-restic-version.sh diff --git a/internal/resticinstaller/resticinstaller.go b/internal/resticinstaller/resticinstaller.go index 5c477a2d..ef8a9adb 100644 --- a/internal/resticinstaller/resticinstaller.go +++ b/internal/resticinstaller/resticinstaller.go @@ -4,6 +4,9 @@ import ( "archive/zip" "bytes" "compress/bzip2" + "context" + "crypto/sha256" + "encoding/hex" "errors" "fmt" "io" @@ -36,68 +39,122 @@ func resticDownloadURL(version string) string { return fmt.Sprintf("https://github.com/restic/restic/releases/download/v%v/restic_%v_%v_%v.bz2", version, version, runtime.GOOS, runtime.GOARCH) } -func downloadFile(url string, downloadPath string) error { - // Download ur as a file and save it to path +func hashDownloadURL(version string) string { + return fmt.Sprintf("https://github.com/restic/restic/releases/download/v%v/SHA256SUMS", version) +} + +func sigDownloadURL(version string) string { + return fmt.Sprintf("https://github.com/restic/restic/releases/download/v%v/SHA256SUMS.asc", version) +} + +// getURL downloads the given url and returns the response body as a string. +func getURL(url string) ([]byte, error) { resp, err := http.Get(url) if err != nil { - return fmt.Errorf("http GET %v: %w", url, err) + return nil, fmt.Errorf("http GET %v: %w", url, err) + } + defer resp.Body.Close() + + var body bytes.Buffer + _, err = io.Copy(&body, resp.Body) + if err != nil { + return nil, fmt.Errorf("copy response body to buffer: %w", err) + } + return body.Bytes(), nil +} + +func verify(sha256 string) error { + sha256sums, err := getURL(hashDownloadURL(RequiredResticVersion)) + if err != nil { + return fmt.Errorf("get sha256sums: %w", err) + } + + signature, err := getURL(sigDownloadURL(RequiredResticVersion)) + if err != nil { + return fmt.Errorf("get signature: %w", err) + } + + if ok, err := gpgVerify(sha256sums, signature); !ok || err != nil { + return fmt.Errorf("gpg verification failed: ok=%v err=%v", ok, err) + } + + if !strings.Contains(string(sha256sums), sha256) { + fmt.Fprintf(os.Stderr, "sha256sums:\n%v\n", string(sha256sums)) + return fmt.Errorf("sha256sums do not contain %v", sha256) + } + + return nil +} + +// downloadFile downloads a file from the given url and saves it to the given path. The sha256 checksum of the file is returned on success. +func downloadFile(url string, downloadPath string) (string, error) { + // Download ur as a file and save it to path + req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil) + if err != nil { + return "", err + } + req.Header.Set("Accept", "application/octet-stream") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", fmt.Errorf("http GET %v: %w", url, err) } defer resp.Body.Close() - var body io.Reader = resp.Body + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("http GET %v: %v", url, resp.Status) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("read response body: %w", err) + } + hash := sha256.Sum256(body) + if strings.HasSuffix(url, ".bz2") { - body = bzip2.NewReader(resp.Body) + body, err = io.ReadAll(bzip2.NewReader(bytes.NewReader(body))) + if err != nil { + return "", fmt.Errorf("bz2 decompress body: %w", err) + } } else if strings.HasSuffix(url, ".zip") { var fullBody bytes.Buffer _, err := io.Copy(&fullBody, resp.Body) if err != nil { - return fmt.Errorf("copy response body to buffer: %w", err) + return "", fmt.Errorf("copy response body to buffer: %w", err) } archive, err := zip.NewReader(bytes.NewReader(fullBody.Bytes()), int64(fullBody.Len())) if err != nil { - return fmt.Errorf("open zip archive: %w", err) + return "", fmt.Errorf("open zip archive: %w", err) } if len(archive.File) != 1 { - return fmt.Errorf("expected zip archive to contain exactly one file, got %v", len(archive.File)) + return "", fmt.Errorf("expected zip archive to contain exactly one file, got %v", len(archive.File)) + } + f, err := archive.File[0].Open() + if err != nil { + return "", fmt.Errorf("open zip archive file %v: %w", archive.File[0].Name, err) } - body, err = archive.File[0].Open() + + body, err = io.ReadAll(f) if err != nil { - return fmt.Errorf("open zip archive file %v: %w", archive.File[0].Name, err) + return "", fmt.Errorf("read zip archive file %v: %w", archive.File[0].Name, err) } } out, err := os.Create(downloadPath) if err != nil { - return fmt.Errorf("create file %v: %w", downloadPath, err) + return "", fmt.Errorf("create file %v: %w", downloadPath, err) } defer out.Close() if err != nil { - return fmt.Errorf("create file %v: %w", downloadPath, err) + return "", fmt.Errorf("create file %v: %w", downloadPath, err) } - _, err = io.Copy(out, body) + _, err = io.Copy(out, bytes.NewReader(body)) if err != nil { - return fmt.Errorf("copy response body to file %v: %w", downloadPath, err) - } - - return nil -} - -func downloadExecutable(url string, path string) error { - if err := downloadFile(url, path+".tmp"); err != nil { - return err - } - - if err := os.Chmod(path+".tmp", 0755); err != nil { - return fmt.Errorf("chmod executable %v: %w", path, err) + return "", fmt.Errorf("copy response body to file %v: %w", downloadPath, err) } - if err := os.Rename(path+".tmp", path); err != nil { - return fmt.Errorf("rename %v.tmp to %v: %w", path, path, err) - } - - return nil + return hex.EncodeToString(hash[:]), nil } func installResticIfNotExists(resticInstallPath string) error { @@ -112,8 +169,22 @@ func installResticIfNotExists(resticInstallPath string) error { return fmt.Errorf("create restic install directory %v: %w", path.Dir(resticInstallPath), err) } - if err := downloadExecutable(resticDownloadURL(RequiredResticVersion), resticInstallPath); err != nil { - return fmt.Errorf("download restic version %v: %w", RequiredResticVersion, err) + hash, err := downloadFile(resticDownloadURL(RequiredResticVersion), resticInstallPath+".tmp") + if err != nil { + return err + } + + if err := verify(hash); err != nil { + os.Remove(resticInstallPath) // try to remove the bad binary. + return fmt.Errorf("failed to verify the authenticity of the downloaded restic binary: %v", err) + } + + if err := os.Chmod(resticInstallPath+".tmp", 0755); err != nil { + return fmt.Errorf("chmod executable %v: %w", resticInstallPath, err) + } + + if err := os.Rename(resticInstallPath+".tmp", resticInstallPath); err != nil { + return fmt.Errorf("rename %v.tmp to %v: %w", resticInstallPath, resticInstallPath, err) } return nil diff --git a/internal/resticinstaller/verify.go b/internal/resticinstaller/verify.go new file mode 100644 index 00000000..49610a44 --- /dev/null +++ b/internal/resticinstaller/verify.go @@ -0,0 +1,188 @@ +package resticinstaller + +// Original implementation in restic: https://github.com/restic/restic/blob/master/internal/selfupdate/verify.go + +import ( + "bytes" + "fmt" + + "golang.org/x/crypto/openpgp" +) + +var key = []byte(` +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +CwGc +=AV20 +-----END PGP PUBLIC KEY BLOCK----- +`) + +// gpgVerify checks the authenticity of data by verifying the signature sig, +// which must be ASCII armored (base64). When the signature matches, GPGVerify +// returns true and a nil error. +func gpgVerify(data, sig []byte) (ok bool, err error) { + keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(key)) + if err != nil { + return false, fmt.Errorf("read keyring fialed: %w", err) + } + + _, err = openpgp.CheckArmoredDetachedSignature(keyring, bytes.NewReader(data), bytes.NewReader(sig)) + if err != nil { + return false, err + } + + return true, nil +} diff --git a/scripts/latest-restic-version.sh b/scripts/latest-restic-version.sh new file mode 100755 index 00000000..e11a6eca --- /dev/null +++ b/scripts/latest-restic-version.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +curl -s https://api.github.com/repos/restic/restic/releases/latest \ +| grep "https://api.github.com/repos/restic/restic/tarball/" \ +| sed -E 's/.*v([0-9]+\.[0-9]+\.[0-9]+).*/\1/ +