Skip to content
Open
99 changes: 9 additions & 90 deletions cmd/cosign/cli/attest/attest_blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,7 @@ import (
"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"
protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1"
"github.com/sigstore/sigstore/pkg/signature"
"google.golang.org/protobuf/encoding/protojson"
)

// nolint
Expand All @@ -51,14 +48,7 @@ type AttestBlobCommand struct {
PredicatePath string
PredicateType string

TlogUpload bool
Timeout time.Duration

OutputSignature string
OutputAttestation string
OutputCertificate string

RekorEntryType string
Timeout time.Duration
}

// nolint
Expand All @@ -72,10 +62,6 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error
return fmt.Errorf("one of --predicate or --statement must be set")
}

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

if c.Timeout != 0 {
var cancelFn context.CancelFunc
ctx, cancelFn = context.WithTimeout(ctx, c.Timeout)
Expand Down Expand Up @@ -147,89 +133,22 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error
}

if c.SigningConfig == nil {
var err error
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, _, _, err := signcommon.NewAttestationBundle(ctx, c.KeyOpts, c.CertPath, c.CertChainPath, bundleOpts, c.SigningConfig, c.TrustedMaterial)
_, err = signcommon.ShouldUploadToTlog(ctx, c.KeyOpts, nil, len(c.SigningConfig.RekorLogURLs()) > 0)

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 is throwing away the result of ShouldUploadToTlog, so the attestation would be unconditionally uploaded to the transparency log.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Whether the attestation would be uploaded to the transparency log depends on:

  • The value of len(c.SigningConfig.RekorLogURLs()) > 0, which if true would enable the upload confirmation prompt.
  • Whether the user accepts the prompt. If they don't, execution stops with an error. If they do, the signing config goes to sigstore-go, which would upload because a log is present.

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.

Ah, so this is just about presenting the prompt? I see we do the same in sign/attest but override what's in the signing config, what's the reason for that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's to support users who want to sign a private container image without uploading to the transparency log.

If a user uses a signing config with a Rekor URL, but a private repository is detected:

  1. The user will be prompted to confirm that they wish to upload to Rekor despite the repository being private. If the user disagrees, shouldUpload returns false, and execution proceeds without a Rekor upload. If the user agrees...
  2. The user will receive the Immutable Records confirmation prompt. If the user disagrees, execution stops with an error. If the user agrees, shouldUpload returns true and the signing config goes to sigstore-go, which would upload because a log is present.

This matches current functionality.

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

if c.NewBundleFormat {
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)
return nil
}

var pb protobundle.Bundle
if err := protojson.Unmarshal(bundleBytes, &pb); err != nil {
return fmt.Errorf("unmarshalling bundle: %w", err)
return fmt.Errorf("should upload to tlog: %w", err)
}

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 err
}

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

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

if c.OutputSignature != "" {
if err := os.WriteFile(c.OutputSignature, bundleComponents.Signature, 0600); err != nil {
return fmt.Errorf("create signature file: %w", err)
}
fmt.Fprintf(os.Stderr, "Signature written in %s\n", c.OutputSignature)
} else {
fmt.Fprintln(os.Stdout, string(bundleComponents.Signature))
}

if c.OutputAttestation != "" {
if err := os.WriteFile(c.OutputAttestation, payload, 0600); err != nil {
return fmt.Errorf("create attestation file: %w", err)
}
fmt.Fprintf(os.Stderr, "Attestation written in %s\n", c.OutputAttestation)
}

if c.OutputCertificate != "" {
if len(bundleComponents.Certificates) == 0 {
return fmt.Errorf("no certificate found in bundle")
}
certPem, _ := signcommon.EncodeCertificatesToPEM(bundleComponents.Certificates)
if err := os.WriteFile(c.OutputCertificate, certPem, 0600); err != nil {
return fmt.Errorf("create certificate file: %w", err)
}
fmt.Fprintln(os.Stderr, "Certificate written to file ", c.OutputCertificate)
return fmt.Errorf("creating bundle: %w", err)
}

if c.RFC3161TimestampPath != "" {
if len(bundleComponents.RFC3161Timestamps) == 0 {
return fmt.Errorf("no RFC3161 timestamp found in bundle")
}
legacyTimestamp := cbundle.TimestampToRFC3161Timestamp(bundleComponents.RFC3161Timestamps[0].GetSignedTimestamp())
ts, err := json.Marshal(legacyTimestamp)
if err != nil {
return fmt.Errorf("marshalling timestamp: %w", err)
}
if err := os.WriteFile(c.RFC3161TimestampPath, ts, 0600); err != nil {
return fmt.Errorf("create timestamp file: %w", err)
}
fmt.Fprintln(os.Stderr, "Timestamp wrote in the file ", c.RFC3161TimestampPath)
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)

