Skip to content

Commit c099110

Browse files
authored
Add checksum package tests and log file contents in case of problems (#4244)
* Add dumping hash file contents in logs when validation fails * add package tests for checksums * add tests for TestVerifySHA512HashWithCleanup * add skip for permission test on windows * fixup! add skip for permission test on windows
1 parent bdefb2c commit c099110

File tree

3 files changed

+203
-1
lines changed

3 files changed

+203
-1
lines changed

dev-tools/packaging/package_test.go

+72
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ import (
1313
"bufio"
1414
"bytes"
1515
"compress/gzip"
16+
"crypto/sha512"
17+
"encoding/hex"
1618
"encoding/json"
1719
"errors"
1820
"flag"
1921
"fmt"
22+
"hash"
2023
"io"
2124
"os"
2225
"path"
@@ -174,6 +177,8 @@ func checkTar(t *testing.T, file string) {
174177
containingDir := strings.TrimSuffix(path.Base(file), ".tar.gz")
175178
checkManifestFileContents(t, filepath.Join(tempExtractionPath, containingDir))
176179
})
180+
181+
checkSha512PackageHash(t, file)
177182
}
178183

179184
func checkZip(t *testing.T, file string) {
@@ -197,6 +202,8 @@ func checkZip(t *testing.T, file string) {
197202
containingDir := strings.TrimSuffix(path.Base(file), ".zip")
198203
checkManifestFileContents(t, filepath.Join(tempExtractionPath, containingDir))
199204
})
205+
206+
checkSha512PackageHash(t, file)
200207
}
201208

202209
func checkManifestFileContents(t *testing.T, extractedPackageDir string) {
@@ -939,3 +946,68 @@ func readDockerManifest(r io.Reader) (*dockerManifest, error) {
939946

940947
return manifests[0], nil
941948
}
949+
950+
func checkSha512PackageHash(t *testing.T, packageFile string) {
951+
t.Run("check hash file", func(t *testing.T) {
952+
expectedHashFile := packageFile + ".sha512"
953+
require.FileExists(t, expectedHashFile, "hash file for package %q should exist with name %q", packageFile, expectedHashFile)
954+
955+
// calculate SHA512 hash for the file
956+
hashFile, err := os.Open(expectedHashFile)
957+
require.NoError(t, err, "hash file should be readable")
958+
959+
checksumsMap := readHashFile(t, hashFile)
960+
961+
packageBaseName := filepath.Base(packageFile)
962+
require.Containsf(t, checksumsMap, packageBaseName, "checksum file should contain an entry for %q", packageBaseName)
963+
964+
// compare checksum entry with actual package hash
965+
checksum := calculateChecksum(t, packageFile, sha512.New())
966+
967+
assert.Equalf(t, checksum, checksumsMap[packageBaseName], "checksum for file %q does not match", packageFile)
968+
})
969+
}
970+
971+
func calculateChecksum(t *testing.T, file string, hasher hash.Hash) string {
972+
973+
input, err := os.Open(file)
974+
require.NoErrorf(t, err, "error opening input file %q", file)
975+
976+
defer func(input *os.File) {
977+
errClose := input.Close()
978+
assert.NoErrorf(t, errClose, "error closing input file %q", file)
979+
}(input)
980+
981+
_, err = io.Copy(hasher, input)
982+
require.NoError(t, err, "error reading file to calculate hash")
983+
984+
return hex.EncodeToString(hasher.Sum(nil))
985+
}
986+
987+
// readHashFile return a map of {filename, hash} reading a .sha512 file.
988+
// If any line has not exactly 2 tokens separated by white spaces, it will fail the test.
989+
// When it's done reading it will close the reader
990+
func readHashFile(t *testing.T, reader io.ReadCloser) map[string]string {
991+
992+
defer func(reader io.ReadCloser) {
993+
err := reader.Close()
994+
assert.NoError(t, err, "error closing hash file reader")
995+
}(reader)
996+
997+
checksums := map[string]string{}
998+
scanner := bufio.NewScanner(reader)
999+
for scanner.Scan() {
1000+
line := scanner.Text()
1001+
parts := strings.Fields(line)
1002+
if len(parts) != 2 {
1003+
// Fail test because it's malformed.
1004+
assert.Failf(t, "malformed line %q in hash file", line)
1005+
continue
1006+
}
1007+
filename := strings.TrimLeft(parts[1], "*")
1008+
checksum := parts[0]
1009+
checksums[filename] = checksum
1010+
}
1011+
1012+
return checksums
1013+
}

internal/pkg/agent/application/upgrade/artifact/download/verifier.go

+18-1
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,15 @@ func VerifySHA512HashWithCleanup(log infoWarnLogger, filename string) error {
101101
log.Warnf("failed clean up after sha512 check: failed to remove %q: %v",
102102
filename+".sha512", err)
103103
}
104+
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
105+
// it's not a simple hash mismatch, probably something is wrong with the hash file
106+
hashFileName := getHashFileName(filename)
107+
hashFileBytes, readErr := os.ReadFile(hashFileName)
108+
if readErr != nil {
109+
log.Warnf("error verifying the package using hash file %q, unable do read contents for logging: %v", getHashFileName(filename), readErr)
110+
} else {
111+
log.Warnf("error verifying the package using hash file %q, contents: %q", getHashFileName(filename), string(hashFileBytes))
112+
}
104113
}
105114

106115
return err
@@ -109,12 +118,20 @@ func VerifySHA512HashWithCleanup(log infoWarnLogger, filename string) error {
109118
return nil
110119
}
111120

