diff --git a/controllers/ocirepository_controller.go b/controllers/ocirepository_controller.go index 95ec1ec6f..c784ec389 100644 --- a/controllers/ocirepository_controller.go +++ b/controllers/ocirepository_controller.go @@ -297,6 +297,7 @@ func (r *OCIRepositoryReconciler) reconcile(ctx context.Context, obj *sourcev1.O // reconcileSource fetches the upstream OCI artifact metadata and content. // If this fails, it records v1beta2.FetchFailedCondition=True on the object and returns early. func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sourcev1.OCIRepository, metadata *sourcev1.Artifact, dir string) (sreconcile.Result, error) { + var verifyOpts []remote.Option ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration) defer cancel() @@ -308,7 +309,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour conditions.Delete(obj, sourcev1.SourceVerifiedCondition) } - options := r.craneOptions(ctxTimeout, obj.Spec.Insecure) + craneOpts := r.craneOptions(ctxTimeout, obj.Spec.Insecure) // Generate the registry credential keychain either from static credentials or using cloud OIDC keychain, err := r.keychain(ctx, obj) @@ -320,7 +321,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) return sreconcile.ResultEmpty, e } - options = append(options, crane.WithAuthFromKeychain(keychain)) + craneOpts = append(craneOpts, crane.WithAuthFromKeychain(keychain)) if _, ok := keychain.(soci.Anonymous); obj.Spec.Provider != sourcev1.GenericOCIProvider && ok { auth, authErr := oidcAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider) @@ -333,7 +334,11 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour return sreconcile.ResultEmpty, e } if auth != nil { - options = append(options, crane.WithAuth(auth)) + craneOpts = append(craneOpts, crane.WithAuth(auth)) + verifyOpts = append(verifyOpts, remote.WithAuth(auth)) + } else { + // If no auth is configured at all, use anonymous access + verifyOpts = append(verifyOpts, remote.WithAuthFromKeychain(keychain)) } } @@ -348,11 +353,18 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour return sreconcile.ResultEmpty, e } if transport != nil { - options = append(options, crane.WithTransport(transport)) + craneOpts = append(craneOpts, crane.WithTransport(transport)) + verifyOpts = append(verifyOpts, remote.WithTransport(transport)) + } else if obj.Spec.Insecure { + // If no transport is generated, but the repository is marked as insecure, + // use the default remote transport. + transport := remote.DefaultTransport.Clone() + transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + verifyOpts = append(verifyOpts, remote.WithTransport(transport)) } // Determine which artifact revision to pull - url, err := r.getArtifactURL(obj, options) + url, err := r.getArtifactURL(obj, craneOpts) if err != nil { if _, ok := err.(invalidOCIURLError); ok { e := serror.NewStalling( @@ -370,7 +382,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour } // Get the upstream revision from the artifact digest - revision, err := r.getRevision(url, options) + revision, err := r.getRevision(url, craneOpts) if err != nil { e := serror.NewGeneric( fmt.Errorf("failed to determine artifact digest: %w", err), @@ -401,7 +413,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour } else if !obj.GetArtifact().HasRevision(revision) || conditions.GetObservedGeneration(obj, sourcev1.SourceVerifiedCondition) != obj.Generation || conditions.IsFalse(obj, sourcev1.SourceVerifiedCondition) { - err := r.verifySignature(ctx, obj, url, keychain) + err := r.verifySignature(ctx, obj, url, verifyOpts...) if err != nil { provider := obj.Spec.Verify.Provider if obj.Spec.Verify.SecretRef == nil { @@ -425,7 +437,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour } // Pull artifact from the remote container registry - img, err := crane.Pull(url, options...) + img, err := crane.Pull(url, craneOpts...) if err != nil { e := serror.NewGeneric( fmt.Errorf("failed to pull artifact from '%s': %w", obj.Spec.URL, err), @@ -585,7 +597,7 @@ func (r *OCIRepositoryReconciler) digestFromRevision(revision string) string { // verifySignature verifies the authenticity of the given image reference url. First, it tries using a key // if a secret with a valid public key is provided. If not, it falls back to a keyless approach for verification. -func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *sourcev1.OCIRepository, url string, keychain authn.Keychain) error { +func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *sourcev1.OCIRepository, url string, opt ...remote.Option) error { ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration) defer cancel() @@ -593,7 +605,7 @@ func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *sour switch provider { case "cosign": defaultCosignOciOpts := []soci.Options{ - soci.WithAuthnKeychain(keychain), + soci.WithRemoteOptions(opt...), } ref, err := name.ParseReference(url) diff --git a/internal/oci/verifier.go b/internal/oci/verifier.go index 17a5345db..b8d9c5d49 100644 --- a/internal/oci/verifier.go +++ b/internal/oci/verifier.go @@ -20,7 +20,7 @@ import ( "context" "crypto" "fmt" - "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/cmd/cosign/cli/rekor" @@ -37,7 +37,7 @@ import ( // options is a struct that holds options for verifier. type options struct { PublicKey []byte - Keychain authn.Keychain + ROpt []remote.Option } // Options is a function that configures the options applied to a Verifier. @@ -50,9 +50,11 @@ func WithPublicKey(publicKey []byte) Options { } } -func WithAuthnKeychain(keychain authn.Keychain) Options { - return func(opts *options) { - opts.Keychain = keychain +// WithRemoteOptions is a functional option for overriding the default +// remote options used by the verifier. +func WithRemoteOptions(opts ...remote.Option) Options { + return func(o *options) { + o.ROpt = opts } } @@ -76,8 +78,8 @@ func NewVerifier(ctx context.Context, opts ...Options) (*Verifier, error) { return nil, err } - if o.Keychain != nil { - co = append(co, ociremote.WithRemoteOptions(remote.WithAuthFromKeychain(o.Keychain))) + if o.ROpt != nil { + co = append(co, ociremote.WithRemoteOptions(o.ROpt...)) } checkOpts.RegistryClientOpts = co diff --git a/internal/oci/verifier_test.go b/internal/oci/verifier_test.go new file mode 100644 index 000000000..8b3ae3865 --- /dev/null +++ b/internal/oci/verifier_test.go @@ -0,0 +1,105 @@ +/* +Copyright 2022 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 oci + +import ( + "net/http" + "reflect" + "testing" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +func TestOptions(t *testing.T) { + tests := []struct { + name string + opts []Options + want *options + }{{ + name: "no options", + want: &options{}, + }, { + name: "signature option", + opts: []Options{WithPublicKey([]byte("foo"))}, + want: &options{ + PublicKey: []byte("foo"), + ROpt: nil, + }, + }, { + name: "keychain option", + opts: []Options{WithRemoteOptions(remote.WithAuthFromKeychain(authn.DefaultKeychain))}, + want: &options{ + PublicKey: nil, + ROpt: []remote.Option{remote.WithAuthFromKeychain(authn.DefaultKeychain)}, + }, + }, { + name: "keychain and authenticator option", + opts: []Options{WithRemoteOptions( + remote.WithAuth(&authn.Basic{Username: "foo", Password: "bar"}), + remote.WithAuthFromKeychain(authn.DefaultKeychain), + )}, + want: &options{ + PublicKey: nil, + ROpt: []remote.Option{ + remote.WithAuth(&authn.Basic{Username: "foo", Password: "bar"}), + remote.WithAuthFromKeychain(authn.DefaultKeychain), + }, + }, + }, { + name: "keychain, authenticator and transport option", + opts: []Options{WithRemoteOptions( + remote.WithAuth(&authn.Basic{Username: "foo", Password: "bar"}), + remote.WithAuthFromKeychain(authn.DefaultKeychain), + remote.WithTransport(http.DefaultTransport), + )}, + want: &options{ + PublicKey: nil, + ROpt: []remote.Option{ + remote.WithAuth(&authn.Basic{Username: "foo", Password: "bar"}), + remote.WithAuthFromKeychain(authn.DefaultKeychain), + remote.WithTransport(http.DefaultTransport), + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + o := options{} + for _, opt := range test.opts { + opt(&o) + } + if !reflect.DeepEqual(o.PublicKey, test.want.PublicKey) { + t.Errorf("got %#v, want %#v", &o.PublicKey, test.want.PublicKey) + } + + if test.want.ROpt != nil { + if len(o.ROpt) != len(test.want.ROpt) { + t.Errorf("got %d remote options, want %d", len(o.ROpt), len(test.want.ROpt)) + } + return + } + + if test.want.ROpt == nil { + if len(o.ROpt) != 0 { + t.Errorf("got %d remote options, want %d", len(o.ROpt), 0) + } + } + }) + } +}