Merge pull request #916 from souleb/fix-915

Accept a slice of remote.Option for cosign verification
This commit is contained in:
Stefan Prodan 2022-09-29 16:03:21 +03:00 committed by GitHub
commit 2a2b525dbd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 211 additions and 41 deletions

View File

@ -299,6 +299,8 @@ func (r *OCIRepositoryReconciler) reconcile(ctx context.Context, obj *sourcev1.O
// reconcileSource fetches the upstream OCI artifact metadata and content. // reconcileSource fetches the upstream OCI artifact metadata and content.
// If this fails, it records v1beta2.FetchFailedCondition=True on the object and returns early. // 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) { func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sourcev1.OCIRepository, metadata *sourcev1.Artifact, dir string) (sreconcile.Result, error) {
var auth authn.Authenticator
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration) ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
defer cancel() defer cancel()
@ -310,8 +312,6 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
conditions.Delete(obj, sourcev1.SourceVerifiedCondition) conditions.Delete(obj, sourcev1.SourceVerifiedCondition)
} }
options := r.craneOptions(ctxTimeout, obj.Spec.Insecure)
// Generate the registry credential keychain either from static credentials or using cloud OIDC // Generate the registry credential keychain either from static credentials or using cloud OIDC
keychain, err := r.keychain(ctx, obj) keychain, err := r.keychain(ctx, obj)
if err != nil { if err != nil {
@ -322,10 +322,10 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e return sreconcile.ResultEmpty, e
} }
options = append(options, crane.WithAuthFromKeychain(keychain))
if _, ok := keychain.(soci.Anonymous); obj.Spec.Provider != sourcev1.GenericOCIProvider && ok { if _, ok := keychain.(soci.Anonymous); obj.Spec.Provider != sourcev1.GenericOCIProvider && ok {
auth, authErr := oidcAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider) var authErr error
auth, authErr = oidcAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider)
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) { if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
e := serror.NewGeneric( e := serror.NewGeneric(
fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr), 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
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e return sreconcile.ResultEmpty, e
} }
if auth != nil {
options = append(options, crane.WithAuth(auth))
}
} }
// Generate the transport for remote operations // Generate the transport for remote operations
@ -349,12 +346,11 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error()) conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e return sreconcile.ResultEmpty, e
} }
if transport != nil {
options = append(options, crane.WithTransport(transport)) opts := makeRemoteOptions(ctx, obj, transport, keychain, auth)
}
// Determine which artifact revision to pull // Determine which artifact revision to pull
url, err := r.getArtifactURL(obj, options) url, err := r.getArtifactURL(obj, opts.craneOpts)
if err != nil { if err != nil {
if _, ok := err.(invalidOCIURLError); ok { if _, ok := err.(invalidOCIURLError); ok {
e := serror.NewStalling( e := serror.NewStalling(
@ -372,7 +368,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
} }
// Get the upstream revision from the artifact digest // Get the upstream revision from the artifact digest
revision, err := r.getRevision(url, options) revision, err := r.getRevision(url, opts.craneOpts)
if err != nil { if err != nil {
e := serror.NewGeneric( e := serror.NewGeneric(
fmt.Errorf("failed to determine artifact digest: %w", err), fmt.Errorf("failed to determine artifact digest: %w", err),
@ -403,7 +399,18 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
} else if !obj.GetArtifact().HasRevision(revision) || } else if !obj.GetArtifact().HasRevision(revision) ||
conditions.GetObservedGeneration(obj, sourcev1.SourceVerifiedCondition) != obj.Generation || conditions.GetObservedGeneration(obj, sourcev1.SourceVerifiedCondition) != obj.Generation ||
conditions.IsFalse(obj, sourcev1.SourceVerifiedCondition) { conditions.IsFalse(obj, sourcev1.SourceVerifiedCondition) {
err := r.verifySignature(ctx, obj, url, keychain)
// Insecure is not supported for verification
if obj.Spec.Insecure {
e := serror.NewGeneric(
fmt.Errorf("cosign does not support insecure registries"),
sourcev1.VerificationError,
)
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e
}
err := r.verifySignature(ctx, obj, url, opts.verifyOpts...)
if err != nil { if err != nil {
provider := obj.Spec.Verify.Provider provider := obj.Spec.Verify.Provider
if obj.Spec.Verify.SecretRef == nil { if obj.Spec.Verify.SecretRef == nil {
@ -429,7 +436,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
} }
// Pull artifact from the remote container registry // Pull artifact from the remote container registry
img, err := crane.Pull(url, options...) img, err := crane.Pull(url, opts.craneOpts...)
if err != nil { if err != nil {
e := serror.NewGeneric( e := serror.NewGeneric(
fmt.Errorf("failed to pull artifact from '%s': %w", obj.Spec.URL, err), fmt.Errorf("failed to pull artifact from '%s': %w", obj.Spec.URL, err),
@ -589,7 +596,7 @@ func (r *OCIRepositoryReconciler) digestFromRevision(revision string) string {
// verifySignature verifies the authenticity of the given image reference url. First, it tries using a key // 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. // 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) ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
defer cancel() defer cancel()
@ -597,7 +604,7 @@ func (r *OCIRepositoryReconciler) verifySignature(ctx context.Context, obj *sour
switch provider { switch provider {
case "cosign": case "cosign":
defaultCosignOciOpts := []soci.Options{ defaultCosignOciOpts := []soci.Options{
soci.WithAuthnKeychain(keychain), soci.WithRemoteOptions(opt...),
} }
ref, err := name.ParseReference(url) ref, err := name.ParseReference(url)
@ -857,21 +864,6 @@ func oidcAuth(ctx context.Context, url, provider string) (authn.Authenticator, e
return login.NewManager().Login(ctx, u, ref, opts) return login.NewManager().Login(ctx, u, ref, opts)
} }
// craneOptions sets the auth headers, timeout and user agent
// for all operations against remote container registries.
func (r *OCIRepositoryReconciler) craneOptions(ctx context.Context, insecure bool) []crane.Option {
options := []crane.Option{
crane.WithContext(ctx),
crane.WithUserAgent(oci.UserAgent),
}
if insecure {
options = append(options, crane.Insecure)
}
return options
}
// reconcileStorage ensures the current state of the storage matches the // reconcileStorage ensures the current state of the storage matches the
// desired and previously observed state. // desired and previously observed state.
// //
@ -1166,3 +1158,53 @@ func (r *OCIRepositoryReconciler) calculateContentConfigChecksum(obj *sourcev1.O
return fmt.Sprintf("sha256:%x", sha256.Sum256(c)) return fmt.Sprintf("sha256:%x", sha256.Sum256(c))
} }
// craneOptions sets the auth headers, timeout and user agent
// for all operations against remote container registries.
func craneOptions(ctx context.Context, insecure bool) []crane.Option {
options := []crane.Option{
crane.WithContext(ctx),
crane.WithUserAgent(oci.UserAgent),
}
if insecure {
options = append(options, crane.Insecure)
}
return options
}
// makeRemoteOptions returns a remoteOptions struct with the authentication and transport options set.
// The returned struct can be used to interact with a remote registry using go-containerregistry based libraries.
func makeRemoteOptions(ctxTimeout context.Context, obj *sourcev1.OCIRepository, transport http.RoundTripper,
keychain authn.Keychain, auth authn.Authenticator) remoteOptions {
o := remoteOptions{
craneOpts: craneOptions(ctxTimeout, obj.Spec.Insecure),
verifyOpts: []remote.Option{},
}
if transport != nil {
o.craneOpts = append(o.craneOpts, crane.WithTransport(transport))
o.verifyOpts = append(o.verifyOpts, remote.WithTransport(transport))
}
if auth != nil {
// auth take precedence over keychain here as we expect the caller to set
// the auth only if it is required.
o.verifyOpts = append(o.verifyOpts, remote.WithAuth(auth))
o.craneOpts = append(o.craneOpts, crane.WithAuth(auth))
return o
}
o.verifyOpts = append(o.verifyOpts, remote.WithAuthFromKeychain(keychain))
o.craneOpts = append(o.craneOpts, crane.WithAuthFromKeychain(keychain))
return o
}
// remoteOptions contains the options to interact with a remote registry.
// It can be used to pass options to go-containerregistry based libraries.
type remoteOptions struct {
craneOpts []crane.Option
verifyOpts []remote.Option
}

View File

@ -681,7 +681,7 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) {
Storage: testStorage, Storage: testStorage,
} }
opts := r.craneOptions(ctx, true) opts := craneOptions(ctx, true)
opts = append(opts, crane.WithAuthFromKeychain(authn.DefaultKeychain)) opts = append(opts, crane.WithAuthFromKeychain(authn.DefaultKeychain))
repoURL, err := r.getArtifactURL(obj, opts) repoURL, err := r.getArtifactURL(obj, opts)
g.Expect(err).To(BeNil()) g.Expect(err).To(BeNil())
@ -1036,6 +1036,7 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
reference *sourcev1.OCIRepositoryRef reference *sourcev1.OCIRepositoryRef
insecure bool
digest string digest string
want sreconcile.Result want sreconcile.Result
wantErr bool wantErr bool
@ -1132,6 +1133,22 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) {
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, "Verified", "verified"), *conditions.TrueCondition(sourcev1.SourceVerifiedCondition, "Verified", "verified"),
}, },
}, },
{
name: "insecure registries are not supported",
reference: &sourcev1.OCIRepositoryRef{
Tag: "6.1.4",
},
digest: img4.digest.Hex,
shouldSign: true,
insecure: true,
wantErr: true,
want: sreconcile.ResultEmpty,
assertConditions: []metav1.Condition{
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new revision '<digest>' for '<url>'"),
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new revision '<digest>' for '<url>'"),
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "cosign does not support insecure registries"),
},
},
} }
builder := fakeclient.NewClientBuilder().WithScheme(testEnv.GetScheme()) builder := fakeclient.NewClientBuilder().WithScheme(testEnv.GetScheme())
@ -1181,6 +1198,10 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) {
}, },
} }
if tt.insecure {
obj.Spec.Insecure = true
}
if !tt.keyless { if !tt.keyless {
obj.Spec.Verify.SecretRef = &meta.LocalObjectReference{Name: "cosign-key"} obj.Spec.Verify.SecretRef = &meta.LocalObjectReference{Name: "cosign-key"}
} }
@ -1194,7 +1215,7 @@ func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) {
g.Expect(err).ToNot(HaveOccurred()) g.Expect(err).ToNot(HaveOccurred())
} }
opts := r.craneOptions(ctx, true) opts := craneOptions(ctx, true)
opts = append(opts, crane.WithAuthFromKeychain(keychain)) opts = append(opts, crane.WithAuthFromKeychain(keychain))
artifactURL, err := r.getArtifactURL(obj, opts) artifactURL, err := r.getArtifactURL(obj, opts)
g.Expect(err).ToNot(HaveOccurred()) g.Expect(err).ToNot(HaveOccurred())
@ -1677,7 +1698,7 @@ func TestOCIRepository_getArtifactURL(t *testing.T) {
obj.Spec.Reference = tt.reference obj.Spec.Reference = tt.reference
} }
opts := r.craneOptions(ctx, true) opts := craneOptions(ctx, true)
opts = append(opts, crane.WithAuthFromKeychain(authn.DefaultKeychain)) opts = append(opts, crane.WithAuthFromKeychain(authn.DefaultKeychain))
got, err := r.getArtifactURL(obj, opts) got, err := r.getArtifactURL(obj, opts)
if tt.wantErr { if tt.wantErr {

View File

@ -20,7 +20,7 @@ import (
"context" "context"
"crypto" "crypto"
"fmt" "fmt"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/sigstore/cosign/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/cmd/cosign/cli/fulcio"
"github.com/sigstore/cosign/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/cmd/cosign/cli/rekor"
@ -37,7 +37,7 @@ import (
// options is a struct that holds options for verifier. // options is a struct that holds options for verifier.
type options struct { type options struct {
PublicKey []byte PublicKey []byte
Keychain authn.Keychain ROpt []remote.Option
} }
// Options is a function that configures the options applied to a Verifier. // 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 { // WithRemoteOptions is a functional option for overriding the default
return func(opts *options) { // remote options used by the verifier.
opts.Keychain = keychain 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 return nil, err
} }
if o.Keychain != nil { if o.ROpt != nil {
co = append(co, ociremote.WithRemoteOptions(remote.WithAuthFromKeychain(o.Keychain))) co = append(co, ociremote.WithRemoteOptions(o.ROpt...))
} }
checkOpts.RegistryClientOpts = co checkOpts.RegistryClientOpts = co

View File

@ -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)
}
}
})
}
}