-
Notifications
You must be signed in to change notification settings - Fork 173
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Philip Laine <[email protected]>
- Loading branch information
1 parent
22531dd
commit 96f9ac8
Showing
5 changed files
with
341 additions
and
125 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,300 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// SPDX-FileCopyrightText: 2021-Present The Zarf Authors | ||
|
||
// Package layout contains functions for inteacting the Zarf packages. | ||
package layout | ||
|
||
import ( | ||
"archive/tar" | ||
"context" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"io/fs" | ||
"os" | ||
"path/filepath" | ||
"strings" | ||
|
||
"github.com/defenseunicorns/pkg/helpers/v2" | ||
goyaml "github.com/goccy/go-yaml" | ||
registryv1 "github.com/google/go-containerregistry/pkg/v1" | ||
"github.com/google/go-containerregistry/pkg/v1/layout" | ||
"github.com/mholt/archiver/v3" | ||
ocispec "github.com/opencontainers/image-spec/specs-go/v1" | ||
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options" | ||
"github.com/sigstore/cosign/v2/cmd/cosign/cli/verify" | ||
|
||
"github.com/zarf-dev/zarf/src/api/v1alpha1" | ||
"github.com/zarf-dev/zarf/src/config" | ||
"github.com/zarf-dev/zarf/src/pkg/packager/deprecated" | ||
"github.com/zarf-dev/zarf/src/pkg/transform" | ||
"github.com/zarf-dev/zarf/src/pkg/utils" | ||
) | ||
|
||
const ( | ||
zarfYAML = "zarf.yaml" | ||
signature = "zarf.yaml.sig" | ||
checksums = "checksums.txt" | ||
imagesDir = "images" | ||
componentsDir = "components" | ||
) | ||
|
||
// PackageLayout manages the layout for a package. | ||
type PackageLayout struct { | ||
dirPath string | ||
Pkg v1alpha1.ZarfPackage | ||
} | ||
|
||
// PackageLayoutOptions are the options used when loading a package. | ||
type PackageLayoutOptions struct { | ||
PublicKeyPath string | ||
SkipSignatureValidation bool | ||
IsPartial bool | ||
} | ||
|
||
// LoadFromTar unpacks the give compressed package and loads it. | ||
func LoadFromTar(ctx context.Context, tarPath string, opt PackageLayoutOptions) (*PackageLayout, error) { | ||
dirPath, err := utils.MakeTempDir(config.CommonOptions.TempDirectory) | ||
if err != nil { | ||
return nil, err | ||
} | ||
err = archiver.Walk(tarPath, func(f archiver.File) error { | ||
if f.IsDir() { | ||
return nil | ||
} | ||
header, ok := f.Header.(*tar.Header) | ||
if !ok { | ||
return fmt.Errorf("expected header to be *tar.Header but was %T", f.Header) | ||
} | ||
// If path has nested directories we want to create them. | ||
dir := filepath.Dir(header.Name) | ||
if dir != "." { | ||
err := os.MkdirAll(filepath.Join(dirPath, dir), helpers.ReadExecuteAllWriteUser) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
dst, err := os.Create(filepath.Join(dirPath, header.Name)) | ||
if err != nil { | ||
return err | ||
} | ||
defer dst.Close() | ||
_, err = io.Copy(dst, f) | ||
if err != nil { | ||
return err | ||
} | ||
return nil | ||
}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
p, err := LoadFromDir(ctx, dirPath, opt) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return p, nil | ||
} | ||
|
||
// LoadFromDir loads and validates a package from the given directory path. | ||
func LoadFromDir(ctx context.Context, dirPath string, opt PackageLayoutOptions) (*PackageLayout, error) { | ||
b, err := os.ReadFile(filepath.Join(dirPath, zarfYAML)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
var pkg v1alpha1.ZarfPackage | ||
err = goyaml.Unmarshal(b, &pkg) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if len(pkg.Build.Migrations) > 0 { | ||
for idx, component := range pkg.Components { | ||
pkg.Components[idx], _ = deprecated.MigrateComponent(pkg.Build, component) | ||
} | ||
} | ||
|
||
pkgLayout := &PackageLayout{ | ||
dirPath: dirPath, | ||
Pkg: pkg, | ||
} | ||
err = validatePackageIntegrity(pkgLayout, opt.IsPartial) | ||
if err != nil { | ||
return nil, err | ||
} | ||
err = validatePackageSignature(ctx, pkgLayout, opt.PublicKeyPath, opt.SkipSignatureValidation) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return pkgLayout, nil | ||
} | ||
|
||
// Cleanup removes any temporary directories created. | ||
func (p *PackageLayout) Cleanup() error { | ||
err := os.RemoveAll(p.dirPath) | ||
if err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
// GetDirectory returns a path to the directory in the given component. | ||
// TODO (phillebaba): Make dirType an enum. | ||
func (p *PackageLayout) GetDirectory(component string, dirType string) (string, error) { | ||
sourcePath := filepath.Join(p.dirPath, componentsDir, fmt.Sprintf("%s.tar", component)) | ||
_, err := os.Stat(sourcePath) | ||
if err != nil { | ||
return "", err | ||
} | ||
dirPath, err := utils.MakeTempDir(config.CommonOptions.TempDirectory) | ||
if err != nil { | ||
return "", err | ||
} | ||
err = archiver.Extract(sourcePath, filepath.Join(component, dirType), dirPath) | ||
if err != nil { | ||
return "", err | ||
} | ||
_, err = os.Stat(filepath.Join(dirPath, dirType)) | ||
if err != nil { | ||
return "", err | ||
} | ||
return filepath.Join(dirPath, dirType), nil | ||
} | ||
|
||
// GetImage returns the image with the given reference in the package layout. | ||
func (p *PackageLayout) GetImage(ref transform.Image) (registryv1.Image, error) { | ||
// Use the manifest within the index.json to load the specific image we want | ||
layoutPath := layout.Path(filepath.Join(p.dirPath, imagesDir)) | ||
imgIdx, err := layoutPath.ImageIndex() | ||
if err != nil { | ||
return nil, err | ||
} | ||
idxManifest, err := imgIdx.IndexManifest() | ||
if err != nil { | ||
return nil, err | ||
} | ||
// Search through all the manifests within this package until we find the annotation that matches our ref | ||
for _, manifest := range idxManifest.Manifests { | ||
if manifest.Annotations[ocispec.AnnotationBaseImageName] == ref.Reference || | ||
// A backwards compatibility shim for older Zarf versions that would leave docker.io off of image annotations | ||
(manifest.Annotations[ocispec.AnnotationBaseImageName] == ref.Path+ref.TagOrDigest && ref.Host == "docker.io") { | ||
// This is the image we are looking for, load it and then return | ||
return layoutPath.Image(manifest.Digest) | ||
} | ||
} | ||
return nil, fmt.Errorf("unable to find the image %s", ref.Reference) | ||
} | ||
|
||
func validatePackageIntegrity(pkgLayout *PackageLayout, isPartial bool) error { | ||
_, err := os.Stat(filepath.Join(pkgLayout.dirPath, zarfYAML)) | ||
if err != nil { | ||
return err | ||
} | ||
_, err = os.Stat(filepath.Join(pkgLayout.dirPath, checksums)) | ||
if err != nil { | ||
return err | ||
} | ||
err = helpers.SHAsMatch(filepath.Join(pkgLayout.dirPath, checksums), pkgLayout.Pkg.Metadata.AggregateChecksum) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
packageFiles := map[string]interface{}{} | ||
err = filepath.Walk(pkgLayout.dirPath, func(path string, info fs.FileInfo, err error) error { | ||
if info.IsDir() { | ||
return nil | ||
} | ||
packageFiles[path] = nil | ||
return err | ||
}) | ||
if err != nil { | ||
return err | ||
} | ||
// Remove files which are not in the checksums. | ||
delete(packageFiles, filepath.Join(pkgLayout.dirPath, zarfYAML)) | ||
delete(packageFiles, filepath.Join(pkgLayout.dirPath, checksums)) | ||
delete(packageFiles, filepath.Join(pkgLayout.dirPath, signature)) | ||
|
||
b, err := os.ReadFile(filepath.Join(pkgLayout.dirPath, checksums)) | ||
if err != nil { | ||
return err | ||
} | ||
lines := strings.Split(string(b), "\n") | ||
for _, line := range lines { | ||
// If the line is empty (i.e. there is no checksum) simply skip it, this can result from a package with no images/components. | ||
if line == "" { | ||
continue | ||
} | ||
|
||
split := strings.Split(line, " ") | ||
if len(split) != 2 { | ||
return fmt.Errorf("invalid checksum line: %s", line) | ||
} | ||
sha := split[0] | ||
rel := split[1] | ||
if sha == "" || rel == "" { | ||
return fmt.Errorf("invalid checksum line: %s", line) | ||
} | ||
|
||
path := filepath.Join(pkgLayout.dirPath, rel) | ||
_, ok := packageFiles[path] | ||
if !ok && isPartial { | ||
delete(packageFiles, path) | ||
continue | ||
} | ||
if !ok { | ||
return fmt.Errorf("file %s from checksum missing in layout", rel) | ||
} | ||
err = helpers.SHAsMatch(path, sha) | ||
if err != nil { | ||
return err | ||
} | ||
delete(packageFiles, path) | ||
} | ||
|
||
if len(packageFiles) > 0 { | ||
// TODO (phillebaba): Replace with maps.Keys after upgrading to Go 1.23. | ||
filePaths := []string{} | ||
for k := range packageFiles { | ||
filePaths = append(filePaths, k) | ||
} | ||
return fmt.Errorf("package contains additional files not present in the checksum %s", strings.Join(filePaths, ", ")) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func validatePackageSignature(ctx context.Context, pkgLayout *PackageLayout, publicKeyPath string, skipSignatureValidation bool) error { | ||
if skipSignatureValidation { | ||
return nil | ||
} | ||
|
||
signaturePath := filepath.Join(pkgLayout.dirPath, signature) | ||
zarfYamlPath := filepath.Join(pkgLayout.dirPath, zarfYAML) | ||
|
||
sigExist := true | ||
_, err := os.Stat(signaturePath) | ||
if err != nil { | ||
sigExist = false | ||
} | ||
if !sigExist && publicKeyPath == "" { | ||
// Nobody was expecting a signature, so we can just return | ||
return nil | ||
} else if sigExist && publicKeyPath == "" { | ||
return errors.New("package is signed but no key was provided") | ||
} else if !sigExist && publicKeyPath != "" { | ||
return errors.New("a key was provided but the package is not signed") | ||
} | ||
|
||
keyOptions := options.KeyOpts{KeyRef: publicKeyPath} | ||
cmd := &verify.VerifyBlobCmd{ | ||
KeyOpts: keyOptions, | ||
SigRef: signaturePath, | ||
IgnoreSCT: true, | ||
Offline: true, | ||
IgnoreTlog: true, | ||
} | ||
err = cmd.Exec(ctx, zarfYamlPath) | ||
if err != nil { | ||
return fmt.Errorf("package signature did not match the provided key: %w", err) | ||
} | ||
return nil | ||
} |
Oops, something went wrong.