From 81bdda370875baa261747fccb4dca740aa0e473d Mon Sep 17 00:00:00 2001 From: leigh capili Date: Thu, 4 Jun 2026 01:59:33 -0600 Subject: [PATCH 1/4] api: regenerate deepcopy for TrustedRootSecretRef field Signed-off-by: leigh capili --- api/v1/zz_generated.deepcopy.go | 5 +++++ go.mod | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 14f1ba3c2..dc13584ae 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -970,6 +970,11 @@ func (in *OCIRepositoryVerification) DeepCopyInto(out *OCIRepositoryVerification *out = make([]OIDCIdentityMatch, len(*in)) copy(*out, *in) } + if in.TrustedRootSecretRef != nil { + in, out := &in.TrustedRootSecretRef, &out.TrustedRootSecretRef + *out = new(meta.LocalObjectReference) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OCIRepositoryVerification. diff --git a/go.mod b/go.mod index 77702fe34..316ce51e3 100644 --- a/go.mod +++ b/go.mod @@ -58,6 +58,7 @@ require ( github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 github.com/prometheus/client_golang v1.23.2 github.com/sigstore/cosign/v3 v3.0.6 + github.com/sigstore/rekor v1.5.1 github.com/sigstore/sigstore v1.10.5 github.com/sigstore/sigstore-go v1.1.4 github.com/sirupsen/logrus v1.9.4 @@ -332,7 +333,6 @@ require ( github.com/shopspring/decimal v1.4.0 // indirect github.com/sigstore/fulcio v1.8.5 // indirect github.com/sigstore/protobuf-specs v0.5.0 // indirect - github.com/sigstore/rekor v1.5.1 // indirect github.com/sigstore/rekor-tiles/v2 v2.2.1 // indirect github.com/sigstore/timestamp-authority/v2 v2.0.6 // indirect github.com/skeema/knownhosts v1.3.2 // indirect From 41cd3828aa4f70f8513dee47052fffb36ae78f3d Mon Sep 17 00:00:00 2001 From: leigh capili Date: Thu, 4 Jun 2026 01:59:40 -0600 Subject: [PATCH 2/4] cosign: add WithInsecure and WithTLSConfig verifier options WithInsecure passes name.Insecure to GetBundles/VerifyImageAttestations for v3 bundle discovery on HTTP registries. Follows the same pattern as notation's WithInsecureRegistry. WithTLSConfig passes a *tls.Config to the Rekor client, supporting private CAs from certSecretRef. Replaces the cosign CLI rekor wrapper with a direct rekor.GetRekorClient call to thread the option through. Includes a test using a fake non-loopback hostname to verify the insecure option is required for bundle discovery on HTTP registries. Signed-off-by: leigh capili --- internal/oci/cosign/cosign.go | 57 +++++++-- internal/oci/cosign/verify_insecure_test.go | 127 ++++++++++++++++++++ 2 files changed, 175 insertions(+), 9 deletions(-) create mode 100644 internal/oci/cosign/verify_insecure_test.go diff --git a/internal/oci/cosign/cosign.go b/internal/oci/cosign/cosign.go index d87d91dae..3be4ab03e 100644 --- a/internal/oci/cosign/cosign.go +++ b/internal/oci/cosign/cosign.go @@ -19,6 +19,7 @@ package cosign import ( "context" "crypto" + "crypto/tls" "fmt" "sync" "time" @@ -27,9 +28,10 @@ import ( "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio" coptions "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/oci" + rekorclient "github.com/sigstore/rekor/pkg/client" + rekorgenclient "github.com/sigstore/rekor/pkg/generated/client" ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" "github.com/sigstore/sigstore-go/pkg/root" @@ -45,6 +47,8 @@ type options struct { rOpt []remote.Option identities []cosign.Identity trustedRoot []byte + insecure bool + tlsConfig *tls.Config } // Options is a function that configures the options applied to a Verifier. @@ -83,9 +87,27 @@ func WithTrustedRoot(trustedRoot []byte) Options { } } +// WithInsecure sets the verifier to use HTTP when discovering v3 bundle +// signatures from the container registry via OCI referrers tag fallback. +// Does not affect Rekor connections. +func WithInsecure(insecure bool) Options { + return func(opts *options) { + opts.insecure = insecure + } +} + +// WithTLSConfig sets the TLS configuration for Rekor client connections. +// When nil, the system trust store is used. +func WithTLSConfig(tlsConfig *tls.Config) Options { + return func(opts *options) { + opts.tlsConfig = tlsConfig + } +} + // CosignVerifier is a struct which is responsible for executing verification logic. type CosignVerifier struct { - opts *cosign.CheckOpts + opts *cosign.CheckOpts + insecure bool } // CosignVerifierFactory is a factory for creating Verifiers with shared state. @@ -152,7 +174,7 @@ func (f *CosignVerifierFactory) NewCosignVerifier(ctx context.Context, opts ...O return nil, err } - return &CosignVerifier{opts: checkOpts}, nil + return &CosignVerifier{opts: checkOpts, insecure: o.insecure}, nil } // Keyless verification: when a custom trusted root is provided, use it @@ -171,16 +193,16 @@ func (f *CosignVerifierFactory) NewCosignVerifier(ctx context.Context, opts ...O return nil, fmt.Errorf("unable to extract Rekor URL from trusted root: %w", err) } - checkOpts.RekorClient, err = rekor.NewClient(rekorURL) + checkOpts.RekorClient, err = newRekorClient(rekorURL, o.tlsConfig) if err != nil { return nil, fmt.Errorf("unable to create Rekor client: %w", err) } - return &CosignVerifier{opts: checkOpts}, nil + return &CosignVerifier{opts: checkOpts, insecure: o.insecure}, nil } // Keyless verification using the public Sigstore infrastructure. - checkOpts.RekorClient, err = rekor.NewClient(coptions.DefaultRekorURL) + checkOpts.RekorClient, err = newRekorClient(coptions.DefaultRekorURL, o.tlsConfig) if err != nil { return nil, fmt.Errorf("unable to create Rekor client: %w", err) } @@ -233,7 +255,17 @@ func (f *CosignVerifierFactory) NewCosignVerifier(ctx context.Context, opts ...O return nil, fmt.Errorf("unable to get Fulcio intermediate certs: %w", err) } - return &CosignVerifier{opts: checkOpts}, nil + return &CosignVerifier{opts: checkOpts, insecure: o.insecure}, nil +} + +// newRekorClient creates a Rekor client with optional TLS configuration. +// If tlsConfig is nil, the default system trust store is used. +func newRekorClient(rekorURL string, tlsConfig *tls.Config) (*rekorgenclient.Rekor, error) { + opts := []rekorclient.Option{rekorclient.WithUserAgent(coptions.UserAgent())} + if tlsConfig != nil { + opts = append(opts, rekorclient.WithTLSConfig(tlsConfig)) + } + return rekorclient.GetRekorClient(rekorURL, opts...) } // rekorURLFromTrustedRoot extracts the Rekor base URL from a trusted root's @@ -265,14 +297,21 @@ func (v *CosignVerifier) Verify(ctx context.Context, ref name.Reference) (soci.V var signatures []oci.Signature // copy options since we'll need to change them based on bundle discovery on the ref opts := *v.opts - newBundles, _, err := cosign.GetBundles(ctx, ref, opts.RegistryClientOpts) + + // Pass insecure to GetBundles for internal bundle digest references. + var nameOpts []name.Option + if v.insecure { + nameOpts = append(nameOpts, name.Insecure) + } + + newBundles, _, err := cosign.GetBundles(ctx, ref, opts.RegistryClientOpts, nameOpts...) // if no bundles are returned, let's fallback to the cosign v2 behavior, similar to the cosign CLI if len(newBundles) == 0 || err != nil { opts.NewBundleFormat = false signatures, _, err = cosign.VerifyImageSignatures(ctx, ref, &opts) } else { opts.NewBundleFormat = true - signatures, _, err = cosign.VerifyImageAttestations(ctx, ref, &opts) + signatures, _, err = cosign.VerifyImageAttestations(ctx, ref, &opts, nameOpts...) } if err != nil { return soci.VerificationResultFailed, err diff --git a/internal/oci/cosign/verify_insecure_test.go b/internal/oci/cosign/verify_insecure_test.go new file mode 100644 index 000000000..8247479b9 --- /dev/null +++ b/internal/oci/cosign/verify_insecure_test.go @@ -0,0 +1,127 @@ +/* +Copyright 2026 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cosign + +import ( + "context" + "fmt" + "net" + "net/http" + "os" + "path" + "testing" + "time" + + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/types" + . "github.com/onsi/gomega" + coptions "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/sign" + "github.com/sigstore/cosign/v3/pkg/cosign" + + soci "github.com/fluxcd/source-controller/internal/oci" + testregistry "github.com/fluxcd/source-controller/tests/registry" +) + +// TestVerifyInsecureV3Bundle tests v3 bundle-format signature verification +// against an HTTP-only registry accessed via a non-loopback hostname. +// +// go-containerregistry uses HTTP implicitly for localhost/127.0.0.1/RFC1918. +// This test uses a fake external hostname to cover the case of in-cluster +// registries like "my-registry:5000" where name.Insecure must be explicit. +// +// GetBundles() creates new name.Reference objects for bundle digests via +// name.ParseReference without carrying over name.Insecure from the original +// ref, so WithInsecure(true) on the verifier is needed to make it work. +func TestVerifyInsecureV3Bundle(t *testing.T) { + g := NewWithT(t) + ctx := context.Background() + + // Start an HTTP-only registry on a random port + registryAddr := testregistry.New(t) + _, port, _ := net.SplitHostPort(registryAddr) + + // Use a fake external hostname that requires name.Insecure + fakeHost := "fake-external-registry.example.com" + fakeAddr := fmt.Sprintf("%s:%s", fakeHost, port) + + // Custom transport that resolves the fake hostname to 127.0.0.1 + transport := &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + if host, p, _ := net.SplitHostPort(addr); host == fakeHost { + addr = net.JoinHostPort("127.0.0.1", p) + } + return (&net.Dialer{}).DialContext(ctx, network, addr) + }, + } + + // Generate cosign key pair + keys, err := cosign.GenerateKeyPair(func(b bool) ([]byte, error) { + return []byte(""), nil + }) + g.Expect(err).NotTo(HaveOccurred()) + + tmpDir := t.TempDir() + keyPath := path.Join(tmpDir, "cosign.key") + err = os.WriteFile(keyPath, keys.PrivateBytes, 0600) + g.Expect(err).NotTo(HaveOccurred()) + + // Push a test image using the real loopback address + realRef := fmt.Sprintf("%s/test/v3bundle:v1", registryAddr) + img := mutate.MediaType(empty.Image, types.OCIManifestSchema1) + err = crane.Push(img, realRef) + g.Expect(err).NotTo(HaveOccurred()) + + // Sign with v3 bundle format using the real loopback address + // (the bundle is stored by digest, so it's discoverable from any hostname) + pf := func(_ bool) ([]byte, error) { return []byte(""), nil } + ko := coptions.KeyOpts{ + KeyRef: keyPath, + PassFunc: pf, + NewBundleFormat: true, + } + ro := &coptions.RootOptions{Timeout: 30 * time.Second} + err = sign.SignCmd(ctx, ro, ko, coptions.SignOptions{ + Upload: true, + SkipConfirmation: true, + TlogUpload: false, + NewBundleFormat: true, + Registry: coptions.RegistryOptions{AllowInsecure: true, AllowHTTPRegistry: true}, + }, []string{realRef}) + g.Expect(err).NotTo(HaveOccurred()) + + // Parse reference with name.Insecure (as source-controller does for spec.insecure=true) + ref, err := name.ParseReference(fmt.Sprintf("%s/test/v3bundle:v1", fakeAddr), name.Insecure) + g.Expect(err).NotTo(HaveOccurred()) + + // Verify using the CosignVerifier with the custom transport + vf := NewCosignVerifierFactory() + verifier, err := vf.NewCosignVerifier(ctx, + WithPublicKey(keys.PublicBytes), + WithRemoteOptions(remote.WithTransport(transport)), + WithInsecure(true), + ) + g.Expect(err).NotTo(HaveOccurred()) + + result, err := verifier.Verify(ctx, ref) + g.Expect(err).NotTo(HaveOccurred(), "v3 bundle verification should succeed on insecure registry with non-loopback hostname") + g.Expect(result).To(Equal(soci.VerificationResultSuccess)) +} From c22363b583e0eca24bf62f571bee638e630f7b6d Mon Sep 17 00:00:00 2001 From: leigh capili Date: Thu, 4 Jun 2026 01:59:43 -0600 Subject: [PATCH 3/4] controller: wire insecure and TLS config to cosign verifier Pass obj.Spec.Insecure and transport.TLSClientConfig to the cosign verifier so v3 bundle discovery and Rekor connections use the same transport settings as the registry. Signed-off-by: leigh capili --- internal/controller/ocirepository_controller.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/controller/ocirepository_controller.go b/internal/controller/ocirepository_controller.go index 69dd408bd..9e2090eca 100644 --- a/internal/controller/ocirepository_controller.go +++ b/internal/controller/ocirepository_controller.go @@ -680,6 +680,8 @@ func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *sour case "cosign": defaultCosignOciOpts := []scosign.Options{ scosign.WithRemoteOptions(opt...), + scosign.WithInsecure(obj.Spec.Insecure), + scosign.WithTLSConfig(transport.TLSClientConfig), } // If a trusted root secret is provided, read and pass it to the verifier. From 34c7c9c32649f946193bf3e0b5931e8d2ef86949 Mon Sep 17 00:00:00 2001 From: leigh capili Date: Thu, 4 Jun 2026 01:59:48 -0600 Subject: [PATCH 4/4] controller: pass TLS config and insecure to cosign verifier for HelmChart OCI Pass clientOpts.TLSConfig and clientOpts.Insecure to the cosign verifier in makeVerifiers so that HelmChart verification of OCI-sourced charts works against registries behind private CAs and on HTTP. Signed-off-by: leigh capili --- internal/controller/helmchart_controller.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/controller/helmchart_controller.go b/internal/controller/helmchart_controller.go index 963d75dde..d8f2eb679 100644 --- a/internal/controller/helmchart_controller.go +++ b/internal/controller/helmchart_controller.go @@ -1313,6 +1313,7 @@ func (r *HelmChartReconciler) makeVerifiers(ctx context.Context, obj *sourcev1.H case "cosign": defaultCosignOciOpts := []scosign.Options{ scosign.WithRemoteOptions(verifyOpts...), + scosign.WithTLSConfig(clientOpts.TLSConfig), } // get the public keys from the given secret