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
77 changes: 75 additions & 2 deletions cmd/cosign/cli/attach/sbom.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,30 @@ 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"
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, sbomRef string, sbomType ocitypes.MediaType, imageRef string) error {
if options.EnableOCIExperimental() {
return sbomCmdOCIExperimental(ctx, regOpts, sbomRef, sbomType, imageRef)
}

ref, err := name.ParseReference(imageRef, regOpts.NameOptions()...)
if err != nil {
return err
Expand Down Expand Up @@ -60,6 +72,67 @@ func SBOMCmd(ctx context.Context, regOpts options.RegistryOptions, sbomRef strin
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
7 changes: 7 additions & 0 deletions cmd/cosign/cli/options/experimental.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,10 @@ func EnableExperimental() bool {
}
return false
}

func EnableOCIExperimental() bool {
if b, err := strconv.ParseBool(env.Getenv(env.VariableOCIExperimental)); err == nil {
return b
}
return false
}
7 changes: 7 additions & 0 deletions cmd/cosign/cli/sign/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"fmt"
"os"
"path/filepath"
"strconv"
"strings"

"github.com/google/go-containerregistry/pkg/name"
Expand All @@ -42,6 +43,7 @@ import (
"github.com/sigstore/cosign/v2/internal/pkg/cosign/tsa"
"github.com/sigstore/cosign/v2/internal/ui"
"github.com/sigstore/cosign/v2/pkg/cosign"
"github.com/sigstore/cosign/v2/pkg/cosign/env"
"github.com/sigstore/cosign/v2/pkg/cosign/pivkey"
"github.com/sigstore/cosign/v2/pkg/cosign/pkcs11key"
cremote "github.com/sigstore/cosign/v2/pkg/cosign/remote"
Expand Down Expand Up @@ -311,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 b, err := strconv.ParseBool(env.Getenv(env.VariableOCIExperimental)); err == nil && b {
Copy link
Contributor

Choose a reason for hiding this comment

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

Use exprimental.EnableOciWhatever()?

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
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.5.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)
}
6 changes: 6 additions & 0 deletions pkg/cosign/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const (
VariablePKCS11Pin Variable = "COSIGN_PKCS11_PIN"
VariablePKCS11ModulePath Variable = "COSIGN_PKCS11_MODULE_PATH"
VariableRepository Variable = "COSIGN_REPOSITORY"
VariableOCIExperimental Variable = "COSIGN_OCI_EXPERIMENTAL"

// Sigstore environment variables
VariableSigstoreCTLogPublicKeyFile Variable = "SIGSTORE_CT_LOG_PUBLIC_KEY_FILE"
Expand All @@ -75,6 +76,11 @@ var (
Expects: "1 if experimental features should be enabled (0 by default)",
Sensitive: false,
},
VariableOCIExperimental: {
Description: "enables experimental cosign features for OCI (1.1+)",
Copy link
Contributor

Choose a reason for hiding this comment

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

my preference is to make this a more specific flag

rather than "all experimental OCI features" it should be "experimental support for OCI referrers"

lumping many experiments under the same flag is why the cosign 2.0 effort is taking so long 😄

Expects: "1 if experimental OCI features should be enabled (0 by default)",
Sensitive: false,
},
VariableDockerMediaTypes: {
Description: "to be used with registries that do not support OCI media types",
Expects: "1 to fallback to legacy OCI media types equivalents (0 by default)",
Expand Down
65 changes: 65 additions & 0 deletions pkg/cosign/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ import (
"fmt"
"os"
"regexp"
"strconv"
"strings"
"time"

"github.com/digitorus/timestamp"
cbundle "github.com/sigstore/cosign/v2/pkg/cosign/bundle"
"github.com/sigstore/cosign/v2/pkg/cosign/env"
"github.com/sigstore/sigstore/pkg/tuf"

"github.com/sigstore/cosign/v2/pkg/blob"
Expand All @@ -46,6 +48,7 @@ import (
v1 "github.com/google/go-containerregistry/pkg/v1"

ssldsse "github.com/secure-systems-lab/go-securesystemslib/dsse"
ociexperimental "github.com/sigstore/cosign/v2/internal/pkg/oci/remote"
"github.com/sigstore/cosign/v2/pkg/oci"
"github.com/sigstore/cosign/v2/pkg/oci/layout"
ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
Expand Down Expand Up @@ -461,6 +464,14 @@ func (fos *fakeOCISignatures) Get() ([]oci.Signature, error) {
// VerifyImageSignatures does all the main cosign checks in a loop, returning the verified signatures.
// If there were no valid signatures, we return an error.
func VerifyImageSignatures(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) {
if b, err := strconv.ParseBool(env.Getenv(env.VariableOCIExperimental)); err == nil && b {
Copy link
Contributor

Choose a reason for hiding this comment

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

My preference is actually to make experimental solely a CLI concern. Then, we can add a field OciReferrers: bool or something to CheckOpts and rely on that.

Or even better to oci.Options (aka co.RegistryClientOpts)!

verified, bundleVerified, err := verifyImageSignaturesExperimentalOCI(ctx, signedImgRef, co)
if err == nil {
return verified, bundleVerified, nil
}
fmt.Println("Unable to locate sig attachment using digest tag, trying older scheme")
jdolitsky marked this conversation as resolved.
Show resolved Hide resolved
}

// Enforce this up front.
if co.RootCerts == nil && co.SigVerifier == nil {
return nil, false, errors.New("one of verifier or root certs is required")
Expand Down Expand Up @@ -1281,3 +1292,57 @@ func correctAnnotations(wanted, have map[string]interface{}) bool {
}
return true
}

// verifyImageSignaturesExperimentalOCI does all the main cosign checks in a loop, returning the verified signatures.
// If there were no valid signatures, we return an error, using OCI 1.1+ behavior.
func verifyImageSignaturesExperimentalOCI(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would prefer to label this as just verifyImageSignaturesReferrers

(You can mention that this support is experimental in a doc comment.)

znewman01 marked this conversation as resolved.
Show resolved Hide resolved
// Enforce this up front.
if co.RootCerts == nil && co.SigVerifier == nil {
return nil, false, errors.New("one of verifier or root certs is required")
}

// This is a carefully optimized sequence for fetching the signatures of the
// entity that minimizes registry requests when supplied with a digest input
digest, err := ociremote.ResolveDigest(signedImgRef, co.RegistryClientOpts...)
if err != nil {
return nil, false, err
}
h, err := v1.NewHash(digest.Identifier())
if err != nil {
return nil, false, err
}

var sigs oci.Signatures
sigRef := co.SignatureRef
if sigRef == "" {
artifactType := ociexperimental.ArtifactType("sig")
index, err := ociremote.Referrers(digest, artifactType, co.RegistryClientOpts...)
if err != nil {
return nil, false, err
}
results := index.Manifests
numResults := len(results)
if numResults == 0 {
return nil, false, fmt.Errorf("unable to locate reference with artifactType %s", artifactType)
} else if numResults > 1 {
// TODO: if there is more than 1 result.. what does that even mean?
fmt.Printf("WARNING: there were a total of %d references with artifactType %s\n", numResults, artifactType)
Copy link
Contributor

Choose a reason for hiding this comment

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

ui.Warnf

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe: "expected only 1" in the warning?

Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't a terribly actionable warning right now sadly. I guess because we don't know why the heck it would happen, ha

}
lastResult := results[numResults-1]
st, err := name.ParseReference(fmt.Sprintf("%s@%s", digest.Repository, lastResult.Digest.String()))
if err != nil {
return nil, false, err
}
sigs, err = ociremote.Signatures(st, co.RegistryClientOpts...)
if err != nil {
return nil, false, err
}
} else {
sigs, err = loadSignatureFromFile(sigRef, signedImgRef, co)
if err != nil {
return nil, false, err
}
}

return verifySignatures(ctx, sigs, h, co)
}
30 changes: 30 additions & 0 deletions pkg/oci/remote/referrers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// 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 (
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
)

// Referrers fetches references using registry options.
func Referrers(d name.Digest, artifactType string, opts ...Option) (*v1.IndexManifest, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Can we move this to an internal package? I don't think we want folks outside of cosign to use this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wasn't able to figure out a way to make this method non-public. It is used outside of this package, and unfortunately the options parser (makeOptions) is unavailable outside this package... so this will not reliably pass along registry options etc.

I'm open to other solutions, maybe even panicking if the method is called and the env var is unset 🤷

Copy link
Contributor

Choose a reason for hiding this comment

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

The rot runs deep.

I think I'm okay with a "this is subject to change, don't use this" in the docstring

o := makeOptions(name.Repository{}, opts...)
rOpt := o.ROpt
rOpt = append(rOpt, remote.WithFilter("artifactType", artifactType))
return remote.Referrers(d, rOpt...)
}
Loading