Skip to content

Commit

Permalink
- implemented check for signature and checksums for warrensbox#160 and
Browse files Browse the repository at this point in the history
…warrensbox#290

- added github action for testing
- added test for checksum matching
- add gitattributes for windows testing. if not present the lf line endings will be converted to crlf which messes with the checksum tests.
- update changelog and readme
  • Loading branch information
MatrixCrawler committed Apr 2, 2024
1 parent 6aaa2c4 commit ca5001b
Show file tree
Hide file tree
Showing 15 changed files with 298 additions and 35 deletions.
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* text=auto
* text eol=lf
30 changes: 30 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: Go

on:
push:
branches: [ '*' ]

jobs:

integration_tests:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
go_version: ['1.22']
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go_version }}

- name: Build
run: go build -v ./...

- name: Test
run: go test -v ./...
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
### Added
- added verification of the checksums for downloaded terraform files.

## [0.13.1201] - 2021-11-28
### Bug fixes
- No matter what users pass to --bin or -b, the local binary is called terraform. User can resume old behavior where -b is custom
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,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.21.0
golang.org/x/sys v0.18.0
)

Expand Down
9 changes: 7 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,22 @@ github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3a
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/errwrap v0.0.0-20180715044906-d6c0cd880357/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v0.0.0-20180717150148-3d5d8f294aa0/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/hcl/v2 v2.0.0 h1:efQznTz+ydmQXq3BOnRa3AXzvCeTq1P4dKj/z5GLlY8=
github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90=
github.com/hashicorp/hcl2 v0.0.0-20191002203319-fb75b3253c80 h1:PFfGModn55JA0oBsvFghhj0v93me+Ctr3uHC/UmFAls=
github.com/hashicorp/hcl2 v0.0.0-20191002203319-fb75b3253c80/go.mod h1:Cxv+IJLuBiEhQ7pBYGEuORa0nr4U994pE8mYLuFd7v0=
github.com/hashicorp/terraform-config-inspect v0.0.0-20231204233900-a34142ec2a72 h1:nZ5gGjbe5o7XUu1d7j+Y5Ztcxlp+yaumTKH9i0D3wlg=
github.com/hashicorp/terraform-config-inspect v0.0.0-20231204233900-a34142ec2a72/go.mod h1:l8HcFPm9cQh6Q0KSWoYPiePqMvRFenybP1CH2MjKdlg=
github.com/hashicorp/terraform-config-inspect v0.0.0-20211115214459-90acf1ca460f h1:R8UIC07Ha9jZYkdcJ51l4ownCB8xYwfJtrgZSMvqjWI=
github.com/hashicorp/terraform-config-inspect v0.0.0-20211115214459-90acf1ca460f/go.mod h1:Z0Nnk4+3Cy89smEbrq+sl1bxc9198gIP4I7wcQF6Kqs=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
Expand Down Expand Up @@ -87,6 +89,7 @@ github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNo
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
Expand All @@ -112,6 +115,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.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down
73 changes: 73 additions & 0 deletions lib/checksum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package lib

