diff --git a/cmd/cosign/cli/attest/attest.go b/cmd/cosign/cli/attest/attest.go index 473064b376f..c16f841a44b 100644 --- a/cmd/cosign/cli/attest/attest.go +++ b/cmd/cosign/cli/attest/attest.go @@ -22,27 +22,17 @@ import ( "encoding/json" "fmt" "os" - "time" - "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/pkg/errors" - - "github.com/sigstore/cosign/cmd/cosign/cli/options" "github.com/sigstore/cosign/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/cmd/cosign/cli/sign" "github.com/sigstore/cosign/pkg/cosign" "github.com/sigstore/cosign/pkg/cosign/attestation" + "github.com/sigstore/cosign/pkg/cosign/bundle" cbundle "github.com/sigstore/cosign/pkg/cosign/bundle" - cremote "github.com/sigstore/cosign/pkg/cosign/remote" - "github.com/sigstore/cosign/pkg/oci/mutate" - ociremote "github.com/sigstore/cosign/pkg/oci/remote" - "github.com/sigstore/cosign/pkg/oci/static" sigs "github.com/sigstore/cosign/pkg/signature" - "github.com/sigstore/cosign/pkg/types" "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/generated/models" - "github.com/sigstore/sigstore/pkg/signature/dsse" + "github.com/sigstore/sigstore/pkg/signature" signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" ) @@ -73,130 +63,28 @@ func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string, return cbundle.EntryToBundle(entry), nil } -//nolint -func AttestCmd(ctx context.Context, ko options.KeyOpts, regOpts options.RegistryOptions, imageRef string, certPath string, certChainPath string, - noUpload bool, predicatePath string, force bool, predicateType string, replace bool, timeout time.Duration) error { - // A key file or token is required unless we're in experimental mode! - if options.EnableExperimental() { - if options.NOf(ko.KeyRef, ko.Sk) > 1 { - return &options.KeyParseError{} - } - } else { - if !options.OneOf(ko.KeyRef, ko.Sk) { - return &options.KeyParseError{} - } - } - - predicateURI, err := options.ParsePredicateType(predicateType) - if err != nil { - return err - } - - ref, err := name.ParseReference(imageRef) - if err != nil { - return errors.Wrap(err, "parsing reference") - } - - if timeout != 0 { - var cancelFn context.CancelFunc - ctx, cancelFn = context.WithTimeout(ctx, timeout) - defer cancelFn() - } - - ociremoteOpts, err := regOpts.ClientOpts(ctx) - if err != nil { - return err - } - digest, err := ociremote.ResolveDigest(ref, ociremoteOpts...) - if err != nil { - return err - } - h, _ := v1.NewHash(digest.Identifier()) - // Overwrite "ref" with a digest to avoid a race where we use a tag - // multiple times, and it potentially points to different things at - // each access. - ref = digest // nolint - - sv, err := sign.SignerFromKeyOpts(ctx, certPath, certChainPath, ko) - if err != nil { - return errors.Wrap(err, "getting signer") - } - defer sv.Close() - wrapped := dsse.WrapSigner(sv, types.IntotoPayloadType) - dd := cremote.NewDupeDetector(sv) - - fmt.Fprintln(os.Stderr, "Using payload from:", predicatePath) - predicate, err := os.Open(predicatePath) - if err != nil { - return err - } - defer predicate.Close() +func getSignedPayload(ctx context.Context, wrapped signature.Signer, predicate *os.File, predicatePath string, + predicateType string, hexDigest string, repo string) ([]byte, error) { sh, err := attestation.GenerateStatement(attestation.GenerateOpts{ Predicate: predicate, Type: predicateType, - Digest: h.Hex, - Repo: digest.Repository.String(), + Digest: hexDigest, + Repo: repo, }) if err != nil { - return err + return nil, err } payload, err := json.Marshal(sh) if err != nil { - return err - } - signedPayload, err := wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx)) - if err != nil { - return errors.Wrap(err, "signing") - } - - if noUpload { - fmt.Println(string(signedPayload)) - return nil - } - - opts := []static.Option{static.WithLayerMediaType(types.DssePayloadType)} - if sv.Cert != nil { - opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain)) - } - - // Check whether we should be uploading to the transparency log - if sign.ShouldUploadToTlog(ctx, digest, force, ko.RekorURL) { - bundle, err := uploadToTlog(ctx, sv, ko.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) { - return cosign.TLogUploadInTotoAttestation(ctx, r, signedPayload, b) - }) - if err != nil { - return err - } - opts = append(opts, static.WithBundle(bundle)) - } - - sig, err := static.NewAttestation(signedPayload, opts...) - if err != nil { - return err - } - - se, err := ociremote.SignedEntity(digest, ociremoteOpts...) - if err != nil { - return err - } - - signOpts := []mutate.SignOption{ - mutate.WithDupeDetector(dd), - } - - if replace { - ro := cremote.NewReplaceOp(predicateURI) - signOpts = append(signOpts, mutate.WithReplaceOp(ro)) - } - - // Attach the attestation to the entity. - newSE, err := mutate.AttachAttestationToEntity(se, sig, signOpts...) - if err != nil { - return err + return nil, err } + return wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx)) +} - // Publish the attestations associated with this entity - return ociremote.WriteAttestations(digest.Repository, newSE, ociremoteOpts...) +func attest(ctx context.Context, sv *sign.SignerVerifier, signedPayload []byte, rekorURL string) (*bundle.RekorBundle, error) { + return uploadToTlog(ctx, sv, rekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) { + return cosign.TLogUploadInTotoAttestation(ctx, r, signedPayload, b) + }) } diff --git a/cmd/cosign/cli/attest/attest_blob.go b/cmd/cosign/cli/attest/attest_blob.go index 3ea3e67dff7..c9d0a2a1d0e 100644 --- a/cmd/cosign/cli/attest/attest_blob.go +++ b/cmd/cosign/cli/attest/attest_blob.go @@ -5,7 +5,6 @@ import ( "context" "crypto" "encoding/hex" - "encoding/json" "fmt" "io" "os" @@ -17,18 +16,15 @@ import ( "github.com/pkg/errors" "github.com/sigstore/cosign/cmd/cosign/cli/options" "github.com/sigstore/cosign/cmd/cosign/cli/sign" - "github.com/sigstore/cosign/pkg/cosign" - "github.com/sigstore/cosign/pkg/cosign/attestation" - "github.com/sigstore/cosign/pkg/oci/static" "github.com/sigstore/cosign/pkg/types" - "github.com/sigstore/rekor/pkg/generated/client" - "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/dsse" - signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" ) -func AttestBlobCmd(ctx context.Context, ko options.KeyOpts, artifactPath string, artifactHash string, certPath string, certChainPath string, noUpload bool, predicatePath string, force bool, predicateType string, replace bool, timeout time.Duration) error { +// AttestBlobCmd implements the logic to attach attestation for a specified blob +func AttestBlobCmd(ctx context.Context, ko options.KeyOpts, artifactPath string, artifactHash string, certPath string, + certChainPath string, noUpload bool, predicatePath string, predicateType string, timeout time.Duration) error { + // A key file or token is required unless we're in experimental mode! if options.EnableExperimental() { if options.NOf(ko.KeyRef, ko.Sk) > 1 { @@ -60,6 +56,16 @@ func AttestBlobCmd(ctx context.Context, ko options.KeyOpts, artifactPath string, } } + if artifactHash == "" { + digest, _, err := signature.ComputeDigestForSigning(bytes.NewReader(artifact), crypto.SHA256, []crypto.Hash{crypto.SHA256, crypto.SHA384}) + if err != nil { + return err + } + hexDigest = strings.ToLower(hex.EncodeToString(digest)) + } else { + hexDigest = artifactHash + } + sv, err := sign.SignerFromKeyOpts(ctx, certPath, certChainPath, ko) if err != nil { return errors.Wrap(err, "getting signer") @@ -69,15 +75,6 @@ func AttestBlobCmd(ctx context.Context, ko options.KeyOpts, artifactPath string, if err != nil { return err } - /*pem, err := cryptoutils.MarshalPublicKeyToPEM(pub) - if err != nil { - return errors.Wrap(err, "key to pem") - }*/ - - predicateURI, err := options.ParsePredicateType(predicateType) - if err != nil { - return err - } if timeout != 0 { var cancelFn context.CancelFunc @@ -85,15 +82,6 @@ func AttestBlobCmd(ctx context.Context, ko options.KeyOpts, artifactPath string, defer cancelFn() } - if artifactHash == "" { - digest, _, err := signature.ComputeDigestForSigning(bytes.NewReader(artifact), crypto.SHA256, []crypto.Hash{crypto.SHA256, crypto.SHA384}) - if err != nil { - return err - } - hexDigest = strings.ToLower(hex.EncodeToString(digest)) - } else { - hexDigest = artifactHash - } wrapped := dsse.WrapSigner(sv, types.IntotoPayloadType) fmt.Fprintln(os.Stderr, "Using payload from:", predicatePath) @@ -105,21 +93,7 @@ func AttestBlobCmd(ctx context.Context, ko options.KeyOpts, artifactPath string, base := path.Base(artifactPath) - sh, err := attestation.GenerateStatement(attestation.GenerateOpts{ - Predicate: predicate, - Type: predicateType, - Digest: hexDigest, - Repo: base, - }) - if err != nil { - return err - } - - payload, err := json.Marshal(sh) - if err != nil { - return err - } - signedPayload, err := wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(ctx)) + signedPayload, err := getSignedPayload(ctx, wrapped, predicate, predicatePath, predicateType, hexDigest, base) if err != nil { return errors.Wrap(err, "signing") } @@ -129,30 +103,13 @@ func AttestBlobCmd(ctx context.Context, ko options.KeyOpts, artifactPath string, return nil } - opts := []static.Option{static.WithLayerMediaType(types.DssePayloadType)} - if sv.Cert != nil { - opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain)) - } - // Check whether we should be uploading to the transparency log if options.EnableExperimental() { fmt.Println("Uploading to Rekor") - /*r, err := rc.GetRekorClient(ko.RekorURL) - if err != nil { - return err - }*/ - _, err := uploadToTlog(ctx, sv, ko.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) { - return cosign.TLogUploadInTotoAttestation(ctx, r, signedPayload, b) - }) + _, err := attest(ctx, sv, signedPayload, ko.RekorURL) if err != nil { return err } - /*l, err := cosign.TLogUploadInTotoAttestation(ctx, r, signedPayload, pem) - if err != nil { - return err - }*/ - - //fmt.Fprintln(os.Stderr, "Log id:", *bundle.LogIndex) } return err } diff --git a/cmd/cosign/cli/attest/attest_image.go b/cmd/cosign/cli/attest/attest_image.go new file mode 100644 index 00000000000..253584a2446 --- /dev/null +++ b/cmd/cosign/cli/attest/attest_image.go @@ -0,0 +1,149 @@ +// +// Copyright 2021 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 attest + +import ( + "context" + _ "crypto/sha256" // for `crypto.SHA256` + "fmt" + "os" + "time" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/pkg/errors" + + "github.com/sigstore/cosign/cmd/cosign/cli/options" + "github.com/sigstore/cosign/cmd/cosign/cli/sign" + cremote "github.com/sigstore/cosign/pkg/cosign/remote" + "github.com/sigstore/cosign/pkg/oci/mutate" + ociremote "github.com/sigstore/cosign/pkg/oci/remote" + "github.com/sigstore/cosign/pkg/oci/static" + "github.com/sigstore/cosign/pkg/types" + "github.com/sigstore/sigstore/pkg/signature/dsse" +) + +//nolint +func AttestCmd(ctx context.Context, ko options.KeyOpts, regOpts options.RegistryOptions, imageRef string, certPath string, certChainPath string, + noUpload bool, predicatePath string, force bool, predicateType string, replace bool, timeout time.Duration) error { + // A key file or token is required unless we're in experimental mode! + if options.EnableExperimental() { + if options.NOf(ko.KeyRef, ko.Sk) > 1 { + return &options.KeyParseError{} + } + } else { + if !options.OneOf(ko.KeyRef, ko.Sk) { + return &options.KeyParseError{} + } + } + + predicateURI, err := options.ParsePredicateType(predicateType) + if err != nil { + return err + } + + ref, err := name.ParseReference(imageRef) + if err != nil { + return errors.Wrap(err, "parsing reference") + } + + if timeout != 0 { + var cancelFn context.CancelFunc + ctx, cancelFn = context.WithTimeout(ctx, timeout) + defer cancelFn() + } + + ociremoteOpts, err := regOpts.ClientOpts(ctx) + if err != nil { + return err + } + digest, err := ociremote.ResolveDigest(ref, ociremoteOpts...) + if err != nil { + return err + } + h, _ := v1.NewHash(digest.Identifier()) + // Overwrite "ref" with a digest to avoid a race where we use a tag + // multiple times, and it potentially points to different things at + // each access. + ref = digest // nolint + + sv, err := sign.SignerFromKeyOpts(ctx, certPath, certChainPath, ko) + if err != nil { + return errors.Wrap(err, "getting signer") + } + defer sv.Close() + wrapped := dsse.WrapSigner(sv, types.IntotoPayloadType) + dd := cremote.NewDupeDetector(sv) + + fmt.Fprintln(os.Stderr, "Using payload from:", predicatePath) + predicate, err := os.Open(predicatePath) + if err != nil { + return err + } + defer predicate.Close() + + signedPayload, err := getSignedPayload(ctx, wrapped, predicate, predicatePath, predicateType, h.Hex, digest.Repository.String()) + if err != nil { + return errors.Wrap(err, "signing") + } + + if noUpload { + fmt.Println(string(signedPayload)) + return nil + } + + opts := []static.Option{static.WithLayerMediaType(types.DssePayloadType)} + if sv.Cert != nil { + opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain)) + } + + // Check whether we should be uploading to the transparency log + if sign.ShouldUploadToTlog(ctx, digest, force, ko.RekorURL) { + bundle, err := attest(ctx, sv, signedPayload, ko.RekorURL) + if err != nil { + return err + } + opts = append(opts, static.WithBundle(bundle)) + } + + sig, err := static.NewAttestation(signedPayload, opts...) + if err != nil { + return err + } + + se, err := ociremote.SignedEntity(digest, ociremoteOpts...) + if err != nil { + return err + } + + signOpts := []mutate.SignOption{ + mutate.WithDupeDetector(dd), + } + + if replace { + ro := cremote.NewReplaceOp(predicateURI) + signOpts = append(signOpts, mutate.WithReplaceOp(ro)) + } + + // Attach the attestation to the entity. + newSE, err := mutate.AttachAttestationToEntity(se, sig, signOpts...) + if err != nil { + return err + } + + // Publish the attestations associated with this entity + return ociremote.WriteAttestations(digest.Repository, newSE, ociremoteOpts...) +} diff --git a/cmd/cosign/cli/attestblob.go b/cmd/cosign/cli/attestblob.go index f3c5407b2fb..4db13b61f64 100644 --- a/cmd/cosign/cli/attestblob.go +++ b/cmd/cosign/cli/attestblob.go @@ -56,7 +56,7 @@ func AttestBlob() *cobra.Command { } for _, artifact := range args { if err := attest.AttestBlobCmd(cmd.Context(), ko, artifact, o.Hash, o.Cert, o.CertChain, o.NoUpload, - o.Predicate.Path, o.Force, o.Predicate.Type, o.Replace, ro.Timeout); err != nil { + o.Predicate.Path, o.Predicate.Type, ro.Timeout); err != nil { return errors.Wrapf(err, "attesting %s", artifact) } } diff --git a/cmd/cosign/cli/options/attestblob.go b/cmd/cosign/cli/options/attestblob.go index 2f8c784ffd2..0c9013ecd8b 100644 --- a/cmd/cosign/cli/options/attestblob.go +++ b/cmd/cosign/cli/options/attestblob.go @@ -27,9 +27,7 @@ type AttestBlobOptions struct { Cert string CertChain string NoUpload bool - Force bool Recursive bool - Replace bool Timeout time.Duration Hash string @@ -65,12 +63,6 @@ func (o *AttestBlobOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().BoolVar(&o.NoUpload, "no-upload", false, "do not upload the generated attestation") - cmd.Flags().BoolVarP(&o.Force, "force", "f", false, - "skip warnings and confirmations") - - cmd.Flags().BoolVarP(&o.Replace, "replace", "", false, - "") - cmd.Flags().DurationVar(&o.Timeout, "timeout", time.Second*30, "HTTP Timeout defaults to 30 seconds")