121+
func getHashFileName(filename string) string {
122+
const hashFileExt = ".sha512"
123+
if strings.HasSuffix(filename, hashFileExt) {
124+
return filename
125+
}
126+
return filename + hashFileExt
127+
}
128+
112129
// VerifySHA512Hash checks that a sidecar file containing a sha512 checksum
113130
// exists and that the checksum in the sidecar file matches the checksum of
114131
// the file. It returns an error if validation fails.
115132
func VerifySHA512Hash(filename string) error {
116133
// Read expected checksum.
117-
expectedHash, err := readChecksumFile(filename+".sha512", filepath.Base(filename))
134+
expectedHash, err := readChecksumFile(getHashFileName(filename), filepath.Base(filename))
118135
if err != nil {
119136
return fmt.Errorf("could not read checksum file: %w", err)
120137
}

internal/pkg/agent/application/upgrade/artifact/download/verifier_test.go

+113
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import (
1010
"encoding/hex"
1111
"fmt"
1212
"io"
13+
"io/fs"
1314
"net/http"
1415
"os"
1516
"path/filepath"
17+
"runtime"
1618
"testing"
1719

1820
"github.com/stretchr/testify/assert"
@@ -192,6 +194,117 @@ func TestVerifySHA512HashWithCleanup_failure(t *testing.T) {
192194
}
193195
}
194196

197+
func TestVerifySHA512HashWithCleanup_BrokenHashFile(t *testing.T) {
198+
199+
const data = "" +
200+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. " +
201+
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " +
202+
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " +
203+
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
204+
// if you change data, the constant below should be updated
205+
const correct_data_hash = "8ba760cac29cb2b2ce66858ead169174057aa1298ccd581514e6db6dee3285280ee6e3a54c9319071dc8165ff061d77783100d449c937ff1fb4cd1bb516a69b9"
206+
207+
const filename = "lorem_ipsum.txt"
208+
const hashFileName = filename + ".sha512"
209+
210+
type skipFunc func(t *testing.T)
211+
212+
type testcase struct {
213+
name string
214+
skip skipFunc
215+
hash []byte
216+
hashPermissions fs.FileMode
217+
wantErr assert.ErrorAssertionFunc
218+
wantLogSnippets []string
219+
}
220+
221+
testcases := []testcase{
222+
{
223+
name: "happy path - correct hash and format",
224+
hash: []byte(correct_data_hash + " " + filename),
225+
hashPermissions: 0o640,
226+
wantErr: assert.NoError,
227+
},
228+
{
229+
name: "happy path - broken lines before correct hash and format",
230+
hash: []byte("this_is just_filler\n" + "some_more_filler\n" + correct_data_hash + " " + filename),
231+
hashPermissions: 0o640,
232+
wantErr: assert.NoError,
233+
},
234+
{
235+
name: "truncated hash line - no filename",
236+
hash: []byte(correct_data_hash),
237+
hashPermissions: 0o640,
238+
wantErr: assert.Error,
239+
wantLogSnippets: []string{`contents: "` + correct_data_hash + `"`},
240+
},
241+
{
242+
name: "truncated hash",
243+
hash: []byte(correct_data_hash[:8] + " " + filename),
244+
hashPermissions: 0o640,
245+
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
246+
target := new(ChecksumMismatchError)
247+
return assert.ErrorAs(t, err, &target, "mismatched hash has a specific error type", i)
248+
},
249+
},
250+
{
251+
name: "empty hash file",
252+
hash: []byte{},
253+
hashPermissions: 0o640,
254+
wantErr: assert.Error,
255+
wantLogSnippets: []string{`contents: ""`},
256+
},
257+
{
258+
name: "non-existing hash file",
259+
hash: nil,
260+
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
261+
return assert.ErrorIs(t, err, fs.ErrNotExist, i)
262+
},
263+
},
264+
{
265+
name: "unreadable hash file",
266+
skip: func(t *testing.T) {
267+
if runtime.GOOS == "windows" {
268+
t.Skip("write-only permissions cannot be set on windows")
269+
}
270+
},
271+
hash: []byte(correct_data_hash + " " + filename),
272+
hashPermissions: 0o222,
273+
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
274+
return assert.ErrorIs(t, err, fs.ErrPermission, i)
275+
},
276+
wantLogSnippets: []string{hashFileName + `", unable do read contents for logging:`},
277+
},
278+
}
279+
280+
for _, tt := range testcases {
281+
t.Run(tt.name, func(t *testing.T) {
282+
if tt.skip != nil {
283+
tt.skip(t)
284+
}
285+
286+
dir := t.TempDir()
287+
dataFilePath := filepath.Join(dir, filename)
288+
err := os.WriteFile(dataFilePath, []byte(data), 0o750)
289+
require.NoError(t, err, "could not write sample data file")
290+
291+
if tt.hash != nil {
292+
hashFilePath := filepath.Join(dir, hashFileName)
293+
err = os.WriteFile(hashFilePath, tt.hash, tt.hashPermissions)
294+
require.NoError(t, err, "could not write test hash file")
295+
}
296+
297+
testLogger, obsLogs := logger.NewTesting(tt.name)
298+
err = VerifySHA512HashWithCleanup(testLogger, dataFilePath)
299+
tt.wantErr(t, err)
300+
for _, log := range tt.wantLogSnippets {
301+
filteredLogs := obsLogs.FilterMessageSnippet(log)
302+
assert.NotEmptyf(t, filteredLogs, "there should be logs matching snippet %q", log)
303+
}
304+
})
305+
}
306+
}
307+
195308
type testlogger struct {
196309
t *testing.T
197310
}

0 commit comments

Comments
 (0)