-
Notifications
You must be signed in to change notification settings - Fork 249
feat(sign): support for keyless signing and offline verification #4891
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
brandtkeller
wants to merge
31
commits into
main
Choose a base branch
from
2805_keyless_signing
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
895809a
feat(sign): align signing and verification to cosign
brandtkeller 972fa31
chore(docs): update comments and language
brandtkeller 762484e
fix(cosign): implement non-prompting passfunc
brandtkeller 30bbdbd
feat(sign): support for embedded TrustedRoot
brandtkeller 17dfcd1
feat(sign): enable keyless signing and verification
brandtkeller cab4e08
fix(options): deprecate duplicitive fields
brandtkeller bfd2a5f
Merge branch 'main' of github.com:zarf-dev/zarf into 4572_sign_verify…
brandtkeller 4f2d01e
fix(flags): review flags that can be further hidden based on utility
brandtkeller e30354e
fix(flags): trustedroot flag is not operational... yet
brandtkeller 4c26a4e
Merge branch 'main' of github.com:zarf-dev/zarf into 4572_sign_verify…
brandtkeller 1719f3b
chore(update): resolve changes from 4572 branch
brandtkeller fc8151b
fix(sigstore): retrieve identity/issuer from bundle
brandtkeller 7682107
fix(cmd): explicit opt-in for cosign options/flags
brandtkeller 1fa7225
choer(merge): merge 4572 into 2805
brandtkeller bc9797d
feat(sign): implement keyless signing options explicitly
brandtkeller 6956676
fix(sign): support for oci signing operations
brandtkeller fa088d4
fix(test): update error message for verification material
brandtkeller 4f49439
fix(cosign): add timeout to verify
brandtkeller 650aebd
Merge branch '4572_sign_verify_flags' of github.com:zarf-dev/zarf int…
brandtkeller c49d85d
fix(requirement): add version requirement for bundle signing verifica…
brandtkeller 69067ca
merge main into 2805_keyless_signing
brandtkeller aed485c
feat(sign): add signature build metadata for future utility
brandtkeller 19259d7
fix(bundle): implement generic bundle parsing
brandtkeller efb1d68
chore: generate the updated schema
brandtkeller d73dcde
fix(test): update test for error output
brandtkeller bcaa110
fix(test): update expected errors
brandtkeller 0e6a4e2
feat(signing): isolate cosign signing and verification into new package
brandtkeller bcccd08
fix(schema): remove signature field and regen schema
brandtkeller 2487805
move deprecation logic earlier in the chain
brandtkeller cfe6743
Update hack/refresh-trusted-root.sh
brandtkeller 3f63c37
feat(cmd): support signing/verify viper configuration
brandtkeller File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,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/signing/embedded_trusted_root.json" | ||
| ZARF_BIN="${REPO_ROOT}/build/zarf" | ||
|
|
||
| if [ ! -x "${ZARF_BIN}" ]; then | ||
| echo "build/zarf not found; run 'make build' first" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| if ! command -v jq >/dev/null 2>&1; then | ||
| echo "jq is required to format the embedded trusted root for reviewable diffs" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| "${ZARF_BIN}" tools trusted-root create --with-default-services --out "${EMBED_PATH}" | ||
| jq --indent 2 . "${EMBED_PATH}" > "${EMBED_PATH}.tmp" && mv "${EMBED_PATH}.tmp" "${EMBED_PATH}" | ||
| echo "Refreshed ${EMBED_PATH}" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,13 +1747,17 @@ func (o *packagePublishOptions) run(cmd *cobra.Command, args []string) error { | |
| err = errors.Join(err, pkgLayout.Cleanup()) | ||
| }() | ||
|
|
||
| publishSignOpts := signing.DefaultSignBlobOptions() | ||
| publishSignOpts.Key = o.signingKeyPath | ||
| publishSignOpts.Password = o.signingKeyPassword | ||
| publishSignOpts.Overwrite = true | ||
|
|
||
| publishPackageOpts := packager.PublishPackageOptions{ | ||
| OCIConcurrency: o.ociConcurrency, | ||
| SigningKeyPath: o.signingKeyPath, | ||
| SigningKeyPassword: o.signingKeyPassword, | ||
| Retries: o.retries, | ||
| RemoteOptions: defaultRemoteOptions(), | ||
| Tag: o.tag, | ||
| OCIConcurrency: o.ociConcurrency, | ||
| SignBlobOptions: publishSignOpts, | ||
| Retries: o.retries, | ||
| RemoteOptions: defaultRemoteOptions(), | ||
| Tag: o.tag, | ||
| } | ||
|
|
||
| _, err = packager.PublishPackage(ctx, pkgLayout, dstRef, publishPackageOpts) | ||
|
|
@@ -1848,6 +1853,17 @@ type packageSignOptions struct { | |
| ociConcurrency int | ||
| retries int | ||
| verify bool | ||
| // Keyless signing flags. Each is hand-rolled and individually opted-in; | ||
| // new cosign flags will not appear here automatically on dependency bumps. | ||
| keyless bool | ||
| identityToken string | ||
| fulcioURL string | ||
| fulcioAuthFlow string | ||
| oidcIssuer string | ||
| oidcClientID string | ||
| rekorURL string | ||
| tlogUpload bool | ||
| confirm bool | ||
| } | ||
|
|
||
| func newPackageSignCommand(v *viper.Viper) *cobra.Command { | ||
|
|
@@ -1872,6 +1888,18 @@ func newPackageSignCommand(v *viper.Viper) *cobra.Command { | |
| cmd.Flags().IntVar(&o.retries, "retries", v.GetInt(VPkgRetries), lang.CmdPackageFlagRetries) | ||
| cmd.Flags().BoolVar(&o.verify, "verify", v.GetBool(VPkgVerify), lang.CmdPackageFlagVerify) | ||
|
|
||
| cmd.Flags().BoolVar(&o.keyless, "keyless", false, lang.CmdPackageSignFlagKeyless) | ||
|
brandtkeller marked this conversation as resolved.
|
||
| cmd.Flags().StringVar(&o.identityToken, "identity-token", "", lang.CmdPackageSignFlagIdentityToken) | ||
| cmd.Flags().StringVar(&o.fulcioURL, "fulcio-url", v.GetString(VPkgSignFulcioURL), lang.CmdPackageSignFlagFulcioURL) | ||
| cmd.Flags().StringVar(&o.fulcioAuthFlow, "fulcio-auth-flow", v.GetString(VPkgSignFulcioAuthFlow), lang.CmdPackageSignFlagFulcioAuthFlow) | ||
| cmd.Flags().StringVar(&o.oidcIssuer, "oidc-issuer", v.GetString(VPkgSignOIDCIssuer), lang.CmdPackageSignFlagOIDCIssuer) | ||
| cmd.Flags().StringVar(&o.oidcClientID, "oidc-client-id", v.GetString(VPkgSignOIDCClientID), lang.CmdPackageSignFlagOIDCClientID) | ||
| cmd.Flags().StringVar(&o.rekorURL, "rekor-url", v.GetString(VPkgSignRekorURL), lang.CmdPackageSignFlagRekorURL) | ||
| cmd.Flags().BoolVar(&o.tlogUpload, "tlog-upload", v.GetBool(VPkgSignTlogUpload), lang.CmdPackageSignFlagTlogUpload) | ||
| cmd.Flags().BoolVar(&o.confirm, "confirm", false, lang.CmdPackageSignFlagConfirm) | ||
|
|
||
| cmd.MarkFlagsMutuallyExclusive("keyless", "signing-key") | ||
|
|
||
| return cmd | ||
| } | ||
|
|
||
|
|
@@ -1880,8 +1908,8 @@ func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error { | |
| l := logger.From(ctx) | ||
| packageSource := args[0] | ||
|
|
||
| if o.signingKeyPath == "" { | ||
| return errors.New("--signing-key is required") | ||
| if !o.keyless && o.signingKeyPath == "" { | ||
| return errors.New("--signing-key is required (or pass --keyless for Sigstore keyless flow)") | ||
| } | ||
|
|
||
| // Determine output destination | ||
|
|
@@ -1913,31 +1941,34 @@ func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error { | |
| } | ||
| } | ||
|
|
||
| // If output is OCI (either default or user-specified), delegate to publish workflow | ||
| if helpers.IsOCIURL(outputDest) { | ||
| l.Info("signing and publishing package to OCI registry", "source", packageSource, "destination", outputDest) | ||
|
|
||
| // Create publish options from sign options | ||
| publishOpts := &packagePublishOptions{ | ||
| signingKeyPath: o.signingKeyPath, | ||
| signingKeyPassword: o.signingKeyPassword, | ||
| ociConcurrency: o.ociConcurrency, | ||
| retries: o.retries, | ||
| publicKeyPath: o.publicKeyPath, | ||
| verify: o.verify, | ||
| } | ||
|
|
||
| // Call publish with source and destination repository | ||
| return publishOpts.run(cmd, []string{packageSource, outputDest}) | ||
| } | ||
|
|
||
| // For local file output, use existing sign logic | ||
| cachePath, err := getCachePath(ctx) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| // Load the package - do not verify | ||
| // Pull from OCI to a local temp dir before loading | ||
| if helpers.IsOCIURL(packageSource) { | ||
| tmpdir, err := utils.MakeTempDir(config.CommonOptions.TempDirectory) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| defer func() { | ||
| if removeErr := os.RemoveAll(tmpdir); removeErr != nil { | ||
| l.Warn("failed to remove temp dir", "error", removeErr) | ||
| } | ||
| }() | ||
| packageSource, err = packager.Pull(ctx, packageSource, tmpdir, packager.PullOptions{ | ||
| VerificationStrategy: layout.VerifyNever, | ||
| Architecture: config.GetArch(), | ||
| OCIConcurrency: o.ociConcurrency, | ||
| RemoteOptions: defaultRemoteOptions(), | ||
| CachePath: cachePath, | ||
| }) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to pull package: %w", err) | ||
| } | ||
| } | ||
|
|
||
| loadOpts := packager.LoadOptions{ | ||
| Filter: filters.Empty(), | ||
| Architecture: config.GetArch(), | ||
|
|
@@ -1974,20 +2005,57 @@ func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error { | |
| } | ||
| } | ||
|
|
||
| // Sign the package | ||
| l.Info("signing package with provided key") | ||
| if o.keyless { | ||
| l.Info("signing package via Sigstore keyless flow") | ||
| } else { | ||
| l.Info("signing package with provided key") | ||
| } | ||
|
|
||
| signOpts := utils.DefaultSignBlobOptions() | ||
| signOpts := signing.DefaultSignBlobOptions() | ||
| signOpts.Key = o.signingKeyPath | ||
| signOpts.Password = o.signingKeyPassword | ||
| signOpts.Overwrite = o.overwrite | ||
| signOpts.Keyless = o.keyless | ||
| signOpts.Fulcio.IdentityToken = o.identityToken | ||
| signOpts.Fulcio.URL = o.fulcioURL | ||
| signOpts.Fulcio.AuthFlow = o.fulcioAuthFlow | ||
| signOpts.OIDC.Issuer = o.oidcIssuer | ||
| signOpts.OIDC.ClientID = o.oidcClientID | ||
| signOpts.Rekor.URL = o.rekorURL | ||
| signOpts.TlogUpload = o.tlogUpload | ||
| signOpts.SkipConfirmation = o.confirm | ||
|
|
||
| // Keyless certs are short-lived (~10 min). Without Rekor or a TSA timestamp | ||
| // the signature is unverifiable past expiry. Default --tlog-upload=true for | ||
| // keyless unless the user explicitly opted out. | ||
| if o.keyless && !cmd.Flags().Changed("tlog-upload") { | ||
| signOpts.TlogUpload = true | ||
| } | ||
|
Comment on lines
+2031
to
+2033
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the current plan to support tsa using the same enable flag if keyless method? Cosign does use tsa by default and our default trusted root has the |
||
|
|
||
| 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 { | ||
|
|
@@ -2001,6 +2069,13 @@ func (o *packageSignOptions) run(cmd *cobra.Command, args []string) error { | |
| type packageVerifyOptions struct { | ||
| publicKeyPath string | ||
| ociConcurrency int | ||
| // Keyless verify flags. Each is hand-rolled and individually opted-in. | ||
| certificateIdentity string | ||
| certificateIdentityRegexp string | ||
| certificateOIDCIssuer string | ||
| certificateOIDCIssuerRegexp string | ||
| trustedRoot string | ||
| insecureIgnoreTlog bool | ||
| } | ||
|
|
||
| func newPackageVerifyCommand(v *viper.Viper) *cobra.Command { | ||
|
|
@@ -2019,6 +2094,13 @@ func newPackageVerifyCommand(v *viper.Viper) *cobra.Command { | |
| cmd.Flags().StringVarP(&o.publicKeyPath, "key", "k", v.GetString(VPkgPublicKey), lang.CmdPackageVerifyFlagKey) | ||
| cmd.Flags().IntVar(&o.ociConcurrency, "oci-concurrency", v.GetInt(VPkgOCIConcurrency), lang.CmdPackageFlagConcurrency) | ||
|
|
||
| cmd.Flags().StringVar(&o.certificateIdentity, "certificate-identity", v.GetString(VPkgVerifyCertIdentity), lang.CmdPackageVerifyFlagCertificateIdentity) | ||
| cmd.Flags().StringVar(&o.certificateIdentityRegexp, "certificate-identity-regexp", v.GetString(VPkgVerifyCertIdentityRegexp), lang.CmdPackageVerifyFlagCertificateIdentityRegexp) | ||
| cmd.Flags().StringVar(&o.certificateOIDCIssuer, "certificate-oidc-issuer", v.GetString(VPkgVerifyCertOIDCIssuer), lang.CmdPackageVerifyFlagCertificateOIDCIssuer) | ||
| cmd.Flags().StringVar(&o.certificateOIDCIssuerRegexp, "certificate-oidc-issuer-regexp", v.GetString(VPkgVerifyCertOIDCIssuerRegexp), lang.CmdPackageVerifyFlagCertificateOIDCIssuerRegexp) | ||
| cmd.Flags().StringVar(&o.trustedRoot, "trusted-root", v.GetString(VPkgVerifyTrustedRoot), lang.CmdPackageVerifyFlagTrustedRoot) | ||
| cmd.Flags().BoolVar(&o.insecureIgnoreTlog, "insecure-ignore-tlog", v.GetBool(VPkgVerifyInsecureIgnoreTlog), lang.CmdPackageVerifyFlagInsecureIgnoreTlog) | ||
|
|
||
| return cmd | ||
| } | ||
|
|
||
|
|
@@ -2037,8 +2119,24 @@ func (o *packageVerifyOptions) run(cmd *cobra.Command, args []string) error { | |
| // Load the package with verification enabled | ||
| // The verify command always uses strict verification (VerifyAlways) | ||
| // This will error if: signed package without key, or unsigned package with key | ||
| verifyOpts := signing.DefaultVerifyBlobOptions() | ||
| verifyOpts.Key = o.publicKeyPath | ||
| verifyOpts.CertVerify.CertIdentity = o.certificateIdentity | ||
| verifyOpts.CertVerify.CertIdentityRegexp = o.certificateIdentityRegexp | ||
| verifyOpts.CertVerify.CertOidcIssuer = o.certificateOIDCIssuer | ||
| verifyOpts.CertVerify.CertOidcIssuerRegexp = o.certificateOIDCIssuerRegexp | ||
| verifyOpts.CommonVerifyOptions.TrustedRootPath = o.trustedRoot | ||
| verifyOpts.CommonVerifyOptions.IgnoreTlog = o.insecureIgnoreTlog | ||
|
|
||
| // Optimally by default use the inclusion proof to establish when a signature was made. | ||
| // this is offline-compliant and airgap compatible given keyless signed bundle outputs. | ||
| hasKeylessIdentity := o.certificateIdentity != "" || o.certificateIdentityRegexp != "" | ||
| if hasKeylessIdentity && !cmd.Flags().Changed("insecure-ignore-tlog") { | ||
| verifyOpts.CommonVerifyOptions.IgnoreTlog = false | ||
| } | ||
|
|
||
| loadOpts := packager.LoadOptions{ | ||
| VerifyBlobOptions: verifyBlobOptionsFromKeyPath(o.publicKeyPath), | ||
| VerifyBlobOptions: &verifyOpts, | ||
| VerificationStrategy: layout.VerifyAlways, // Always enforce strict verification | ||
| Filter: filters.Empty(), | ||
| Architecture: config.GetArch(), | ||
|
|
@@ -2145,8 +2243,8 @@ func getVerificationStrategy(verify bool) layout.VerificationStrategy { | |
| return layout.VerifyIfPossible | ||
| } | ||
|
|
||
| func verifyBlobOptionsFromKeyPath(keyPath string) *utils.VerifyBlobOptions { | ||
| opts := utils.DefaultVerifyBlobOptions() | ||
| func verifyBlobOptionsFromKeyPath(keyPath string) *signing.VerifyBlobOptions { | ||
| opts := signing.DefaultVerifyBlobOptions() | ||
| opts.Key = keyPath | ||
| return &opts | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Making a mental note to schedule daily checks and notify the zarf channel using this process. A lag in releasing new trusted roots won't be critical but should be nice to have.