Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add COSIGN_OCI_EXPERIMENTAL, push .sig/.sbom using OCI 1.1+ digest tag #2684

Merged
merged 12 commits into from
Feb 14, 2023
2 changes: 1 addition & 1 deletion cmd/cosign/cli/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func attachSBOM() *cobra.Command {
return err
}
fmt.Fprintf(os.Stderr, "WARNING: Attaching SBOMs this way does not sign them. If you want to sign them, use 'cosign attest --predicate %s --key <key path>' or 'cosign sign --key <key path> --attachment sbom <image uri>'.\n", o.SBOM)
return attach.SBOMCmd(cmd.Context(), o.Registry, o.SBOM, mediaType, args[0])
return attach.SBOMCmd(cmd.Context(), o.Registry, o.RegistryExperimental, o.SBOM, mediaType, args[0])
},
}

Expand Down
80 changes: 77 additions & 3 deletions cmd/cosign/cli/attach/sbom.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,31 @@ import (
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"

"github.com/google/go-containerregistry/pkg/logs"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/types"
"github.com/google/go-containerregistry/pkg/v1/remote/transport"
ocistatic "github.com/google/go-containerregistry/pkg/v1/static"
ocitypes "github.com/google/go-containerregistry/pkg/v1/types"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
ociexperimental "github.com/sigstore/cosign/v2/internal/pkg/oci/remote"
"github.com/sigstore/cosign/v2/internal/ui"
ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
"github.com/sigstore/cosign/v2/pkg/oci/static"
)

