github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/pkg/packager/sources/validate.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // SPDX-FileCopyrightText: 2021-Present The Jackal Authors
     3  
     4  // Package sources contains core implementations of the PackageSource interface.
     5  package sources
     6  
     7  import (
     8  	"bufio"
     9  	"errors"
    10  	"fmt"
    11  	"io/fs"
    12  	"os"
    13  	"path/filepath"
    14  	"strings"
    15  
    16  	"github.com/Racer159/jackal/src/config"
    17  	"github.com/Racer159/jackal/src/pkg/layout"
    18  	"github.com/Racer159/jackal/src/pkg/message"
    19  	"github.com/Racer159/jackal/src/pkg/utils"
    20  	"github.com/defenseunicorns/pkg/helpers"
    21  )
    22  
    23  var (
    24  	// ErrPkgKeyButNoSig is returned when a key was provided but the package is not signed
    25  	ErrPkgKeyButNoSig = errors.New("a key was provided but the package is not signed - the package may be corrupted or the --key flag was erroneously specified")
    26  	// ErrPkgSigButNoKey is returned when a package is signed but no key was provided
    27  	ErrPkgSigButNoKey = errors.New("package is signed but no key was provided - add a key with the --key flag or use the --insecure flag and run the command again")
    28  )
    29  
    30  // ValidatePackageSignature validates the signature of a package
    31  func ValidatePackageSignature(paths *layout.PackagePaths, publicKeyPath string) error {
    32  	// If the insecure flag was provided ignore the signature validation
    33  	if config.CommonOptions.Insecure {
    34  		return nil
    35  	}
    36  
    37  	if publicKeyPath != "" {
    38  		message.Debugf("Using public key %q for signature validation", publicKeyPath)
    39  	}
    40  
    41  	// Handle situations where there is no signature within the package
    42  	sigExist := paths.Signature != ""
    43  	if !sigExist && publicKeyPath == "" {
    44  		// Nobody was expecting a signature, so we can just return
    45  		return nil
    46  	} else if sigExist && publicKeyPath == "" {
    47  		// The package is signed but no key was provided
    48  		return ErrPkgSigButNoKey
    49  	} else if !sigExist && publicKeyPath != "" {
    50  		// A key was provided but there is no signature
    51  		return ErrPkgKeyButNoSig
    52  	}
    53  
    54  	// Validate the signature with the key we were provided
    55  	if err := utils.CosignVerifyBlob(paths.JackalYAML, paths.Signature, publicKeyPath); err != nil {
    56  		return fmt.Errorf("package signature did not match the provided key: %w", err)
    57  	}
    58  
    59  	return nil
    60  }
    61  
    62  // ValidatePackageIntegrity validates the integrity of a package by comparing checksums
    63  func ValidatePackageIntegrity(loaded *layout.PackagePaths, aggregateChecksum string, isPartial bool) error {
    64  	// ensure checksums.txt and jackal.yaml were loaded
    65  	if helpers.InvalidPath(loaded.Checksums) {
    66  		return fmt.Errorf("unable to validate checksums, %s was not loaded", layout.Checksums)
    67  	}
    68  	if helpers.InvalidPath(loaded.JackalYAML) {
    69  		return fmt.Errorf("unable to validate checksums, %s was not loaded", layout.JackalYAML)
    70  	}
    71  
    72  	checksumPath := loaded.Checksums
    73  	if err := helpers.SHAsMatch(checksumPath, aggregateChecksum); err != nil {
    74  		return err
    75  	}
    76  
    77  	checkedMap, err := pathCheckMap(loaded.Base)
    78  	if err != nil {
    79  		return err
    80  	}
    81  
    82  	checkedMap[loaded.JackalYAML] = true
    83  	checkedMap[loaded.Checksums] = true
    84  	checkedMap[loaded.Signature] = true
    85  
    86  	err = lineByLine(checksumPath, func(line string) error {
    87  		// If the line is empty (i.e. there is no checksum) simply skip it - this can result from a package with no images/components
    88  		if line == "" {
    89  			return nil
    90  		}
    91  
    92  		split := strings.Split(line, " ")
    93  		// If the line is not splitable into two pieces the file is likely corrupted
    94  		if len(split) != 2 {
    95  			return fmt.Errorf("invalid checksum line: %s", line)
    96  		}
    97  
    98  		sha := split[0]
    99  		rel := split[1]
   100  
   101  		if sha == "" || rel == "" {
   102  			return fmt.Errorf("invalid checksum line: %s", line)
   103  		}
   104  		path := filepath.Join(loaded.Base, rel)
   105  
   106  		if helpers.InvalidPath(path) {
   107  			if !isPartial && !checkedMap[path] {
   108  				return fmt.Errorf("unable to validate checksums - missing file: %s", rel)
   109  			} else if isPartial {
   110  				wasLoaded := false
   111  				for rel := range loaded.Files() {
   112  					if path == rel {
   113  						wasLoaded = true
   114  					}
   115  				}
   116  				if wasLoaded {
   117  					return fmt.Errorf("unable to validate partial checksums - missing file: %s", rel)
   118  				}
   119  			}
   120  			// it's okay if we're doing a partial check and the file isn't there as long as the path wasn't loaded
   121  			return nil
   122  		}
   123  
   124  		if err := helpers.SHAsMatch(path, sha); err != nil {
   125  			return err
   126  		}
   127  
   128  		checkedMap[path] = true
   129  
   130  		return nil
   131  	})
   132  	if err != nil {
   133  		return err
   134  	}
   135  
   136  	// Make sure we've checked all the files we loaded
   137  	for _, path := range loaded.Files() {
   138  		if !checkedMap[path] {
   139  			return fmt.Errorf("unable to validate loaded checksums, %s did not get checked", path)
   140  		}
   141  	}
   142  
   143  	// Check that all of the files in the loaded directory were checked (i.e. no files were weren't expecting got added)
   144  	for path, checked := range checkedMap {
   145  		if !checked {
   146  			return fmt.Errorf("unable to validate checksums, %s did not get checked", path)
   147  		}
   148  	}
   149  
   150  	return nil
   151  }
   152  
   153  // pathCheckMap returns a map of all the files in a directory and a boolean to use for checking status.
   154  func pathCheckMap(dir string) (map[string]bool, error) {
   155  	filepathMap := make(map[string]bool)
   156  	err := filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error {
   157  		if info.IsDir() {
   158  			return nil
   159  		}
   160  		filepathMap[path] = false
   161  		return err
   162  	})
   163  	return filepathMap, err
   164  }
   165  
   166  // lineByLine reads a file line by line and calls a callback function for each line.
   167  func lineByLine(path string, cb func(line string) error) error {
   168  	file, err := os.Open(path)
   169  	if err != nil {
   170  		return err
   171  	}
   172  	defer file.Close()
   173  
   174  	// Read line by line
   175  	scanner := bufio.NewScanner(file)
   176  	scanner.Split(bufio.ScanLines)
   177  	for scanner.Scan() {
   178  		err := cb(scanner.Text())
   179  		if err != nil {
   180  			return err
   181  		}
   182  	}
   183  	return nil
   184  }