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 27, 2022
1 parent 8bc36bc commit d5854dc
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 17 deletions.
32 changes: 22 additions & 10 deletions controllers/ocirepository_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

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

Expand All @@ -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(
Expand All @@ -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),
Expand Down Expand Up @@ -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 {
Expand All @@ -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),
Expand Down Expand Up @@ -585,15 +597,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
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 d5854dc

Please sign in to comment.