Skip to content

Commit 1ee9a5e

Browse files
authored
Merge pull request #767 from puerco/limit-days
Signature Check: Check Expected Identity in Certificate
2 parents 27f5136 + 964e96f commit 1ee9a5e

File tree

3 files changed

+157
-24
lines changed

3 files changed

+157
-24
lines changed

cmd/kpromo/cmd/sigcheck/sigcheck.go

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,21 @@ func Add(parent *cobra.Command) {
3131
3232
This subcommand checks the signature consistency across promoted images
3333
to ensure copies in all mirrors have their signatures attached.
34+
35+
By default, kpromo sigcheck will look at all images promoted during the last
36+
%d days. You can change the default using --from-days and determine a range
37+
using --to-days. For example, to verify all images promoted in an interval
38+
between 10 and 5 days ago run:
39+
40+
kpromo sigcheck --from-days=10 --to-days=5
41+
42+
To debug the signature checker, you can limit the number of images kpromo
43+
verifies using --limit. When no limit is specified, kpromo will check the
44+
signatures of all images in the specified date range. As an example, to limit
45+
kpromo to the first three images it finds run:
46+
47+
kpromo sigcheck --limit=3
48+
3449
`,
3550
SilenceUsage: true,
3651
SilenceErrors: true,
@@ -55,10 +70,38 @@ to ensure copies in all mirrors have their signatures attached.
5570
)
5671

5772
cmd.PersistentFlags().IntVar(
58-
&opts.SignCheckDays,
59-
"days",
60-
5,
61-
"check images uploaded this many days ago",
73+
&opts.SignCheckFromDays,
74+
"from-days",
75+
promoteropts.DefaultOptions.SignCheckFromDays,
76+
"check images uploaded starting this many days ago",
77+
)
78+
79+
cmd.PersistentFlags().IntVar(
80+
&opts.SignCheckToDays,
81+
"to-days",
82+
0,
83+
"check images --from-days ago to this many days ago (defaults to today)",
84+
)
85+
86+
cmd.PersistentFlags().IntVar(
87+
&opts.SignCheckMaxImages,
88+
"limit",
89+
0,
90+
"limit signature checks to a number of images (defaults to checking all)",
91+
)
92+
93+
cmd.PersistentFlags().StringVar(
94+
&opts.SignCheckIdentity,
95+
"certificate-identity",
96+
promoteropts.DefaultOptions.SignCheckIdentity,
97+
"identity to look for when verifying signatures",
98+
)
99+
100+
cmd.PersistentFlags().StringVar(
101+
&opts.SignCheckIssuer,
102+
"certificate-oidc-issuer",
103+
promoteropts.DefaultOptions.SignCheckIssuer,
104+
"issuer of the OIDC token used to generate the signature certificate",
62105
)
63106

64107
parent.AddCommand(cmd)

internal/promoter/image/signcheck.go

Lines changed: 93 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
package imagepromoter
1818

1919
import (
20+
"bytes"
21+
"encoding/json"
2022
"fmt"
2123
"net/url"
2224
"strings"
@@ -33,6 +35,9 @@ import (
3335
"github.com/google/go-containerregistry/pkg/name"
3436
"github.com/google/go-containerregistry/pkg/v1/google"
3537
"github.com/sirupsen/logrus"
38+
39+
"github.com/sigstore/sigstore/pkg/cryptoutils"
40+
3641
checkresults "sigs.k8s.io/promo-tools/v3/promoter/image/checkresults"
3742
options "sigs.k8s.io/promo-tools/v3/promoter/image/options"
3843
"sigs.k8s.io/promo-tools/v3/types/image"
@@ -64,7 +69,7 @@ func (di *DefaultPromoterImplementation) GetLatestImages(opts *options.Options)
6469
if err != nil {
6570
return nil, fmt.Errorf("fetching latest images: %w", err)
6671
}
67-
logrus.Infof("+%v", images)
72+
logrus.Infof("Images to check: +%v", images)
6873
return images, nil
6974
}
7075

@@ -148,7 +153,7 @@ func (di *DefaultPromoterImplementation) GetSignatureStatus(
148153
}
149154

150155
logrus.Infof("Checking %s for signatures in %d mirrors", refString, len(targetImages))
151-
existing, missing, err := checkObjects(targetImages)
156+
existing, missing, err := di.CheckSignatureLayers(opts, targetImages)
152157
if err != nil {
153158
return results, fmt.Errorf("checking objects: %w", err)
154159
}
@@ -160,37 +165,90 @@ func (di *DefaultPromoterImplementation) GetSignatureStatus(
160165
return results, nil
161166
}
162167

163-
func checkObjects(oList []string) (existing, missing []string, err error) {
168+
// miniManifest is a minimal representation of the sigstore signature manifest
169+
type miniManifest struct {
170+
Layers []struct {
171+
MediaType string
172+
Annotations map[string]string `json:"annotations"`
173+
} `json:"layers"`
174+
}
175+
176+
// CheckSignatureLayers checks a list of signature layers to ensure
177+
func (di *DefaultPromoterImplementation) CheckSignatureLayers(opts *options.Options, oList []string) (existing, missing []string, err error) {
164178
// TODO: Parallelize this check
165179
existing = []string{}
166180
missing = []string{}
167181
for _, s := range oList {
168-
e, err := objectExists(s)
182+
e, err := objectExists(opts, s)
169183
if err != nil {
170184
return existing, missing, fmt.Errorf("checking reference: %w", err)
171185
}
172186

173-
if e {
174-
existing = append(existing, s)
175-
} else {
187+
if !e {
176188
missing = append(missing, s)
189+
continue
177190
}
191+
192+
existing = append(existing, s)
178193
}
179194
return existing, missing, nil
180195
}
181196

182-
func objectExists(refString string) (bool, error) {
197+
func objectExists(opts *options.Options, refString string) (bool, error) {
183198
// Check
184-
_, err := crane.Digest(refString)
185-
if err == nil {
186-
return true, nil
199+
manifestData, err := crane.Manifest(refString)
200+
if err != nil {
201+
if strings.Contains(err.Error(), "MANIFEST_UNKNOWN") {
202+
logrus.WithField("image", refString).Info("No signature found")
203+
return false, nil
204+
}
205+
return false, fmt.Errorf("pulling signature manifest: %w", err)
187206
}
188207

189-
if strings.Contains(err.Error(), "MANIFEST_UNKNOWN") {
208+
manifest := &miniManifest{}
209+
if err := json.Unmarshal(manifestData, manifest); err != nil {
210+
return false, fmt.Errorf("parsing .sig image manifest: %w", err)
211+
}
212+
213+
// Get the certificate
214+
if manifest.Layers == nil || len(manifest.Layers) == 0 {
190215
return false, nil
191216
}
217+
signedLayers := 0
218+
for _, layer := range manifest.Layers {
219+
if layer.MediaType != "application/vnd.dev.cosign.simplesigning.v1+json" {
220+
continue
221+
}
222+
223+
certData, ok := layer.Annotations["dev.sigstore.cosign/certificate"]
224+
if !ok {
225+
continue
226+
}
192227

193-
return false, fmt.Errorf("checking if reference exists in the registry: %w", err)
228+
var b bytes.Buffer
229+
b.Write([]byte(certData))
230+
231+
certs, err := cryptoutils.LoadCertificatesFromPEM(&b)
232+
if err != nil {
233+
return false, err
234+
}
235+
236+
names := cryptoutils.GetSubjectAlternateNames(certs[0])
237+
for _, n := range names {
238+
if n == opts.SignCheckIdentity {
239+
return true, nil
240+
}
241+
}
242+
signedLayers++
243+
}
244+
245+
if signedLayers == 0 {
246+
logrus.WithField("image", refString).Debugf("No certificates found")
247+
} else {
248+
logrus.WithField("image", refString).Debugf("Image signed, but not with expected identity")
249+
}
250+
251+
return false, nil
194252
}
195253

196254
// FixMissingSignatures signs an image that has no signatures at all
@@ -200,13 +258,15 @@ func (di *DefaultPromoterImplementation) FixMissingSignatures(opts *options.Opti
200258
continue
201259
}
202260

203-
logrus.Infof("Signing and replicating %s", mainImg)
261+
logrus.Infof("Signing and replicating first mirror (%s)", mainImg)
262+
204263
// Build the digest of the first missing one
205-
digestRef := strings.ReplaceAll(res.Missing[0], ":sha256-", "@sha256:")
264+
digestRef := strings.TrimSuffix(strings.ReplaceAll(res.Missing[0], ":sha256-", "@sha256:"), ".sig")
206265
if err := di.signReference(opts, digestRef); err != nil {
207-
return fmt.Errorf("signing %s: %w", digestRef, err)
266+
return fmt.Errorf("signing first mirror reference %s: %w", digestRef, err)
208267
}
209268

269+
logrus.Infof("Replicating image to %d mirrors", len(res.Missing[1:]))
210270
for _, targetRef := range res.Missing[1:] {
211271
if err := di.replicateReference(opts, res.Missing[0], targetRef); err != nil {
212272
return fmt.Errorf("replicating signature: %w", err)
@@ -304,7 +364,15 @@ func (di *DefaultPromoterImplementation) readLatestImages(opts *options.Options)
304364
google.Keychain,
305365
))
306366

307-
dateCutOff := time.Now().AddDate(0, 0, opts.SignCheckDays*-1)
367+
dateCutOff := time.Now().AddDate(0, 0, opts.SignCheckFromDays*-1)
368+
dateCutOffTo := time.Now()
369+
if opts.SignCheckToDays > 0 {
370+
dateCutOffTo = time.Now().AddDate(0, 0, opts.SignCheckToDays*-1)
371+
}
372+
logrus.Infof("Checking images from %s to %s",
373+
dateCutOff.Local().Format(time.RFC822),
374+
dateCutOffTo.Local().Format(time.RFC822),
375+
)
308376
images := []string{}
309377

310378
repo, err := name.NewRepository(scanRegistry+"/"+repositoryPath, name.WeakValidation)
@@ -342,6 +410,10 @@ func (di *DefaultPromoterImplementation) readLatestImages(opts *options.Options)
342410
continue
343411
}
344412

413+
if opts.SignCheckToDays > 0 && manifest.Uploaded.After(dateCutOffTo) {
414+
continue
415+
}
416+
345417
mt.Lock()
346418
images = append(images, strings.ReplaceAll(
347419
fmt.Sprintf("%s:%s", repo, manifest.Tags[0]),
@@ -356,5 +428,9 @@ func (di *DefaultPromoterImplementation) readLatestImages(opts *options.Options)
356428
return nil, fmt.Errorf("walking repo: %w", err)
357429
}
358430

431+
if opts.SignCheckMaxImages != 0 && len(images) > opts.SignCheckMaxImages {
432+
images = images[0:opts.SignCheckMaxImages]
433+
}
434+
359435
return images, nil
360436
}

promoter/image/options/options.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,20 @@ type Options struct {
109109
// SignCheckFix when true, fix missing signatures
110110
SignCheckFix bool
111111

112-
// SignCheckDays number of days back to check for signatrures
113-
SignCheckDays int
112+
// SignCheckFromDays number of days back to check for signatrures
113+
SignCheckFromDays int
114+
115+
// SignCheckToDays complements SignCheckFromDays to enable date ranges
116+
SignCheckToDays int
117+
118+
// SignCheckMaxImages limits the number of images to look when verifying
119+
SignCheckMaxImages int
120+
121+
// SignCheckIdentity is the account we expect to sign all imges
122+
SignCheckIdentity string
123+
124+
// SignCheckIssuer is the iisuer of the OIDC tokens used to identify the signer
125+
SignCheckIssuer string
114126
}
115127

116128
var DefaultOptions = &Options{
@@ -122,7 +134,9 @@ var DefaultOptions = &Options{
122134
SignerAccount: "[email protected]",
123135
SignCheckFix: false,
124136
SignCheckReferences: []string{},
125-
SignCheckDays: 5,
137+
SignCheckFromDays: 5,
138+
SignCheckIdentity: "[email protected]",
139+
SignCheckIssuer: "https://accounts.google.com",
126140
}
127141

128142
func (o *Options) Validate() error {

0 commit comments

Comments
 (0)