Skip to content

Commit e97e8ce

Browse files
author
github-actions
committed
Merge branch 'main' into cras
2 parents 405e080 + 82e350c commit e97e8ce

File tree

5 files changed

+163
-12
lines changed

5 files changed

+163
-12
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
public key with the `--key` flag. Verification passes if at least one
2727
signature that can be validated with the provided key is present. The JSON
2828
payloads of all valid signatures are displayed.
29+
- `singularity push` now supports pushing cosign signatures in an OCI-SIF to
30+
an OCI registry, via the `--with-cosign` flag.
2931

3032
## Requirements / Packaging
3133

cmd/internal/cli/push.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ var (
3636

3737
// pushLayerFormat sets the layer format to be used when pushing OCI images.
3838
pushLayerFormat string
39+
40+
// pushWithCosign sets whether cosign signatures are pushed when pushing OCI images.
41+
pushWithCosign bool
3942
)
4043

4144
// --library
@@ -79,6 +82,16 @@ var pushLayerFormatFlag = cmdline.Flag{
7982
EnvKeys: []string{"LAYER_FORMAT"},
8083
}
8184

85+
// --with-cosign
86+
var pushWithCosignFlag = cmdline.Flag{
87+
ID: "pushWithCosignFlag",
88+
Value: &pushWithCosign,
89+
DefaultValue: false,
90+
Name: "with-cosign",
91+
Usage: "push cosign signatures from OCI-SIF images",
92+
EnvKeys: []string{"WITH_COSIGN"},
93+
}
94+
8295
func init() {
8396
addCmdInit(func(cmdManager *cmdline.CommandManager) {
8497
cmdManager.RegisterCmd(PushCmd)
@@ -95,6 +108,7 @@ func init() {
95108
cmdManager.RegisterFlagForCmd(&commonTmpDirFlag, PushCmd)
96109

97110
cmdManager.RegisterFlagForCmd(&pushLayerFormatFlag, PushCmd)
111+
cmdManager.RegisterFlagForCmd(&pushWithCosignFlag, PushCmd)
98112
})
99113
}
100114

@@ -206,6 +220,7 @@ var PushCmd = &cobra.Command{
206220
Auth: ociAuth,
207221
AuthFile: reqAuthFile,
208222
LayerFormat: pushLayerFormat,
223+
WithCosign: pushWithCosign,
209224
}
210225
if err := oci.Push(cmd.Context(), file, ref, opts); err != nil {
211226
sylog.Fatalf("Unable to push image to oci registry: %v", err)

e2e/push/push.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,79 @@ func (c ctx) testPushOCIOverlay(t *testing.T) {
273273
}
274274
}
275275

276+
func (c ctx) testPushOCICosign(t *testing.T) {
277+
e2e.EnsureOCISIF(t, c.env)
278+
279+
imgRef := fmt.Sprintf("docker://%s/docker_oci-cosign:test", c.env.TestRegistry)
280+
281+
testSif := filepath.Join(t.TempDir(), "signed.sif")
282+
if err := fs.CopyFile(c.env.OCISIFPath, testSif, 0o755); err != nil {
283+
t.Fatal(err)
284+
}
285+
keyPath := filepath.Join("..", "test", "keys", "cosign.key")
286+
c.env.RunSingularity(
287+
t,
288+
e2e.AsSubtest("sign"),
289+
e2e.WithProfile(e2e.UserProfile),
290+
e2e.WithCommand("sign"),
291+
e2e.WithArgs("--cosign", "--key", keyPath, testSif),
292+
e2e.ExpectExit(0),
293+
)
294+
295+
tests := []struct {
296+
name string
297+
withCosign bool
298+
layerFormat string
299+
expectExit int
300+
resultOps []e2e.SingularityCmdResultOp
301+
}{
302+
{
303+
name: "Default",
304+
withCosign: false,
305+
layerFormat: "",
306+
expectExit: 0,
307+
resultOps: []e2e.SingularityCmdResultOp{
308+
e2e.ExpectError(e2e.UnwantedContainMatch, "Writing cosign signatures"),
309+
},
310+
},
311+
{
312+
name: "WithCosign",
313+
withCosign: true,
314+
layerFormat: "",
315+
expectExit: 0,
316+
resultOps: []e2e.SingularityCmdResultOp{
317+
e2e.ExpectError(e2e.ContainMatch, "Writing cosign signatures"),
318+
},
319+
},
320+
{
321+
name: "WithCosignMutated",
322+
withCosign: true,
323+
layerFormat: "tar",
324+
expectExit: 255,
325+
},
326+
}
327+
328+
for _, tt := range tests {
329+
args := []string{}
330+
if tt.layerFormat != "" {
331+
args = []string{"--layer-format", tt.layerFormat}
332+
}
333+
if tt.withCosign {
334+
args = append(args, "--with-cosign")
335+
}
336+
args = append(args, testSif, imgRef)
337+
338+
c.env.RunSingularity(
339+
t,
340+
e2e.AsSubtest(tt.name),
341+
e2e.WithProfile(e2e.UserProfile),
342+
e2e.WithCommand("push"),
343+
e2e.WithArgs(args...),
344+
e2e.ExpectExit(tt.expectExit, tt.resultOps...),
345+
)
346+
}
347+
}
348+
276349
// E2ETests is the main func to trigger the test suite
277350
func E2ETests(env e2e.TestEnv) testhelper.Tests {
278351
c := ctx{
@@ -284,5 +357,6 @@ func E2ETests(env e2e.TestEnv) testhelper.Tests {
284357
"oras": c.testPushCmd,
285358
"oci tar layers": c.testPushOCITarLayers,
286359
"oci overlay": c.testPushOCIOverlay,
360+
"oci cosign": c.testPushOCICosign,
287361
}
288362
}

internal/pkg/client/oci/push.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ type PushOptions struct {
2828
// TmpDir is a temporary directory to be used for an temporary files created
2929
// during the push.
3030
TmpDir string
31+
// WithCosign sets whether to push any associated cosign signatures when
32+
// pushing an OCI-SIF to a registry.
33+
WithCosign bool
3134
}
3235

3336
// Push pushes an image into an OCI registry, as an OCI image (not an ORAS artifact).
@@ -46,6 +49,7 @@ func Push(ctx context.Context, sourceFile string, destRef string, opts PushOptio
4649
AuthFile: opts.AuthFile,
4750
LayerFormat: opts.LayerFormat,
4851
TmpDir: opts.TmpDir,
52+
WithCosign: opts.WithCosign,
4953
}
5054
return ocisif.PushOCISIF(ctx, sourceFile, destRef, ocisifOpts)
5155
case image.SIF:

internal/pkg/client/ocisif/ocisif.go

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package ocisif
77

88
import (
99
"context"
10+
"errors"
1011
"fmt"
1112
"os"
1213
"path/filepath"
@@ -18,7 +19,9 @@ import (
1819
v1 "github.com/google/go-containerregistry/pkg/v1"
1920
"github.com/google/go-containerregistry/pkg/v1/remote"
2021
"github.com/google/go-containerregistry/pkg/v1/tarball"
22+
cosignremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
2123
ocimutate "github.com/sylabs/oci-tools/pkg/mutate"
24+
"github.com/sylabs/oci-tools/pkg/sourcesink"
2225
"github.com/sylabs/sif/v2/pkg/sif"
2326
"github.com/sylabs/singularity/v4/internal/pkg/cache"
2427
"github.com/sylabs/singularity/v4/internal/pkg/client/progress"
@@ -45,6 +48,7 @@ type PullOptions struct {
4548
Platform ggcrv1.Platform
4649
ReqAuthFile string
4750
KeepLayers bool
51+
WithCosign bool
4852
}
4953

5054
// PullOCISIF will create an OCI-SIF image in the cache if directTo="", or a specific file if directTo is set.
@@ -172,6 +176,9 @@ type PushOptions struct {
172176
// TmpDir is a temporary directory to be used for an temporary files created
173177
// during the push.
174178
TmpDir string
179+
// WithCosign controls whether cosign signatures present in the SIF are also
180+
// pushed to the destination repository in the registry.
181+
WithCosign bool
175182
}
176183

177184
// PushOCISIF pushes a single image from sourceFile to the OCI registry destRef.
@@ -187,18 +194,20 @@ func PushOCISIF(ctx context.Context, sourceFile, destRef string, opts PushOption
187194
return err
188195
}
189196

190-
fi, err := sif.LoadContainerFromPath(sourceFile, sif.OptLoadWithFlag(os.O_RDONLY))
197+
ss, err := sourcesink.SIFFromPath(sourceFile)
191198
if err != nil {
192-
return err
199+
return fmt.Errorf("failed to open OCI-SIF: %w", err)
193200
}
194-
defer fi.UnloadContainer()
195-
196-
image, err := ocisif.GetSingleImage(fi)
201+
d, err := ss.Get(ctx)
202+
if err != nil {
203+
return fmt.Errorf("while fetching image from OCI-SIF: %v", err)
204+
}
205+
image, err := d.Image()
197206
if err != nil {
198-
return fmt.Errorf("while obtaining image: %w", err)
207+
return fmt.Errorf("failed to retrieve image: %w", err)
199208
}
200209

201-
image, err = transformLayers(image, opts.LayerFormat, opts.TmpDir)
210+
image, err = transformLayers(image, opts)
202211
if err != nil {
203212
return err
204213
}
@@ -237,10 +246,18 @@ func PushOCISIF(ctx context.Context, sourceFile, destRef string, opts PushOption
237246
remoteOpts = append(remoteOpts, remote.WithProgress(progChan))
238247
}
239248

240-
return remote.Write(ir, image, remoteOpts...)
249+
if err := remote.Write(ir, image, remoteOpts...); err != nil {
250+
return err
251+
}
252+
253+
if opts.WithCosign {
254+
return writeSignatures(ctx, ir, d, opts)
255+
}
256+
257+
return nil
241258
}
242259

243-
func transformLayers(base v1.Image, layerFormat, tmpDir string) (v1.Image, error) {
260+
func transformLayers(base v1.Image, opts PushOptions) (v1.Image, error) {
244261
ls, err := base.Layers()
245262
if err != nil {
246263
return nil, err
@@ -254,15 +271,15 @@ func transformLayers(base v1.Image, layerFormat, tmpDir string) (v1.Image, error
254271
return nil, err
255272
}
256273

257-
switch layerFormat {
274+
switch opts.LayerFormat {
258275
case DefaultLayerFormat:
259276
continue
260277
case SquashfsLayerFormat:
261278
if mt != ocisif.SquashfsLayerMediaType {
262279
return nil, fmt.Errorf("unexpected layer mediaType: %v", mt)
263280
}
264281
case TarLayerFormat:
265-
opener, err := ocimutate.TarFromSquashfsLayer(l, ocimutate.OptTarTempDir(tmpDir))
282+
opener, err := ocimutate.TarFromSquashfsLayer(l, ocimutate.OptTarTempDir(opts.TmpDir))
266283
if err != nil {
267284
return nil, err
268285
}
@@ -272,10 +289,14 @@ func transformLayers(base v1.Image, layerFormat, tmpDir string) (v1.Image, error
272289
}
273290
ms = append(ms, ocimutate.SetLayer(i, tarLayer))
274291
default:
275-
return nil, fmt.Errorf("unsupported layer format: %v", layerFormat)
292+
return nil, fmt.Errorf("unsupported layer format: %v", opts.TmpDir)
276293
}
277294
}
278295

296+
if len(ms) > 0 && opts.WithCosign {
297+
return nil, fmt.Errorf("cannot push signature - invalidated by transforming layer format to %s", opts.LayerFormat)
298+
}
299+
279300
return ocimutate.Apply(base, ms...)
280301
}
281302

@@ -295,7 +316,42 @@ func handleOverlay(sourceFile string, opts PushOptions) error {
295316
return fmt.Errorf("cannot push overlay with layer format %q, use 'overlay seal' before pushing this image ", opts.LayerFormat)
296317
}
297318

319+
if opts.WithCosign {
320+
return errors.New("cannot push signature - would be invalidated by synchronizing overlay")
321+
}
322+
298323
// Make sure true overlay digest have been synced to the OCI constructs.
299324
sylog.Infof("Synchronizing overlay digest to OCI image.")
300325
return ocisif.SyncOverlay(sourceFile)
301326
}
327+
328+
func writeSignatures(ctx context.Context, ir name.Reference, d sourcesink.Descriptor, opts PushOptions) error {
329+
sd, ok := d.(sourcesink.SignedDescriptor)
330+
if !ok {
331+
return fmt.Errorf("failed to upgrade Descriptor to SignedDescriptor")
332+
}
333+
si, err := sd.SignedImage(ctx)
334+
if err != nil {
335+
return fmt.Errorf("failed to retrieve SignedImage: %w", err)
336+
}
337+
id, err := si.Digest()
338+
if err != nil {
339+
return fmt.Errorf("failed to retrieve image digest: %w", err)
340+
}
341+
sigImg, err := si.Signatures()
342+
if err != nil {
343+
return fmt.Errorf("failed to retrieve signatures: %w", err)
344+
}
345+
csRef, err := sourcesink.CosignRef(id, ir, cosignremote.SignatureTagSuffix)
346+
if err != nil {
347+
return err
348+
}
349+
350+
sylog.Infof("Writing cosign signatures: %s", csRef.Name())
351+
remoteOpts := []remote.Option{
352+
ociauth.AuthOptn(opts.Auth, opts.AuthFile),
353+
remote.WithUserAgent(useragent.Value()),
354+
remote.WithContext(ctx),
355+
}
356+
return remote.Write(csRef, sigImg, remoteOpts...)
357+
}

0 commit comments

Comments
 (0)