func SBOMCmd(ctx context.Context, regOpts options.RegistryOptions, sbomRef string, sbomType types.MediaType, imageRef string) error {
func SBOMCmd(ctx context.Context, regOpts options.RegistryOptions, regExpOpts options.RegistryExperimentalOptions, sbomRef string, sbomType ocitypes.MediaType, imageRef string) error {
if regExpOpts.RegistryReferrersMode == options.RegistryReferrersModeOCI11 {
return sbomCmdOCIExperimental(ctx, regOpts, sbomRef, sbomType, imageRef)
}

ref, err := name.ParseReference(imageRef, regOpts.NameOptions()...)
if err != nil {
return err
Expand All @@ -52,14 +65,75 @@ func SBOMCmd(ctx context.Context, regOpts options.RegistryOptions, sbomRef strin
return err
}

fmt.Fprintf(os.Stderr, "Uploading SBOM file for [%s] to [%s] with mediaType [%s].\n", ref.Name(), dstRef.Name(), sbomType)
ui.Infof(ctx, "Uploading SBOM file for [%s] to [%s] with mediaType [%s].\n", ref.Name(), dstRef.Name(), sbomType)
img, err := static.NewFile(b, static.WithLayerMediaType(sbomType))
if err != nil {
return err
}
return remote.Write(dstRef, img, regOpts.GetRegistryClientOpts(ctx)...)
}

func sbomCmdOCIExperimental(ctx context.Context, regOpts options.RegistryOptions, sbomRef string, sbomType ocitypes.MediaType, imageRef string) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit long for the cmd/ package. Can we move some of it into pkg/?

There's also a fair bit of overlap with SBOMCmd. I bet if we reorder things you can actually stick the if EnableOCIExperimental() at the end of SBOMCmd and call this something like writeSBOM.

var dig name.Digest
ref, err := name.ParseReference(imageRef, regOpts.NameOptions()...)
if err != nil {
return err
}
if digr, ok := ref.(name.Digest); ok {
dig = digr
} else {
desc, err := remote.Head(ref, regOpts.GetRegistryClientOpts(ctx)...)
if err != nil {
return err
}
dig = ref.Context().Digest(desc.Digest.String())
}

artifactType := ociexperimental.ArtifactType("sbom")

desc, err := remote.Head(dig, regOpts.GetRegistryClientOpts(ctx)...)
var terr *transport.Error
if errors.As(err, &terr) && terr.StatusCode == http.StatusNotFound {
h, err := v1.NewHash(dig.DigestStr())
if err != nil {
return err
}
// The subject doesn't exist, attach to it as if it's an empty OCI image.
logs.Progress.Println("subject doesn't exist, attaching to empty image")
desc = &v1.Descriptor{
ArtifactType: artifactType,
MediaType: ocitypes.OCIManifestSchema1,
Size: 0,
Digest: h,
}
} else if err != nil {
return err
}

b, err := sbomBytes(sbomRef)
if err != nil {
return err
}

empty := mutate.MediaType(
mutate.ConfigMediaType(empty.Image, ocitypes.MediaType(artifactType)),
ocitypes.OCIManifestSchema1)
att, err := mutate.AppendLayers(empty, ocistatic.NewLayer(b, sbomType))
if err != nil {
return err
}
att = mutate.Subject(att, *desc).(v1.Image)
attdig, err := att.Digest()
if err != nil {
return err
}
dstRef := ref.Context().Digest(attdig.String())

fmt.Fprintf(os.Stderr, "Uploading SBOM file for [%s] to [%s] with config.mediaType [%s] layers[0].mediaType [%s].\n",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ui.Infof

ref.Name(), dstRef.String(), artifactType, sbomType)
return remote.Write(dstRef, att, regOpts.GetRegistryClientOpts(ctx)...)
}

func sbomBytes(sbomRef string) ([]byte, error) {
// sbomRef can be "-", a string or a file.
switch signatureType(sbomRef) {
Expand Down
10 changes: 6 additions & 4 deletions cmd/cosign/cli/options/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,19 @@ func (o *AttachSignatureOptions) AddFlags(cmd *cobra.Command) {

// AttachSBOMOptions is the top level wrapper for the attach sbom command.
type AttachSBOMOptions struct {
SBOM string
SBOMType string
SBOMInputFormat string
Registry RegistryOptions
SBOM string
SBOMType string
SBOMInputFormat string
Registry RegistryOptions
RegistryExperimental RegistryExperimentalOptions
}

var _ Interface = (*AttachSBOMOptions)(nil)

// AddFlags implements Interface
func (o *AttachSBOMOptions) AddFlags(cmd *cobra.Command) {
o.Registry.AddFlags(cmd)
o.RegistryExperimental.AddFlags(cmd)

cmd.Flags().StringVar(&o.SBOM, "sbom", "",
"path to the sbom, or {-} for stdin")
Expand Down
46 changes: 46 additions & 0 deletions cmd/cosign/cli/options/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ package options
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net/http"

Expand Down Expand Up @@ -111,3 +113,47 @@ func (o *RegistryOptions) GetRegistryClientOpts(ctx context.Context) []remote.Op
}
return opts
}

type RegistryReferrersMode string

const (
RegistryReferrersModeLegacy RegistryReferrersMode = "legacy"
RegistryReferrersModeOCI11 RegistryReferrersMode = "oci-1-1"
)

func (e *RegistryReferrersMode) String() string {
return string(*e)
}

func (e *RegistryReferrersMode) Set(v string) error {
switch v {
case "legacy":
*e = RegistryReferrersMode(v)
return nil
case "oci-1-1":
if !EnableExperimental() {
return fmt.Errorf(`in order to use mode "%s", you must set COSIGN_EXPERIMENTAL=1`, v)
}
*e = RegistryReferrersMode(v)
return nil
default:
return errors.New(`must be one of "legacy", "oci-1-1"`)
}
}

func (e *RegistryReferrersMode) Type() string {
return "registryReferrersMode"
}

// RegistryExperimentalOptions is the wrapper for the registry experimental options.
type RegistryExperimentalOptions struct {
RegistryReferrersMode RegistryReferrersMode
}

var _ Interface = (*RegistryExperimentalOptions)(nil)

// AddFlags implements Interface
func (o *RegistryExperimentalOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().Var(&o.RegistryReferrersMode, "registry-referrers-mode",
"mode for fetching references from the registry. allowed: legacy, oci-1-1")
}
4 changes: 3 additions & 1 deletion cmd/cosign/cli/options/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ type SignOptions struct {
OIDC OIDCOptions
SecurityKey SecurityKeyOptions
AnnotationOptions
Registry RegistryOptions
Registry RegistryOptions
RegistryExperimental RegistryExperimentalOptions
}

var _ Interface = (*SignOptions)(nil)
Expand All @@ -54,6 +55,7 @@ func (o *SignOptions) AddFlags(cmd *cobra.Command) {
o.SecurityKey.AddFlags(cmd)
o.AnnotationOptions.AddFlags(cmd)
o.Registry.AddFlags(cmd)
o.RegistryExperimental.AddFlags(cmd)

cmd.Flags().StringVar(&o.Key, "key", "",
"path to the private key file, KMS URI or Kubernetes Secret")
Expand Down
12 changes: 9 additions & 3 deletions cmd/cosign/cli/sign/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ func SignCmd(ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignO
ErrDone = mutate.ErrSkipChildren
}
regOpts := signOpts.Registry
regExpOpts := signOpts.RegistryExperimental
opts, err := regOpts.ClientOpts(ctx)
if err != nil {
return fmt.Errorf("constructing client options: %w", err)
Expand All @@ -182,7 +183,7 @@ func SignCmd(ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignO
if err != nil {
return fmt.Errorf("accessing image: %w", err)
}
err = signDigest(ctx, digest, staticPayload, ko, regOpts, annotations, signOpts.Upload, signOpts.OutputSignature, signOpts.OutputCertificate, signOpts.Recursive, signOpts.TlogUpload, dd, sv, se)
err = signDigest(ctx, digest, staticPayload, ko, regOpts, regExpOpts, annotations, signOpts.Upload, signOpts.OutputSignature, signOpts.OutputCertificate, signOpts.Recursive, signOpts.TlogUpload, dd, sv, se)
if err != nil {
return fmt.Errorf("signing digest: %w", err)
}
Expand All @@ -201,7 +202,7 @@ func SignCmd(ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignO
return fmt.Errorf("computing digest: %w", err)
}
digest := ref.Context().Digest(d.String())
err = signDigest(ctx, digest, staticPayload, ko, regOpts, annotations, signOpts.Upload, signOpts.OutputSignature, signOpts.OutputCertificate, signOpts.Recursive, signOpts.TlogUpload, dd, sv, se)
err = signDigest(ctx, digest, staticPayload, ko, regOpts, regExpOpts, annotations, signOpts.Upload, signOpts.OutputSignature, signOpts.OutputCertificate, signOpts.Recursive, signOpts.TlogUpload, dd, sv, se)
if err != nil {
return fmt.Errorf("signing digest: %w", err)
}
Expand All @@ -215,7 +216,7 @@ func SignCmd(ro *options.RootOptions, ko options.KeyOpts, signOpts options.SignO
}

func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko options.KeyOpts,
regOpts options.RegistryOptions, annotations map[string]interface{}, upload bool, outputSignature, outputCertificate string, recursive bool, tlogUpload bool,
regOpts options.RegistryOptions, regExpOpts options.RegistryExperimentalOptions, annotations map[string]interface{}, upload bool, outputSignature, outputCertificate string, recursive bool, tlogUpload bool,
dd mutate.DupeDetector, sv *SignerVerifier, se oci.SignedEntity) error {
var err error
// The payload can be passed to skip generation.
Expand Down Expand Up @@ -312,6 +313,11 @@ func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko opti
ui.Infof(ctx, "Pushing signature to: %s", repo.RepositoryStr())
}

// Publish the signatures associated with this entity (using OCI 1.1+ behavior)
if regExpOpts.RegistryReferrersMode == options.RegistryReferrersModeOCI11 {
return ociremote.WriteSignaturesExperimentalOCI(digest, newSE, walkOpts...)
}

// Publish the signatures associated with this entity
if err := ociremote.WriteSignatures(digest.Repository, newSE, walkOpts...); err != nil {
return err
Expand Down
1 change: 1 addition & 0 deletions doc/cosign_attach_sbom.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions doc/cosign_sign.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ require (
github.com/go-piv/piv-go v1.10.0
github.com/google/certificate-transparency-go v1.1.4
github.com/google/go-cmp v0.5.9
github.com/google/go-containerregistry v0.13.0
github.com/google/go-containerregistry v0.13.1-0.20230203223142-b3c23b4c3f28
github.com/google/go-github/v50 v50.0.0
github.com/in-toto/in-toto-golang v0.6.0
github.com/kelseyhightower/envconfig v1.4.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -507,8 +507,8 @@ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.13.0 h1:y1C7Z3e149OJbOPDBxLYR8ITPz8dTKqQwjErKVHJC8k=
github.com/google/go-containerregistry v0.13.0/go.mod h1:J9FQ+eSS4a1aC2GNZxvNpbWhgp0487v+cgiilB4FqDo=
github.com/google/go-containerregistry v0.13.1-0.20230203223142-b3c23b4c3f28 h1:gFDKHwyCxpzgUozSOM8eLCx0V7muSr30QYU2QH+p48E=
github.com/google/go-containerregistry v0.13.1-0.20230203223142-b3c23b4c3f28/go.mod h1:J9FQ+eSS4a1aC2GNZxvNpbWhgp0487v+cgiilB4FqDo=
github.com/google/go-github/v50 v50.0.0 h1:gdO1AeuSZZK4iYWwVbjni7zg8PIQhp7QfmPunr016Jk=
github.com/google/go-github/v50 v50.0.0/go.mod h1:Ev4Tre8QoKiolvbpOSG3FIi4Mlon3S2Nt9W5JYqKiwA=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
Expand Down
25 changes: 25 additions & 0 deletions internal/pkg/oci/remote/remote.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// Copyright 2023 The Sigstore 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 remote

import (
"fmt"
)

// ArtifactType converts a attachment name (sig/sbom/att/etc.) into a valid artifactType (OCI 1.1+).
func ArtifactType(attName string) string {
return fmt.Sprintf("application/vnd.dev.cosign.artifact.%s.v1+json", attName)
}
Loading