Skip to content

Commit 2a2b525

Browse files
authored
Merge pull request #916 from souleb/fix-915
Accept a slice of remote.Option for cosign verification
2 parents 95cbf40 + f51c98e commit 2a2b525

File tree

4 files changed

+211
-41
lines changed

4 files changed

+211
-41
lines changed

controllers/ocirepository_controller.go

Lines changed: 73 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,8 @@ func (r *OCIRepositoryReconciler) reconcile(ctx context.Context, obj *sourcev1.O
299299
// reconcileSource fetches the upstream OCI artifact metadata and content.
300300
// If this fails, it records v1beta2.FetchFailedCondition=True on the object and returns early.
301301
func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sourcev1.OCIRepository, metadata *sourcev1.Artifact, dir string) (sreconcile.Result, error) {
302+
var auth authn.Authenticator
303+
302304
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
303305
defer cancel()
304306

@@ -310,8 +312,6 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
310312
conditions.Delete(obj, sourcev1.SourceVerifiedCondition)
311313
}
312314

313-
options := r.craneOptions(ctxTimeout, obj.Spec.Insecure)
314-
315315
// Generate the registry credential keychain either from static credentials or using cloud OIDC
316316
keychain, err := r.keychain(ctx, obj)
317317
if err != nil {
@@ -322,10 +322,10 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
322322
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
323323
return sreconcile.ResultEmpty, e
324324
}
325-
options = append(options, crane.WithAuthFromKeychain(keychain))
326325

327326
if _, ok := keychain.(soci.Anonymous); obj.Spec.Provider != sourcev1.GenericOCIProvider && ok {
328-
auth, authErr := oidcAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider)
327+
var authErr error
328+
auth, authErr = oidcAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider)
329329
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
330330
e := serror.NewGeneric(
331331
fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr),
@@ -334,9 +334,6 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
334334
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
335335
return sreconcile.ResultEmpty, e
336336
}
337-
if auth != nil {
338-
options = append(options, crane.WithAuth(auth))
339-
}
340337
}
341338

342339
// Generate the transport for remote operations
@@ -349,12 +346,11 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
349346
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
350347
return sreconcile.ResultEmpty, e
351348
}
352-
if transport != nil {
353-
options = append(options, crane.WithTransport(transport))
354-
}
349+
350+
opts := makeRemoteOptions(ctx, obj, transport, keychain, auth)
355351

