From ad93b6f808bc3a4bc1596a4115591e02e81d5d69 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Wed, 5 Aug 2026 04:59:04 +0000 Subject: [PATCH 1/8] feat: direct verification of bundle with sigstore-go Signed-off-by: Brandt Keller --- go.mod | 2 +- src/pkg/packager/layout/package.go | 4 +- src/pkg/signing/cosign_test.go | 125 ++++++++++ src/pkg/signing/sigstore.go | 377 +++++++++++++++++++++++++++++ 4 files changed, 505 insertions(+), 3 deletions(-) create mode 100644 src/pkg/signing/sigstore.go diff --git a/go.mod b/go.mod index c29f644be2..948312da84 100644 --- a/go.mod +++ b/go.mod @@ -570,7 +570,7 @@ require ( github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sigstore/rekor v1.5.3 // indirect - github.com/sigstore/sigstore v1.10.8 // indirect + github.com/sigstore/sigstore v1.10.8 github.com/sirupsen/logrus v1.9.4 github.com/skeema/knownhosts v1.3.1 // indirect github.com/spdx/tools-golang v0.6.0-rc4 // indirect diff --git a/src/pkg/packager/layout/package.go b/src/pkg/packager/layout/package.go index 6bd5d823f0..b071c2756d 100644 --- a/src/pkg/packager/layout/package.go +++ b/src/pkg/packager/layout/package.go @@ -360,7 +360,7 @@ func (p *PackageLayout) VerifyPackageSignature(ctx context.Context, opts signing opts.Key = opts.KeyRef //nolint:staticcheck // intentional read of deprecated alias for migration sync } - hasKey := opts.Key != "" + hasKey := opts.Key != "" || opts.SecurityKey.Use hasKeylessIdentity := opts.CertVerify.CertIdentity != "" || opts.CertVerify.CertIdentityRegexp != "" hasCert := opts.CertVerify.Cert != "" hasVerificationMaterial := hasKey || hasKeylessIdentity || hasCert @@ -413,7 +413,7 @@ func (p *PackageLayout) VerifyPackageSignature(ctx context.Context, opts signing opts.CommonVerifyOptions.UseSignedTimestamps = true } ZarfYAMLPath := filepath.Join(p.dirPath, ZarfYAML) - return signing.CosignVerifyBlobWithOptions(ctx, ZarfYAMLPath, opts) + return signing.SigstoreVerifyBundleWithOptions(ctx, ZarfYAMLPath, opts) } if !errors.Is(bundleErr, os.ErrNotExist) { return fmt.Errorf("error checking bundle signature: %w", bundleErr) diff --git a/src/pkg/signing/cosign_test.go b/src/pkg/signing/cosign_test.go index 12c39891db..61e7359ca8 100644 --- a/src/pkg/signing/cosign_test.go +++ b/src/pkg/signing/cosign_test.go @@ -4,6 +4,11 @@ package signing import ( + "crypto/sha256" + "encoding/hex" + "net" + "net/http" + "net/http/httptest" "os" "path/filepath" "testing" @@ -23,6 +28,126 @@ func TestDefaultSignBlobOptions_EmptyAuthFlow(t *testing.T) { require.Empty(t, opts.Fulcio.AuthFlow) } +func TestSigstoreVerifyBundleWithOptions(t *testing.T) { + ctx := testutil.TestContext(t) + + const keyPath = "./testdata/cosign.key" + const pubPath = "./testdata/cosign.pub" + const password = "test" + + newBundle := func(t *testing.T) (string, string) { + t.Helper() + blobPath := filepath.Join(t.TempDir(), "payload.txt") + bundlePath := filepath.Join(t.TempDir(), "sig.bundle") + require.NoError(t, os.WriteFile(blobPath, []byte("direct verifier payload"), 0o644)) + + signOpts := DefaultSignBlobOptions() + signOpts.Key = keyPath + signOpts.Password = password + signOpts.BundlePath = bundlePath + _, err := CosignSignBlobWithOptions(ctx, blobPath, signOpts) + require.NoError(t, err) + return blobPath, bundlePath + } + + verify := func(t *testing.T, blobPath, bundlePath, key string) error { + t.Helper() + opts := DefaultVerifyBlobOptions() + opts.Key = key + opts.BundlePath = bundlePath + return SigstoreVerifyBundleWithOptions(ctx, blobPath, opts) + } + + t.Run("matches cosign for valid and tampered local-key bundles", func(t *testing.T) { + blobPath, bundlePath := newBundle(t) + require.NoError(t, verify(t, blobPath, bundlePath, pubPath)) + + cosignOpts := DefaultVerifyBlobOptions() + cosignOpts.Key = pubPath + cosignOpts.BundlePath = bundlePath + require.NoError(t, CosignVerifyBlobWithOptions(ctx, blobPath, cosignOpts)) + + require.NoError(t, os.WriteFile(blobPath, []byte("tampered"), 0o644)) + require.Error(t, verify(t, blobPath, bundlePath, pubPath)) + require.Error(t, CosignVerifyBlobWithOptions(ctx, blobPath, cosignOpts)) + }) + + t.Run("matches cosign for digest artifact references", func(t *testing.T) { + blobPath, bundlePath := newBundle(t) + payload, err := os.ReadFile(blobPath) + require.NoError(t, err) + digest := sha256.Sum256(payload) + artifactRef := "sha256:" + hex.EncodeToString(digest[:]) + + directOpts := DefaultVerifyBlobOptions() + directOpts.Key = pubPath + directOpts.BundlePath = bundlePath + require.NoError(t, SigstoreVerifyBundleWithOptions(ctx, artifactRef, directOpts)) + + cosignOpts := directOpts + require.NoError(t, CosignVerifyBlobWithOptions(ctx, artifactRef, cosignOpts)) + }) + + t.Run("accepts environment public-key references", func(t *testing.T) { + blobPath, bundlePath := newBundle(t) + publicKey, err := os.ReadFile(pubPath) + require.NoError(t, err) + t.Setenv("ZARF_TEST_COSIGN_PUBLIC_KEY", string(publicKey)) + require.NoError(t, verify(t, blobPath, bundlePath, "env://ZARF_TEST_COSIGN_PUBLIC_KEY")) + }) + + t.Run("accepts URL public-key references", func(t *testing.T) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Skipf("loopback listener unavailable: %v", err) + } + blobPath, bundlePath := newBundle(t) + publicKey, err := os.ReadFile(pubPath) + require.NoError(t, err) + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if _, err := w.Write(publicKey); err != nil { + t.Errorf("writing public-key response: %v", err) + } + })) + server.Listener = listener + server.Start() + defer server.Close() + require.NoError(t, verify(t, blobPath, bundlePath, server.URL)) + }) + + t.Run("rejects wrong keys and corrupt bundles without cosign fallback", func(t *testing.T) { + blobPath, bundlePath := newBundle(t) + require.Error(t, verify(t, blobPath, bundlePath, "./testdata/nonexistent.pub")) + require.NoError(t, os.WriteFile(bundlePath, []byte("not a bundle"), 0o644)) + require.Error(t, verify(t, blobPath, bundlePath, pubPath)) + }) + + t.Run("rejects detached verification material for bundles", func(t *testing.T) { + blobPath, bundlePath := newBundle(t) + opts := DefaultVerifyBlobOptions() + opts.Key = pubPath + opts.BundlePath = bundlePath + opts.Signature = "detached.sig" + err := SigstoreVerifyBundleWithOptions(ctx, blobPath, opts) + require.ErrorContains(t, err, "--signature") + }) + + t.Run("supports deprecated key alias", func(t *testing.T) { + blobPath, bundlePath := newBundle(t) + opts := DefaultVerifyBlobOptions() + opts.KeyRef = pubPath + opts.BundlePath = bundlePath + require.NoError(t, SigstoreVerifyBundleWithOptions(ctx, blobPath, opts)) + }) + + t.Run("uses embedded trusted root for keyless verification", func(t *testing.T) { + opts := DefaultVerifyBlobOptions() + material, err := trustedMaterialForBundle(opts, nil, false) + require.NoError(t, err) + require.NotEmpty(t, material.FulcioCertificateAuthorities()) + }) +} + func TestShouldSign_KeyRefAlias(t *testing.T) { t.Parallel() diff --git a/src/pkg/signing/sigstore.go b/src/pkg/signing/sigstore.go new file mode 100644 index 0000000000..dc0fafe3e0 --- /dev/null +++ b/src/pkg/signing/sigstore.go @@ -0,0 +1,377 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package signing + +import ( + "bytes" + "context" + "crypto" + "encoding/hex" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + cosigngit "github.com/sigstore/cosign/v3/pkg/cosign/git" + "github.com/sigstore/cosign/v3/pkg/cosign/kubernetes" + "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" + "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" + "github.com/sigstore/sigstore-go/pkg/bundle" + "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/verify" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/kms" + + "github.com/zarf-dev/zarf/src/pkg/logger" +) + +// SigstoreVerifyBundleWithOptions verifies a Sigstore bundle directly with +// sigstore-go. Callers must use CosignVerifyBlobWithOptions for legacy .sig files. +func SigstoreVerifyBundleWithOptions(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 + } + } + if err := validateSigstoreBundleOptions(opts); err != nil { + return err + } + if opts.BundlePath == "" { + return errors.New("provide a bundle with --bundle") + } + if opts.Timeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, opts.Timeout) + defer cancel() + } + + b, err := bundle.LoadJSONFromPath(opts.BundlePath) + if err != nil { + return fmt.Errorf("loading Sigstore bundle: %w", err) + } + + hashAlgorithm, err := opts.SignatureDigest.HashAlgorithm() + if err != nil { + return err + } + keyVerifier, closeKey, err := resolveBundleVerifier(ctx, opts, hashAlgorithm) + if err != nil { + return fmt.Errorf("loading verifier from key options: %w", err) + } + defer closeKey() + + useSignedTimestamps := opts.CommonVerifyOptions.UseSignedTimestamps + if !opts.CommonVerifyOptions.IgnoreTlog && keyVerifier == nil { + v1, v2, err := rekorBundleVersions(b) + if err != nil { + return err + } + // Rekor v2 does not provide an integrated timestamp. This mirrors + // cosign's new-bundle verifier, which enables TSA validation for a + // v2-only bundle automatically. + if v2 && !v1 { + useSignedTimestamps = true + } + } + + trustedMaterial, err := trustedMaterialForBundle(opts, keyVerifier, useSignedTimestamps) + if err != nil { + return err + } + verifierOptions, policyOptions, err := sigstoreVerificationOptions(opts, keyVerifier != nil, useSignedTimestamps) + if err != nil { + return err + } + sev, err := verify.NewVerifier(trustedMaterial, verifierOptions...) + if err != nil { + return fmt.Errorf("creating Sigstore verifier: %w", err) + } + + artifactPolicy, err := bundleArtifactPolicy(blobPath) + if err != nil { + return err + } + if _, err := sev.Verify(b, verify.NewPolicy(artifactPolicy, policyOptions...)); err != nil { + return err + } + l.Debug("blob signature verified successfully with sigstore-go") + return nil +} + +func validateSigstoreBundleOptions(opts VerifyBlobOptions) error { + if opts.Key != "" && (opts.CertVerify.CertIdentity != "" || opts.CertVerify.CertIdentityRegexp != "") { + return errors.New("--key cannot be used with certificate identity verification") + } + if opts.Key != "" && opts.SecurityKey.Use { + return errors.New("--key and --sk cannot be used together") + } + // These options are rejected by cosign for protobuf bundles. Keeping that + // contract prevents detached material from silently weakening verification. + unsupported := []struct { + value string + name string + }{ + {opts.Signature, "--signature"}, + {opts.CertVerify.Cert, "--certificate"}, + {opts.CertVerify.CertChain, "--certificate-chain"}, + {opts.CertVerify.CARoots, "--ca-roots"}, + {opts.CertVerify.CAIntermediates, "--ca-intermediates"}, + {opts.CommonVerifyOptions.TSACertChainPath, "--timestamp-certificate-chain"}, + {opts.CertVerify.SCT, "--sct"}, + } + for _, option := range unsupported { + if option.value != "" { + return fmt.Errorf("unsupported: %s when using bundle format", option.name) + } + } + return nil +} + +func sigstoreVerificationOptions(opts VerifyBlobOptions, hasKey bool, useSignedTimestamps bool) ([]verify.VerifierOption, []verify.PolicyOption, error) { + verifierOptions := []verify.VerifierOption{} + policyOptions := []verify.PolicyOption{} + if hasKey { + policyOptions = append(policyOptions, verify.WithKey()) + } else { + san, err := verify.NewSANMatcher(opts.CertVerify.CertIdentity, opts.CertVerify.CertIdentityRegexp) + if err != nil { + return nil, nil, err + } + issuer, err := verify.NewIssuerMatcher(opts.CertVerify.CertOidcIssuer, opts.CertVerify.CertOidcIssuerRegexp) + if err != nil { + return nil, nil, err + } + identity, err := verify.NewCertificateIdentity(san, issuer, certificate.Extensions{ + GithubWorkflowTrigger: opts.CertVerify.CertGithubWorkflowTrigger, + GithubWorkflowSHA: opts.CertVerify.CertGithubWorkflowSha, + GithubWorkflowName: opts.CertVerify.CertGithubWorkflowName, + GithubWorkflowRepository: opts.CertVerify.CertGithubWorkflowRepository, + GithubWorkflowRef: opts.CertVerify.CertGithubWorkflowRef, + }) + if err != nil { + return nil, nil, err + } + policyOptions = append(policyOptions, verify.WithCertificateIdentity(identity)) + if !opts.CertVerify.IgnoreSCT { + verifierOptions = append(verifierOptions, verify.WithSignedCertificateTimestamps(1)) + } + } + + if !opts.CommonVerifyOptions.IgnoreTlog { + verifierOptions = append(verifierOptions, verify.WithTransparencyLog(1)) + if !useSignedTimestamps { + if hasKey { + verifierOptions = append(verifierOptions, verify.WithNoObserverTimestamps()) + } else { + verifierOptions = append(verifierOptions, verify.WithIntegratedTimestamps(1)) + } + } + } + if useSignedTimestamps { + verifierOptions = append(verifierOptions, verify.WithSignedTimestamps(1)) + } + if opts.CommonVerifyOptions.IgnoreTlog && !useSignedTimestamps { + if hasKey { + verifierOptions = append(verifierOptions, verify.WithNoObserverTimestamps()) + } else { + verifierOptions = append(verifierOptions, verify.WithCurrentTime()) + } + } + return verifierOptions, policyOptions, nil +} + +func trustedMaterialForBundle(opts VerifyBlobOptions, keyVerifier signature.Verifier, useSignedTimestamps bool) (root.TrustedMaterial, error) { + var material root.TrustedMaterial = &root.BaseTrustedMaterial{} + needRoot := opts.CommonVerifyOptions.TrustedRootPath != "" || keyVerifier == nil || !opts.CommonVerifyOptions.IgnoreTlog || useSignedTimestamps + if needRoot { + var err error + if path := opts.CommonVerifyOptions.TrustedRootPath; path != "" { + material, err = root.NewTrustedRootFromPath(path) + } else if keyVerifier == nil { + material, err = root.NewTrustedRootFromJSON(embeddedTrustedRoot) + } else { + // Cosign fetches TUF material for key verification when the caller + // asks to verify a tlog entry or TSA timestamp. + material, err = root.FetchTrustedRoot() + } + if err != nil { + return nil, fmt.Errorf("loading trusted root: %w", err) + } + } + if keyVerifier == nil { + return material, nil + } + expiringKey := root.NewExpiringKey(keyVerifier, time.Time{}, time.Time{}) + keyMaterial := root.NewTrustedPublicKeyMaterial(func(_ string) (root.TimeConstrainedVerifier, error) { + return expiringKey, nil + }) + return root.TrustedMaterialCollection{material, keyMaterial}, nil +} + +func rekorBundleVersions(b *bundle.Bundle) (hasV1, hasV2 bool, err error) { + entries, err := b.TlogEntries() + if err != nil { + return false, false, err + } + for _, entry := range entries { + if entry.IntegratedTime().IsZero() { + hasV2 = true + } else { + hasV1 = true + } + } + return hasV1, hasV2, nil +} + +func resolveBundleVerifier(ctx context.Context, opts VerifyBlobOptions, hashAlgorithm crypto.Hash) (signature.Verifier, func(), error) { + if opts.SecurityKey.Use { + key, err := pivkey.GetKeyWithSlot(opts.SecurityKey.Slot) + if err != nil { + return nil, func() {}, err + } + verifier, err := key.Verifier() + if err != nil { + key.Close() + return nil, func() {}, err + } + return verifier, key.Close, nil + } + if opts.Key == "" { + return nil, func() {}, nil + } + if strings.HasPrefix(opts.Key, "k8s://") { + secret, err := kubernetes.GetKeyPairSecret(ctx, opts.Key) + if err != nil { + return nil, func() {}, err + } + return verifierFromPEM(secret.Data["cosign.pub"], hashAlgorithm) + } + if strings.HasPrefix(opts.Key, "gitlab://") { + provider, reference, ok := strings.Cut(opts.Key, "://") + if !ok || reference == "" { + return nil, func() {}, errors.New("could not parse key reference, use gitlab://") + } + gitProvider := cosigngit.GetProvider(provider) + if gitProvider == nil { + return nil, func() {}, fmt.Errorf("no git provider found for %q", provider) + } + publicKey, err := gitProvider.GetSecret(ctx, reference, "COSIGN_PUBLIC_KEY") + if err != nil { + return nil, func() {}, err + } + return verifierFromPEM([]byte(publicKey), hashAlgorithm) + } + if strings.HasPrefix(opts.Key, "pkcs11:") { + config := pkcs11key.NewPkcs11UriConfig() + if err := config.Parse(opts.Key); err != nil { + return nil, func() {}, fmt.Errorf("parsing pkcs11 uri: %w", err) + } + key, err := pkcs11key.GetKeyWithURIConfig(config, false) + if err != nil { + return nil, func() {}, fmt.Errorf("opening pkcs11 token key: %w", err) + } + verifier, err := key.Verifier() + if err != nil { + key.Close() + return nil, func() {}, fmt.Errorf("initializing pkcs11 token verifier: %w", err) + } + return verifier, key.Close, nil + } + + verifier, err := kms.Get(ctx, opts.Key, hashAlgorithm) + if err == nil { + return verifier, func() {}, nil + } + var providerNotFound *kms.ProviderNotFoundError + if !errors.As(err, &providerNotFound) { + return nil, func() {}, fmt.Errorf("kms get: %w", err) + } + raw, err := loadPublicKeyReference(opts.Key) + if err != nil { + return nil, func() {}, err + } + return verifierFromPEM(raw, hashAlgorithm) +} + +func verifierFromPEM(raw []byte, hashAlgorithm crypto.Hash) (signature.Verifier, func(), error) { + publicKey, err := cryptoutils.UnmarshalPEMToPublicKey(raw) + if err != nil { + return nil, func() {}, fmt.Errorf("pem to public key: %w", err) + } + verifier, err := signature.LoadVerifier(publicKey, hashAlgorithm) + return verifier, func() {}, err +} + +func loadPublicKeyReference(reference string) ([]byte, error) { + switch { + case strings.HasPrefix(reference, "env://"): + value, ok := os.LookupEnv(strings.TrimPrefix(reference, "env://")) + if !ok { + return nil, fmt.Errorf("loading URL: env var $%s not found", strings.TrimPrefix(reference, "env://")) + } + return []byte(value), nil + case strings.HasPrefix(reference, "http://") || strings.HasPrefix(reference, "https://"): + // #nosec G107 -- the public key location is an explicit user input. + response, err := http.Get(reference) + if err != nil { + return nil, err + } + if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices { + if closeErr := response.Body.Close(); closeErr != nil { + return nil, closeErr + } + return nil, fmt.Errorf("loading URL %s: server returned HTTP %d", reference, response.StatusCode) + } + raw, readErr := io.ReadAll(response.Body) + if closeErr := response.Body.Close(); closeErr != nil && readErr == nil { + return nil, closeErr + } + return raw, readErr + case strings.Contains(reference, "://"): + return nil, fmt.Errorf("loading URL: unrecognized scheme: %s", strings.SplitN(reference, "://", 2)[0]+"://") + default: + return os.ReadFile(filepath.Clean(reference)) + } +} + +func readBundleArtifact(reference string) ([]byte, error) { + if reference == "-" { + return io.ReadAll(os.Stdin) + } + return loadPublicKeyReference(reference) +} + +// bundleArtifactPolicy mirrors cosign's blob verifier: an unreadable artifact +// may instead be an explicitly supplied algorithm:hex-digest reference. +func bundleArtifactPolicy(reference string) (verify.ArtifactPolicyOption, error) { + artifact, readErr := readBundleArtifact(reference) + if readErr == nil { + return verify.WithArtifact(bytes.NewReader(artifact)), nil + } + + algorithm, encodedDigest, found := strings.Cut(reference, ":") + if !found { + return nil, readErr + } + digest, err := hex.DecodeString(encodedDigest) + if err != nil { + return nil, err + } + return verify.WithArtifactDigest(algorithm, digest), nil +} From 051bd769cb5ed86f1f2b276ae4e15c29163f25e2 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Wed, 5 Aug 2026 05:20:22 +0000 Subject: [PATCH 2/8] fix: testing assertions update Signed-off-by: Brandt Keller --- src/test/e2e/34_custom_init_package_test.go | 4 +++- 1 file changed, 3 insertions(+), 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 c6ecbe56d5..4388eefde5 100644 --- a/src/test/e2e/34_custom_init_package_test.go +++ b/src/test/e2e/34_custom_init_package_test.go @@ -32,7 +32,9 @@ func TestCustomInit(t *testing.T) { // Test that we don't get an error when we remember to provide the public key stdOut, stdErr, err = e2e.Zarf(t, "package", "inspect", "definition", pkgName, publicKeyFlag) require.NoError(t, err, stdOut, stdErr) - require.Contains(t, stdErr, "Verified OK") + // Bundle verification is performed directly by sigstore-go, which does not + // emit cosign's legacy success text. + require.NotContains(t, stdErr, "Verified OK") /* Test operations during package deploy */ // Test that we get an error when trying to deploy a package without providing the public key From 8830cdc26108fcc22112be0650bdcf13546c8c54 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Thu, 6 Aug 2026 03:52:20 +0000 Subject: [PATCH 3/8] fix: decouple from cosign further - work on parity Signed-off-by: Brandt Keller --- src/pkg/signing/cosign_test.go | 116 +++++++++++++++++- src/pkg/signing/sigstore.go | 24 ++-- ...sigstore-js-2.0.0-provenance.sigstore.json | 60 +++++++++ src/pkg/signing/trustedroot.go | 69 +++++++++++ src/pkg/signing/trustedroot_test.go | 90 ++++++++++++++ 5 files changed, 346 insertions(+), 13 deletions(-) create mode 100644 src/pkg/signing/testdata/sigstore-js-2.0.0-provenance.sigstore.json diff --git a/src/pkg/signing/cosign_test.go b/src/pkg/signing/cosign_test.go index 61e7359ca8..4117417f93 100644 --- a/src/pkg/signing/cosign_test.go +++ b/src/pkg/signing/cosign_test.go @@ -129,7 +129,8 @@ func TestSigstoreVerifyBundleWithOptions(t *testing.T) { opts.BundlePath = bundlePath opts.Signature = "detached.sig" err := SigstoreVerifyBundleWithOptions(ctx, blobPath, opts) - require.ErrorContains(t, err, "--signature") + require.ErrorContains(t, err, "detached signature") + require.NotContains(t, err.Error(), "--") }) t.Run("supports deprecated key alias", func(t *testing.T) { @@ -146,6 +147,119 @@ func TestSigstoreVerifyBundleWithOptions(t *testing.T) { require.NoError(t, err) require.NotEmpty(t, material.FulcioCertificateAuthorities()) }) + + t.Run("verifies keyless public-good bundle", func(t *testing.T) { + opts := DefaultVerifyBlobOptions() + opts.BundlePath = "./testdata/sigstore-js-2.0.0-provenance.sigstore.json" + opts.CertVerify.CertIdentityRegexp = "^https://github.com/sigstore/sigstore-js/" + opts.CertVerify.CertOidcIssuer = "https://token.actions.githubusercontent.com" + opts.CommonVerifyOptions.IgnoreTlog = false + + const digestReference = "sha512:46d4e2f74c4877316640000a6fdf8a8b59f1e0847667973e9859f774dd31b8f1e0937813b777fb66a2ac67d50540fe34640966eee9fc2ccca387082b4c85cd3c" + require.NoError(t, SigstoreVerifyBundleWithOptions(ctx, digestReference, opts)) + + invalidOpts := opts + invalidOpts.CertVerify.CertIdentityRegexp = "^https://github.com/sigstore/other-project/" + require.Error(t, SigstoreVerifyBundleWithOptions(ctx, digestReference, invalidOpts)) + }) +} + +func TestSigstoreBundleValidationErrorsUseLibraryTerms(t *testing.T) { + tests := []struct { + name string + configure func(*VerifyBlobOptions) + want string + }{ + { + name: "requires bundle path", + want: "bundle path is required", + }, + { + name: "rejects key with certificate identity", + configure: func(opts *VerifyBlobOptions) { + opts.BundlePath = "bundle.json" + opts.Key = "key.pem" + opts.CertVerify.CertIdentity = "https://example.test" + }, + want: "key cannot be combined with certificate identity verification", + }, + { + name: "rejects key with security key", + configure: func(opts *VerifyBlobOptions) { + opts.BundlePath = "bundle.json" + opts.Key = "key.pem" + opts.SecurityKey.Use = true + }, + want: "key cannot be combined with security-key verification", + }, + { + name: "rejects detached signature", + configure: func(opts *VerifyBlobOptions) { + opts.Signature = "signature" + }, + want: "unsupported verification material for Sigstore bundles: detached signature", + }, + { + name: "rejects certificate", + configure: func(opts *VerifyBlobOptions) { + opts.CertVerify.Cert = "certificate" + }, + want: "unsupported verification material for Sigstore bundles: certificate", + }, + { + name: "rejects certificate chain", + configure: func(opts *VerifyBlobOptions) { + opts.CertVerify.CertChain = "chain" + }, + want: "unsupported verification material for Sigstore bundles: certificate chain", + }, + { + name: "rejects certificate authority roots", + configure: func(opts *VerifyBlobOptions) { + opts.CertVerify.CARoots = "roots" + }, + want: "unsupported verification material for Sigstore bundles: certificate authority roots", + }, + { + name: "rejects certificate authority intermediates", + configure: func(opts *VerifyBlobOptions) { + opts.CertVerify.CAIntermediates = "intermediates" + }, + want: "unsupported verification material for Sigstore bundles: certificate authority intermediates", + }, + { + name: "rejects timestamp certificate chain", + configure: func(opts *VerifyBlobOptions) { + opts.CommonVerifyOptions.TSACertChainPath = "timestamp-chain" + }, + want: "unsupported verification material for Sigstore bundles: timestamp certificate chain", + }, + { + name: "rejects signed certificate timestamp", + configure: func(opts *VerifyBlobOptions) { + opts.CertVerify.SCT = "sct" + }, + want: "unsupported verification material for Sigstore bundles: signed certificate timestamp", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + opts := DefaultVerifyBlobOptions() + if tc.configure != nil { + tc.configure(&opts) + } + + var err error + if opts.BundlePath == "" { + err = SigstoreVerifyBundleWithOptions(testutil.TestContext(t), "", opts) + } else { + err = validateSigstoreBundleOptions(opts) + } + require.ErrorContains(t, err, tc.want) + require.NotContains(t, err.Error(), "--") + }) + } } func TestShouldSign_KeyRefAlias(t *testing.T) { diff --git a/src/pkg/signing/sigstore.go b/src/pkg/signing/sigstore.go index dc0fafe3e0..80dc52c3ac 100644 --- a/src/pkg/signing/sigstore.go +++ b/src/pkg/signing/sigstore.go @@ -53,7 +53,7 @@ func SigstoreVerifyBundleWithOptions(ctx context.Context, blobPath string, opts return err } if opts.BundlePath == "" { - return errors.New("provide a bundle with --bundle") + return errors.New("bundle path is required") } if opts.Timeout > 0 { var cancel context.CancelFunc @@ -116,10 +116,10 @@ func SigstoreVerifyBundleWithOptions(ctx context.Context, blobPath string, opts func validateSigstoreBundleOptions(opts VerifyBlobOptions) error { if opts.Key != "" && (opts.CertVerify.CertIdentity != "" || opts.CertVerify.CertIdentityRegexp != "") { - return errors.New("--key cannot be used with certificate identity verification") + return errors.New("key cannot be combined with certificate identity verification") } if opts.Key != "" && opts.SecurityKey.Use { - return errors.New("--key and --sk cannot be used together") + return errors.New("key cannot be combined with security-key verification") } // These options are rejected by cosign for protobuf bundles. Keeping that // contract prevents detached material from silently weakening verification. @@ -127,17 +127,17 @@ func validateSigstoreBundleOptions(opts VerifyBlobOptions) error { value string name string }{ - {opts.Signature, "--signature"}, - {opts.CertVerify.Cert, "--certificate"}, - {opts.CertVerify.CertChain, "--certificate-chain"}, - {opts.CertVerify.CARoots, "--ca-roots"}, - {opts.CertVerify.CAIntermediates, "--ca-intermediates"}, - {opts.CommonVerifyOptions.TSACertChainPath, "--timestamp-certificate-chain"}, - {opts.CertVerify.SCT, "--sct"}, + {opts.Signature, "detached signature"}, + {opts.CertVerify.Cert, "certificate"}, + {opts.CertVerify.CertChain, "certificate chain"}, + {opts.CertVerify.CARoots, "certificate authority roots"}, + {opts.CertVerify.CAIntermediates, "certificate authority intermediates"}, + {opts.CommonVerifyOptions.TSACertChainPath, "timestamp certificate chain"}, + {opts.CertVerify.SCT, "signed certificate timestamp"}, } for _, option := range unsupported { if option.value != "" { - return fmt.Errorf("unsupported: %s when using bundle format", option.name) + return fmt.Errorf("unsupported verification material for Sigstore bundles: %s", option.name) } } return nil @@ -208,7 +208,7 @@ func trustedMaterialForBundle(opts VerifyBlobOptions, keyVerifier signature.Veri } else { // Cosign fetches TUF material for key verification when the caller // asks to verify a tlog entry or TSA timestamp. - material, err = root.FetchTrustedRoot() + material, err = configuredLiveTrustedRoot() } if err != nil { return nil, fmt.Errorf("loading trusted root: %w", err) diff --git a/src/pkg/signing/testdata/sigstore-js-2.0.0-provenance.sigstore.json b/src/pkg/signing/testdata/sigstore-js-2.0.0-provenance.sigstore.json new file mode 100644 index 0000000000..628496f378 --- /dev/null +++ b/src/pkg/signing/testdata/sigstore-js-2.0.0-provenance.sigstore.json @@ -0,0 +1,60 @@ +{ + "mediaType": "application/vnd.dev.sigstore.bundle+json;version=0.1", + "verificationMaterial": { + "x509CertificateChain": { + "certificates": [ + { + "rawBytes": "MIIGtzCCBjygAwIBAgIUfd/5FN88EX4bwp7c7Q5ZrOXgRw4wCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjMwODE4MTYwNTM1WhcNMjMwODE4MTYxNTM1WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2CZZ4gTXAq4i5mYEl36bdw+RUVA1IaC5uw6IsBwiyfE/DLsMnbPpb/0vwXEh0d1FDWeel5RZd19wT+I0eD8sLKOCBVswggVXMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUIHAeQbQZz9vBuCr+LkarZTn38CkwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wYwYDVR0RAQH/BFkwV4ZVaHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlL3NpZ3N0b3JlLWpzLy5naXRodWIvd29ya2Zsb3dzL3JlbGVhc2UueW1sQHJlZnMvaGVhZHMvbWFpbjA5BgorBgEEAYO/MAEBBCtodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tMBIGCisGAQQBg78wAQIEBHB1c2gwNgYKKwYBBAGDvzABAwQoZjBiNDlhMDRlNWE2MjI1MGUwZjYwZmIxMjgwMDRhNzMxMTBmZTMxMTAVBgorBgEEAYO/MAEEBAdSZWxlYXNlMCIGCisGAQQBg78wAQUEFHNpZ3N0b3JlL3NpZ3N0b3JlLWpzMB0GCisGAQQBg78wAQYED3JlZnMvaGVhZHMvbWFpbjA7BgorBgEEAYO/MAEIBC0MK2h0dHBzOi8vdG9rZW4uYWN0aW9ucy5naXRodWJ1c2VyY29udGVudC5jb20wZQYKKwYBBAGDvzABCQRXDFVodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55bWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wAQoEKgwoZjBiNDlhMDRlNWE2MjI1MGUwZjYwZmIxMjgwMDRhNzMxMTBmZTMxMTAdBgorBgEEAYO/MAELBA8MDWdpdGh1Yi1ob3N0ZWQwNwYKKwYBBAGDvzABDAQpDCdodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMwOAYKKwYBBAGDvzABDQQqDChmMGI0OWEwNGU1YTYyMjUwZTBmNjBmYjEyODAwNGE3MzExMGZlMzExMB8GCisGAQQBg78wAQ4EEQwPcmVmcy9oZWFkcy9tYWluMBkGCisGAQQBg78wAQ8ECwwJNDk1NTc0NTU1MCsGCisGAQQBg78wARAEHQwbaHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlMBgGCisGAQQBg78wAREECgwINzEwOTYzNTMwZQYKKwYBBAGDvzABEgRXDFVodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMvLmdpdGh1Yi93b3JrZmxvd3MvcmVsZWFzZS55bWxAcmVmcy9oZWFkcy9tYWluMDgGCisGAQQBg78wARMEKgwoZjBiNDlhMDRlNWE2MjI1MGUwZjYwZmIxMjgwMDRhNzMxMTBmZTMxMTAUBgorBgEEAYO/MAEUBAYMBHB1c2gwWgYKKwYBBAGDvzABFQRMDEpodHRwczovL2dpdGh1Yi5jb20vc2lnc3RvcmUvc2lnc3RvcmUtanMvYWN0aW9ucy9ydW5zLzU5MDQ2OTY3NjQvYXR0ZW1wdHMvMTAWBgorBgEEAYO/MAEWBAgMBnB1YmxpYzCBiwYKKwYBBAHWeQIEAgR9BHsAeQB3AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAvKe6OAAABigllGRAAAAQDAEgwRgIhAI+83BJd9c8hMU3oN33BSGow7UM4bs9jBGjoPZKu1SJSAiEAocFiN6CQF8tl+Ys1A39ctFFxOFn2Cr5NaO89QzbGVNUwCgYIKoZIzj0EAwMDaQAwZgIxAMCitzMG8PVXCibkqAYHOEcirlSuNdqLOGSxjvQvZq+n/LQDAXPGovz//vUH3HUZLAIxAJ8PpZWpESht+wC/n1+2TEGBB7aEIAJbcFYJ2AqFQIIjjsTcBLmNJT3EDAgtJCHFHA==" + } + ] + }, + "tlogEntries": [ + { + "logIndex": "31821305", + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + }, + "kindVersion": { + "kind": "intoto", + "version": "0.0.2" + }, + "integratedTime": "1692374735", + "inclusionPromise": { + "signedEntryTimestamp": "MEQCIBIG9TnhANgIZKrx20e1YQ0V7rnVs4/cKTf9tn3Y+NVIAiB8A0UwYu+Mc+E9pcP9ju7QOQYvLk8NajSeLp6sPLB1aA==" + }, + "inclusionProof": { + "logIndex": "27657874", + "rootHash": "v+7gOn1wovHHKBEVizJ5FFgTKUBCN9UxLo5KQ1Jz8cw=", + "treeSize": "27657875", + "hashes": [ + "/pZbqoFwAGIZaonQ2KdQj3HSGP7/4yfdZBUxKadw9Z8=", + "xZNrgfzUc8Ys5AKdeIpQ91hqM3mgCVdekTXsrM3GeBk=", + "0vtqRSUOxFOmLkErow/DJ4p9SYw2PsjCgIRfKa7/twg=", + "KXsEVwvzXH3v7vszv53J+jiAoKq1S9NCESUsKPStlUE=", + "NTFwGNVKjiF6zpAaoug3Zdn4bcdMPFje53W1Nq5UgEI=", + "aOgwCE1YnPdqr2RqEQElhpXvw1/6v+l9KuwI8pDg/j8=", + "ZW26eQRJVw4L+5bsecao28mT5P+mmfOQkz1yVnnLHOY=", + "uLuBRins5nkqq2rqd17R27pQTUF+xetttC6MsmlUzd0=", + "jRUq4D8O+FI47Wbw96s7yHCu4qzWUxpIVfxQEeprDmc=", + "rXEsmEJN4PEoTU8US4qVtdIsGB1MCiRlGOepoiC99kM=" + ], + "checkpoint": { + "envelope": "rekor.sigstore.dev - 2605736670972794746\n27657875\nv+7gOn1wovHHKBEVizJ5FFgTKUBCN9UxLo5KQ1Jz8cw=\nTimestamp: 1692374735595899989\n\n— rekor.sigstore.dev wNI9ajBEAiAzHmfHSCMNTSzP9h0Pzzdg95z3uaFP2n1992qoazwr5AIgPdgJIrzOe2CRYLLZTjMWFe9pBIg0r2hAevmsWrnXSyk=\n" + } + }, + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjIiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7ImVudmVsb3BlIjp7InBheWxvYWRUeXBlIjoiYXBwbGljYXRpb24vdm5kLmluLXRvdG8ranNvbiIsInNpZ25hdHVyZXMiOlt7InB1YmxpY0tleSI6IkxTMHRMUzFDUlVkSlRpQkRSVkpVU1VaSlEwRlVSUzB0TFMwdENrMUpTVWQwZWtORFFtcDVaMEYzU1VKQlowbFZabVF2TlVaT09EaEZXRFJpZDNBM1l6ZFJOVnB5VDFoblVuYzBkME5uV1VsTGIxcEplbW93UlVGM1RYY0tUbnBGVmsxQ1RVZEJNVlZGUTJoTlRXTXliRzVqTTFKMlkyMVZkVnBIVmpKTlVqUjNTRUZaUkZaUlVVUkZlRlo2WVZka2VtUkhPWGxhVXpGd1ltNVNiQXBqYlRGc1drZHNhR1JIVlhkSWFHTk9UV3BOZDA5RVJUUk5WRmwzVGxSTk1WZG9ZMDVOYWsxM1QwUkZORTFVV1hoT1ZFMHhWMnBCUVUxR2EzZEZkMWxJQ2t0dldrbDZhakJEUVZGWlNVdHZXa2w2YWpCRVFWRmpSRkZuUVVVeVExcGFOR2RVV0VGeE5HazFiVmxGYkRNMlltUjNLMUpWVmtFeFNXRkROWFYzTmtrS2MwSjNhWGxtUlM5RVRITk5ibUpRY0dJdk1IWjNXRVZvTUdReFJrUlhaV1ZzTlZKYVpERTVkMVFyU1RCbFJEaHpURXRQUTBKV2MzZG5aMVpZVFVFMFJ3cEJNVlZrUkhkRlFpOTNVVVZCZDBsSVowUkJWRUpuVGxaSVUxVkZSRVJCUzBKblozSkNaMFZHUWxGalJFRjZRV1JDWjA1V1NGRTBSVVpuVVZWSlNFRmxDbEZpVVZwNk9YWkNkVU55SzB4cllYSmFWRzR6T0VOcmQwaDNXVVJXVWpCcVFrSm5kMFp2UVZVek9WQndlakZaYTBWYVlqVnhUbXB3UzBaWGFYaHBORmtLV2tRNGQxbDNXVVJXVWpCU1FWRklMMEpHYTNkV05GcFdZVWhTTUdOSVRUWk1lVGx1WVZoU2IyUlhTWFZaTWpsMFRETk9jRm96VGpCaU0wcHNURE5PY0FwYU0wNHdZak5LYkV4WGNIcE1lVFZ1WVZoU2IyUlhTWFprTWpsNVlUSmFjMkl6WkhwTU0wcHNZa2RXYUdNeVZYVmxWekZ6VVVoS2JGcHVUWFpoUjFab0NscElUWFppVjBad1ltcEJOVUpuYjNKQ1owVkZRVmxQTDAxQlJVSkNRM1J2WkVoU2QyTjZiM1pNTTFKMllUSldkVXh0Um1wa1IyeDJZbTVOZFZveWJEQUtZVWhXYVdSWVRteGpiVTUyWW01U2JHSnVVWFZaTWpsMFRVSkpSME5wYzBkQlVWRkNaemM0ZDBGUlNVVkNTRUl4WXpKbmQwNW5XVXRMZDFsQ1FrRkhSQXAyZWtGQ1FYZFJiMXBxUW1sT1JHeG9UVVJTYkU1WFJUSk5ha2t4VFVkVmQxcHFXWGRhYlVsNFRXcG5kMDFFVW1oT2VrMTRUVlJDYlZwVVRYaE5WRUZXQ2tKbmIzSkNaMFZGUVZsUEwwMUJSVVZDUVdSVFdsZDRiRmxZVG14TlEwbEhRMmx6UjBGUlVVSm5OemgzUVZGVlJVWklUbkJhTTA0d1lqTktiRXd6VG5BS1dqTk9NR0l6U214TVYzQjZUVUl3UjBOcGMwZEJVVkZDWnpjNGQwRlJXVVZFTTBwc1dtNU5kbUZIVm1oYVNFMTJZbGRHY0dKcVFUZENaMjl5UW1kRlJRcEJXVTh2VFVGRlNVSkRNRTFMTW1nd1pFaENlazlwT0haa1J6bHlXbGMwZFZsWFRqQmhWemwxWTNrMWJtRllVbTlrVjBveFl6SldlVmt5T1hWa1IxWjFDbVJETldwaU1qQjNXbEZaUzB0M1dVSkNRVWRFZG5wQlFrTlJVbGhFUmxadlpFaFNkMk42YjNaTU1tUndaRWRvTVZscE5XcGlNakIyWXpKc2JtTXpVbllLWTIxVmRtTXliRzVqTTFKMlkyMVZkR0Z1VFhaTWJXUndaRWRvTVZscE9UTmlNMHB5V20xNGRtUXpUWFpqYlZaeldsZEdlbHBUTlRWaVYzaEJZMjFXYlFwamVUbHZXbGRHYTJONU9YUlpWMngxVFVSblIwTnBjMGRCVVZGQ1p6YzRkMEZSYjBWTFozZHZXbXBDYVU1RWJHaE5SRkpzVGxkRk1rMXFTVEZOUjFWM0NscHFXWGRhYlVsNFRXcG5kMDFFVW1oT2VrMTRUVlJDYlZwVVRYaE5WRUZrUW1kdmNrSm5SVVZCV1U4dlRVRkZURUpCT0UxRVYyUndaRWRvTVZscE1XOEtZak5PTUZwWFVYZE9kMWxMUzNkWlFrSkJSMFIyZWtGQ1JFRlJjRVJEWkc5a1NGSjNZM3B2ZGt3eVpIQmtSMmd4V1drMWFtSXlNSFpqTW14dVl6TlNkZ3BqYlZWMll6SnNibU16VW5aamJWVjBZVzVOZDA5QldVdExkMWxDUWtGSFJIWjZRVUpFVVZGeFJFTm9iVTFIU1RCUFYwVjNUa2RWTVZsVVdYbE5hbFYzQ2xwVVFtMU9ha0p0V1dwRmVVOUVRWGRPUjBVelRYcEZlRTFIV214TmVrVjRUVUk0UjBOcGMwZEJVVkZDWnpjNGQwRlJORVZGVVhkUVkyMVdiV041T1c4S1dsZEdhMk41T1hSWlYyeDFUVUpyUjBOcGMwZEJVVkZDWnpjNGQwRlJPRVZEZDNkS1RrUnJNVTVVWXpCT1ZGVXhUVU56UjBOcGMwZEJVVkZDWnpjNGR3cEJVa0ZGU0ZGM1ltRklVakJqU0UwMlRIazVibUZZVW05a1YwbDFXVEk1ZEV3elRuQmFNMDR3WWpOS2JFMUNaMGREYVhOSFFWRlJRbWMzT0hkQlVrVkZDa05uZDBsT2VrVjNUMVJaZWs1VVRYZGFVVmxMUzNkWlFrSkJSMFIyZWtGQ1JXZFNXRVJHVm05a1NGSjNZM3B2ZGt3eVpIQmtSMmd4V1drMWFtSXlNSFlLWXpKc2JtTXpVblpqYlZWMll6SnNibU16VW5aamJWVjBZVzVOZGt4dFpIQmtSMmd4V1drNU0ySXpTbkphYlhoMlpETk5kbU50Vm5OYVYwWjZXbE0xTlFwaVYzaEJZMjFXYldONU9XOWFWMFpyWTNrNWRGbFhiSFZOUkdkSFEybHpSMEZSVVVKbk56aDNRVkpOUlV0bmQyOWFha0pwVGtSc2FFMUVVbXhPVjBVeUNrMXFTVEZOUjFWM1dtcFpkMXB0U1hoTmFtZDNUVVJTYUU1NlRYaE5WRUp0V2xSTmVFMVVRVlZDWjI5eVFtZEZSVUZaVHk5TlFVVlZRa0ZaVFVKSVFqRUtZekpuZDFkbldVdExkMWxDUWtGSFJIWjZRVUpHVVZKTlJFVndiMlJJVW5kamVtOTJUREprY0dSSGFERlphVFZxWWpJd2RtTXliRzVqTTFKMlkyMVZkZ3BqTW14dVl6TlNkbU50VlhSaGJrMTJXVmRPTUdGWE9YVmplVGw1WkZjMWVreDZWVFZOUkZFeVQxUlpNMDVxVVhaWldGSXdXbGN4ZDJSSVRYWk5WRUZYQ2tKbmIzSkNaMFZGUVZsUEwwMUJSVmRDUVdkTlFtNUNNVmx0ZUhCWmVrTkNhWGRaUzB0M1dVSkNRVWhYWlZGSlJVRm5VamxDU0hOQlpWRkNNMEZPTURrS1RVZHlSM2g0UlhsWmVHdGxTRXBzYms1M1MybFRiRFkwTTJwNWRDODBaVXRqYjBGMlMyVTJUMEZCUVVKcFoyeHNSMUpCUVVGQlVVUkJSV2QzVW1kSmFBcEJTU3M0TTBKS1pEbGpPR2hOVlROdlRqTXpRbE5IYjNjM1ZVMDBZbk01YWtKSGFtOVFXa3QxTVZOS1UwRnBSVUZ2WTBacFRqWkRVVVk0ZEd3cldYTXhDa0V6T1dOMFJrWjRUMFp1TWtOeU5VNWhUemc1VVhwaVIxWk9WWGREWjFsSlMyOWFTWHBxTUVWQmQwMUVZVkZCZDFwblNYaEJUVU5wZEhwTlJ6aFFWbGdLUTJsaWEzRkJXVWhQUldOcGNteFRkVTVrY1V4UFIxTjRhblpSZGxweEsyNHZURkZFUVZoUVIyOTJlaTh2ZGxWSU0waFZXa3hCU1hoQlNqaFFjRnBYY0FwRlUyaDBLM2RETDI0eEt6SlVSVWRDUWpkaFJVbEJTbUpqUmxsS01rRnhSbEZKU1dwcWMxUmpRa3h0VGtwVU0wVkVRV2QwU2tOSVJraEJQVDBLTFMwdExTMUZUa1FnUTBWU1ZFbEdTVU5CVkVVdExTMHRMUT09Iiwic2lnIjoiVFVWUlEwbEdWM0pRY0ROcE5UaHpibFZKYXpsSU5UbG9lbmxZU0hwUVJuTXpLMGRhUkhBclEzcGtUa3RZWTBKRlFXbENVVkZxZGxWaFZFZDRTMmxQUjJ4SE1VZFJlRXRzT1RGWldrVTRhMFZZTW5kaFVYQnpNRTVPVTFORlp6MDkifV19LCJoYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZTBjZjg1NDI4MzQ0ZDRmZjE3N2E4ZWRjNDMxZTNmOTJiNDQ4Nzc1YTJiMDBiN2ZjZDdhN2FiM2QyZjk4ZWNhYyJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjA3NDJhNmZlMmE5MWViN2UyYzI3NDE0NGY2MTIzZjU5YTc5OTczMmM5ZDliZmQzYjdmZWFjNDg3ZjcyZWI0NGMifX19fQ==" + } + ], + "timestampVerificationData": null + }, + "dsseEnvelope": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoicGtnOm5wbS9zaWdzdG9yZUAyLjAuMCIsImRpZ2VzdCI6eyJzaGE1MTIiOiI0NmQ0ZTJmNzRjNDg3NzMxNjY0MDAwMGE2ZmRmOGE4YjU5ZjFlMDg0NzY2Nzk3M2U5ODU5Zjc3NGRkMzFiOGYxZTA5Mzc4MTNiNzc3ZmI2NmEyYWM2N2Q1MDU0MGZlMzQ2NDA5NjZlZWU5ZmMyY2NjYTM4NzA4MmI0Yzg1Y2QzYyJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjEiLCJwcmVkaWNhdGUiOnsiYnVpbGREZWZpbml0aW9uIjp7ImJ1aWxkVHlwZSI6Imh0dHBzOi8vc2xzYS1mcmFtZXdvcmsuZ2l0aHViLmlvL2dpdGh1Yi1hY3Rpb25zLWJ1aWxkdHlwZXMvd29ya2Zsb3cvdjEiLCJleHRlcm5hbFBhcmFtZXRlcnMiOnsid29ya2Zsb3ciOnsicmVmIjoicmVmcy9oZWFkcy9tYWluIiwicmVwb3NpdG9yeSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS9zaWdzdG9yZS1qcyIsInBhdGgiOiIuZ2l0aHViL3dvcmtmbG93cy9yZWxlYXNlLnltbCJ9fSwiaW50ZXJuYWxQYXJhbWV0ZXJzIjp7ImdpdGh1YiI6eyJldmVudF9uYW1lIjoicHVzaCIsInJlcG9zaXRvcnlfaWQiOiI0OTU1NzQ1NTUiLCJyZXBvc2l0b3J5X293bmVyX2lkIjoiNzEwOTYzNTMifX0sInJlc29sdmVkRGVwZW5kZW5jaWVzIjpbeyJ1cmkiOiJnaXQraHR0cHM6Ly9naXRodWIuY29tL3NpZ3N0b3JlL3NpZ3N0b3JlLWpzQHJlZnMvaGVhZHMvbWFpbiIsImRpZ2VzdCI6eyJnaXRDb21taXQiOiJmMGI0OWEwNGU1YTYyMjUwZTBmNjBmYjEyODAwNGE3MzExMGZlMzExIn19XX0sInJ1bkRldGFpbHMiOnsiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9hY3Rpb25zL3J1bm5lci9naXRodWItaG9zdGVkIn0sIm1ldGFkYXRhIjp7Imludm9jYXRpb25JZCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9zaWdzdG9yZS9zaWdzdG9yZS1qcy9hY3Rpb25zL3J1bnMvNTkwNDY5Njc2NC9hdHRlbXB0cy8xIn19fX0=", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "sig": "MEQCIFWrPp3i58snUIk9H59hzyXHzPFs3+GZDp+CzdNKXcBEAiBQQjvUaTGxKiOGlG1GQxKl91YZE8kEX2waQps0NNSSEg==", + "keyid": "" + } + ] + } +} diff --git a/src/pkg/signing/trustedroot.go b/src/pkg/signing/trustedroot.go index bbc322670f..e8b4e1dcd0 100644 --- a/src/pkg/signing/trustedroot.go +++ b/src/pkg/signing/trustedroot.go @@ -5,9 +5,15 @@ package signing import ( _ "embed" + "encoding/json" "errors" "fmt" + "io/fs" "os" + "path/filepath" + + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/tuf" ) // embeddedTrustedRoot is the Sigstore TrustedRoot JSON shipped with the binary. @@ -16,6 +22,69 @@ import ( //go:embed embedded_trusted_root.json var embeddedTrustedRoot []byte +// configuredLiveTrustedRoot loads trusted material using the TUF configuration +// contract shared with Cosign without coupling direct verification to Cosign. +func configuredLiveTrustedRoot() (root.TrustedMaterial, error) { + opts, err := configuredTUFOptions() + if err != nil { + return nil, fmt.Errorf("loading configured trusted root: %w", err) + } + material, err := root.NewLiveTrustedRoot(opts) + if err != nil { + return nil, fmt.Errorf("loading configured trusted root: %w", err) + } + return material, nil +} + +// configuredTUFOptions resolves TUF_ROOT, TUF_MIRROR, and TUF_ROOT_JSON with +// the same precedence as Cosign's configured trusted root. +func configuredTUFOptions() (*tuf.Options, error) { + opts := tuf.DefaultOptions() + if cachePath := os.Getenv("TUF_ROOT"); cachePath != "" { + opts.CachePath = cachePath + } + + if mirror := os.Getenv("TUF_MIRROR"); mirror != "" { + opts.RepositoryBaseURL = mirror + } else { + remotePath := filepath.Join(opts.CachePath, "remote.json") + remoteJSON, err := os.ReadFile(remotePath) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return nil, fmt.Errorf("reading configured TUF remote file %q: %w", remotePath, err) + } + if err == nil { + var remote struct { + Mirror string `json:"mirror"` + } + if err := json.Unmarshal(remoteJSON, &remote); err != nil { + return nil, fmt.Errorf("decoding configured TUF remote file %q: %w", remotePath, err) + } + opts.RepositoryBaseURL = remote.Mirror + } + } + + if opts.RepositoryBaseURL == tuf.DefaultMirror { + return opts, nil + } + + if rootPath := os.Getenv("TUF_ROOT_JSON"); rootPath != "" { + rootJSON, err := os.ReadFile(rootPath) + if err != nil { + return nil, fmt.Errorf("reading configured TUF root JSON %q: %w", rootPath, err) + } + opts.Root = rootJSON + return opts, nil + } + + cachedRootPath := filepath.Join(opts.CachePath, tuf.URLToPath(opts.RepositoryBaseURL), "root.json") + rootJSON, err := os.ReadFile(cachedRootPath) + if err != nil { + return nil, fmt.Errorf("reading configured TUF cached root %q: %w", cachedRootPath, err) + } + opts.Root = rootJSON + return opts, nil +} + // 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. diff --git a/src/pkg/signing/trustedroot_test.go b/src/pkg/signing/trustedroot_test.go index d997c4af28..f91ed9286f 100644 --- a/src/pkg/signing/trustedroot_test.go +++ b/src/pkg/signing/trustedroot_test.go @@ -4,10 +4,13 @@ package signing import ( + "bytes" "encoding/json" "os" + "path/filepath" "testing" + "github.com/sigstore/sigstore-go/pkg/tuf" "github.com/stretchr/testify/require" ) @@ -53,3 +56,90 @@ func TestWriteEmbeddedTrustedRoot(t *testing.T) { require.NotEqual(t, p1, p2) }) } + +func TestConfiguredTUFOptions(t *testing.T) { + tests := []struct { + name string + setup func(*testing.T) (string, func(*tuf.Options)) + }{ + { + name: "uses explicit cache mirror and root", + setup: func(t *testing.T) (string, func(*tuf.Options)) { + cachePath := t.TempDir() + rootPath := filepath.Join(t.TempDir(), "root.json") + rootJSON := `{"root":"explicit"}` + require.NoError(t, os.WriteFile(rootPath, []byte(rootJSON), 0o600)) + t.Setenv("TUF_ROOT", cachePath) + t.Setenv("TUF_MIRROR", "https://mirror.example.test") + t.Setenv("TUF_ROOT_JSON", rootPath) + return "", func(opts *tuf.Options) { + require.Equal(t, cachePath, opts.CachePath) + require.Equal(t, "https://mirror.example.test", opts.RepositoryBaseURL) + require.True(t, bytes.Equal([]byte(rootJSON), opts.Root)) + } + }, + }, + { + name: "uses cached mirror and root", + setup: func(t *testing.T) (string, func(*tuf.Options)) { + cachePath := t.TempDir() + mirror := "https://mirror.example.test" + rootJSON := `{"root":"cached"}` + cachedRootPath := filepath.Join(cachePath, tuf.URLToPath(mirror), "root.json") + require.NoError(t, os.MkdirAll(filepath.Dir(cachedRootPath), 0o700)) + require.NoError(t, os.WriteFile(cachedRootPath, []byte(rootJSON), 0o600)) + remoteJSON, err := json.Marshal(map[string]string{"mirror": mirror}) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(cachePath, "remote.json"), remoteJSON, 0o600)) + t.Setenv("TUF_ROOT", cachePath) + return "", func(opts *tuf.Options) { + require.Equal(t, cachePath, opts.CachePath) + require.Equal(t, mirror, opts.RepositoryBaseURL) + require.True(t, bytes.Equal([]byte(rootJSON), opts.Root)) + } + }, + }, + { + name: "rejects custom mirror without root", + setup: func(t *testing.T) (string, func(*tuf.Options)) { + cachePath := t.TempDir() + mirror := "https://mirror.example.test" + t.Setenv("TUF_ROOT", cachePath) + t.Setenv("TUF_MIRROR", mirror) + return filepath.Join(cachePath, tuf.URLToPath(mirror), "root.json"), nil + }, + }, + { + name: "uses embedded root for default mirror", + setup: func(t *testing.T) (string, func(*tuf.Options)) { + cachePath := t.TempDir() + rootPath := filepath.Join(t.TempDir(), "root.json") + require.NoError(t, os.WriteFile(rootPath, []byte(`{"root":"ignored"}`), 0o600)) + t.Setenv("TUF_ROOT", cachePath) + t.Setenv("TUF_MIRROR", "") + t.Setenv("TUF_ROOT_JSON", rootPath) + return "", func(opts *tuf.Options) { + require.Equal(t, tuf.DefaultMirror, opts.RepositoryBaseURL) + require.Equal(t, tuf.DefaultRoot(), opts.Root) + } + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Setenv("TUF_ROOT", "") + t.Setenv("TUF_MIRROR", "") + t.Setenv("TUF_ROOT_JSON", "") + expectedErrorPath, check := tc.setup(t) + opts, err := configuredTUFOptions() + if expectedErrorPath != "" { + require.Error(t, err) + require.ErrorContains(t, err, expectedErrorPath) + return + } + require.NoError(t, err) + check(opts) + }) + } +} From 26b194a6ba447fa1b38feb031119deadcf694b6c Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Thu, 6 Aug 2026 17:59:12 +0000 Subject: [PATCH 4/8] fix: revert to minimal migration path Signed-off-by: Brandt Keller --- src/pkg/signing/cosign_test.go | 16 +++++ src/pkg/signing/sigstore.go | 6 +- src/pkg/signing/trustedroot.go | 69 ---------------------- src/pkg/signing/trustedroot_test.go | 90 ----------------------------- 4 files changed, 17 insertions(+), 164 deletions(-) diff --git a/src/pkg/signing/cosign_test.go b/src/pkg/signing/cosign_test.go index 4117417f93..9363860759 100644 --- a/src/pkg/signing/cosign_test.go +++ b/src/pkg/signing/cosign_test.go @@ -4,6 +4,7 @@ package signing import ( + "crypto" "crypto/sha256" "encoding/hex" "net" @@ -148,6 +149,21 @@ func TestSigstoreVerifyBundleWithOptions(t *testing.T) { require.NotEmpty(t, material.FulcioCertificateAuthorities()) }) + t.Run("uses embedded trusted root for keyed tlog verification", func(t *testing.T) { + publicKey, err := os.ReadFile(pubPath) + require.NoError(t, err) + keyVerifier, closeVerifier, err := verifierFromPEM(publicKey, crypto.SHA256) + require.NoError(t, err) + defer closeVerifier() + + opts := DefaultVerifyBlobOptions() + opts.CommonVerifyOptions.IgnoreTlog = false + material, err := trustedMaterialForBundle(opts, keyVerifier, false) + require.NoError(t, err) + require.NotEmpty(t, material.RekorLogs()) + require.NotEmpty(t, material.TimestampingAuthorities()) + }) + t.Run("verifies keyless public-good bundle", func(t *testing.T) { opts := DefaultVerifyBlobOptions() opts.BundlePath = "./testdata/sigstore-js-2.0.0-provenance.sigstore.json" diff --git a/src/pkg/signing/sigstore.go b/src/pkg/signing/sigstore.go index 80dc52c3ac..14696c63a4 100644 --- a/src/pkg/signing/sigstore.go +++ b/src/pkg/signing/sigstore.go @@ -203,12 +203,8 @@ func trustedMaterialForBundle(opts VerifyBlobOptions, keyVerifier signature.Veri var err error if path := opts.CommonVerifyOptions.TrustedRootPath; path != "" { material, err = root.NewTrustedRootFromPath(path) - } else if keyVerifier == nil { - material, err = root.NewTrustedRootFromJSON(embeddedTrustedRoot) } else { - // Cosign fetches TUF material for key verification when the caller - // asks to verify a tlog entry or TSA timestamp. - material, err = configuredLiveTrustedRoot() + material, err = root.NewTrustedRootFromJSON(embeddedTrustedRoot) } if err != nil { return nil, fmt.Errorf("loading trusted root: %w", err) diff --git a/src/pkg/signing/trustedroot.go b/src/pkg/signing/trustedroot.go index e8b4e1dcd0..bbc322670f 100644 --- a/src/pkg/signing/trustedroot.go +++ b/src/pkg/signing/trustedroot.go @@ -5,15 +5,9 @@ package signing import ( _ "embed" - "encoding/json" "errors" "fmt" - "io/fs" "os" - "path/filepath" - - "github.com/sigstore/sigstore-go/pkg/root" - "github.com/sigstore/sigstore-go/pkg/tuf" ) // embeddedTrustedRoot is the Sigstore TrustedRoot JSON shipped with the binary. @@ -22,69 +16,6 @@ import ( //go:embed embedded_trusted_root.json var embeddedTrustedRoot []byte -// configuredLiveTrustedRoot loads trusted material using the TUF configuration -// contract shared with Cosign without coupling direct verification to Cosign. -func configuredLiveTrustedRoot() (root.TrustedMaterial, error) { - opts, err := configuredTUFOptions() - if err != nil { - return nil, fmt.Errorf("loading configured trusted root: %w", err) - } - material, err := root.NewLiveTrustedRoot(opts) - if err != nil { - return nil, fmt.Errorf("loading configured trusted root: %w", err) - } - return material, nil -} - -// configuredTUFOptions resolves TUF_ROOT, TUF_MIRROR, and TUF_ROOT_JSON with -// the same precedence as Cosign's configured trusted root. -func configuredTUFOptions() (*tuf.Options, error) { - opts := tuf.DefaultOptions() - if cachePath := os.Getenv("TUF_ROOT"); cachePath != "" { - opts.CachePath = cachePath - } - - if mirror := os.Getenv("TUF_MIRROR"); mirror != "" { - opts.RepositoryBaseURL = mirror - } else { - remotePath := filepath.Join(opts.CachePath, "remote.json") - remoteJSON, err := os.ReadFile(remotePath) - if err != nil && !errors.Is(err, fs.ErrNotExist) { - return nil, fmt.Errorf("reading configured TUF remote file %q: %w", remotePath, err) - } - if err == nil { - var remote struct { - Mirror string `json:"mirror"` - } - if err := json.Unmarshal(remoteJSON, &remote); err != nil { - return nil, fmt.Errorf("decoding configured TUF remote file %q: %w", remotePath, err) - } - opts.RepositoryBaseURL = remote.Mirror - } - } - - if opts.RepositoryBaseURL == tuf.DefaultMirror { - return opts, nil - } - - if rootPath := os.Getenv("TUF_ROOT_JSON"); rootPath != "" { - rootJSON, err := os.ReadFile(rootPath) - if err != nil { - return nil, fmt.Errorf("reading configured TUF root JSON %q: %w", rootPath, err) - } - opts.Root = rootJSON - return opts, nil - } - - cachedRootPath := filepath.Join(opts.CachePath, tuf.URLToPath(opts.RepositoryBaseURL), "root.json") - rootJSON, err := os.ReadFile(cachedRootPath) - if err != nil { - return nil, fmt.Errorf("reading configured TUF cached root %q: %w", cachedRootPath, err) - } - opts.Root = rootJSON - return opts, nil -} - // 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. diff --git a/src/pkg/signing/trustedroot_test.go b/src/pkg/signing/trustedroot_test.go index f91ed9286f..d997c4af28 100644 --- a/src/pkg/signing/trustedroot_test.go +++ b/src/pkg/signing/trustedroot_test.go @@ -4,13 +4,10 @@ package signing import ( - "bytes" "encoding/json" "os" - "path/filepath" "testing" - "github.com/sigstore/sigstore-go/pkg/tuf" "github.com/stretchr/testify/require" ) @@ -56,90 +53,3 @@ func TestWriteEmbeddedTrustedRoot(t *testing.T) { require.NotEqual(t, p1, p2) }) } - -func TestConfiguredTUFOptions(t *testing.T) { - tests := []struct { - name string - setup func(*testing.T) (string, func(*tuf.Options)) - }{ - { - name: "uses explicit cache mirror and root", - setup: func(t *testing.T) (string, func(*tuf.Options)) { - cachePath := t.TempDir() - rootPath := filepath.Join(t.TempDir(), "root.json") - rootJSON := `{"root":"explicit"}` - require.NoError(t, os.WriteFile(rootPath, []byte(rootJSON), 0o600)) - t.Setenv("TUF_ROOT", cachePath) - t.Setenv("TUF_MIRROR", "https://mirror.example.test") - t.Setenv("TUF_ROOT_JSON", rootPath) - return "", func(opts *tuf.Options) { - require.Equal(t, cachePath, opts.CachePath) - require.Equal(t, "https://mirror.example.test", opts.RepositoryBaseURL) - require.True(t, bytes.Equal([]byte(rootJSON), opts.Root)) - } - }, - }, - { - name: "uses cached mirror and root", - setup: func(t *testing.T) (string, func(*tuf.Options)) { - cachePath := t.TempDir() - mirror := "https://mirror.example.test" - rootJSON := `{"root":"cached"}` - cachedRootPath := filepath.Join(cachePath, tuf.URLToPath(mirror), "root.json") - require.NoError(t, os.MkdirAll(filepath.Dir(cachedRootPath), 0o700)) - require.NoError(t, os.WriteFile(cachedRootPath, []byte(rootJSON), 0o600)) - remoteJSON, err := json.Marshal(map[string]string{"mirror": mirror}) - require.NoError(t, err) - require.NoError(t, os.WriteFile(filepath.Join(cachePath, "remote.json"), remoteJSON, 0o600)) - t.Setenv("TUF_ROOT", cachePath) - return "", func(opts *tuf.Options) { - require.Equal(t, cachePath, opts.CachePath) - require.Equal(t, mirror, opts.RepositoryBaseURL) - require.True(t, bytes.Equal([]byte(rootJSON), opts.Root)) - } - }, - }, - { - name: "rejects custom mirror without root", - setup: func(t *testing.T) (string, func(*tuf.Options)) { - cachePath := t.TempDir() - mirror := "https://mirror.example.test" - t.Setenv("TUF_ROOT", cachePath) - t.Setenv("TUF_MIRROR", mirror) - return filepath.Join(cachePath, tuf.URLToPath(mirror), "root.json"), nil - }, - }, - { - name: "uses embedded root for default mirror", - setup: func(t *testing.T) (string, func(*tuf.Options)) { - cachePath := t.TempDir() - rootPath := filepath.Join(t.TempDir(), "root.json") - require.NoError(t, os.WriteFile(rootPath, []byte(`{"root":"ignored"}`), 0o600)) - t.Setenv("TUF_ROOT", cachePath) - t.Setenv("TUF_MIRROR", "") - t.Setenv("TUF_ROOT_JSON", rootPath) - return "", func(opts *tuf.Options) { - require.Equal(t, tuf.DefaultMirror, opts.RepositoryBaseURL) - require.Equal(t, tuf.DefaultRoot(), opts.Root) - } - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - t.Setenv("TUF_ROOT", "") - t.Setenv("TUF_MIRROR", "") - t.Setenv("TUF_ROOT_JSON", "") - expectedErrorPath, check := tc.setup(t) - opts, err := configuredTUFOptions() - if expectedErrorPath != "" { - require.Error(t, err) - require.ErrorContains(t, err, expectedErrorPath) - return - } - require.NoError(t, err) - check(opts) - }) - } -} From f6d836a0e14595d8556372b690845ba79bb08267 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Fri, 7 Aug 2026 14:21:40 +0000 Subject: [PATCH 5/8] feat: return a verification result Signed-off-by: Brandt Keller --- src/pkg/packager/layout/package.go | 3 +- src/pkg/signing/cosign_test.go | 21 +++++++++----- src/pkg/signing/sigstore.go | 32 +++++++++++---------- src/test/e2e/34_custom_init_package_test.go | 3 -- 4 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/pkg/packager/layout/package.go b/src/pkg/packager/layout/package.go index b071c2756d..9a2e50de2c 100644 --- a/src/pkg/packager/layout/package.go +++ b/src/pkg/packager/layout/package.go @@ -413,7 +413,8 @@ func (p *PackageLayout) VerifyPackageSignature(ctx context.Context, opts signing opts.CommonVerifyOptions.UseSignedTimestamps = true } ZarfYAMLPath := filepath.Join(p.dirPath, ZarfYAML) - return signing.SigstoreVerifyBundleWithOptions(ctx, ZarfYAMLPath, opts) + _, err := signing.SigstoreVerifyBundleWithOptions(ctx, ZarfYAMLPath, opts) + return err } if !errors.Is(bundleErr, os.ErrNotExist) { return fmt.Errorf("error checking bundle signature: %w", bundleErr) diff --git a/src/pkg/signing/cosign_test.go b/src/pkg/signing/cosign_test.go index 9363860759..0dbd144268 100644 --- a/src/pkg/signing/cosign_test.go +++ b/src/pkg/signing/cosign_test.go @@ -56,7 +56,8 @@ func TestSigstoreVerifyBundleWithOptions(t *testing.T) { opts := DefaultVerifyBlobOptions() opts.Key = key opts.BundlePath = bundlePath - return SigstoreVerifyBundleWithOptions(ctx, blobPath, opts) + _, err := SigstoreVerifyBundleWithOptions(ctx, blobPath, opts) + return err } t.Run("matches cosign for valid and tampered local-key bundles", func(t *testing.T) { @@ -83,7 +84,8 @@ func TestSigstoreVerifyBundleWithOptions(t *testing.T) { directOpts := DefaultVerifyBlobOptions() directOpts.Key = pubPath directOpts.BundlePath = bundlePath - require.NoError(t, SigstoreVerifyBundleWithOptions(ctx, artifactRef, directOpts)) + _, err = SigstoreVerifyBundleWithOptions(ctx, artifactRef, directOpts) + require.NoError(t, err) cosignOpts := directOpts require.NoError(t, CosignVerifyBlobWithOptions(ctx, artifactRef, cosignOpts)) @@ -129,7 +131,7 @@ func TestSigstoreVerifyBundleWithOptions(t *testing.T) { opts.Key = pubPath opts.BundlePath = bundlePath opts.Signature = "detached.sig" - err := SigstoreVerifyBundleWithOptions(ctx, blobPath, opts) + _, err := SigstoreVerifyBundleWithOptions(ctx, blobPath, opts) require.ErrorContains(t, err, "detached signature") require.NotContains(t, err.Error(), "--") }) @@ -139,7 +141,8 @@ func TestSigstoreVerifyBundleWithOptions(t *testing.T) { opts := DefaultVerifyBlobOptions() opts.KeyRef = pubPath opts.BundlePath = bundlePath - require.NoError(t, SigstoreVerifyBundleWithOptions(ctx, blobPath, opts)) + _, err := SigstoreVerifyBundleWithOptions(ctx, blobPath, opts) + require.NoError(t, err) }) t.Run("uses embedded trusted root for keyless verification", func(t *testing.T) { @@ -172,11 +175,15 @@ func TestSigstoreVerifyBundleWithOptions(t *testing.T) { opts.CommonVerifyOptions.IgnoreTlog = false const digestReference = "sha512:46d4e2f74c4877316640000a6fdf8a8b59f1e0847667973e9859f774dd31b8f1e0937813b777fb66a2ac67d50540fe34640966eee9fc2ccca387082b4c85cd3c" - require.NoError(t, SigstoreVerifyBundleWithOptions(ctx, digestReference, opts)) + result, err := SigstoreVerifyBundleWithOptions(ctx, digestReference, opts) + require.NoError(t, err) + require.NotNil(t, result) + require.NotNil(t, result.VerifiedIdentity) invalidOpts := opts invalidOpts.CertVerify.CertIdentityRegexp = "^https://github.com/sigstore/other-project/" - require.Error(t, SigstoreVerifyBundleWithOptions(ctx, digestReference, invalidOpts)) + _, err = SigstoreVerifyBundleWithOptions(ctx, digestReference, invalidOpts) + require.Error(t, err) }) } @@ -268,7 +275,7 @@ func TestSigstoreBundleValidationErrorsUseLibraryTerms(t *testing.T) { var err error if opts.BundlePath == "" { - err = SigstoreVerifyBundleWithOptions(testutil.TestContext(t), "", opts) + _, err = SigstoreVerifyBundleWithOptions(testutil.TestContext(t), "", opts) } else { err = validateSigstoreBundleOptions(opts) } diff --git a/src/pkg/signing/sigstore.go b/src/pkg/signing/sigstore.go index 14696c63a4..11c7409535 100644 --- a/src/pkg/signing/sigstore.go +++ b/src/pkg/signing/sigstore.go @@ -33,8 +33,9 @@ import ( ) // SigstoreVerifyBundleWithOptions verifies a Sigstore bundle directly with -// sigstore-go. Callers must use CosignVerifyBlobWithOptions for legacy .sig files. -func SigstoreVerifyBundleWithOptions(ctx context.Context, blobPath string, opts VerifyBlobOptions) error { +// sigstore-go and returns the verified bundle contents. Callers must use +// CosignVerifyBlobWithOptions for legacy .sig files. +func SigstoreVerifyBundleWithOptions(ctx context.Context, blobPath string, opts VerifyBlobOptions) (*verify.VerificationResult, error) { l := logger.From(ctx) if opts.KeyRef != "" { @@ -50,10 +51,10 @@ func SigstoreVerifyBundleWithOptions(ctx context.Context, blobPath string, opts } } if err := validateSigstoreBundleOptions(opts); err != nil { - return err + return nil, err } if opts.BundlePath == "" { - return errors.New("bundle path is required") + return nil, errors.New("bundle path is required") } if opts.Timeout > 0 { var cancel context.CancelFunc @@ -63,16 +64,16 @@ func SigstoreVerifyBundleWithOptions(ctx context.Context, blobPath string, opts b, err := bundle.LoadJSONFromPath(opts.BundlePath) if err != nil { - return fmt.Errorf("loading Sigstore bundle: %w", err) + return nil, fmt.Errorf("loading Sigstore bundle: %w", err) } hashAlgorithm, err := opts.SignatureDigest.HashAlgorithm() if err != nil { - return err + return nil, err } keyVerifier, closeKey, err := resolveBundleVerifier(ctx, opts, hashAlgorithm) if err != nil { - return fmt.Errorf("loading verifier from key options: %w", err) + return nil, fmt.Errorf("loading verifier from key options: %w", err) } defer closeKey() @@ -80,7 +81,7 @@ func SigstoreVerifyBundleWithOptions(ctx context.Context, blobPath string, opts if !opts.CommonVerifyOptions.IgnoreTlog && keyVerifier == nil { v1, v2, err := rekorBundleVersions(b) if err != nil { - return err + return nil, err } // Rekor v2 does not provide an integrated timestamp. This mirrors // cosign's new-bundle verifier, which enables TSA validation for a @@ -92,26 +93,27 @@ func SigstoreVerifyBundleWithOptions(ctx context.Context, blobPath string, opts trustedMaterial, err := trustedMaterialForBundle(opts, keyVerifier, useSignedTimestamps) if err != nil { - return err + return nil, err } verifierOptions, policyOptions, err := sigstoreVerificationOptions(opts, keyVerifier != nil, useSignedTimestamps) if err != nil { - return err + return nil, err } sev, err := verify.NewVerifier(trustedMaterial, verifierOptions...) if err != nil { - return fmt.Errorf("creating Sigstore verifier: %w", err) + return nil, fmt.Errorf("creating Sigstore verifier: %w", err) } artifactPolicy, err := bundleArtifactPolicy(blobPath) if err != nil { - return err + return nil, err } - if _, err := sev.Verify(b, verify.NewPolicy(artifactPolicy, policyOptions...)); err != nil { - return err + result, err := sev.Verify(b, verify.NewPolicy(artifactPolicy, policyOptions...)) + if err != nil { + return nil, err } l.Debug("blob signature verified successfully with sigstore-go") - return nil + return result, nil } func validateSigstoreBundleOptions(opts VerifyBlobOptions) error { diff --git a/src/test/e2e/34_custom_init_package_test.go b/src/test/e2e/34_custom_init_package_test.go index 4388eefde5..df3b64e8c0 100644 --- a/src/test/e2e/34_custom_init_package_test.go +++ b/src/test/e2e/34_custom_init_package_test.go @@ -32,9 +32,6 @@ func TestCustomInit(t *testing.T) { // Test that we don't get an error when we remember to provide the public key stdOut, stdErr, err = e2e.Zarf(t, "package", "inspect", "definition", pkgName, publicKeyFlag) require.NoError(t, err, stdOut, stdErr) - // Bundle verification is performed directly by sigstore-go, which does not - // emit cosign's legacy success text. - require.NotContains(t, stdErr, "Verified OK") /* Test operations during package deploy */ // Test that we get an error when trying to deploy a package without providing the public key From 4bf7d3f7dab6a1a0fadd13e6a126916de9578ab6 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Fri, 7 Aug 2026 14:25:22 +0000 Subject: [PATCH 6/8] fix: move testing to isolate Signed-off-by: Brandt Keller --- src/pkg/signing/cosign_test.go | 262 ----------------------------- src/pkg/signing/sigstore.go | 3 +- src/pkg/signing/sigstore_test.go | 276 +++++++++++++++++++++++++++++++ 3 files changed, 277 insertions(+), 264 deletions(-) create mode 100644 src/pkg/signing/sigstore_test.go diff --git a/src/pkg/signing/cosign_test.go b/src/pkg/signing/cosign_test.go index 0dbd144268..12c39891db 100644 --- a/src/pkg/signing/cosign_test.go +++ b/src/pkg/signing/cosign_test.go @@ -4,12 +4,6 @@ package signing import ( - "crypto" - "crypto/sha256" - "encoding/hex" - "net" - "net/http" - "net/http/httptest" "os" "path/filepath" "testing" @@ -29,262 +23,6 @@ func TestDefaultSignBlobOptions_EmptyAuthFlow(t *testing.T) { require.Empty(t, opts.Fulcio.AuthFlow) } -func TestSigstoreVerifyBundleWithOptions(t *testing.T) { - ctx := testutil.TestContext(t) - - const keyPath = "./testdata/cosign.key" - const pubPath = "./testdata/cosign.pub" - const password = "test" - - newBundle := func(t *testing.T) (string, string) { - t.Helper() - blobPath := filepath.Join(t.TempDir(), "payload.txt") - bundlePath := filepath.Join(t.TempDir(), "sig.bundle") - require.NoError(t, os.WriteFile(blobPath, []byte("direct verifier payload"), 0o644)) - - signOpts := DefaultSignBlobOptions() - signOpts.Key = keyPath - signOpts.Password = password - signOpts.BundlePath = bundlePath - _, err := CosignSignBlobWithOptions(ctx, blobPath, signOpts) - require.NoError(t, err) - return blobPath, bundlePath - } - - verify := func(t *testing.T, blobPath, bundlePath, key string) error { - t.Helper() - opts := DefaultVerifyBlobOptions() - opts.Key = key - opts.BundlePath = bundlePath - _, err := SigstoreVerifyBundleWithOptions(ctx, blobPath, opts) - return err - } - - t.Run("matches cosign for valid and tampered local-key bundles", func(t *testing.T) { - blobPath, bundlePath := newBundle(t) - require.NoError(t, verify(t, blobPath, bundlePath, pubPath)) - - cosignOpts := DefaultVerifyBlobOptions() - cosignOpts.Key = pubPath - cosignOpts.BundlePath = bundlePath - require.NoError(t, CosignVerifyBlobWithOptions(ctx, blobPath, cosignOpts)) - - require.NoError(t, os.WriteFile(blobPath, []byte("tampered"), 0o644)) - require.Error(t, verify(t, blobPath, bundlePath, pubPath)) - require.Error(t, CosignVerifyBlobWithOptions(ctx, blobPath, cosignOpts)) - }) - - t.Run("matches cosign for digest artifact references", func(t *testing.T) { - blobPath, bundlePath := newBundle(t) - payload, err := os.ReadFile(blobPath) - require.NoError(t, err) - digest := sha256.Sum256(payload) - artifactRef := "sha256:" + hex.EncodeToString(digest[:]) - - directOpts := DefaultVerifyBlobOptions() - directOpts.Key = pubPath - directOpts.BundlePath = bundlePath - _, err = SigstoreVerifyBundleWithOptions(ctx, artifactRef, directOpts) - require.NoError(t, err) - - cosignOpts := directOpts - require.NoError(t, CosignVerifyBlobWithOptions(ctx, artifactRef, cosignOpts)) - }) - - t.Run("accepts environment public-key references", func(t *testing.T) { - blobPath, bundlePath := newBundle(t) - publicKey, err := os.ReadFile(pubPath) - require.NoError(t, err) - t.Setenv("ZARF_TEST_COSIGN_PUBLIC_KEY", string(publicKey)) - require.NoError(t, verify(t, blobPath, bundlePath, "env://ZARF_TEST_COSIGN_PUBLIC_KEY")) - }) - - t.Run("accepts URL public-key references", func(t *testing.T) { - listener, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Skipf("loopback listener unavailable: %v", err) - } - blobPath, bundlePath := newBundle(t) - publicKey, err := os.ReadFile(pubPath) - require.NoError(t, err) - server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - if _, err := w.Write(publicKey); err != nil { - t.Errorf("writing public-key response: %v", err) - } - })) - server.Listener = listener - server.Start() - defer server.Close() - require.NoError(t, verify(t, blobPath, bundlePath, server.URL)) - }) - - t.Run("rejects wrong keys and corrupt bundles without cosign fallback", func(t *testing.T) { - blobPath, bundlePath := newBundle(t) - require.Error(t, verify(t, blobPath, bundlePath, "./testdata/nonexistent.pub")) - require.NoError(t, os.WriteFile(bundlePath, []byte("not a bundle"), 0o644)) - require.Error(t, verify(t, blobPath, bundlePath, pubPath)) - }) - - t.Run("rejects detached verification material for bundles", func(t *testing.T) { - blobPath, bundlePath := newBundle(t) - opts := DefaultVerifyBlobOptions() - opts.Key = pubPath - opts.BundlePath = bundlePath - opts.Signature = "detached.sig" - _, err := SigstoreVerifyBundleWithOptions(ctx, blobPath, opts) - require.ErrorContains(t, err, "detached signature") - require.NotContains(t, err.Error(), "--") - }) - - t.Run("supports deprecated key alias", func(t *testing.T) { - blobPath, bundlePath := newBundle(t) - opts := DefaultVerifyBlobOptions() - opts.KeyRef = pubPath - opts.BundlePath = bundlePath - _, err := SigstoreVerifyBundleWithOptions(ctx, blobPath, opts) - require.NoError(t, err) - }) - - t.Run("uses embedded trusted root for keyless verification", func(t *testing.T) { - opts := DefaultVerifyBlobOptions() - material, err := trustedMaterialForBundle(opts, nil, false) - require.NoError(t, err) - require.NotEmpty(t, material.FulcioCertificateAuthorities()) - }) - - t.Run("uses embedded trusted root for keyed tlog verification", func(t *testing.T) { - publicKey, err := os.ReadFile(pubPath) - require.NoError(t, err) - keyVerifier, closeVerifier, err := verifierFromPEM(publicKey, crypto.SHA256) - require.NoError(t, err) - defer closeVerifier() - - opts := DefaultVerifyBlobOptions() - opts.CommonVerifyOptions.IgnoreTlog = false - material, err := trustedMaterialForBundle(opts, keyVerifier, false) - require.NoError(t, err) - require.NotEmpty(t, material.RekorLogs()) - require.NotEmpty(t, material.TimestampingAuthorities()) - }) - - t.Run("verifies keyless public-good bundle", func(t *testing.T) { - opts := DefaultVerifyBlobOptions() - opts.BundlePath = "./testdata/sigstore-js-2.0.0-provenance.sigstore.json" - opts.CertVerify.CertIdentityRegexp = "^https://github.com/sigstore/sigstore-js/" - opts.CertVerify.CertOidcIssuer = "https://token.actions.githubusercontent.com" - opts.CommonVerifyOptions.IgnoreTlog = false - - const digestReference = "sha512:46d4e2f74c4877316640000a6fdf8a8b59f1e0847667973e9859f774dd31b8f1e0937813b777fb66a2ac67d50540fe34640966eee9fc2ccca387082b4c85cd3c" - result, err := SigstoreVerifyBundleWithOptions(ctx, digestReference, opts) - require.NoError(t, err) - require.NotNil(t, result) - require.NotNil(t, result.VerifiedIdentity) - - invalidOpts := opts - invalidOpts.CertVerify.CertIdentityRegexp = "^https://github.com/sigstore/other-project/" - _, err = SigstoreVerifyBundleWithOptions(ctx, digestReference, invalidOpts) - require.Error(t, err) - }) -} - -func TestSigstoreBundleValidationErrorsUseLibraryTerms(t *testing.T) { - tests := []struct { - name string - configure func(*VerifyBlobOptions) - want string - }{ - { - name: "requires bundle path", - want: "bundle path is required", - }, - { - name: "rejects key with certificate identity", - configure: func(opts *VerifyBlobOptions) { - opts.BundlePath = "bundle.json" - opts.Key = "key.pem" - opts.CertVerify.CertIdentity = "https://example.test" - }, - want: "key cannot be combined with certificate identity verification", - }, - { - name: "rejects key with security key", - configure: func(opts *VerifyBlobOptions) { - opts.BundlePath = "bundle.json" - opts.Key = "key.pem" - opts.SecurityKey.Use = true - }, - want: "key cannot be combined with security-key verification", - }, - { - name: "rejects detached signature", - configure: func(opts *VerifyBlobOptions) { - opts.Signature = "signature" - }, - want: "unsupported verification material for Sigstore bundles: detached signature", - }, - { - name: "rejects certificate", - configure: func(opts *VerifyBlobOptions) { - opts.CertVerify.Cert = "certificate" - }, - want: "unsupported verification material for Sigstore bundles: certificate", - }, - { - name: "rejects certificate chain", - configure: func(opts *VerifyBlobOptions) { - opts.CertVerify.CertChain = "chain" - }, - want: "unsupported verification material for Sigstore bundles: certificate chain", - }, - { - name: "rejects certificate authority roots", - configure: func(opts *VerifyBlobOptions) { - opts.CertVerify.CARoots = "roots" - }, - want: "unsupported verification material for Sigstore bundles: certificate authority roots", - }, - { - name: "rejects certificate authority intermediates", - configure: func(opts *VerifyBlobOptions) { - opts.CertVerify.CAIntermediates = "intermediates" - }, - want: "unsupported verification material for Sigstore bundles: certificate authority intermediates", - }, - { - name: "rejects timestamp certificate chain", - configure: func(opts *VerifyBlobOptions) { - opts.CommonVerifyOptions.TSACertChainPath = "timestamp-chain" - }, - want: "unsupported verification material for Sigstore bundles: timestamp certificate chain", - }, - { - name: "rejects signed certificate timestamp", - configure: func(opts *VerifyBlobOptions) { - opts.CertVerify.SCT = "sct" - }, - want: "unsupported verification material for Sigstore bundles: signed certificate timestamp", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - opts := DefaultVerifyBlobOptions() - if tc.configure != nil { - tc.configure(&opts) - } - - var err error - if opts.BundlePath == "" { - _, err = SigstoreVerifyBundleWithOptions(testutil.TestContext(t), "", opts) - } else { - err = validateSigstoreBundleOptions(opts) - } - require.ErrorContains(t, err, tc.want) - require.NotContains(t, err.Error(), "--") - }) - } -} - func TestShouldSign_KeyRefAlias(t *testing.T) { t.Parallel() diff --git a/src/pkg/signing/sigstore.go b/src/pkg/signing/sigstore.go index 11c7409535..b5b4c6c06e 100644 --- a/src/pkg/signing/sigstore.go +++ b/src/pkg/signing/sigstore.go @@ -33,8 +33,7 @@ import ( ) // SigstoreVerifyBundleWithOptions verifies a Sigstore bundle directly with -// sigstore-go and returns the verified bundle contents. Callers must use -// CosignVerifyBlobWithOptions for legacy .sig files. +// sigstore-go and returns the verified bundle contents. func SigstoreVerifyBundleWithOptions(ctx context.Context, blobPath string, opts VerifyBlobOptions) (*verify.VerificationResult, error) { l := logger.From(ctx) diff --git a/src/pkg/signing/sigstore_test.go b/src/pkg/signing/sigstore_test.go new file mode 100644 index 0000000000..620fa8798a --- /dev/null +++ b/src/pkg/signing/sigstore_test.go @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package signing + +import ( + "crypto" + "crypto/sha256" + "encoding/hex" + "net" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/zarf-dev/zarf/src/test/testutil" +) + +func TestSigstoreVerifyBundleWithOptions(t *testing.T) { + ctx := testutil.TestContext(t) + + const keyPath = "./testdata/cosign.key" + const pubPath = "./testdata/cosign.pub" + const password = "test" + + newBundle := func(t *testing.T) (string, string) { + t.Helper() + blobPath := filepath.Join(t.TempDir(), "payload.txt") + bundlePath := filepath.Join(t.TempDir(), "sig.bundle") + require.NoError(t, os.WriteFile(blobPath, []byte("direct verifier payload"), 0o644)) + + signOpts := DefaultSignBlobOptions() + signOpts.Key = keyPath + signOpts.Password = password + signOpts.BundlePath = bundlePath + _, err := CosignSignBlobWithOptions(ctx, blobPath, signOpts) + require.NoError(t, err) + return blobPath, bundlePath + } + + verify := func(t *testing.T, blobPath, bundlePath, key string) error { + t.Helper() + opts := DefaultVerifyBlobOptions() + opts.Key = key + opts.BundlePath = bundlePath + _, err := SigstoreVerifyBundleWithOptions(ctx, blobPath, opts) + return err + } + + t.Run("matches cosign for valid and tampered local-key bundles", func(t *testing.T) { + blobPath, bundlePath := newBundle(t) + require.NoError(t, verify(t, blobPath, bundlePath, pubPath)) + + cosignOpts := DefaultVerifyBlobOptions() + cosignOpts.Key = pubPath + cosignOpts.BundlePath = bundlePath + require.NoError(t, CosignVerifyBlobWithOptions(ctx, blobPath, cosignOpts)) + + require.NoError(t, os.WriteFile(blobPath, []byte("tampered"), 0o644)) + require.Error(t, verify(t, blobPath, bundlePath, pubPath)) + require.Error(t, CosignVerifyBlobWithOptions(ctx, blobPath, cosignOpts)) + }) + + t.Run("matches cosign for digest artifact references", func(t *testing.T) { + blobPath, bundlePath := newBundle(t) + payload, err := os.ReadFile(blobPath) + require.NoError(t, err) + digest := sha256.Sum256(payload) + artifactRef := "sha256:" + hex.EncodeToString(digest[:]) + + directOpts := DefaultVerifyBlobOptions() + directOpts.Key = pubPath + directOpts.BundlePath = bundlePath + _, err = SigstoreVerifyBundleWithOptions(ctx, artifactRef, directOpts) + require.NoError(t, err) + + cosignOpts := directOpts + require.NoError(t, CosignVerifyBlobWithOptions(ctx, artifactRef, cosignOpts)) + }) + + t.Run("accepts environment public-key references", func(t *testing.T) { + blobPath, bundlePath := newBundle(t) + publicKey, err := os.ReadFile(pubPath) + require.NoError(t, err) + t.Setenv("ZARF_TEST_COSIGN_PUBLIC_KEY", string(publicKey)) + require.NoError(t, verify(t, blobPath, bundlePath, "env://ZARF_TEST_COSIGN_PUBLIC_KEY")) + }) + + t.Run("accepts URL public-key references", func(t *testing.T) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Skipf("loopback listener unavailable: %v", err) + } + blobPath, bundlePath := newBundle(t) + publicKey, err := os.ReadFile(pubPath) + require.NoError(t, err) + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if _, err := w.Write(publicKey); err != nil { + t.Errorf("writing public-key response: %v", err) + } + })) + server.Listener = listener + server.Start() + defer server.Close() + require.NoError(t, verify(t, blobPath, bundlePath, server.URL)) + }) + + t.Run("rejects wrong keys and corrupt bundles without cosign fallback", func(t *testing.T) { + blobPath, bundlePath := newBundle(t) + require.Error(t, verify(t, blobPath, bundlePath, "./testdata/nonexistent.pub")) + require.NoError(t, os.WriteFile(bundlePath, []byte("not a bundle"), 0o644)) + require.Error(t, verify(t, blobPath, bundlePath, pubPath)) + }) + + t.Run("rejects detached verification material for bundles", func(t *testing.T) { + blobPath, bundlePath := newBundle(t) + opts := DefaultVerifyBlobOptions() + opts.Key = pubPath + opts.BundlePath = bundlePath + opts.Signature = "detached.sig" + _, err := SigstoreVerifyBundleWithOptions(ctx, blobPath, opts) + require.ErrorContains(t, err, "detached signature") + require.NotContains(t, err.Error(), "--") + }) + + t.Run("supports deprecated key alias", func(t *testing.T) { + blobPath, bundlePath := newBundle(t) + opts := DefaultVerifyBlobOptions() + opts.KeyRef = pubPath + opts.BundlePath = bundlePath + _, err := SigstoreVerifyBundleWithOptions(ctx, blobPath, opts) + require.NoError(t, err) + }) + + t.Run("uses embedded trusted root for keyless verification", func(t *testing.T) { + opts := DefaultVerifyBlobOptions() + material, err := trustedMaterialForBundle(opts, nil, false) + require.NoError(t, err) + require.NotEmpty(t, material.FulcioCertificateAuthorities()) + }) + + t.Run("uses embedded trusted root for keyed tlog verification", func(t *testing.T) { + publicKey, err := os.ReadFile(pubPath) + require.NoError(t, err) + keyVerifier, closeVerifier, err := verifierFromPEM(publicKey, crypto.SHA256) + require.NoError(t, err) + defer closeVerifier() + + opts := DefaultVerifyBlobOptions() + opts.CommonVerifyOptions.IgnoreTlog = false + material, err := trustedMaterialForBundle(opts, keyVerifier, false) + require.NoError(t, err) + require.NotEmpty(t, material.RekorLogs()) + require.NotEmpty(t, material.TimestampingAuthorities()) + }) + + t.Run("verifies keyless public-good bundle", func(t *testing.T) { + opts := DefaultVerifyBlobOptions() + opts.BundlePath = "./testdata/sigstore-js-2.0.0-provenance.sigstore.json" + opts.CertVerify.CertIdentityRegexp = "^https://github.com/sigstore/sigstore-js/" + opts.CertVerify.CertOidcIssuer = "https://token.actions.githubusercontent.com" + opts.CommonVerifyOptions.IgnoreTlog = false + + const digestReference = "sha512:46d4e2f74c4877316640000a6fdf8a8b59f1e0847667973e9859f774dd31b8f1e0937813b777fb66a2ac67d50540fe34640966eee9fc2ccca387082b4c85cd3c" + result, err := SigstoreVerifyBundleWithOptions(ctx, digestReference, opts) + require.NoError(t, err) + require.NotNil(t, result) + require.NotNil(t, result.VerifiedIdentity) + + invalidOpts := opts + invalidOpts.CertVerify.CertIdentityRegexp = "^https://github.com/sigstore/other-project/" + _, err = SigstoreVerifyBundleWithOptions(ctx, digestReference, invalidOpts) + require.Error(t, err) + }) +} + +func TestSigstoreBundleValidationErrorsUseLibraryTerms(t *testing.T) { + tests := []struct { + name string + configure func(*VerifyBlobOptions) + want string + }{ + { + name: "requires bundle path", + want: "bundle path is required", + }, + { + name: "rejects key with certificate identity", + configure: func(opts *VerifyBlobOptions) { + opts.BundlePath = "bundle.json" + opts.Key = "key.pem" + opts.CertVerify.CertIdentity = "https://example.test" + }, + want: "key cannot be combined with certificate identity verification", + }, + { + name: "rejects key with security key", + configure: func(opts *VerifyBlobOptions) { + opts.BundlePath = "bundle.json" + opts.Key = "key.pem" + opts.SecurityKey.Use = true + }, + want: "key cannot be combined with security-key verification", + }, + { + name: "rejects detached signature", + configure: func(opts *VerifyBlobOptions) { + opts.Signature = "signature" + }, + want: "unsupported verification material for Sigstore bundles: detached signature", + }, + { + name: "rejects certificate", + configure: func(opts *VerifyBlobOptions) { + opts.CertVerify.Cert = "certificate" + }, + want: "unsupported verification material for Sigstore bundles: certificate", + }, + { + name: "rejects certificate chain", + configure: func(opts *VerifyBlobOptions) { + opts.CertVerify.CertChain = "chain" + }, + want: "unsupported verification material for Sigstore bundles: certificate chain", + }, + { + name: "rejects certificate authority roots", + configure: func(opts *VerifyBlobOptions) { + opts.CertVerify.CARoots = "roots" + }, + want: "unsupported verification material for Sigstore bundles: certificate authority roots", + }, + { + name: "rejects certificate authority intermediates", + configure: func(opts *VerifyBlobOptions) { + opts.CertVerify.CAIntermediates = "intermediates" + }, + want: "unsupported verification material for Sigstore bundles: certificate authority intermediates", + }, + { + name: "rejects timestamp certificate chain", + configure: func(opts *VerifyBlobOptions) { + opts.CommonVerifyOptions.TSACertChainPath = "timestamp-chain" + }, + want: "unsupported verification material for Sigstore bundles: timestamp certificate chain", + }, + { + name: "rejects signed certificate timestamp", + configure: func(opts *VerifyBlobOptions) { + opts.CertVerify.SCT = "sct" + }, + want: "unsupported verification material for Sigstore bundles: signed certificate timestamp", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + opts := DefaultVerifyBlobOptions() + if tc.configure != nil { + tc.configure(&opts) + } + + var err error + if opts.BundlePath == "" { + _, err = SigstoreVerifyBundleWithOptions(testutil.TestContext(t), "", opts) + } else { + err = validateSigstoreBundleOptions(opts) + } + require.ErrorContains(t, err, tc.want) + require.NotContains(t, err.Error(), "--") + }) + } +} From abfa98d00fa8732a2f7d04b9425f68d5a68ac71a Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Fri, 7 Aug 2026 18:12:51 +0000 Subject: [PATCH 7/8] fix: k8s source test and http context Signed-off-by: Brandt Keller --- src/pkg/signing/sigstore.go | 22 ++++++----- src/pkg/signing/sigstore_test.go | 66 ++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/src/pkg/signing/sigstore.go b/src/pkg/signing/sigstore.go index b5b4c6c06e..53dbe3cd12 100644 --- a/src/pkg/signing/sigstore.go +++ b/src/pkg/signing/sigstore.go @@ -103,7 +103,7 @@ func SigstoreVerifyBundleWithOptions(ctx context.Context, blobPath string, opts return nil, fmt.Errorf("creating Sigstore verifier: %w", err) } - artifactPolicy, err := bundleArtifactPolicy(blobPath) + artifactPolicy, err := bundleArtifactPolicy(ctx, blobPath) if err != nil { return nil, err } @@ -111,7 +111,7 @@ func SigstoreVerifyBundleWithOptions(ctx context.Context, blobPath string, opts if err != nil { return nil, err } - l.Debug("blob signature verified successfully with sigstore-go") + l.Debug("blob signature verified successfully") return result, nil } @@ -299,7 +299,7 @@ func resolveBundleVerifier(ctx context.Context, opts VerifyBlobOptions, hashAlgo if !errors.As(err, &providerNotFound) { return nil, func() {}, fmt.Errorf("kms get: %w", err) } - raw, err := loadPublicKeyReference(opts.Key) + raw, err := loadPublicKeyReference(ctx, opts.Key) if err != nil { return nil, func() {}, err } @@ -315,7 +315,7 @@ func verifierFromPEM(raw []byte, hashAlgorithm crypto.Hash) (signature.Verifier, return verifier, func() {}, err } -func loadPublicKeyReference(reference string) ([]byte, error) { +func loadPublicKeyReference(ctx context.Context, reference string) ([]byte, error) { switch { case strings.HasPrefix(reference, "env://"): value, ok := os.LookupEnv(strings.TrimPrefix(reference, "env://")) @@ -325,7 +325,11 @@ func loadPublicKeyReference(reference string) ([]byte, error) { return []byte(value), nil case strings.HasPrefix(reference, "http://") || strings.HasPrefix(reference, "https://"): // #nosec G107 -- the public key location is an explicit user input. - response, err := http.Get(reference) + request, err := http.NewRequestWithContext(ctx, http.MethodGet, reference, nil) + if err != nil { + return nil, err + } + response, err := http.DefaultClient.Do(request) if err != nil { return nil, err } @@ -347,17 +351,17 @@ func loadPublicKeyReference(reference string) ([]byte, error) { } } -func readBundleArtifact(reference string) ([]byte, error) { +func readBundleArtifact(ctx context.Context, reference string) ([]byte, error) { if reference == "-" { return io.ReadAll(os.Stdin) } - return loadPublicKeyReference(reference) + return loadPublicKeyReference(ctx, reference) } // bundleArtifactPolicy mirrors cosign's blob verifier: an unreadable artifact // may instead be an explicitly supplied algorithm:hex-digest reference. -func bundleArtifactPolicy(reference string) (verify.ArtifactPolicyOption, error) { - artifact, readErr := readBundleArtifact(reference) +func bundleArtifactPolicy(ctx context.Context, reference string) (verify.ArtifactPolicyOption, error) { + artifact, readErr := readBundleArtifact(ctx, reference) if readErr == nil { return verify.WithArtifact(bytes.NewReader(artifact)), nil } diff --git a/src/pkg/signing/sigstore_test.go b/src/pkg/signing/sigstore_test.go index 620fa8798a..eb1c516fe4 100644 --- a/src/pkg/signing/sigstore_test.go +++ b/src/pkg/signing/sigstore_test.go @@ -4,19 +4,24 @@ package signing import ( + "context" "crypto" "crypto/sha256" "encoding/hex" + "encoding/json" + "fmt" "net" "net/http" "net/http/httptest" "os" "path/filepath" "testing" + "time" "github.com/stretchr/testify/require" "github.com/zarf-dev/zarf/src/test/testutil" + corev1 "k8s.io/api/core/v1" ) func TestSigstoreVerifyBundleWithOptions(t *testing.T) { @@ -89,6 +94,44 @@ func TestSigstoreVerifyBundleWithOptions(t *testing.T) { require.NoError(t, verify(t, blobPath, bundlePath, "env://ZARF_TEST_COSIGN_PUBLIC_KEY")) }) + t.Run("accepts Kubernetes public-key references", func(t *testing.T) { + blobPath, bundlePath := newBundle(t) + publicKey, err := os.ReadFile(pubPath) + require.NoError(t, err) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) { + w.Header().Set("Content-Type", "application/json") + if request.URL.Path != "/api/v1/namespaces/signature-test/secrets/signing-public-key" { + http.NotFound(w, request) + return + } + if err := json.NewEncoder(w).Encode(corev1.Secret{ + Data: map[string][]byte{"cosign.pub": publicKey}, + }); err != nil { + t.Errorf("writing Kubernetes Secret response: %v", err) + } + })) + t.Cleanup(server.Close) + + kubeconfigPath := filepath.Join(t.TempDir(), "kubeconfig") + kubeconfig := fmt.Sprintf(`apiVersion: v1 +clusters: +- cluster: + server: %s + name: signing-test +contexts: +- context: + cluster: signing-test + namespace: signature-test + name: signing-test +current-context: signing-test +`, server.URL) + require.NoError(t, os.WriteFile(kubeconfigPath, []byte(kubeconfig), 0o600)) + t.Setenv("KUBECONFIG", kubeconfigPath) + + require.NoError(t, verify(t, blobPath, bundlePath, "k8s://signature-test/signing-public-key")) + }) + t.Run("accepts URL public-key references", func(t *testing.T) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { @@ -108,6 +151,29 @@ func TestSigstoreVerifyBundleWithOptions(t *testing.T) { require.NoError(t, verify(t, blobPath, bundlePath, server.URL)) }) + t.Run("URL public-key retrieval honors verification timeout", func(t *testing.T) { + blobPath, bundlePath := newBundle(t) + requestStarted := make(chan struct{}) + server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, request *http.Request) { + close(requestStarted) + <-request.Context().Done() + })) + t.Cleanup(server.Close) + + opts := DefaultVerifyBlobOptions() + opts.Key = server.URL + opts.BundlePath = bundlePath + opts.Timeout = 100 * time.Millisecond + _, err := SigstoreVerifyBundleWithOptions(ctx, blobPath, opts) + + require.ErrorIs(t, err, context.DeadlineExceeded) + select { + case <-requestStarted: + default: + t.Fatal("expected public-key request") + } + }) + t.Run("rejects wrong keys and corrupt bundles without cosign fallback", func(t *testing.T) { blobPath, bundlePath := newBundle(t) require.Error(t, verify(t, blobPath, bundlePath, "./testdata/nonexistent.pub")) From b2e154a8ab40be75ed526721b53223000f007fb4 Mon Sep 17 00:00:00 2001 From: Brandt Keller Date: Fri, 7 Aug 2026 18:43:59 +0000 Subject: [PATCH 8/8] feat: isolate kms providers from either implementation Signed-off-by: Brandt Keller --- src/pkg/signing/cosign.go | 6 ------ src/pkg/signing/providers.go | 12 ++++++++++++ src/pkg/signing/providers_test.go | 25 +++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 src/pkg/signing/providers.go create mode 100644 src/pkg/signing/providers_test.go diff --git a/src/pkg/signing/cosign.go b/src/pkg/signing/cosign.go index 3ee8c0bf83..97d513f326 100644 --- a/src/pkg/signing/cosign.go +++ b/src/pkg/signing/cosign.go @@ -19,12 +19,6 @@ import ( // Register ambient OIDC credential providers (GitHub Actions, GCP, SPIFFE, etc.) _ "github.com/sigstore/cosign/v3/pkg/providers/all" - // Register the provider-specific plugins - _ "github.com/sigstore/sigstore/pkg/signature/kms/aws" - _ "github.com/sigstore/sigstore/pkg/signature/kms/azure" - _ "github.com/sigstore/sigstore/pkg/signature/kms/gcp" - _ "github.com/sigstore/sigstore/pkg/signature/kms/hashivault" - "github.com/zarf-dev/zarf/src/pkg/logger" ) diff --git a/src/pkg/signing/providers.go b/src/pkg/signing/providers.go new file mode 100644 index 0000000000..a8d0f40350 --- /dev/null +++ b/src/pkg/signing/providers.go @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package signing + +import ( + // Register KMS schemes used by both legacy Cosign and direct Sigstore verification. + _ "github.com/sigstore/sigstore/pkg/signature/kms/aws" + _ "github.com/sigstore/sigstore/pkg/signature/kms/azure" + _ "github.com/sigstore/sigstore/pkg/signature/kms/gcp" + _ "github.com/sigstore/sigstore/pkg/signature/kms/hashivault" +) diff --git a/src/pkg/signing/providers_test.go b/src/pkg/signing/providers_test.go new file mode 100644 index 0000000000..970ded1052 --- /dev/null +++ b/src/pkg/signing/providers_test.go @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2021-Present The Zarf Authors + +package signing + +import ( + "testing" + + "github.com/sigstore/sigstore/pkg/signature/kms" + "github.com/stretchr/testify/require" +) + +func TestKMSProvidersRegistered(t *testing.T) { + providers := kms.SupportedProviders() + + for _, scheme := range []string{ + "awskms://", + "azurekms://", + "gcpkms://", + "hashivault://", + "openbao://", + } { + require.Contains(t, providers, scheme) + } +}