Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions internal/controller/helmchart_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions internal/controller/ocirepository_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
57 changes: 48 additions & 9 deletions internal/oci/cosign/cosign.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package cosign
import (
"context"
"crypto"
"crypto/tls"
"fmt"
"sync"
"time"
Expand All @@ -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"
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
127 changes: 127 additions & 0 deletions internal/oci/cosign/verify_insecure_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
Loading