356352
// Determine which artifact revision to pull
357-
url, err := r.getArtifactURL(obj, options)
353+
url, err := r.getArtifactURL(obj, opts.craneOpts)
358354
if err != nil {
359355
if _, ok := err.(invalidOCIURLError); ok {
360356
e := serror.NewStalling(
@@ -372,7 +368,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
372368
}
373369

374370
// Get the upstream revision from the artifact digest
375-
revision, err := r.getRevision(url, options)
371+
revision, err := r.getRevision(url, opts.craneOpts)
376372
if err != nil {
377373
e := serror.NewGeneric(
378374
fmt.Errorf("failed to determine artifact digest: %w", err),
@@ -403,7 +399,18 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
403399
} else if !obj.GetArtifact().HasRevision(revision) ||
404400
conditions.GetObservedGeneration(obj, sourcev1.SourceVerifiedCondition) != obj.Generation ||
405401
conditions.IsFalse(obj, sourcev1.SourceVerifiedCondition) {
406-
err := r.verifySignature(ctx, obj, url, keychain)
402+
403+
// Insecure is not supported for verification
404+
if obj.Spec.Insecure {
405+
e := serror.NewGeneric(
406+
fmt.Errorf("cosign does not support insecure registries"),
407+
sourcev1.VerificationError,
408+
)
409+
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error())
410+
return sreconcile.ResultEmpty, e
411+
}
412+
413+
err := r.verifySignature(ctx, obj, url, opts.verifyOpts...)
407414
if err != nil {
408415
provider := obj.Spec.Verify.Provider
409416
if obj.Spec.Verify.SecretRef == nil {
@@ -429,7 +436,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
429436
}
430437

431438
// Pull artifact from the remote container registry
432-
img, err := crane.Pull(url, options...)
439+
img, err := crane.Pull(url, opts.craneOpts...)
433440
if err != nil {
434441
e := serror.NewGeneric(
435442
fmt.Errorf("failed to pull artifact from '%s': %w", obj.Spec.URL, err),
@@ -589,15 +596,15 @@ func (r *OCIRepositoryReconciler) digestFromRevision(revision string) string {
589596

590597
// verifySignature verifies the authenticity of the given image reference url. First, it tries using a key
591598
// if a secret with a valid public key is provided. If not, it falls back to a keyless approach for verification.
592-
func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *sourcev1.OCIRepository, url string, keychain authn.Keychain) error {
599+
func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *sourcev1.OCIRepository, url string, opt ...remote.Option) error {
593600
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
594601
defer cancel()
595602

596603
provider := obj.Spec.Verify.Provider
597604
switch provider {
598605
case "cosign":
599606
defaultCosignOciOpts := []soci.Options{
600-
soci.WithAuthnKeychain(keychain),
607+
soci.WithRemoteOptions(opt...),
601608
}
602609

603610
ref, err := name.ParseReference(url)
@@ -857,21 +864,6 @@ func oidcAuth(ctx context.Context, url, provider string) (authn.Authenticator, e
857864
return login.NewManager().Login(ctx, u, ref, opts)
858865
}
859866

860-
// craneOptions sets the auth headers, timeout and user agent
861-
// for all operations against remote container registries.
862-
func (r *OCIRepositoryReconciler) craneOptions(ctx context.Context, insecure bool) []crane.Option {
863-
options := []crane.Option{
864-
crane.WithContext(ctx),
865-
crane.WithUserAgent(oci.UserAgent),
866-
}
867-
868-
if insecure {
869-
options = append(options, crane.Insecure)
870-
}
871-
872-
return options
873-
}
874-
875867
// reconcileStorage ensures the current state of the storage matches the
876868
// desired and previously observed state.
877869
//
@@ -1166,3 +1158,53 @@ func (r *OCIRepositoryReconciler) calculateContentConfigChecksum(obj *sourcev1.O
11661158

11671159
return fmt.Sprintf("sha256:%x", sha256.Sum256(c))
11681160
}
1161+
1162+
// craneOptions sets the auth headers, timeout and user agent
1163+
// for all operations against remote container registries.
1164+
func craneOptions(ctx context.Context, insecure bool) []crane.Option {
1165+
options := []crane.Option{
1166+
crane.WithContext(ctx),
1167+
crane.WithUserAgent(oci.UserAgent),
1168+
}
1169+
1170+
if insecure {
1171+
options = append(options, crane.Insecure)
1172+
}
1173+
1174+
return options
1175+
}
1176+
1177+
// makeRemoteOptions returns a remoteOptions struct with the authentication and transport options set.
1178+
// The returned struct can be used to interact with a remote registry using go-containerregistry based libraries.
1179+
func makeRemoteOptions(ctxTimeout context.Context, obj *sourcev1.OCIRepository, transport http.RoundTripper,
1180+
keychain authn.Keychain, auth authn.Authenticator) remoteOptions {
1181+
o := remoteOptions{
1182+
craneOpts: craneOptions(ctxTimeout, obj.Spec.Insecure),
1183+
verifyOpts: []remote.Option{},
1184+
}
1185+
1186+
if transport != nil {
1187+
o.craneOpts = append(o.craneOpts, crane.WithTransport(transport))
1188+
o.verifyOpts = append(o.verifyOpts, remote.WithTransport(transport))
1189+
}
1190+
1191+
if auth != nil {
1192+
// auth take precedence over keychain here as we expect the caller to set
1193+
// the auth only if it is required.
1194+
o.verifyOpts = append(o.verifyOpts, remote.WithAuth(auth))
1195+
o.craneOpts = append(o.craneOpts, crane.WithAuth(auth))
1196+
return o
1197+
}
1198+
1199+
o.verifyOpts = append(o.verifyOpts, remote.WithAuthFromKeychain(keychain))
1200+
o.craneOpts = append(o.craneOpts, crane.WithAuthFromKeychain(keychain))
1201+
1202+
return o
1203+
}
1204+
1205+
// remoteOptions contains the options to interact with a remote registry.
1206+
// It can be used to pass options to go-containerregistry based libraries.
1207+
type remoteOptions struct {
1208+
craneOpts []crane.Option
1209+
verifyOpts []remote.Option
1210+
}

controllers/ocirepository_controller_test.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,7 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) {
681681
Storage: testStorage,
682682
}
683683

684-
opts := r.craneOptions(ctx, true)
684+
opts := craneOptions(ctx, true)
685685
opts = append(opts, crane.WithAuthFromKeychain(authn.DefaultKeychain))
686686
repoURL, err := r.getArtifactURL(obj, opts)
687687
g.Expect(err).To(BeNil())
@@ -1036,6 +1036,7 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) {
10361036
tests := []struct {
10371037
name string
10381038
reference *sourcev1.OCIRepositoryRef
1039+
insecure bool
10391040
digest string
10401041
want sreconcile.Result
10411042
wantErr bool
@@ -1132,6 +1133,22 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) {
11321133
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, "Verified", "verified"),
11331134
},
11341135
},
1136+
{
1137+
name: "insecure registries are not supported",
1138+
reference: &sourcev1.OCIRepositoryRef{
1139+
Tag: "6.1.4",
1140+
},
1141+
digest: img4.digest.Hex,
1142+
shouldSign: true,
1143+
insecure: true,
1144+
wantErr: true,
1145+
want: sreconcile.ResultEmpty,
1146+
assertConditions: []metav1.Condition{
1147+
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new revision '<digest>' for '<url>'"),
1148+
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new revision '<digest>' for '<url>'"),
1149+
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "cosign does not support insecure registries"),
1150+
},
1151+
},
11351152
}
11361153

11371154
builder := fakeclient.NewClientBuilder().WithScheme(testEnv.GetScheme())
@@ -1181,6 +1198,10 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) {
11811198
},
11821199
}
11831200

1201+
if tt.insecure {
1202+
obj.Spec.Insecure = true
1203+
}
1204+
11841205
if !tt.keyless {
11851206
obj.Spec.Verify.SecretRef = &meta.LocalObjectReference{Name: "cosign-key"}
11861207
}
@@ -1194,7 +1215,7 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) {
11941215
g.Expect(err).ToNot(HaveOccurred())
11951216
}
11961217

