Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
895809a
feat(sign): align signing and verification to cosign
brandtkeller May 7, 2026
972fa31
chore(docs): update comments and language
brandtkeller May 7, 2026
762484e
fix(cosign): implement non-prompting passfunc
brandtkeller May 8, 2026
30bbdbd
feat(sign): support for embedded TrustedRoot
brandtkeller May 8, 2026
17dfcd1
feat(sign): enable keyless signing and verification
brandtkeller May 9, 2026
cab4e08
fix(options): deprecate duplicitive fields
brandtkeller May 11, 2026
bfd2a5f
Merge branch 'main' of github.com:zarf-dev/zarf into 4572_sign_verify…
brandtkeller May 11, 2026
4f2d01e
fix(flags): review flags that can be further hidden based on utility
brandtkeller May 11, 2026
e30354e
fix(flags): trustedroot flag is not operational... yet
brandtkeller May 11, 2026
4c26a4e
Merge branch 'main' of github.com:zarf-dev/zarf into 4572_sign_verify…
brandtkeller May 11, 2026
1719f3b
chore(update): resolve changes from 4572 branch
brandtkeller May 11, 2026
fc8151b
fix(sigstore): retrieve identity/issuer from bundle
brandtkeller May 11, 2026
7682107
fix(cmd): explicit opt-in for cosign options/flags
brandtkeller May 11, 2026
1fa7225
choer(merge): merge 4572 into 2805
brandtkeller May 11, 2026
bc9797d
feat(sign): implement keyless signing options explicitly
brandtkeller May 11, 2026
6956676
fix(sign): support for oci signing operations
brandtkeller May 11, 2026
fa088d4
fix(test): update error message for verification material
brandtkeller May 12, 2026
4f49439
fix(cosign): add timeout to verify
brandtkeller May 12, 2026
650aebd
Merge branch '4572_sign_verify_flags' of github.com:zarf-dev/zarf int…
brandtkeller May 12, 2026
c49d85d
fix(requirement): add version requirement for bundle signing verifica…
brandtkeller May 12, 2026
69067ca
merge main into 2805_keyless_signing
brandtkeller May 12, 2026
aed485c
feat(sign): add signature build metadata for future utility
brandtkeller May 12, 2026
19259d7
fix(bundle): implement generic bundle parsing
brandtkeller May 13, 2026
efb1d68
chore: generate the updated schema
brandtkeller May 13, 2026
d73dcde
fix(test): update test for error output
brandtkeller May 13, 2026
bcaa110
fix(test): update expected errors
brandtkeller May 13, 2026
0e6a4e2
feat(signing): isolate cosign signing and verification into new package
brandtkeller May 13, 2026
bcccd08
fix(schema): remove signature field and regen schema
brandtkeller May 13, 2026
2487805
move deprecation logic earlier in the chain
brandtkeller May 13, 2026
cfe6743
Update hack/refresh-trusted-root.sh
brandtkeller May 13, 2026
3f63c37
feat(cmd): support signing/verify viper configuration
brandtkeller May 14, 2026
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
14 changes: 14 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,27 @@ Once the prerelease artifacts are published, a Homebrew Tap PR is created. It is

### Standard Releases (via Release Please)

