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 Mar 28, 2024
1 parent a487630 commit c932b6f
Show file tree
Hide file tree
Showing 13 changed files with 355 additions and 476 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
34 changes: 19 additions & 15 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,35 @@ require (
github.com/manifoldco/promptui v0.9.0
github.com/mitchellh/go-homedir v1.1.0
github.com/pborman/getopt v1.1.0
github.com/spf13/viper v1.12.0
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b
github.com/spf13/viper v1.18.2
golang.org/x/crypto v0.21.0
golang.org/x/sys v0.18.0
)

require (
github.com/agext/levenshtein v1.2.2 // indirect
github.com/apparentlymart/go-textseg v1.0.0 // indirect
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/hcl/v2 v2.0.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.3.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/zclconf/go-cty v1.1.0 // indirect
golang.org/x/text v0.7.0 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
498 changes: 56 additions & 442 deletions go.sum

Large diffs are not rendered by default.

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)
_, err := os.Stat(targetFileName)
if err != nil {
// 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
}
Loading

0 comments on commit c932b6f

Please sign in to comment.