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 }