import (
"bufio"
"crypto/sha256"
"encoding/hex"
"fmt"
"golang.org/x/crypto/openpgp"
"io"
"log"
"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 {
fmt.Println("[Error]: Could not open ", signatureFilePath)
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 {
_, fileName := filepath.Split(targetFile.Name())
expectedChecksum, err := getChecksumFromHashFile(hashFile, fileName)
if err != nil {
fmt.Println("[Error]: could not get expected checksum from file: " + err.Error())
return false
}
hash := sha256.New()
if _, err := io.Copy(hash, targetFile); err != nil {
fmt.Println("[Error]: Calculating Checksum failed: " + err.Error())
return false
}
checksum := hex.EncodeToString(hash.Sum(nil))
if expectedChecksum != checksum {
fmt.Println("[Error]: Checksum mismatch. Expected: ", expectedChecksum, " got ", checksum)
return false
}
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 {
log.Println("Verifying signature of checksum file...")
keyring, err := openpgp.ReadArmoredKeyRing(keyRingReader)
if err != nil {
log.Fatal("[Error]: Read armored key ring: " + err.Error())
return false
}

_, err = openpgp.CheckDetachedSignature(keyring, hashFile, signatureFile)
if err != nil {
log.Fatal("[Error]: Checking detached signature: " + err.Error())
return false
}
log.Println("Verification successful.")
return true
}
29 changes: 29 additions & 0 deletions lib/checksum_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
111 changes: 103 additions & 8 deletions lib/download.go
Original file line number Diff line number Diff line change
@@ -1,47 +1,142 @@
package lib

import (
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
)

// DownloadFromURL : Downloads the binary from the source url
func DownloadFromURL(installLocation string, url string) (string, error) {
const (
pubKeyId = "72D7468F"
pubKeyPrefix = "hashicorp_"
pubKeySuffix = ".asc"
pubKeyUri = "https://www.hashicorp.com/.well-known/pgp-key.txt"
)

// 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 {
log.Fatal("[Error]: Could not download public key file")
return "", err
}

log.Println("Downloading ", zipUrl)
zipFilePath, err := downloadFromURL(installLocation, zipUrl)
if err != nil {
log.Fatal("[Error]: Could not download zip file")
return "", err
}

log.Println("Downloading ", hashUrl)
hashFilePath, err := downloadFromURL(installLocation, hashUrl)
if err != nil {
log.Fatal("[Error]: Could not download hash file")
return "", err
}

log.Println("Downloading ", hashSignatureUrl)
hashSigFilePath, err := downloadFromURL(installLocation, hashSignatureUrl)
if err != nil {
log.Fatal("[Error]: Could not download hash signature file")
return "", err
}

publicKeyFile, err := os.Open(pubKeyFilename)
if err != nil {
log.Fatal("[Error]: Could not open the public key")
return "", err
}

signatureFile, err := os.Open(hashSigFilePath)
if err != nil {
log.Fatal("[Error]: Could not open the public key")
return "", err
}

targetFile, err := os.Open(zipFilePath)
if err != nil {
log.Fatal("[Error]: Could not open the terraform binary for signature verification.")
return "", err
}

hashFile, err := os.Open(hashFilePath)
if err != nil {
log.Fatal("[Error]: Could not open the terraform binary for signature verification.")
return "", err
}
verified := checkSignatureOfChecksums(publicKeyFile, hashFile, signatureFile)
if !verified {
return "", errors.New("signature of checksum files 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]
fmt.Printf("Downloading to: %s\n", installLocation)
log.Printf("Downloading to: %s\n", filepath.Join(installLocation, "/", fileName))

response, err := http.Get(url)
if err != nil {
fmt.Println("[Error] : Error while downloading", url, "-", err)
log.Fatal("[Error] : Error while downloading", url, "-", err)
return "", err
}
defer response.Body.Close()

if response.StatusCode != 200 {
//Sometimes hashicorp terraform file names are not consistent
//For example 0.12.0-alpha4 naming convention in the release repo is not consistent
return "", fmt.Errorf("[Error] : Unable to download from %s", url)
log.Fatalf("[Error] : Unable to download from %s", url)
}

zipFile := filepath.Join(installLocation, fileName)
output, err := os.Create(zipFile)
if err != nil {
fmt.Println("[Error] : Error while creating", zipFile, "-", err)
log.Fatal("[Error] : Error while creating", zipFile, "-", err)
return "", err
}
defer output.Close()

n, err := io.Copy(output, response.Body)
if err != nil {
fmt.Println("[Error] : Error while downloading", url, "-", err)
log.Fatal("[Error] : Error while writing file", url, "-", err)
return "", err
}

fmt.Println(n, "bytes downloaded")
log.Println(n, "bytes downloaded")
return zipFile, nil
}

func downloadPublicKey(installLocation string, targetFileName string) error {
fmt.Println("Looking up public key file at ", 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 {
log.Fatal("[Error]: Error while fetching the public key file from ", pubKeyUri)
return errDl
}
errRename := os.Rename(pubKeyFile, targetFileName)
if errRename != nil {
log.Fatal("[Error]: Error while renaming the public key file from ", pubKeyFile, " to ", targetFileName)
return errRename
}
}
return nil
}
7 changes: 4 additions & 3 deletions lib/download_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ func TestDownloadFromURL_FileNameMatch(t *testing.T) {
installPath := fmt.Sprintf(tempDir + string(os.PathSeparator) + ".terraform.versions_test")
macOS := "_darwin_amd64.zip"

home, err := homedir.Dir()
if err != nil {
log.Fatalf("Could not detect home directory.")
// get current user
home, errCurr := homedir.Dir()
if errCurr != nil {
log.Fatal(errCurr)
}

fmt.Printf("Current user homedir: %v \n", home)
Expand Down
3 changes: 1 addition & 2 deletions lib/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,7 @@ 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 {
Expand Down
12 changes: 12 additions & 0 deletions lib/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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()
}
Loading

0 comments on commit ca5001b

Please sign in to comment.