From 895809a3b58b7c23ee30747a51cba865e4b96dbc Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Thu, 7 May 2026 22:15:47 +0000 Subject: [PATCH 01/25] feat(sign): align signing and verification to cosign Signed-off-by: Brandt Keller --- .../docs/commands/zarf_package_sign.md | 41 ++- .../docs/commands/zarf_package_verify.md | 33 ++- src/cmd/package.go | 97 ++++++- src/config/lang/english.go | 2 - src/pkg/packager/layout/assemble.go | 4 +- src/pkg/packager/layout/package.go | 10 +- src/pkg/packager/layout/package_test.go | 217 +++++----------- src/pkg/packager/load.go | 2 +- src/pkg/packager/load_test.go | 2 +- src/pkg/packager/publish.go | 2 +- src/pkg/packager/publish_test.go | 2 +- src/pkg/packager/pull.go | 2 +- src/pkg/utils/cosign.go | 245 ++++++++++-------- src/test/e2e/12_package_signing_test.go | 54 ++++ 14 files changed, 415 insertions(+), 298 deletions(-) diff --git a/site/src/content/docs/commands/zarf_package_sign.md b/site/src/content/docs/commands/zarf_package_sign.md index a6ebcdbc74..c5112bbe73 100644 --- a/site/src/content/docs/commands/zarf_package_sign.md +++ b/site/src/content/docs/commands/zarf_package_sign.md @@ -42,15 +42,38 @@ $ zarf package sign zarf-package-demo-amd64-1.0.0.tar.zst --signing-key awskms:/ ### Options ``` - -h, --help help for sign - -k, --key string Public key to verify the existing signature before re-signing (optional) - --oci-concurrency int Number of concurrent layer operations when pulling or pushing images or packages to/from OCI registries. (default 6) - -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 - --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 - --verify Verify the Zarf package signature + --certificate string path to the X.509 certificate for signing attestation + --certificate-chain string path to a list of CA X.509 certificates in PEM format which will be needed when building the certificate chain for the signed attestation. Must start with the parent intermediate CA certificate of the signing certificate and end with the root certificate. + --fulcio-auth-flow string fulcio interactive oauth2 flow to use for certificate from fulcio. Defaults to determining the flow based on the runtime environment. (options) normal|device|token|client_credentials + --fulcio-url string address of sigstore PKI server (default "https://fulcio.sigstore.dev") + -h, --help help for sign + --identity-token string identity token to use for certificate from fulcio. the token or a path to a file containing the token is accepted. + --insecure-skip-verify skip verifying fulcio published to the SCT (this should only be used for testing). + -k, --key string Public key to verify the existing signature before re-signing (optional) + --new-bundle-format output bundle in new format that contains all verification material (default true) + --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 for application (default "sigstore") + --oidc-client-secret-file string Path to file containing OIDC client secret for application + --oidc-disable-ambient-providers Disable ambient OIDC providers. When true, ambient credentials will not be read + --oidc-issuer string OIDC provider to be used to issue ID token (default "https://oauth2.sigstore.dev/auth") + --oidc-provider string Specify the provider to get the OIDC token from (Optional). If unset, all options will be tried. Options include: [spiffe, google, github-actions, filesystem, buildkite-agent] + --oidc-redirect-url string OIDC redirect URL (Optional). The default oidc-redirect-url is 'http://localhost:0/auth/callback'. + -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 address of rekor STL server (default "https://rekor.sigstore.dev") + --retries int Number of retries to perform for Zarf operations like git/image pushes (default 3) + --signing-algorithm string signing algorithm to use for signing/hashing (allowed ecdsa-sha2-256-nistp256, ecdsa-sha2-384-nistp384, ecdsa-sha2-512-nistp521, rsa-sign-pkcs1-2048-sha256, rsa-sign-pkcs1-3072-sha256, rsa-sign-pkcs1-4096-sha256) (default "ecdsa-sha2-256-nistp256") + --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 + --sk whether to use a hardware security key + --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) + --timestamp-client-cacert string path to the X.509 CA certificate file in PEM format to be used for the connection to the TSA Server + --timestamp-client-cert string path to the X.509 certificate file in PEM format to be used for the connection to the TSA Server + --timestamp-client-key string path to the X.509 private key file in PEM format to be used, together with the 'timestamp-client-cert' value, for the connection to the TSA Server + --timestamp-server-name string SAN name to use as the 'ServerName' tls.Config field to verify the mTLS connection to the TSA Server + --timestamp-server-url string url to the Timestamp RFC3161 server, default none. Must be the path to the API to request timestamp responses, e.g. https://freetsa.org/tsr + --verify Verify the Zarf package signature + -y, --yes skip confirmation prompts for non-destructive operations ``` ### Options inherited from parent commands diff --git a/site/src/content/docs/commands/zarf_package_verify.md b/site/src/content/docs/commands/zarf_package_verify.md index 86819bfb77..bf88a7d664 100644 --- a/site/src/content/docs/commands/zarf_package_verify.md +++ b/site/src/content/docs/commands/zarf_package_verify.md @@ -33,9 +33,36 @@ $ 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) + --ca-intermediates string path to a file of intermediate CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. The flag is optional and must be used together with --ca-roots, conflicts with --certificate-chain. + --ca-roots string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. + --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. + --certificate-chain string path to a list of CA certificates in PEM format which will be needed when building the certificate chain for the signing certificate. Must start with the parent intermediate CA certificate of the signing certificate and end with the root certificate. Conflicts with --ca-roots and --ca-intermediates. + --certificate-github-workflow-name string contains the workflow claim from the GitHub OIDC Identity token that contains the name of the executed workflow. + --certificate-github-workflow-ref string contains the ref claim from the GitHub OIDC Identity token that contains the git ref that the workflow run was based upon. + --certificate-github-workflow-repository string contains the repository claim from the GitHub OIDC Identity token that contains the repository that the workflow run was based upon + --certificate-github-workflow-sha string contains the sha claim from the GitHub OIDC Identity token that contains the commit SHA that the workflow run was based upon. + --certificate-github-workflow-trigger string contains the event_name claim from the GitHub OIDC Identity token that contains the name of the event that triggered the workflow run + --certificate-identity string The identity expected in a valid Fulcio certificate. Valid values include email address, DNS names, IP addresses, and URIs. Either --certificate-identity or --certificate-identity-regexp must be set for keyless flows. + --certificate-identity-regexp string A regular expression alternative to --certificate-identity. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --certificate-identity or --certificate-identity-regexp must be set for keyless flows. + --certificate-oidc-issuer string The OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth. Either --certificate-oidc-issuer or --certificate-oidc-issuer-regexp must be set for keyless flows. + --certificate-oidc-issuer-regexp string A regular expression alternative to --certificate-oidc-issuer. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --certificate-oidc-issuer or --certificate-oidc-issuer-regexp must be set for keyless flows. + --experimental-oci11 set to true to enable experimental OCI 1.1 behaviour (unrelated to bundle format) + -h, --help help for verify + --insecure-ignore-sct when set, verification will not check that a certificate contains an embedded SCT, a proof of inclusion in a certificate transparency log (default true) + --insecure-ignore-tlog ignore transparency log verification, to be used when an artifact signature has not been uploaded to the transparency log. Artifacts cannot be publicly verified when not included in a log (default true) + -k, --key string path to the public key file, KMS URI or Kubernetes Secret + --max-workers int the amount of maximum workers for parallel executions (default 10) + --new-bundle-format expect the signature/attestation to be packaged in a Sigstore bundle (default true) + --oci-concurrency int Number of concurrent layer operations when pulling or pushing images or packages to/from OCI registries. (default 6) + --private-infrastructure skip transparency log verification when verifying artifacts in a privately deployed infrastructure + --rekor-url string address of rekor STL server (default "https://rekor.sigstore.dev") + --sct string path to a detached Signed Certificate Timestamp, formatted as a RFC6962 AddChainResponse struct. If a certificate contains an SCT, verification will check both the detached and embedded SCTs. + --signature-digest-algorithm string digest algorithm to use when processing a signature (sha224|sha256|sha384|sha512) (default "sha256") + --sk whether to use a hardware security key + --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) + --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp + --trusted-root string Path to a Sigstore TrustedRoot JSON file. Requires --new-bundle-format to be set. + --use-signed-timestamps verify rfc3161 timestamps ``` ### Options inherited from parent commands diff --git a/src/cmd/package.go b/src/cmd/package.go index c77daababb..055c306959 100644 --- a/src/cmd/package.go +++ b/src/cmd/package.go @@ -23,7 +23,9 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/defenseunicorns/pkg/helpers/v2" goyaml "github.com/goccy/go-yaml" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/spf13/viper" "oras.land/oras-go/v2/registry" @@ -1816,7 +1818,7 @@ func (o *packagePullOptions) run(cmd *cobra.Command, args []string) error { } type packageSignOptions struct { - signingKeyPath string + cosign options.SignBlobOptions signingKeyPassword string publicKeyPath string overwrite bool @@ -1839,7 +1841,9 @@ func newPackageSignCommand(v *viper.Viper) *cobra.Command { RunE: o.run, } - cmd.Flags().StringVar(&o.signingKeyPath, "signing-key", v.GetString(VPkgSignSigningKey), lang.CmdPackageSignFlagSigningKey) + // Zarf's pre-existing flags must register first so they win the name collisions + // (--key, --output) when cosign's AddFlags is folded in below via AddFlagSet. + cmd.Flags().StringVar(&o.cosign.Key, "signing-key", v.GetString(VPkgSignSigningKey), lang.CmdPackageSignFlagSigningKey) cmd.Flags().StringVar(&o.signingKeyPassword, "signing-key-pass", v.GetString(VPkgSignSigningKeyPassword), lang.CmdPackageSignFlagSigningKeyPass) cmd.Flags().StringVarP(&o.output, "output", "o", v.GetString(VPkgSignOutput), lang.CmdPackageSignFlagOutput) cmd.Flags().BoolVar(&o.overwrite, "overwrite", v.GetBool(VPkgSignOverwrite), lang.CmdPackageSignFlagOverwrite) @@ -1848,15 +1852,55 @@ 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) + mergeCosignSignFlags(cmd, &o.cosign) + return cmd } +// mergeCosignSignFlags binds cosign's SignBlobOptions onto a throwaway command, +// hides flags whose support is deferred or are deprecated upstream, applies +// zarf's air-gap default overrides, and folds the result into cmd via AddFlagSet. +// AddFlagSet skips flag names already registered, so zarf's pre-existing --key +// and --output retain their semantics. +func mergeCosignSignFlags(cmd *cobra.Command, opts *options.SignBlobOptions) { + side := &cobra.Command{} + opts.AddFlags(side) + hideAndOverrideSign(side.Flags(), opts) + cmd.Flags().AddFlagSet(side.Flags()) +} + +func hideAndOverrideSign(fs *pflag.FlagSet, opts *options.SignBlobOptions) { + for _, name := range []string{ + "bundle", "output-signature", "output-certificate", + "b64", "rfc3161-timestamp", "issue-certificate", + "signing-config", "use-signing-config", "trusted-root", + } { + if f := fs.Lookup(name); f != nil { + f.Hidden = true + } + } + + opts.TlogUpload = false + if f := fs.Lookup("tlog-upload"); f != nil { + setFlagDefault(f, "false") + } +} + +// setFlagDefault updates both the runtime value and the help-rendered default of a flag. +// Panics on invalid value because callers pass static literals validated by flag type. +func setFlagDefault(f *pflag.Flag, value string) { + f.DefValue = value + if err := f.Value.Set(value); err != nil { + panic(fmt.Sprintf("setting flag %q default to %q: %v", f.Name, value, err)) + } +} + func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error { ctx := cmd.Context() l := logger.From(ctx) packageSource := args[0] - if o.signingKeyPath == "" { + if o.cosign.Key == "" { return errors.New("--signing-key is required") } @@ -1895,7 +1939,7 @@ func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error { // Create publish options from sign options publishOpts := &packagePublishOptions{ - signingKeyPath: o.signingKeyPath, + signingKeyPath: o.cosign.Key, signingKeyPassword: o.signingKeyPassword, ociConcurrency: o.ociConcurrency, retries: o.retries, @@ -1954,7 +1998,7 @@ func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error { l.Info("signing package with provided key") signOpts := utils.DefaultSignBlobOptions() - signOpts.KeyRef = o.signingKeyPath + signOpts.SignBlobOptions = o.cosign signOpts.Password = o.signingKeyPassword signOpts.Overwrite = o.overwrite @@ -1975,7 +2019,7 @@ func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error { } type packageVerifyOptions struct { - publicKeyPath string + cosign options.VerifyBlobOptions ociConcurrency int } @@ -1992,12 +2036,44 @@ func newPackageVerifyCommand(v *viper.Viper) *cobra.Command { RunE: o.run, } - cmd.Flags().StringVarP(&o.publicKeyPath, "key", "k", v.GetString(VPkgPublicKey), lang.CmdPackageVerifyFlagKey) cmd.Flags().IntVar(&o.ociConcurrency, "oci-concurrency", v.GetInt(VPkgOCIConcurrency), lang.CmdPackageFlagConcurrency) + mergeCosignVerifyFlags(cmd, &o.cosign) + + // Re-add zarf's existing -k shorthand on cosign's --key (verify semantic matches). + if f := cmd.Flags().Lookup("key"); f != nil { + f.Shorthand = "k" + if v.IsSet(VPkgPublicKey) { + setFlagDefault(f, v.GetString(VPkgPublicKey)) + } + } + return cmd } +func mergeCosignVerifyFlags(cmd *cobra.Command, opts *options.VerifyBlobOptions) { + side := &cobra.Command{} + opts.AddFlags(side) + hideAndOverrideVerify(side.Flags(), opts) + cmd.Flags().AddFlagSet(side.Flags()) +} + +func hideAndOverrideVerify(fs *pflag.FlagSet, opts *options.VerifyBlobOptions) { + for _, name := range []string{"bundle", "signature", "rfc3161-timestamp"} { + if f := fs.Lookup(name); f != nil { + f.Hidden = true + } + } + + opts.CommonVerifyOptions.IgnoreTlog = true + opts.CertVerify.IgnoreSCT = true + for _, name := range []string{"insecure-ignore-tlog", "insecure-ignore-sct"} { + if f := fs.Lookup(name); f != nil { + setFlagDefault(f, "true") + } + } +} + func (o *packageVerifyOptions) run(cmd *cobra.Command, args []string) error { ctx := cmd.Context() l := logger.From(ctx) @@ -2013,8 +2089,11 @@ 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 := utils.DefaultVerifyBlobOptions() + verifyOpts.VerifyBlobOptions = o.cosign + loadOpts := packager.LoadOptions{ - VerifyBlobOptions: verifyBlobOptionsFromKeyPath(o.publicKeyPath), + VerifyBlobOptions: &verifyOpts, VerificationStrategy: layout.VerifyAlways, // Always enforce strict verification Filter: filters.Empty(), Architecture: config.GetArch(), @@ -2123,6 +2202,6 @@ func getVerificationStrategy(verify bool) layout.VerificationStrategy { func verifyBlobOptionsFromKeyPath(keyPath string) *utils.VerifyBlobOptions { opts := utils.DefaultVerifyBlobOptions() - opts.KeyRef = keyPath + opts.Key = keyPath return &opts } diff --git a/src/config/lang/english.go b/src/config/lang/english.go index c0553539ce..09c0a3fa66 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -368,8 +368,6 @@ $ zarf package verify zarf-package-demo-amd64-1.0.0.tar.zst --key ./public-key.p # Verify an unsigned package (checksums only) $ zarf package verify zarf-package-demo-amd64-1.0.0.tar.zst ` - CmdPackageVerifyFlagKey = "Public key for signature verification" - CmdPackagePullShort = "Pulls a Zarf package from a remote registry and save to the local file system" CmdPackagePullExample = ` # Pull a package matching the current architecture diff --git a/src/pkg/packager/layout/assemble.go b/src/pkg/packager/layout/assemble.go index bbd41310db..0b1705e014 100644 --- a/src/pkg/packager/layout/assemble.go +++ b/src/pkg/packager/layout/assemble.go @@ -221,7 +221,7 @@ func AssemblePackage(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath // Sign the package with the provided options signOpts := utils.DefaultSignBlobOptions() - signOpts.KeyRef = opts.SigningKeyPath + signOpts.Key = opts.SigningKeyPath signOpts.Password = opts.SigningKeyPassword err = pkgLayout.SignPackage(ctx, signOpts) @@ -303,7 +303,7 @@ func AssembleSkeleton(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath // Sign the package with the provided options signOpts := utils.DefaultSignBlobOptions() - signOpts.KeyRef = opts.SigningKeyPath + signOpts.Key = opts.SigningKeyPath signOpts.Password = opts.SigningKeyPassword err = pkgLayout.SignPackage(ctx, signOpts) diff --git a/src/pkg/packager/layout/package.go b/src/pkg/packager/layout/package.go index 610756db49..e0bb3aab9a 100644 --- a/src/pkg/packager/layout/package.go +++ b/src/pkg/packager/layout/package.go @@ -115,7 +115,7 @@ func LoadFromDir(ctx context.Context, dirPath string, opts PackageLayoutOptions) // ensuring the new API takes precedence over the deprecated field. if opts.VerifyBlobOptions == nil && opts.PublicKeyPath != "" { defaults := utils.DefaultVerifyBlobOptions() - defaults.KeyRef = opts.PublicKeyPath + defaults.Key = opts.PublicKeyPath opts.VerifyBlobOptions = &defaults } @@ -338,7 +338,7 @@ func (p *PackageLayout) VerifyPackageSignature(ctx context.Context, opts utils.V // Handle the case where the package is not signed if !p.IsSigned() { // Note: add future logic for verification material here - if opts.KeyRef != "" { + if opts.Key != "" { return errors.New("a key was provided but the package is not signed") } @@ -347,7 +347,7 @@ func (p *PackageLayout) VerifyPackageSignature(ctx context.Context, opts utils.V // Validate that we have required verification material // Note: this will later be replaced when verification enhancements are made - if opts.KeyRef == "" { + if opts.Key == "" { return errors.New("package is signed but no verification material was provided (Public Key, etc.)") } @@ -375,9 +375,9 @@ func (p *PackageLayout) VerifyPackageSignature(ctx context.Context, opts utils.V // Legacy signature found l.Warn("bundle format signature not found: legacy signature is being deprecated. consider resigning this zarf package with the --features='bundle-signature=true' flag.") - opts.SigRef = signaturePath + opts.Signature = signaturePath - opts.NewBundleFormat = false + opts.CommonVerifyOptions.NewBundleFormat = false ZarfYAMLPath := filepath.Join(p.dirPath, ZarfYAML) return utils.CosignVerifyBlobWithOptions(ctx, ZarfYAMLPath, opts) } diff --git a/src/pkg/packager/layout/package_test.go b/src/pkg/packager/layout/package_test.go index f447485dc3..f5f4405dd3 100644 --- a/src/pkg/packager/layout/package_test.go +++ b/src/pkg/packager/layout/package_test.go @@ -22,7 +22,7 @@ import ( func verifyOptsFromKey(keyPath string) *utils.VerifyBlobOptions { opts := utils.DefaultVerifyBlobOptions() - opts.KeyRef = keyPath + opts.Key = keyPath return &opts } @@ -242,12 +242,9 @@ func TestPackageLayoutSignPackage(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) opts := utils.DefaultSignBlobOptions() - opts.KeyRef = "./testdata/cosign.key" - opts.PassFunc = passFunc + opts.Key = "./testdata/cosign.key" + opts.Password = "test" err = pkgLayout.SignPackage(ctx, opts) require.NoError(t, err) @@ -270,12 +267,9 @@ func TestPackageLayoutSignPackage(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("wrongpassword"), nil - }) opts := utils.DefaultSignBlobOptions() - opts.KeyRef = "./testdata/cosign.key" - opts.PassFunc = passFunc + opts.Key = "./testdata/cosign.key" + opts.Password = "wrongpassword" err = pkgLayout.SignPackage(ctx, opts) require.ErrorContains(t, err, "failed to sign package") @@ -291,12 +285,9 @@ func TestPackageLayoutSignPackage(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) opts := utils.DefaultSignBlobOptions() - opts.KeyRef = "./testdata/cosign.key" - opts.PassFunc = passFunc + opts.Key = "./testdata/cosign.key" + opts.Password = "test" err := pkgLayout.SignPackage(ctx, opts) require.Error(t, err) @@ -309,12 +300,9 @@ func TestPackageLayoutSignPackage(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) opts := utils.DefaultSignBlobOptions() - opts.KeyRef = "./testdata/cosign.key" - opts.PassFunc = passFunc + opts.Key = "./testdata/cosign.key" + opts.Password = "test" err := pkgLayout.SignPackage(ctx, opts) require.Error(t, err) @@ -327,12 +315,9 @@ func TestPackageLayoutSignPackage(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) opts := utils.DefaultSignBlobOptions() - opts.KeyRef = "./testdata/cosign.key" - opts.PassFunc = passFunc + opts.Key = "./testdata/cosign.key" + opts.Password = "test" err := pkgLayout.SignPackage(ctx, opts) require.EqualError(t, err, "invalid package layout: dirPath is empty") @@ -355,12 +340,9 @@ func TestPackageLayoutSignPackage(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) opts := utils.DefaultSignBlobOptions() - opts.KeyRef = "./testdata/cosign.key" - opts.PassFunc = passFunc + opts.Key = "./testdata/cosign.key" + opts.Password = "test" opts.Overwrite = true // Should overwrite the existing signature (with warning logged) @@ -411,12 +393,9 @@ func TestPackageLayoutSignPackage(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) opts := utils.DefaultSignBlobOptions() - opts.KeyRef = "./testdata/cosign.key" - opts.PassFunc = passFunc + opts.Key = "./testdata/cosign.key" + opts.Password = "test" err = pkgLayout.SignPackage(ctx, opts) require.Error(t, err) @@ -435,12 +414,9 @@ func TestPackageLayoutSignPackage(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) opts := utils.DefaultSignBlobOptions() - opts.KeyRef = "./testdata/cosign.key" - opts.PassFunc = passFunc + opts.Key = "./testdata/cosign.key" + opts.Password = "test" opts.OutputSignature = "/some/custom/path.sig" // Store original value @@ -467,12 +443,9 @@ func TestPackageLayoutSignPackage(t *testing.T) { } // Wrong password should cause signing to fail - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("wrongpassword"), nil - }) opts := utils.DefaultSignBlobOptions() - opts.KeyRef = "./testdata/cosign.key" - opts.PassFunc = passFunc + opts.Key = "./testdata/cosign.key" + opts.Password = "wrongpassword" err = pkgLayout.SignPackage(ctx, opts) require.Error(t, err) @@ -537,12 +510,9 @@ func TestPackageLayoutSignPackage(t *testing.T) { require.NoError(t, err) // Sign the package - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) opts := utils.DefaultSignBlobOptions() - opts.KeyRef = "./testdata/cosign.key" - opts.PassFunc = passFunc + opts.Key = "./testdata/cosign.key" + opts.Password = "test" err = pkgLayout.SignPackage(ctx, opts) require.NoError(t, err) @@ -599,12 +569,9 @@ func TestPackageLayoutSignPackageValidation(t *testing.T) { }, } - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) opts := utils.DefaultSignBlobOptions() - opts.KeyRef = "./testdata/cosign.key" - opts.PassFunc = passFunc + opts.Key = "./testdata/cosign.key" + opts.Password = "test" return layout, opts }, @@ -629,12 +596,9 @@ func TestPackageLayoutSignPackageValidation(t *testing.T) { }, } - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) opts := utils.DefaultSignBlobOptions() - opts.KeyRef = "./testdata/cosign.key" - opts.PassFunc = passFunc + opts.Key = "./testdata/cosign.key" + opts.Password = "test" return layout, opts }, @@ -654,12 +618,9 @@ func TestPackageLayoutSignPackageValidation(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) opts := utils.DefaultSignBlobOptions() - opts.KeyRef = "./testdata/cosign.key" - opts.PassFunc = passFunc + opts.Key = "./testdata/cosign.key" + opts.Password = "test" return layout, opts }, @@ -683,7 +644,7 @@ func TestPackageLayoutSignPackageValidation(t *testing.T) { return nil, os.ErrPermission }) opts := utils.DefaultSignBlobOptions() - opts.KeyRef = "./testdata/cosign.key" + opts.Key = "./testdata/cosign.key" opts.PassFunc = passFunc return layout, opts @@ -707,12 +668,9 @@ func TestPackageLayoutSignPackageValidation(t *testing.T) { }, } - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) opts := utils.DefaultSignBlobOptions() - opts.KeyRef = "./testdata/cosign.key" - opts.PassFunc = passFunc + opts.Key = "./testdata/cosign.key" + opts.Password = "test" return layout, opts }, @@ -776,12 +734,9 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { } // Sign the package (legacy only, bundle feature disabled by default) - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) signOpts := utils.DefaultSignBlobOptions() - signOpts.KeyRef = "./testdata/cosign.key" - signOpts.PassFunc = passFunc + signOpts.Key = "./testdata/cosign.key" + signOpts.Password = "test" err = pkgLayout.SignPackage(ctx, signOpts) require.NoError(t, err) @@ -789,7 +744,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { // Verify the signature (should use legacy format) verifyOpts := utils.DefaultVerifyBlobOptions() - verifyOpts.KeyRef = "./testdata/cosign.pub" + verifyOpts.Key = "./testdata/cosign.pub" err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) require.NoError(t, err) @@ -809,19 +764,16 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { } // Sign with the test key - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) signOpts := utils.DefaultSignBlobOptions() - signOpts.KeyRef = "./testdata/cosign.key" - signOpts.PassFunc = passFunc + signOpts.Key = "./testdata/cosign.key" + signOpts.Password = "test" err = pkgLayout.SignPackage(ctx, signOpts) require.NoError(t, err) // Try to verify with a different (non-existent) key - should fail verifyOpts := utils.DefaultVerifyBlobOptions() - verifyOpts.KeyRef = "./testdata/nonexistent.pub" + verifyOpts.Key = "./testdata/nonexistent.pub" err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) require.Error(t, err) @@ -841,7 +793,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { } verifyOpts := utils.DefaultVerifyBlobOptions() - verifyOpts.KeyRef = "./testdata/cosign.pub" + verifyOpts.Key = "./testdata/cosign.pub" err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) require.Error(t, err) @@ -855,7 +807,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { } verifyOpts := utils.DefaultVerifyBlobOptions() - verifyOpts.KeyRef = "./testdata/cosign.pub" + verifyOpts.Key = "./testdata/cosign.pub" err := pkgLayout.VerifyPackageSignature(ctx, verifyOpts) require.EqualError(t, err, "invalid package layout: dirPath is empty") @@ -868,7 +820,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { } verifyOpts := utils.DefaultVerifyBlobOptions() - verifyOpts.KeyRef = "./testdata/cosign.pub" + verifyOpts.Key = "./testdata/cosign.pub" err := pkgLayout.VerifyPackageSignature(ctx, verifyOpts) require.Error(t, err) @@ -887,7 +839,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { } verifyOpts := utils.DefaultVerifyBlobOptions() - verifyOpts.KeyRef = "./testdata/cosign.pub" + verifyOpts.Key = "./testdata/cosign.pub" err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) require.Error(t, err) @@ -908,19 +860,16 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { } // Sign the package - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) signOpts := utils.DefaultSignBlobOptions() - signOpts.KeyRef = "./testdata/cosign.key" - signOpts.PassFunc = passFunc + signOpts.Key = "./testdata/cosign.key" + signOpts.Password = "test" err = pkgLayout.SignPackage(ctx, signOpts) require.NoError(t, err) // Try to verify without providing a key verifyOpts := utils.DefaultVerifyBlobOptions() - verifyOpts.KeyRef = "" // Empty key + verifyOpts.Key = "" // Empty key err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) require.EqualError(t, err, "package is signed but no verification material was provided (Public Key, etc.)") @@ -940,12 +889,9 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { } // Sign the package - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) signOpts := utils.DefaultSignBlobOptions() - signOpts.KeyRef = "./testdata/cosign.key" - signOpts.PassFunc = passFunc + signOpts.Key = "./testdata/cosign.key" + signOpts.Password = "test" err = pkgLayout.SignPackage(ctx, signOpts) require.NoError(t, err) @@ -961,7 +907,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { // Try to verify with corrupted signature(s) verifyOpts := utils.DefaultVerifyBlobOptions() - verifyOpts.KeyRef = "./testdata/cosign.pub" + verifyOpts.Key = "./testdata/cosign.pub" err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) require.Error(t, err) @@ -981,12 +927,9 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { } // Sign the package - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) signOpts := utils.DefaultSignBlobOptions() - signOpts.KeyRef = "./testdata/cosign.key" - signOpts.PassFunc = passFunc + signOpts.Key = "./testdata/cosign.key" + signOpts.Password = "test" err = pkgLayout.SignPackage(ctx, signOpts) require.NoError(t, err) @@ -997,7 +940,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { // Verification should fail because content doesn't match signature verifyOpts := utils.DefaultVerifyBlobOptions() - verifyOpts.KeyRef = "./testdata/cosign.pub" + verifyOpts.Key = "./testdata/cosign.pub" err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) require.Error(t, err) @@ -1019,12 +962,9 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { } // Sign the package - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) signOpts := utils.DefaultSignBlobOptions() - signOpts.KeyRef = "./testdata/cosign.key" - signOpts.PassFunc = passFunc + signOpts.Key = "./testdata/cosign.key" + signOpts.Password = "test" err = pkgLayout.SignPackage(ctx, signOpts) require.NoError(t, err) @@ -1036,7 +976,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { // Verification should work with legacy signature (fallback path) verifyOpts := utils.DefaultVerifyBlobOptions() - verifyOpts.KeyRef = "./testdata/cosign.pub" + verifyOpts.Key = "./testdata/cosign.pub" err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) require.NoError(t, err, "verification should succeed with legacy signature format") @@ -1069,12 +1009,9 @@ func TestSignPackageBundleSignatureEnabled(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) opts := utils.DefaultSignBlobOptions() - opts.KeyRef = "./testdata/cosign.key" - opts.PassFunc = passFunc + opts.Key = "./testdata/cosign.key" + opts.Password = "test" err = pkgLayout.SignPackage(ctx, opts) require.NoError(t, err) @@ -1109,12 +1046,9 @@ func TestSignPackageBundleSignatureEnabled(t *testing.T) { err = os.WriteFile(yamlPath, b, 0o644) require.NoError(t, err) - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) opts := utils.DefaultSignBlobOptions() - opts.KeyRef = "./testdata/cosign.key" - opts.PassFunc = passFunc + opts.Key = "./testdata/cosign.key" + opts.Password = "test" err = pkgLayout.SignPackage(ctx, opts) require.NoError(t, err) @@ -1144,19 +1078,16 @@ func TestSignPackageBundleSignatureEnabled(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) signOpts := utils.DefaultSignBlobOptions() - signOpts.KeyRef = "./testdata/cosign.key" - signOpts.PassFunc = passFunc + signOpts.Key = "./testdata/cosign.key" + signOpts.Password = "test" err = pkgLayout.SignPackage(ctx, signOpts) require.NoError(t, err) require.FileExists(t, bundlePath) verifyOpts := utils.DefaultVerifyBlobOptions() - verifyOpts.KeyRef = "./testdata/cosign.pub" + verifyOpts.Key = "./testdata/cosign.pub" err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) require.NoError(t, err) @@ -1176,12 +1107,9 @@ func TestSignPackageBundleSignatureEnabled(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) signOpts := utils.DefaultSignBlobOptions() - signOpts.KeyRef = "./testdata/cosign.key" - signOpts.PassFunc = passFunc + signOpts.Key = "./testdata/cosign.key" + signOpts.Password = "test" err = pkgLayout.SignPackage(ctx, signOpts) require.NoError(t, err) @@ -1193,7 +1121,7 @@ func TestSignPackageBundleSignatureEnabled(t *testing.T) { require.NoError(t, err) verifyOpts := utils.DefaultVerifyBlobOptions() - verifyOpts.KeyRef = "./testdata/cosign.pub" + verifyOpts.Key = "./testdata/cosign.pub" err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) require.NoError(t, err, "should fall back to legacy signature") @@ -1383,12 +1311,9 @@ func TestLoadFromDir_VerificationStrategies(t *testing.T) { Pkg: pkg, } - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) signOpts := utils.DefaultSignBlobOptions() - signOpts.KeyRef = "./testdata/cosign.key" - signOpts.PassFunc = passFunc + signOpts.Key = "./testdata/cosign.key" + signOpts.Password = "test" err = pkgLayout.SignPackage(ctx, signOpts) require.NoError(t, err) @@ -1742,12 +1667,9 @@ func TestSignPackage_PopulatesProvenanceFiles(t *testing.T) { }, } - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("test"), nil - }) opts := utils.DefaultSignBlobOptions() - opts.KeyRef = "./testdata/cosign.key" - opts.PassFunc = passFunc + opts.Key = "./testdata/cosign.key" + opts.Password = "test" err = pkgLayout.SignPackage(ctx, opts) require.NoError(t, err) @@ -1773,12 +1695,9 @@ func TestSignPackage_PopulatesProvenanceFiles(t *testing.T) { }, } - passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { - return []byte("wrongpassword"), nil - }) opts := utils.DefaultSignBlobOptions() - opts.KeyRef = "./testdata/cosign.key" - opts.PassFunc = passFunc + opts.Key = "./testdata/cosign.key" + opts.Password = "wrongpassword" err = pkgLayout.SignPackage(ctx, opts) require.Error(t, err) diff --git a/src/pkg/packager/load.go b/src/pkg/packager/load.go index b67deaa2ed..97b1293332 100644 --- a/src/pkg/packager/load.go +++ b/src/pkg/packager/load.go @@ -64,7 +64,7 @@ func LoadPackage(ctx context.Context, source string, opts LoadOptions) (_ *layou // ensuring the new API takes precedence over the deprecated field. if opts.VerifyBlobOptions == nil && opts.PublicKeyPath != "" { defaults := utils.DefaultVerifyBlobOptions() - defaults.KeyRef = opts.PublicKeyPath + defaults.Key = opts.PublicKeyPath opts.VerifyBlobOptions = &defaults } diff --git a/src/pkg/packager/load_test.go b/src/pkg/packager/load_test.go index e6e239fe1d..409980bbb7 100644 --- a/src/pkg/packager/load_test.go +++ b/src/pkg/packager/load_test.go @@ -70,7 +70,7 @@ func TestLoadPackage(t *testing.T) { keyPath := filepath.Join("layout", "testdata", "cosign.pub") verifyOpts := utils.DefaultVerifyBlobOptions() - verifyOpts.KeyRef = keyPath + verifyOpts.Key = keyPath // VerifyNever should skip verification entirely and succeed opt := LoadOptions{ diff --git a/src/pkg/packager/publish.go b/src/pkg/packager/publish.go index d2657158e4..c6369c8495 100644 --- a/src/pkg/packager/publish.go +++ b/src/pkg/packager/publish.go @@ -143,7 +143,7 @@ func PublishPackage(ctx context.Context, pkgLayout *layout.PackageLayout, dst re // Sign the package with the provided options signOpts := utils.DefaultSignBlobOptions() - signOpts.KeyRef = opts.SigningKeyPath + signOpts.Key = opts.SigningKeyPath signOpts.Password = opts.SigningKeyPassword // Publish never re-writes the tarball content - overwrite explicitly signOpts.Overwrite = true diff --git a/src/pkg/packager/publish_test.go b/src/pkg/packager/publish_test.go index 9ccc5bb395..c79927ae3d 100644 --- a/src/pkg/packager/publish_test.go +++ b/src/pkg/packager/publish_test.go @@ -37,7 +37,7 @@ func pullFromRemote(ctx context.Context, t *testing.T, packageRef string, archit t.Helper() verifyOpts := utils.DefaultVerifyBlobOptions() - verifyOpts.KeyRef = publicKeyPath + verifyOpts.Key = publicKeyPath // Generate tmpdir and pull published package from local registry pullOCIOpts := pullOCIOptions{ diff --git a/src/pkg/packager/pull.go b/src/pkg/packager/pull.go index a8b2867c00..08725280f1 100644 --- a/src/pkg/packager/pull.go +++ b/src/pkg/packager/pull.go @@ -81,7 +81,7 @@ func Pull(ctx context.Context, source, destination string, opts PullOptions) (_ // ensuring the new API takes precedence over the deprecated field. if opts.VerifyBlobOptions == nil && opts.PublicKeyPath != "" { defaults := utils.DefaultVerifyBlobOptions() - defaults.KeyRef = opts.PublicKeyPath + defaults.Key = opts.PublicKeyPath opts.VerifyBlobOptions = &defaults } diff --git a/src/pkg/utils/cosign.go b/src/pkg/utils/cosign.go index 82d843367a..9a2b0a41e4 100644 --- a/src/pkg/utils/cosign.go +++ b/src/pkg/utils/cosign.go @@ -11,6 +11,7 @@ import ( "time" "github.com/google/go-containerregistry/pkg/name" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/generate" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/sign" "github.com/sigstore/cosign/v3/cmd/cosign/cli/verify" @@ -26,62 +27,33 @@ import ( "github.com/zarf-dev/zarf/src/pkg/logger" ) -// Default cosign configuration -const ( - CosignDefaultTimeout = 3 * time.Minute -) +// CosignDefaultTimeout is the default timeout for cosign sign and verify operations. +const CosignDefaultTimeout = 3 * time.Minute -// SignBlobOptions embeds Cosign's native options and adds Zarf-specific configuration. -// By embedding options.KeyOpts, we get direct access to all Cosign signing capabilities -// while maintaining a clean interface for Zarf users. +// SignBlobOptions wraps cosign's SignBlobOptions with zarf-specific fields. type SignBlobOptions struct { - // Embed Cosign's KeyOpts for signing configuration - options.KeyOpts - - // Certificate signing support - X.509 certificate - Cert string - CertChain string - - // Zarf-specific options for output control - OutputSignature string // Custom path for signature file - OutputCertificate string // Where to write certificate (keyless mode) + options.SignBlobOptions - // General options - Verbose bool // Enable debug output - Timeout time.Duration // Timeout for signing operations - - // Password provides password for encrypted keys without requiring cosign.PassFunc import - Password string - // Overwrite allows for opting-into the overwrite of an existing signature + Verbose bool + Timeout time.Duration + Password string + PassFunc cosign.PassFunc Overwrite bool } -// VerifyBlobOptions embeds Cosign's native options for verification. -// By embedding options.KeyOpts and options.CertVerifyOptions, we get direct access -// to all Cosign verification capabilities. +// VerifyBlobOptions wraps cosign's VerifyBlobOptions with zarf-specific fields. type VerifyBlobOptions struct { - // Embed Cosign's KeyOpts for key-based verification - options.KeyOpts - - // Embed Cosign's CertVerifyOptions for certificate-based (keyless) verification - options.CertVerifyOptions - - // Verification-specific options - SigRef string // Path to signature file - Offline bool // Enable offline verification mode - IgnoreTlog bool // Skip transparency log verification + options.VerifyBlobOptions - // General options - Timeout time.Duration // Timeout for verification operations + Timeout time.Duration } -// ShouldSign returns true if the options indicate that signing should be performed. -// This checks if any signing key material is configured (KeyRef, IDToken, or Sk). +// ShouldSign returns true if any signing key material is configured. func (opts SignBlobOptions) ShouldSign() bool { - return opts.KeyRef != "" || opts.IDToken != "" || opts.Sk + return opts.Key != "" || opts.Fulcio.IdentityToken != "" || opts.SecurityKey.Use } -// CheckOverwrite validates that output files can be written, returning an error if they exist and Overwrite is false. +// CheckOverwrite errors if any output file exists and Overwrite is false. func (opts SignBlobOptions) CheckOverwrite(ctx context.Context) error { for _, path := range []string{opts.BundlePath, opts.OutputCertificate, opts.OutputSignature} { if path == "" { @@ -97,62 +69,86 @@ func (opts SignBlobOptions) CheckOverwrite(ctx context.Context) error { return nil } -// DefaultSignBlobOptions returns SignBlobOptions with Zarf defaults. -// Configures sensible defaults for offline/air-gapped environments. +// DefaultSignBlobOptions returns SignBlobOptions seeded with zarf defaults. +// Divergence: TlogUpload defaults to false (cosign default true) for air-gap. func DefaultSignBlobOptions() SignBlobOptions { - return SignBlobOptions{ - KeyOpts: options.KeyOpts{ - Slot: "signature", - OIDCIssuer: "", // https://oauth2.sigstore.dev/auth - OIDCClientID: "sigstore", - OIDCRedirectURL: "", // http://localhost:0/auth/callback - FulcioAuthFlow: "normal", - FulcioURL: "", // https://fulcio.sigstore.dev - RekorURL: "", // https://rekor.sigstore.dev - NewBundleFormat: true, - SkipConfirmation: false, - }, - Timeout: CosignDefaultTimeout, - Verbose: false, - } + var opts SignBlobOptions + opts.TlogUpload = false + opts.Base64Output = true + opts.NewBundleFormat = true + opts.SecurityKey.Slot = "signature" + opts.OIDC.ClientID = "sigstore" + opts.Fulcio.AuthFlow = "normal" + opts.Timeout = CosignDefaultTimeout + return opts } -// DefaultVerifyBlobOptions returns VerifyBlobOptions with Zarf defaults. -// Configures sensible defaults for offline/air-gapped environments. +// DefaultVerifyBlobOptions returns VerifyBlobOptions seeded with zarf defaults. +// Divergences: IgnoreTlog and IgnoreSCT default to true (cosign default false) for air-gap. func DefaultVerifyBlobOptions() VerifyBlobOptions { - return VerifyBlobOptions{ - KeyOpts: options.KeyOpts{ - NewBundleFormat: true, - }, - CertVerifyOptions: options.CertVerifyOptions{ - IgnoreSCT: true, // Skip SCT verification by default - }, - Offline: true, - IgnoreTlog: true, - Timeout: CosignDefaultTimeout, - } + var opts VerifyBlobOptions + opts.CommonVerifyOptions.IgnoreTlog = true + opts.CertVerify.IgnoreSCT = true + opts.CommonVerifyOptions.NewBundleFormat = true + opts.Timeout = CosignDefaultTimeout + return opts } -// CosignSignBlobWithOptions signs a blob with comprehensive cosign options. -// This function supports all cosign v3 sign-blob capabilities by leveraging -// the embedded KeyOpts structure. +// CosignSignBlobWithOptions signs a blob via cosign's SignBlobCmd. +// Mirrors cmd/cosign/cli/signblob.go (v3.0.6) SignBlob().RunE. +// +// Stage-2 limitation: signcommon.LoadTrustedMaterialAndSigningConfig is not +// invoked here — it would emit deprecation warnings against zarf's internal +// --output-signature tempfile. As a result, --signing-config, --use-signing-config, +// and --trusted-root on sign are bound but inert; they're hidden in package sign. +// Stage 3 wires this alongside trusted-root embedding. func CosignSignBlobWithOptions(ctx context.Context, blobPath string, opts SignBlobOptions) ([]byte, error) { l := logger.From(ctx) - // Build root options rootOpts := &options.RootOptions{ Verbose: opts.Verbose, Timeout: opts.Timeout, } - // Use the embedded KeyOpts directly - keyOpts := opts.KeyOpts + oidcClientSecret, err := opts.OIDC.ClientSecret() + if err != nil { + return nil, err + } + + ko := options.KeyOpts{ + KeyRef: opts.Key, + PassFunc: generate.GetPass, + Sk: opts.SecurityKey.Use, + Slot: opts.SecurityKey.Slot, + FulcioURL: opts.Fulcio.URL, + IDToken: opts.Fulcio.IdentityToken, + FulcioAuthFlow: opts.Fulcio.AuthFlow, + InsecureSkipFulcioVerify: opts.Fulcio.InsecureSkipFulcioVerify, + RekorURL: opts.Rekor.URL, + OIDCIssuer: opts.OIDC.Issuer, + OIDCClientID: opts.OIDC.ClientID, + OIDCClientSecret: oidcClientSecret, + OIDCRedirectURL: opts.OIDC.RedirectURL, + OIDCDisableProviders: opts.OIDC.DisableAmbientProviders, + BundlePath: opts.BundlePath, + NewBundleFormat: opts.NewBundleFormat, + SkipConfirmation: opts.SkipConfirmation, + TSAClientCACert: opts.TSAClientCACert, + TSAClientCert: opts.TSAClientCert, + TSAClientKey: opts.TSAClientKey, + TSAServerName: opts.TSAServerName, + TSAServerURL: opts.TSAServerURL, + RFC3161TimestampPath: opts.RFC3161TimestampPath, + IssueCertificateForExistingKey: opts.IssueCertificate, + SigningAlgorithm: opts.SigningAlgorithm, + } - // If Password field is set and PassFunc is not, create PassFunc from Password - // This allows users to avoid importing cosign.PassFunc directly - if opts.Password != "" && keyOpts.PassFunc == nil { - password := opts.Password // Capture for closure - keyOpts.PassFunc = cosign.PassFunc(func(_ bool) ([]byte, error) { + switch { + case opts.PassFunc != nil: + ko.PassFunc = opts.PassFunc + case opts.Password != "": + password := opts.Password + ko.PassFunc = cosign.PassFunc(func(_ bool) ([]byte, error) { return []byte(password), nil }) } @@ -162,26 +158,21 @@ func CosignSignBlobWithOptions(ctx context.Context, blobPath string, opts SignBl } l.Debug("signing blob with cosign", - "keyRef", opts.KeyRef, - "sk", opts.Sk, + "key", opts.Key, + "sk", opts.SecurityKey.Use, "bundlePath", opts.BundlePath) - // SignBlobCmd signature: (ro *RootOptions, ko KeyOpts, payloadPath string, b64 bool, outputSignature string, outputCertificate string, tlogUpload bool) - // Note: Some params like b64 and tlogUpload are not in KeyOpts, so we need to handle defaults - b64 := true // Default: base64 encode signature - tlogUpload := false // Zarf default: don't upload to transparency log (offline/air-gap friendly) - sig, err := sign.SignBlobCmd( ctx, rootOpts, - keyOpts, + ko, blobPath, opts.Cert, opts.CertChain, - b64, + opts.Base64Output, opts.OutputSignature, opts.OutputCertificate, - tlogUpload, + opts.TlogUpload, ) if err != nil { return nil, err @@ -191,32 +182,60 @@ func CosignSignBlobWithOptions(ctx context.Context, blobPath string, opts SignBl return sig, nil } -// CosignVerifyBlobWithOptions verifies a blob signature with comprehensive cosign options. -// This function supports all cosign v3 verify-blob capabilities by leveraging -// the embedded KeyOpts and CertVerifyOptions structures. +// CosignVerifyBlobWithOptions verifies a blob via cosign's VerifyBlobCmd. +// Mirrors cmd/cosign/cli/verify.go (v3.0.6) VerifyBlob().RunE. func CosignVerifyBlobWithOptions(ctx context.Context, blobPath string, opts VerifyBlobOptions) error { l := logger.From(ctx) - // Use the embedded structs directly - no need to copy fields! - keyOpts := opts.KeyOpts - certVerifyOpts := opts.CertVerifyOptions + hashAlgorithm, err := opts.SignatureDigest.HashAlgorithm() + if err != nil { + return err + } + + if opts.CommonVerifyOptions.PrivateInfrastructure { + opts.CommonVerifyOptions.IgnoreTlog = true + } + + ko := options.KeyOpts{ + KeyRef: opts.Key, + Sk: opts.SecurityKey.Use, + Slot: opts.SecurityKey.Slot, + RekorURL: opts.Rekor.URL, + BundlePath: opts.BundlePath, + RFC3161TimestampPath: opts.RFC3161TimestampPath, + TSACertChainPath: opts.CommonVerifyOptions.TSACertChainPath, + NewBundleFormat: opts.CommonVerifyOptions.NewBundleFormat, + } cmd := &verify.VerifyBlobCmd{ - KeyOpts: keyOpts, - CertVerifyOptions: certVerifyOpts, - SigRef: opts.SigRef, - IgnoreSCT: opts.IgnoreSCT, // From CertVerifyOptions - Offline: opts.Offline, - IgnoreTlog: opts.IgnoreTlog, + KeyOpts: ko, + CertVerifyOptions: opts.CertVerify, + CertRef: opts.CertVerify.Cert, + CertChain: opts.CertVerify.CertChain, + CARoots: opts.CertVerify.CARoots, + CAIntermediates: opts.CertVerify.CAIntermediates, + SigRef: opts.Signature, + CertGithubWorkflowTrigger: opts.CertVerify.CertGithubWorkflowTrigger, + CertGithubWorkflowSHA: opts.CertVerify.CertGithubWorkflowSha, + CertGithubWorkflowName: opts.CertVerify.CertGithubWorkflowName, + CertGithubWorkflowRepository: opts.CertVerify.CertGithubWorkflowRepository, + CertGithubWorkflowRef: opts.CertVerify.CertGithubWorkflowRef, + IgnoreSCT: opts.CertVerify.IgnoreSCT, + SCTRef: opts.CertVerify.SCT, + Offline: opts.CommonVerifyOptions.Offline, + IgnoreTlog: opts.CommonVerifyOptions.IgnoreTlog, + UseSignedTimestamps: opts.CommonVerifyOptions.UseSignedTimestamps, + TrustedRootPath: opts.CommonVerifyOptions.TrustedRootPath, + HashAlgorithm: hashAlgorithm, } l.Debug("verifying blob with cosign", - "keyRef", opts.KeyRef, - "sigRef", opts.SigRef, - "offline", opts.Offline) + "key", opts.Key, + "signature", opts.Signature, + "bundlePath", opts.BundlePath, + "offline", opts.CommonVerifyOptions.Offline) - err := cmd.Exec(ctx, blobPath) - if err != nil { + if err := cmd.Exec(ctx, blobPath); err != nil { return err } @@ -224,7 +243,7 @@ func CosignVerifyBlobWithOptions(ctx context.Context, blobPath string, opts Veri return nil } -// GetCosignArtifacts returns signatures and attestations for the given image +// GetCosignArtifacts returns signatures and attestations for the given image. func GetCosignArtifacts(image string) ([]string, error) { var nameOpts []name.Option @@ -233,14 +252,12 @@ func GetCosignArtifacts(image string) ([]string, error) { return nil, err } - // Return empty if we don't have a signature on the image var remoteOpts []ociremote.Option simg, _ := ociremote.SignedEntity(ref, remoteOpts...) //nolint:errcheck if simg == nil { return nil, nil } - // Errors are dogsled because these functions always return a name.Tag which we can check for layers sigRef, _ := ociremote.SignatureTag(ref, remoteOpts...) //nolint:errcheck attRef, _ := ociremote.AttestationTag(ref, remoteOpts...) //nolint:errcheck diff --git a/src/test/e2e/12_package_signing_test.go b/src/test/e2e/12_package_signing_test.go index 11d5eed835..5320e0c8c0 100644 --- a/src/test/e2e/12_package_signing_test.go +++ b/src/test/e2e/12_package_signing_test.go @@ -86,4 +86,58 @@ func TestPackageSigning(t *testing.T) { require.Error(t, err) require.Contains(t, stdErr, "a key was provided but the package is not signed") }) + + // Confirms cosign-aligned flag surface is bound on verify. Round-trip keyless verify + // against real Sigstore fixtures lands in Stage 3 alongside trusted-root embedding. + t.Run("Cosign-aligned verify flags are accepted", func(t *testing.T) { + stdOut, stdErr, err := e2e.Zarf(t, "package", "verify", "--help") + require.NoError(t, err, stdOut, stdErr) + for _, flag := range []string{ + "--certificate-identity", + "--certificate-oidc-issuer", + "--certificate-identity-regexp", + "--certificate-oidc-issuer-regexp", + "--certificate-github-workflow-trigger", + "--trusted-root", + "--insecure-ignore-tlog", + "--insecure-ignore-sct", + "--rekor-url", + } { + require.Contains(t, stdOut, flag, "expected %q in `package verify --help`", flag) + } + // Air-gap-safe defaults must remain enabled. + require.Contains(t, stdOut, "--insecure-ignore-tlog ignore transparency log verification, to be used when an artifact signature has not been uploaded to the transparency log. Artifacts cannot be publicly verified when not included in a log (default true)") + require.Contains(t, stdOut, "--insecure-ignore-sct when set, verification will not check that a certificate contains an embedded SCT, a proof of inclusion in a certificate transparency log (default true)") + }) + + t.Run("Cosign-aligned sign flags are accepted", func(t *testing.T) { + stdOut, stdErr, err := e2e.Zarf(t, "package", "sign", "--help") + require.NoError(t, err, stdOut, stdErr) + for _, flag := range []string{ + "--fulcio-url", + "--rekor-url", + "--oidc-issuer", + "--identity-token", + "--certificate", + "--certificate-chain", + "--sk", + "--slot", + } { + require.Contains(t, stdOut, flag, "expected %q in `package sign --help`", flag) + } + }) + + t.Run("Hidden flags do not appear in help", func(t *testing.T) { + stdOut, stdErr, err := e2e.Zarf(t, "package", "verify", "--help") + require.NoError(t, err, stdOut, stdErr) + for _, hidden := range []string{"--bundle", "--signature", "--rfc3161-timestamp"} { + require.NotContains(t, stdOut, hidden+" string", "expected %q to be hidden in `package verify --help`", hidden) + } + + stdOut, stdErr, err = e2e.Zarf(t, "package", "sign", "--help") + require.NoError(t, err, stdOut, stdErr) + for _, hidden := range []string{"--bundle ", "--output-signature", "--output-certificate", "--issue-certificate", "--signing-config", "--use-signing-config"} { + require.NotContains(t, stdOut, hidden, "expected %q to be hidden in `package sign --help`", hidden) + } + }) } From 972fa3163d4566ee767ac1d251bd413c282bb019 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Thu, 7 May 2026 22:59:45 +0000 Subject: [PATCH 02/25] chore(docs): update comments and language Signed-off-by: Brandt Keller --- src/pkg/utils/cosign.go | 10 ++-------- src/test/e2e/12_package_signing_test.go | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/pkg/utils/cosign.go b/src/pkg/utils/cosign.go index 9a2b0a41e4..45a4d63b41 100644 --- a/src/pkg/utils/cosign.go +++ b/src/pkg/utils/cosign.go @@ -70,7 +70,7 @@ func (opts SignBlobOptions) CheckOverwrite(ctx context.Context) error { } // DefaultSignBlobOptions returns SignBlobOptions seeded with zarf defaults. -// Divergence: TlogUpload defaults to false (cosign default true) for air-gap. +// Divergence: TlogUpload defaults to false (cosign default true) for airgap. func DefaultSignBlobOptions() SignBlobOptions { var opts SignBlobOptions opts.TlogUpload = false @@ -84,7 +84,7 @@ func DefaultSignBlobOptions() SignBlobOptions { } // DefaultVerifyBlobOptions returns VerifyBlobOptions seeded with zarf defaults. -// Divergences: IgnoreTlog and IgnoreSCT default to true (cosign default false) for air-gap. +// Divergences: IgnoreTlog and IgnoreSCT default to true (cosign default false) for airgap. func DefaultVerifyBlobOptions() VerifyBlobOptions { var opts VerifyBlobOptions opts.CommonVerifyOptions.IgnoreTlog = true @@ -96,12 +96,6 @@ func DefaultVerifyBlobOptions() VerifyBlobOptions { // CosignSignBlobWithOptions signs a blob via cosign's SignBlobCmd. // Mirrors cmd/cosign/cli/signblob.go (v3.0.6) SignBlob().RunE. -// -// Stage-2 limitation: signcommon.LoadTrustedMaterialAndSigningConfig is not -// invoked here — it would emit deprecation warnings against zarf's internal -// --output-signature tempfile. As a result, --signing-config, --use-signing-config, -// and --trusted-root on sign are bound but inert; they're hidden in package sign. -// Stage 3 wires this alongside trusted-root embedding. func CosignSignBlobWithOptions(ctx context.Context, blobPath string, opts SignBlobOptions) ([]byte, error) { l := logger.From(ctx) diff --git a/src/test/e2e/12_package_signing_test.go b/src/test/e2e/12_package_signing_test.go index 5320e0c8c0..9065b0ebda 100644 --- a/src/test/e2e/12_package_signing_test.go +++ b/src/test/e2e/12_package_signing_test.go @@ -105,7 +105,7 @@ func TestPackageSigning(t *testing.T) { } { require.Contains(t, stdOut, flag, "expected %q in `package verify --help`", flag) } - // Air-gap-safe defaults must remain enabled. + // airgap-safe defaults must remain enabled. require.Contains(t, stdOut, "--insecure-ignore-tlog ignore transparency log verification, to be used when an artifact signature has not been uploaded to the transparency log. Artifacts cannot be publicly verified when not included in a log (default true)") require.Contains(t, stdOut, "--insecure-ignore-sct when set, verification will not check that a certificate contains an embedded SCT, a proof of inclusion in a certificate transparency log (default true)") }) From 762484e67e8507fdae8a3de6f82d2df2e6fcb430 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Fri, 8 May 2026 17:14:13 +0000 Subject: [PATCH 03/25] fix(cosign): implement non-prompting passfunc Signed-off-by: Brandt Keller --- src/pkg/utils/cosign.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/pkg/utils/cosign.go b/src/pkg/utils/cosign.go index 45a4d63b41..3f42500100 100644 --- a/src/pkg/utils/cosign.go +++ b/src/pkg/utils/cosign.go @@ -11,7 +11,6 @@ import ( "time" "github.com/google/go-containerregistry/pkg/name" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/generate" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/sign" "github.com/sigstore/cosign/v3/cmd/cosign/cli/verify" @@ -30,6 +29,15 @@ import ( // CosignDefaultTimeout is the default timeout for cosign sign and verify operations. const CosignDefaultTimeout = 3 * time.Minute +// nonPromptingPassFunc resolves a private-key password without ever blocking on +// terminal or stdin input. +var nonPromptingPassFunc = cosign.PassFunc(func(_ bool) ([]byte, error) { + if pw, ok := os.LookupEnv("COSIGN_PASSWORD"); ok { + return []byte(pw), nil + } + return []byte{}, nil +}) + // SignBlobOptions wraps cosign's SignBlobOptions with zarf-specific fields. type SignBlobOptions struct { options.SignBlobOptions @@ -111,7 +119,7 @@ func CosignSignBlobWithOptions(ctx context.Context, blobPath string, opts SignBl ko := options.KeyOpts{ KeyRef: opts.Key, - PassFunc: generate.GetPass, + PassFunc: nonPromptingPassFunc, Sk: opts.SecurityKey.Use, Slot: opts.SecurityKey.Slot, FulcioURL: opts.Fulcio.URL, From 30bbdbdfaaba653025cd8d894f482bda737e505c Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Fri, 8 May 2026 21:12:32 +0000 Subject: [PATCH 04/25] feat(sign): support for embedded TrustedRoot Signed-off-by: Brandt Keller --- RELEASES.md | 14 +++ hack/refresh-trusted-root.sh | 23 +++++ src/pkg/utils/cosign.go | 18 +++- src/pkg/utils/embedded_trusted_root.json | 126 +++++++++++++++++++++++ src/pkg/utils/trustedroot.go | 41 ++++++++ src/pkg/utils/trustedroot_test.go | 55 ++++++++++ 6 files changed, 275 insertions(+), 2 deletions(-) create mode 100755 hack/refresh-trusted-root.sh create mode 100644 src/pkg/utils/embedded_trusted_root.json create mode 100644 src/pkg/utils/trustedroot.go create mode 100644 src/pkg/utils/trustedroot_test.go diff --git a/RELEASES.md b/RELEASES.md index a8ac0e8985..4c84c7c20f 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -60,6 +60,7 @@ 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: @@ -67,6 +68,19 @@ Once the prerelease artifacts are published, a Homebrew Tap PR is created. It is * [ ] 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): diff --git a/hack/refresh-trusted-root.sh b/hack/refresh-trusted-root.sh new file mode 100755 index 0000000000..77b65846ab --- /dev/null +++ b/hack/refresh-trusted-root.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Refresh the embedded Sigstore TrustedRoot used for keyless verification. +# Run before each release. Commit the result. + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +EMBED_PATH="${REPO_ROOT}/src/pkg/utils/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}" diff --git a/src/pkg/utils/cosign.go b/src/pkg/utils/cosign.go index 3f42500100..1f30de8358 100644 --- a/src/pkg/utils/cosign.go +++ b/src/pkg/utils/cosign.go @@ -6,6 +6,7 @@ package utils import ( "context" + "errors" "fmt" "os" "time" @@ -186,7 +187,7 @@ func CosignSignBlobWithOptions(ctx context.Context, blobPath string, opts SignBl // CosignVerifyBlobWithOptions verifies a blob via cosign's VerifyBlobCmd. // Mirrors cmd/cosign/cli/verify.go (v3.0.6) VerifyBlob().RunE. -func CosignVerifyBlobWithOptions(ctx context.Context, blobPath string, opts VerifyBlobOptions) error { +func CosignVerifyBlobWithOptions(ctx context.Context, blobPath string, opts VerifyBlobOptions) (err error) { l := logger.From(ctx) hashAlgorithm, err := opts.SignatureDigest.HashAlgorithm() @@ -198,6 +199,19 @@ func CosignVerifyBlobWithOptions(ctx context.Context, blobPath string, opts Veri opts.CommonVerifyOptions.IgnoreTlog = true } + // Keyless verify needs a trusted root. If the user didn't supply --trusted-root, + // fall back to the embedded copy. Key-based and cert-based paths skip this + // entirely so they don't pay for an unused tempfile per verify. + trustedRootPath := opts.CommonVerifyOptions.TrustedRootPath + if trustedRootPath == "" && opts.Key == "" && opts.CertVerify.Cert == "" { + path, cleanup, prepErr := writeEmbeddedTrustedRoot() + if prepErr != nil { + return fmt.Errorf("preparing embedded trusted root: %w", prepErr) + } + defer func() { err = errors.Join(err, cleanup()) }() + trustedRootPath = path + } + ko := options.KeyOpts{ KeyRef: opts.Key, Sk: opts.SecurityKey.Use, @@ -227,7 +241,7 @@ func CosignVerifyBlobWithOptions(ctx context.Context, blobPath string, opts Veri Offline: opts.CommonVerifyOptions.Offline, IgnoreTlog: opts.CommonVerifyOptions.IgnoreTlog, UseSignedTimestamps: opts.CommonVerifyOptions.UseSignedTimestamps, - TrustedRootPath: opts.CommonVerifyOptions.TrustedRootPath, + TrustedRootPath: trustedRootPath, HashAlgorithm: hashAlgorithm, } diff --git a/src/pkg/utils/embedded_trusted_root.json b/src/pkg/utils/embedded_trusted_root.json new file mode 100644 index 0000000000..effb0a19e6 --- /dev/null +++ b/src/pkg/utils/embedded_trusted_root.json @@ -0,0 +1,126 @@ +{ + "mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1", + "tlogs": [ + { + "baseUrl": "https://rekor.sigstore.dev", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2021-01-12T11:53:27Z" + } + }, + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + } + }, + { + "baseUrl": "https://log2025-1.rekor.sigstore.dev", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MCowBQYDK2VwAyEAt8rlp1knGwjfbcXAYPYAkn0XiLz1x8O4t0YkEhie244=", + "keyDetails": "PKIX_ED25519", + "validFor": { + "start": "2025-09-23T00:00:00Z" + } + }, + "logId": { + "keyId": "zxGZFVvd0FEmjR8WrFwMdcAJ9vtaY/QXf44Y1wUeP6A=" + } + } + ], + "certificateAuthorities": [ + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore" + }, + "uri": "https://fulcio.sigstore.dev", + "certChain": { + "certificates": [ + { + "rawBytes": "MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==" + } + ] + }, + "validFor": { + "start": "2021-03-07T03:20:29Z", + "end": "2022-12-31T23:59:59.999Z" + } + }, + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore" + }, + "uri": "https://fulcio.sigstore.dev", + "certChain": { + "certificates": [ + { + "rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow=" + }, + { + "rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ" + } + ] + }, + "validFor": { + "start": "2022-04-13T20:06:15Z" + } + } + ], + "ctlogs": [ + { + "baseUrl": "https://ctfe.sigstore.dev/test", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2021-03-14T00:00:00Z", + "end": "2022-10-31T23:59:59.999Z" + } + }, + "logId": { + "keyId": "CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I=" + } + }, + { + "baseUrl": "https://ctfe.sigstore.dev/2022", + "hashAlgorithm": "SHA2_256", + "publicKey": { + "rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==", + "keyDetails": "PKIX_ECDSA_P256_SHA_256", + "validFor": { + "start": "2022-10-20T00:00:00Z" + } + }, + "logId": { + "keyId": "3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4=" + } + } + ], + "timestampAuthorities": [ + { + "subject": { + "organization": "sigstore.dev", + "commonName": "sigstore-tsa-selfsigned" + }, + "uri": "https://timestamp.sigstore.dev/api/v1/timestamp", + "certChain": { + "certificates": [ + { + "rawBytes": "MIICEDCCAZagAwIBAgIUOhNULwyQYe68wUMvy4qOiyojiwwwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4ra2Z8hKNig2T9kFjCAToGG30jky+WQv3BzL+mKvh1SKNR/UwuwsfNCg4sryoYAd8E6isovVA3M4aoNdm9QDi50Z8nTEyvqgfDPtTIwXItfiW/AFf1V7uwkbkAoj0xxco2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFIn9eUOHz9BlRsMCRscsc1t9tOsDMB8GA1UdIwQYMBaAFJjsAe9/u1H/1JUeb4qImFMHic6/MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2gAMGUCMDtpsV/6KaO0qyF/UMsX2aSUXKQFdoGTptQGc0ftq1csulHPGG6dsmyMNd3JB+G3EQIxAOajvBcjpJmKb4Nv+2Taoj8Uc5+b6ih6FXCCKraSqupe07zqswMcXJTe1cExvHvvlw==" + }, + { + "rawBytes": "MIIB9zCCAXygAwIBAgIUV7f0GLDOoEzIh8LXSW80OJiUp14wCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQUQNtfRT/ou3YATa6wB/kKTe70cfJwyRIBovMnt8RcJph/COE82uyS6FmppLLL1VBPGcPfpQPYJNXzWwi8icwhKQ6W/Qe2h3oebBb2FHpwNJDqo+TMaC/tdfkv/ElJB72jRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSY7AHvf7tR/9SVHm+KiJhTB4nOvzAKBggqhkjOPQQDAwNpADBmAjEAwGEGrfGZR1cen1R8/DTVMI943LssZmJRtDp/i7SfGHmGRP6gRbuj9vOK3b67Z0QQAjEAuT2H673LQEaHTcyQSZrkp4mX7WwkmF+sVbkYY5mXN+RMH13KUEHHOqASaemYWK/E" + } + ] + }, + "validFor": { + "start": "2025-07-04T00:00:00Z" + } + } + ] +} diff --git a/src/pkg/utils/trustedroot.go b/src/pkg/utils/trustedroot.go new file mode 100644 index 0000000000..dccdf7c024 --- /dev/null +++ b/src/pkg/utils/trustedroot.go @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package utils + +import ( + _ "embed" + "errors" + "fmt" + "os" +) + +// embeddedTrustedRoot is the Sigstore TrustedRoot JSON shipped with the binary. +// Refresh before each release with hack/refresh-trusted-root.sh. +// +//go:embed embedded_trusted_root.json +var embeddedTrustedRoot []byte + +// writeEmbeddedTrustedRoot stages the embedded TrustedRoot JSON to a tempfile so +// cosign's VerifyBlobCmd (which only accepts file paths) can consume it. +// Caller must invoke cleanup when done; cleanup returns the os.Remove error. +func writeEmbeddedTrustedRoot() (string, func() error, error) { + f, err := os.CreateTemp("", "zarf-trusted-root-*.json") + if err != nil { + return "", func() error { return nil }, fmt.Errorf("creating tempfile: %w", err) + } + cleanup := func() error { return os.Remove(f.Name()) } + + if _, writeErr := f.Write(embeddedTrustedRoot); writeErr != nil { + closeErr := f.Close() + removeErr := cleanup() + return "", func() error { return nil }, + fmt.Errorf("writing embedded trusted root: %w", errors.Join(writeErr, closeErr, removeErr)) + } + if closeErr := f.Close(); closeErr != nil { + removeErr := cleanup() + return "", func() error { return nil }, + fmt.Errorf("closing embedded trusted root tempfile: %w", errors.Join(closeErr, removeErr)) + } + return f.Name(), cleanup, nil +} diff --git a/src/pkg/utils/trustedroot_test.go b/src/pkg/utils/trustedroot_test.go new file mode 100644 index 0000000000..387d6fd760 --- /dev/null +++ b/src/pkg/utils/trustedroot_test.go @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package utils + +import ( + "encoding/json" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestWriteEmbeddedTrustedRoot(t *testing.T) { + t.Parallel() + + t.Run("writes valid JSON to a tempfile", func(t *testing.T) { + t.Parallel() + path, cleanup, err := writeEmbeddedTrustedRoot() + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, cleanup()) }) + require.NotEmpty(t, path) + + contents, err := os.ReadFile(path) + require.NoError(t, err) + require.NotEmpty(t, contents) + + var parsed map[string]any + require.NoError(t, json.Unmarshal(contents, &parsed)) + require.Equal(t, "application/vnd.dev.sigstore.trustedroot+json;version=0.1", parsed["mediaType"], + "embedded trusted root must be a Sigstore TrustedRoot v0.1 document") + }) + + t.Run("cleanup removes the tempfile", func(t *testing.T) { + t.Parallel() + path, cleanup, err := writeEmbeddedTrustedRoot() + require.NoError(t, err) + require.FileExists(t, path) + + require.NoError(t, cleanup()) + _, statErr := os.Stat(path) + require.ErrorIs(t, statErr, os.ErrNotExist) + }) + + t.Run("each call produces a distinct tempfile", func(t *testing.T) { + t.Parallel() + p1, c1, err := writeEmbeddedTrustedRoot() + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, c1()) }) + p2, c2, err := writeEmbeddedTrustedRoot() + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, c2()) }) + require.NotEqual(t, p1, p2) + }) +} From 17dfcd1539dd5e61e94a4610413197afd531d116 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Sat, 9 May 2026 03:54:14 +0000 Subject: [PATCH 05/25] feat(sign): enable keyless signing and verification Signed-off-by: Brandt Keller --- .../docs/commands/zarf_package_sign.md | 4 + src/cmd/package.go | 26 +++- src/config/lang/english.go | 1 + src/pkg/packager/layout/package.go | 43 ++++-- src/pkg/packager/layout/package_test.go | 4 +- src/pkg/utils/cosign.go | 27 +++- src/pkg/utils/keyless.go | 83 ++++++++++++ src/pkg/utils/keyless_test.go | 128 ++++++++++++++++++ src/test/e2e/12_package_signing_test.go | 19 ++- 9 files changed, 312 insertions(+), 23 deletions(-) create mode 100644 src/pkg/utils/keyless.go create mode 100644 src/pkg/utils/keyless_test.go diff --git a/site/src/content/docs/commands/zarf_package_sign.md b/site/src/content/docs/commands/zarf_package_sign.md index c5112bbe73..2702025a3d 100644 --- a/site/src/content/docs/commands/zarf_package_sign.md +++ b/site/src/content/docs/commands/zarf_package_sign.md @@ -50,6 +50,7 @@ $ zarf package sign zarf-package-demo-amd64-1.0.0.tar.zst --signing-key awskms:/ --identity-token string identity token to use for certificate from fulcio. the token or a path to a file containing the token is accepted. --insecure-skip-verify skip verifying fulcio published to the SCT (this should only be used for testing). -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) --new-bundle-format output bundle in new format that contains all verification material (default true) --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 for application (default "sigstore") @@ -63,6 +64,7 @@ $ zarf package sign zarf-package-demo-amd64-1.0.0.tar.zst --signing-key awskms:/ --rekor-url string address of rekor STL server (default "https://rekor.sigstore.dev") --retries int Number of retries to perform for Zarf operations like git/image pushes (default 3) --signing-algorithm string signing algorithm to use for signing/hashing (allowed ecdsa-sha2-256-nistp256, ecdsa-sha2-384-nistp384, ecdsa-sha2-512-nistp521, rsa-sign-pkcs1-2048-sha256, rsa-sign-pkcs1-3072-sha256, rsa-sign-pkcs1-4096-sha256) (default "ecdsa-sha2-256-nistp256") + --signing-config string path to a signing config file. Must provide --bundle, which will output verification material in the new format --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 --sk whether to use a hardware security key @@ -72,6 +74,8 @@ $ zarf package sign zarf-package-demo-amd64-1.0.0.tar.zst --signing-key awskms:/ --timestamp-client-key string path to the X.509 private key file in PEM format to be used, together with the 'timestamp-client-cert' value, for the connection to the TSA Server --timestamp-server-name string SAN name to use as the 'ServerName' tls.Config field to verify the mTLS connection to the TSA Server --timestamp-server-url string url to the Timestamp RFC3161 server, default none. Must be the path to the API to request timestamp responses, e.g. https://freetsa.org/tsr + --trusted-root string optional path to a TrustedRoot JSON file to verify a signature after signing + --use-signing-config whether to use a TUF-provided signing config for the service URLs. Must provide --bundle, which will output verification material in the new format --verify Verify the Zarf package signature -y, --yes skip confirmation prompts for non-destructive operations ``` diff --git a/src/cmd/package.go b/src/cmd/package.go index 055c306959..b31038ec72 100644 --- a/src/cmd/package.go +++ b/src/cmd/package.go @@ -1826,6 +1826,7 @@ type packageSignOptions struct { ociConcurrency int retries int verify bool + keyless bool } func newPackageSignCommand(v *viper.Viper) *cobra.Command { @@ -1851,6 +1852,7 @@ func newPackageSignCommand(v *viper.Viper) *cobra.Command { cmd.Flags().IntVar(&o.ociConcurrency, "oci-concurrency", v.GetInt(VPkgOCIConcurrency), lang.CmdPackageFlagConcurrency) 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) mergeCosignSignFlags(cmd, &o.cosign) @@ -1873,7 +1875,6 @@ func hideAndOverrideSign(fs *pflag.FlagSet, opts *options.SignBlobOptions) { for _, name := range []string{ "bundle", "output-signature", "output-certificate", "b64", "rfc3161-timestamp", "issue-certificate", - "signing-config", "use-signing-config", "trusted-root", } { if f := fs.Lookup(name); f != nil { f.Hidden = true @@ -1884,6 +1885,10 @@ func hideAndOverrideSign(fs *pflag.FlagSet, opts *options.SignBlobOptions) { if f := fs.Lookup("tlog-upload"); f != nil { setFlagDefault(f, "false") } + opts.UseSigningConfig = false + if f := fs.Lookup("use-signing-config"); f != nil { + setFlagDefault(f, "false") + } } // setFlagDefault updates both the runtime value and the help-rendered default of a flag. @@ -1900,8 +1905,8 @@ func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error { l := logger.From(ctx) packageSource := args[0] - if o.cosign.Key == "" { - return errors.New("--signing-key is required") + if !o.keyless && o.cosign.Key == "" { + return errors.New("--signing-key is required (or pass --keyless for Sigstore keyless flow)") } // Determine output destination @@ -1994,13 +1999,24 @@ 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.SignBlobOptions = o.cosign signOpts.Password = o.signingKeyPassword signOpts.Overwrite = o.overwrite + signOpts.Keyless = o.keyless + + // 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 + } err = pkgLayout.SignPackage(ctx, signOpts) if err != nil { diff --git a/src/config/lang/english.go b/src/config/lang/english.go index 09c0a3fa66..8233bc269a 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -358,6 +358,7 @@ $ zarf package sign zarf-package-demo-amd64-1.0.0.tar.zst --signing-key awskms:/ CmdPackageSignFlagOutput = "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" CmdPackageSignFlagOverwrite = "Overwrite an existing signature if the package is already signed" CmdPackageSignFlagKey = "Public key to verify the existing signature before re-signing (optional)" + CmdPackageSignFlagKeyless = "Sign without a private key using Sigstore's keyless flow (Fulcio/OIDC)" CmdPackageVerifyShort = "Verify the signature and integrity of a Zarf package" 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." diff --git a/src/pkg/packager/layout/package.go b/src/pkg/packager/layout/package.go index e0bb3aab9a..b87533674b 100644 --- a/src/pkg/packager/layout/package.go +++ b/src/pkg/packager/layout/package.go @@ -238,7 +238,10 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOpti p.Pkg.Build.ProvenanceFiles = append(p.Pkg.Build.ProvenanceFiles, Signature) } - if feature.IsEnabled(feature.BundleSignature) && !slices.Contains(p.Pkg.Build.ProvenanceFiles, Bundle) { + // Keyless requires bundle format — the cert chain is the only verification material. + bundleEnabled := feature.IsEnabled(feature.BundleSignature) || opts.Keyless + + if bundleEnabled && !slices.Contains(p.Pkg.Build.ProvenanceFiles, Bundle) { p.Pkg.Build.ProvenanceFiles = append(p.Pkg.Build.ProvenanceFiles, Bundle) } @@ -267,7 +270,7 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOpti actualSignaturePath := filepath.Join(p.dirPath, Signature) actualBundlePath := filepath.Join(p.dirPath, Bundle) signOpts.OutputSignature = actualSignaturePath - if feature.IsEnabled(feature.BundleSignature) { + if bundleEnabled { signOpts.BundlePath = actualBundlePath } else { signOpts.NewBundleFormat = false @@ -279,7 +282,7 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOpti } signOpts.OutputSignature = tmpSignaturePath - if feature.IsEnabled(feature.BundleSignature) { + if bundleEnabled { signOpts.BundlePath = tmpBundlePath } @@ -309,13 +312,21 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOpti return fmt.Errorf("failed to move signature after signing: %w", err) } - if feature.IsEnabled(feature.BundleSignature) { + if bundleEnabled { err = os.Rename(tmpBundlePath, actualBundlePath) if err != nil { return fmt.Errorf("failed to move bundle after signing: %w", err) } } + if opts.Keyless { + if identity, issuer, ierr := utils.ReadKeylessIdentityFromBundle(actualBundlePath); ierr == nil { + l.Info("signed package keyless", "identity", identity, "issuer", issuer) + } else { + l.Debug("could not read keyless identity from bundle", "error", ierr) + } + } + l.Info("package signed successfully") return nil } @@ -335,20 +346,21 @@ func (p *PackageLayout) VerifyPackageSignature(ctx context.Context, opts utils.V return fmt.Errorf("invalid package layout: %s is not a directory", p.dirPath) } + hasKey := opts.Key != "" + hasKeylessIdentity := opts.CertVerify.CertIdentity != "" || opts.CertVerify.CertIdentityRegexp != "" + hasCert := opts.CertVerify.Cert != "" + hasVerificationMaterial := hasKey || hasKeylessIdentity || hasCert + // Handle the case where the package is not signed if !p.IsSigned() { - // Note: add future logic for verification material here - if opts.Key != "" { - return errors.New("a key was provided but the package is not signed") + if hasVerificationMaterial { + return errors.New("verification material was provided but the package is not signed") } - return errors.New("package is not signed - verification cannot be performed") } - // Validate that we have required verification material - // Note: this will later be replaced when verification enhancements are made - if opts.Key == "" { - return errors.New("package is signed but no verification material was provided (Public Key, etc.)") + if !hasVerificationMaterial { + return errors.New("package is signed but no verification material was provided (--key, --certificate-identity + --certificate-oidc-issuer, or --certificate)") } // Check for bundle format signature (preferred) @@ -373,6 +385,13 @@ func (p *PackageLayout) VerifyPackageSignature(ctx context.Context, opts utils.V return fmt.Errorf("error checking legacy signature: %w", err) } + // Legacy signatures don't carry a certificate chain, so keyless identity + // verification has nothing to match against. Fail fast with a clear message + // rather than letting cosign emit a generic key/cert/bundle error. + if hasKeylessIdentity { + return errors.New("keyless verification requires bundle-format signatures, but this package has only a legacy .sig. Ask the publisher to re-sign with bundle format, or verify with --key") + } + // Legacy signature found l.Warn("bundle format signature not found: legacy signature is being deprecated. consider resigning this zarf package with the --features='bundle-signature=true' flag.") opts.Signature = signaturePath diff --git a/src/pkg/packager/layout/package_test.go b/src/pkg/packager/layout/package_test.go index f5f4405dd3..4287823fd5 100644 --- a/src/pkg/packager/layout/package_test.go +++ b/src/pkg/packager/layout/package_test.go @@ -797,7 +797,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) require.Error(t, err) - require.Contains(t, err.Error(), "a key was provided but the package is not signed") + require.Contains(t, err.Error(), "verification material was provided but the package is not signed") }) t.Run("verification fails with empty dirPath", func(t *testing.T) { @@ -872,7 +872,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { verifyOpts.Key = "" // Empty key err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) - require.EqualError(t, err, "package is signed but no verification material was provided (Public Key, etc.)") + require.EqualError(t, err, "package is signed but no verification material was provided (--key, --certificate-identity + --certificate-oidc-issuer, or --certificate)") }) t.Run("verification fails when signature is corrupted", func(t *testing.T) { diff --git a/src/pkg/utils/cosign.go b/src/pkg/utils/cosign.go index 1f30de8358..0b50c66e36 100644 --- a/src/pkg/utils/cosign.go +++ b/src/pkg/utils/cosign.go @@ -14,6 +14,7 @@ import ( "github.com/google/go-containerregistry/pkg/name" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/sign" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" "github.com/sigstore/cosign/v3/cmd/cosign/cli/verify" "github.com/sigstore/cosign/v3/pkg/cosign" ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" @@ -48,6 +49,11 @@ type SignBlobOptions struct { Password string PassFunc cosign.PassFunc Overwrite bool + // Keyless gates zarf-specific sign-side guards on top of cosign's behavior. + // When true, --signing-key is no longer required and ShouldSign returns true + // even without explicit Key/IDToken/Sk material — cosign resolves identity + // via Fulcio/OIDC at sign time. + Keyless bool } // VerifyBlobOptions wraps cosign's VerifyBlobOptions with zarf-specific fields. @@ -57,9 +63,9 @@ type VerifyBlobOptions struct { Timeout time.Duration } -// ShouldSign returns true if any signing key material is configured. +// ShouldSign returns true if any signing key material is configured or keyless is requested. func (opts SignBlobOptions) ShouldSign() bool { - return opts.Key != "" || opts.Fulcio.IdentityToken != "" || opts.SecurityKey.Use + return opts.Key != "" || opts.Fulcio.IdentityToken != "" || opts.SecurityKey.Use || opts.Keyless } // CheckOverwrite errors if any output file exists and Overwrite is false. @@ -79,10 +85,14 @@ func (opts SignBlobOptions) CheckOverwrite(ctx context.Context) error { } // DefaultSignBlobOptions returns SignBlobOptions seeded with zarf defaults. -// Divergence: TlogUpload defaults to false (cosign default true) for airgap. +// Divergences from cosign defaults (air-gap): +// - TlogUpload=false (cosign default true) +// - UseSigningConfig=false (cosign default true) — required because cosign rejects +// UseSigningConfig=true combined with TlogUpload=false. func DefaultSignBlobOptions() SignBlobOptions { var opts SignBlobOptions opts.TlogUpload = false + opts.UseSigningConfig = false opts.Base64Output = true opts.NewBundleFormat = true opts.SecurityKey.Slot = "signature" @@ -160,6 +170,17 @@ func CosignSignBlobWithOptions(ctx context.Context, blobPath string, opts SignBl return nil, err } + // Empty output-path params suppress cosign's deprecation warnings; zarf manages + // OutputSignature internally and the warnings would fire on every sign. + if err := signcommon.LoadTrustedMaterialAndSigningConfig(ctx, &ko, + opts.UseSigningConfig, opts.SigningConfigPath, + opts.Rekor.URL, opts.Fulcio.URL, opts.OIDC.Issuer, opts.TSAServerURL, opts.TrustedRootPath, + opts.TlogUpload, opts.NewBundleFormat, opts.BundlePath, opts.Key, opts.IssueCertificate, + "", "", "", "", "", "", + ); err != nil { + return nil, err + } + l.Debug("signing blob with cosign", "key", opts.Key, "sk", opts.SecurityKey.Use, diff --git a/src/pkg/utils/keyless.go b/src/pkg/utils/keyless.go new file mode 100644 index 0000000000..2618afdaf8 --- /dev/null +++ b/src/pkg/utils/keyless.go @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package utils + +import ( + "crypto/x509" + "encoding/asn1" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "os" +) + +// Sigstore custom OIDs for the OIDC issuer claim embedded in Fulcio certs. +// - sigstoreIssuerOIDLegacy: raw string in extension value +// - sigstoreIssuerOIDV2: DER-encoded UTF8String, used by Fulcio v1+ +var ( + sigstoreIssuerOIDLegacy = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1} + sigstoreIssuerOIDV2 = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 8} +) + +// ReadKeylessIdentityFromBundle parses a Sigstore bundle file and returns the +// signer identity (cert SAN) and OIDC issuer claim. Used at sign time so users +// learn the identity their keyless flow resolved to. +func ReadKeylessIdentityFromBundle(bundlePath string) (identity, issuer string, err error) { + data, err := os.ReadFile(bundlePath) + if err != nil { + return "", "", err + } + + var b struct { + VerificationMaterial struct { + X509CertificateChain struct { + Certificates []struct { + RawBytes string `json:"rawBytes"` + } `json:"certificates"` + } `json:"x509CertificateChain"` + } `json:"verificationMaterial"` + } + if err := json.Unmarshal(data, &b); err != nil { + return "", "", fmt.Errorf("parsing bundle JSON: %w", err) + } + + certs := b.VerificationMaterial.X509CertificateChain.Certificates + if len(certs) == 0 { + return "", "", errors.New("bundle contains no certificate") + } + + der, err := base64.StdEncoding.DecodeString(certs[0].RawBytes) + if err != nil { + return "", "", fmt.Errorf("decoding cert: %w", err) + } + cert, err := x509.ParseCertificate(der) + if err != nil { + return "", "", fmt.Errorf("parsing cert: %w", err) + } + + switch { + case len(cert.EmailAddresses) > 0: + identity = cert.EmailAddresses[0] + case len(cert.URIs) > 0: + identity = cert.URIs[0].String() + case len(cert.DNSNames) > 0: + identity = cert.DNSNames[0] + } + + for _, ext := range cert.Extensions { + switch { + case ext.Id.Equal(sigstoreIssuerOIDV2): + var s string + if _, decErr := asn1.Unmarshal(ext.Value, &s); decErr == nil { + issuer = s + return identity, issuer, nil + } + case ext.Id.Equal(sigstoreIssuerOIDLegacy) && issuer == "": + issuer = string(ext.Value) + } + } + + return identity, issuer, nil +} diff --git a/src/pkg/utils/keyless_test.go b/src/pkg/utils/keyless_test.go new file mode 100644 index 0000000000..ccf04a80f2 --- /dev/null +++ b/src/pkg/utils/keyless_test.go @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package utils + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/base64" + "encoding/json" + "math/big" + "net/url" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func writeBundleFixture(t *testing.T, certDER []byte) string { + t.Helper() + bundle := map[string]any{ + "verificationMaterial": map[string]any{ + "x509CertificateChain": map[string]any{ + "certificates": []map[string]any{ + {"rawBytes": base64.StdEncoding.EncodeToString(certDER)}, + }, + }, + }, + } + data, err := json.Marshal(bundle) + require.NoError(t, err) + path := filepath.Join(t.TempDir(), "zarf.bundle.sig") + require.NoError(t, os.WriteFile(path, data, 0o600)) + return path +} + +func makeCert(t *testing.T, tmpl *x509.Certificate) []byte { + t.Helper() + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + tmpl.SerialNumber = big.NewInt(1) + tmpl.NotBefore = time.Now() + tmpl.NotAfter = time.Now().Add(time.Hour) + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv) + require.NoError(t, err) + return der +} + +func TestReadKeylessIdentityFromBundle(t *testing.T) { + t.Parallel() + + t.Run("email SAN with V2 issuer extension", func(t *testing.T) { + t.Parallel() + issuerVal, err := asn1.Marshal("https://oauth2.sigstore.dev/auth") + require.NoError(t, err) + der := makeCert(t, &x509.Certificate{ + Subject: pkix.Name{CommonName: "ephemeral"}, + EmailAddresses: []string{"signer@example.com"}, + ExtraExtensions: []pkix.Extension{ + {Id: sigstoreIssuerOIDV2, Value: issuerVal}, + }, + }) + path := writeBundleFixture(t, der) + + identity, issuer, err := ReadKeylessIdentityFromBundle(path) + require.NoError(t, err) + require.Equal(t, "signer@example.com", identity) + require.Equal(t, "https://oauth2.sigstore.dev/auth", issuer) + }) + + t.Run("URI SAN with legacy issuer extension", func(t *testing.T) { + t.Parallel() + ghaURI, err := url.Parse("https://github.com/example/repo/.github/workflows/release.yml@refs/heads/main") + require.NoError(t, err) + der := makeCert(t, &x509.Certificate{ + Subject: pkix.Name{CommonName: "ephemeral"}, + URIs: []*url.URL{ghaURI}, + ExtraExtensions: []pkix.Extension{ + {Id: sigstoreIssuerOIDLegacy, Value: []byte("https://token.actions.githubusercontent.com")}, + }, + }) + path := writeBundleFixture(t, der) + + identity, issuer, err := ReadKeylessIdentityFromBundle(path) + require.NoError(t, err) + require.Equal(t, ghaURI.String(), identity) + require.Equal(t, "https://token.actions.githubusercontent.com", issuer) + }) + + t.Run("V2 takes precedence over legacy when both are present", func(t *testing.T) { + t.Parallel() + v2Val, err := asn1.Marshal("https://v2-issuer.example.com") + require.NoError(t, err) + der := makeCert(t, &x509.Certificate{ + Subject: pkix.Name{CommonName: "ephemeral"}, + EmailAddresses: []string{"signer@example.com"}, + ExtraExtensions: []pkix.Extension{ + {Id: sigstoreIssuerOIDLegacy, Value: []byte("https://legacy.example.com")}, + {Id: sigstoreIssuerOIDV2, Value: v2Val}, + }, + }) + path := writeBundleFixture(t, der) + + _, issuer, err := ReadKeylessIdentityFromBundle(path) + require.NoError(t, err) + require.Equal(t, "https://v2-issuer.example.com", issuer) + }) + + t.Run("missing bundle file errors", func(t *testing.T) { + t.Parallel() + _, _, err := ReadKeylessIdentityFromBundle(filepath.Join(t.TempDir(), "nonexistent.json")) + require.Error(t, err) + }) + + t.Run("bundle without certificate errors", func(t *testing.T) { + t.Parallel() + path := filepath.Join(t.TempDir(), "empty.json") + require.NoError(t, os.WriteFile(path, []byte(`{"verificationMaterial":{}}`), 0o600)) + _, _, err := ReadKeylessIdentityFromBundle(path) + require.ErrorContains(t, err, "no certificate") + }) +} diff --git a/src/test/e2e/12_package_signing_test.go b/src/test/e2e/12_package_signing_test.go index 9065b0ebda..01a65309a6 100644 --- a/src/test/e2e/12_package_signing_test.go +++ b/src/test/e2e/12_package_signing_test.go @@ -122,6 +122,10 @@ func TestPackageSigning(t *testing.T) { "--certificate-chain", "--sk", "--slot", + "--keyless", + "--signing-config", + "--use-signing-config", + "--trusted-root", } { require.Contains(t, stdOut, flag, "expected %q in `package sign --help`", flag) } @@ -136,8 +140,21 @@ func TestPackageSigning(t *testing.T) { stdOut, stdErr, err = e2e.Zarf(t, "package", "sign", "--help") require.NoError(t, err, stdOut, stdErr) - for _, hidden := range []string{"--bundle ", "--output-signature", "--output-certificate", "--issue-certificate", "--signing-config", "--use-signing-config"} { + for _, hidden := range []string{"--bundle ", "--output-signature", "--output-certificate", "--issue-certificate"} { require.NotContains(t, stdOut, hidden, "expected %q to be hidden in `package sign --help`", hidden) } }) + + t.Run("--keyless lifts the --signing-key requirement", func(t *testing.T) { + // Without --signing-key and without --keyless: should error with the guard message. + _, stdErr, err := e2e.Zarf(t, "package", "sign", "nonexistent.tar.zst") + require.Error(t, err) + require.Contains(t, stdErr, "--signing-key is required") + + // With --keyless: guard is lifted. The command will fail later for unrelated + // reasons (no package, no OIDC), but not on the --signing-key check. + _, stdErr, err = e2e.Zarf(t, "package", "sign", "nonexistent.tar.zst", "--keyless") + require.Error(t, err) + require.NotContains(t, stdErr, "--signing-key is required") + }) } From cab4e0862452864f5359917dc60f940c2e360f04 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Mon, 11 May 2026 16:58:15 +0000 Subject: [PATCH 06/25] fix(options): deprecate duplicitive fields Signed-off-by: Brandt Keller --- .../docs/commands/zarf_package_sign.md | 44 ++++--------- .../docs/commands/zarf_package_verify.md | 40 +++--------- src/cmd/package.go | 41 +++++++++++- src/pkg/packager/layout/package.go | 8 +++ src/pkg/utils/cosign.go | 32 ++++++++- src/pkg/utils/cosign_test.go | 30 +++++++++ src/test/e2e/12_package_signing_test.go | 65 +++++++++---------- 7 files changed, 163 insertions(+), 97 deletions(-) create mode 100644 src/pkg/utils/cosign_test.go diff --git a/site/src/content/docs/commands/zarf_package_sign.md b/site/src/content/docs/commands/zarf_package_sign.md index c5112bbe73..17727f554f 100644 --- a/site/src/content/docs/commands/zarf_package_sign.md +++ b/site/src/content/docs/commands/zarf_package_sign.md @@ -42,38 +42,18 @@ $ zarf package sign zarf-package-demo-amd64-1.0.0.tar.zst --signing-key awskms:/ ### Options ``` - --certificate string path to the X.509 certificate for signing attestation - --certificate-chain string path to a list of CA X.509 certificates in PEM format which will be needed when building the certificate chain for the signed attestation. Must start with the parent intermediate CA certificate of the signing certificate and end with the root certificate. - --fulcio-auth-flow string fulcio interactive oauth2 flow to use for certificate from fulcio. Defaults to determining the flow based on the runtime environment. (options) normal|device|token|client_credentials - --fulcio-url string address of sigstore PKI server (default "https://fulcio.sigstore.dev") - -h, --help help for sign - --identity-token string identity token to use for certificate from fulcio. the token or a path to a file containing the token is accepted. - --insecure-skip-verify skip verifying fulcio published to the SCT (this should only be used for testing). - -k, --key string Public key to verify the existing signature before re-signing (optional) - --new-bundle-format output bundle in new format that contains all verification material (default true) - --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 for application (default "sigstore") - --oidc-client-secret-file string Path to file containing OIDC client secret for application - --oidc-disable-ambient-providers Disable ambient OIDC providers. When true, ambient credentials will not be read - --oidc-issuer string OIDC provider to be used to issue ID token (default "https://oauth2.sigstore.dev/auth") - --oidc-provider string Specify the provider to get the OIDC token from (Optional). If unset, all options will be tried. Options include: [spiffe, google, github-actions, filesystem, buildkite-agent] - --oidc-redirect-url string OIDC redirect URL (Optional). The default oidc-redirect-url is 'http://localhost:0/auth/callback'. - -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 address of rekor STL server (default "https://rekor.sigstore.dev") - --retries int Number of retries to perform for Zarf operations like git/image pushes (default 3) - --signing-algorithm string signing algorithm to use for signing/hashing (allowed ecdsa-sha2-256-nistp256, ecdsa-sha2-384-nistp384, ecdsa-sha2-512-nistp521, rsa-sign-pkcs1-2048-sha256, rsa-sign-pkcs1-3072-sha256, rsa-sign-pkcs1-4096-sha256) (default "ecdsa-sha2-256-nistp256") - --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 - --sk whether to use a hardware security key - --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) - --timestamp-client-cacert string path to the X.509 CA certificate file in PEM format to be used for the connection to the TSA Server - --timestamp-client-cert string path to the X.509 certificate file in PEM format to be used for the connection to the TSA Server - --timestamp-client-key string path to the X.509 private key file in PEM format to be used, together with the 'timestamp-client-cert' value, for the connection to the TSA Server - --timestamp-server-name string SAN name to use as the 'ServerName' tls.Config field to verify the mTLS connection to the TSA Server - --timestamp-server-url string url to the Timestamp RFC3161 server, default none. Must be the path to the API to request timestamp responses, e.g. https://freetsa.org/tsr - --verify Verify the Zarf package signature - -y, --yes skip confirmation prompts for non-destructive operations + -h, --help help for sign + -k, --key string Public key to verify the existing signature before re-signing (optional) + --new-bundle-format output bundle in new format that contains all verification material (default true) + --oci-concurrency int Number of concurrent layer operations when pulling or pushing images or packages to/from OCI registries. (default 6) + -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 address of rekor STL server (default "https://rekor.sigstore.dev") + --retries int Number of retries to perform for Zarf operations like git/image pushes (default 3) + --signing-algorithm string signing algorithm to use for signing/hashing (allowed ecdsa-sha2-256-nistp256, ecdsa-sha2-384-nistp384, ecdsa-sha2-512-nistp521, rsa-sign-pkcs1-2048-sha256, rsa-sign-pkcs1-3072-sha256, rsa-sign-pkcs1-4096-sha256) (default "ecdsa-sha2-256-nistp256") + --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 + --verify Verify the Zarf package signature ``` ### Options inherited from parent commands diff --git a/site/src/content/docs/commands/zarf_package_verify.md b/site/src/content/docs/commands/zarf_package_verify.md index bf88a7d664..15042aa3ca 100644 --- a/site/src/content/docs/commands/zarf_package_verify.md +++ b/site/src/content/docs/commands/zarf_package_verify.md @@ -33,36 +33,16 @@ $ zarf package verify zarf-package-demo-amd64-1.0.0.tar.zst ### Options ``` - --ca-intermediates string path to a file of intermediate CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. The flag is optional and must be used together with --ca-roots, conflicts with --certificate-chain. - --ca-roots string path to a bundle file of CA certificates in PEM format which will be needed when building the certificate chains for the signing certificate. Conflicts with --certificate-chain. - --certificate string path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed. - --certificate-chain string path to a list of CA certificates in PEM format which will be needed when building the certificate chain for the signing certificate. Must start with the parent intermediate CA certificate of the signing certificate and end with the root certificate. Conflicts with --ca-roots and --ca-intermediates. - --certificate-github-workflow-name string contains the workflow claim from the GitHub OIDC Identity token that contains the name of the executed workflow. - --certificate-github-workflow-ref string contains the ref claim from the GitHub OIDC Identity token that contains the git ref that the workflow run was based upon. - --certificate-github-workflow-repository string contains the repository claim from the GitHub OIDC Identity token that contains the repository that the workflow run was based upon - --certificate-github-workflow-sha string contains the sha claim from the GitHub OIDC Identity token that contains the commit SHA that the workflow run was based upon. - --certificate-github-workflow-trigger string contains the event_name claim from the GitHub OIDC Identity token that contains the name of the event that triggered the workflow run - --certificate-identity string The identity expected in a valid Fulcio certificate. Valid values include email address, DNS names, IP addresses, and URIs. Either --certificate-identity or --certificate-identity-regexp must be set for keyless flows. - --certificate-identity-regexp string A regular expression alternative to --certificate-identity. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --certificate-identity or --certificate-identity-regexp must be set for keyless flows. - --certificate-oidc-issuer string The OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth. Either --certificate-oidc-issuer or --certificate-oidc-issuer-regexp must be set for keyless flows. - --certificate-oidc-issuer-regexp string A regular expression alternative to --certificate-oidc-issuer. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --certificate-oidc-issuer or --certificate-oidc-issuer-regexp must be set for keyless flows. - --experimental-oci11 set to true to enable experimental OCI 1.1 behaviour (unrelated to bundle format) - -h, --help help for verify - --insecure-ignore-sct when set, verification will not check that a certificate contains an embedded SCT, a proof of inclusion in a certificate transparency log (default true) - --insecure-ignore-tlog ignore transparency log verification, to be used when an artifact signature has not been uploaded to the transparency log. Artifacts cannot be publicly verified when not included in a log (default true) - -k, --key string path to the public key file, KMS URI or Kubernetes Secret - --max-workers int the amount of maximum workers for parallel executions (default 10) - --new-bundle-format expect the signature/attestation to be packaged in a Sigstore bundle (default true) - --oci-concurrency int Number of concurrent layer operations when pulling or pushing images or packages to/from OCI registries. (default 6) - --private-infrastructure skip transparency log verification when verifying artifacts in a privately deployed infrastructure - --rekor-url string address of rekor STL server (default "https://rekor.sigstore.dev") - --sct string path to a detached Signed Certificate Timestamp, formatted as a RFC6962 AddChainResponse struct. If a certificate contains an SCT, verification will check both the detached and embedded SCTs. - --signature-digest-algorithm string digest algorithm to use when processing a signature (sha224|sha256|sha384|sha512) (default "sha256") - --sk whether to use a hardware security key - --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) - --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp - --trusted-root string Path to a Sigstore TrustedRoot JSON file. Requires --new-bundle-format to be set. - --use-signed-timestamps verify rfc3161 timestamps + -h, --help help for verify + --insecure-ignore-sct when set, verification will not check that a certificate contains an embedded SCT, a proof of inclusion in a certificate transparency log (default true) + --insecure-ignore-tlog ignore transparency log verification, to be used when an artifact signature has not been uploaded to the transparency log. Artifacts cannot be publicly verified when not included in a log (default true) + -k, --key string path to the public key file, KMS URI or Kubernetes Secret + --max-workers int the amount of maximum workers for parallel executions (default 10) + --new-bundle-format expect the signature/attestation to be packaged in a Sigstore bundle (default true) + --oci-concurrency int Number of concurrent layer operations when pulling or pushing images or packages to/from OCI registries. (default 6) + --rekor-url string address of rekor STL server (default "https://rekor.sigstore.dev") + --signature-digest-algorithm string digest algorithm to use when processing a signature (sha224|sha256|sha384|sha512) (default "sha256") + --trusted-root string Path to a Sigstore TrustedRoot JSON file. Requires --new-bundle-format to be set. ``` ### Options inherited from parent commands diff --git a/src/cmd/package.go b/src/cmd/package.go index 055c306959..f3ede38be0 100644 --- a/src/cmd/package.go +++ b/src/cmd/package.go @@ -1869,11 +1869,28 @@ func mergeCosignSignFlags(cmd *cobra.Command, opts *options.SignBlobOptions) { cmd.Flags().AddFlagSet(side.Flags()) } +// hideAndOverrideSign hides cosign flags whose underlying flow is not yet wired. func hideAndOverrideSign(fs *pflag.FlagSet, opts *options.SignBlobOptions) { for _, name := range []string{ + // zarf manages bundle/signature output internally "bundle", "output-signature", "output-certificate", + // Deprecated upstream "b64", "rfc3161-timestamp", "issue-certificate", + // Trust-material wiring (LoadTrustedMaterialAndSigningConfig) lands later "signing-config", "use-signing-config", "trusted-root", + // Keyless sign requires lifting the --signing-key guard + "fulcio-url", "identity-token", "fulcio-auth-flow", "insecure-skip-verify", + "oidc-issuer", "oidc-client-id", "oidc-redirect-url", + "oidc-disable-ambient-providers", "oidc-provider", "oidc-client-secret-file", + // Hardware-key signing also blocked by --signing-key guard + "sk", "slot", + // TSA flow not wired + "timestamp-client-cacert", "timestamp-client-cert", "timestamp-client-key", + "timestamp-server-name", "timestamp-server-url", + // Cert-based signing blocked by --signing-key guard + "certificate", "certificate-chain", + // Only meaningful for keyless prompts + "yes", } { if f := fs.Lookup(name); f != nil { f.Hidden = true @@ -2058,8 +2075,30 @@ func mergeCosignVerifyFlags(cmd *cobra.Command, opts *options.VerifyBlobOptions) cmd.Flags().AddFlagSet(side.Flags()) } +// hideAndOverrideVerify hides cosign flags whose underlying flow is not yet wired. func hideAndOverrideVerify(fs *pflag.FlagSet, opts *options.VerifyBlobOptions) { - for _, name := range []string{"bundle", "signature", "rfc3161-timestamp"} { + for _, name := range []string{ + // zarf provides bundle/signature from the package archive + "bundle", "signature", + // Deprecated upstream + "rfc3161-timestamp", + // Keyless verify identity — blocked by the --key guard + "certificate-identity", "certificate-identity-regexp", + "certificate-oidc-issuer", "certificate-oidc-issuer-regexp", + "certificate-github-workflow-trigger", "certificate-github-workflow-sha", + "certificate-github-workflow-name", "certificate-github-workflow-repository", + "certificate-github-workflow-ref", + // Cert-based verify — blocked by the --key guard + "certificate", "certificate-chain", "ca-roots", "ca-intermediates", + // Hardware-key verify — blocked by the --key guard + "sk", "slot", + // TSA verify not wired + "timestamp-certificate-chain", "use-signed-timestamps", + // SCT material for keyless cert validation + "sct", + // Advanced/experimental — outside zarf's Stage 2 surface + "private-infrastructure", "experimental-oci11", + } { if f := fs.Lookup(name); f != nil { f.Hidden = true } diff --git a/src/pkg/packager/layout/package.go b/src/pkg/packager/layout/package.go index e0bb3aab9a..71b677ff59 100644 --- a/src/pkg/packager/layout/package.go +++ b/src/pkg/packager/layout/package.go @@ -335,6 +335,14 @@ func (p *PackageLayout) VerifyPackageSignature(ctx context.Context, opts utils.V return fmt.Errorf("invalid package layout: %s is not a directory", p.dirPath) } + // Sync the deprecated KeyRef alias before the gate so a caller using only the + // old field name isn't rejected for missing material. CosignVerifyBlobWithOptions + // emits the deprecation warning when invoked. Touching the deprecated field is + // the only way to perform the migration sync from this package. + if opts.Key == "" && opts.KeyRef != "" { //nolint:staticcheck // intentional read of deprecated alias for migration sync + opts.Key = opts.KeyRef //nolint:staticcheck // intentional read of deprecated alias for migration sync + } + // Handle the case where the package is not signed if !p.IsSigned() { // Note: add future logic for verification material here diff --git a/src/pkg/utils/cosign.go b/src/pkg/utils/cosign.go index 3f42500100..ce167d2604 100644 --- a/src/pkg/utils/cosign.go +++ b/src/pkg/utils/cosign.go @@ -47,6 +47,9 @@ type SignBlobOptions struct { Password string PassFunc cosign.PassFunc Overwrite bool + + // Deprecated: use Key (promoted from the embedded SignBlobOptions). Removed in v1.0. + KeyRef string } // VerifyBlobOptions wraps cosign's VerifyBlobOptions with zarf-specific fields. @@ -54,11 +57,18 @@ type VerifyBlobOptions struct { options.VerifyBlobOptions Timeout time.Duration + + // Deprecated: use Key (promoted from the embedded VerifyBlobOptions). Removed in v1.0. + KeyRef string + // Deprecated: use Signature (promoted from the embedded VerifyBlobOptions). Removed in v1.0. + SigRef string } // ShouldSign returns true if any signing key material is configured. +// KeyRef is included for backward compatibility; it's synced to Key in +// CosignSignBlobWithOptions. func (opts SignBlobOptions) ShouldSign() bool { - return opts.Key != "" || opts.Fulcio.IdentityToken != "" || opts.SecurityKey.Use + return opts.Key != "" || opts.KeyRef != "" || opts.Fulcio.IdentityToken != "" || opts.SecurityKey.Use } // CheckOverwrite errors if any output file exists and Overwrite is false. @@ -107,6 +117,13 @@ func DefaultVerifyBlobOptions() VerifyBlobOptions { func CosignSignBlobWithOptions(ctx context.Context, blobPath string, opts SignBlobOptions) ([]byte, error) { l := logger.From(ctx) + if opts.KeyRef != "" { + l.Warn("SignBlobOptions.KeyRef is deprecated, use Key (removed in v1.0)") + if opts.Key == "" { + opts.Key = opts.KeyRef + } + } + rootOpts := &options.RootOptions{ Verbose: opts.Verbose, Timeout: opts.Timeout, @@ -189,6 +206,19 @@ func CosignSignBlobWithOptions(ctx context.Context, blobPath string, opts SignBl func CosignVerifyBlobWithOptions(ctx context.Context, blobPath string, opts VerifyBlobOptions) error { l := logger.From(ctx) + if opts.KeyRef != "" { + l.Warn("VerifyBlobOptions.KeyRef is deprecated, use Key (removed in v1.0)") + if opts.Key == "" { + opts.Key = opts.KeyRef + } + } + if opts.SigRef != "" { + l.Warn("VerifyBlobOptions.SigRef is deprecated, use Signature (removed in v1.0)") + if opts.Signature == "" { + opts.Signature = opts.SigRef + } + } + hashAlgorithm, err := opts.SignatureDigest.HashAlgorithm() if err != nil { return err diff --git a/src/pkg/utils/cosign_test.go b/src/pkg/utils/cosign_test.go new file mode 100644 index 0000000000..f1267f8fc6 --- /dev/null +++ b/src/pkg/utils/cosign_test.go @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package utils + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestShouldSign_KeyRefAlias(t *testing.T) { + t.Parallel() + + t.Run("KeyRef alone triggers signing", func(t *testing.T) { + opts := SignBlobOptions{} + opts.KeyRef = "/path/to/key" + require.True(t, opts.ShouldSign()) + }) + + t.Run("Key alone triggers signing", func(t *testing.T) { + opts := SignBlobOptions{} + opts.Key = "/path/to/key" + require.True(t, opts.ShouldSign()) + }) + + t.Run("empty options skip signing", func(t *testing.T) { + require.False(t, SignBlobOptions{}.ShouldSign()) + }) +} diff --git a/src/test/e2e/12_package_signing_test.go b/src/test/e2e/12_package_signing_test.go index 9065b0ebda..db4114d473 100644 --- a/src/test/e2e/12_package_signing_test.go +++ b/src/test/e2e/12_package_signing_test.go @@ -87,17 +87,13 @@ func TestPackageSigning(t *testing.T) { require.Contains(t, stdErr, "a key was provided but the package is not signed") }) - // Confirms cosign-aligned flag surface is bound on verify. Round-trip keyless verify - // against real Sigstore fixtures lands in Stage 3 alongside trusted-root embedding. - t.Run("Cosign-aligned verify flags are accepted", func(t *testing.T) { + // Exposes only the cosign flags whose underlying flow is wired through. + // Keyless, hardware-key, TSA, and cert-based flags are hidden until later stages + // wire them. + t.Run("visible cosign flags on verify", func(t *testing.T) { stdOut, stdErr, err := e2e.Zarf(t, "package", "verify", "--help") require.NoError(t, err, stdOut, stdErr) for _, flag := range []string{ - "--certificate-identity", - "--certificate-oidc-issuer", - "--certificate-identity-regexp", - "--certificate-oidc-issuer-regexp", - "--certificate-github-workflow-trigger", "--trusted-root", "--insecure-ignore-tlog", "--insecure-ignore-sct", @@ -105,39 +101,42 @@ func TestPackageSigning(t *testing.T) { } { require.Contains(t, stdOut, flag, "expected %q in `package verify --help`", flag) } - // airgap-safe defaults must remain enabled. - require.Contains(t, stdOut, "--insecure-ignore-tlog ignore transparency log verification, to be used when an artifact signature has not been uploaded to the transparency log. Artifacts cannot be publicly verified when not included in a log (default true)") - require.Contains(t, stdOut, "--insecure-ignore-sct when set, verification will not check that a certificate contains an embedded SCT, a proof of inclusion in a certificate transparency log (default true)") + // Airgap-safe defaults must remain enabled. Match by trailing "(default true)" on the + // flag line rather than full column-aligned strings (cobra recomputes alignment as the + // visible flag set changes). + require.Regexp(t, `--insecure-ignore-tlog[^\n]*\(default true\)`, stdOut) + require.Regexp(t, `--insecure-ignore-sct[^\n]*\(default true\)`, stdOut) }) - t.Run("Cosign-aligned sign flags are accepted", func(t *testing.T) { - stdOut, stdErr, err := e2e.Zarf(t, "package", "sign", "--help") + t.Run("hidden verify flags", func(t *testing.T) { + stdOut, stdErr, err := e2e.Zarf(t, "package", "verify", "--help") require.NoError(t, err, stdOut, stdErr) - for _, flag := range []string{ - "--fulcio-url", - "--rekor-url", - "--oidc-issuer", - "--identity-token", - "--certificate", - "--certificate-chain", - "--sk", - "--slot", + for _, hidden := range []string{ + "--bundle ", "--signature ", "--rfc3161-timestamp", + "--certificate-identity", "--certificate-oidc-issuer", + "--certificate-github-workflow", + "--certificate ", "--certificate-chain", "--ca-roots", "--ca-intermediates", + "--sk ", "--slot ", + "--timestamp-certificate-chain", "--use-signed-timestamps", + "--sct ", "--private-infrastructure", "--experimental-oci11", } { - require.Contains(t, stdOut, flag, "expected %q in `package sign --help`", flag) + require.NotContains(t, stdOut, hidden, "expected %q hidden from `package verify --help`", hidden) } }) - t.Run("Hidden flags do not appear in help", func(t *testing.T) { - stdOut, stdErr, err := e2e.Zarf(t, "package", "verify", "--help") - require.NoError(t, err, stdOut, stdErr) - for _, hidden := range []string{"--bundle", "--signature", "--rfc3161-timestamp"} { - require.NotContains(t, stdOut, hidden+" string", "expected %q to be hidden in `package verify --help`", hidden) - } - - stdOut, stdErr, err = e2e.Zarf(t, "package", "sign", "--help") + t.Run("hidden sign flags", func(t *testing.T) { + stdOut, stdErr, err := e2e.Zarf(t, "package", "sign", "--help") require.NoError(t, err, stdOut, stdErr) - for _, hidden := range []string{"--bundle ", "--output-signature", "--output-certificate", "--issue-certificate", "--signing-config", "--use-signing-config"} { - require.NotContains(t, stdOut, hidden, "expected %q to be hidden in `package sign --help`", hidden) + for _, hidden := range []string{ + "--bundle ", "--output-signature", "--output-certificate", "--issue-certificate", + "--signing-config", "--use-signing-config", "--trusted-root", + "--fulcio-url", "--identity-token", "--oidc-issuer", "--oidc-client-id", + "--fulcio-auth-flow", "--insecure-skip-verify", + "--sk ", "--slot ", + "--timestamp-client", "--timestamp-server", + "--certificate ", "--certificate-chain", + } { + require.NotContains(t, stdOut, hidden, "expected %q hidden from `package sign --help`", hidden) } }) } From 4f2d01e332ae0d60389fe8622dcd5fb7ae566795 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Mon, 11 May 2026 17:26:55 +0000 Subject: [PATCH 07/25] fix(flags): review flags that can be further hidden based on utility Signed-off-by: Brandt Keller --- .../docs/commands/zarf_package_sign.md | 21 ++++---- .../docs/commands/zarf_package_verify.md | 3 -- src/cmd/package.go | 12 ++++- src/test/e2e/12_package_signing_test.go | 48 +++++++++++-------- 4 files changed, 46 insertions(+), 38 deletions(-) diff --git a/site/src/content/docs/commands/zarf_package_sign.md b/site/src/content/docs/commands/zarf_package_sign.md index 17727f554f..a6ebcdbc74 100644 --- a/site/src/content/docs/commands/zarf_package_sign.md +++ b/site/src/content/docs/commands/zarf_package_sign.md @@ -42,18 +42,15 @@ $ zarf package sign zarf-package-demo-amd64-1.0.0.tar.zst --signing-key awskms:/ ### Options ``` - -h, --help help for sign - -k, --key string Public key to verify the existing signature before re-signing (optional) - --new-bundle-format output bundle in new format that contains all verification material (default true) - --oci-concurrency int Number of concurrent layer operations when pulling or pushing images or packages to/from OCI registries. (default 6) - -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 address of rekor STL server (default "https://rekor.sigstore.dev") - --retries int Number of retries to perform for Zarf operations like git/image pushes (default 3) - --signing-algorithm string signing algorithm to use for signing/hashing (allowed ecdsa-sha2-256-nistp256, ecdsa-sha2-384-nistp384, ecdsa-sha2-512-nistp521, rsa-sign-pkcs1-2048-sha256, rsa-sign-pkcs1-3072-sha256, rsa-sign-pkcs1-4096-sha256) (default "ecdsa-sha2-256-nistp256") - --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 - --verify Verify the Zarf package signature + -h, --help help for sign + -k, --key string Public key to verify the existing signature before re-signing (optional) + --oci-concurrency int Number of concurrent layer operations when pulling or pushing images or packages to/from OCI registries. (default 6) + -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 + --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 + --verify Verify the Zarf package signature ``` ### Options inherited from parent commands diff --git a/site/src/content/docs/commands/zarf_package_verify.md b/site/src/content/docs/commands/zarf_package_verify.md index 15042aa3ca..850bb939a4 100644 --- a/site/src/content/docs/commands/zarf_package_verify.md +++ b/site/src/content/docs/commands/zarf_package_verify.md @@ -34,11 +34,8 @@ $ zarf package verify zarf-package-demo-amd64-1.0.0.tar.zst ``` -h, --help help for verify - --insecure-ignore-sct when set, verification will not check that a certificate contains an embedded SCT, a proof of inclusion in a certificate transparency log (default true) --insecure-ignore-tlog ignore transparency log verification, to be used when an artifact signature has not been uploaded to the transparency log. Artifacts cannot be publicly verified when not included in a log (default true) -k, --key string path to the public key file, KMS URI or Kubernetes Secret - --max-workers int the amount of maximum workers for parallel executions (default 10) - --new-bundle-format expect the signature/attestation to be packaged in a Sigstore bundle (default true) --oci-concurrency int Number of concurrent layer operations when pulling or pushing images or packages to/from OCI registries. (default 6) --rekor-url string address of rekor STL server (default "https://rekor.sigstore.dev") --signature-digest-algorithm string digest algorithm to use when processing a signature (sha224|sha256|sha384|sha512) (default "sha256") diff --git a/src/cmd/package.go b/src/cmd/package.go index da18f42b67..41e39335f5 100644 --- a/src/cmd/package.go +++ b/src/cmd/package.go @@ -1897,7 +1897,7 @@ func mergeCosignSignFlags(cmd *cobra.Command, opts *options.SignBlobOptions) { func hideAndOverrideSign(fs *pflag.FlagSet, opts *options.SignBlobOptions) { for _, name := range []string{ // zarf manages bundle/signature output internally - "bundle", "output-signature", "output-certificate", + "bundle", "output-signature", "output-certificate", "new-bundle-format", // Deprecated upstream "b64", "rfc3161-timestamp", "issue-certificate", // Trust-material wiring (LoadTrustedMaterialAndSigningConfig) lands later @@ -1915,6 +1915,10 @@ func hideAndOverrideSign(fs *pflag.FlagSet, opts *options.SignBlobOptions) { "certificate", "certificate-chain", // Only meaningful for keyless prompts "yes", + // Rekor flag is inert without --tlog-upload (hidden, cosign-deprecated) + "rekor-url", + // SigningAlgorithm is only consumed by cosign's signerFromNewKey (keyless ephemeral key path); ignored for pre-existing keys + "signing-algorithm", } { if f := fs.Lookup(name); f != nil { f.Hidden = true @@ -2103,9 +2107,13 @@ func mergeCosignVerifyFlags(cmd *cobra.Command, opts *options.VerifyBlobOptions) func hideAndOverrideVerify(fs *pflag.FlagSet, opts *options.VerifyBlobOptions) { for _, name := range []string{ // zarf provides bundle/signature from the package archive - "bundle", "signature", + "bundle", "signature", "new-bundle-format", // Deprecated upstream "rfc3161-timestamp", + // SCT validation only applies to Fulcio certs (keyless); inert for key-based bundles + "insecure-ignore-sct", + // MaxWorkers is on VerifyCommand (image verify) — not consumed by VerifyBlobCmd + "max-workers", // Keyless verify identity — blocked by the --key guard "certificate-identity", "certificate-identity-regexp", "certificate-oidc-issuer", "certificate-oidc-issuer-regexp", diff --git a/src/test/e2e/12_package_signing_test.go b/src/test/e2e/12_package_signing_test.go index db4114d473..e1c9e1b446 100644 --- a/src/test/e2e/12_package_signing_test.go +++ b/src/test/e2e/12_package_signing_test.go @@ -96,31 +96,33 @@ func TestPackageSigning(t *testing.T) { for _, flag := range []string{ "--trusted-root", "--insecure-ignore-tlog", - "--insecure-ignore-sct", "--rekor-url", } { require.Contains(t, stdOut, flag, "expected %q in `package verify --help`", flag) } - // Airgap-safe defaults must remain enabled. Match by trailing "(default true)" on the - // flag line rather than full column-aligned strings (cobra recomputes alignment as the - // visible flag set changes). + // Air-gap-safe default must remain enabled. require.Regexp(t, `--insecure-ignore-tlog[^\n]*\(default true\)`, stdOut) - require.Regexp(t, `--insecure-ignore-sct[^\n]*\(default true\)`, stdOut) }) + // Hidden-flag assertions match flag-entry lines specifically (leading indent + + // flag name) so substrings inside other flags' description text don't false-match. t.Run("hidden verify flags", func(t *testing.T) { stdOut, stdErr, err := e2e.Zarf(t, "package", "verify", "--help") require.NoError(t, err, stdOut, stdErr) for _, hidden := range []string{ - "--bundle ", "--signature ", "--rfc3161-timestamp", - "--certificate-identity", "--certificate-oidc-issuer", - "--certificate-github-workflow", - "--certificate ", "--certificate-chain", "--ca-roots", "--ca-intermediates", - "--sk ", "--slot ", - "--timestamp-certificate-chain", "--use-signed-timestamps", - "--sct ", "--private-infrastructure", "--experimental-oci11", + "bundle", "signature", "rfc3161-timestamp", "new-bundle-format", + "insecure-ignore-sct", "max-workers", + "certificate-identity", "certificate-oidc-issuer", + "certificate-github-workflow-trigger", "certificate-github-workflow-sha", + "certificate-github-workflow-name", "certificate-github-workflow-repository", + "certificate-github-workflow-ref", + "certificate", "certificate-chain", "ca-roots", "ca-intermediates", + "sk", "slot", + "timestamp-certificate-chain", "use-signed-timestamps", + "sct", "private-infrastructure", "experimental-oci11", } { - require.NotContains(t, stdOut, hidden, "expected %q hidden from `package verify --help`", hidden) + require.NotRegexp(t, `(?m)^\s+--`+hidden+`( |$)`, stdOut, + "expected --%s hidden from `package verify --help`", hidden) } }) @@ -128,15 +130,19 @@ func TestPackageSigning(t *testing.T) { stdOut, stdErr, err := e2e.Zarf(t, "package", "sign", "--help") require.NoError(t, err, stdOut, stdErr) for _, hidden := range []string{ - "--bundle ", "--output-signature", "--output-certificate", "--issue-certificate", - "--signing-config", "--use-signing-config", "--trusted-root", - "--fulcio-url", "--identity-token", "--oidc-issuer", "--oidc-client-id", - "--fulcio-auth-flow", "--insecure-skip-verify", - "--sk ", "--slot ", - "--timestamp-client", "--timestamp-server", - "--certificate ", "--certificate-chain", + "bundle", "output-signature", "output-certificate", "issue-certificate", + "new-bundle-format", + "rekor-url", "signing-algorithm", + "signing-config", "use-signing-config", "trusted-root", + "fulcio-url", "identity-token", "oidc-issuer", "oidc-client-id", + "fulcio-auth-flow", "insecure-skip-verify", + "sk", "slot", + "timestamp-client-cacert", "timestamp-client-cert", "timestamp-client-key", + "timestamp-server-name", "timestamp-server-url", + "certificate", "certificate-chain", } { - require.NotContains(t, stdOut, hidden, "expected %q hidden from `package sign --help`", hidden) + require.NotRegexp(t, `(?m)^\s+--`+hidden+`( |$)`, stdOut, + "expected --%s hidden from `package sign --help`", hidden) } }) } From e30354ea82b3a182ffd68f14c4e565cc2f5fb73b Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Mon, 11 May 2026 17:42:13 +0000 Subject: [PATCH 08/25] fix(flags): trustedroot flag is not operational... yet Signed-off-by: Brandt Keller --- site/src/content/docs/commands/zarf_package_verify.md | 1 - src/cmd/package.go | 4 ++++ src/test/e2e/12_package_signing_test.go | 3 +-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/site/src/content/docs/commands/zarf_package_verify.md b/site/src/content/docs/commands/zarf_package_verify.md index 850bb939a4..d367f76d07 100644 --- a/site/src/content/docs/commands/zarf_package_verify.md +++ b/site/src/content/docs/commands/zarf_package_verify.md @@ -39,7 +39,6 @@ $ zarf package verify zarf-package-demo-amd64-1.0.0.tar.zst --oci-concurrency int Number of concurrent layer operations when pulling or pushing images or packages to/from OCI registries. (default 6) --rekor-url string address of rekor STL server (default "https://rekor.sigstore.dev") --signature-digest-algorithm string digest algorithm to use when processing a signature (sha224|sha256|sha384|sha512) (default "sha256") - --trusted-root string Path to a Sigstore TrustedRoot JSON file. Requires --new-bundle-format to be set. ``` ### Options inherited from parent commands diff --git a/src/cmd/package.go b/src/cmd/package.go index 41e39335f5..1b657f4502 100644 --- a/src/cmd/package.go +++ b/src/cmd/package.go @@ -2114,6 +2114,10 @@ func hideAndOverrideVerify(fs *pflag.FlagSet, opts *options.VerifyBlobOptions) { "insecure-ignore-sct", // MaxWorkers is on VerifyCommand (image verify) — not consumed by VerifyBlobCmd "max-workers", + // TrustedRoot only matters for keyless cert validation / Rekor inclusion / TSA timestamps — + // all blocked in Stage 2 by the --key gate, no keyless flow, no TSA wiring. Help text also + // references the hidden --new-bundle-format flag. + "trusted-root", // Keyless verify identity — blocked by the --key guard "certificate-identity", "certificate-identity-regexp", "certificate-oidc-issuer", "certificate-oidc-issuer-regexp", diff --git a/src/test/e2e/12_package_signing_test.go b/src/test/e2e/12_package_signing_test.go index e1c9e1b446..111966427f 100644 --- a/src/test/e2e/12_package_signing_test.go +++ b/src/test/e2e/12_package_signing_test.go @@ -94,7 +94,6 @@ func TestPackageSigning(t *testing.T) { stdOut, stdErr, err := e2e.Zarf(t, "package", "verify", "--help") require.NoError(t, err, stdOut, stdErr) for _, flag := range []string{ - "--trusted-root", "--insecure-ignore-tlog", "--rekor-url", } { @@ -111,7 +110,7 @@ func TestPackageSigning(t *testing.T) { require.NoError(t, err, stdOut, stdErr) for _, hidden := range []string{ "bundle", "signature", "rfc3161-timestamp", "new-bundle-format", - "insecure-ignore-sct", "max-workers", + "insecure-ignore-sct", "max-workers", "trusted-root", "certificate-identity", "certificate-oidc-issuer", "certificate-github-workflow-trigger", "certificate-github-workflow-sha", "certificate-github-workflow-name", "certificate-github-workflow-repository", From fc8151bb7cff667ef1e535f03031d00afdf7d5f9 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Mon, 11 May 2026 20:17:30 +0000 Subject: [PATCH 09/25] fix(sigstore): retrieve identity/issuer from bundle Signed-off-by: Brandt Keller --- src/pkg/utils/keyless.go | 15 ++++++++++--- src/pkg/utils/keyless_test.go | 42 ++++++++++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/pkg/utils/keyless.go b/src/pkg/utils/keyless.go index 2618afdaf8..c9506c1f1f 100644 --- a/src/pkg/utils/keyless.go +++ b/src/pkg/utils/keyless.go @@ -30,8 +30,14 @@ func ReadKeylessIdentityFromBundle(bundlePath string) (identity, issuer string, return "", "", err } + // Sigstore bundle VerificationMaterial is a oneof: newer keyless bundles use + // "certificate" (single Fulcio cert), legacy/chain variants use + // "x509CertificateChain.certificates[]". Try both. var b struct { VerificationMaterial struct { + Certificate struct { + RawBytes string `json:"rawBytes"` + } `json:"certificate"` X509CertificateChain struct { Certificates []struct { RawBytes string `json:"rawBytes"` @@ -43,12 +49,15 @@ func ReadKeylessIdentityFromBundle(bundlePath string) (identity, issuer string, return "", "", fmt.Errorf("parsing bundle JSON: %w", err) } - certs := b.VerificationMaterial.X509CertificateChain.Certificates - if len(certs) == 0 { + rawBytes := b.VerificationMaterial.Certificate.RawBytes + if rawBytes == "" && len(b.VerificationMaterial.X509CertificateChain.Certificates) > 0 { + rawBytes = b.VerificationMaterial.X509CertificateChain.Certificates[0].RawBytes + } + if rawBytes == "" { return "", "", errors.New("bundle contains no certificate") } - der, err := base64.StdEncoding.DecodeString(certs[0].RawBytes) + der, err := base64.StdEncoding.DecodeString(rawBytes) if err != nil { return "", "", fmt.Errorf("decoding cert: %w", err) } diff --git a/src/pkg/utils/keyless_test.go b/src/pkg/utils/keyless_test.go index ccf04a80f2..92f80eafe9 100644 --- a/src/pkg/utils/keyless_test.go +++ b/src/pkg/utils/keyless_test.go @@ -24,15 +24,32 @@ import ( func writeBundleFixture(t *testing.T, certDER []byte) string { t.Helper() - bundle := map[string]any{ - "verificationMaterial": map[string]any{ + return writeBundleFixtureWithShape(t, certDER, "x509CertificateChain") +} + +// shape: "x509CertificateChain" (chain variant) or "certificate" (singular Fulcio cert). +func writeBundleFixtureWithShape(t *testing.T, certDER []byte, shape string) string { + t.Helper() + var verificationMaterial map[string]any + switch shape { + case "certificate": + verificationMaterial = map[string]any{ + "certificate": map[string]any{ + "rawBytes": base64.StdEncoding.EncodeToString(certDER), + }, + } + case "x509CertificateChain": + verificationMaterial = map[string]any{ "x509CertificateChain": map[string]any{ "certificates": []map[string]any{ {"rawBytes": base64.StdEncoding.EncodeToString(certDER)}, }, }, - }, + } + default: + t.Fatalf("unknown bundle shape: %s", shape) } + bundle := map[string]any{"verificationMaterial": verificationMaterial} data, err := json.Marshal(bundle) require.NoError(t, err) path := filepath.Join(t.TempDir(), "zarf.bundle.sig") @@ -93,6 +110,25 @@ func TestReadKeylessIdentityFromBundle(t *testing.T) { require.Equal(t, "https://token.actions.githubusercontent.com", issuer) }) + t.Run("singular certificate variant (newer keyless bundle)", func(t *testing.T) { + t.Parallel() + issuerVal, err := asn1.Marshal("https://github.com/login/oauth") + require.NoError(t, err) + der := makeCert(t, &x509.Certificate{ + Subject: pkix.Name{CommonName: "ephemeral"}, + EmailAddresses: []string{"signer@example.com"}, + ExtraExtensions: []pkix.Extension{ + {Id: sigstoreIssuerOIDV2, Value: issuerVal}, + }, + }) + path := writeBundleFixtureWithShape(t, der, "certificate") + + identity, issuer, err := ReadKeylessIdentityFromBundle(path) + require.NoError(t, err) + require.Equal(t, "signer@example.com", identity) + require.Equal(t, "https://github.com/login/oauth", issuer) + }) + t.Run("V2 takes precedence over legacy when both are present", func(t *testing.T) { t.Parallel() v2Val, err := asn1.Marshal("https://v2-issuer.example.com") From 7682107ba7512fe4057a268dd646699aa8be2b68 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Mon, 11 May 2026 21:31:47 +0000 Subject: [PATCH 10/25] fix(cmd): explicit opt-in for cosign options/flags Signed-off-by: Brandt Keller --- .../docs/commands/zarf_package_verify.md | 9 +- src/cmd/package.go | 146 +----------------- src/config/lang/english.go | 2 + src/test/e2e/12_package_signing_test.go | 58 ------- 4 files changed, 13 insertions(+), 202 deletions(-) diff --git a/site/src/content/docs/commands/zarf_package_verify.md b/site/src/content/docs/commands/zarf_package_verify.md index d367f76d07..86819bfb77 100644 --- a/site/src/content/docs/commands/zarf_package_verify.md +++ b/site/src/content/docs/commands/zarf_package_verify.md @@ -33,12 +33,9 @@ $ zarf package verify zarf-package-demo-amd64-1.0.0.tar.zst ### Options ``` - -h, --help help for verify - --insecure-ignore-tlog ignore transparency log verification, to be used when an artifact signature has not been uploaded to the transparency log. Artifacts cannot be publicly verified when not included in a log (default true) - -k, --key string path to the public key file, KMS URI or Kubernetes Secret - --oci-concurrency int Number of concurrent layer operations when pulling or pushing images or packages to/from OCI registries. (default 6) - --rekor-url string address of rekor STL server (default "https://rekor.sigstore.dev") - --signature-digest-algorithm string digest algorithm to use when processing a signature (sha224|sha256|sha384|sha512) (default "sha256") + -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) ``` ### Options inherited from parent commands diff --git a/src/cmd/package.go b/src/cmd/package.go index 1b657f4502..ded80eebe5 100644 --- a/src/cmd/package.go +++ b/src/cmd/package.go @@ -22,9 +22,7 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/defenseunicorns/pkg/helpers/v2" goyaml "github.com/goccy/go-yaml" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/spf13/cobra" - "github.com/spf13/pflag" "github.com/spf13/viper" "oras.land/oras-go/v2/registry" @@ -1842,7 +1840,7 @@ func (o *packagePullOptions) run(cmd *cobra.Command, args []string) error { } type packageSignOptions struct { - cosign options.SignBlobOptions + signingKeyPath string signingKeyPassword string publicKeyPath string overwrite bool @@ -1865,9 +1863,7 @@ func newPackageSignCommand(v *viper.Viper) *cobra.Command { RunE: o.run, } - // Zarf's pre-existing flags must register first so they win the name collisions - // (--key, --output) when cosign's AddFlags is folded in below via AddFlagSet. - cmd.Flags().StringVar(&o.cosign.Key, "signing-key", v.GetString(VPkgSignSigningKey), lang.CmdPackageSignFlagSigningKey) + cmd.Flags().StringVar(&o.signingKeyPath, "signing-key", v.GetString(VPkgSignSigningKey), lang.CmdPackageSignFlagSigningKey) cmd.Flags().StringVar(&o.signingKeyPassword, "signing-key-pass", v.GetString(VPkgSignSigningKeyPassword), lang.CmdPackageSignFlagSigningKeyPass) cmd.Flags().StringVarP(&o.output, "output", "o", v.GetString(VPkgSignOutput), lang.CmdPackageSignFlagOutput) cmd.Flags().BoolVar(&o.overwrite, "overwrite", v.GetBool(VPkgSignOverwrite), lang.CmdPackageSignFlagOverwrite) @@ -1876,76 +1872,15 @@ 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) - mergeCosignSignFlags(cmd, &o.cosign) - return cmd } -// mergeCosignSignFlags binds cosign's SignBlobOptions onto a throwaway command, -// hides flags whose support is deferred or are deprecated upstream, applies -// zarf's air-gap default overrides, and folds the result into cmd via AddFlagSet. -// AddFlagSet skips flag names already registered, so zarf's pre-existing --key -// and --output retain their semantics. -func mergeCosignSignFlags(cmd *cobra.Command, opts *options.SignBlobOptions) { - side := &cobra.Command{} - opts.AddFlags(side) - hideAndOverrideSign(side.Flags(), opts) - cmd.Flags().AddFlagSet(side.Flags()) -} - -// hideAndOverrideSign hides cosign flags whose underlying flow is not yet wired. -func hideAndOverrideSign(fs *pflag.FlagSet, opts *options.SignBlobOptions) { - for _, name := range []string{ - // zarf manages bundle/signature output internally - "bundle", "output-signature", "output-certificate", "new-bundle-format", - // Deprecated upstream - "b64", "rfc3161-timestamp", "issue-certificate", - // Trust-material wiring (LoadTrustedMaterialAndSigningConfig) lands later - "signing-config", "use-signing-config", "trusted-root", - // Keyless sign requires lifting the --signing-key guard - "fulcio-url", "identity-token", "fulcio-auth-flow", "insecure-skip-verify", - "oidc-issuer", "oidc-client-id", "oidc-redirect-url", - "oidc-disable-ambient-providers", "oidc-provider", "oidc-client-secret-file", - // Hardware-key signing also blocked by --signing-key guard - "sk", "slot", - // TSA flow not wired - "timestamp-client-cacert", "timestamp-client-cert", "timestamp-client-key", - "timestamp-server-name", "timestamp-server-url", - // Cert-based signing blocked by --signing-key guard - "certificate", "certificate-chain", - // Only meaningful for keyless prompts - "yes", - // Rekor flag is inert without --tlog-upload (hidden, cosign-deprecated) - "rekor-url", - // SigningAlgorithm is only consumed by cosign's signerFromNewKey (keyless ephemeral key path); ignored for pre-existing keys - "signing-algorithm", - } { - if f := fs.Lookup(name); f != nil { - f.Hidden = true - } - } - - opts.TlogUpload = false - if f := fs.Lookup("tlog-upload"); f != nil { - setFlagDefault(f, "false") - } -} - -// setFlagDefault updates both the runtime value and the help-rendered default of a flag. -// Panics on invalid value because callers pass static literals validated by flag type. -func setFlagDefault(f *pflag.Flag, value string) { - f.DefValue = value - if err := f.Value.Set(value); err != nil { - panic(fmt.Sprintf("setting flag %q default to %q: %v", f.Name, value, err)) - } -} - func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error { ctx := cmd.Context() l := logger.From(ctx) packageSource := args[0] - if o.cosign.Key == "" { + if o.signingKeyPath == "" { return errors.New("--signing-key is required") } @@ -1984,7 +1919,7 @@ func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error { // Create publish options from sign options publishOpts := &packagePublishOptions{ - signingKeyPath: o.cosign.Key, + signingKeyPath: o.signingKeyPath, signingKeyPassword: o.signingKeyPassword, ociConcurrency: o.ociConcurrency, retries: o.retries, @@ -2043,7 +1978,7 @@ func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error { l.Info("signing package with provided key") signOpts := utils.DefaultSignBlobOptions() - signOpts.SignBlobOptions = o.cosign + signOpts.Key = o.signingKeyPath signOpts.Password = o.signingKeyPassword signOpts.Overwrite = o.overwrite @@ -2064,7 +1999,7 @@ func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error { } type packageVerifyOptions struct { - cosign options.VerifyBlobOptions + publicKeyPath string ociConcurrency int } @@ -2081,74 +2016,12 @@ func newPackageVerifyCommand(v *viper.Viper) *cobra.Command { RunE: o.run, } + cmd.Flags().StringVarP(&o.publicKeyPath, "key", "k", v.GetString(VPkgPublicKey), lang.CmdPackageVerifyFlagKey) cmd.Flags().IntVar(&o.ociConcurrency, "oci-concurrency", v.GetInt(VPkgOCIConcurrency), lang.CmdPackageFlagConcurrency) - mergeCosignVerifyFlags(cmd, &o.cosign) - - // Re-add zarf's existing -k shorthand on cosign's --key (verify semantic matches). - if f := cmd.Flags().Lookup("key"); f != nil { - f.Shorthand = "k" - if v.IsSet(VPkgPublicKey) { - setFlagDefault(f, v.GetString(VPkgPublicKey)) - } - } - return cmd } -func mergeCosignVerifyFlags(cmd *cobra.Command, opts *options.VerifyBlobOptions) { - side := &cobra.Command{} - opts.AddFlags(side) - hideAndOverrideVerify(side.Flags(), opts) - cmd.Flags().AddFlagSet(side.Flags()) -} - -// hideAndOverrideVerify hides cosign flags whose underlying flow is not yet wired. -func hideAndOverrideVerify(fs *pflag.FlagSet, opts *options.VerifyBlobOptions) { - for _, name := range []string{ - // zarf provides bundle/signature from the package archive - "bundle", "signature", "new-bundle-format", - // Deprecated upstream - "rfc3161-timestamp", - // SCT validation only applies to Fulcio certs (keyless); inert for key-based bundles - "insecure-ignore-sct", - // MaxWorkers is on VerifyCommand (image verify) — not consumed by VerifyBlobCmd - "max-workers", - // TrustedRoot only matters for keyless cert validation / Rekor inclusion / TSA timestamps — - // all blocked in Stage 2 by the --key gate, no keyless flow, no TSA wiring. Help text also - // references the hidden --new-bundle-format flag. - "trusted-root", - // Keyless verify identity — blocked by the --key guard - "certificate-identity", "certificate-identity-regexp", - "certificate-oidc-issuer", "certificate-oidc-issuer-regexp", - "certificate-github-workflow-trigger", "certificate-github-workflow-sha", - "certificate-github-workflow-name", "certificate-github-workflow-repository", - "certificate-github-workflow-ref", - // Cert-based verify — blocked by the --key guard - "certificate", "certificate-chain", "ca-roots", "ca-intermediates", - // Hardware-key verify — blocked by the --key guard - "sk", "slot", - // TSA verify not wired - "timestamp-certificate-chain", "use-signed-timestamps", - // SCT material for keyless cert validation - "sct", - // Advanced/experimental — outside zarf's Stage 2 surface - "private-infrastructure", "experimental-oci11", - } { - if f := fs.Lookup(name); f != nil { - f.Hidden = true - } - } - - opts.CommonVerifyOptions.IgnoreTlog = true - opts.CertVerify.IgnoreSCT = true - for _, name := range []string{"insecure-ignore-tlog", "insecure-ignore-sct"} { - if f := fs.Lookup(name); f != nil { - setFlagDefault(f, "true") - } - } -} - func (o *packageVerifyOptions) run(cmd *cobra.Command, args []string) error { ctx := cmd.Context() l := logger.From(ctx) @@ -2164,11 +2037,8 @@ 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 := utils.DefaultVerifyBlobOptions() - verifyOpts.VerifyBlobOptions = o.cosign - loadOpts := packager.LoadOptions{ - VerifyBlobOptions: &verifyOpts, + VerifyBlobOptions: verifyBlobOptionsFromKeyPath(o.publicKeyPath), VerificationStrategy: layout.VerifyAlways, // Always enforce strict verification Filter: filters.Empty(), Architecture: config.GetArch(), diff --git a/src/config/lang/english.go b/src/config/lang/english.go index 09c0a3fa66..c0553539ce 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -368,6 +368,8 @@ $ zarf package verify zarf-package-demo-amd64-1.0.0.tar.zst --key ./public-key.p # Verify an unsigned package (checksums only) $ zarf package verify zarf-package-demo-amd64-1.0.0.tar.zst ` + CmdPackageVerifyFlagKey = "Public key for signature verification" + CmdPackagePullShort = "Pulls a Zarf package from a remote registry and save to the local file system" CmdPackagePullExample = ` # Pull a package matching the current architecture diff --git a/src/test/e2e/12_package_signing_test.go b/src/test/e2e/12_package_signing_test.go index 111966427f..11d5eed835 100644 --- a/src/test/e2e/12_package_signing_test.go +++ b/src/test/e2e/12_package_signing_test.go @@ -86,62 +86,4 @@ func TestPackageSigning(t *testing.T) { require.Error(t, err) require.Contains(t, stdErr, "a key was provided but the package is not signed") }) - - // Exposes only the cosign flags whose underlying flow is wired through. - // Keyless, hardware-key, TSA, and cert-based flags are hidden until later stages - // wire them. - t.Run("visible cosign flags on verify", func(t *testing.T) { - stdOut, stdErr, err := e2e.Zarf(t, "package", "verify", "--help") - require.NoError(t, err, stdOut, stdErr) - for _, flag := range []string{ - "--insecure-ignore-tlog", - "--rekor-url", - } { - require.Contains(t, stdOut, flag, "expected %q in `package verify --help`", flag) - } - // Air-gap-safe default must remain enabled. - require.Regexp(t, `--insecure-ignore-tlog[^\n]*\(default true\)`, stdOut) - }) - - // Hidden-flag assertions match flag-entry lines specifically (leading indent + - // flag name) so substrings inside other flags' description text don't false-match. - t.Run("hidden verify flags", func(t *testing.T) { - stdOut, stdErr, err := e2e.Zarf(t, "package", "verify", "--help") - require.NoError(t, err, stdOut, stdErr) - for _, hidden := range []string{ - "bundle", "signature", "rfc3161-timestamp", "new-bundle-format", - "insecure-ignore-sct", "max-workers", "trusted-root", - "certificate-identity", "certificate-oidc-issuer", - "certificate-github-workflow-trigger", "certificate-github-workflow-sha", - "certificate-github-workflow-name", "certificate-github-workflow-repository", - "certificate-github-workflow-ref", - "certificate", "certificate-chain", "ca-roots", "ca-intermediates", - "sk", "slot", - "timestamp-certificate-chain", "use-signed-timestamps", - "sct", "private-infrastructure", "experimental-oci11", - } { - require.NotRegexp(t, `(?m)^\s+--`+hidden+`( |$)`, stdOut, - "expected --%s hidden from `package verify --help`", hidden) - } - }) - - t.Run("hidden sign flags", func(t *testing.T) { - stdOut, stdErr, err := e2e.Zarf(t, "package", "sign", "--help") - require.NoError(t, err, stdOut, stdErr) - for _, hidden := range []string{ - "bundle", "output-signature", "output-certificate", "issue-certificate", - "new-bundle-format", - "rekor-url", "signing-algorithm", - "signing-config", "use-signing-config", "trusted-root", - "fulcio-url", "identity-token", "oidc-issuer", "oidc-client-id", - "fulcio-auth-flow", "insecure-skip-verify", - "sk", "slot", - "timestamp-client-cacert", "timestamp-client-cert", "timestamp-client-key", - "timestamp-server-name", "timestamp-server-url", - "certificate", "certificate-chain", - } { - require.NotRegexp(t, `(?m)^\s+--`+hidden+`( |$)`, stdOut, - "expected --%s hidden from `package sign --help`", hidden) - } - }) } From bc9797d4d5ba8cd5155f85f96c0f4495757316f8 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Mon, 11 May 2026 22:57:52 +0000 Subject: [PATCH 11/25] feat(sign): implement keyless signing options explicitly Signed-off-by: Brandt Keller --- .../docs/commands/zarf_package_sign.md | 9 +++ .../docs/commands/zarf_package_verify.md | 12 +++- src/cmd/package.go | 61 ++++++++++++++++++- src/config/lang/english.go | 16 ++++- src/pkg/feature/feature.go | 2 +- src/pkg/packager/layout/package.go | 28 +++------ 6 files changed, 100 insertions(+), 28 deletions(-) diff --git a/site/src/content/docs/commands/zarf_package_sign.md b/site/src/content/docs/commands/zarf_package_sign.md index a6ebcdbc74..c968d31e53 100644 --- a/site/src/content/docs/commands/zarf_package_sign.md +++ b/site/src/content/docs/commands/zarf_package_sign.md @@ -42,15 +42,24 @@ $ zarf package sign zarf-package-demo-amd64-1.0.0.tar.zst --signing-key awskms:/ ### Options ``` + --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 + --yes Skip the interactive confirmation prompt before uploading to the Rekor transparency log. ``` ### Options inherited from parent commands diff --git a/site/src/content/docs/commands/zarf_package_verify.md b/site/src/content/docs/commands/zarf_package_verify.md index 86819bfb77..b2fb5e9f22 100644 --- a/site/src/content/docs/commands/zarf_package_verify.md +++ b/site/src/content/docs/commands/zarf_package_verify.md @@ -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 diff --git a/src/cmd/package.go b/src/cmd/package.go index fe19f61734..92326f555d 100644 --- a/src/cmd/package.go +++ b/src/cmd/package.go @@ -1848,7 +1848,17 @@ type packageSignOptions struct { ociConcurrency int retries int verify bool - keyless 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 + yes bool } func newPackageSignCommand(v *viper.Viper) *cobra.Command { @@ -1872,7 +1882,16 @@ func newPackageSignCommand(v *viper.Viper) *cobra.Command { cmd.Flags().IntVar(&o.ociConcurrency, "oci-concurrency", v.GetInt(VPkgOCIConcurrency), lang.CmdPackageFlagConcurrency) 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) + cmd.Flags().StringVar(&o.identityToken, "identity-token", "", lang.CmdPackageSignFlagIdentityToken) + cmd.Flags().StringVar(&o.fulcioURL, "fulcio-url", "https://fulcio.sigstore.dev", lang.CmdPackageSignFlagFulcioURL) + cmd.Flags().StringVar(&o.fulcioAuthFlow, "fulcio-auth-flow", "normal", lang.CmdPackageSignFlagFulcioAuthFlow) + cmd.Flags().StringVar(&o.oidcIssuer, "oidc-issuer", "https://oauth2.sigstore.dev/auth", lang.CmdPackageSignFlagOIDCIssuer) + cmd.Flags().StringVar(&o.oidcClientID, "oidc-client-id", "sigstore", lang.CmdPackageSignFlagOIDCClientID) + cmd.Flags().StringVar(&o.rekorURL, "rekor-url", "https://rekor.sigstore.dev", lang.CmdPackageSignFlagRekorURL) + cmd.Flags().BoolVar(&o.tlogUpload, "tlog-upload", false, lang.CmdPackageSignFlagTlogUpload) + cmd.Flags().BoolVar(&o.yes, "yes", false, lang.CmdPackageSignFlagYes) return cmd } @@ -1987,6 +2006,14 @@ func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error { 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.yes // Keyless certs are short-lived (~10 min). Without Rekor or a TSA timestamp // the signature is unverifiable past expiry. Default --tlog-upload=true for @@ -2014,6 +2041,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 { @@ -2032,6 +2066,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", "", lang.CmdPackageVerifyFlagCertificateIdentity) + cmd.Flags().StringVar(&o.certificateIdentityRegexp, "certificate-identity-regexp", "", lang.CmdPackageVerifyFlagCertificateIdentityRegexp) + cmd.Flags().StringVar(&o.certificateOIDCIssuer, "certificate-oidc-issuer", "", lang.CmdPackageVerifyFlagCertificateOIDCIssuer) + cmd.Flags().StringVar(&o.certificateOIDCIssuerRegexp, "certificate-oidc-issuer-regexp", "", lang.CmdPackageVerifyFlagCertificateOIDCIssuerRegexp) + cmd.Flags().StringVar(&o.trustedRoot, "trusted-root", "", lang.CmdPackageVerifyFlagTrustedRoot) + cmd.Flags().BoolVar(&o.insecureIgnoreTlog, "insecure-ignore-tlog", true, lang.CmdPackageVerifyFlagInsecureIgnoreTlog) + return cmd } @@ -2050,8 +2091,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 := utils.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(), diff --git a/src/config/lang/english.go b/src/config/lang/english.go index c53eb5cc17..1ab6296aa0 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -359,6 +359,14 @@ $ zarf package sign zarf-package-demo-amd64-1.0.0.tar.zst --signing-key awskms:/ CmdPackageSignFlagOverwrite = "Overwrite an existing signature if the package is already signed" CmdPackageSignFlagKey = "Public key to verify the existing signature before re-signing (optional)" CmdPackageSignFlagKeyless = "Sign without a private key using Sigstore's keyless flow (Fulcio/OIDC)" + CmdPackageSignFlagIdentityToken = "Pre-acquired OIDC identity token (or path to a file containing one) for non-interactive keyless signing" + CmdPackageSignFlagFulcioURL = "Fulcio certificate authority URL. Override for private Sigstore deployments." + CmdPackageSignFlagFulcioAuthFlow = "Fulcio OAuth flow: normal (browser), device (device code), token, client_credentials" + CmdPackageSignFlagOIDCIssuer = "OIDC issuer URL used to obtain an identity token for keyless signing. Override for private Sigstore deployments." + CmdPackageSignFlagOIDCClientID = "OIDC client ID used when requesting an identity token. Override for private Sigstore deployments." + CmdPackageSignFlagRekorURL = "Rekor transparency log URL. Override for private Sigstore deployments." + CmdPackageSignFlagTlogUpload = "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)." + CmdPackageSignFlagYes = "Skip the interactive confirmation prompt before uploading to the Rekor transparency log." CmdPackageVerifyShort = "Verify the signature and integrity of a Zarf package" 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." @@ -369,7 +377,13 @@ $ zarf package verify zarf-package-demo-amd64-1.0.0.tar.zst --key ./public-key.p # Verify an unsigned package (checksums only) $ zarf package verify zarf-package-demo-amd64-1.0.0.tar.zst ` - CmdPackageVerifyFlagKey = "Public key for signature verification" + CmdPackageVerifyFlagKey = "Public key for signature verification" + CmdPackageVerifyFlagCertificateIdentity = "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" + CmdPackageVerifyFlagCertificateIdentityRegexp = "Regex variant of --certificate-identity" + CmdPackageVerifyFlagCertificateOIDCIssuer = "Required OIDC issuer claim in the signing certificate (keyless verify). Example: https://github.com/login/oauth or https://token.actions.githubusercontent.com" + CmdPackageVerifyFlagCertificateOIDCIssuerRegexp = "Regex variant of --certificate-oidc-issuer" + CmdPackageVerifyFlagTrustedRoot = "Path to a Sigstore TrustedRoot JSON. Falls back to the binary-embedded copy when omitted." + CmdPackageVerifyFlagInsecureIgnoreTlog = "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)." CmdPackagePullShort = "Pulls a Zarf package from a remote registry and save to the local file system" CmdPackagePullExample = ` diff --git a/src/pkg/feature/feature.go b/src/pkg/feature/feature.go index 7fe52ccd63..03a26c0d0c 100644 --- a/src/pkg/feature/feature.go +++ b/src/pkg/feature/feature.go @@ -247,7 +247,7 @@ func init() { { Name: BundleSignature, Description: "Enables Sigstore bundle format signatures. When disabled, only legacy signature format is produced.", - Enabled: false, + Enabled: true, Since: "v0.72.0", Stage: Alpha, }, diff --git a/src/pkg/packager/layout/package.go b/src/pkg/packager/layout/package.go index 9bc4b44309..d8a86e83fe 100644 --- a/src/pkg/packager/layout/package.go +++ b/src/pkg/packager/layout/package.go @@ -22,7 +22,6 @@ import ( "github.com/zarf-dev/zarf/src/internal/pkgcfg" "github.com/zarf-dev/zarf/src/internal/split" "github.com/zarf-dev/zarf/src/pkg/archive" - "github.com/zarf-dev/zarf/src/pkg/feature" "github.com/zarf-dev/zarf/src/pkg/logger" "github.com/zarf-dev/zarf/src/pkg/packager/filters" "github.com/zarf-dev/zarf/src/pkg/utils" @@ -238,12 +237,7 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOpti p.Pkg.Build.ProvenanceFiles = append(p.Pkg.Build.ProvenanceFiles, Signature) } - // Keyless requires bundle format — the cert chain is the only verification material. - bundleEnabled := feature.IsEnabled(feature.BundleSignature) || opts.Keyless - - if bundleEnabled && !slices.Contains(p.Pkg.Build.ProvenanceFiles, Bundle) { - p.Pkg.Build.ProvenanceFiles = append(p.Pkg.Build.ProvenanceFiles, Bundle) - } + p.Pkg.Build.ProvenanceFiles = append(p.Pkg.Build.ProvenanceFiles, Bundle) // Marshal package with signed:true b, err := goyaml.Marshal(p.Pkg) @@ -270,21 +264,15 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOpti actualSignaturePath := filepath.Join(p.dirPath, Signature) actualBundlePath := filepath.Join(p.dirPath, Bundle) signOpts.OutputSignature = actualSignaturePath - if bundleEnabled { - signOpts.BundlePath = actualBundlePath - } else { - signOpts.NewBundleFormat = false - signOpts.BundlePath = "" - } + signOpts.BundlePath = actualBundlePath + err = signOpts.CheckOverwrite(ctx) if err != nil { return err } signOpts.OutputSignature = tmpSignaturePath - if bundleEnabled { - signOpts.BundlePath = tmpBundlePath - } + signOpts.BundlePath = tmpBundlePath // Perform the signing operation on the temp file l.Debug("signing package", "source", tmpZarfYAMLPath, "signature", tmpSignaturePath) @@ -312,11 +300,9 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOpti return fmt.Errorf("failed to move signature after signing: %w", err) } - if bundleEnabled { - err = os.Rename(tmpBundlePath, actualBundlePath) - if err != nil { - return fmt.Errorf("failed to move bundle after signing: %w", err) - } + err = os.Rename(tmpBundlePath, actualBundlePath) + if err != nil { + return fmt.Errorf("failed to move bundle after signing: %w", err) } if opts.Keyless { From 695667661bae32430f2535e13c718d28e5878143 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Mon, 11 May 2026 23:18:26 +0000 Subject: [PATCH 12/25] fix(sign): support for oci signing operations Signed-off-by: Brandt Keller --- src/cmd/package.go | 79 +++++++++++++++++++++----------- src/pkg/packager/publish.go | 25 +++++----- src/pkg/packager/publish_test.go | 11 +++-- 3 files changed, 73 insertions(+), 42 deletions(-) diff --git a/src/cmd/package.go b/src/cmd/package.go index 92326f555d..fb55f1729d 100644 --- a/src/cmd/package.go +++ b/src/cmd/package.go @@ -1746,13 +1746,17 @@ func (o *packagePublishOptions) run(cmd *cobra.Command, args []string) error { err = errors.Join(err, pkgLayout.Cleanup()) }() + publishSignOpts := utils.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) @@ -1934,31 +1938,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(), @@ -2022,12 +2029,30 @@ func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error { signOpts.TlogUpload = true } + 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 { diff --git a/src/pkg/packager/publish.go b/src/pkg/packager/publish.go index c6369c8495..a738f013f1 100644 --- a/src/pkg/packager/publish.go +++ b/src/pkg/packager/publish.go @@ -107,15 +107,18 @@ func PublishFromOCI(ctx context.Context, src registry.Reference, dst registry.Re type PublishPackageOptions struct { // OCIConcurrency configures the amount of layers to push in parallel OCIConcurrency int - // SigningKeyPath points to a signing key on the local disk. - SigningKeyPath string - // SigningKeyPassword holds a password to use the key at SigningKeyPath. - SigningKeyPassword string + // SignBlobOptions holds all signing configuration. Use utils.DefaultSignBlobOptions() as a base. + SignBlobOptions utils.SignBlobOptions // Retries specifies the number of retries to use Retries int types.RemoteOptions // Tag is an optional tag for the OCI reference separate from the package metadata.version Tag string + + // Deprecated: populate SignBlobOptions.Key directly. + SigningKeyPath string + // Deprecated: populate SignBlobOptions.Password directly. + SigningKeyPassword string } // PublishPackage takes a package layout and pushes the package to the given registry. @@ -141,14 +144,14 @@ func PublishPackage(ctx context.Context, pkgLayout *layout.PackageLayout, dst re return registry.Reference{}, fmt.Errorf("package layout must be specified") } - // Sign the package with the provided options - signOpts := utils.DefaultSignBlobOptions() - signOpts.Key = opts.SigningKeyPath - signOpts.Password = opts.SigningKeyPassword - // Publish never re-writes the tarball content - overwrite explicitly - signOpts.Overwrite = true + if opts.SigningKeyPath != "" && opts.SignBlobOptions.Key == "" { + opts.SignBlobOptions.Key = opts.SigningKeyPath + } + if opts.SigningKeyPassword != "" && opts.SignBlobOptions.Password == "" { + opts.SignBlobOptions.Password = opts.SigningKeyPassword + } - if err := pkgLayout.SignPackage(ctx, signOpts); err != nil { + if err := pkgLayout.SignPackage(ctx, opts.SignBlobOptions); err != nil { return registry.Reference{}, fmt.Errorf("unable to sign package: %w", err) } diff --git a/src/pkg/packager/publish_test.go b/src/pkg/packager/publish_test.go index c79927ae3d..6fddbe3e2d 100644 --- a/src/pkg/packager/publish_test.go +++ b/src/pkg/packager/publish_test.go @@ -221,6 +221,10 @@ func TestPublishSkeleton(t *testing.T) { } func TestPublishPackage(t *testing.T) { + signOpts := utils.DefaultSignBlobOptions() + signOpts.Key = filepath.Join("testdata", "publish", "cosign.key") + signOpts.Password = "password" + tt := []struct { name string path string @@ -240,9 +244,8 @@ func TestPublishPackage(t *testing.T) { name: "Sign and publish package", path: filepath.Join("testdata", "load-package", "compressed", "zarf-package-test-amd64-0.0.1.tar.zst"), opts: PublishPackageOptions{ - RemoteOptions: defaultTestRemoteOptions(), - SigningKeyPath: filepath.Join("testdata", "publish", "cosign.key"), - SigningKeyPassword: "password", + RemoteOptions: defaultTestRemoteOptions(), + SignBlobOptions: signOpts, }, publicKeyPath: filepath.Join("testdata", "publish", "cosign.pub"), expectedTag: "0.0.1", @@ -279,7 +282,7 @@ func TestPublishPackage(t *testing.T) { //build data changes when signed layoutActual.Pkg.Build = v1alpha1.ZarfBuildData{} require.Equal(t, layoutExpected.Pkg, layoutActual.Pkg, "Uploaded package is not identical to downloaded package") - if tc.opts.SigningKeyPath != "" { + if tc.opts.SignBlobOptions.Key != "" { require.FileExists(t, filepath.Join(layoutActual.DirPath(), layout.Signature)) } }) From fa088d424dd454dda170e492e71e2d5e5a9c5911 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Tue, 12 May 2026 03:17:35 +0000 Subject: [PATCH 13/25] fix(test): update error message for verification material Signed-off-by: Brandt Keller --- src/test/e2e/12_package_signing_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/e2e/12_package_signing_test.go b/src/test/e2e/12_package_signing_test.go index 11d5eed835..48252d3274 100644 --- a/src/test/e2e/12_package_signing_test.go +++ b/src/test/e2e/12_package_signing_test.go @@ -84,6 +84,6 @@ func TestPackageSigning(t *testing.T) { // try to verify with key but package is not signed (should fail) _, stdErr, err = e2e.Zarf(t, "package", "verify", testPath, "--key", filepath.Join("src", "test", "packages", "zarf-test.pub")) require.Error(t, err) - require.Contains(t, stdErr, "a key was provided but the package is not signed") + require.Contains(t, stdErr, "verification material was provided but the package is not signed") }) } From 4f49439cb7252048cb9cbc60a40a83ac2f1ccb13 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Tue, 12 May 2026 15:43:08 +0000 Subject: [PATCH 14/25] fix(cosign): add timeout to verify Signed-off-by: Brandt Keller --- src/pkg/utils/cosign.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pkg/utils/cosign.go b/src/pkg/utils/cosign.go index ce167d2604..0fb31ec00c 100644 --- a/src/pkg/utils/cosign.go +++ b/src/pkg/utils/cosign.go @@ -267,6 +267,12 @@ func CosignVerifyBlobWithOptions(ctx context.Context, blobPath string, opts Veri "bundlePath", opts.BundlePath, "offline", opts.CommonVerifyOptions.Offline) + if opts.Timeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, opts.Timeout) + defer cancel() + } + if err := cmd.Exec(ctx, blobPath); err != nil { return err } From c49d85d45299b8695b32971befe38c488117cc92 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Tue, 12 May 2026 17:00:16 +0000 Subject: [PATCH 15/25] fix(requirement): add version requirement for bundle signing verification Signed-off-by: Brandt Keller --- src/pkg/packager/layout/package.go | 38 +++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/pkg/packager/layout/package.go b/src/pkg/packager/layout/package.go index d8a86e83fe..19fb836cd1 100644 --- a/src/pkg/packager/layout/package.go +++ b/src/pkg/packager/layout/package.go @@ -22,6 +22,7 @@ import ( "github.com/zarf-dev/zarf/src/internal/pkgcfg" "github.com/zarf-dev/zarf/src/internal/split" "github.com/zarf-dev/zarf/src/pkg/archive" + "github.com/zarf-dev/zarf/src/pkg/feature" "github.com/zarf-dev/zarf/src/pkg/logger" "github.com/zarf-dev/zarf/src/pkg/packager/filters" "github.com/zarf-dev/zarf/src/pkg/utils" @@ -222,12 +223,17 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOpti tmpSignaturePath := filepath.Join(tmpDir, Signature) tmpBundlePath := filepath.Join(tmpDir, Bundle) - // Update in-memory state to signed:true + // Update in-memory state signed := true p.Pkg.Build.Signed = &signed - // Save original provenance files for rollback + // Save original provenance files and version requirements for rollback originalProvenanceFiles := slices.Clone(p.Pkg.Build.ProvenanceFiles) + originalVersionRequirements := slices.Clone(p.Pkg.Build.VersionRequirements) + + // Keyless signatures require bundle format — the cert chain cannot be stored in the + // legacy .sig file. For key-based signing, respect the BundleSignature feature flag. + bundleEnabled := feature.IsEnabled(feature.BundleSignature) || opts.Keyless // Append signature files to the provenance files list. // These are created after checksum generation and cannot be in checksums.txt. @@ -236,8 +242,13 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOpti if !slices.Contains(p.Pkg.Build.ProvenanceFiles, Signature) { p.Pkg.Build.ProvenanceFiles = append(p.Pkg.Build.ProvenanceFiles, Signature) } - - p.Pkg.Build.ProvenanceFiles = append(p.Pkg.Build.ProvenanceFiles, Bundle) + if bundleEnabled && !slices.Contains(p.Pkg.Build.ProvenanceFiles, Bundle) { + p.Pkg.Build.ProvenanceFiles = append(p.Pkg.Build.ProvenanceFiles, Bundle) + p.Pkg.Build.VersionRequirements = append(p.Pkg.Build.VersionRequirements, v1alpha1.VersionRequirement{ + Version: "v0.71.0", + Reason: "This package contains a bundle format signature which requires Zarf v0.71.0 or later", + }) + } // Marshal package with signed:true b, err := goyaml.Marshal(p.Pkg) @@ -245,6 +256,7 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOpti // Rollback p.Pkg.Build.Signed = originalSigned p.Pkg.Build.ProvenanceFiles = originalProvenanceFiles + p.Pkg.Build.VersionRequirements = originalVersionRequirements return fmt.Errorf("failed to marshal package for signing: %w", err) } @@ -254,17 +266,21 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOpti // Rollback p.Pkg.Build.Signed = originalSigned p.Pkg.Build.ProvenanceFiles = originalProvenanceFiles + p.Pkg.Build.VersionRequirements = originalVersionRequirements return fmt.Errorf("failed to write temp %s: %w", ZarfYAML, err) } // Configure signing to write to temp directory signOpts := opts + signOpts.NewBundleFormat = bundleEnabled // Validate outputs before setting temporary paths actualSignaturePath := filepath.Join(p.dirPath, Signature) actualBundlePath := filepath.Join(p.dirPath, Bundle) signOpts.OutputSignature = actualSignaturePath - signOpts.BundlePath = actualBundlePath + if bundleEnabled { + signOpts.BundlePath = actualBundlePath + } err = signOpts.CheckOverwrite(ctx) if err != nil { @@ -272,7 +288,9 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOpti } signOpts.OutputSignature = tmpSignaturePath - signOpts.BundlePath = tmpBundlePath + if bundleEnabled { + signOpts.BundlePath = tmpBundlePath + } // Perform the signing operation on the temp file l.Debug("signing package", "source", tmpZarfYAMLPath, "signature", tmpSignaturePath) @@ -281,6 +299,7 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOpti // Rollback in-memory state p.Pkg.Build.Signed = originalSigned p.Pkg.Build.ProvenanceFiles = originalProvenanceFiles + p.Pkg.Build.VersionRequirements = originalVersionRequirements return fmt.Errorf("failed to sign package: %w", err) } @@ -300,9 +319,10 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOpti return fmt.Errorf("failed to move signature after signing: %w", err) } - err = os.Rename(tmpBundlePath, actualBundlePath) - if err != nil { - return fmt.Errorf("failed to move bundle after signing: %w", err) + if bundleEnabled { + if err = os.Rename(tmpBundlePath, actualBundlePath); err != nil { + return fmt.Errorf("failed to move bundle after signing: %w", err) + } } if opts.Keyless { From aed485c2eac51d3d77301e7e21f7f88b5a1069c8 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Tue, 12 May 2026 22:59:47 +0000 Subject: [PATCH 16/25] feat(sign): add signature build metadata for future utility Signed-off-by: Brandt Keller --- src/api/v1alpha1/package.go | 13 +++++++++++++ src/pkg/packager/layout/package.go | 24 +++++++++++++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/api/v1alpha1/package.go b/src/api/v1alpha1/package.go index d77cce55f3..6a1350b698 100644 --- a/src/api/v1alpha1/package.go +++ b/src/api/v1alpha1/package.go @@ -267,6 +267,8 @@ type ZarfBuildData struct { Flavor string `json:"flavor,omitempty" jsonschema:"pattern=^[^/\\\\]*$"` // Whether this package was signed Signed *bool `json:"signed,omitempty"` + // Metadata about the signature applied to this package, populated after signing. + Signature *ZarfSignatureMetadata `json:"signature,omitempty"` // Requirements for specific package operations. VersionRequirements []VersionRequirement `json:"versionRequirements,omitempty"` // ProvenanceFiles lists files present in the package that are not included in checksums.txt. @@ -290,3 +292,14 @@ type VersionRequirement struct { // Explanation for why this version is required Reason string `json:"reason,omitempty"` } + +// ZarfSignatureMetadata records how a package was signed. +// Populated by SignPackage and included in the signed zarf.yaml. +// Fields are limited to values known before signing begins; identity and issuer +// (available only after the Fulcio OIDC exchange) are stored in the Sigstore bundle. +type ZarfSignatureMetadata struct { + // Method is the signing method used. + Method string `json:"method" jsonschema:"enum=keyless,enum=key"` + // TlogUploaded indicates whether the signature was uploaded to the Rekor transparency log. + TlogUploaded bool `json:"tlogUploaded"` +} diff --git a/src/pkg/packager/layout/package.go b/src/pkg/packager/layout/package.go index 19fb836cd1..2430ac138b 100644 --- a/src/pkg/packager/layout/package.go +++ b/src/pkg/packager/layout/package.go @@ -227,9 +227,10 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOpti signed := true p.Pkg.Build.Signed = &signed - // Save original provenance files and version requirements for rollback + // Save original fields for rollback originalProvenanceFiles := slices.Clone(p.Pkg.Build.ProvenanceFiles) originalVersionRequirements := slices.Clone(p.Pkg.Build.VersionRequirements) + originalSignature := p.Pkg.Build.Signature // Keyless signatures require bundle format — the cert chain cannot be stored in the // legacy .sig file. For key-based signing, respect the BundleSignature feature flag. @@ -250,23 +251,36 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOpti }) } - // Marshal package with signed:true + // Set signature metadata before marshalling so it is included in the signed zarf.yaml. + // Identity and issuer are intentionally omitted: they are only knowable after the Fulcio + // OIDC exchange at sign time, and are stored in the Sigstore bundle provenance file. + sigMeta := &v1alpha1.ZarfSignatureMetadata{ + TlogUploaded: opts.TlogUpload, + } + if opts.Keyless { + sigMeta.Method = "keyless" + } else { + sigMeta.Method = "key" + } + p.Pkg.Build.Signature = sigMeta + + // Marshal package with signed:true and signature metadata b, err := goyaml.Marshal(p.Pkg) if err != nil { - // Rollback p.Pkg.Build.Signed = originalSigned p.Pkg.Build.ProvenanceFiles = originalProvenanceFiles p.Pkg.Build.VersionRequirements = originalVersionRequirements + p.Pkg.Build.Signature = originalSignature return fmt.Errorf("failed to marshal package for signing: %w", err) } // Write to temporary file err = os.WriteFile(tmpZarfYAMLPath, b, helpers.ReadWriteUser) if err != nil { - // Rollback p.Pkg.Build.Signed = originalSigned p.Pkg.Build.ProvenanceFiles = originalProvenanceFiles p.Pkg.Build.VersionRequirements = originalVersionRequirements + p.Pkg.Build.Signature = originalSignature return fmt.Errorf("failed to write temp %s: %w", ZarfYAML, err) } @@ -296,10 +310,10 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOpti l.Debug("signing package", "source", tmpZarfYAMLPath, "signature", tmpSignaturePath) _, err = utils.CosignSignBlobWithOptions(ctx, tmpZarfYAMLPath, signOpts) if err != nil { - // Rollback in-memory state p.Pkg.Build.Signed = originalSigned p.Pkg.Build.ProvenanceFiles = originalProvenanceFiles p.Pkg.Build.VersionRequirements = originalVersionRequirements + p.Pkg.Build.Signature = originalSignature return fmt.Errorf("failed to sign package: %w", err) } From 19259d75c1d15146c6ad8366921ba67d4dc32dce Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Wed, 13 May 2026 01:20:52 +0000 Subject: [PATCH 17/25] fix(bundle): implement generic bundle parsing Signed-off-by: Brandt Keller --- src/pkg/packager/layout/package.go | 15 ++ src/pkg/packager/layout/package_test.go | 2 +- src/pkg/utils/bundle.go | 74 ++++++++ src/pkg/utils/bundle_test.go | 226 ++++++++++++++++++++++++ src/pkg/utils/keyless.go | 92 ---------- src/pkg/utils/keyless_test.go | 164 ----------------- src/test/e2e/12_package_signing_test.go | 2 +- 7 files changed, 317 insertions(+), 258 deletions(-) create mode 100644 src/pkg/utils/bundle.go create mode 100644 src/pkg/utils/bundle_test.go delete mode 100644 src/pkg/utils/keyless.go delete mode 100644 src/pkg/utils/keyless_test.go diff --git a/src/pkg/packager/layout/package.go b/src/pkg/packager/layout/package.go index 2430ac138b..cd27057ebd 100644 --- a/src/pkg/packager/layout/package.go +++ b/src/pkg/packager/layout/package.go @@ -386,6 +386,21 @@ func (p *PackageLayout) VerifyPackageSignature(ctx context.Context, opts utils.V return errors.New("package is not signed - verification cannot be performed") } + // Early validation using build.signature: fail fast with a method-specific message + // before cosign emits a generic error. Nil for packages predating this field. + if sig := p.Pkg.Build.Signature; sig != nil { + switch sig.Method { + case "keyless": + if !hasKeylessIdentity && !hasCert { + return errors.New("package was signed with keyless method; provide --certificate-identity + --certificate-oidc-issuer to verify") + } + case "key": + if !hasKey && !hasCert { + return errors.New("package was signed with a key; provide --key to verify") + } + } + } + if !hasVerificationMaterial { return errors.New("package is signed but no verification material was provided (--key, --certificate-identity + --certificate-oidc-issuer, or --certificate)") } diff --git a/src/pkg/packager/layout/package_test.go b/src/pkg/packager/layout/package_test.go index f34952590b..367bbe1c30 100644 --- a/src/pkg/packager/layout/package_test.go +++ b/src/pkg/packager/layout/package_test.go @@ -872,7 +872,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { verifyOpts.Key = "" // Empty key err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) - require.EqualError(t, err, "package is signed but no verification material was provided (--key, --certificate-identity + --certificate-oidc-issuer, or --certificate)") + require.EqualError(t, err, "package was signed with a key; provide --key to verify") }) t.Run("verification fails when signature is corrupted", func(t *testing.T) { diff --git a/src/pkg/utils/bundle.go b/src/pkg/utils/bundle.go new file mode 100644 index 0000000000..c5510a8a2a --- /dev/null +++ b/src/pkg/utils/bundle.go @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package utils provides generic utility functions. +package utils + +import ( + "crypto/x509" + "encoding/asn1" + "errors" + "fmt" + + "github.com/sigstore/sigstore-go/pkg/bundle" +) + +// Sigstore custom OIDs for the OIDC issuer claim embedded in Fulcio certs. +// - sigstoreIssuerOIDLegacy: raw string in extension value +// - sigstoreIssuerOIDV2: DER-encoded UTF8String, used by Fulcio v1+ +var ( + sigstoreIssuerOIDLegacy = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1} + sigstoreIssuerOIDV2 = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 8} +) + +// extractIdentityFromCert extracts the signer identity (cert SAN) and OIDC issuer +// from a Fulcio-issued X.509 certificate using Sigstore OID extensions. +// SAN priority: email > URI > DNS. OID priority: V2 > legacy. +func extractIdentityFromCert(cert *x509.Certificate) (identity, issuer string) { + switch { + case len(cert.EmailAddresses) > 0: + identity = cert.EmailAddresses[0] + case len(cert.URIs) > 0: + identity = cert.URIs[0].String() + case len(cert.DNSNames) > 0: + identity = cert.DNSNames[0] + } + + for _, ext := range cert.Extensions { + switch { + case ext.Id.Equal(sigstoreIssuerOIDV2): + var s string + if _, decErr := asn1.Unmarshal(ext.Value, &s); decErr == nil { + issuer = s + return identity, issuer + } + case ext.Id.Equal(sigstoreIssuerOIDLegacy) && issuer == "": + issuer = string(ext.Value) + } + } + + return identity, issuer +} + +// ReadKeylessIdentityFromBundle parses a Sigstore bundle file and returns the +// signer identity (cert SAN) and OIDC issuer claim. Returns an error if the +// bundle does not contain a certificate (i.e. is not a keyless signature). +func ReadKeylessIdentityFromBundle(bundlePath string) (identity, issuer string, err error) { + b, err := bundle.LoadJSONFromPath(bundlePath) + if err != nil { + return "", "", fmt.Errorf("loading bundle: %w", err) + } + + vc, err := b.VerificationContent() + if err != nil { + return "", "", fmt.Errorf("reading verification content: %w", err) + } + + certHolder, ok := vc.(*bundle.Certificate) + if !ok { + return "", "", errors.New("bundle does not contain a certificate (not a keyless signature)") + } + + identity, issuer = extractIdentityFromCert(certHolder.Certificate()) + return identity, issuer, nil +} diff --git a/src/pkg/utils/bundle_test.go b/src/pkg/utils/bundle_test.go new file mode 100644 index 0000000000..c52e89f990 --- /dev/null +++ b/src/pkg/utils/bundle_test.go @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package utils + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/base64" + "encoding/json" + "math/big" + "net/url" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// makeCert creates a self-signed test certificate from the given template. +func makeCert(t *testing.T, tmpl *x509.Certificate) *x509.Certificate { + t.Helper() + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + tmpl.SerialNumber = big.NewInt(1) + tmpl.NotBefore = time.Now() + tmpl.NotAfter = time.Now().Add(time.Hour) + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv) + require.NoError(t, err) + cert, err := x509.ParseCertificate(der) + require.NoError(t, err) + return cert +} + +// writeBundleFixture writes a minimal sigstore-go-valid bundle containing cert. +// shape: "x509CertificateChain" or "certificate". +func writeBundleFixture(t *testing.T, cert *x509.Certificate, shape string) string { + t.Helper() + var verMaterial map[string]any + switch shape { + case "certificate": + verMaterial = map[string]any{ + "certificate": map[string]any{ + "rawBytes": base64.StdEncoding.EncodeToString(cert.Raw), + }, + } + case "x509CertificateChain": + verMaterial = map[string]any{ + "x509CertificateChain": map[string]any{ + "certificates": []map[string]any{ + {"rawBytes": base64.StdEncoding.EncodeToString(cert.Raw)}, + }, + }, + } + default: + t.Fatalf("unknown bundle shape: %s", shape) + } + b := map[string]any{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", + "verificationMaterial": verMaterial, + "messageSignature": map[string]any{ + "messageDigest": map[string]any{ + "algorithm": "SHA2_256", + "digest": base64.StdEncoding.EncodeToString(make([]byte, 32)), + }, + "signature": base64.StdEncoding.EncodeToString([]byte("dummy")), + }, + } + data, err := json.Marshal(b) + require.NoError(t, err) + path := filepath.Join(t.TempDir(), "zarf.bundle.json") + require.NoError(t, os.WriteFile(path, data, 0o600)) + return path +} + +// writeKeyBasedBundleFixture writes a minimal sigstore-go-valid bundle using a +// public key hint (not a certificate), representing a key-based signature. +func writeKeyBasedBundleFixture(t *testing.T) string { + t.Helper() + b := map[string]any{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", + "verificationMaterial": map[string]any{ + "publicKey": map[string]any{"hint": "test-key-id"}, + }, + "messageSignature": map[string]any{ + "messageDigest": map[string]any{ + "algorithm": "SHA2_256", + "digest": base64.StdEncoding.EncodeToString(make([]byte, 32)), + }, + "signature": base64.StdEncoding.EncodeToString([]byte("dummy")), + }, + } + data, err := json.Marshal(b) + require.NoError(t, err) + path := filepath.Join(t.TempDir(), "zarf.bundle.json") + require.NoError(t, os.WriteFile(path, data, 0o600)) + return path +} + +func TestExtractIdentityFromCert(t *testing.T) { + t.Parallel() + + t.Run("email SAN with V2 issuer OID", func(t *testing.T) { + t.Parallel() + issuerVal, err := asn1.Marshal("https://oauth2.sigstore.dev/auth") + require.NoError(t, err) + cert := makeCert(t, &x509.Certificate{ + Subject: pkix.Name{CommonName: "ephemeral"}, + EmailAddresses: []string{"signer@example.com"}, + ExtraExtensions: []pkix.Extension{ + {Id: sigstoreIssuerOIDV2, Value: issuerVal}, + }, + }) + + identity, issuer := extractIdentityFromCert(cert) + require.Equal(t, "signer@example.com", identity) + require.Equal(t, "https://oauth2.sigstore.dev/auth", issuer) + }) + + t.Run("URI SAN with legacy issuer OID", func(t *testing.T) { + t.Parallel() + ghaURI, err := url.Parse("https://github.com/example/repo/.github/workflows/release.yml@refs/heads/main") + require.NoError(t, err) + cert := makeCert(t, &x509.Certificate{ + Subject: pkix.Name{CommonName: "ephemeral"}, + URIs: []*url.URL{ghaURI}, + ExtraExtensions: []pkix.Extension{ + {Id: sigstoreIssuerOIDLegacy, Value: []byte("https://token.actions.githubusercontent.com")}, + }, + }) + + identity, issuer := extractIdentityFromCert(cert) + require.Equal(t, ghaURI.String(), identity) + require.Equal(t, "https://token.actions.githubusercontent.com", issuer) + }) + + t.Run("V2 OID takes precedence over legacy when both present", func(t *testing.T) { + t.Parallel() + v2Val, err := asn1.Marshal("https://v2-issuer.example.com") + require.NoError(t, err) + cert := makeCert(t, &x509.Certificate{ + Subject: pkix.Name{CommonName: "ephemeral"}, + EmailAddresses: []string{"signer@example.com"}, + ExtraExtensions: []pkix.Extension{ + {Id: sigstoreIssuerOIDLegacy, Value: []byte("https://legacy.example.com")}, + {Id: sigstoreIssuerOIDV2, Value: v2Val}, + }, + }) + + _, issuer := extractIdentityFromCert(cert) + require.Equal(t, "https://v2-issuer.example.com", issuer) + }) + + t.Run("DNS SAN used when no email or URI", func(t *testing.T) { + t.Parallel() + cert := makeCert(t, &x509.Certificate{ + Subject: pkix.Name{CommonName: "ephemeral"}, + DNSNames: []string{"host.example.com"}, + }) + + identity, _ := extractIdentityFromCert(cert) + require.Equal(t, "host.example.com", identity) + }) +} + +func TestReadKeylessIdentityFromBundle(t *testing.T) { + t.Parallel() + + t.Run("x509CertificateChain bundle returns identity and issuer", func(t *testing.T) { + t.Parallel() + issuerVal, err := asn1.Marshal("https://token.actions.githubusercontent.com") + require.NoError(t, err) + ghaURI, err := url.Parse("https://github.com/example/repo/.github/workflows/release.yml@refs/heads/main") + require.NoError(t, err) + cert := makeCert(t, &x509.Certificate{ + Subject: pkix.Name{CommonName: "ephemeral"}, + URIs: []*url.URL{ghaURI}, + ExtraExtensions: []pkix.Extension{ + {Id: sigstoreIssuerOIDV2, Value: issuerVal}, + }, + }) + path := writeBundleFixture(t, cert, "x509CertificateChain") + + identity, issuer, err := ReadKeylessIdentityFromBundle(path) + require.NoError(t, err) + require.Equal(t, ghaURI.String(), identity) + require.Equal(t, "https://token.actions.githubusercontent.com", issuer) + }) + + t.Run("certificate bundle returns identity and issuer", func(t *testing.T) { + t.Parallel() + issuerVal, err := asn1.Marshal("https://github.com/login/oauth") + require.NoError(t, err) + cert := makeCert(t, &x509.Certificate{ + Subject: pkix.Name{CommonName: "ephemeral"}, + EmailAddresses: []string{"signer@example.com"}, + ExtraExtensions: []pkix.Extension{ + {Id: sigstoreIssuerOIDV2, Value: issuerVal}, + }, + }) + path := writeBundleFixture(t, cert, "certificate") + + identity, issuer, err := ReadKeylessIdentityFromBundle(path) + require.NoError(t, err) + require.Equal(t, "signer@example.com", identity) + require.Equal(t, "https://github.com/login/oauth", issuer) + }) + + t.Run("missing bundle file errors", func(t *testing.T) { + t.Parallel() + _, _, err := ReadKeylessIdentityFromBundle(filepath.Join(t.TempDir(), "nonexistent.json")) + require.Error(t, err) + }) + + t.Run("key-based bundle errors", func(t *testing.T) { + t.Parallel() + path := writeKeyBasedBundleFixture(t) + _, _, err := ReadKeylessIdentityFromBundle(path) + require.ErrorContains(t, err, "not a keyless signature") + }) +} diff --git a/src/pkg/utils/keyless.go b/src/pkg/utils/keyless.go deleted file mode 100644 index c9506c1f1f..0000000000 --- a/src/pkg/utils/keyless.go +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2021-Present The Zarf Authors - -package utils - -import ( - "crypto/x509" - "encoding/asn1" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "os" -) - -// Sigstore custom OIDs for the OIDC issuer claim embedded in Fulcio certs. -// - sigstoreIssuerOIDLegacy: raw string in extension value -// - sigstoreIssuerOIDV2: DER-encoded UTF8String, used by Fulcio v1+ -var ( - sigstoreIssuerOIDLegacy = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1} - sigstoreIssuerOIDV2 = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 8} -) - -// ReadKeylessIdentityFromBundle parses a Sigstore bundle file and returns the -// signer identity (cert SAN) and OIDC issuer claim. Used at sign time so users -// learn the identity their keyless flow resolved to. -func ReadKeylessIdentityFromBundle(bundlePath string) (identity, issuer string, err error) { - data, err := os.ReadFile(bundlePath) - if err != nil { - return "", "", err - } - - // Sigstore bundle VerificationMaterial is a oneof: newer keyless bundles use - // "certificate" (single Fulcio cert), legacy/chain variants use - // "x509CertificateChain.certificates[]". Try both. - var b struct { - VerificationMaterial struct { - Certificate struct { - RawBytes string `json:"rawBytes"` - } `json:"certificate"` - X509CertificateChain struct { - Certificates []struct { - RawBytes string `json:"rawBytes"` - } `json:"certificates"` - } `json:"x509CertificateChain"` - } `json:"verificationMaterial"` - } - if err := json.Unmarshal(data, &b); err != nil { - return "", "", fmt.Errorf("parsing bundle JSON: %w", err) - } - - rawBytes := b.VerificationMaterial.Certificate.RawBytes - if rawBytes == "" && len(b.VerificationMaterial.X509CertificateChain.Certificates) > 0 { - rawBytes = b.VerificationMaterial.X509CertificateChain.Certificates[0].RawBytes - } - if rawBytes == "" { - return "", "", errors.New("bundle contains no certificate") - } - - der, err := base64.StdEncoding.DecodeString(rawBytes) - if err != nil { - return "", "", fmt.Errorf("decoding cert: %w", err) - } - cert, err := x509.ParseCertificate(der) - if err != nil { - return "", "", fmt.Errorf("parsing cert: %w", err) - } - - switch { - case len(cert.EmailAddresses) > 0: - identity = cert.EmailAddresses[0] - case len(cert.URIs) > 0: - identity = cert.URIs[0].String() - case len(cert.DNSNames) > 0: - identity = cert.DNSNames[0] - } - - for _, ext := range cert.Extensions { - switch { - case ext.Id.Equal(sigstoreIssuerOIDV2): - var s string - if _, decErr := asn1.Unmarshal(ext.Value, &s); decErr == nil { - issuer = s - return identity, issuer, nil - } - case ext.Id.Equal(sigstoreIssuerOIDLegacy) && issuer == "": - issuer = string(ext.Value) - } - } - - return identity, issuer, nil -} diff --git a/src/pkg/utils/keyless_test.go b/src/pkg/utils/keyless_test.go deleted file mode 100644 index 92f80eafe9..0000000000 --- a/src/pkg/utils/keyless_test.go +++ /dev/null @@ -1,164 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2021-Present The Zarf Authors - -package utils - -import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/x509" - "crypto/x509/pkix" - "encoding/asn1" - "encoding/base64" - "encoding/json" - "math/big" - "net/url" - "os" - "path/filepath" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -func writeBundleFixture(t *testing.T, certDER []byte) string { - t.Helper() - return writeBundleFixtureWithShape(t, certDER, "x509CertificateChain") -} - -// shape: "x509CertificateChain" (chain variant) or "certificate" (singular Fulcio cert). -func writeBundleFixtureWithShape(t *testing.T, certDER []byte, shape string) string { - t.Helper() - var verificationMaterial map[string]any - switch shape { - case "certificate": - verificationMaterial = map[string]any{ - "certificate": map[string]any{ - "rawBytes": base64.StdEncoding.EncodeToString(certDER), - }, - } - case "x509CertificateChain": - verificationMaterial = map[string]any{ - "x509CertificateChain": map[string]any{ - "certificates": []map[string]any{ - {"rawBytes": base64.StdEncoding.EncodeToString(certDER)}, - }, - }, - } - default: - t.Fatalf("unknown bundle shape: %s", shape) - } - bundle := map[string]any{"verificationMaterial": verificationMaterial} - data, err := json.Marshal(bundle) - require.NoError(t, err) - path := filepath.Join(t.TempDir(), "zarf.bundle.sig") - require.NoError(t, os.WriteFile(path, data, 0o600)) - return path -} - -func makeCert(t *testing.T, tmpl *x509.Certificate) []byte { - t.Helper() - priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) - tmpl.SerialNumber = big.NewInt(1) - tmpl.NotBefore = time.Now() - tmpl.NotAfter = time.Now().Add(time.Hour) - der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv) - require.NoError(t, err) - return der -} - -func TestReadKeylessIdentityFromBundle(t *testing.T) { - t.Parallel() - - t.Run("email SAN with V2 issuer extension", func(t *testing.T) { - t.Parallel() - issuerVal, err := asn1.Marshal("https://oauth2.sigstore.dev/auth") - require.NoError(t, err) - der := makeCert(t, &x509.Certificate{ - Subject: pkix.Name{CommonName: "ephemeral"}, - EmailAddresses: []string{"signer@example.com"}, - ExtraExtensions: []pkix.Extension{ - {Id: sigstoreIssuerOIDV2, Value: issuerVal}, - }, - }) - path := writeBundleFixture(t, der) - - identity, issuer, err := ReadKeylessIdentityFromBundle(path) - require.NoError(t, err) - require.Equal(t, "signer@example.com", identity) - require.Equal(t, "https://oauth2.sigstore.dev/auth", issuer) - }) - - t.Run("URI SAN with legacy issuer extension", func(t *testing.T) { - t.Parallel() - ghaURI, err := url.Parse("https://github.com/example/repo/.github/workflows/release.yml@refs/heads/main") - require.NoError(t, err) - der := makeCert(t, &x509.Certificate{ - Subject: pkix.Name{CommonName: "ephemeral"}, - URIs: []*url.URL{ghaURI}, - ExtraExtensions: []pkix.Extension{ - {Id: sigstoreIssuerOIDLegacy, Value: []byte("https://token.actions.githubusercontent.com")}, - }, - }) - path := writeBundleFixture(t, der) - - identity, issuer, err := ReadKeylessIdentityFromBundle(path) - require.NoError(t, err) - require.Equal(t, ghaURI.String(), identity) - require.Equal(t, "https://token.actions.githubusercontent.com", issuer) - }) - - t.Run("singular certificate variant (newer keyless bundle)", func(t *testing.T) { - t.Parallel() - issuerVal, err := asn1.Marshal("https://github.com/login/oauth") - require.NoError(t, err) - der := makeCert(t, &x509.Certificate{ - Subject: pkix.Name{CommonName: "ephemeral"}, - EmailAddresses: []string{"signer@example.com"}, - ExtraExtensions: []pkix.Extension{ - {Id: sigstoreIssuerOIDV2, Value: issuerVal}, - }, - }) - path := writeBundleFixtureWithShape(t, der, "certificate") - - identity, issuer, err := ReadKeylessIdentityFromBundle(path) - require.NoError(t, err) - require.Equal(t, "signer@example.com", identity) - require.Equal(t, "https://github.com/login/oauth", issuer) - }) - - t.Run("V2 takes precedence over legacy when both are present", func(t *testing.T) { - t.Parallel() - v2Val, err := asn1.Marshal("https://v2-issuer.example.com") - require.NoError(t, err) - der := makeCert(t, &x509.Certificate{ - Subject: pkix.Name{CommonName: "ephemeral"}, - EmailAddresses: []string{"signer@example.com"}, - ExtraExtensions: []pkix.Extension{ - {Id: sigstoreIssuerOIDLegacy, Value: []byte("https://legacy.example.com")}, - {Id: sigstoreIssuerOIDV2, Value: v2Val}, - }, - }) - path := writeBundleFixture(t, der) - - _, issuer, err := ReadKeylessIdentityFromBundle(path) - require.NoError(t, err) - require.Equal(t, "https://v2-issuer.example.com", issuer) - }) - - t.Run("missing bundle file errors", func(t *testing.T) { - t.Parallel() - _, _, err := ReadKeylessIdentityFromBundle(filepath.Join(t.TempDir(), "nonexistent.json")) - require.Error(t, err) - }) - - t.Run("bundle without certificate errors", func(t *testing.T) { - t.Parallel() - path := filepath.Join(t.TempDir(), "empty.json") - require.NoError(t, os.WriteFile(path, []byte(`{"verificationMaterial":{}}`), 0o600)) - _, _, err := ReadKeylessIdentityFromBundle(path) - require.ErrorContains(t, err, "no certificate") - }) -} diff --git a/src/test/e2e/12_package_signing_test.go b/src/test/e2e/12_package_signing_test.go index 48252d3274..4399801974 100644 --- a/src/test/e2e/12_package_signing_test.go +++ b/src/test/e2e/12_package_signing_test.go @@ -69,7 +69,7 @@ func TestPackageSigning(t *testing.T) { // try to verify without key (should fail) _, stdErr, err = e2e.Zarf(t, "package", "verify", testPath) require.Error(t, err) - require.Contains(t, stdErr, "no verification material was provided") + require.Contains(t, stdErr, "package was signed with a key; provide --key to verify") }) t.Run("Verify with key but unsigned package fails", func(t *testing.T) { From efb1d680620e7faf38ffc97dd7707ebc7bbfd78a Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Wed, 13 May 2026 01:29:59 +0000 Subject: [PATCH 18/25] chore: generate the updated schema Signed-off-by: Brandt Keller --- src/pkg/schema/zarf-v1alpha1-schema.json | 30 ++++++++++++++++++++++++ zarf.schema.json | 30 ++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/src/pkg/schema/zarf-v1alpha1-schema.json b/src/pkg/schema/zarf-v1alpha1-schema.json index 7e7b2ea428..239cd5c946 100644 --- a/src/pkg/schema/zarf-v1alpha1-schema.json +++ b/src/pkg/schema/zarf-v1alpha1-schema.json @@ -362,6 +362,10 @@ "description": "Any registry domains that were overridden on package create when pulling images.", "type": "object" }, + "signature": { + "$ref": "#/$defs/ZarfSignatureMetadata", + "description": "Metadata about the signature applied to this package, populated after signing." + }, "signed": { "description": "Whether this package was signed", "type": "boolean" @@ -1257,6 +1261,32 @@ ], "type": "object" }, + "ZarfSignatureMetadata": { + "additionalProperties": false, + "description": "ZarfSignatureMetadata records how a package was signed.", + "patternProperties": { + "^x-": {} + }, + "properties": { + "method": { + "description": "Method is the signing method used.", + "enum": [ + "keyless", + "key" + ], + "type": "string" + }, + "tlogUploaded": { + "description": "TlogUploaded indicates whether the signature was uploaded to the Rekor transparency log.", + "type": "boolean" + } + }, + "required": [ + "method", + "tlogUploaded" + ], + "type": "object" + }, "ZarfValues": { "additionalProperties": false, "description": "ZarfValues imports package-level values files and validation.", diff --git a/zarf.schema.json b/zarf.schema.json index 7e7b2ea428..239cd5c946 100644 --- a/zarf.schema.json +++ b/zarf.schema.json @@ -362,6 +362,10 @@ "description": "Any registry domains that were overridden on package create when pulling images.", "type": "object" }, + "signature": { + "$ref": "#/$defs/ZarfSignatureMetadata", + "description": "Metadata about the signature applied to this package, populated after signing." + }, "signed": { "description": "Whether this package was signed", "type": "boolean" @@ -1257,6 +1261,32 @@ ], "type": "object" }, + "ZarfSignatureMetadata": { + "additionalProperties": false, + "description": "ZarfSignatureMetadata records how a package was signed.", + "patternProperties": { + "^x-": {} + }, + "properties": { + "method": { + "description": "Method is the signing method used.", + "enum": [ + "keyless", + "key" + ], + "type": "string" + }, + "tlogUploaded": { + "description": "TlogUploaded indicates whether the signature was uploaded to the Rekor transparency log.", + "type": "boolean" + } + }, + "required": [ + "method", + "tlogUploaded" + ], + "type": "object" + }, "ZarfValues": { "additionalProperties": false, "description": "ZarfValues imports package-level values files and validation.", From d73dcdec849a59164fd45b79e96fa60e88ead37b Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Wed, 13 May 2026 01:52:06 +0000 Subject: [PATCH 19/25] fix(test): update test for error output Signed-off-by: Brandt Keller --- src/test/e2e/31_checksum_and_signature_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/e2e/31_checksum_and_signature_test.go b/src/test/e2e/31_checksum_and_signature_test.go index be12db76df..f67141d96e 100644 --- a/src/test/e2e/31_checksum_and_signature_test.go +++ b/src/test/e2e/31_checksum_and_signature_test.go @@ -36,7 +36,7 @@ func TestChecksumAndSignature(t *testing.T) { // Test that we get an error when trying to deploy a package without providing the public key stdOut, stdErr, err = e2e.Zarf(t, "package", "deploy", pkgName, "--verify", "--confirm") require.Error(t, err, stdOut, stdErr) - require.Contains(t, stdErr, "package is signed but no verification material was provided") + require.Contains(t, stdErr, "package was signed with a key; provide --key to verify") // Test that we don't get an error when we remember to provide the public key stdOut, stdErr, err = e2e.Zarf(t, "package", "deploy", pkgName, publicKeyFlag, "--confirm") From bcaa11068b9e6f4c40d486d724a58335d395745b Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Wed, 13 May 2026 03:14:47 +0000 Subject: [PATCH 20/25] fix(test): update expected errors Signed-off-by: Brandt Keller --- src/test/e2e/34_custom_init_package_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/e2e/34_custom_init_package_test.go b/src/test/e2e/34_custom_init_package_test.go index 9fd6f75207..e92feeeb9d 100644 --- a/src/test/e2e/34_custom_init_package_test.go +++ b/src/test/e2e/34_custom_init_package_test.go @@ -38,7 +38,7 @@ func TestCustomInit(t *testing.T) { // Test that we get an error when trying to deploy a package without providing the public key stdOut, stdErr, err = e2e.Zarf(t, "init", "--verify", "--confirm") require.Error(t, err, stdOut, stdErr) - require.Contains(t, stdErr, "package is signed but no verification material was provided") + require.Contains(t, stdErr, "package was signed with a key; provide --key to verify") /* Test operations during package deploy */ // Test that we can deploy the package with the public key From 0e6a4e2fef45e808b14f0b511f4f7a4f72196663 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Wed, 13 May 2026 19:05:13 +0000 Subject: [PATCH 21/25] feat(signing): isolate cosign signing and verification into new package Signed-off-by: Brandt Keller --- src/cmd/package.go | 19 ++-- src/cmd/tools_trustedroot.go | 4 +- src/config/lang/english.go | 2 +- src/pkg/packager/layout/assemble.go | 5 +- src/pkg/packager/layout/package.go | 59 +++++----- src/pkg/packager/layout/package_test.go | 102 +++++++++--------- src/pkg/packager/load.go | 5 +- src/pkg/packager/load_test.go | 4 +- src/pkg/packager/publish.go | 5 +- src/pkg/packager/publish_test.go | 6 +- src/pkg/packager/pull.go | 7 +- src/pkg/{utils => signing}/bundle.go | 47 +++++--- src/pkg/{utils => signing}/bundle_test.go | 43 +++++++- src/pkg/{utils => signing}/cosign.go | 52 +-------- src/pkg/{utils => signing}/cosign_test.go | 2 +- .../embedded_trusted_root.json | 0 src/pkg/{utils => signing}/trustedroot.go | 2 +- .../{utils => signing}/trustedroot_test.go | 2 +- src/pkg/utils/oci_artifacts.go | 56 ++++++++++ 19 files changed, 251 insertions(+), 171 deletions(-) rename src/pkg/{utils => signing}/bundle.go (63%) rename src/pkg/{utils => signing}/bundle_test.go (84%) rename src/pkg/{utils => signing}/cosign.go (89%) rename src/pkg/{utils => signing}/cosign_test.go (97%) rename src/pkg/{utils => signing}/embedded_trusted_root.json (100%) rename src/pkg/{utils => signing}/trustedroot.go (98%) rename src/pkg/{utils => signing}/trustedroot_test.go (98%) create mode 100644 src/pkg/utils/oci_artifacts.go diff --git a/src/cmd/package.go b/src/cmd/package.go index fb55f1729d..2c58064493 100644 --- a/src/cmd/package.go +++ b/src/cmd/package.go @@ -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" @@ -1746,7 +1747,7 @@ func (o *packagePublishOptions) run(cmd *cobra.Command, args []string) error { err = errors.Join(err, pkgLayout.Cleanup()) }() - publishSignOpts := utils.DefaultSignBlobOptions() + publishSignOpts := signing.DefaultSignBlobOptions() publishSignOpts.Key = o.signingKeyPath publishSignOpts.Password = o.signingKeyPassword publishSignOpts.Overwrite = true @@ -1862,7 +1863,7 @@ type packageSignOptions struct { oidcClientID string rekorURL string tlogUpload bool - yes bool + confirm bool } func newPackageSignCommand(v *viper.Viper) *cobra.Command { @@ -1895,7 +1896,9 @@ func newPackageSignCommand(v *viper.Viper) *cobra.Command { cmd.Flags().StringVar(&o.oidcClientID, "oidc-client-id", "sigstore", lang.CmdPackageSignFlagOIDCClientID) cmd.Flags().StringVar(&o.rekorURL, "rekor-url", "https://rekor.sigstore.dev", lang.CmdPackageSignFlagRekorURL) cmd.Flags().BoolVar(&o.tlogUpload, "tlog-upload", false, lang.CmdPackageSignFlagTlogUpload) - cmd.Flags().BoolVar(&o.yes, "yes", false, lang.CmdPackageSignFlagYes) + cmd.Flags().BoolVar(&o.confirm, "confirm", false, lang.CmdPackageSignFlagConfirm) + + cmd.MarkFlagsMutuallyExclusive("keyless", "signing-key") return cmd } @@ -2008,7 +2011,7 @@ func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error { 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 @@ -2020,7 +2023,7 @@ func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error { signOpts.OIDC.ClientID = o.oidcClientID signOpts.Rekor.URL = o.rekorURL signOpts.TlogUpload = o.tlogUpload - signOpts.SkipConfirmation = o.yes + 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 @@ -2116,7 +2119,7 @@ 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 := utils.DefaultVerifyBlobOptions() + verifyOpts := signing.DefaultVerifyBlobOptions() verifyOpts.Key = o.publicKeyPath verifyOpts.CertVerify.CertIdentity = o.certificateIdentity verifyOpts.CertVerify.CertIdentityRegexp = o.certificateIdentityRegexp @@ -2240,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 } diff --git a/src/cmd/tools_trustedroot.go b/src/cmd/tools_trustedroot.go index fc687d0bfe..f488c5a083 100644 --- a/src/cmd/tools_trustedroot.go +++ b/src/cmd/tools_trustedroot.go @@ -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 { @@ -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) }, diff --git a/src/config/lang/english.go b/src/config/lang/english.go index 1ab6296aa0..3dd1543f63 100644 --- a/src/config/lang/english.go +++ b/src/config/lang/english.go @@ -366,7 +366,7 @@ $ zarf package sign zarf-package-demo-amd64-1.0.0.tar.zst --signing-key awskms:/ CmdPackageSignFlagOIDCClientID = "OIDC client ID used when requesting an identity token. Override for private Sigstore deployments." CmdPackageSignFlagRekorURL = "Rekor transparency log URL. Override for private Sigstore deployments." CmdPackageSignFlagTlogUpload = "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)." - CmdPackageSignFlagYes = "Skip the interactive confirmation prompt before uploading to the Rekor transparency log." + CmdPackageSignFlagConfirm = "Skip the interactive confirmation prompt before uploading to the Rekor transparency log (equivalent to cosign --yes)." CmdPackageVerifyShort = "Verify the signature and integrity of a Zarf package" 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." diff --git a/src/pkg/packager/layout/assemble.go b/src/pkg/packager/layout/assemble.go index 0b1705e014..768939a6bc 100644 --- a/src/pkg/packager/layout/assemble.go +++ b/src/pkg/packager/layout/assemble.go @@ -33,6 +33,7 @@ import ( "github.com/zarf-dev/zarf/src/pkg/logger" "github.com/zarf-dev/zarf/src/pkg/packager/actions" "github.com/zarf-dev/zarf/src/pkg/packager/filters" + "github.com/zarf-dev/zarf/src/pkg/signing" "github.com/zarf-dev/zarf/src/pkg/transform" "github.com/zarf-dev/zarf/src/pkg/utils" "github.com/zarf-dev/zarf/src/pkg/value" @@ -220,7 +221,7 @@ func AssemblePackage(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath } // Sign the package with the provided options - signOpts := utils.DefaultSignBlobOptions() + signOpts := signing.DefaultSignBlobOptions() signOpts.Key = opts.SigningKeyPath signOpts.Password = opts.SigningKeyPassword @@ -302,7 +303,7 @@ func AssembleSkeleton(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath } // Sign the package with the provided options - signOpts := utils.DefaultSignBlobOptions() + signOpts := signing.DefaultSignBlobOptions() signOpts.Key = opts.SigningKeyPath signOpts.Password = opts.SigningKeyPassword diff --git a/src/pkg/packager/layout/package.go b/src/pkg/packager/layout/package.go index cd27057ebd..a8eb642174 100644 --- a/src/pkg/packager/layout/package.go +++ b/src/pkg/packager/layout/package.go @@ -25,6 +25,7 @@ import ( "github.com/zarf-dev/zarf/src/pkg/feature" "github.com/zarf-dev/zarf/src/pkg/logger" "github.com/zarf-dev/zarf/src/pkg/packager/filters" + "github.com/zarf-dev/zarf/src/pkg/signing" "github.com/zarf-dev/zarf/src/pkg/utils" ) @@ -42,7 +43,7 @@ type PackageLayoutOptions struct { VerificationStrategy VerificationStrategy IsPartial bool Filter filters.ComponentFilterStrategy - VerifyBlobOptions *utils.VerifyBlobOptions + VerifyBlobOptions *signing.VerifyBlobOptions } // VerificationStrategy describes a strategy for determining whether to verify a package. @@ -114,13 +115,13 @@ func LoadFromDir(ctx context.Context, dirPath string, opts PackageLayoutOptions) // Only applies when VerifyBlobOptions is not already set, // ensuring the new API takes precedence over the deprecated field. if opts.VerifyBlobOptions == nil && opts.PublicKeyPath != "" { - defaults := utils.DefaultVerifyBlobOptions() + defaults := signing.DefaultVerifyBlobOptions() defaults.Key = opts.PublicKeyPath opts.VerifyBlobOptions = &defaults } if opts.VerificationStrategy < VerifyNever { - verifyOptions := utils.DefaultVerifyBlobOptions() + verifyOptions := signing.DefaultVerifyBlobOptions() if opts.VerifyBlobOptions != nil { verifyOptions = *opts.VerifyBlobOptions } @@ -166,7 +167,7 @@ func (p *PackageLayout) ContainsSBOM() bool { // SignPackage signs the zarf package using cosign with the provided options. // If the options do not indicate signing should be performed (no key material configured), // this is a no-op and returns nil. -func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOptions) (err error) { +func (p *PackageLayout) SignPackage(ctx context.Context, opts signing.SignBlobOptions) (err error) { // Note: This function: // 1. Updates Pkg.Build.Signed = true in memory // 2. Writes the updated zarf.yaml (with signed:true) to a temporary file @@ -308,7 +309,7 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOpti // Perform the signing operation on the temp file l.Debug("signing package", "source", tmpZarfYAMLPath, "signature", tmpSignaturePath) - _, err = utils.CosignSignBlobWithOptions(ctx, tmpZarfYAMLPath, signOpts) + _, err = signing.CosignSignBlobWithOptions(ctx, tmpZarfYAMLPath, signOpts) if err != nil { p.Pkg.Build.Signed = originalSigned p.Pkg.Build.ProvenanceFiles = originalProvenanceFiles @@ -339,11 +340,13 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOpti } } - if opts.Keyless { - if identity, issuer, ierr := utils.ReadKeylessIdentityFromBundle(actualBundlePath); ierr == nil { - l.Info("signed package keyless", "identity", identity, "issuer", issuer) + if bundleEnabled { + if info, bundleErr := signing.ReadBundleInfo(actualBundlePath); bundleErr == nil { + if info.Identity != "" { + l.Info("signed package keyless", "identity", info.Identity, "issuer", info.Issuer) + } } else { - l.Debug("could not read keyless identity from bundle", "error", ierr) + l.Debug("could not read bundle info after signing", "error", bundleErr) } } @@ -352,7 +355,7 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts utils.SignBlobOpti } // VerifyPackageSignature verifies the package signature -func (p *PackageLayout) VerifyPackageSignature(ctx context.Context, opts utils.VerifyBlobOptions) error { +func (p *PackageLayout) VerifyPackageSignature(ctx context.Context, opts signing.VerifyBlobOptions) error { l := logger.From(ctx) l.Debug("verifying package signature") @@ -386,10 +389,15 @@ func (p *PackageLayout) VerifyPackageSignature(ctx context.Context, opts utils.V return errors.New("package is not signed - verification cannot be performed") } - // Early validation using build.signature: fail fast with a method-specific message - // before cosign emits a generic error. Nil for packages predating this field. - if sig := p.Pkg.Build.Signature; sig != nil { - switch sig.Method { + // Check for bundle format signature (preferred). Parse it once for both method + // detection (fast-fail below) and the verify path. + bundlePath := filepath.Join(p.dirPath, Bundle) + bundleInfo, bundleErr := signing.ReadBundleInfo(bundlePath) + hasBundleInfo := bundleErr == nil + + // Early validation: fail fast with a method-specific message before cosign emits a generic error. + if hasBundleInfo { + switch bundleInfo.Method { case "keyless": if !hasKeylessIdentity && !hasCert { return errors.New("package was signed with keyless method; provide --certificate-identity + --certificate-oidc-issuer to verify") @@ -405,26 +413,23 @@ func (p *PackageLayout) VerifyPackageSignature(ctx context.Context, opts utils.V return errors.New("package is signed but no verification material was provided (--key, --certificate-identity + --certificate-oidc-issuer, or --certificate)") } - // Check for bundle format signature (preferred) - bundlePath := filepath.Join(p.dirPath, Bundle) - _, err := os.Stat(bundlePath) - if err == nil { + if hasBundleInfo { opts.BundlePath = bundlePath ZarfYAMLPath := filepath.Join(p.dirPath, ZarfYAML) - return utils.CosignVerifyBlobWithOptions(ctx, ZarfYAMLPath, opts) + return signing.CosignVerifyBlobWithOptions(ctx, ZarfYAMLPath, opts) } - if !errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("error checking bundle signature: %w", err) + if !errors.Is(bundleErr, os.ErrNotExist) { + return fmt.Errorf("error checking bundle signature: %w", bundleErr) } // Bundle doesn't exist, check for legacy signature format signaturePath := filepath.Join(p.dirPath, Signature) - _, err = os.Stat(signaturePath) - if err != nil { - if errors.Is(err, os.ErrNotExist) { + _, sigStatErr := os.Stat(signaturePath) + if sigStatErr != nil { + if errors.Is(sigStatErr, os.ErrNotExist) { return fmt.Errorf("signature not found: neither bundle nor legacy signature exists") } - return fmt.Errorf("error checking legacy signature: %w", err) + return fmt.Errorf("error checking legacy signature: %w", sigStatErr) } // Legacy signatures don't carry a certificate chain, so keyless identity @@ -435,12 +440,12 @@ func (p *PackageLayout) VerifyPackageSignature(ctx context.Context, opts utils.V } // Legacy signature found - l.Warn("bundle format signature not found: legacy signature is being deprecated. consider resigning this zarf package with the --features='bundle-signature=true' flag.") + l.Warn("bundle format signature not found: legacy signature is being deprecated.") opts.Signature = signaturePath opts.CommonVerifyOptions.NewBundleFormat = false ZarfYAMLPath := filepath.Join(p.dirPath, ZarfYAML) - return utils.CosignVerifyBlobWithOptions(ctx, ZarfYAMLPath, opts) + return signing.CosignVerifyBlobWithOptions(ctx, ZarfYAMLPath, opts) } // IsSigned returns true if the package is signed. diff --git a/src/pkg/packager/layout/package_test.go b/src/pkg/packager/layout/package_test.go index 367bbe1c30..b0bdaef150 100644 --- a/src/pkg/packager/layout/package_test.go +++ b/src/pkg/packager/layout/package_test.go @@ -16,12 +16,12 @@ import ( "github.com/zarf-dev/zarf/src/api/v1alpha1" "github.com/zarf-dev/zarf/src/pkg/feature" - "github.com/zarf-dev/zarf/src/pkg/utils" + "github.com/zarf-dev/zarf/src/pkg/signing" "github.com/zarf-dev/zarf/src/test/testutil" ) -func verifyOptsFromKey(keyPath string) *utils.VerifyBlobOptions { - opts := utils.DefaultVerifyBlobOptions() +func verifyOptsFromKey(keyPath string) *signing.VerifyBlobOptions { + opts := signing.DefaultVerifyBlobOptions() opts.Key = keyPath return &opts } @@ -242,7 +242,7 @@ func TestPackageLayoutSignPackage(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - opts := utils.DefaultSignBlobOptions() + opts := signing.DefaultSignBlobOptions() opts.Key = "./testdata/cosign.key" opts.Password = "test" @@ -267,7 +267,7 @@ func TestPackageLayoutSignPackage(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - opts := utils.DefaultSignBlobOptions() + opts := signing.DefaultSignBlobOptions() opts.Key = "./testdata/cosign.key" opts.Password = "wrongpassword" @@ -285,7 +285,7 @@ func TestPackageLayoutSignPackage(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - opts := utils.DefaultSignBlobOptions() + opts := signing.DefaultSignBlobOptions() opts.Key = "./testdata/cosign.key" opts.Password = "test" @@ -300,7 +300,7 @@ func TestPackageLayoutSignPackage(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - opts := utils.DefaultSignBlobOptions() + opts := signing.DefaultSignBlobOptions() opts.Key = "./testdata/cosign.key" opts.Password = "test" @@ -315,7 +315,7 @@ func TestPackageLayoutSignPackage(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - opts := utils.DefaultSignBlobOptions() + opts := signing.DefaultSignBlobOptions() opts.Key = "./testdata/cosign.key" opts.Password = "test" @@ -340,7 +340,7 @@ func TestPackageLayoutSignPackage(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - opts := utils.DefaultSignBlobOptions() + opts := signing.DefaultSignBlobOptions() opts.Key = "./testdata/cosign.key" opts.Password = "test" opts.Overwrite = true @@ -372,7 +372,7 @@ func TestPackageLayoutSignPackage(t *testing.T) { } // Empty options - no signing key material configured - opts := utils.SignBlobOptions{} + opts := signing.SignBlobOptions{} // Should skip signing without error err = pkgLayout.SignPackage(ctx, opts) @@ -393,7 +393,7 @@ func TestPackageLayoutSignPackage(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - opts := utils.DefaultSignBlobOptions() + opts := signing.DefaultSignBlobOptions() opts.Key = "./testdata/cosign.key" opts.Password = "test" @@ -414,7 +414,7 @@ func TestPackageLayoutSignPackage(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - opts := utils.DefaultSignBlobOptions() + opts := signing.DefaultSignBlobOptions() opts.Key = "./testdata/cosign.key" opts.Password = "test" opts.OutputSignature = "/some/custom/path.sig" @@ -443,7 +443,7 @@ func TestPackageLayoutSignPackage(t *testing.T) { } // Wrong password should cause signing to fail - opts := utils.DefaultSignBlobOptions() + opts := signing.DefaultSignBlobOptions() opts.Key = "./testdata/cosign.key" opts.Password = "wrongpassword" @@ -472,7 +472,7 @@ func TestPackageLayoutSignPackage(t *testing.T) { } // Empty options - should skip signing - opts := utils.SignBlobOptions{} + opts := signing.SignBlobOptions{} err = pkgLayout.SignPackage(ctx, opts) require.NoError(t, err) @@ -510,7 +510,7 @@ func TestPackageLayoutSignPackage(t *testing.T) { require.NoError(t, err) // Sign the package - opts := utils.DefaultSignBlobOptions() + opts := signing.DefaultSignBlobOptions() opts.Key = "./testdata/cosign.key" opts.Password = "test" @@ -547,14 +547,14 @@ func TestPackageLayoutSignPackageValidation(t *testing.T) { tests := []struct { name string - setupFunc func(t *testing.T) (*PackageLayout, utils.SignBlobOptions) + setupFunc func(t *testing.T) (*PackageLayout, signing.SignBlobOptions) expectedErr string expectSigned bool expectSignFile bool }{ { name: "package with existing false Signed value gets updated on success", - setupFunc: func(t *testing.T) (*PackageLayout, utils.SignBlobOptions) { + setupFunc: func(t *testing.T) (*PackageLayout, signing.SignBlobOptions) { tmpDir := t.TempDir() yamlPath := filepath.Join(tmpDir, ZarfYAML) require.NoError(t, os.WriteFile(yamlPath, []byte("foobar"), 0o644)) @@ -569,7 +569,7 @@ func TestPackageLayoutSignPackageValidation(t *testing.T) { }, } - opts := utils.DefaultSignBlobOptions() + opts := signing.DefaultSignBlobOptions() opts.Key = "./testdata/cosign.key" opts.Password = "test" @@ -581,7 +581,7 @@ func TestPackageLayoutSignPackageValidation(t *testing.T) { }, { name: "package with existing true Signed value gets overwritten", - setupFunc: func(t *testing.T) (*PackageLayout, utils.SignBlobOptions) { + setupFunc: func(t *testing.T) (*PackageLayout, signing.SignBlobOptions) { tmpDir := t.TempDir() yamlPath := filepath.Join(tmpDir, ZarfYAML) require.NoError(t, os.WriteFile(yamlPath, []byte("foobar"), 0o644)) @@ -596,7 +596,7 @@ func TestPackageLayoutSignPackageValidation(t *testing.T) { }, } - opts := utils.DefaultSignBlobOptions() + opts := signing.DefaultSignBlobOptions() opts.Key = "./testdata/cosign.key" opts.Password = "test" @@ -608,7 +608,7 @@ func TestPackageLayoutSignPackageValidation(t *testing.T) { }, { name: "sign with different password-protected key", - setupFunc: func(t *testing.T) (*PackageLayout, utils.SignBlobOptions) { + setupFunc: func(t *testing.T) (*PackageLayout, signing.SignBlobOptions) { tmpDir := t.TempDir() yamlPath := filepath.Join(tmpDir, ZarfYAML) require.NoError(t, os.WriteFile(yamlPath, []byte("test content"), 0o644)) @@ -618,7 +618,7 @@ func TestPackageLayoutSignPackageValidation(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - opts := utils.DefaultSignBlobOptions() + opts := signing.DefaultSignBlobOptions() opts.Key = "./testdata/cosign.key" opts.Password = "test" @@ -630,7 +630,7 @@ func TestPackageLayoutSignPackageValidation(t *testing.T) { }, { name: "passFunc returns error", - setupFunc: func(t *testing.T) (*PackageLayout, utils.SignBlobOptions) { + setupFunc: func(t *testing.T) (*PackageLayout, signing.SignBlobOptions) { tmpDir := t.TempDir() yamlPath := filepath.Join(tmpDir, ZarfYAML) require.NoError(t, os.WriteFile(yamlPath, []byte("foobar"), 0o644)) @@ -643,7 +643,7 @@ func TestPackageLayoutSignPackageValidation(t *testing.T) { passFunc := cosign.PassFunc(func(_ bool) ([]byte, error) { return nil, os.ErrPermission }) - opts := utils.DefaultSignBlobOptions() + opts := signing.DefaultSignBlobOptions() opts.Key = "./testdata/cosign.key" opts.PassFunc = passFunc @@ -655,7 +655,7 @@ func TestPackageLayoutSignPackageValidation(t *testing.T) { }, { name: "empty package metadata still signs", - setupFunc: func(t *testing.T) (*PackageLayout, utils.SignBlobOptions) { + setupFunc: func(t *testing.T) (*PackageLayout, signing.SignBlobOptions) { tmpDir := t.TempDir() yamlPath := filepath.Join(tmpDir, ZarfYAML) require.NoError(t, os.WriteFile(yamlPath, []byte("foobar"), 0o644)) @@ -668,7 +668,7 @@ func TestPackageLayoutSignPackageValidation(t *testing.T) { }, } - opts := utils.DefaultSignBlobOptions() + opts := signing.DefaultSignBlobOptions() opts.Key = "./testdata/cosign.key" opts.Password = "test" @@ -734,7 +734,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { } // Sign the package (legacy only, bundle feature disabled by default) - signOpts := utils.DefaultSignBlobOptions() + signOpts := signing.DefaultSignBlobOptions() signOpts.Key = "./testdata/cosign.key" signOpts.Password = "test" @@ -743,7 +743,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { require.FileExists(t, legacySignaturePath, "legacy signature should exist") // Verify the signature (should use legacy format) - verifyOpts := utils.DefaultVerifyBlobOptions() + verifyOpts := signing.DefaultVerifyBlobOptions() verifyOpts.Key = "./testdata/cosign.pub" err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) @@ -764,7 +764,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { } // Sign with the test key - signOpts := utils.DefaultSignBlobOptions() + signOpts := signing.DefaultSignBlobOptions() signOpts.Key = "./testdata/cosign.key" signOpts.Password = "test" @@ -772,7 +772,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { require.NoError(t, err) // Try to verify with a different (non-existent) key - should fail - verifyOpts := utils.DefaultVerifyBlobOptions() + verifyOpts := signing.DefaultVerifyBlobOptions() verifyOpts.Key = "./testdata/nonexistent.pub" err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) @@ -792,7 +792,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - verifyOpts := utils.DefaultVerifyBlobOptions() + verifyOpts := signing.DefaultVerifyBlobOptions() verifyOpts.Key = "./testdata/cosign.pub" err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) @@ -806,7 +806,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - verifyOpts := utils.DefaultVerifyBlobOptions() + verifyOpts := signing.DefaultVerifyBlobOptions() verifyOpts.Key = "./testdata/cosign.pub" err := pkgLayout.VerifyPackageSignature(ctx, verifyOpts) @@ -819,7 +819,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - verifyOpts := utils.DefaultVerifyBlobOptions() + verifyOpts := signing.DefaultVerifyBlobOptions() verifyOpts.Key = "./testdata/cosign.pub" err := pkgLayout.VerifyPackageSignature(ctx, verifyOpts) @@ -838,7 +838,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - verifyOpts := utils.DefaultVerifyBlobOptions() + verifyOpts := signing.DefaultVerifyBlobOptions() verifyOpts.Key = "./testdata/cosign.pub" err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) @@ -860,7 +860,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { } // Sign the package - signOpts := utils.DefaultSignBlobOptions() + signOpts := signing.DefaultSignBlobOptions() signOpts.Key = "./testdata/cosign.key" signOpts.Password = "test" @@ -868,7 +868,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { require.NoError(t, err) // Try to verify without providing a key - verifyOpts := utils.DefaultVerifyBlobOptions() + verifyOpts := signing.DefaultVerifyBlobOptions() verifyOpts.Key = "" // Empty key err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) @@ -889,7 +889,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { } // Sign the package - signOpts := utils.DefaultSignBlobOptions() + signOpts := signing.DefaultSignBlobOptions() signOpts.Key = "./testdata/cosign.key" signOpts.Password = "test" @@ -906,7 +906,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { } // Try to verify with corrupted signature(s) - verifyOpts := utils.DefaultVerifyBlobOptions() + verifyOpts := signing.DefaultVerifyBlobOptions() verifyOpts.Key = "./testdata/cosign.pub" err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) @@ -927,7 +927,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { } // Sign the package - signOpts := utils.DefaultSignBlobOptions() + signOpts := signing.DefaultSignBlobOptions() signOpts.Key = "./testdata/cosign.key" signOpts.Password = "test" @@ -939,7 +939,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { require.NoError(t, err) // Verification should fail because content doesn't match signature - verifyOpts := utils.DefaultVerifyBlobOptions() + verifyOpts := signing.DefaultVerifyBlobOptions() verifyOpts.Key = "./testdata/cosign.pub" err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) @@ -962,7 +962,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { } // Sign the package - signOpts := utils.DefaultSignBlobOptions() + signOpts := signing.DefaultSignBlobOptions() signOpts.Key = "./testdata/cosign.key" signOpts.Password = "test" @@ -975,7 +975,7 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { require.NoError(t, err) // Verification should work with legacy signature (fallback path) - verifyOpts := utils.DefaultVerifyBlobOptions() + verifyOpts := signing.DefaultVerifyBlobOptions() verifyOpts.Key = "./testdata/cosign.pub" err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) @@ -1009,7 +1009,7 @@ func TestSignPackageBundleSignatureEnabled(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - opts := utils.DefaultSignBlobOptions() + opts := signing.DefaultSignBlobOptions() opts.Key = "./testdata/cosign.key" opts.Password = "test" @@ -1046,7 +1046,7 @@ func TestSignPackageBundleSignatureEnabled(t *testing.T) { err = os.WriteFile(yamlPath, b, 0o644) require.NoError(t, err) - opts := utils.DefaultSignBlobOptions() + opts := signing.DefaultSignBlobOptions() opts.Key = "./testdata/cosign.key" opts.Password = "test" @@ -1078,7 +1078,7 @@ func TestSignPackageBundleSignatureEnabled(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - signOpts := utils.DefaultSignBlobOptions() + signOpts := signing.DefaultSignBlobOptions() signOpts.Key = "./testdata/cosign.key" signOpts.Password = "test" @@ -1086,7 +1086,7 @@ func TestSignPackageBundleSignatureEnabled(t *testing.T) { require.NoError(t, err) require.FileExists(t, bundlePath) - verifyOpts := utils.DefaultVerifyBlobOptions() + verifyOpts := signing.DefaultVerifyBlobOptions() verifyOpts.Key = "./testdata/cosign.pub" err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) @@ -1107,7 +1107,7 @@ func TestSignPackageBundleSignatureEnabled(t *testing.T) { Pkg: v1alpha1.ZarfPackage{}, } - signOpts := utils.DefaultSignBlobOptions() + signOpts := signing.DefaultSignBlobOptions() signOpts.Key = "./testdata/cosign.key" signOpts.Password = "test" @@ -1120,7 +1120,7 @@ func TestSignPackageBundleSignatureEnabled(t *testing.T) { err = os.Remove(bundlePath) require.NoError(t, err) - verifyOpts := utils.DefaultVerifyBlobOptions() + verifyOpts := signing.DefaultVerifyBlobOptions() verifyOpts.Key = "./testdata/cosign.pub" err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) @@ -1311,7 +1311,7 @@ func TestLoadFromDir_VerificationStrategies(t *testing.T) { Pkg: pkg, } - signOpts := utils.DefaultSignBlobOptions() + signOpts := signing.DefaultSignBlobOptions() signOpts.Key = "./testdata/cosign.key" signOpts.Password = "test" @@ -1667,7 +1667,7 @@ func TestSignPackage_PopulatesProvenanceFiles(t *testing.T) { }, } - opts := utils.DefaultSignBlobOptions() + opts := signing.DefaultSignBlobOptions() opts.Key = "./testdata/cosign.key" opts.Password = "test" @@ -1695,7 +1695,7 @@ func TestSignPackage_PopulatesProvenanceFiles(t *testing.T) { }, } - opts := utils.DefaultSignBlobOptions() + opts := signing.DefaultSignBlobOptions() opts.Key = "./testdata/cosign.key" opts.Password = "wrongpassword" diff --git a/src/pkg/packager/load.go b/src/pkg/packager/load.go index 97b1293332..47ba66ba70 100644 --- a/src/pkg/packager/load.go +++ b/src/pkg/packager/load.go @@ -21,6 +21,7 @@ import ( "github.com/zarf-dev/zarf/src/pkg/cluster" "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" @@ -33,7 +34,7 @@ type LoadOptions struct { Architecture string // Deprecated: Use VerifyBlobOptions instead. PublicKeyPath string - VerifyBlobOptions *utils.VerifyBlobOptions + VerifyBlobOptions *signing.VerifyBlobOptions Filter filters.ComponentFilterStrategy Output string // number of layers to pull in parallel @@ -63,7 +64,7 @@ func LoadPackage(ctx context.Context, source string, opts LoadOptions) (_ *layou // Only applies when VerifyBlobOptions is not already set, // ensuring the new API takes precedence over the deprecated field. if opts.VerifyBlobOptions == nil && opts.PublicKeyPath != "" { - defaults := utils.DefaultVerifyBlobOptions() + defaults := signing.DefaultVerifyBlobOptions() defaults.Key = opts.PublicKeyPath opts.VerifyBlobOptions = &defaults } diff --git a/src/pkg/packager/load_test.go b/src/pkg/packager/load_test.go index 409980bbb7..ce1e2a3a11 100644 --- a/src/pkg/packager/load_test.go +++ b/src/pkg/packager/load_test.go @@ -17,7 +17,7 @@ import ( "github.com/zarf-dev/zarf/src/pkg/cluster" "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/utils" + "github.com/zarf-dev/zarf/src/pkg/signing" "github.com/zarf-dev/zarf/src/test/testutil" ) @@ -69,7 +69,7 @@ func TestLoadPackage(t *testing.T) { tarPath := filepath.Join("testdata", "load-package", "compressed", "zarf-package-test-amd64-0.0.1.tar.zst") keyPath := filepath.Join("layout", "testdata", "cosign.pub") - verifyOpts := utils.DefaultVerifyBlobOptions() + verifyOpts := signing.DefaultVerifyBlobOptions() verifyOpts.Key = keyPath // VerifyNever should skip verification entirely and succeed diff --git a/src/pkg/packager/publish.go b/src/pkg/packager/publish.go index a738f013f1..035cfd63e2 100644 --- a/src/pkg/packager/publish.go +++ b/src/pkg/packager/publish.go @@ -12,6 +12,7 @@ import ( "github.com/zarf-dev/zarf/src/api/v1alpha1" "github.com/zarf-dev/zarf/src/config" "github.com/zarf-dev/zarf/src/pkg/logger" + "github.com/zarf-dev/zarf/src/pkg/signing" "github.com/zarf-dev/zarf/src/pkg/utils" "github.com/zarf-dev/zarf/src/pkg/zoci" "github.com/zarf-dev/zarf/src/types" @@ -107,8 +108,8 @@ func PublishFromOCI(ctx context.Context, src registry.Reference, dst registry.Re type PublishPackageOptions struct { // OCIConcurrency configures the amount of layers to push in parallel OCIConcurrency int - // SignBlobOptions holds all signing configuration. Use utils.DefaultSignBlobOptions() as a base. - SignBlobOptions utils.SignBlobOptions + // SignBlobOptions holds all signing configuration. Use signing.DefaultSignBlobOptions() as a base. + SignBlobOptions signing.SignBlobOptions // Retries specifies the number of retries to use Retries int types.RemoteOptions diff --git a/src/pkg/packager/publish_test.go b/src/pkg/packager/publish_test.go index 6fddbe3e2d..9999e3cc10 100644 --- a/src/pkg/packager/publish_test.go +++ b/src/pkg/packager/publish_test.go @@ -17,7 +17,7 @@ import ( "github.com/zarf-dev/zarf/src/api/v1alpha1" "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/utils" + "github.com/zarf-dev/zarf/src/pkg/signing" "github.com/zarf-dev/zarf/src/pkg/zoci" "github.com/zarf-dev/zarf/src/test/testutil" "github.com/zarf-dev/zarf/src/types" @@ -36,7 +36,7 @@ func defaultTestRemoteOptions() types.RemoteOptions { func pullFromRemote(ctx context.Context, t *testing.T, packageRef string, architecture string, publicKeyPath string, cachePath string) *layout.PackageLayout { t.Helper() - verifyOpts := utils.DefaultVerifyBlobOptions() + verifyOpts := signing.DefaultVerifyBlobOptions() verifyOpts.Key = publicKeyPath // Generate tmpdir and pull published package from local registry @@ -221,7 +221,7 @@ func TestPublishSkeleton(t *testing.T) { } func TestPublishPackage(t *testing.T) { - signOpts := utils.DefaultSignBlobOptions() + signOpts := signing.DefaultSignBlobOptions() signOpts.Key = filepath.Join("testdata", "publish", "cosign.key") signOpts.Password = "password" diff --git a/src/pkg/packager/pull.go b/src/pkg/packager/pull.go index 08725280f1..eaa9ea0be3 100644 --- a/src/pkg/packager/pull.go +++ b/src/pkg/packager/pull.go @@ -16,6 +16,7 @@ import ( "time" "github.com/zarf-dev/zarf/src/pkg/logger" + "github.com/zarf-dev/zarf/src/pkg/signing" "github.com/zarf-dev/zarf/src/pkg/utils" "github.com/zarf-dev/zarf/src/types" @@ -39,7 +40,7 @@ type PullOptions struct { // Deprecated: Use VerifyBlobOptions instead. PublicKeyPath validates the create-time signage of a package. PublicKeyPath string // VerifyBlobOptions configures package signature verification. - VerifyBlobOptions *utils.VerifyBlobOptions + VerifyBlobOptions *signing.VerifyBlobOptions // OCIConcurrency is the number of layers pulled in parallel OCIConcurrency int // CachePath is used to cache layers from OCI package pulls @@ -80,7 +81,7 @@ func Pull(ctx context.Context, source, destination string, opts PullOptions) (_ // Only applies when VerifyBlobOptions is not already set, // ensuring the new API takes precedence over the deprecated field. if opts.VerifyBlobOptions == nil && opts.PublicKeyPath != "" { - defaults := utils.DefaultVerifyBlobOptions() + defaults := signing.DefaultVerifyBlobOptions() defaults.Key = opts.PublicKeyPath opts.VerifyBlobOptions = &defaults } @@ -118,7 +119,7 @@ type pullOCIOptions struct { Filter filters.ComponentFilterStrategy OCIConcurrency int CachePath string - VerifyBlobOptions *utils.VerifyBlobOptions + VerifyBlobOptions *signing.VerifyBlobOptions Connected bool types.RemoteOptions layout.VerificationStrategy diff --git a/src/pkg/utils/bundle.go b/src/pkg/signing/bundle.go similarity index 63% rename from src/pkg/utils/bundle.go rename to src/pkg/signing/bundle.go index c5510a8a2a..c55acda744 100644 --- a/src/pkg/utils/bundle.go +++ b/src/pkg/signing/bundle.go @@ -1,8 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2021-Present The Zarf Authors -// Package utils provides generic utility functions. -package utils +package signing import ( "crypto/x509" @@ -50,25 +49,45 @@ func extractIdentityFromCert(cert *x509.Certificate) (identity, issuer string) { return identity, issuer } -// ReadKeylessIdentityFromBundle parses a Sigstore bundle file and returns the -// signer identity (cert SAN) and OIDC issuer claim. Returns an error if the -// bundle does not contain a certificate (i.e. is not a keyless signature). -func ReadKeylessIdentityFromBundle(bundlePath string) (identity, issuer string, err error) { +// BundleInfo contains parsed metadata from a Sigstore bundle file. +type BundleInfo struct { + // Method is "keyless" for Fulcio-issued certificate bundles, "key" for public-key bundles. + Method string + Identity string // cert SAN — empty for key-based signatures + Issuer string // OIDC issuer — empty for key-based signatures +} + +// ReadBundleInfo parses a Sigstore bundle file and returns its signing metadata. +func ReadBundleInfo(bundlePath string) (BundleInfo, error) { b, err := bundle.LoadJSONFromPath(bundlePath) if err != nil { - return "", "", fmt.Errorf("loading bundle: %w", err) + return BundleInfo{}, fmt.Errorf("loading bundle: %w", err) } - vc, err := b.VerificationContent() if err != nil { - return "", "", fmt.Errorf("reading verification content: %w", err) + return BundleInfo{}, fmt.Errorf("reading verification content: %w", err) + } + switch v := vc.(type) { + case *bundle.Certificate: + identity, issuer := extractIdentityFromCert(v.Certificate()) + return BundleInfo{Method: "keyless", Identity: identity, Issuer: issuer}, nil + case *bundle.PublicKey: + return BundleInfo{Method: "key"}, nil + default: + return BundleInfo{}, fmt.Errorf("unrecognised verification content type %T", vc) } +} - certHolder, ok := vc.(*bundle.Certificate) - if !ok { +// ReadKeylessIdentityFromBundle parses a Sigstore bundle file and returns the +// signer identity (cert SAN) and OIDC issuer claim. Returns an error if the +// bundle does not contain a certificate (i.e. is not a keyless signature). +func ReadKeylessIdentityFromBundle(bundlePath string) (identity, issuer string, err error) { + info, err := ReadBundleInfo(bundlePath) + if err != nil { + return "", "", err + } + if info.Method != "keyless" { return "", "", errors.New("bundle does not contain a certificate (not a keyless signature)") } - - identity, issuer = extractIdentityFromCert(certHolder.Certificate()) - return identity, issuer, nil + return info.Identity, info.Issuer, nil } diff --git a/src/pkg/utils/bundle_test.go b/src/pkg/signing/bundle_test.go similarity index 84% rename from src/pkg/utils/bundle_test.go rename to src/pkg/signing/bundle_test.go index c52e89f990..0d84064bff 100644 --- a/src/pkg/utils/bundle_test.go +++ b/src/pkg/signing/bundle_test.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2021-Present The Zarf Authors -package utils +package signing import ( "crypto/ecdsa" @@ -168,6 +168,47 @@ func TestExtractIdentityFromCert(t *testing.T) { }) } +func TestReadBundleInfo(t *testing.T) { + t.Parallel() + + t.Run("certificate bundle returns keyless method with identity and issuer", func(t *testing.T) { + t.Parallel() + issuerVal, err := asn1.Marshal("https://github.com/login/oauth") + require.NoError(t, err) + cert := makeCert(t, &x509.Certificate{ + Subject: pkix.Name{CommonName: "ephemeral"}, + EmailAddresses: []string{"signer@example.com"}, + ExtraExtensions: []pkix.Extension{ + {Id: sigstoreIssuerOIDV2, Value: issuerVal}, + }, + }) + path := writeBundleFixture(t, cert, "certificate") + + info, err := ReadBundleInfo(path) + require.NoError(t, err) + require.Equal(t, "keyless", info.Method) + require.Equal(t, "signer@example.com", info.Identity) + require.Equal(t, "https://github.com/login/oauth", info.Issuer) + }) + + t.Run("key-based bundle returns key method with empty identity and issuer", func(t *testing.T) { + t.Parallel() + path := writeKeyBasedBundleFixture(t) + + info, err := ReadBundleInfo(path) + require.NoError(t, err) + require.Equal(t, "key", info.Method) + require.Empty(t, info.Identity) + require.Empty(t, info.Issuer) + }) + + t.Run("missing bundle file errors", func(t *testing.T) { + t.Parallel() + _, err := ReadBundleInfo(filepath.Join(t.TempDir(), "nonexistent.json")) + require.Error(t, err) + }) +} + func TestReadKeylessIdentityFromBundle(t *testing.T) { t.Parallel() diff --git a/src/pkg/utils/cosign.go b/src/pkg/signing/cosign.go similarity index 89% rename from src/pkg/utils/cosign.go rename to src/pkg/signing/cosign.go index d5fc982ba3..e50256df5a 100644 --- a/src/pkg/utils/cosign.go +++ b/src/pkg/signing/cosign.go @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2021-Present The Zarf Authors -// Package utils provides generic utility functions. -package utils +// Package signing provides cosign-based signing and verification for Zarf packages. +package signing import ( "context" @@ -11,13 +11,11 @@ import ( "os" "time" - "github.com/google/go-containerregistry/pkg/name" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/sign" "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" "github.com/sigstore/cosign/v3/cmd/cosign/cli/verify" "github.com/sigstore/cosign/v3/pkg/cosign" - ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" // Register the provider-specific plugins _ "github.com/sigstore/sigstore/pkg/signature/kms/aws" @@ -315,49 +313,3 @@ func CosignVerifyBlobWithOptions(ctx context.Context, blobPath string, opts Veri l.Debug("blob signature verified successfully") return nil } - -// GetCosignArtifacts returns signatures and attestations for the given image. -func GetCosignArtifacts(image string) ([]string, error) { - var nameOpts []name.Option - - ref, err := name.ParseReference(image, nameOpts...) - if err != nil { - return nil, err - } - - var remoteOpts []ociremote.Option - simg, _ := ociremote.SignedEntity(ref, remoteOpts...) //nolint:errcheck - if simg == nil { - return nil, nil - } - - sigRef, _ := ociremote.SignatureTag(ref, remoteOpts...) //nolint:errcheck - attRef, _ := ociremote.AttestationTag(ref, remoteOpts...) //nolint:errcheck - - ss, err := simg.Signatures() - if err != nil { - return nil, err - } - ssLayers, err := ss.Layers() - if err != nil { - return nil, err - } - - var cosignArtifactList = make([]string, 0) - if 0 < len(ssLayers) { - cosignArtifactList = append(cosignArtifactList, sigRef.String()) - } - - atts, err := simg.Attestations() - if err != nil { - return nil, err - } - aLayers, err := atts.Layers() - if err != nil { - return nil, err - } - if 0 < len(aLayers) { - cosignArtifactList = append(cosignArtifactList, attRef.String()) - } - return cosignArtifactList, nil -} diff --git a/src/pkg/utils/cosign_test.go b/src/pkg/signing/cosign_test.go similarity index 97% rename from src/pkg/utils/cosign_test.go rename to src/pkg/signing/cosign_test.go index f1267f8fc6..cc3fad16c5 100644 --- a/src/pkg/utils/cosign_test.go +++ b/src/pkg/signing/cosign_test.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2021-Present The Zarf Authors -package utils +package signing import ( "testing" diff --git a/src/pkg/utils/embedded_trusted_root.json b/src/pkg/signing/embedded_trusted_root.json similarity index 100% rename from src/pkg/utils/embedded_trusted_root.json rename to src/pkg/signing/embedded_trusted_root.json diff --git a/src/pkg/utils/trustedroot.go b/src/pkg/signing/trustedroot.go similarity index 98% rename from src/pkg/utils/trustedroot.go rename to src/pkg/signing/trustedroot.go index dccdf7c024..04f4ee3ba8 100644 --- a/src/pkg/utils/trustedroot.go +++ b/src/pkg/signing/trustedroot.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2021-Present The Zarf Authors -package utils +package signing import ( _ "embed" diff --git a/src/pkg/utils/trustedroot_test.go b/src/pkg/signing/trustedroot_test.go similarity index 98% rename from src/pkg/utils/trustedroot_test.go rename to src/pkg/signing/trustedroot_test.go index 387d6fd760..e35be4ab76 100644 --- a/src/pkg/utils/trustedroot_test.go +++ b/src/pkg/signing/trustedroot_test.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2021-Present The Zarf Authors -package utils +package signing import ( "encoding/json" diff --git a/src/pkg/utils/oci_artifacts.go b/src/pkg/utils/oci_artifacts.go new file mode 100644 index 0000000000..d392d5e318 --- /dev/null +++ b/src/pkg/utils/oci_artifacts.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +// Package utils provides generic utility functions. +package utils + +import ( + "github.com/google/go-containerregistry/pkg/name" + ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" +) + +// GetCosignArtifacts returns signatures and attestations for the given image. +func GetCosignArtifacts(image string) ([]string, error) { + var nameOpts []name.Option + + ref, err := name.ParseReference(image, nameOpts...) + if err != nil { + return nil, err + } + + var remoteOpts []ociremote.Option + simg, _ := ociremote.SignedEntity(ref, remoteOpts...) //nolint:errcheck + if simg == nil { + return nil, nil + } + + sigRef, _ := ociremote.SignatureTag(ref, remoteOpts...) //nolint:errcheck + attRef, _ := ociremote.AttestationTag(ref, remoteOpts...) //nolint:errcheck + + ss, err := simg.Signatures() + if err != nil { + return nil, err + } + ssLayers, err := ss.Layers() + if err != nil { + return nil, err + } + + var cosignArtifactList = make([]string, 0) + if 0 < len(ssLayers) { + cosignArtifactList = append(cosignArtifactList, sigRef.String()) + } + + atts, err := simg.Attestations() + if err != nil { + return nil, err + } + aLayers, err := atts.Layers() + if err != nil { + return nil, err + } + if 0 < len(aLayers) { + cosignArtifactList = append(cosignArtifactList, attRef.String()) + } + return cosignArtifactList, nil +} From bcccd08cfdabde3af10a7f41a167823c13ed5cc0 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Wed, 13 May 2026 19:13:31 +0000 Subject: [PATCH 22/25] fix(schema): remove signature field and regen schema Signed-off-by: Brandt Keller --- .../docs/commands/zarf_package_sign.md | 2 +- src/api/v1alpha1/package.go | 13 -------- src/pkg/packager/layout/package.go | 19 +----------- src/pkg/schema/zarf-v1alpha1-schema.json | 30 ------------------- zarf.schema.json | 30 ------------------- 5 files changed, 2 insertions(+), 92 deletions(-) diff --git a/site/src/content/docs/commands/zarf_package_sign.md b/site/src/content/docs/commands/zarf_package_sign.md index c968d31e53..cd3f86d3c2 100644 --- a/site/src/content/docs/commands/zarf_package_sign.md +++ b/site/src/content/docs/commands/zarf_package_sign.md @@ -42,6 +42,7 @@ $ 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 @@ -59,7 +60,6 @@ $ zarf package sign zarf-package-demo-amd64-1.0.0.tar.zst --signing-key awskms:/ --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 - --yes Skip the interactive confirmation prompt before uploading to the Rekor transparency log. ``` ### Options inherited from parent commands diff --git a/src/api/v1alpha1/package.go b/src/api/v1alpha1/package.go index 6a1350b698..d77cce55f3 100644 --- a/src/api/v1alpha1/package.go +++ b/src/api/v1alpha1/package.go @@ -267,8 +267,6 @@ type ZarfBuildData struct { Flavor string `json:"flavor,omitempty" jsonschema:"pattern=^[^/\\\\]*$"` // Whether this package was signed Signed *bool `json:"signed,omitempty"` - // Metadata about the signature applied to this package, populated after signing. - Signature *ZarfSignatureMetadata `json:"signature,omitempty"` // Requirements for specific package operations. VersionRequirements []VersionRequirement `json:"versionRequirements,omitempty"` // ProvenanceFiles lists files present in the package that are not included in checksums.txt. @@ -292,14 +290,3 @@ type VersionRequirement struct { // Explanation for why this version is required Reason string `json:"reason,omitempty"` } - -// ZarfSignatureMetadata records how a package was signed. -// Populated by SignPackage and included in the signed zarf.yaml. -// Fields are limited to values known before signing begins; identity and issuer -// (available only after the Fulcio OIDC exchange) are stored in the Sigstore bundle. -type ZarfSignatureMetadata struct { - // Method is the signing method used. - Method string `json:"method" jsonschema:"enum=keyless,enum=key"` - // TlogUploaded indicates whether the signature was uploaded to the Rekor transparency log. - TlogUploaded bool `json:"tlogUploaded"` -} diff --git a/src/pkg/packager/layout/package.go b/src/pkg/packager/layout/package.go index a8eb642174..5ccfcb2693 100644 --- a/src/pkg/packager/layout/package.go +++ b/src/pkg/packager/layout/package.go @@ -231,7 +231,6 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts signing.SignBlobOp // Save original fields for rollback originalProvenanceFiles := slices.Clone(p.Pkg.Build.ProvenanceFiles) originalVersionRequirements := slices.Clone(p.Pkg.Build.VersionRequirements) - originalSignature := p.Pkg.Build.Signature // Keyless signatures require bundle format — the cert chain cannot be stored in the // legacy .sig file. For key-based signing, respect the BundleSignature feature flag. @@ -252,26 +251,12 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts signing.SignBlobOp }) } - // Set signature metadata before marshalling so it is included in the signed zarf.yaml. - // Identity and issuer are intentionally omitted: they are only knowable after the Fulcio - // OIDC exchange at sign time, and are stored in the Sigstore bundle provenance file. - sigMeta := &v1alpha1.ZarfSignatureMetadata{ - TlogUploaded: opts.TlogUpload, - } - if opts.Keyless { - sigMeta.Method = "keyless" - } else { - sigMeta.Method = "key" - } - p.Pkg.Build.Signature = sigMeta - - // Marshal package with signed:true and signature metadata + // Marshal package with signed:true b, err := goyaml.Marshal(p.Pkg) if err != nil { p.Pkg.Build.Signed = originalSigned p.Pkg.Build.ProvenanceFiles = originalProvenanceFiles p.Pkg.Build.VersionRequirements = originalVersionRequirements - p.Pkg.Build.Signature = originalSignature return fmt.Errorf("failed to marshal package for signing: %w", err) } @@ -281,7 +266,6 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts signing.SignBlobOp p.Pkg.Build.Signed = originalSigned p.Pkg.Build.ProvenanceFiles = originalProvenanceFiles p.Pkg.Build.VersionRequirements = originalVersionRequirements - p.Pkg.Build.Signature = originalSignature return fmt.Errorf("failed to write temp %s: %w", ZarfYAML, err) } @@ -314,7 +298,6 @@ func (p *PackageLayout) SignPackage(ctx context.Context, opts signing.SignBlobOp p.Pkg.Build.Signed = originalSigned p.Pkg.Build.ProvenanceFiles = originalProvenanceFiles p.Pkg.Build.VersionRequirements = originalVersionRequirements - p.Pkg.Build.Signature = originalSignature return fmt.Errorf("failed to sign package: %w", err) } diff --git a/src/pkg/schema/zarf-v1alpha1-schema.json b/src/pkg/schema/zarf-v1alpha1-schema.json index 239cd5c946..7e7b2ea428 100644 --- a/src/pkg/schema/zarf-v1alpha1-schema.json +++ b/src/pkg/schema/zarf-v1alpha1-schema.json @@ -362,10 +362,6 @@ "description": "Any registry domains that were overridden on package create when pulling images.", "type": "object" }, - "signature": { - "$ref": "#/$defs/ZarfSignatureMetadata", - "description": "Metadata about the signature applied to this package, populated after signing." - }, "signed": { "description": "Whether this package was signed", "type": "boolean" @@ -1261,32 +1257,6 @@ ], "type": "object" }, - "ZarfSignatureMetadata": { - "additionalProperties": false, - "description": "ZarfSignatureMetadata records how a package was signed.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "method": { - "description": "Method is the signing method used.", - "enum": [ - "keyless", - "key" - ], - "type": "string" - }, - "tlogUploaded": { - "description": "TlogUploaded indicates whether the signature was uploaded to the Rekor transparency log.", - "type": "boolean" - } - }, - "required": [ - "method", - "tlogUploaded" - ], - "type": "object" - }, "ZarfValues": { "additionalProperties": false, "description": "ZarfValues imports package-level values files and validation.", diff --git a/zarf.schema.json b/zarf.schema.json index 239cd5c946..7e7b2ea428 100644 --- a/zarf.schema.json +++ b/zarf.schema.json @@ -362,10 +362,6 @@ "description": "Any registry domains that were overridden on package create when pulling images.", "type": "object" }, - "signature": { - "$ref": "#/$defs/ZarfSignatureMetadata", - "description": "Metadata about the signature applied to this package, populated after signing." - }, "signed": { "description": "Whether this package was signed", "type": "boolean" @@ -1261,32 +1257,6 @@ ], "type": "object" }, - "ZarfSignatureMetadata": { - "additionalProperties": false, - "description": "ZarfSignatureMetadata records how a package was signed.", - "patternProperties": { - "^x-": {} - }, - "properties": { - "method": { - "description": "Method is the signing method used.", - "enum": [ - "keyless", - "key" - ], - "type": "string" - }, - "tlogUploaded": { - "description": "TlogUploaded indicates whether the signature was uploaded to the Rekor transparency log.", - "type": "boolean" - } - }, - "required": [ - "method", - "tlogUploaded" - ], - "type": "object" - }, "ZarfValues": { "additionalProperties": false, "description": "ZarfValues imports package-level values files and validation.", From 2487805b39befdebd0e5727275c4ef2bf071bb78 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Wed, 13 May 2026 19:35:45 +0000 Subject: [PATCH 23/25] move deprecation logic earlier in the chain Signed-off-by: Brandt Keller --- src/pkg/packager/layout/package.go | 14 +++++------ src/pkg/packager/layout/package_test.go | 31 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/pkg/packager/layout/package.go b/src/pkg/packager/layout/package.go index 5ccfcb2693..71eea643be 100644 --- a/src/pkg/packager/layout/package.go +++ b/src/pkg/packager/layout/package.go @@ -352,17 +352,17 @@ func (p *PackageLayout) VerifyPackageSignature(ctx context.Context, opts signing return fmt.Errorf("invalid package layout: %s is not a directory", p.dirPath) } + // Sync the deprecated KeyRef alias before computing hasKey so callers using + // only KeyRef are not rejected for missing material. CosignVerifyBlobWithOptions + // emits the deprecation warning when invoked. + if opts.Key == "" && opts.KeyRef != "" { //nolint:staticcheck // intentional read of deprecated alias for migration sync + opts.Key = opts.KeyRef //nolint:staticcheck // intentional read of deprecated alias for migration sync + } + hasKey := opts.Key != "" hasKeylessIdentity := opts.CertVerify.CertIdentity != "" || opts.CertVerify.CertIdentityRegexp != "" hasCert := opts.CertVerify.Cert != "" hasVerificationMaterial := hasKey || hasKeylessIdentity || hasCert - // Sync the deprecated KeyRef alias before the gate so a caller using only the - // old field name isn't rejected for missing material. CosignVerifyBlobWithOptions - // emits the deprecation warning when invoked. Touching the deprecated field is - // the only way to perform the migration sync from this package. - if opts.Key == "" && opts.KeyRef != "" { //nolint:staticcheck // intentional read of deprecated alias for migration sync - opts.Key = opts.KeyRef //nolint:staticcheck // intentional read of deprecated alias for migration sync - } // Handle the case where the package is not signed if !p.IsSigned() { diff --git a/src/pkg/packager/layout/package_test.go b/src/pkg/packager/layout/package_test.go index b0bdaef150..0418ca9d44 100644 --- a/src/pkg/packager/layout/package_test.go +++ b/src/pkg/packager/layout/package_test.go @@ -981,6 +981,37 @@ func TestPackageLayoutVerifyPackageSignature(t *testing.T) { err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) require.NoError(t, err, "verification should succeed with legacy signature format") }) + + t.Run("deprecated KeyRef alias resolves before hasKey is computed", func(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + yamlPath := filepath.Join(tmpDir, ZarfYAML) + + err := os.WriteFile(yamlPath, []byte("test content"), 0o644) + require.NoError(t, err) + + pkgLayout := &PackageLayout{ + dirPath: tmpDir, + Pkg: v1alpha1.ZarfPackage{}, + } + + signOpts := signing.DefaultSignBlobOptions() + signOpts.Key = "./testdata/cosign.key" + signOpts.Password = "test" + + err = pkgLayout.SignPackage(ctx, signOpts) + require.NoError(t, err) + + // Use only the deprecated KeyRef alias with Key intentionally left empty. + // Without the ordering fix, hasKey was computed before the KeyRef→Key sync, + // causing verification to fail with "package was signed with a key; provide --key to verify". + verifyOpts := signing.DefaultVerifyBlobOptions() + verifyOpts.KeyRef = "./testdata/cosign.pub" //nolint:staticcheck + + err = pkgLayout.VerifyPackageSignature(ctx, verifyOpts) + require.NoError(t, err) + }) } // TestSignPackageBundleSignatureEnabled tests signing behavior when the BundleSignature From cfe67434fa467ecf933456b4bac5b36bad80cbbf Mon Sep 17 00:00:00 2001 From: Brandt Keller <43887158+brandtkeller@users.noreply.github.com> Date: Wed, 13 May 2026 15:28:51 -0700 Subject: [PATCH 24/25] Update hack/refresh-trusted-root.sh Co-authored-by: Austin Abro <37223396+AustinAbro321@users.noreply.github.com> Signed-off-by: Brandt Keller <43887158+brandtkeller@users.noreply.github.com> --- hack/refresh-trusted-root.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hack/refresh-trusted-root.sh b/hack/refresh-trusted-root.sh index 77b65846ab..cf56e7c9dc 100755 --- a/hack/refresh-trusted-root.sh +++ b/hack/refresh-trusted-root.sh @@ -5,7 +5,7 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -EMBED_PATH="${REPO_ROOT}/src/pkg/utils/embedded_trusted_root.json" +EMBED_PATH="${REPO_ROOT}/src/pkg/signing/embedded_trusted_root.json" ZARF_BIN="${REPO_ROOT}/build/zarf" if [ ! -x "${ZARF_BIN}" ]; then From 3f63c3709bd101af7555cf1dc5f604c6df65af1d Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Thu, 14 May 2026 04:02:49 +0000 Subject: [PATCH 25/25] feat(cmd): support signing/verify viper configuration Signed-off-by: Brandt Keller --- src/cmd/package.go | 24 ++++++++++++------------ src/cmd/viper.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/src/cmd/package.go b/src/cmd/package.go index 2c58064493..1301b44c17 100644 --- a/src/cmd/package.go +++ b/src/cmd/package.go @@ -1890,12 +1890,12 @@ func newPackageSignCommand(v *viper.Viper) *cobra.Command { cmd.Flags().BoolVar(&o.keyless, "keyless", false, lang.CmdPackageSignFlagKeyless) cmd.Flags().StringVar(&o.identityToken, "identity-token", "", lang.CmdPackageSignFlagIdentityToken) - cmd.Flags().StringVar(&o.fulcioURL, "fulcio-url", "https://fulcio.sigstore.dev", lang.CmdPackageSignFlagFulcioURL) - cmd.Flags().StringVar(&o.fulcioAuthFlow, "fulcio-auth-flow", "normal", lang.CmdPackageSignFlagFulcioAuthFlow) - cmd.Flags().StringVar(&o.oidcIssuer, "oidc-issuer", "https://oauth2.sigstore.dev/auth", lang.CmdPackageSignFlagOIDCIssuer) - cmd.Flags().StringVar(&o.oidcClientID, "oidc-client-id", "sigstore", lang.CmdPackageSignFlagOIDCClientID) - cmd.Flags().StringVar(&o.rekorURL, "rekor-url", "https://rekor.sigstore.dev", lang.CmdPackageSignFlagRekorURL) - cmd.Flags().BoolVar(&o.tlogUpload, "tlog-upload", false, lang.CmdPackageSignFlagTlogUpload) + 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") @@ -2094,12 +2094,12 @@ 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", "", lang.CmdPackageVerifyFlagCertificateIdentity) - cmd.Flags().StringVar(&o.certificateIdentityRegexp, "certificate-identity-regexp", "", lang.CmdPackageVerifyFlagCertificateIdentityRegexp) - cmd.Flags().StringVar(&o.certificateOIDCIssuer, "certificate-oidc-issuer", "", lang.CmdPackageVerifyFlagCertificateOIDCIssuer) - cmd.Flags().StringVar(&o.certificateOIDCIssuerRegexp, "certificate-oidc-issuer-regexp", "", lang.CmdPackageVerifyFlagCertificateOIDCIssuerRegexp) - cmd.Flags().StringVar(&o.trustedRoot, "trusted-root", "", lang.CmdPackageVerifyFlagTrustedRoot) - cmd.Flags().BoolVar(&o.insecureIgnoreTlog, "insecure-ignore-tlog", true, lang.CmdPackageVerifyFlagInsecureIgnoreTlog) + 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 } diff --git a/src/cmd/viper.go b/src/cmd/viper.go index 6051567f0e..b33e096791 100644 --- a/src/cmd/viper.go +++ b/src/cmd/viper.go @@ -121,6 +121,24 @@ const ( VPkgSignOutput = "package.sign.output" VPkgSignOverwrite = "package.sign.overwrite" + // Package sign keyless config keys + + VPkgSignFulcioURL = "package.sign.fulcio_url" + VPkgSignFulcioAuthFlow = "package.sign.fulcio_auth_flow" + VPkgSignOIDCIssuer = "package.sign.oidc_issuer" + VPkgSignOIDCClientID = "package.sign.oidc_client_id" + VPkgSignRekorURL = "package.sign.rekor_url" + VPkgSignTlogUpload = "package.sign.tlog_upload" + + // Package verify keyless config keys + + VPkgVerifyCertIdentity = "package.verify.certificate_identity" + VPkgVerifyCertIdentityRegexp = "package.verify.certificate_identity_regexp" + VPkgVerifyCertOIDCIssuer = "package.verify.certificate_oidc_issuer" + VPkgVerifyCertOIDCIssuerRegexp = "package.verify.certificate_oidc_issuer_regexp" + VPkgVerifyTrustedRoot = "package.verify.trusted_root" + VPkgVerifyInsecureIgnoreTlog = "package.verify.insecure_ignore_tlog" + // Package pull config keys VPkgPullOutputDir = "package.pull.output_directory" @@ -252,6 +270,16 @@ func setDefaults() { // Package publish opts that are non-zero values v.SetDefault(VPkgPublishRetries, 1) + // Package sign keyless opts that are non-zero values + v.SetDefault(VPkgSignFulcioURL, "https://fulcio.sigstore.dev") + v.SetDefault(VPkgSignFulcioAuthFlow, "normal") + v.SetDefault(VPkgSignOIDCIssuer, "https://oauth2.sigstore.dev/auth") + v.SetDefault(VPkgSignOIDCClientID, "sigstore") + v.SetDefault(VPkgSignRekorURL, "https://rekor.sigstore.dev") + + // Package verify opts that are non-zero values + v.SetDefault(VPkgVerifyInsecureIgnoreTlog, true) + // Dev deploy defaults v.SetDefault(VDevDeployConnected, true) }