return nil
}
Expand Down
91 changes: 27 additions & 64 deletions cmd/cosign/cli/attest/attest_blob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ import (
"context"
"crypto"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"os"
"path/filepath"
Expand All @@ -30,6 +28,9 @@ import (

"errors"

"encoding/base64"
"encoding/json"

ssldsse "github.com/secure-systems-lab/go-securesystemslib/dsse"
"github.com/secure-systems-lab/go-securesystemslib/encrypted"
"github.com/sigstore/cosign/v3/cmd/cosign/cli/generate"
Expand Down Expand Up @@ -148,12 +149,6 @@ func TestAttestBlobCmdLocalKeyAndCert(t *testing.T) {
certref: subCertPem,
errString: "public key in certificate does not match the provided public key",
},
{
name: "cert chain matches key",
keyref: keyRef,
certref: certRef,
certchainref: subCertPem,
},
{
name: "cert chain partial",
keyref: keyRef,
Expand All @@ -175,18 +170,13 @@ func TestAttestBlobCmdLocalKeyAndCert(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
keyOpts := options.KeyOpts{KeyRef: tc.keyref}
if tc.newBundle {
keyOpts.NewBundleFormat = true
keyOpts.BundlePath = filepath.Join(td, "output.bundle")
}
keyOpts := options.KeyOpts{KeyRef: tc.keyref, BundlePath: filepath.Join(td, "output.bundle")}
at := AttestBlobCommand{
KeyOpts: keyOpts,
CertPath: tc.certref,
CertChainPath: tc.certchainref,
PredicatePath: predicatePath,
PredicateType: predicateType,
RekorEntryType: "dsse",
KeyOpts: keyOpts,
CertPath: tc.certref,
CertChainPath: tc.certchainref,
PredicatePath: predicatePath,
PredicateType: predicateType,
}
err := at.Exec(ctx, blob)
if err != nil {
Expand Down Expand Up @@ -228,25 +218,30 @@ func TestAttestBlob(t *testing.T) {

for predicateType, predicatePath := range predicates {
t.Run(predicateType, func(t *testing.T) {
dssePath := filepath.Join(td, "dsse.intoto.jsonl")
bundlePath := filepath.Join(td, "bundle.json")
keyOpts := options.KeyOpts{KeyRef: keyRef, BundlePath: bundlePath}
at := AttestBlobCommand{
KeyOpts: options.KeyOpts{KeyRef: keyRef},
PredicatePath: predicatePath,
PredicateType: predicateType,
OutputSignature: dssePath,
RekorEntryType: "dsse",
KeyOpts: keyOpts,
PredicatePath: predicatePath,
PredicateType: predicateType,
}
err := at.Exec(ctx, blobPath)
if err != nil {
t.Fatal(err)
}

// Load the attestation.
dsseBytes, _ := os.ReadFile(dssePath)
env := &ssldsse.Envelope{}
if err := json.Unmarshal(dsseBytes, env); err != nil {
// Load the attestation bundle.
bundleBytes, _ := os.ReadFile(bundlePath)
var bundleJSON struct {
DsseEnvelope *ssldsse.Envelope `json:"dsseEnvelope"`
}
if err := json.Unmarshal(bundleBytes, &bundleJSON); err != nil {
t.Fatal(err)
}
env := bundleJSON.DsseEnvelope
if env == nil {
t.Fatal("expected dsse envelope in bundle")
}

if len(env.Signatures) != 1 {
t.Fatalf("expected 1 signature, got %d", len(env.Signatures))
Expand Down Expand Up @@ -284,38 +279,6 @@ func TestAttestBlob(t *testing.T) {
}
}

func TestBadRekorEntryType(t *testing.T) {
ctx := context.Background()
td := t.TempDir()

keys, _ := cosign.GenerateKeyPair(nil)
keyRef := writeFile(t, td, string(keys.PrivateBytes), "key.pem")

blob := []byte("foo")
blobPath := writeFile(t, td, string(blob), "foo.txt")

predicates := map[string]string{}
predicates["slsaprovenance"] = makeSLSA02PredicateFile(t, td)
predicates["slsaprovenance1"] = makeSLSA1PredicateFile(t, td)

for predicateType, predicatePath := range predicates {
t.Run(predicateType, func(t *testing.T) {
dssePath := filepath.Join(td, "dsse.intoto.jsonl")
at := AttestBlobCommand{
KeyOpts: options.KeyOpts{KeyRef: keyRef},
PredicatePath: predicatePath,
PredicateType: predicateType,
OutputSignature: dssePath,
RekorEntryType: "badvalue",
}
err := at.Exec(ctx, blobPath)
if err == nil || err.Error() != "unknown value for rekor-entry-type" {
t.Fatal("expected an error due to unknown rekor entry type")
}
})
}
}

func TestStatementPath(t *testing.T) {
ctx := context.Background()
td := t.TempDir()
Expand All @@ -340,10 +303,10 @@ func TestStatementPath(t *testing.T) {
}`
statementPath := writeFile(t, td, statement, "statement.json")

keyOpts := options.KeyOpts{KeyRef: keyRef, BundlePath: filepath.Join(td, "bundle.json")}
at := AttestBlobCommand{
KeyOpts: options.KeyOpts{KeyRef: keyRef},
StatementPath: statementPath,
RekorEntryType: "dsse",
KeyOpts: keyOpts,
StatementPath: statementPath,
}
err := at.Exec(ctx, "")
assert.NoError(t, err)
Expand Down
Loading