* [ ] Refresh the embedded Sigstore TrustedRoot used for keyless verification (see [Embedded TrustedRoot](#embedded-trustedroot))
* [ ] Review and merge the open Release Please PR
* [ ] The tag is automatically created and pushed, triggering the release workflow
* [ ] Review the GitHub release:
* [ ] Add a summary of release updates and any required documentation around updates or breaking changes
* [ ] Ensure goreleaser workflows execute successfully and review the release assets
* [ ] Review, approve, and merge the [homebrew-tap](https://github.com/defenseunicorns/homebrew-tap) PR for the zarf release

### Embedded TrustedRoot

Zarf binaries embed a Sigstore TrustedRoot JSON used by `zarf package verify` for keyless verification when `--trusted-root` is not supplied. The TUF-fetched copy is committed at `src/pkg/utils/embedded_trusted_root.json` and must be refreshed before each release so binaries ship with current Sigstore trust material.

```bash
make build
hack/refresh-trusted-root.sh
git add src/pkg/utils/embedded_trusted_root.json
git commit -m "chore: refresh embedded trusted root"
```

The script wraps `zarf tools trusted-root create --with-default-services`, which reaches `tuf-repo-cdn.sigstore.dev` and writes the verified TrustedRoot to the embed path. Users who run their own Sigstore infrastructure can bypass the embedded copy at runtime via `zarf package verify --trusted-root /path/to/custom.json`.

### Manual Releases (if needed)

For cases where you need to manually create a release (e.g., release candidates):
Expand Down
23 changes: 23 additions & 0 deletions hack/refresh-trusted-root.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Refresh the embedded Sigstore TrustedRoot used for keyless verification.
# Run before each release. Commit the result.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making a mental note to schedule daily checks and notify the zarf channel using this process. A lag in releasing new trusted roots won't be critical but should be nice to have.

set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
EMBED_PATH="${REPO_ROOT}/src/pkg/signing/embedded_trusted_root.json"
ZARF_BIN="${REPO_ROOT}/build/zarf"

if [ ! -x "${ZARF_BIN}" ]; then
echo "build/zarf not found; run 'make build' first" >&2
exit 1
fi

if ! command -v jq >/dev/null 2>&1; then
echo "jq is required to format the embedded trusted root for reviewable diffs" >&2
exit 1
fi

"${ZARF_BIN}" tools trusted-root create --with-default-services --out "${EMBED_PATH}"
jq --indent 2 . "${EMBED_PATH}" > "${EMBED_PATH}.tmp" && mv "${EMBED_PATH}.tmp" "${EMBED_PATH}"
echo "Refreshed ${EMBED_PATH}"
9 changes: 9 additions & 0 deletions site/src/content/docs/commands/zarf_package_sign.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,23 @@ $ zarf package sign zarf-package-demo-amd64-1.0.0.tar.zst --signing-key awskms:/
### Options

```
--confirm Skip the interactive confirmation prompt before uploading to the Rekor transparency log (equivalent to cosign --yes).
--fulcio-auth-flow string Fulcio OAuth flow: normal (browser), device (device code), token, client_credentials (default "normal")
--fulcio-url string Fulcio certificate authority URL. Override for private Sigstore deployments. (default "https://fulcio.sigstore.dev")
-h, --help help for sign
--identity-token string Pre-acquired OIDC identity token (or path to a file containing one) for non-interactive keyless signing
-k, --key string Public key to verify the existing signature before re-signing (optional)
--keyless Sign without a private key using Sigstore's keyless flow (Fulcio/OIDC)
--oci-concurrency int Number of concurrent layer operations when pulling or pushing images or packages to/from OCI registries. (default 6)
--oidc-client-id string OIDC client ID used when requesting an identity token. Override for private Sigstore deployments. (default "sigstore")
--oidc-issuer string OIDC issuer URL used to obtain an identity token for keyless signing. Override for private Sigstore deployments. (default "https://oauth2.sigstore.dev/auth")
-o, --output string Output destination for the signed package. Can be a local directory or an OCI registry URL (oci://). Default: same directory as source package for files, current directory for OCI sources
--overwrite Overwrite an existing signature if the package is already signed
--rekor-url string Rekor transparency log URL. Override for private Sigstore deployments. (default "https://rekor.sigstore.dev")
--retries int Number of retries to perform for Zarf operations like git/image pushes (default 3)
--signing-key string Private key for signing packages. Accepts either a local file path or a Cosign-supported key provider (awskms://, gcpkms://, azurekms://, hashivault://)
--signing-key-pass string Password for encrypted private key
--tlog-upload Upload the signature to the Rekor transparency log. Auto-enabled when --keyless is set (required for keyless signatures to remain verifiable past the ~10 minute Fulcio certificate validity window).
--verify Verify the Zarf package signature
```

Expand Down
12 changes: 9 additions & 3 deletions site/src/content/docs/commands/zarf_package_verify.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,15 @@ $ zarf package verify zarf-package-demo-amd64-1.0.0.tar.zst
### Options

```
-h, --help help for verify
-k, --key string Public key for signature verification
--oci-concurrency int Number of concurrent layer operations when pulling or pushing images or packages to/from OCI registries. (default 6)
--certificate-identity string Required identity claim in the signing certificate (keyless verify). Example: signer@example.com or https://github.com/org/repo/.github/workflows/release.yml@refs/heads/main
--certificate-identity-regexp string Regex variant of --certificate-identity
--certificate-oidc-issuer string Required OIDC issuer claim in the signing certificate (keyless verify). Example: https://github.com/login/oauth or https://token.actions.githubusercontent.com
--certificate-oidc-issuer-regexp string Regex variant of --certificate-oidc-issuer
-h, --help help for verify
--insecure-ignore-tlog Skip Rekor transparency log inclusion verification. Default true for air-gap. Auto-disabled when keyless identity flags are set (keyless signatures require Rekor inclusion proof to remain verifiable past certificate expiry). (default true)
-k, --key string Public key for signature verification
--oci-concurrency int Number of concurrent layer operations when pulling or pushing images or packages to/from OCI registries. (default 6)
--trusted-root string Path to a Sigstore TrustedRoot JSON. Falls back to the binary-embedded copy when omitted.
```

### Options inherited from parent commands
Expand Down
168 changes: 133 additions & 35 deletions src/cmd/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"github.com/zarf-dev/zarf/src/pkg/packager"
"github.com/zarf-dev/zarf/src/pkg/packager/filters"
"github.com/zarf-dev/zarf/src/pkg/packager/layout"
"github.com/zarf-dev/zarf/src/pkg/signing"
"github.com/zarf-dev/zarf/src/pkg/state"
"github.com/zarf-dev/zarf/src/pkg/utils"
"github.com/zarf-dev/zarf/src/pkg/zoci"
Expand Down Expand Up @@ -1746,13 +1747,17 @@ func (o *packagePublishOptions) run(cmd *cobra.Command, args []string) error {
err = errors.Join(err, pkgLayout.Cleanup())
}()

publishSignOpts := signing.DefaultSignBlobOptions()
publishSignOpts.Key = o.signingKeyPath
publishSignOpts.Password = o.signingKeyPassword
publishSignOpts.Overwrite = true

publishPackageOpts := packager.PublishPackageOptions{
OCIConcurrency: o.ociConcurrency,
SigningKeyPath: o.signingKeyPath,
SigningKeyPassword: o.signingKeyPassword,
Retries: o.retries,
RemoteOptions: defaultRemoteOptions(),
Tag: o.tag,
OCIConcurrency: o.ociConcurrency,
SignBlobOptions: publishSignOpts,
Retries: o.retries,
RemoteOptions: defaultRemoteOptions(),
Tag: o.tag,
}

_, err = packager.PublishPackage(ctx, pkgLayout, dstRef, publishPackageOpts)
Expand Down Expand Up @@ -1848,6 +1853,17 @@ type packageSignOptions struct {
ociConcurrency int
retries int
verify bool
// Keyless signing flags. Each is hand-rolled and individually opted-in;
// new cosign flags will not appear here automatically on dependency bumps.
keyless bool
identityToken string
fulcioURL string
fulcioAuthFlow string
oidcIssuer string
oidcClientID string
rekorURL string
tlogUpload bool
confirm bool
}

func newPackageSignCommand(v *viper.Viper) *cobra.Command {
Expand All @@ -1872,6 +1888,18 @@ func newPackageSignCommand(v *viper.Viper) *cobra.Command {
cmd.Flags().IntVar(&o.retries, "retries", v.GetInt(VPkgRetries), lang.CmdPackageFlagRetries)
cmd.Flags().BoolVar(&o.verify, "verify", v.GetBool(VPkgVerify), lang.CmdPackageFlagVerify)

cmd.Flags().BoolVar(&o.keyless, "keyless", false, lang.CmdPackageSignFlagKeyless)
Comment thread
brandtkeller marked this conversation as resolved.
cmd.Flags().StringVar(&o.identityToken, "identity-token", "", lang.CmdPackageSignFlagIdentityToken)
cmd.Flags().StringVar(&o.fulcioURL, "fulcio-url", v.GetString(VPkgSignFulcioURL), lang.CmdPackageSignFlagFulcioURL)
cmd.Flags().StringVar(&o.fulcioAuthFlow, "fulcio-auth-flow", v.GetString(VPkgSignFulcioAuthFlow), lang.CmdPackageSignFlagFulcioAuthFlow)
cmd.Flags().StringVar(&o.oidcIssuer, "oidc-issuer", v.GetString(VPkgSignOIDCIssuer), lang.CmdPackageSignFlagOIDCIssuer)
cmd.Flags().StringVar(&o.oidcClientID, "oidc-client-id", v.GetString(VPkgSignOIDCClientID), lang.CmdPackageSignFlagOIDCClientID)
cmd.Flags().StringVar(&o.rekorURL, "rekor-url", v.GetString(VPkgSignRekorURL), lang.CmdPackageSignFlagRekorURL)
cmd.Flags().BoolVar(&o.tlogUpload, "tlog-upload", v.GetBool(VPkgSignTlogUpload), lang.CmdPackageSignFlagTlogUpload)
cmd.Flags().BoolVar(&o.confirm, "confirm", false, lang.CmdPackageSignFlagConfirm)

cmd.MarkFlagsMutuallyExclusive("keyless", "signing-key")

return cmd
}

Expand All @@ -1880,8 +1908,8 @@ func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error {
l := logger.From(ctx)
packageSource := args[0]

if o.signingKeyPath == "" {
return errors.New("--signing-key is required")
if !o.keyless && o.signingKeyPath == "" {
return errors.New("--signing-key is required (or pass --keyless for Sigstore keyless flow)")
}

// Determine output destination
Expand Down Expand Up @@ -1913,31 +1941,34 @@ func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error {
}
}

// If output is OCI (either default or user-specified), delegate to publish workflow
if helpers.IsOCIURL(outputDest) {
l.Info("signing and publishing package to OCI registry", "source", packageSource, "destination", outputDest)

// Create publish options from sign options
publishOpts := &packagePublishOptions{
signingKeyPath: o.signingKeyPath,
signingKeyPassword: o.signingKeyPassword,
ociConcurrency: o.ociConcurrency,
retries: o.retries,
publicKeyPath: o.publicKeyPath,
verify: o.verify,
}

// Call publish with source and destination repository
return publishOpts.run(cmd, []string{packageSource, outputDest})
}

// For local file output, use existing sign logic
cachePath, err := getCachePath(ctx)
if err != nil {
return err
}

// Load the package - do not verify
// Pull from OCI to a local temp dir before loading
if helpers.IsOCIURL(packageSource) {
tmpdir, err := utils.MakeTempDir(config.CommonOptions.TempDirectory)
if err != nil {
return err
}
defer func() {
if removeErr := os.RemoveAll(tmpdir); removeErr != nil {
l.Warn("failed to remove temp dir", "error", removeErr)
}
}()
packageSource, err = packager.Pull(ctx, packageSource, tmpdir, packager.PullOptions{
VerificationStrategy: layout.VerifyNever,
Architecture: config.GetArch(),
OCIConcurrency: o.ociConcurrency,
RemoteOptions: defaultRemoteOptions(),
CachePath: cachePath,
})
if err != nil {
return fmt.Errorf("failed to pull package: %w", err)
}
}

loadOpts := packager.LoadOptions{
Filter: filters.Empty(),
Architecture: config.GetArch(),
Expand Down Expand Up @@ -1974,20 +2005,57 @@ func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error {
}
}

// Sign the package
l.Info("signing package with provided key")
if o.keyless {
l.Info("signing package via Sigstore keyless flow")
} else {
l.Info("signing package with provided key")
}

signOpts := utils.DefaultSignBlobOptions()
signOpts := signing.DefaultSignBlobOptions()
signOpts.Key = o.signingKeyPath
signOpts.Password = o.signingKeyPassword
signOpts.Overwrite = o.overwrite
signOpts.Keyless = o.keyless
signOpts.Fulcio.IdentityToken = o.identityToken
signOpts.Fulcio.URL = o.fulcioURL
signOpts.Fulcio.AuthFlow = o.fulcioAuthFlow
signOpts.OIDC.Issuer = o.oidcIssuer
signOpts.OIDC.ClientID = o.oidcClientID
signOpts.Rekor.URL = o.rekorURL
signOpts.TlogUpload = o.tlogUpload
signOpts.SkipConfirmation = o.confirm

// Keyless certs are short-lived (~10 min). Without Rekor or a TSA timestamp
// the signature is unverifiable past expiry. Default --tlog-upload=true for
// keyless unless the user explicitly opted out.
if o.keyless && !cmd.Flags().Changed("tlog-upload") {
signOpts.TlogUpload = true
}
Comment on lines +2031 to +2033
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the current plan to support tsa using the same enable flag if keyless method? Cosign does use tsa by default and our default trusted root has the timestampAuthorities field


if helpers.IsOCIURL(outputDest) {
parts := strings.Split(strings.TrimPrefix(outputDest, helpers.OCIURLPrefix), "/")
dstRef := registry.Reference{
Registry: parts[0],
Repository: strings.Join(parts[1:], "/"),
}
if err := dstRef.ValidateRegistry(); err != nil {
return fmt.Errorf("invalid destination registry: %w", err)
}
l.Info("signing and publishing package to OCI registry", "destination", outputDest)
_, err = packager.PublishPackage(ctx, pkgLayout, dstRef, packager.PublishPackageOptions{
OCIConcurrency: o.ociConcurrency,
SignBlobOptions: signOpts,
Retries: o.retries,
RemoteOptions: defaultRemoteOptions(),
})
return err
}

err = pkgLayout.SignPackage(ctx, signOpts)
if err != nil {
return fmt.Errorf("failed to sign package: %w", err)
}

// Archive to local directory
l.Info("archiving signed package to local directory", "directory", outputDest)
signedPath, err := pkgLayout.Archive(ctx, outputDest, 0)
if err != nil {
Expand All @@ -2001,6 +2069,13 @@ func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error {
type packageVerifyOptions struct {
publicKeyPath string
ociConcurrency int
// Keyless verify flags. Each is hand-rolled and individually opted-in.
certificateIdentity string
certificateIdentityRegexp string
certificateOIDCIssuer string
certificateOIDCIssuerRegexp string
trustedRoot string
insecureIgnoreTlog bool
}

func newPackageVerifyCommand(v *viper.Viper) *cobra.Command {
Expand All @@ -2019,6 +2094,13 @@ func newPackageVerifyCommand(v *viper.Viper) *cobra.Command {
cmd.Flags().StringVarP(&o.publicKeyPath, "key", "k", v.GetString(VPkgPublicKey), lang.CmdPackageVerifyFlagKey)
cmd.Flags().IntVar(&o.ociConcurrency, "oci-concurrency", v.GetInt(VPkgOCIConcurrency), lang.CmdPackageFlagConcurrency)

cmd.Flags().StringVar(&o.certificateIdentity, "certificate-identity", v.GetString(VPkgVerifyCertIdentity), lang.CmdPackageVerifyFlagCertificateIdentity)
cmd.Flags().StringVar(&o.certificateIdentityRegexp, "certificate-identity-regexp", v.GetString(VPkgVerifyCertIdentityRegexp), lang.CmdPackageVerifyFlagCertificateIdentityRegexp)
cmd.Flags().StringVar(&o.certificateOIDCIssuer, "certificate-oidc-issuer", v.GetString(VPkgVerifyCertOIDCIssuer), lang.CmdPackageVerifyFlagCertificateOIDCIssuer)
cmd.Flags().StringVar(&o.certificateOIDCIssuerRegexp, "certificate-oidc-issuer-regexp", v.GetString(VPkgVerifyCertOIDCIssuerRegexp), lang.CmdPackageVerifyFlagCertificateOIDCIssuerRegexp)
cmd.Flags().StringVar(&o.trustedRoot, "trusted-root", v.GetString(VPkgVerifyTrustedRoot), lang.CmdPackageVerifyFlagTrustedRoot)
cmd.Flags().BoolVar(&o.insecureIgnoreTlog, "insecure-ignore-tlog", v.GetBool(VPkgVerifyInsecureIgnoreTlog), lang.CmdPackageVerifyFlagInsecureIgnoreTlog)

return cmd
}

Expand All @@ -2037,8 +2119,24 @@ func (o *packageVerifyOptions) run(cmd *cobra.Command, args []string) error {
// Load the package with verification enabled
// The verify command always uses strict verification (VerifyAlways)
// This will error if: signed package without key, or unsigned package with key
verifyOpts := signing.DefaultVerifyBlobOptions()
verifyOpts.Key = o.publicKeyPath
verifyOpts.CertVerify.CertIdentity = o.certificateIdentity
verifyOpts.CertVerify.CertIdentityRegexp = o.certificateIdentityRegexp
verifyOpts.CertVerify.CertOidcIssuer = o.certificateOIDCIssuer
verifyOpts.CertVerify.CertOidcIssuerRegexp = o.certificateOIDCIssuerRegexp
verifyOpts.CommonVerifyOptions.TrustedRootPath = o.trustedRoot
verifyOpts.CommonVerifyOptions.IgnoreTlog = o.insecureIgnoreTlog

// Optimally by default use the inclusion proof to establish when a signature was made.
// this is offline-compliant and airgap compatible given keyless signed bundle outputs.
hasKeylessIdentity := o.certificateIdentity != "" || o.certificateIdentityRegexp != ""
if hasKeylessIdentity && !cmd.Flags().Changed("insecure-ignore-tlog") {
verifyOpts.CommonVerifyOptions.IgnoreTlog = false
}

loadOpts := packager.LoadOptions{
VerifyBlobOptions: verifyBlobOptionsFromKeyPath(o.publicKeyPath),
VerifyBlobOptions: &verifyOpts,
VerificationStrategy: layout.VerifyAlways, // Always enforce strict verification
Filter: filters.Empty(),
Architecture: config.GetArch(),
Expand Down Expand Up @@ -2145,8 +2243,8 @@ func getVerificationStrategy(verify bool) layout.VerificationStrategy {
return layout.VerifyIfPossible
}

func verifyBlobOptionsFromKeyPath(keyPath string) *utils.VerifyBlobOptions {
opts := utils.DefaultVerifyBlobOptions()
func verifyBlobOptionsFromKeyPath(keyPath string) *signing.VerifyBlobOptions {
opts := signing.DefaultVerifyBlobOptions()
opts.Key = keyPath
return &opts
}
4 changes: 2 additions & 2 deletions src/cmd/tools_trustedroot.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"github.com/spf13/cobra"

"github.com/zarf-dev/zarf/src/config/lang"
"github.com/zarf-dev/zarf/src/pkg/utils"
"github.com/zarf-dev/zarf/src/pkg/signing"
)

func newTrustedRootCommand() *cobra.Command {
Expand Down Expand Up @@ -79,7 +79,7 @@ func newTrustedRootCreateCommand() *cobra.Command {
return errors.New("provide --with-default-services to retrieve the public Sigstore trusted root, or specify --fulcio/--rekor/--ctfe/--tsa to compose a custom trusted root")
}

ctx, cancel := context.WithTimeout(cmd.Context(), utils.CosignDefaultTimeout)
ctx, cancel := context.WithTimeout(cmd.Context(), signing.CosignDefaultTimeout)
defer cancel()
return optionsToCreateCmd(o).Exec(ctx)
},
Expand Down
Loading
Loading