Skip to content

Commit 64c0e70

Browse files
committed
Accept a slice of remote.Option for cosign verification
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>
1 parent 8bc36bc commit 64c0e70

File tree

3 files changed

+169
-23
lines changed

3 files changed

+169
-23
lines changed

controllers/ocirepository_controller.go

+55-16
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,8 @@ func (r *OCIRepositoryReconciler) reconcile(ctx context.Context, obj *sourcev1.O
297297
// reconcileSource fetches the upstream OCI artifact metadata and content.
298298
// If this fails, it records v1beta2.FetchFailedCondition=True on the object and returns early.
299299
func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sourcev1.OCIRepository, metadata *sourcev1.Artifact, dir string) (sreconcile.Result, error) {
300+
var auth authn.Authenticator
301+
300302
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
301303
defer cancel()
302304

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

311-
options := r.craneOptions(ctxTimeout, obj.Spec.Insecure)
312-
313313
// Generate the registry credential keychain either from static credentials or using cloud OIDC
314314
keychain, err := r.keychain(ctx, obj)
315315
if err != nil {
@@ -320,10 +320,10 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
320320
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
321321
return sreconcile.ResultEmpty, e
322322
}
323-
options = append(options, crane.WithAuthFromKeychain(keychain))
324323

325324
if _, ok := keychain.(soci.Anonymous); obj.Spec.Provider != sourcev1.GenericOCIProvider && ok {
326-
auth, authErr := oidcAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider)
325+
var authErr error
326+
auth, authErr = oidcAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider)
327327
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
328328
e := serror.NewGeneric(
329329
fmt.Errorf("failed to get credential from %s: %w", obj.Spec.Provider, authErr),
@@ -332,9 +332,6 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
332332
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
333333
return sreconcile.ResultEmpty, e
334334
}
335-
if auth != nil {
336-
options = append(options, crane.WithAuth(auth))
337-
}
338335
}
339336

340337
// Generate the transport for remote operations
@@ -347,12 +344,11 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
347344
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, e.Err.Error())
348345
return sreconcile.ResultEmpty, e
349346
}
350-
if transport != nil {
351-
options = append(options, crane.WithTransport(transport))
352-
}
347+
348+
opts := r.makeOptions(ctx, obj, withTransport(transport), withKeychainOrAuth(keychain, auth))
353349

