Skip to content
Open
53 changes: 25 additions & 28 deletions cmd/cosign/cli/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func Attest() *cobra.Command {
cmd := &cobra.Command{
Use: "attest",
Short: "Attest the supplied container image",
Example: ` cosign attest --key <key path>|<kms uri> [--predicate <path>] [--a key=value] [--no-upload=true|false] [--record-creation-timestamp=true|false] [--f] [--r] <image uri>
Example: ` cosign attest --key <key path>|<kms uri> [--predicate <path>] [--no-upload=true|false] [--yes] <image uri>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

--key should be optional


# attach an attestation to a container image Google sign-in
cosign attest --timeout 90s --predicate <FILE> --type <TYPE> <IMAGE>
Expand Down Expand Up @@ -61,16 +61,24 @@ func Attest() *cobra.Command {
echo <PAYLOAD> | cosign attest --predicate - <IMAGE>

# write attestation to stdout
cosign attest --predicate <FILE> --type <TYPE> --key cosign.key --no-upload true <IMAGE>

# attach an attestation to a container image and honor the creation timestamp of the signature
cosign attest --predicate <FILE> --type <TYPE> --key cosign.key --record-creation-timestamp <IMAGE>`,
cosign attest --predicate <FILE> --type <TYPE> --key cosign.key --no-upload true <IMAGE>`,

Args: cobra.MinimumNArgs(1),
PersistentPreRun: options.BindViper,
PreRunE: func(_ *cobra.Command, _ []string) error {
if o.NewBundleFormat && o.NoUpload && o.BundlePath == "" {
return fmt.Errorf("must enable upload to the OCI registry or specify a local --bundle path with --new-bundle-format")
if o.NoUpload && o.BundlePath == "" {
return fmt.Errorf("must enable upload to the OCI registry or specify a local --bundle path")
}
var attestType string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same here, what's attestType for?

if o.Key == "" && !o.SecurityKey.Use {
attestType = "keyless"
} else if o.IssueCertificate {
attestType = "certificate-based"
}
if attestType != "" {
if !o.UseSigningConfig && o.SigningConfigPath == "" {
return fmt.Errorf("%s attesting requires a signing config (either from TUF via --use-signing-config or explicitly via a file with --signing-config)", attestType)
}
}
return nil
},
Expand All @@ -85,46 +93,35 @@ func Attest() *cobra.Command {
PassFunc: generate.GetPass,
Sk: o.SecurityKey.Use,
Slot: o.SecurityKey.Slot,
FulcioURL: o.Fulcio.URL,
IDToken: o.Fulcio.IdentityToken,
FulcioAuthFlow: o.Fulcio.AuthFlow,
InsecureSkipFulcioVerify: o.Fulcio.InsecureSkipFulcioVerify,
RekorURL: o.Rekor.URL,
OIDCIssuer: o.OIDC.Issuer,
OIDCClientID: o.OIDC.ClientID,
OIDCClientSecret: oidcClientSecret,
OIDCRedirectURL: o.OIDC.RedirectURL,
OIDCDisableProviders: o.OIDC.DisableAmbientProviders,
OIDCProvider: o.OIDC.Provider,
SkipConfirmation: o.SkipConfirmation,
TSAClientCACert: o.TSAClientCACert,
TSAClientKey: o.TSAClientKey,
TSAClientCert: o.TSAClientCert,
TSAServerName: o.TSAServerName,
TSAServerURL: o.TSAServerURL,
IssueCertificateForExistingKey: o.IssueCertificate,
BundlePath: o.BundlePath,
NewBundleFormat: o.NewBundleFormat,
}
if err := signcommon.LoadTrustedMaterialAndSigningConfig(cmd.Context(), &ko, o.UseSigningConfig, o.SigningConfigPath,
o.Rekor.URL, o.Fulcio.URL, o.OIDC.Issuer, o.TSAServerURL, o.TrustedRootPath, o.TlogUpload,
o.NewBundleFormat, "", o.Key, o.IssueCertificate,
"", "", "", "", "", ""); err != nil {
o.TrustedRootPath, o.Key); err != nil {
return err
}

attestCommand := attest.AttestCommand{
KeyOpts: ko,
RegistryOptions: o.Registry,
CertPath: o.Cert,
CertChainPath: o.CertChain,
NoUpload: o.NoUpload,
PredicatePath: o.Predicate.Path,
PredicateType: o.Predicate.Type,
Replace: o.Replace,
Timeout: ro.Timeout,
TlogUpload: o.TlogUpload,
RekorEntryType: o.RekorEntryType,
RecordCreationTimestamp: o.RecordCreationTimestamp,
KeyOpts: ko,
RegistryOptions: o.Registry,
CertPath: o.Cert,
CertChainPath: o.CertChain,
NoUpload: o.NoUpload,
PredicatePath: o.Predicate.Path,
PredicateType: o.Predicate.Type,
Timeout: ro.Timeout,
}

for _, img := range args {
Expand Down
136 changes: 19 additions & 117 deletions cmd/cosign/cli/attest/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,37 +24,24 @@ import (

"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"google.golang.org/protobuf/encoding/protojson"

"github.com/sigstore/cosign/v3/cmd/cosign/cli/options"
"github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon"
"github.com/sigstore/cosign/v3/internal/ui"
"github.com/sigstore/cosign/v3/pkg/cosign/attestation"
cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle"
cremote "github.com/sigstore/cosign/v3/pkg/cosign/remote"
"github.com/sigstore/cosign/v3/pkg/oci/mutate"
ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote"
"github.com/sigstore/cosign/v3/pkg/oci/static"
"github.com/sigstore/cosign/v3/pkg/types"
protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1"
"github.com/sigstore/sigstore/pkg/signature"
)

// nolint
type AttestCommand struct {
options.KeyOpts
options.RegistryOptions
CertPath string
CertChainPath string
NoUpload bool
PredicatePath string
PredicateType string
Replace bool
Timeout time.Duration
TlogUpload bool
TSAServerURL string
RekorEntryType string
RecordCreationTimestamp bool
CertPath string
CertChainPath string
NoUpload bool
PredicatePath string
PredicateType string
Timeout time.Duration
}

// nolint
Expand All @@ -68,10 +55,6 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error {
return fmt.Errorf("predicate cannot be empty")
}

if c.RekorEntryType != "dsse" && c.RekorEntryType != "intoto" {
return fmt.Errorf("unknown value for rekor-entry-type")
}

predicateURI, err := options.ParsePredicateType(c.PredicateType)
if err != nil {
return err
Expand Down Expand Up @@ -135,115 +118,34 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error {
}

if c.SigningConfig == nil {
c.SigningConfig, err = signcommon.NewSigningConfigFromKeyOpts(c.KeyOpts, c.TlogUpload)
if err != nil {
return fmt.Errorf("creating signing config: %w", err)
}
c.SigningConfig = signcommon.NewEmptySigningConfig()
}

bundleBytes, pubKey, hashAlgProto, err := signcommon.NewAttestationBundle(ctx, c.KeyOpts, c.CertPath, c.CertChainPath, bundleOpts, c.SigningConfig, c.TrustedMaterial)
shouldUpload, err := signcommon.ShouldUploadToTlog(ctx, c.KeyOpts, digest, len(c.SigningConfig.RekorLogURLs()) > 0)
if err != nil {
return fmt.Errorf("creating bundle: %w", err)
}

if c.NewBundleFormat {
if c.BundlePath != "" {
if err := os.WriteFile(c.BundlePath, bundleBytes, 0600); err != nil {
return fmt.Errorf("create bundle file: %w", err)
}
ui.Infof(ctx, "Wrote bundle to file %s", c.BundlePath)
}

if !c.NoUpload {
if err := ociremote.WriteAttestationNewBundleFormat(digest, bundleBytes, bundleOpts.PredicateType, ociremoteOpts...); err != nil {
return fmt.Errorf("writing bundle: %w", err)
}
}
return nil
return fmt.Errorf("should upload to tlog: %w", err)
}

var pb protobundle.Bundle
if err := protojson.Unmarshal(bundleBytes, &pb); err != nil {
return fmt.Errorf("unmarshalling bundle: %w", err)
if !shouldUpload {
c.SigningConfig.WithRekorLogURLs()
}

bundleComponents, err := signcommon.ExtractComponentsFromProtoBundle(&pb)
bundleBytes, err := signcommon.NewAttestationBundle(ctx, c.KeyOpts, c.CertPath, c.CertChainPath, bundleOpts, c.SigningConfig, c.TrustedMaterial)
if err != nil {
return fmt.Errorf("extracting components from bundle: %w", err)
}

legacyBundleBytes, err := signcommon.NewLegacyBundleFromProtoBundleComponents(bundleComponents)
if err != nil {
return fmt.Errorf("creating legacy bundle: %w", err)
return fmt.Errorf("creating bundle: %w", err)
}

if c.BundlePath != "" {
if err := os.WriteFile(c.BundlePath, legacyBundleBytes, 0600); err != nil {
if err := os.WriteFile(c.BundlePath, bundleBytes, 0600); err != nil {
return fmt.Errorf("create bundle file: %w", err)
}
ui.Infof(ctx, "Wrote bundle to file %s", c.BundlePath)
}

if c.NoUpload {
return nil
}

certPem, chainPem := signcommon.EncodeCertificatesToPEM(bundleComponents.Certificates)

opts := []static.Option{
static.WithLayerMediaType(types.DssePayloadType),
static.WithAnnotations(map[string]string{
"predicateType": predicateURI,
}),
}
if certPem != nil {
opts = append(opts, static.WithCertChain(certPem, chainPem))
}

if len(bundleComponents.RFC3161Timestamps) > 0 {
opts = append(opts, static.WithRFC3161Timestamp(cbundle.TimestampToRFC3161Timestamp(bundleComponents.RFC3161Timestamps[0].GetSignedTimestamp())))
}

predicateTypeAnnotation := map[string]string{
"predicateType": predicateURI,
}
// Add predicateType as manifest annotation
opts = append(opts, static.WithAnnotations(predicateTypeAnnotation))

if len(bundleComponents.RekorEntries) > 0 {
opts = append(opts, static.WithBundle(signcommon.RekorBundleFromProtoTlogEntry(bundleComponents.RekorEntries[0])))
}

ociSig, err := static.NewAttestation(bundleComponents.Signature, opts...)
if err != nil {
return fmt.Errorf("creating attestation: %w", err)
}

// We don't actually need to access the remote entity to attach things to it
// so we use a placeholder here.
se := ociremote.SignedUnknown(digest, ociremoteOpts...)

ddVerifier, err := signature.LoadVerifier(pubKey, signcommon.ProtoHashAlgoToHash(hashAlgProto))
if err != nil {
return fmt.Errorf("loading verifier: %w", err)
}
dd := cremote.NewDupeDetector(ddVerifier)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should be outdated at this point, but I just want to explicitly confirm that there's no reason to keep this around. Duplications aren't relevant because we're not attaching a signature multiple times to an annotation. I'm not certain why record creation timestamp was added though, that's the one that gives me pause.

cc @steiza

signOpts := []mutate.SignOption{
mutate.WithDupeDetector(dd),
mutate.WithRecordCreationTimestamp(c.RecordCreationTimestamp),
}

if c.Replace {
ro := cremote.NewReplaceOp(predicateURI)
signOpts = append(signOpts, mutate.WithReplaceOp(ro))
}

// Attach the attestation to the entity.
newSE, err := mutate.AttachAttestationToEntity(se, ociSig, signOpts...)
if err != nil {
return fmt.Errorf("attaching attestation: %w", err)
if !c.NoUpload {
if err := ociremote.WriteAttestationNewBundleFormat(digest, bundleBytes, bundleOpts.PredicateType, ociremoteOpts...); err != nil {
return fmt.Errorf("writing bundle: %w", err)
}
}

// Publish the attestations associated with this entity
return ociremote.WriteAttestations(digest.Repository, newSE, ociremoteOpts...)
return nil
}
Loading