Skip to content
This repository was archived by the owner on Mar 16, 2024. It is now read-only.

Commit 09f636a

Browse files
authored
fix: signature verification registry authentication and rate limits (#1412 & #1414) (#1416)
We faced issues with failing signature verification when using private/authenticated registries and also hit rate-limits on DockerHub due to lots of GET requests (pulling images, signature artifacts and digest requests). I made some changes that should fix/improve this: - refactor: reduce the number of places where GET requests can occur (i.e. pulling them out of the for-loops where possible) - before, crane functions used the default keychain instead of the one derived from the Acorn environment - before, we used ociremote functions to determine the digests, which defaulted to GET requests that count against e.g. DockerHub's pull rate limits -> switching to crane functions should help here
1 parent 1530d0c commit 09f636a

File tree

4 files changed

+104
-57
lines changed

4 files changed

+104
-57
lines changed

pkg/cosign/cosign.go

Lines changed: 67 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,9 @@ import (
1111

1212
v1 "github.com/acorn-io/acorn/pkg/apis/internal.acorn.io/v1"
1313
"github.com/acorn-io/acorn/pkg/imagesystem"
14-
"github.com/google/go-containerregistry/pkg/authn"
1514
"github.com/google/go-containerregistry/pkg/crane"
1615
"github.com/google/go-containerregistry/pkg/name"
1716
ggcrv1 "github.com/google/go-containerregistry/pkg/v1"
18-
"github.com/google/go-containerregistry/pkg/v1/remote"
1917
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
2018
"github.com/sigstore/cosign/pkg/cosign"
2119
"github.com/sigstore/cosign/pkg/oci"
@@ -31,89 +29,83 @@ import (
3129
)
3230

3331
type VerifyOpts struct {
34-
ImageRef string
32+
ImageRef name.Digest
33+
SignatureRef name.Reference
3534
Namespace string
3635
AnnotationRules v1.SignatureAnnotations
3736
Key string
3837
SignatureAlgorithm string
39-
RegistryClientOpts []ociremote.Option
38+
OciRemoteOpts []ociremote.Option
39+
CraneOpts []crane.Option
4040
NoCache bool
4141
}
4242

43-
// verifySignature checks if the image is signed with the given key and if the annotations match the given rules
44-
func VerifySignature(ctx context.Context, c client.Reader, opts VerifyOpts) error {
43+
// EnsureReferences will enrich the VerifyOpts with the image digest and signature reference.
44+
// It's outsourced here, so we can ensure that it's used as few times as possible to reduce the number of potential
45+
// GET requests to the registry which would count against potential rate limits.
46+
func EnsureReferences(ctx context.Context, c client.Reader, img string, opts *VerifyOpts) error {
4547
// --- image name to digest hash
46-
imgRef, err := name.ParseReference(opts.ImageRef)
48+
imgRef, err := name.ParseReference(img)
4749
if err != nil {
48-
return fmt.Errorf("failed to parse image %s: %w", opts.ImageRef, err)
50+
return fmt.Errorf("failed to parse image %s: %w", img, err)
4951
}
5052

51-
imgDigest, err := ociremote.ResolveDigest(imgRef, opts.RegistryClientOpts...)
53+
imgDigest, err := crane.Digest(imgRef.Name(), opts.CraneOpts...) // this uses HEAD to determine the digest, but falls back to GET if HEAD fails
5254
if err != nil {
5355
return fmt.Errorf("failed to resolve image digest: %w", err)
5456
}
5557

56-
imgDigestHash, err := ggcrv1.NewHash(imgDigest.Identifier())
57-
if err != nil {
58-
return err
59-
}
58+
opts.ImageRef = imgRef.Context().Digest(imgDigest)
6059

61-
// --- cosign verifier options
62-
63-
cosignOpts := &cosign.CheckOpts{
64-
Annotations: map[string]interface{}{},
65-
ClaimVerifier: cosign.SimpleClaimVerifier,
66-
RegistryClientOpts: opts.RegistryClientOpts,
60+
signatureRef, err := ensureSignatureArtifact(ctx, c, opts.Namespace, opts.ImageRef, opts.NoCache, opts.OciRemoteOpts, opts.CraneOpts)
61+
if err != nil {
62+
return fmt.Errorf("failed to ensure signature artifact: %w", err)
6763
}
64+
opts.SignatureRef = signatureRef
6865

69-
// --- parse key
70-
if opts.Key != "" {
71-
verifier, err := LoadKey(ctx, opts.Key, opts.SignatureAlgorithm)
72-
if err != nil {
73-
return fmt.Errorf("failed to load key: %w", err)
74-
}
75-
cosignOpts.SigVerifier = verifier
76-
}
66+
return nil
67+
}
7768

69+
func ensureSignatureArtifact(ctx context.Context, c client.Reader, namespace string, img name.Digest, noCache bool, ociRemoteOpts []ociremote.Option, craneOpts []crane.Option) (name.Reference, error) {
7870
// -- signature hash
79-
sigTag, err := ociremote.SignatureTag(imgRef, opts.RegistryClientOpts...)
71+
sigTag, err := ociremote.SignatureTag(img, ociRemoteOpts...) // we force imgRef to be a digest above, so this should *not* make a GET request to the registry
8072
if err != nil {
81-
return fmt.Errorf("failed to get signature tag: %w", err)
73+
return nil, fmt.Errorf("failed to get signature tag: %w", err)
8274
}
8375

84-
sigDigest, err := crane.Digest(sigTag.Name(), crane.WithAuthFromKeychain(authn.DefaultKeychain))
76+
sigDigest, err := crane.Digest(sigTag.Name(), craneOpts...) // this uses HEAD to determine the digest, but falls back to GET if HEAD fails
8577
if err != nil {
8678
if terr, ok := err.(*transport.Error); ok && terr.StatusCode == http.StatusNotFound {
8779
// signature artifact not found -> that's an actual verification error
88-
return fmt.Errorf("%w: expected signature artifact %s not found", cosign.ErrNoMatchingSignatures, sigTag.Name())
80+
return nil, fmt.Errorf("%w: expected signature artifact %s not found", cosign.ErrNoMatchingSignatures, sigTag.Name())
8981
}
90-
return fmt.Errorf("failed to get signature digest: %w", err)
82+
return nil, fmt.Errorf("failed to get signature digest: %w", err)
9183
}
9284

9385
sigRefToUse, err := name.ParseReference(sigTag.Name(), name.WeakValidation)
9486
if err != nil {
95-
return fmt.Errorf("failed to parse signature reference: %w", err)
87+
return nil, fmt.Errorf("failed to parse signature reference: %w", err)
9688
}
9789
logrus.Debugf("Signature %s has digest: %s", sigRefToUse.Name(), sigDigest)
9890

99-
if !opts.NoCache {
100-
internalRepo, _, err := imagesystem.GetInternalRepoForNamespace(ctx, c, opts.Namespace)
91+
if !noCache {
92+
internalRepo, _, err := imagesystem.GetInternalRepoForNamespace(ctx, c, namespace)
10193
if err != nil {
102-
return fmt.Errorf("failed to get internal repo for namespace %s: %w", opts.Namespace, err)
94+
return nil, fmt.Errorf("failed to get internal repo for namespace %s: %w", namespace, err)
10395
}
10496

10597
localSignatureArtifact := fmt.Sprintf("%s:%s", internalRepo, sigTag.Identifier())
10698

10799
// --- check if we have the signature artifact locally, if not, copy it over from external registry
108100
mustPull := false
109-
localSigSHA, err := crane.Digest(localSignatureArtifact, crane.WithAuthFromKeychain(authn.DefaultKeychain))
101+
localSigSHA, err := crane.Digest(localSignatureArtifact, craneOpts...) // this uses HEAD to determine the digest, but falls back to GET if HEAD fails
110102
if err != nil {
111103
var terr *transport.Error
112104
if ok := errors.As(err, &terr); ok && terr.StatusCode == http.StatusNotFound {
113105
logrus.Debugf("signature artifact %s not found locally, will try to pull it", localSignatureArtifact)
114106
mustPull = true
115107
} else {
116-
return fmt.Errorf("failed to get local signature digest, cannot check if we have it cached locally: %w", err)
108+
return nil, fmt.Errorf("failed to get local signature digest, cannot check if we have it cached locally: %w", err)
117109
}
118110
} else if localSigSHA != sigDigest {
119111
logrus.Debugf("Local signature digest %s does not match remote signature digest %s, will try to pull it", localSigSHA, sigDigest)
@@ -122,27 +114,57 @@ func VerifySignature(ctx context.Context, c client.Reader, opts VerifyOpts) erro
122114

123115
if mustPull {
124116
// --- pull signature artifact
125-
err := crane.Copy(sigTag.Name(), localSignatureArtifact, crane.WithAuthFromKeychain(authn.DefaultKeychain))
117+
err := crane.Copy(sigTag.Name(), localSignatureArtifact, craneOpts...) // Pull (GET) counts against the rate limits, so this shouldn't be done too often
126118
if err != nil {
127-
return fmt.Errorf("failed to copy signature artifact: %w", err)
119+
return nil, fmt.Errorf("failed to copy signature artifact: %w", err)
128120
}
129121
}
130122

131123
lname, err := name.ParseReference(localSignatureArtifact, name.WeakValidation)
132124
if err != nil {
133-
return fmt.Errorf("failed to parse local signature artifact %s: %w", localSignatureArtifact, err)
125+
return nil, fmt.Errorf("failed to parse local signature artifact %s: %w", localSignatureArtifact, err)
134126
}
135127

136128
sigRefToUse = lname
137129

138-
logrus.Debugf("Checking if image %s is signed with %s (cache: %s)", imgRef, sigTag, localSignatureArtifact)
130+
logrus.Debugf("Checking if image %s is signed with %s (cache: %s)", img, sigTag, localSignatureArtifact)
139131
}
140132

141-
sigs, err := ociremote.Signatures(sigRefToUse, ociremote.WithRemoteOptions(remote.WithAuthFromKeychain(authn.DefaultKeychain)))
133+
return sigRefToUse, nil
134+
}
135+
136+
// VerifySignature checks if the image is signed with the given key and if the annotations match the given rules
137+
// This does a lot of image and image manifest juggling to fetch artifacts, digests, etc. from the registry, so we have to be
138+
// careful to not do too many GET requests that count against registry rate limits (e.g. for DockerHub).
139+
// Crane uses HEAD (with GET as a fallback) wherever it can, so it's a good choice here e.g. for fetching digests.
140+
func VerifySignature(ctx context.Context, opts VerifyOpts) error {
141+
sigs, err := ociremote.Signatures(opts.SignatureRef, opts.OciRemoteOpts...) // this runs against our internal registry, so it should not count against the rate limits
142142
if err != nil {
143143
return fmt.Errorf("failed to get signatures: %w", err)
144144
}
145145

146+
imgDigestHash, err := ggcrv1.NewHash(opts.ImageRef.DigestStr())
147+
if err != nil {
148+
return err
149+
}
150+
151+
// --- cosign verifier options
152+
153+
cosignOpts := &cosign.CheckOpts{
154+
Annotations: map[string]interface{}{},
155+
ClaimVerifier: cosign.SimpleClaimVerifier,
156+
RegistryClientOpts: opts.OciRemoteOpts,
157+
}
158+
159+
// --- parse key
160+
if opts.Key != "" {
161+
verifier, err := LoadKey(ctx, opts.Key, opts.SignatureAlgorithm)
162+
if err != nil {
163+
return fmt.Errorf("failed to load key: %w", err)
164+
}
165+
cosignOpts.SigVerifier = verifier
166+
}
167+
146168
// --- get and verify signatures
147169
signatures, bundlesVerified, err := verifySignatures(ctx, sigs, imgDigestHash, cosignOpts)
148170
if err != nil {
@@ -152,7 +174,7 @@ func VerifySignature(ctx context.Context, c client.Reader, opts VerifyOpts) erro
152174
return fmt.Errorf("failed to verify image signatures: %w", err)
153175
}
154176

155-
logrus.Debugf("image %s: %d signatures verified (bundle verified: %v)", imgRef, len(signatures), bundlesVerified)
177+
logrus.Debugf("image %s: %d signatures verified (bundle verified: %v)", opts.ImageRef.Name(), len(signatures), bundlesVerified)
156178

157179
// --- extract payloads for subsequent checks
158180
payloads, err := extractPayload(signatures)
@@ -167,7 +189,7 @@ func VerifySignature(ctx context.Context, c client.Reader, opts VerifyOpts) erro
167189
}
168190
return fmt.Errorf("failed to check annotations: %w", err)
169191
}
170-
logrus.Debugf("image %s: Annotations (%+v) verified", imgRef, opts.AnnotationRules)
192+
logrus.Debugf("image %s: Annotations (%+v) verified", opts.ImageRef.Name(), opts.AnnotationRules)
171193

172194
return nil
173195
}

pkg/cosign/cosign_test.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ func pushOCIDir(path string, ref name.Reference) error {
8282
return nil
8383
}
8484

85-
// TODO: mock oci registry and signatures
8685
func TestVerifySignature(t *testing.T) {
8786
imgPath := "./testdata/img.oci"
8887
imgDigestExpected := "sha256:245864d0312e7e33201eff111cfc071727f4eaa9edd10a395c367077e200cad2"
@@ -177,12 +176,13 @@ func TestVerifySignature(t *testing.T) {
177176
}
178177

179178
opts := VerifyOpts{
180-
ImageRef: fmt.Sprintf("%s@%s", imgRepo, imgDigestExpected),
181179
SignatureAlgorithm: "sha256",
182180
NoCache: true,
183181
}
184182

185-
ref, err := name.ParseReference(opts.ImageRef)
183+
imgName := fmt.Sprintf("%s@%s", imgRepo, imgDigestExpected)
184+
185+
ref, err := name.ParseReference(imgName)
186186
if err != nil {
187187
t.Fatal(err)
188188
}
@@ -191,7 +191,7 @@ func TestVerifySignature(t *testing.T) {
191191
t.Fatal(err)
192192
}
193193

194-
digest, err := crane.Digest(opts.ImageRef)
194+
digest, err := crane.Digest(ref.Name())
195195
if err != nil {
196196
t.Fatal(err)
197197
}
@@ -218,7 +218,11 @@ func TestVerifySignature(t *testing.T) {
218218
opts.Key = tc.key
219219
opts.AnnotationRules = tc.annotationrules
220220

221-
err = VerifySignature(context.Background(), nil, opts)
221+
if err := EnsureReferences(context.Background(), nil, imgName, &opts); err != nil {
222+
t.Fatal(err)
223+
}
224+
225+
err = VerifySignature(context.Background(), opts)
222226
if err != nil && !tc.shouldError {
223227
t.Fatalf("[%d] unexpected error: %v, but %s", ti, err, tc.description)
224228
}

pkg/imageallowrules/imageallowrules.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
v1 "github.com/acorn-io/acorn/pkg/apis/internal.acorn.io/v1"
88
"github.com/acorn-io/acorn/pkg/cosign"
99
"github.com/acorn-io/acorn/pkg/images"
10+
"github.com/google/go-containerregistry/pkg/authn"
11+
"github.com/google/go-containerregistry/pkg/crane"
1012
"github.com/google/go-containerregistry/pkg/v1/remote"
1113
"github.com/rancher/wrangler/pkg/merr"
1214
ocosign "github.com/sigstore/cosign/pkg/cosign"
@@ -44,10 +46,15 @@ func CheckImageAllowed(ctx context.Context, c client.Reader, namespace, image st
4446
return err
4547
}
4648

47-
return CheckImageAgainstRules(ctx, c, namespace, image, rulesList.Items, opts...)
49+
keychain, err := images.GetAuthenticationRemoteKeychainWithLocalAuth(ctx, nil, nil, c, namespace)
50+
if err != nil {
51+
return err
52+
}
53+
54+
return CheckImageAgainstRules(ctx, c, namespace, image, rulesList.Items, keychain, opts...)
4855
}
4956

50-
func CheckImageAgainstRules(ctx context.Context, c client.Reader, namespace string, image string, imageAllowRules []v1.ImageAllowRuleInstance, opts ...remote.Option) error {
57+
func CheckImageAgainstRules(ctx context.Context, c client.Reader, namespace string, image string, imageAllowRules []v1.ImageAllowRuleInstance, keychain authn.Keychain, opts ...remote.Option) error {
5158
if len(imageAllowRules) == 0 {
5259
// No ImageAllowRules found, so allow the image
5360
return nil
@@ -57,13 +64,18 @@ func CheckImageAgainstRules(ctx context.Context, c client.Reader, namespace stri
5764

5865
// Check if the image is allowed
5966
verifyOpts := cosign.VerifyOpts{
60-
ImageRef: image,
6167
Namespace: namespace,
6268
AnnotationRules: v1.SignatureAnnotations{},
6369
Key: "",
6470
SignatureAlgorithm: "sha256", // FIXME: make signature algorithm configurable (?)
65-
RegistryClientOpts: []ociremote.Option{ociremote.WithRemoteOptions(opts...)},
71+
OciRemoteOpts: []ociremote.Option{ociremote.WithRemoteOptions(opts...)},
72+
CraneOpts: []crane.Option{crane.WithContext(ctx), crane.WithAuthFromKeychain(keychain)},
6673
}
74+
75+
if err := cosign.EnsureReferences(ctx, c, image, &verifyOpts); err != nil {
76+
return fmt.Errorf("error ensuring references for image %s: %w", image, err)
77+
}
78+
6779
for _, imageAllowRule := range imageAllowRules {
6880
notAllowedErr := &ErrImageNotAllowed{Rule: fmt.Sprintf("%s/%s", imageAllowRule.Namespace, imageAllowRule.Name), Image: image}
6981

@@ -78,7 +90,7 @@ func CheckImageAgainstRules(ctx context.Context, c client.Reader, namespace stri
7890
for allOfRuleIndex, signer := range rule.SignedBy.AllOf {
7991
logrus.Debugf("Checking image %s against %s/%s.signatures.allOf.%d", image, imageAllowRule.Namespace, imageAllowRule.Name, allOfRuleIndex)
8092
verifyOpts.Key = signer
81-
err := cosign.VerifySignature(ctx, c, verifyOpts)
93+
err := cosign.VerifySignature(ctx, verifyOpts)
8294
if err != nil {
8395
if _, ok := err.(*ocosign.VerificationError); ok {
8496
notAllowedErr.SubrulePath += fmt.Sprintf(".allOf.%d (%v)", allOfRuleIndex, err)
@@ -96,7 +108,7 @@ func CheckImageAgainstRules(ctx context.Context, c client.Reader, namespace stri
96108
for anyOfRuleIndex, signer := range rule.SignedBy.AnyOf {
97109
logrus.Debugf("Checking image %s against %s/%s.signatures.anyOf.%d", image, imageAllowRule.Namespace, imageAllowRule.Name, anyOfRuleIndex)
98110
verifyOpts.Key = signer
99-
err := cosign.VerifySignature(ctx, c, verifyOpts)
111+
err := cosign.VerifySignature(ctx, verifyOpts)
100112
if err == nil {
101113
anyOfOK = true
102114
break

pkg/images/operations.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ func GetImageReference(ctx context.Context, c client.Reader, namespace, image st
162162
return imagename.ParseReference(image)
163163
}
164164

165-
func GetAuthenticationRemoteOptionsWithLocalAuth(ctx context.Context, registry authn.Resource, localAuth *apiv1.RegistryAuth, client client.Reader, namespace string, additionalOpts ...remote.Option) ([]remote.Option, error) {
165+
func GetAuthenticationRemoteKeychainWithLocalAuth(ctx context.Context, registry authn.Resource, localAuth *apiv1.RegistryAuth, client client.Reader, namespace string) (authn.Keychain, error) {
166166
authn, err := pullsecret.Keychain(ctx, client, namespace)
167167
if err != nil {
168168
return nil, err
@@ -172,6 +172,15 @@ func GetAuthenticationRemoteOptionsWithLocalAuth(ctx context.Context, registry a
172172
authn = NewSimpleKeychain(registry, *localAuth, authn)
173173
}
174174

175+
return authn, nil
176+
}
177+
178+
func GetAuthenticationRemoteOptionsWithLocalAuth(ctx context.Context, registry authn.Resource, localAuth *apiv1.RegistryAuth, client client.Reader, namespace string, additionalOpts ...remote.Option) ([]remote.Option, error) {
179+
authn, err := GetAuthenticationRemoteKeychainWithLocalAuth(ctx, registry, localAuth, client, namespace)
180+
if err != nil {
181+
return nil, err
182+
}
183+
175184
result := []remote.Option{
176185
remote.WithContext(ctx),
177186
remote.WithAuthFromKeychain(authn),

0 commit comments

Comments
 (0)