354350
// Determine which artifact revision to pull
355-
url, err := r.getArtifactURL(obj, options)
351+
url, err := r.getArtifactURL(obj, opts.craneOpts)
356352
if err != nil {
357353
if _, ok := err.(invalidOCIURLError); ok {
358354
e := serror.NewStalling(
@@ -370,7 +366,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
370366
}
371367

372368
// Get the upstream revision from the artifact digest
373-
revision, err := r.getRevision(url, options)
369+
revision, err := r.getRevision(url, opts.craneOpts)
374370
if err != nil {
375371
e := serror.NewGeneric(
376372
fmt.Errorf("failed to determine artifact digest: %w", err),
@@ -401,7 +397,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
401397
} else if !obj.GetArtifact().HasRevision(revision) ||
402398
conditions.GetObservedGeneration(obj, sourcev1.SourceVerifiedCondition) != obj.Generation ||
403399
conditions.IsFalse(obj, sourcev1.SourceVerifiedCondition) {
404-
err := r.verifySignature(ctx, obj, url, keychain)
400+
err := r.verifySignature(ctx, obj, url, opts.verifyOpts...)
405401
if err != nil {
406402
provider := obj.Spec.Verify.Provider
407403
if obj.Spec.Verify.SecretRef == nil {
@@ -425,7 +421,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
425421
}
426422

427423
// Pull artifact from the remote container registry
428-
img, err := crane.Pull(url, options...)
424+
img, err := crane.Pull(url, opts.craneOpts...)
429425
if err != nil {
430426
e := serror.NewGeneric(
431427
fmt.Errorf("failed to pull artifact from '%s': %w", obj.Spec.URL, err),
@@ -585,15 +581,15 @@ func (r *OCIRepositoryReconciler) digestFromRevision(revision string) string {
585581

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

592588
provider := obj.Spec.Verify.Provider
593589
switch provider {
594590
case "cosign":
595591
defaultCosignOciOpts := []soci.Options{
596-
soci.WithAuthnKeychain(keychain),
592+
soci.WithRemoteOptions(opt...),
597593
}
598594

599595
ref, err := name.ParseReference(url)
@@ -1125,3 +1121,46 @@ func (r *OCIRepositoryReconciler) notify(ctx context.Context, oldObj, newObj *so
11251121
}
11261122
}
11271123
}
1124+
1125+
func (r *OCIRepositoryReconciler) makeOptions(ctxTimeout context.Context, obj *sourcev1.OCIRepository, opts ...Option) remoteOptions {
1126+
o := remoteOptions{
1127+
craneOpts: r.craneOptions(ctxTimeout, obj.Spec.Insecure),
1128+
verifyOpts: []remote.Option{},
1129+
}
1130+
1131+
for _, opt := range opts {
1132+
opt(&o)
1133+
}
1134+
1135+
return o
1136+
}
1137+
1138+
type remoteOptions struct {
1139+
craneOpts []crane.Option
1140+
verifyOpts []remote.Option
1141+
}
1142+
1143+
type Option func(*remoteOptions)
1144+
1145+
func withKeychainOrAuth(keychain authn.Keychain, auth authn.Authenticator) Option {
1146+
return func(o *remoteOptions) {
1147+
if auth != nil {
1148+
// auth take precedence over keychain here as we expect the caller to set
1149+
// the auth only if it is required.
1150+
o.verifyOpts = append(o.verifyOpts, remote.WithAuth(auth))
1151+
o.craneOpts = append(o.craneOpts, crane.WithAuth(auth))
1152+
} else {
1153+
o.verifyOpts = append(o.verifyOpts, remote.WithAuthFromKeychain(keychain))
1154+
o.craneOpts = append(o.craneOpts, crane.WithAuthFromKeychain(keychain))
1155+
}
1156+
}
1157+
}
1158+
1159+
func withTransport(transport http.RoundTripper) Option {
1160+
return func(o *remoteOptions) {
1161+
if transport != nil {
1162+
o.craneOpts = append(o.craneOpts, crane.WithTransport(transport))
1163+
o.verifyOpts = append(o.verifyOpts, remote.WithTransport(transport))
1164+
}
1165+
}
1166+
}

internal/oci/verifier.go

+9-7
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

internal/oci/verifier_test.go

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
Copyright 2022 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package oci
18+
19+
import (
20+
"net/http"
21+
"reflect"
22+
"testing"
23+
24+
"github.com/google/go-containerregistry/pkg/authn"
25+
"github.com/google/go-containerregistry/pkg/v1/remote"
26+
)
27+
28+
func TestOptions(t *testing.T) {
29+
tests := []struct {
30+
name string
31+
opts []Options
32+
want *options
33+
}{{
34+
name: "no options",
35+
want: &options{},
36+
}, {
37+
name: "signature option",
38+
opts: []Options{WithPublicKey([]byte("foo"))},
39+
want: &options{
40+
PublicKey: []byte("foo"),
41+
ROpt: nil,
42+
},
43+
}, {
44+
name: "keychain option",
45+
opts: []Options{WithRemoteOptions(remote.WithAuthFromKeychain(authn.DefaultKeychain))},
46+
want: &options{
47+
PublicKey: nil,
48+
ROpt: []remote.Option{remote.WithAuthFromKeychain(authn.DefaultKeychain)},
49+
},
50+
}, {
51+
name: "keychain and authenticator option",
52+
opts: []Options{WithRemoteOptions(
53+
remote.WithAuth(&authn.Basic{Username: "foo", Password: "bar"}),
54+
remote.WithAuthFromKeychain(authn.DefaultKeychain),
55+
)},
56+
want: &options{
57+
PublicKey: nil,
58+
ROpt: []remote.Option{
59+
remote.WithAuth(&authn.Basic{Username: "foo", Password: "bar"}),
60+
remote.WithAuthFromKeychain(authn.DefaultKeychain),
61+
},
62+
},
63+
}, {
64+
name: "keychain, authenticator and transport option",
65+
opts: []Options{WithRemoteOptions(
66+
remote.WithAuth(&authn.Basic{Username: "foo", Password: "bar"}),
67+
remote.WithAuthFromKeychain(authn.DefaultKeychain),
68+
remote.WithTransport(http.DefaultTransport),
69+
)},
70+
want: &options{
71+
PublicKey: nil,
72+
ROpt: []remote.Option{
73+
remote.WithAuth(&authn.Basic{Username: "foo", Password: "bar"}),
74+
remote.WithAuthFromKeychain(authn.DefaultKeychain),
75+
remote.WithTransport(http.DefaultTransport),
76+
},
77+
},
78+
},
79+
}
80+
81+
for _, test := range tests {
82+
t.Run(test.name, func(t *testing.T) {
83+
o := options{}
84+
for _, opt := range test.opts {
85+
opt(&o)
86+
}
87+
if !reflect.DeepEqual(o.PublicKey, test.want.PublicKey) {
88+
t.Errorf("got %#v, want %#v", &o.PublicKey, test.want.PublicKey)
89+
}
90+
91+
if test.want.ROpt != nil {
92+
if len(o.ROpt) != len(test.want.ROpt) {
93+
t.Errorf("got %d remote options, want %d", len(o.ROpt), len(test.want.ROpt))
94+
}
95+
return
96+
}
97+
98+
if test.want.ROpt == nil {
99+
if len(o.ROpt) != 0 {
100+
t.Errorf("got %d remote options, want %d", len(o.ROpt), 0)
101+
}
102+
}
103+
})
104+
}
105+
}

0 commit comments

Comments
 (0)