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

Feature: adds rekor support for cosign attach command #2994

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions cmd/cosign/cli/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func Attach() *cobra.Command {
attachSignature(),
attachSBOM(),
attachAttestation(),
attachRekorBundle(),
)

return cmd
Expand Down Expand Up @@ -109,3 +110,22 @@ func attachAttestation() *cobra.Command {

return cmd
}

func attachRekorBundle() *cobra.Command {
o := &options.AttachRekorOptions{}

cmd := &cobra.Command{
Use: "rekor",
Short: "Attach rekor bundles to the supplied container image",
Example: " cosign attach rekor <image uri>",
PersistentPreRun: options.BindViper,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return attach.RekorCmd(cmd.Context(), o.Registry, o.RekorURL, o.Signature, o.Payload, o.Cert, o.CertChain, args[0])
},
}

o.AddFlags(cmd)

return cmd
}
152 changes: 152 additions & 0 deletions cmd/cosign/cli/attach/rekor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
//
// 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 attach

import (
"context"
"crypto/sha256"
"errors"
"fmt"
"os"
"path/filepath"

"github.com/google/go-containerregistry/pkg/name"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/sign"
"github.com/sigstore/cosign/v2/pkg/cosign"
"github.com/sigstore/cosign/v2/pkg/cosign/bundle"
"github.com/sigstore/cosign/v2/pkg/oci/mutate"
ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote"
"github.com/sigstore/cosign/v2/pkg/oci/static"
"github.com/sigstore/rekor/pkg/generated/client"
"github.com/sigstore/rekor/pkg/generated/models"
)

type tlogUploadFn func(*client.Rekor, []byte) (*models.LogEntryAnon, error)

func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string, upload tlogUploadFn) (*bundle.RekorBundle, error) {
rekorBytes, err := sv.Bytes(ctx)
if err != nil {
return nil, err
}

rekorClient, err := rekor.NewClient(rekorURL)
if err != nil {
return nil, err
}
entry, err := upload(rekorClient, rekorBytes)
if err != nil {
return nil, err
}
fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex)
return bundle.EntryToBundle(entry), nil
}

func RekorCmd(ctx context.Context, regOpts options.RegistryOptions, rekorURL, sigRef, payloadRef, certRef, certChainRef, imageRef string) error {
b64SigBytes, err := signatureBytes(sigRef)
if err != nil {
return err
} else if len(b64SigBytes) == 0 {
return errors.New("empty signature")
}

ref, err := name.ParseReference(imageRef, regOpts.NameOptions()...)
if err != nil {
return err
}
ociremoteOpts, err := regOpts.ClientOpts(ctx)
if err != nil {
return err
}

digest, err := ociremote.ResolveDigest(ref, ociremoteOpts...)
if err != nil {
return err
}
// 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

var payload []byte
if payloadRef == "" {
payload, err = cosign.ObsoletePayload(ctx, digest)
} else {
payload, err = os.ReadFile(filepath.Clean(payloadRef))
}
if err != nil {
return err
}

sig, err := static.NewSignature(payload, string(b64SigBytes))
if err != nil {
return err
}

var cert []byte
var certChain []byte

if certRef != "" {
cert, err = os.ReadFile(filepath.Clean(certRef))
if err != nil {
return err
}
}

if certChainRef != "" {
certChain, err = os.ReadFile(filepath.Clean(certChainRef))
if err != nil {
return err
}
}

sv, err := sign.SignerFromKeyOpts(ctx, certRef, certChainRef, options.KeyOpts{})
if err != nil {
return fmt.Errorf("getting signer: %w", err)
}
defer sv.Close()

bundle, err := uploadToTlog(ctx, sv, rekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

A bundle should already exist, correct? This command, like attach signature, should take an existing bundle and attach it to the container metadata.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the review, Yes even I felt the same initially but I was skeptical and had little less idea being new to the code base on how we will get rekor Bundle struct back? What should the user pass as a flag? Should rekor-url be passed (like it is passed with attest command?) or should we straight up use sig.Bundle()to see if it exists or not and if it does then write/mutate signature using withBundle() opts ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A rekor bundle is usually in the format:

{"Bundle":{"SignedEntryTimestamp":"MEUCIG...yoIY=","Payload":{"body":"...","integratedTime":1643917737,"logIndex":1,"logID":"4d2e4...97291"}

My only doubt is how will we allow the user to pass it to be attached? Or it hasn't has to be passed at all? And we can leverage sig.Bundle() because it already exists but was not attached in containers manifest (sounds contradicting to me as I am unsure about the passing thing)

Copy link
Contributor

Choose a reason for hiding this comment

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

I would accept a path to a file with the bundle. sig.Bundle is where it's stored, and if it's set, it will be attached to the container.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got it so we will read that file and unmarshal it in the form of *bundle.RekorBundle and do something like this, right?:

	b, err := rekorBytes(rekorRef)
	if err != nil {
		return err
	}

	remoteOpts, err := regOpts.ClientOpts(ctx)
	if err != nil {
		return err
	}

	dstRef, err := ociremote.RekorTag(ref, remoteOpts...) //will have to create new func for RekorTag in ociremote 

	if err != nil {
		return err
	}

        //rough pseudo code only to tell the logic
	var bundle *bundle.RekorBundle
	json.Unmarshal(b, bundle)
	img, err := static.NewFile(b, static.WithBundle(bundle))
	if err != nil {
		return err
	}

checkSum := sha256.New()
if _, err := checkSum.Write(payload); err != nil {
return nil, err
}
return cosign.TLogUpload(ctx, r, b64SigBytes, checkSum, b)
})
if err != nil {
return err
}

recorSig, err := mutate.Signature(sig, mutate.WithCertChain(cert, certChain), mutate.WithBundle(bundle))
if err != nil {
return err
}

se, err := ociremote.SignedEntity(digest, ociremoteOpts...)
if err != nil {
return err
}

// Attach the signature to the entity.
newSE, err := mutate.AttachSignatureToEntity(se, recorSig)
if err != nil {
return err
}

// Publish the signatures associated with this entity
return ociremote.WriteSignatures(digest.Repository, newSE, ociremoteOpts...)
}
35 changes: 35 additions & 0 deletions cmd/cosign/cli/options/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,38 @@ func (o *AttachAttestationOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().StringArrayVarP(&o.Attestations, "attestation", "", nil,
"path to the attestation envelope")
}

// AttachRekorOptions is the top level wrapper for the attach rekor command.
type AttachRekorOptions struct {
RekorURL string
Signature string
Payload string
Cert string
CertChain string
Registry RegistryOptions
}

var _ Interface = (*AttachRekorOptions)(nil)

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

cmd.Flags().StringVar(&o.RekorURL, "rekor-url", "",
"path to the rekor-url, or {-} for stdin")

cmd.Flags().StringVar(&o.Signature, "signature", "",
"path to the signature, or {-} for stdin")

cmd.Flags().StringVar(&o.Payload, "payload", "",
"path to the payload covered by the signature")

cmd.Flags().StringVar(&o.Cert, "certificate", "",
"path to the X.509 certificate in PEM format to include in the OCI Signature")

cmd.Flags().StringVar(&o.CertChain, "certificate-chain", "",
"path to a list of CA X.509 certificates in PEM format which will be needed "+
"when building the certificate chain for the signing certificate. "+
"Must start with the parent intermediate CA certificate of the "+
"signing certificate and end with the root certificate. Included in the OCI Signature")
}
1 change: 1 addition & 0 deletions doc/cosign_attach.md

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

41 changes: 41 additions & 0 deletions doc/cosign_attach_rekor.md

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