Repository: warrensbox/terraform-switcher (go)

Issue:
Verify Downloads Checksum & Signature
Due to the increasing frequency of supply chain attacks, it would be nice if tfswitch verified the checksums and gpg signatures of terraform when it downloads it for the first time, as described here:  
https://www.hashicorp.com/security#template-page-security:~:text=Release%20Archive%20Checksum%20Verification

Here's some example bash code that may provide a useful starting point for doing the equivalent in Go:
https://gist.github.com/markmsmith/cda59d5f24a812bea66fb3dbd7612397


Discussion:

Patch:
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+* text=auto
+* text eol=lf
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
 <!-- ![gopher](https://s3.us-east-2.amazonaws.com/kepler-images/warrensbox/tfswitch/logo.png =100x20) -->
 
 The `tfswitch` command line tool lets you switch between different versions of [terraform](https://www.terraform.io/).
-If you do not have a particular version of terraform installed, `tfswitch` will download the version you desire.
+If you do not have a particular version of terraform installed, `tfswitch` will download and verify the version you desire.
 The installation is minimal and easy.
 Once installed, simply select the version you require from the dropdown and start using terraform.
 
diff --git a/go.mod b/go.mod
--- a/go.mod
+++ b/go.mod
@@ -11,6 +11,7 @@ require (
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/pborman/getopt v1.1.0
 	github.com/spf13/viper v1.18.2
+	golang.org/x/crypto v0.16.0
 	golang.org/x/sys v0.19.0
 )
 
diff --git a/go.sum b/go.sum
--- a/go.sum
+++ b/go.sum
@@ -125,6 +125,8 @@ go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
 go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
+golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
 golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
 golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
 golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
diff --git a/lib/checksum.go b/lib/checksum.go
new file mode 100644
--- /dev/null
+++ b/lib/checksum.go
@@ -0,0 +1,84 @@
+package lib
+
+import (
+	"bufio"
+	"crypto/sha256"
+	"encoding/hex"
+	"golang.org/x/crypto/openpgp"
+	"io"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+// getChecksumFromFile Extract the checksum from the signature file
+func getChecksumFromHashFile(signatureFilePath string, terraformFileName string) (string, error) {
+	readFile, err := os.Open(signatureFilePath)
+	if err != nil {
+		logger.Errorf("Could not open %q: %v", signatureFilePath, err)
+		return "", err
+	}
+	defer readFile.Close()
+
+	scanner := bufio.NewScanner(readFile)
+	scanner.Split(bufio.ScanLines)
+	for scanner.Scan() {
+		split := strings.Split(scanner.Text(), "  ")
+		if split[1] == terraformFileName {
+			return split[0], nil
+		}
+	}
+	return "", nil
+}
+
+// checkChecksumMatches This will calculate and compare the check sum of the downloaded zip file
+func checkChecksumMatches(hashFile string, targetFile *os.File) bool {
+	var fileHandlersToClose []*os.File
+	fileHandlersToClose = append(fileHandlersToClose, targetFile)
+
+	_, fileName := filepath.Split(targetFile.Name())
+	expectedChecksum, err := getChecksumFromHashFile(hashFile, fileName)
+	if err != nil {
+		closeFileHandlers(fileHandlersToClose)
+		logger.Errorf("Could not get checksum from file %q: %v", hashFile, err)
+		return false
+	}
+	hash := sha256.New()
+	if _, err := io.Copy(hash, targetFile); err != nil {
+		closeFileHandlers(fileHandlersToClose)
+		logger.Errorf("Checksum calculation failed for %q: %v", fileName, err)
+		return false
+	}
+	checksum := hex.EncodeToString(hash.Sum(nil))
+	if expectedChecksum != checksum {
+		closeFileHandlers(fileHandlersToClose)
+		logger.Errorf("Checksum mismatch for %q. Expected: %q, calculated: %v", fileName, expectedChecksum, checksum)
+		return false
+	}
+	closeFileHandlers(fileHandlersToClose)
+	return true
+}
+
+// checkSignatureOfChecksums This will verify the signature of the file containing the hash sums
+func checkSignatureOfChecksums(keyRingReader *os.File, hashFile *os.File, signatureFile *os.File) bool {
+	var fileHandlersToClose []*os.File
+	fileHandlersToClose = append(fileHandlersToClose, keyRingReader)
+	fileHandlersToClose = append(fileHandlersToClose, hashFile)
+	fileHandlersToClose = append(fileHandlersToClose, signatureFile)
+	defer closeFileHandlers(fileHandlersToClose)
+
+	logger.Info("Verifying signature of checksum file...")
+	keyring, err := openpgp.ReadArmoredKeyRing(keyRingReader)
+	if err != nil {
+		logger.Errorf("Could not read armored key ring: %v", err)
+		return false
+	}
+
+	_, err = openpgp.CheckDetachedSignature(keyring, hashFile, signatureFile)
+	if err != nil {
+		logger.Errorf("Could not check detached signature: %v", err)
+		return false
+	}
+	logger.Info("Checksum file signature verification successful.")
+	return true
+}
diff --git a/lib/defaults.go b/lib/defaults.go
--- a/lib/defaults.go
+++ b/lib/defaults.go
@@ -7,6 +7,16 @@ import (
 	"github.com/mitchellh/go-homedir"
 )
 
+var (
+	PubKeyId     = "72D7468F"
+	PubKeyPrefix = "hashicorp_"
+	PubKeyUri    = "https://www.hashicorp.com/.well-known/pgp-key.txt"
+)
+
+const (
+	pubKeySuffix = ".asc"
+)
+
 // GetDefaultBin Get default binary path
 func GetDefaultBin() string {
 	var defaultBin = "/usr/local/bin/terraform"
diff --git a/lib/download.go b/lib/download.go
--- a/lib/download.go
+++ b/lib/download.go
@@ -9,11 +9,84 @@ import (
 	"strings"
 )
 
-// DownloadFromURL : Downloads the binary from the source url
-func DownloadFromURL(installLocation string, url string) (string, error) {
+// DownloadFromURL : Downloads the terraform binary and its hash from the source url
+func DownloadFromURL(installLocation string, mirrorURL string, tfversion string, versionPrefix string, goos string, goarch string) (string, error) {
+	pubKeyFilename := filepath.Join(installLocation, "/", PubKeyPrefix+PubKeyId+pubKeySuffix)
+	zipUrl := mirrorURL + tfversion + "/" + versionPrefix + tfversion + "_" + goos + "_" + goarch + ".zip"
+	hashUrl := mirrorURL + tfversion + "/" + versionPrefix + tfversion + "_SHA256SUMS"
+	hashSignatureUrl := mirrorURL + tfversion + "/" + versionPrefix + tfversion + "_SHA256SUMS." + PubKeyId + ".sig"
+
+	err := downloadPublicKey(installLocation, pubKeyFilename)
+	if err != nil {
+		logger.Error("Could not download public PGP key file.")
+		return "", err
+	}
+
+	logger.Infof("Downloading %q", zipUrl)
+	zipFilePath, err := downloadFromURL(installLocation, zipUrl)
+	if err != nil {
+		logger.Error("Could not download zip file.")
+		return "", err
+	}
+
+	logger.Infof("Downloading %q", hashUrl)
+	hashFilePath, err := downloadFromURL(installLocation, hashUrl)
+	if err != nil {
+		logger.Error("Could not download hash file.")
+		return "", err
+	}
+
+	logger.Infof("Downloading %q", hashSignatureUrl)
+	hashSigFilePath, err := downloadFromURL(installLocation, hashSignatureUrl)
+	if err != nil {
+		logger.Error("Could not download hash signature file.")
+		return "", err
+	}
+
+	publicKeyFile, err := os.Open(pubKeyFilename)
+	if err != nil {
+		logger.Errorf("Could not open public key %q: %v", pubKeyFilename, err)
+		return "", err
+	}
+
+	signatureFile, err := os.Open(hashSigFilePath)
+	if err != nil {
+		logger.Errorf("Could not open hash signature file %q: %v", hashSigFilePath, err)
+		return "", err
+	}
+
+	targetFile, err := os.Open(zipFilePath)
+	if err != nil {
+		logger.Errorf("Could not open zip file %q: %v", zipFilePath, err)
+		return "", err
+	}
+
+	hashFile, err := os.Open(hashFilePath)
+	if err != nil {
+		logger.Errorf("Could not open hash file %q: %v", hashFilePath, err)
+		return "", err
+	}
+
+	var filesToCleanup []string
+	filesToCleanup = append(filesToCleanup, hashFilePath)
+	filesToCleanup = append(filesToCleanup, hashSigFilePath)
+	defer cleanup(filesToCleanup)
+
+	verified := checkSignatureOfChecksums(publicKeyFile, hashFile, signatureFile)
+	if !verified {
+		return "", errors.New("Signature of checksum file could not be verified")
+	}
+	match := checkChecksumMatches(hashFilePath, targetFile)
+	if !match {
+		return "", errors.New("Checksums did not match")
+	}
+	return zipFilePath, err
+}
+
+func downloadFromURL(installLocation string, url string) (string, error) {
 	tokens := strings.Split(url, "/")
 	fileName := tokens[len(tokens)-1]
-	logger.Infof("Downloading to %q", installLocation)
+	logger.Infof("Downloading to %q", filepath.Join(installLocation, "/", fileName))
 
 	response, err := http.Get(url)
 	if err != nil {
@@ -28,10 +101,10 @@ func DownloadFromURL(installLocation string, url string) (string, error) {
 		return "", errors.New("Unable to download from " + url)
 	}
 
-	zipFile := filepath.Join(installLocation, fileName)
-	output, err := os.Create(zipFile)
+	filePath := filepath.Join(installLocation, fileName)
+	output, err := os.Create(filePath)
 	if err != nil {
-		logger.Errorf("Error while creating %q: %v", zipFile, err)
+		logger.Errorf("Error creating %q: %v", filePath, err)
 		return "", err
 	}
 	defer output.Close()
@@ -43,5 +116,34 @@ func DownloadFromURL(installLocation string, url string) (string, error) {
 	}
 
 	logger.Info(n, "bytes downloaded")
-	return zipFile, nil
+	return filePath, nil
+}
+
+func downloadPublicKey(installLocation string, targetFileName string) error {
+	logger.Debugf("Looking up public key file at %q", targetFileName)
+	publicKeyFileExists := FileExists(targetFileName)
+	if !publicKeyFileExists {
+		// Public key does not exist. Let's grab it from hashicorp
+		pubKeyFile, errDl := downloadFromURL(installLocation, PubKeyUri)
+		if errDl != nil {
+			logger.Errorf("Error fetching public key file from %s", PubKeyUri)
+			return errDl
+		}
+		errRename := os.Rename(pubKeyFile, targetFileName)
+		if errRename != nil {
+			logger.Errorf("Error renaming public key file from %q to %q", pubKeyFile, targetFileName)
+			return errRename
+		}
+	}
+	return nil
+}
+
+func cleanup(paths []string) {
+	for _, path := range paths {
+		logger.Infof("Deleting %q", path)
+		err := os.Remove(path)
+		if err != nil {
+			logger.Error("Error deleting %q: %v", path, err)
+		}
+	}
 }
diff --git a/lib/install.go b/lib/install.go
--- a/lib/install.go
+++ b/lib/install.go
@@ -120,12 +120,11 @@ func Install(tfversion string, binPath string, mirrorURL string) {
 
 	/* if selected version already exist, */
 	/* proceed to download it from the hashicorp release page */
-	url := mirrorURL + tfversion + "/" + versionPrefix + tfversion + "_" + goos + "_" + goarch + ".zip"
-	zipFile, errDownload := DownloadFromURL(installLocation, url)
+	zipFile, errDownload := DownloadFromURL(installLocation, mirrorURL, tfversion, versionPrefix, goos, goarch)
 
 	/* If unable to download file from url, exit(1) immediately */
 	if errDownload != nil {
-		logger.Fatalf("Error downloading %s: %v", url, errDownload)
+		logger.Fatalf("Error downloading: %s", errDownload)
 		os.Exit(1)
 	}
 
diff --git a/lib/utils.go b/lib/utils.go
new file mode 100644
--- /dev/null
+++ b/lib/utils.go
@@ -0,0 +1,21 @@
+package lib
+
+import (
+	"os"
+)
+
+// FileExists checks if a file exists and is not a directory before we try using it to prevent further errors
+func FileExists(filename string) bool {
+	info, err := os.Stat(filename)
+	if os.IsNotExist(err) {
+		return false
+	}
+	return !info.IsDir()
+}
+
+func closeFileHandlers(handlers []*os.File) {
+	for _, handler := range handlers {
+		logger.Debugf("Closing file handler %q", handler.Name())
+		_ = handler.Close()
+	}
+}
diff --git a/main.go b/main.go
--- a/main.go
+++ b/main.go
@@ -19,7 +19,6 @@ package main
 
 import (
 	"fmt"
-	"io/ioutil"
 	"os"
 	"path/filepath"
 	"strings"
@@ -64,6 +63,11 @@ func main() {
 	chDirPath := getopt.StringLong("chdir", 'c', dir, "Switch to a different working directory before executing the given command. Ex: tfswitch --chdir terraform_project will run tfswitch in the terraform_project directory")
 	versionFlag := getopt.BoolLong("version", 'v', "Displays the version of tfswitch")
 	defaultVersion := getopt.StringLong("default", 'd', defaultLatest, "Default to this version in case no other versions could be detected. Ex: tfswitch --default 1.2.4")
+
+	getopt.StringVarLong(&lib.PubKeyId, "public-key-id", 'k', "The ID of the public key to check the checksums against")
+	getopt.StringVarLong(&lib.PubKeyPrefix, "public-key-prefix", 'x', "The prefix of the public key. i.e. \"hashicorp_\"")
+	getopt.StringVarLong(&lib.PubKeyUri, "public-key-uri", 'y', "The URI to download the public key from")
+
 	helpFlag := getopt.BoolLong("help", 'h', "Displays help message")
 	_ = versionFlag
 
@@ -96,10 +100,10 @@ func main() {
 	 * If you provide a custom binary path with the -b option, this will override the bin value in the toml file
 	 * If you provide a version on the command line, this will override the version value in the toml file
 	 */
-	case fileExists(TOMLConfigFile) || fileExists(HomeTOMLConfigFile):
+	case lib.FileExists(TOMLConfigFile) || lib.FileExists(HomeTOMLConfigFile):
 		version := ""
 		binPath := *custBinPath
-		if fileExists(TOMLConfigFile) { //read from toml from current directory
+		if lib.FileExists(TOMLConfigFile) { //read from toml from current directory
 			version, binPath = getParamsTOML(binPath, *chDirPath)
 		} else { // else read from toml from home directory
 			version, binPath = getParamsTOML(binPath, homedir)
@@ -126,12 +130,12 @@ func main() {
 		case len(args) == 1:
 			installVersion(args[0], &binPath, mirrorURL)
 		/* provide an tfswitchrc file (IN ADDITION TO A TOML FILE) */
-		case fileExists(RCFile) && len(args) == 0:
+		case lib.FileExists(RCFile) && len(args) == 0:
 			readingFileMsg(rcFilename)
 			tfversion := retrieveFileContents(RCFile)
 			installVersion(tfversion, &binPath, mirrorURL)
 		/* if .terraform-version file found (IN ADDITION TO A TOML FILE) */
-		case fileExists(TFVersionFile) && len(args) == 0:
+		case lib.FileExists(TFVersionFile) && len(args) == 0:
 			readingFileMsg(tfvFilename)
 			tfversion := retrieveFileContents(TFVersionFile)
 			installVersion(tfversion, &binPath, mirrorURL)
@@ -144,7 +148,7 @@ func main() {
 			logger.Infof("\"TF_VERSION\" environment variable value: %q", tfversion)
 			installVersion(tfversion, &binPath, mirrorURL)
 		/* if terragrunt.hcl file found (IN ADDITION TO A TOML FILE) */
-		case fileExists(TGHACLFile) && checkVersionDefinedHCL(&TGHACLFile) && len(args) == 0:
+		case lib.FileExists(TGHACLFile) && checkVersionDefinedHCL(&TGHACLFile) && len(args) == 0:
 			installTGHclFile(&TGHACLFile, &binPath, mirrorURL)
 		// if no arg is provided - but toml file is provided
 		case version != "":
@@ -191,13 +195,13 @@ func main() {
 		installVersion(args[0], custBinPath, mirrorURL)
 
 	/* provide an tfswitchrc file */
-	case fileExists(RCFile) && len(args) == 0:
+	case lib.FileExists(RCFile) && len(args) == 0:
 		readingFileMsg(rcFilename)
 		tfversion := retrieveFileContents(RCFile)
 		installVersion(tfversion, custBinPath, mirrorURL)
 
 	/* if .terraform-version file found */
-	case fileExists(TFVersionFile) && len(args) == 0:
+	case lib.FileExists(TFVersionFile) && len(args) == 0:
 		readingFileMsg(tfvFilename)
 		tfversion := retrieveFileContents(TFVersionFile)
 		installVersion(tfversion, custBinPath, mirrorURL)
@@ -207,7 +211,7 @@ func main() {
 		installTFProvidedModule(*chDirPath, custBinPath, mirrorURL)
 
 	/* if terragrunt.hcl file found */
-	case fileExists(TGHACLFile) && checkVersionDefinedHCL(&TGHACLFile) && len(args) == 0:
+	case lib.FileExists(TGHACLFile) && checkVersionDefinedHCL(&TGHACLFile) && len(args) == 0:
 		installTGHclFile(&TGHACLFile, custBinPath, mirrorURL)
 
 	/* if Terraform Version environment variable is set */
@@ -315,7 +319,7 @@ func installVersion(arg string, custBinPath *string, mirrorURL *string) {
 
 // retrive file content of regular file
 func retrieveFileContents(file string) string {
-	fileContents, err := ioutil.ReadFile(file)
+	fileContents, err := os.ReadFile(file)
 	if err != nil {
 		logger.Fatalf("Failed reading %q file: %v\n Follow the README.md instructions for setup: https://github.com/warrensbox/terraform-switcher/blob/master/README.md", tfvFilename, err)
 		os.Exit(1)
@@ -329,15 +333,6 @@ func readingFileMsg(filename string) {
 	logger.Infof("Reading file %q", filename)
 }
 
-// fileExists checks if a file exists and is not a directory before we try using it to prevent further errors.
-func fileExists(filename string) bool {
-	info, err := os.Stat(filename)
-	if os.IsNotExist(err) {
-		return false
-	}
-	return !info.IsDir()
-}
-
 // fileExists checks if a file exists and is not a directory before we
 // try using it to prevent further errors.
 func checkTFModuleFileExist(dir string) bool {
diff --git a/test-data/checksum-check-file b/test-data/checksum-check-file
new file mode 100644
--- /dev/null
+++ b/test-data/checksum-check-file
@@ -0,0 +1,7 @@
+Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
+
+Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
+
+Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi.
+
+Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat.
diff --git a/test-data/terraform_1.7.5_SHA256SUMS b/test-data/terraform_1.7.5_SHA256SUMS
new file mode 100644
--- /dev/null
+++ b/test-data/terraform_1.7.5_SHA256SUMS
@@ -0,0 +1,15 @@
+0eaf64e28f82e2defd06f7a6f3187d8cea03d5d9fcd2af54f549a6c32d6833f7  terraform_1.7.5_darwin_amd64.zip
+99c4d4feafb0183af2f7fbe07beeea6f83e5f5a29ae29fee3168b6810e37ff98  terraform_1.7.5_darwin_arm64.zip
+3885fd4ce29788e040cfb350db88c6c1f3e34491c3f89f7a36ff8476f6c03959  terraform_1.7.5_freebsd_386.zip
+3846d408255336460f40d7f5eeb7e987936f0359c9f45d1eb659a09f3d8893ab  terraform_1.7.5_freebsd_amd64.zip
+b02aa14ecb4052482bf4e64fca7de7a743fce691511f96b4cd58610350886c9e  terraform_1.7.5_freebsd_arm.zip
+e188ce5b45c9d10fa9a5a118438add2eab056d16587f774ecab93e3ca39e1bca  terraform_1.7.5_linux_386.zip
+3ff056b5e8259003f67fd0f0ed7229499cfb0b41f3ff55cc184088589994f7a5  terraform_1.7.5_linux_amd64.zip
+4e74db9394d5cdf0f91cf8fecd290216edf6cf06273eb8f55e35f26eac4a936a  terraform_1.7.5_linux_arm.zip
+08631c385667dd28f03b3a3f77cb980393af4a2fcfc2236c148a678ad9150c8c  terraform_1.7.5_linux_arm64.zip
+c0416b6b9fe0155bb3377e39a3f584b9f7b9a11a1236b9ea8cf7c074a804a513  terraform_1.7.5_openbsd_386.zip
+3bd02023764365b7ae7ee3e597a6258e896049e3fd115896bc01113384864cba  terraform_1.7.5_openbsd_amd64.zip
+385af229bd76a058c221b9c0be56f02a7d0fa2535620040c9c895df00e0f09ee  terraform_1.7.5_solaris_amd64.zip
+2639c9444c6091fd5ad76f112040d592e99931489582ada4d485c12a64a79052  terraform_1.7.5_windows_386.zip
+9b7be6ae159191ec1f4b5b9d27529ae5243e41020fb545c0041235bec8d92269  terraform_1.7.5_windows_amd64.zip
+4083a1996695af4c8d1ac1079c47746cbf4bb58011faa644311c98a55b1de630  checksum-check-file


Test patch:
diff --git a/lib/checksum_test.go b/lib/checksum_test.go
new file mode 100644
--- /dev/null
+++ b/lib/checksum_test.go
@@ -0,0 +1,29 @@
+package lib
+
+import (
+	"os"
+	"testing"
+)
+
+func Test_getChecksumFromHashFile(t *testing.T) {
+	expected := "3ff056b5e8259003f67fd0f0ed7229499cfb0b41f3ff55cc184088589994f7a5"
+	got, err := getChecksumFromHashFile("../test-data/terraform_1.7.5_SHA256SUMS", "terraform_1.7.5_linux_amd64.zip")
+	if err != nil {
+		t.Errorf("getChecksumFromHashFile() error = %v", err)
+		return
+	}
+	if got != expected {
+		t.Errorf("getChecksumFromHashFile() got = %v, expected %v", got, expected)
+	}
+}
+
+func Test_checkChecksumMatches(t *testing.T) {
+	targetFile, err := os.Open("../test-data/checksum-check-file")
+	if err != nil {
+		t.Errorf("[Error]: Could not open testfile for signature verification.")
+	}
+
+	if got := checkChecksumMatches("../test-data/terraform_1.7.5_SHA256SUMS", targetFile); got != true {
+		t.Errorf("checkChecksumMatches() = %v, want %v", got, true)
+	}
+}
diff --git a/lib/download_test.go b/lib/download_test.go
--- a/lib/download_test.go
+++ b/lib/download_test.go
@@ -47,9 +47,9 @@ func TestDownloadFromURL_FileNameMatch(t *testing.T) {
 	/* test download old terraform version */
 	lowestVersion := "0.11.0"
 
-	url := hashiURL + lowestVersion + "/" + installVersion + lowestVersion + macOS
+	urlToDownload := hashiURL + lowestVersion + "/" + installVersion + lowestVersion + macOS
 	expectedFile := filepath.Join(installLocation, installVersion+lowestVersion+macOS)
-	installedFile, errDownload := DownloadFromURL(installLocation, url)
+	installedFile, errDownload := downloadFromURL(installLocation, urlToDownload)
 
 	if errDownload != nil {
 		t.Logf("Expected file name %v to be downloaded", expectedFile)