Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cryptography module, integrity checking commands and small fixes #108

Merged
merged 17 commits into from
Jul 26, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions cmd/commands/checksum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/* SPDX-License-Identifier: Apache-2.0 */
/* Copyright Contributors to the cpackget project. */

package commands

import (
"github.com/open-cmsis-pack/cpackget/cmd/cryptography"
errs "github.com/open-cmsis-pack/cpackget/cmd/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

var checksumCreateCmdFlags struct {
// hashAlgorithm is the cryptographic hash function to be used
hashAlgorithm string

// outputDir is the target directory where the checksum file is written to
outputDir string
}

func init() {
ChecksumCreateCmd.Flags().StringVarP(&checksumCreateCmdFlags.hashAlgorithm, "hash-algorithm", "a", "", "specifies the hash function to be used")
chaws marked this conversation as resolved.
Show resolved Hide resolved
ChecksumCreateCmd.Flags().StringVarP(&checksumCreateCmdFlags.outputDir, "output-dir", "o", "", "specifies output directory for the checksum file")
}

var ChecksumCreateCmd = &cobra.Command{
Use: "checksum-create [<local .path pack>]",
Short: "Generates a .checksum file containing the digests of a pack",
// TODO: Long, show valid hash algorithms
chaws marked this conversation as resolved.
Show resolved Hide resolved
Args: cobra.MinimumNArgs(0),
chaws marked this conversation as resolved.
Show resolved Hide resolved
PersistentPreRunE: configureInstaller,
RunE: func(cmd *cobra.Command, args []string) error {

if len(args) == 0 {
log.Error("missing .pack local path")
return errs.ErrIncorrectCmdArgs
}

return cryptography.GenerateChecksum(args[0], checksumCreateCmdFlags.outputDir, checksumCreateCmdFlags.hashAlgorithm)
},
}

var ChecksumVerifyCmd = &cobra.Command{
Use: "checksum-verify [<local .path pack>] [<local .checksum path>]",
Short: "Verifies the integrity of a pack using its .checksum file",
// TODO: Long
Args: cobra.MinimumNArgs(0),
chaws marked this conversation as resolved.
Show resolved Hide resolved
PersistentPreRunE: configureInstaller,
RunE: func(cmd *cobra.Command, args []string) error {

if len(args) != 2 {
log.Error("Please provide path to pack and checksum file")
return errs.ErrIncorrectCmdArgs
}

return cryptography.VerifyChecksum(args[0], args[1])
},
}
2 changes: 2 additions & 0 deletions cmd/commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ var AllCommands = []*cobra.Command{
RmCmd,
ListCmd,
UpdateIndexCmd,
ChecksumCreateCmd,
ChecksumVerifyCmd,
}

// createPackRoot is a flag that determines if the pack root should be created or not
Expand Down
246 changes: 246 additions & 0 deletions cmd/cryptography/checksum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
package cryptography

import (
"archive/zip"
"bufio"
"crypto/sha256"
"fmt"
"hash"
"io"
"os"
"path/filepath"
"strings"

errs "github.com/open-cmsis-pack/cpackget/cmd/errors"
"github.com/open-cmsis-pack/cpackget/cmd/utils"
log "github.com/sirupsen/logrus"
)

type checksum struct {
hashFunction, digest, filename string
}

func isValidHash(hashFunction string) bool {
valid := false
for _, h := range Hashes {
if h == hashFunction {
valid = true
chaws marked this conversation as resolved.
Show resolved Hide resolved
}
}
return valid
}

// GetChecksumList receives a list of file paths and returns
// their hashed content using either a specified function or
// the default one
func getChecksumList(baseDir string, filePathList []string, hashFunction string) ([]checksum, error) {
digests := make([]checksum, len(filePathList))
for i := 0; i < len(filePathList); i++ {
if utils.DirExists(filePathList[i]) {
continue
}
f, err := os.Open(filePathList[i])
if err != nil {
return []checksum{}, err
}
defer f.Close()

var h hash.Hash
switch hashFunction {
case "sha256":
h = sha256.New()
digests[i].hashFunction = "sha256"
default:
h = sha256.New()
digests[i].hashFunction = "sha256"
}

if _, err := io.Copy(h, f); err != nil {
return []checksum{}, err
}

digests[i].digest = fmt.Sprintf("%x", h.Sum(nil))
digests[i].filename = strings.ReplaceAll(filePathList[i], baseDir, "")
}
return digests, nil
}

func GenerateChecksum(sourcePack, destinationDir, hashFunction string) error {
// Use default Cryptographic Hash Function if none is provided
if hashFunction == "" {
hashFunction = Hashes[0]
}
if !isValidHash(hashFunction) {
return errs.ErrInvalidHashFunction
}

if !utils.FileExists(sourcePack) {
log.Errorf("\"%s\" is not a valid .pack", sourcePack)
chaws marked this conversation as resolved.
Show resolved Hide resolved
return errs.ErrFileNotFound
}

// Checksum file path defaults to the .pack's location
base := ""
if destinationDir == "" {
base = filepath.Clean(strings.TrimSuffix(sourcePack, filepath.Ext(sourcePack)))
} else {
if !utils.DirExists(destinationDir) {
log.Errorf("\"%s\" is not a valid directory", destinationDir)
return errs.ErrDirectoryNotFound
}
base = filepath.Clean(destinationDir) + string(filepath.Separator) + strings.TrimSuffix(string(filepath.Base(sourcePack)), ".pack")
}
checksumFilename := base + "." + strings.Replace(hashFunction, "-", "", -1) + ".checksum"

if utils.FileExists(checksumFilename) {
log.Errorf("\"%s\" already exists, choose a diferent path", checksumFilename)
return errs.ErrPathAlreadyExists
}

// Unpack it to the same directory as the .pack
packDir := strings.TrimSuffix(sourcePack, filepath.Ext(sourcePack)) + string(os.PathSeparator)
if utils.DirExists(packDir) {
log.Errorf("pack was already extracted to \"%s\"", packDir)
return errs.ErrPathAlreadyExists
}

zipReader, err := zip.OpenReader(sourcePack)
if err != nil {
log.Errorf("can't decompress \"%s\": %s", sourcePack, err)
return errs.ErrFailedDecompressingFile
}
var packFileList []string
for _, file := range zipReader.File {
if err := utils.SecureInflateFile(file, packDir, ""); err != nil {
chaws marked this conversation as resolved.
Show resolved Hide resolved
if err == errs.ErrTerminatedByUser {
log.Infof("aborting pack extraction. Removing \"%s\"", packDir)
return os.RemoveAll(packDir)
}
return err
}
packFileList = append(packFileList, filepath.Join(filepath.Clean(packDir), filepath.Clean(file.Name)))
}

digests, err := getChecksumList(packDir, packFileList, hashFunction)
if err != nil {
return err
}

out, err := os.Create(checksumFilename)
if err != nil {
log.Error(err)
return errs.ErrFailedCreatingFile
}
defer out.Close()
for i := 0; i < len(digests); i++ {
_, err := out.Write([]byte(digests[i].digest + " " + digests[i].filename + "\n"))
if err != nil {
return err
}
}
// Cleanup extracted pack
log.Debugf("deleting \"%s\"", packDir)
if err := os.RemoveAll(packDir); err != nil {
return err
}
return nil
}

func VerifyChecksum(sourcePack, checksum string) error {
if !utils.FileExists(sourcePack) {
log.Errorf("\"%s\" is not a valid .pack", sourcePack)
chaws marked this conversation as resolved.
Show resolved Hide resolved
return errs.ErrFileNotFound
}
if !utils.FileExists(checksum) {
log.Errorf("\"%s\" does not exist", checksum)
return errs.ErrFileNotFound
}
hashAlgorithm := filepath.Ext(strings.Split(checksum, ".checksum")[0])[1:]
if !isValidHash(hashAlgorithm) {
log.Errorf("\"%s\" is not a valid .checksum file (correct format is [<pack>].[<hash-algorithm>].checksum). Please confirm if the algorithm is supported.", checksum)
return errs.ErrInvalidHashFunction
}

// Unpack it to the same directory as the .pack
packDir := strings.TrimSuffix(sourcePack, filepath.Ext(sourcePack)) + string(os.PathSeparator)
if utils.DirExists(packDir) {
log.Errorf("pack was already extracted to \"%s\"", packDir)
return errs.ErrPathAlreadyExists
}

zipReader, err := zip.OpenReader(sourcePack)
if err != nil {
log.Errorf("can't decompress \"%s\": %s", sourcePack, err)
return errs.ErrFailedDecompressingFile
}
var packFileList []string
for _, file := range zipReader.File {
chaws marked this conversation as resolved.
Show resolved Hide resolved
if err := utils.SecureInflateFile(file, packDir, ""); err != nil {
if err == errs.ErrTerminatedByUser {
log.Infof("aborting pack extraction. Removing \"%s\"", packDir)
return os.RemoveAll(packDir)
}
return err
}
packFileList = append(packFileList, filepath.Join(filepath.Clean(packDir), filepath.Clean(file.Name)))
}

// Calculate pack digests
digests, err := getChecksumList(packDir, packFileList, hashAlgorithm)
if err != nil {
return err
}
// Compare with target checksum file
checksumFile, err := os.Open(checksum)
if err != nil {
return err
}
defer checksumFile.Close()

scanner := bufio.NewScanner(checksumFile)
linesRead := 0
failure := false
for scanner.Scan() {
// They might not be in the same order
if scanner.Text() == "" {
continue
}
targetDigest := strings.Split(scanner.Text(), " ")[0]
targetFile := strings.Split(scanner.Text(), " ")[1]
// The checksum file might not have the same order
fileMatched := false
for i := 0; i < len(digests); i++ {
if digests[i].filename == targetFile {
if digests[i].digest != targetDigest {
log.Debugf("%s != %s", digests[i].digest, targetDigest)
log.Errorf("%s: computed checksum did NOT match", targetFile)
failure = true
}
fileMatched = true
}
}
// Make sure all files match
if !fileMatched {
log.Errorf("file \"%s\" was not found in the provided checksum file", targetFile)
return errs.ErrCorruptPack
}
linesRead++
}

// Cleanup extracted pack
log.Debugf("deleting \"%s\"", packDir)
if err := os.RemoveAll(packDir); err != nil {
return err
}

if linesRead != len(digests) {
log.Errorf("provided checksum file lists %d files, but pack contains %d files", linesRead, len(digests))
return errs.ErrCorruptPack
}
if failure {
return errs.ErrCorruptPack
}

log.Info("pack integrity verified, all checksums match.")
return nil
}
7 changes: 7 additions & 0 deletions cmd/cryptography/const.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* SPDX-License-Identifier: Apache-2.0 */
/* Copyright Contributors to the cpackget project. */

package cryptography

// Hashes is the list of supported Cryptographic Hash Functions used for the checksum feature
var Hashes = [1]string{"sha256"}
5 changes: 5 additions & 0 deletions cmd/errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,14 @@ var (
ErrFailedCreatingDirectory = errors.New("fail to create directory")
ErrFileNotFound = errors.New("file not found")
ErrDirectoryNotFound = errors.New("directory not found")
ErrPathAlreadyExists = errors.New("path already exists")
ErrCopyingEqualPaths = errors.New("failed copying files: source is the same as destination")
ErrMovingEqualPaths = errors.New("failed moving files: source is the same as destination")

// Cryptography errors
ErrCorruptPack = errors.New("pack is corrupt, checksum verification failed")
ErrInvalidHashFunction = errors.New("provided hash function is not supported")

// Security errors
ErrInsecureZipFileName = errors.New("zip file contains insecure characters: ../")
ErrFileTooBig = errors.New("files cannot be over 20G")
Expand Down