1197-
opts := r.craneOptions(ctx, true)
1218+
opts := craneOptions(ctx, true)
11981219
opts = append(opts, crane.WithAuthFromKeychain(keychain))
11991220
artifactURL, err := r.getArtifactURL(obj, opts)
12001221
g.Expect(err).ToNot(HaveOccurred())
@@ -1677,7 +1698,7 @@ func TestOCIRepository_getArtifactURL(t *testing.T) {
16771698
obj.Spec.Reference = tt.reference
16781699
}
16791700

1680-
opts := r.craneOptions(ctx, true)
1701+
opts := craneOptions(ctx, true)
16811702
opts = append(opts, crane.WithAuthFromKeychain(authn.DefaultKeychain))
16821703
got, err := r.getArtifactURL(obj, opts)
16831704
if tt.wantErr {

internal/oci/verifier.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
"context"
2121
"crypto"
2222
"fmt"
23-
"github.com/google/go-containerregistry/pkg/authn"
23+
2424
"github.com/google/go-containerregistry/pkg/v1/remote"
2525
"github.com/sigstore/cosign/cmd/cosign/cli/fulcio"
2626
"github.com/sigstore/cosign/cmd/cosign/cli/rekor"
@@ -37,7 +37,7 @@ import (
3737
// options is a struct that holds options for verifier.
3838
type options struct {
3939
PublicKey []byte
40-
Keychain authn.Keychain
40+
ROpt []remote.Option
4141
}
4242

4343
// Options is a function that configures the options applied to a Verifier.
@@ -50,9 +50,11 @@ func WithPublicKey(publicKey []byte) Options {
5050
}
5151
}
5252

53-
func WithAuthnKeychain(keychain authn.Keychain) Options {
54-
return func(opts *options) {
55-
opts.Keychain = keychain
53+
// WithRemoteOptions is a functional option for overriding the default
54+
// remote options used by the verifier.
55+
func WithRemoteOptions(opts ...remote.Option) Options {
56+
return func(o *options) {
57+
o.ROpt = opts
5658
}
5759
}
5860

@@ -76,8 +78,8 @@ func NewVerifier(ctx context.Context, opts ...Options) (*Verifier, error) {
7678
return nil, err
7779
}
7880

79-
if o.Keychain != nil {
80-
co = append(co, ociremote.WithRemoteOptions(remote.WithAuthFromKeychain(o.Keychain)))
81+
if o.ROpt != nil {
82+
co = append(co, ociremote.WithRemoteOptions(o.ROpt...))
8183
}
8284

8385
checkOpts.RegistryClientOpts = co

0 commit comments

Comments
 (0)