Skip to content

Commit 15b4424

Browse files
authored
feat(verify): package verify command support (#4325)
Signed-off-by: Brandt Keller <brandt.keller@defenseunicorns.com>
1 parent 2d01cf3 commit 15b4424

File tree

7 files changed

+515
-44
lines changed

7 files changed

+515
-44
lines changed

site/src/content/docs/commands/zarf_package.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,5 @@ Zarf package commands for creating, deploying, and inspecting packages
4242
* [zarf package pull](/commands/zarf_package_pull/) - Pulls a Zarf package from a remote registry and save to the local file system
4343
* [zarf package remove](/commands/zarf_package_remove/) - Removes a Zarf package that has been deployed already (runs offline)
4444
* [zarf package sign](/commands/zarf_package_sign/) - Signs an existing Zarf package
45+
* [zarf package verify](/commands/zarf_package_verify/) - Verify the signature and integrity of a Zarf package
4546

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
title: zarf package verify
3+
description: Zarf CLI command reference for <code>zarf package verify</code>.
4+
tableOfContents: false
5+
---
6+
7+
<!-- Page generated by Zarf; DO NOT EDIT -->
8+
9+
## zarf package verify
10+
11+
Verify the signature and integrity of a Zarf package
12+
13+
### Synopsis
14+
15+
Verify the cryptographic signature (if signed) and checksum integrity of a Zarf package. Returns exit code 0 if valid, non-zero if verification fails.
16+
17+
```
18+
zarf package verify PACKAGE_SOURCE [flags]
19+
```
20+
21+
### Examples
22+
23+
```
24+
25+
# Verify a signed package
26+
$ zarf package verify zarf-package-demo-amd64-1.0.0.tar.zst --key ./public-key.pub
27+
28+
# Verify an unsigned package (checksums only)
29+
$ zarf package verify zarf-package-demo-amd64-1.0.0.tar.zst
30+
31+
```
32+
33+
### Options
34+
35+
```
36+
-h, --help help for verify
37+
-k, --key string Public key for signature verification
38+
--oci-concurrency int Number of concurrent layer operations when pulling or pushing images or packages to/from OCI registries. (default 6)
39+
```
40+
41+
### Options inherited from parent commands
42+
43+
```
44+
-a, --architecture string Architecture for OCI images and Zarf packages
45+
--features stringToString [ALPHA] Provide a comma-separated list of feature names to bools to enable or disable. Ex. --features "foo=true,bar=false,baz=true" (default [])
46+
--insecure-skip-tls-verify Skip checking server's certificate for validity. This flag should only be used if you have a specific reason and accept the reduced security posture.
47+
--log-format string Select a logging format. Defaults to 'console'. Valid options are: 'console', 'json', 'dev'. (default "console")
48+
-l, --log-level string Log level when running Zarf. Valid options are: warn, info, debug, trace (default "info")
49+
--no-color Disable terminal color codes in logging and stdout prints.
50+
--plain-http Force the connections over HTTP instead of HTTPS. This flag should only be used if you have a specific reason and accept the reduced security posture.
51+
--tmpdir string Specify the temporary directory to use for intermediate files
52+
--zarf-cache string Specify the location of the Zarf cache directory (default "~/.zarf-cache")
53+
```
54+
55+
### SEE ALSO
56+
57+
* [zarf package](/commands/zarf_package/) - Zarf package commands for creating, deploying, and inspecting packages
58+

src/cmd/package.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ func newPackageCommand() *cobra.Command {
6161
cmd.AddCommand(newPackagePublishCommand(v))
6262
cmd.AddCommand(newPackagePullCommand(v))
6363
cmd.AddCommand(newPackageSignCommand(v))
64+
cmd.AddCommand(newPackageVerifyCommand(v))
6465

6566
return cmd
6667
}
@@ -1730,6 +1731,99 @@ func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error {
17301731
return nil
17311732
}
17321733

1734+
type packageVerifyOptions struct {
1735+
publicKeyPath string
1736+
ociConcurrency int
1737+
}
1738+
1739+
func newPackageVerifyCommand(v *viper.Viper) *cobra.Command {
1740+
o := &packageVerifyOptions{}
1741+
1742+
cmd := &cobra.Command{
1743+
Use: "verify PACKAGE_SOURCE",
1744+
Aliases: []string{"v"},
1745+
Args: cobra.ExactArgs(1),
1746+
Short: lang.CmdPackageVerifyShort,
1747+
Long: lang.CmdPackageVerifyLong,
1748+
Example: lang.CmdPackageVerifyExample,
1749+
RunE: o.run,
1750+
}
1751+
1752+
cmd.Flags().StringVarP(&o.publicKeyPath, "key", "k", v.GetString(VPkgPublicKey), lang.CmdPackageVerifyFlagKey)
1753+
cmd.Flags().IntVar(&o.ociConcurrency, "oci-concurrency", v.GetInt(VPkgOCIConcurrency), lang.CmdPackageFlagConcurrency)
1754+
1755+
return cmd
1756+
}
1757+
1758+
func (o *packageVerifyOptions) run(cmd *cobra.Command, args []string) error {
1759+
ctx := cmd.Context()
1760+
l := logger.From(ctx)
1761+
packageSource := args[0]
1762+
1763+
l.Info("verifying package", "source", packageSource)
1764+
1765+
cachePath, err := getCachePath(ctx)
1766+
if err != nil {
1767+
return err
1768+
}
1769+
1770+
// Load the package (validates checksums automatically)
1771+
// Note: OCI signature validation does not require the whole package
1772+
loadOpts := packager.LoadOptions{
1773+
PublicKeyPath: "",
1774+
SkipSignatureValidation: true,
1775+
Filter: filters.Empty(),
1776+
Architecture: config.GetArch(),
1777+
OCIConcurrency: o.ociConcurrency,
1778+
RemoteOptions: defaultRemoteOptions(),
1779+
CachePath: cachePath,
1780+
LayersSelector: zoci.MetadataLayers,
1781+
}
1782+
1783+
pkgLayout, err := packager.LoadPackage(ctx, packageSource, loadOpts)
1784+
if err != nil {
1785+
return fmt.Errorf("package verification failed: %w", err)
1786+
}
1787+
defer func() {
1788+
if cleanupErr := pkgLayout.Cleanup(); cleanupErr != nil {
1789+
l.Warn("failed to cleanup package", "error", cleanupErr)
1790+
}
1791+
}()
1792+
1793+
// Checksum verification passed (we successfully loaded the package)
1794+
l.Info("checksum verification", "status", "PASSED")
1795+
1796+
isSigned := pkgLayout.IsSigned()
1797+
1798+
// Handle signature verification logic
1799+
if !isSigned && o.publicKeyPath != "" {
1800+
return errors.New("a key was provided but the package is not signed")
1801+
}
1802+
1803+
if isSigned && o.publicKeyPath == "" {
1804+
return errors.New("package is signed but no public key was provided (use --key)")
1805+
}
1806+
1807+
if !isSigned && o.publicKeyPath == "" {
1808+
l.Warn("package is unsigned", "signed", false)
1809+
l.Info("verification complete", "status", "SUCCESS")
1810+
return nil
1811+
}
1812+
1813+
// Package is signed and we have a key - verify using VerifyPackageSignature
1814+
verifyOpts := utils.DefaultVerifyBlobOptions()
1815+
verifyOpts.KeyRef = o.publicKeyPath
1816+
1817+
err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts)
1818+
if err != nil {
1819+
return fmt.Errorf("signature verification failed: %w", err)
1820+
}
1821+
1822+
l.Info("signature verification", "status", "PASSED")
1823+
l.Info("verification complete", "status", "SUCCESS")
1824+
return nil
1825+
}
1826+
17331827
func choosePackage(ctx context.Context, args []string) (string, error) {
17341828
if len(args) > 0 {
17351829
return args[0], nil

src/config/lang/english.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,17 @@ $ zarf package sign zarf-package-demo-amd64-1.0.0.tar.zst --signing-key awskms:/
335335
CmdPackageSignFlagOverwrite = "Overwrite an existing signature if the package is already signed"
336336
CmdPackageSignFlagKey = "Public key to verify the existing signature before re-signing (optional)"
337337

338+
CmdPackageVerifyShort = "Verify the signature and integrity of a Zarf package"
339+
CmdPackageVerifyLong = "Verify the cryptographic signature (if signed) and checksum integrity of a Zarf package. Returns exit code 0 if valid, non-zero if verification fails."
340+
CmdPackageVerifyExample = `
341+
# Verify a signed package
342+
$ zarf package verify zarf-package-demo-amd64-1.0.0.tar.zst --key ./public-key.pub
343+
344+
# Verify an unsigned package (checksums only)
345+
$ zarf package verify zarf-package-demo-amd64-1.0.0.tar.zst
346+
`
347+
CmdPackageVerifyFlagKey = "Public key for signature verification"
348+
338349
CmdPackagePullShort = "Pulls a Zarf package from a remote registry and save to the local file system"
339350
CmdPackagePullExample = `
340351
# Pull a package matching the current architecture

src/pkg/packager/layout/package.go

Lines changed: 45 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ import (
1616

1717
"github.com/defenseunicorns/pkg/helpers/v2"
1818
goyaml "github.com/goccy/go-yaml"
19-
"github.com/sigstore/cosign/v3/cmd/cosign/cli/options"
20-
"github.com/sigstore/cosign/v3/cmd/cosign/cli/verify"
2119

2220
"github.com/zarf-dev/zarf/src/api/v1alpha1"
2321
"github.com/zarf-dev/zarf/src/config"
@@ -92,10 +90,17 @@ func LoadFromDir(ctx context.Context, dirPath string, opts PackageLayoutOptions)
9290
if err != nil {
9391
return nil, err
9492
}
95-
err = validatePackageSignature(ctx, pkgLayout, opts.PublicKeyPath, opts.SkipSignatureValidation)
96-
if err != nil {
97-
return nil, err
93+
94+
if pkgLayout.IsSigned() && !opts.SkipSignatureValidation {
95+
verifyOptions := utils.DefaultVerifyBlobOptions()
96+
verifyOptions.KeyRef = opts.PublicKeyPath
97+
98+
err = pkgLayout.VerifyPackageSignature(ctx, verifyOptions)
99+
if err != nil {
100+
return nil, err
101+
}
98102
}
103+
99104
return pkgLayout, nil
100105
}
101106

@@ -243,6 +248,41 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOpti
243248
return nil
244249
}
245250

251+
// VerifyPackageSignature verifies the package signature
252+
func (p *PackageLayout) VerifyPackageSignature(ctx context.Context, opts utils.VerifyBlobOptions) error {
253+
l := logger.From(ctx)
254+
l.Debug("verifying package signature")
255+
256+
// Validate package layout state
257+
if p.dirPath == "" {
258+
return errors.New("invalid package layout: dirPath is empty")
259+
}
260+
if info, err := os.Stat(p.dirPath); err != nil {
261+
return fmt.Errorf("invalid package layout directory: %w", err)
262+
} else if !info.IsDir() {
263+
return fmt.Errorf("invalid package layout: %s is not a directory", p.dirPath)
264+
}
265+
266+
// Validate that we have a public key
267+
// Note: this will later be replaced when verification enhancements are made
268+
if opts.KeyRef == "" {
269+
return errors.New("package is signed but no key was provided")
270+
}
271+
272+
// Validate that the signature exists
273+
signaturePath := filepath.Join(p.dirPath, Signature)
274+
if _, err := os.Stat(signaturePath); err != nil {
275+
return fmt.Errorf("signature not found: %w", err)
276+
}
277+
278+
// Note: this is the backwards compatible behavior
279+
// this will change in the future
280+
opts.SigRef = signaturePath
281+
282+
ZarfYAMLPath := filepath.Join(p.dirPath, ZarfYAML)
283+
return utils.CosignVerifyBlobWithOptions(ctx, ZarfYAMLPath, opts)
284+
}
285+
246286
// IsSigned returns true if the package is signed.
247287
// It first checks the package metadata (Build.Signed), then falls back to
248288
// checking for the presence of a signature file for backward compatibility.
@@ -487,38 +527,3 @@ func validatePackageIntegrity(pkgLayout *PackageLayout, isPartial bool) error {
487527

488528
return nil
489529
}
490-
491-
func validatePackageSignature(ctx context.Context, pkgLayout *PackageLayout, publicKeyPath string, skipSignatureValidation bool) error {
492-
if skipSignatureValidation {
493-
return nil
494-
}
495-
496-
signaturePath := filepath.Join(pkgLayout.dirPath, Signature)
497-
sigExist := true
498-
_, err := os.Stat(signaturePath)
499-
if err != nil {
500-
sigExist = false
501-
}
502-
if !sigExist && publicKeyPath == "" {
503-
// Nobody was expecting a signature, so we can just return
504-
return nil
505-
} else if sigExist && publicKeyPath == "" {
506-
return errors.New("package is signed but no key was provided")
507-
} else if !sigExist && publicKeyPath != "" {
508-
return errors.New("a key was provided but the package is not signed")
509-
}
510-
511-
keyOptions := options.KeyOpts{KeyRef: publicKeyPath}
512-
cmd := &verify.VerifyBlobCmd{
513-
KeyOpts: keyOptions,
514-
SigRef: signaturePath,
515-
IgnoreSCT: true,
516-
Offline: true,
517-
IgnoreTlog: true,
518-
}
519-
err = cmd.Exec(ctx, filepath.Join(pkgLayout.dirPath, ZarfYAML))
520-
if err != nil {
521-
return fmt.Errorf("package signature did not match the provided key: %w", err)
522-
}
523-
return nil
524-
}

0 commit comments

Comments
 (0)