Skip to content

Commit 867bc3a

Browse files
authored
Merge pull request #2061 from fluxcd/sigstore-transport
cosign: fix v3 bundle verify on http and private CA registries + pass TLS to Rekor
2 parents 9b2557e + 34c7c9c commit 867bc3a

6 files changed

Lines changed: 184 additions & 10 deletions

File tree

api/v1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ require (
5858
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5
5959
github.com/prometheus/client_golang v1.23.2
6060
github.com/sigstore/cosign/v3 v3.0.6
61+
github.com/sigstore/rekor v1.5.1
6162
github.com/sigstore/sigstore v1.10.5
6263
github.com/sigstore/sigstore-go v1.1.4
6364
github.com/sirupsen/logrus v1.9.4
@@ -332,7 +333,6 @@ require (
332333
github.com/shopspring/decimal v1.4.0 // indirect
333334
github.com/sigstore/fulcio v1.8.5 // indirect
334335
github.com/sigstore/protobuf-specs v0.5.0 // indirect
335-
github.com/sigstore/rekor v1.5.1 // indirect
336336
github.com/sigstore/rekor-tiles/v2 v2.2.1 // indirect
337337
github.com/sigstore/timestamp-authority/v2 v2.0.6 // indirect
338338
github.com/skeema/knownhosts v1.3.2 // indirect

internal/controller/helmchart_controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,6 +1313,7 @@ func (r *HelmChartReconciler) makeVerifiers(ctx context.Context, obj *sourcev1.H
13131313
case "cosign":
13141314
defaultCosignOciOpts := []scosign.Options{
13151315
scosign.WithRemoteOptions(verifyOpts...),
1316+
scosign.WithTLSConfig(clientOpts.TLSConfig),
13161317
}
13171318

13181319
// get the public keys from the given secret

internal/controller/ocirepository_controller.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,8 @@ func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *sour
680680
case "cosign":
681681
defaultCosignOciOpts := []scosign.Options{
682682
scosign.WithRemoteOptions(opt...),
683+
scosign.WithInsecure(obj.Spec.Insecure),
684+
scosign.WithTLSConfig(transport.TLSClientConfig),
683685
}
684686

685687
// If a trusted root secret is provided, read and pass it to the verifier.

internal/oci/cosign/cosign.go

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package cosign
1919
import (
2020
"context"
2121
"crypto"
22+
"crypto/tls"
2223
"fmt"
2324
"sync"
2425
"time"
@@ -27,9 +28,10 @@ import (
2728
"github.com/google/go-containerregistry/pkg/v1/remote"
2829
"github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio"
2930
coptions "github.com/sigstore/cosign/v3/cmd/cosign/cli/options"
30-
"github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor"
3131
"github.com/sigstore/cosign/v3/pkg/cosign"
3232
"github.com/sigstore/cosign/v3/pkg/oci"
33+
rekorclient "github.com/sigstore/rekor/pkg/client"
34+
rekorgenclient "github.com/sigstore/rekor/pkg/generated/client"
3335

3436
ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote"
3537
"github.com/sigstore/sigstore-go/pkg/root"
@@ -45,6 +47,8 @@ type options struct {
4547
rOpt []remote.Option
4648
identities []cosign.Identity
4749
trustedRoot []byte
50+
insecure bool
51+
tlsConfig *tls.Config
4852
}
4953

5054
// Options is a function that configures the options applied to a Verifier.
@@ -83,9 +87,27 @@ func WithTrustedRoot(trustedRoot []byte) Options {
8387
}
8488
}
8589

90+
// WithInsecure sets the verifier to use HTTP when discovering v3 bundle
91+
// signatures from the container registry via OCI referrers tag fallback.
92+
// Does not affect Rekor connections.
93+
func WithInsecure(insecure bool) Options {
94+
return func(opts *options) {
95+
opts.insecure = insecure
96+
}
97+
}
98+
99+
// WithTLSConfig sets the TLS configuration for Rekor client connections.
100+
// When nil, the system trust store is used.
101+
func WithTLSConfig(tlsConfig *tls.Config) Options {
102+
return func(opts *options) {
103+
opts.tlsConfig = tlsConfig
104+
}
105+
}
106+
86107
// CosignVerifier is a struct which is responsible for executing verification logic.
87108
type CosignVerifier struct {
88-
opts *cosign.CheckOpts
109+
opts *cosign.CheckOpts
110+
insecure bool
89111
}
90112

91113
// CosignVerifierFactory is a factory for creating Verifiers with shared state.
@@ -152,7 +174,7 @@ func (f *CosignVerifierFactory) NewCosignVerifier(ctx context.Context, opts ...O
152174
return nil, err
153175
}
154176

155-
return &CosignVerifier{opts: checkOpts}, nil
177+
return &CosignVerifier{opts: checkOpts, insecure: o.insecure}, nil
156178
}
157179

158180
// Keyless verification: when a custom trusted root is provided, use it
@@ -171,16 +193,16 @@ func (f *CosignVerifierFactory) NewCosignVerifier(ctx context.Context, opts ...O
171193
return nil, fmt.Errorf("unable to extract Rekor URL from trusted root: %w", err)
172194
}
173195

174-
checkOpts.RekorClient, err = rekor.NewClient(rekorURL)
196+
checkOpts.RekorClient, err = newRekorClient(rekorURL, o.tlsConfig)
175197
if err != nil {
176198
return nil, fmt.Errorf("unable to create Rekor client: %w", err)
177199
}
178200

179-
return &CosignVerifier{opts: checkOpts}, nil
201+
return &CosignVerifier{opts: checkOpts, insecure: o.insecure}, nil
180202
}
181203

182204
// Keyless verification using the public Sigstore infrastructure.
183-
checkOpts.RekorClient, err = rekor.NewClient(coptions.DefaultRekorURL)
205+
checkOpts.RekorClient, err = newRekorClient(coptions.DefaultRekorURL, o.tlsConfig)
184206
if err != nil {
185207
return nil, fmt.Errorf("unable to create Rekor client: %w", err)
186208
}
@@ -233,7 +255,17 @@ func (f *CosignVerifierFactory) NewCosignVerifier(ctx context.Context, opts ...O
233255
return nil, fmt.Errorf("unable to get Fulcio intermediate certs: %w", err)
234256
}
235257

236-
return &CosignVerifier{opts: checkOpts}, nil
258+
return &CosignVerifier{opts: checkOpts, insecure: o.insecure}, nil
259+
}
260+
261+
// newRekorClient creates a Rekor client with optional TLS configuration.
262+
// If tlsConfig is nil, the default system trust store is used.
263+
func newRekorClient(rekorURL string, tlsConfig *tls.Config) (*rekorgenclient.Rekor, error) {
264+
opts := []rekorclient.Option{rekorclient.WithUserAgent(coptions.UserAgent())}
265+
if tlsConfig != nil {
266+
opts = append(opts, rekorclient.WithTLSConfig(tlsConfig))
267+
}
268+
return rekorclient.GetRekorClient(rekorURL, opts...)
237269
}
238270

239271
// 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
265297
var signatures []oci.Signature
266298
// copy options since we'll need to change them based on bundle discovery on the ref
267299
opts := *v.opts
268-
newBundles, _, err := cosign.GetBundles(ctx, ref, opts.RegistryClientOpts)
300+
301+
// Pass insecure to GetBundles for internal bundle digest references.
302+
var nameOpts []name.Option
303+
if v.insecure {
304+
nameOpts = append(nameOpts, name.Insecure)
305+
}
306+
307+
newBundles, _, err := cosign.GetBundles(ctx, ref, opts.RegistryClientOpts, nameOpts...)
269308
// if no bundles are returned, let's fallback to the cosign v2 behavior, similar to the cosign CLI
270309
if len(newBundles) == 0 || err != nil {
271310
opts.NewBundleFormat = false
272311
signatures, _, err = cosign.VerifyImageSignatures(ctx, ref, &opts)
273312
} else {
274313
opts.NewBundleFormat = true
275-
signatures, _, err = cosign.VerifyImageAttestations(ctx, ref, &opts)
314+
signatures, _, err = cosign.VerifyImageAttestations(ctx, ref, &opts, nameOpts...)
276315
}
277316
if err != nil {
278317
return soci.VerificationResultFailed, err
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
Copyright 2026 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cosign
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"net"
23+
"net/http"
24+
"os"
25+
"path"
26+
"testing"
27+
"time"
28+
29+
"github.com/google/go-containerregistry/pkg/crane"
30+
"github.com/google/go-containerregistry/pkg/name"
31+
"github.com/google/go-containerregistry/pkg/v1/empty"
32+
"github.com/google/go-containerregistry/pkg/v1/mutate"
33+
"github.com/google/go-containerregistry/pkg/v1/remote"
34+
"github.com/google/go-containerregistry/pkg/v1/types"
35+
. "github.com/onsi/gomega"
36+
coptions "github.com/sigstore/cosign/v3/cmd/cosign/cli/options"
37+
"github.com/sigstore/cosign/v3/cmd/cosign/cli/sign"
38+
"github.com/sigstore/cosign/v3/pkg/cosign"
39+
40+
soci "github.com/fluxcd/source-controller/internal/oci"
41+
testregistry "github.com/fluxcd/source-controller/tests/registry"
42+
)
43+
44+
// TestVerifyInsecureV3Bundle tests v3 bundle-format signature verification
45+
// against an HTTP-only registry accessed via a non-loopback hostname.
46+
//
47+
// go-containerregistry uses HTTP implicitly for localhost/127.0.0.1/RFC1918.
48+
// This test uses a fake external hostname to cover the case of in-cluster
49+
// registries like "my-registry:5000" where name.Insecure must be explicit.
50+
//
51+
// GetBundles() creates new name.Reference objects for bundle digests via
52+
// name.ParseReference without carrying over name.Insecure from the original
53+
// ref, so WithInsecure(true) on the verifier is needed to make it work.
54+
func TestVerifyInsecureV3Bundle(t *testing.T) {
55+
g := NewWithT(t)
56+
ctx := context.Background()
57+
58+
// Start an HTTP-only registry on a random port
59+
registryAddr := testregistry.New(t)
60+
_, port, _ := net.SplitHostPort(registryAddr)
61+
62+
// Use a fake external hostname that requires name.Insecure
63+
fakeHost := "fake-external-registry.example.com"
64+
fakeAddr := fmt.Sprintf("%s:%s", fakeHost, port)
65+
66+
// Custom transport that resolves the fake hostname to 127.0.0.1
67+
transport := &http.Transport{
68+
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
69+
if host, p, _ := net.SplitHostPort(addr); host == fakeHost {
70+
addr = net.JoinHostPort("127.0.0.1", p)
71+
}
72+
return (&net.Dialer{}).DialContext(ctx, network, addr)
73+
},
74+
}
75+
76+
// Generate cosign key pair
77+
keys, err := cosign.GenerateKeyPair(func(b bool) ([]byte, error) {
78+
return []byte(""), nil
79+
})
80+
g.Expect(err).NotTo(HaveOccurred())
81+
82+
tmpDir := t.TempDir()
83+
keyPath := path.Join(tmpDir, "cosign.key")
84+
err = os.WriteFile(keyPath, keys.PrivateBytes, 0600)
85+
g.Expect(err).NotTo(HaveOccurred())
86+
87+
// Push a test image using the real loopback address
88+
realRef := fmt.Sprintf("%s/test/v3bundle:v1", registryAddr)
89+
img := mutate.MediaType(empty.Image, types.OCIManifestSchema1)
90+
err = crane.Push(img, realRef)
91+
g.Expect(err).NotTo(HaveOccurred())
92+
93+
// Sign with v3 bundle format using the real loopback address
94+
// (the bundle is stored by digest, so it's discoverable from any hostname)
95+
pf := func(_ bool) ([]byte, error) { return []byte(""), nil }
96+
ko := coptions.KeyOpts{
97+
KeyRef: keyPath,
98+
PassFunc: pf,
99+
NewBundleFormat: true,
100+
}
101+
ro := &coptions.RootOptions{Timeout: 30 * time.Second}
102+
err = sign.SignCmd(ctx, ro, ko, coptions.SignOptions{
103+
Upload: true,
104+
SkipConfirmation: true,
105+
TlogUpload: false,
106+
NewBundleFormat: true,
107+
Registry: coptions.RegistryOptions{AllowInsecure: true, AllowHTTPRegistry: true},
108+
}, []string{realRef})
109+
g.Expect(err).NotTo(HaveOccurred())
110+
111+
// Parse reference with name.Insecure (as source-controller does for spec.insecure=true)
112+
ref, err := name.ParseReference(fmt.Sprintf("%s/test/v3bundle:v1", fakeAddr), name.Insecure)
113+
g.Expect(err).NotTo(HaveOccurred())
114+
115+
// Verify using the CosignVerifier with the custom transport
116+
vf := NewCosignVerifierFactory()
117+
verifier, err := vf.NewCosignVerifier(ctx,
118+
WithPublicKey(keys.PublicBytes),
119+
WithRemoteOptions(remote.WithTransport(transport)),
120+
WithInsecure(true),
121+
)
122+
g.Expect(err).NotTo(HaveOccurred())
123+
124+
result, err := verifier.Verify(ctx, ref)
125+
g.Expect(err).NotTo(HaveOccurred(), "v3 bundle verification should succeed on insecure registry with non-loopback hostname")
126+
g.Expect(result).To(Equal(soci.VerificationResultSuccess))
127+
}

0 commit comments

Comments
 (0)