-
Notifications
You must be signed in to change notification settings - Fork 545
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
Changes from 4 commits
d118235
9dee558
42868b1
a2542ea
4c18c95
6120b11
af86ae9
4bd1d04
2ce0564
e899a84
e79a1c2
d337642
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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 { | ||
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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,7 @@ import ( | |
"fmt" | ||
"os" | ||
"path/filepath" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/google/go-containerregistry/pkg/name" | ||
|
@@ -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" | ||
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
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 | ||
|
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) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
|
@@ -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+)", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
|
@@ -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" | ||
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Or even better to |
||
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") | ||
|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would prefer to label this as just (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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe: "expected only 1" in the warning? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} |
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 🤷 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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...) | ||
} |
There was a problem hiding this comment.
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 intopkg/
?There's also a fair bit of overlap with
SBOMCmd
. I bet if we reorder things you can actually stick theif EnableOCIExperimental()
at the end ofSBOMCmd
and call this something likewriteSBOM
.