Skip to content

Commit

Permalink
Accept a slice of remote.Option for cosign verification
Browse files Browse the repository at this point in the history
If implemented this enable passing a keychain, an authenticator and a
custom transport as remote.Option to the verifier. It enables contextual
login, self-signed certificates and insecure registries.

Signed-off-by: Soule BA <soule@weave.works>
  • Loading branch information
souleb committed Sep 28, 2022
1 parent 8bc36bc commit 64c0e70
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 23 deletions.
71 changes: 55 additions & 16 deletions controllers/ocirepository_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ 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 auth authn.Authenticator

ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
defer cancel()

Expand All @@ -308,8 +310,6 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
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
keychain, err := r.keychain(ctx, obj)
if err != nil {
Expand All @@ -320,10 +320,10 @@ 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))

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) {
e := serror.NewGeneric(
fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr),
Expand All @@ -332,9 +332,6 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e
}
if auth != nil {
options = append(options, crane.WithAuth(auth))
}
}

// Generate the transport for remote operations
Expand All @@ -347,12 +344,11 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
return sreconcile.ResultEmpty, e
}
if transport != nil {
options = append(options, crane.WithTransport(transport))
}

opts := r.makeOptions(ctx, obj, withTransport(transport), withKeychainOrAuth(keychain, auth))

// Determine which artifact revision to pull
url, err := r.getArtifactURL(obj, options)
url, err := r.getArtifactURL(obj, opts.craneOpts)
if err != nil {
if _, ok := err.(invalidOCIURLError); ok {
e := serror.NewStalling(
Expand All @@ -370,7 +366,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, opts.craneOpts)
if err != nil {
e := serror.NewGeneric(
fmt.Errorf("failed to determine artifact digest: %w", err),
Expand Down Expand Up @@ -401,7 +397,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, opts.verifyOpts...)
if err != nil {
provider := obj.Spec.Verify.Provider
if obj.Spec.Verify.SecretRef == nil {
Expand All @@ -425,7 +421,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, opts.craneOpts...)
if err != nil {
e := serror.NewGeneric(
fmt.Errorf("failed to pull artifact from '%s': %w", obj.Spec.URL, err),
Expand Down Expand Up @@ -585,15 +581,15 @@ 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()

provider := obj.Spec.Verify.Provider
switch provider {
case "cosign":
defaultCosignOciOpts := []soci.Options{
soci.WithAuthnKeychain(keychain),
soci.WithRemoteOptions(opt...),
}

ref, err := name.ParseReference(url)
Expand Down Expand Up @@ -1125,3 +1121,46 @@ func (r *OCIRepositoryReconciler) notify(ctx context.Context, oldObj, newObj *so
}
}
}

func (r *OCIRepositoryReconciler) makeOptions(ctxTimeout context.Context, obj *sourcev1.OCIRepository, opts ...Option) remoteOptions {
o := remoteOptions{
craneOpts: r.craneOptions(ctxTimeout, obj.Spec.Insecure),
verifyOpts: []remote.Option{},
}

for _, opt := range opts {
opt(&o)
}

return o
}

type remoteOptions struct {
craneOpts []crane.Option
verifyOpts []remote.Option
}

type Option func(*remoteOptions)

func withKeychainOrAuth(keychain authn.Keychain, auth authn.Authenticator) Option {
return func(o *remoteOptions) {
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))
} else {
o.verifyOpts = append(o.verifyOpts, remote.WithAuthFromKeychain(keychain))
o.craneOpts = append(o.craneOpts, crane.WithAuthFromKeychain(keychain))
}
}
}

func withTransport(transport http.RoundTripper) Option {
return func(o *remoteOptions) {
if transport != nil {
o.craneOpts = append(o.craneOpts, crane.WithTransport(transport))
o.verifyOpts = append(o.verifyOpts, remote.WithTransport(transport))
}
}
}
16 changes: 9 additions & 7 deletions internal/oci/verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand All @@ -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
}
}

Expand All @@ -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
Expand Down
105 changes: 105 additions & 0 deletions internal/oci/verifier_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
})
}
}

0 comments on commit 64c0e70

Please sign in to comment.