Skip to content

Commit

Permalink
Adding protobuf bundle support to sign-blob and attest-blob (#3752)
Browse files Browse the repository at this point in the history
This pull requests addresses the first part of #3139: adding protobuf bundle support for cosign sign-blob and cosign attest-blob.

Signed-off-by: Zach Steindler <steiza@github.com>
  • Loading branch information
steiza committed Jul 23, 2024
1 parent ffde21e commit c6cdf1b
Show file tree
Hide file tree
Showing 15 changed files with 448 additions and 58 deletions.
130 changes: 106 additions & 24 deletions cmd/cosign/cli/attest/attest_blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"bytes"
"context"
"crypto"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
Expand All @@ -30,6 +32,9 @@ import (
"time"

"github.com/pkg/errors"
"github.com/secure-systems-lab/go-securesystemslib/dsse"
"google.golang.org/protobuf/encoding/protojson"

"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"
Expand All @@ -39,10 +44,12 @@ import (
"github.com/sigstore/cosign/v2/pkg/cosign/attestation"
cbundle "github.com/sigstore/cosign/v2/pkg/cosign/bundle"
"github.com/sigstore/cosign/v2/pkg/types"
protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1"
protodsse "github.com/sigstore/protobuf-specs/gen/pb-go/dsse"
"github.com/sigstore/rekor/pkg/generated/models"
"github.com/sigstore/sigstore/pkg/cryptoutils"
"github.com/sigstore/sigstore/pkg/signature"
"github.com/sigstore/sigstore/pkg/signature/dsse"
sigstoredsse "github.com/sigstore/sigstore/pkg/signature/dsse"
signatureoptions "github.com/sigstore/sigstore/pkg/signature/options"
)

Expand Down Expand Up @@ -88,8 +95,8 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error
defer cancelFn()
}

if c.TSAServerURL != "" && c.RFC3161TimestampPath == "" {
return errors.New("expected an rfc3161-timestamp path when using a TSA server")
if c.TSAServerURL != "" && c.RFC3161TimestampPath == "" && !c.NewBundleFormat {
return errors.New("expected either new bundle or an rfc3161-timestamp path when using a TSA server")
}

var artifact []byte
Expand Down Expand Up @@ -129,7 +136,7 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error
return fmt.Errorf("getting signer: %w", err)
}
defer sv.Close()
wrapped := dsse.WrapSigner(sv, types.IntotoPayloadType)
wrapped := sigstoredsse.WrapSigner(sv, types.IntotoPayloadType)

base := path.Base(artifactPath)

Expand All @@ -154,28 +161,34 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error
}

var rfc3161Timestamp *cbundle.RFC3161Timestamp
var timestampBytes []byte
var rekorEntry *models.LogEntryAnon

if c.TSAServerURL != "" {
respBytes, err := tsa.GetTimestampedSignature(sig, client.NewTSAClient(c.TSAServerURL))
timestampBytes, err = tsa.GetTimestampedSignature(sig, client.NewTSAClient(c.TSAServerURL))
if err != nil {
return err
}
rfc3161Timestamp = cbundle.TimestampToRFC3161Timestamp(respBytes)
rfc3161Timestamp = cbundle.TimestampToRFC3161Timestamp(timestampBytes)
// TODO: Consider uploading RFC3161 TS to Rekor

if rfc3161Timestamp == nil {
return fmt.Errorf("rfc3161 timestamp is nil")
}
ts, err := json.Marshal(rfc3161Timestamp)
if err != nil {
return err
}
if err := os.WriteFile(c.RFC3161TimestampPath, ts, 0600); err != nil {
return fmt.Errorf("create RFC3161 timestamp file: %w", err)

if c.RFC3161TimestampPath != "" {
ts, err := json.Marshal(rfc3161Timestamp)
if err != nil {
return err
}
if err := os.WriteFile(c.RFC3161TimestampPath, ts, 0600); err != nil {
return fmt.Errorf("create RFC3161 timestamp file: %w", err)
}
fmt.Fprintln(os.Stderr, "RFC3161 timestamp bundle written to file ", c.RFC3161TimestampPath)
}
fmt.Fprintln(os.Stderr, "RFC3161 timestamp bundle written to file ", c.RFC3161TimestampPath)
}

rekorBytes, err := sv.Bytes(ctx)
signer, err := sv.Bytes(ctx)
if err != nil {
return err
}
Expand All @@ -189,28 +202,36 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error
if err != nil {
return err
}
var entry *models.LogEntryAnon
if c.RekorEntryType == "intoto" {
entry, err = cosign.TLogUploadInTotoAttestation(ctx, rekorClient, sig, rekorBytes)
rekorEntry, err = cosign.TLogUploadInTotoAttestation(ctx, rekorClient, sig, signer)
} else {
entry, err = cosign.TLogUploadDSSEEnvelope(ctx, rekorClient, sig, rekorBytes)
rekorEntry, err = cosign.TLogUploadDSSEEnvelope(ctx, rekorClient, sig, signer)
}

if err != nil {
return err
}
fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex)
signedPayload.Bundle = cbundle.EntryToBundle(entry)
fmt.Fprintln(os.Stderr, "tlog entry created with index:", *rekorEntry.LogIndex)
signedPayload.Bundle = cbundle.EntryToBundle(rekorEntry)
}

if c.BundlePath != "" {
signedPayload.Base64Signature = base64.StdEncoding.EncodeToString(sig)
signedPayload.Cert = base64.StdEncoding.EncodeToString(rekorBytes)
var contents []byte
if c.NewBundleFormat {
contents, err = makeNewBundle(sv, rekorEntry, payload, sig, signer, timestampBytes)
if err != nil {
return err
}
} else {
signedPayload.Base64Signature = base64.StdEncoding.EncodeToString(sig)
signedPayload.Cert = base64.StdEncoding.EncodeToString(signer)

contents, err := json.Marshal(signedPayload)
if err != nil {
return err
contents, err = json.Marshal(signedPayload)
if err != nil {
return err
}
}

if err := os.WriteFile(c.BundlePath, contents, 0600); err != nil {
return fmt.Errorf("create bundle file: %w", err)
}
Expand Down Expand Up @@ -258,3 +279,64 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error

return nil
}

func makeNewBundle(sv *sign.SignerVerifier, rekorEntry *models.LogEntryAnon, payload, sig, signer, timestampBytes []byte) ([]byte, error) {
// Determine if signature is certificate or not
var hint string
var rawCert []byte

cert, err := cryptoutils.UnmarshalCertificatesFromPEM(signer)
if err != nil || len(cert) == 0 {
pubKey, err := sv.PublicKey()
if err != nil {
return nil, err
}
pkixPubKey, err := x509.MarshalPKIXPublicKey(pubKey)
if err != nil {
return nil, err
}
hashedBytes := sha256.Sum256(pkixPubKey)
hint = base64.StdEncoding.EncodeToString(hashedBytes[:])
} else {
rawCert = cert[0].Raw
}

bundle, err := cbundle.MakeProtobufBundle(hint, rawCert, rekorEntry, timestampBytes)
if err != nil {
return nil, err
}

var envelope dsse.Envelope
err = json.Unmarshal(sig, &envelope)
if err != nil {
return nil, err
}

if len(envelope.Signatures) == 0 {
return nil, fmt.Errorf("no signature in DSSE envelope")
}

sigBytes, err := base64.StdEncoding.DecodeString(envelope.Signatures[0].Sig)
if err != nil {
return nil, err
}

bundle.Content = &protobundle.Bundle_DsseEnvelope{
DsseEnvelope: &protodsse.Envelope{
Payload: payload,
PayloadType: envelope.PayloadType,
Signatures: []*protodsse.Signature{
{
Sig: sigBytes,
},
},
},
}

contents, err := protojson.Marshal(bundle)
if err != nil {
return nil, err
}

return contents, nil
}
13 changes: 12 additions & 1 deletion cmd/cosign/cli/attest/attest_blob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ func TestAttestBlobCmdLocalKeyAndCert(t *testing.T) {
keyref string
certref string
certchainref string
newBundle bool
errString string
}{
{
Expand All @@ -133,6 +134,12 @@ func TestAttestBlobCmdLocalKeyAndCert(t *testing.T) {
keyref: keyRef,
certref: certRef,
},
{
name: "new bundle generation",
keyref: keyRef,
certref: certRef,
newBundle: true,
},
{
name: "fail: cert no match key",
keyref: keyRef,
Expand Down Expand Up @@ -160,8 +167,12 @@ 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
}
at := AttestBlobCommand{
KeyOpts: options.KeyOpts{KeyRef: tc.keyref},
KeyOpts: keyOpts,
CertPath: tc.certref,
CertChainPath: tc.certchainref,
PredicatePath: predicatePath,
Expand Down
1 change: 1 addition & 0 deletions cmd/cosign/cli/attest_blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func AttestBlob() *cobra.Command {
TSAServerURL: o.TSAServerURL,
RFC3161TimestampPath: o.RFC3161TimestampPath,
BundlePath: o.BundlePath,
NewBundleFormat: o.NewBundleFormat,
}
v := attest.AttestBlobCommand{
KeyOpts: ko,
Expand Down
5 changes: 5 additions & 0 deletions cmd/cosign/cli/options/attest_blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type AttestBlobOptions struct {
OutputAttestation string
OutputCertificate string
BundlePath string
NewBundleFormat bool

RekorEntryType string

Expand Down Expand Up @@ -85,6 +86,10 @@ func (o *AttestBlobOptions) AddFlags(cmd *cobra.Command) {
"write everything required to verify the blob to a FILE")
_ = cmd.Flags().SetAnnotation("bundle", cobra.BashCompFilenameExt, []string{})

// TODO: have this default to true as a breaking change
cmd.Flags().BoolVar(&o.NewBundleFormat, "new-bundle-format", false,
"output bundle in new format that contains all verification material")

cmd.Flags().StringVar(&o.Hash, "hash", "",
"hash of blob in hexadecimal (base16). Used if you want to sign an artifact stored elsewhere and have the hash")

Expand Down
1 change: 1 addition & 0 deletions cmd/cosign/cli/options/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type KeyOpts struct {
OIDCDisableProviders bool // Disable OIDC credential providers in keyless signer
OIDCProvider string // Specify which OIDC credential provider to use for keyless signer
BundlePath string
NewBundleFormat bool
SkipConfirmation bool
TSAClientCACert string
TSAClientCert string
Expand Down
5 changes: 5 additions & 0 deletions cmd/cosign/cli/options/signblob.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type SignBlobOptions struct {
OIDC OIDCOptions
Registry RegistryOptions
BundlePath string
NewBundleFormat bool
SkipConfirmation bool
TlogUpload bool
TSAClientCACert string
Expand Down Expand Up @@ -75,6 +76,10 @@ func (o *SignBlobOptions) AddFlags(cmd *cobra.Command) {
"write everything required to verify the blob to a FILE")
_ = cmd.Flags().SetAnnotation("bundle", cobra.BashCompFilenameExt, []string{})

// TODO: have this default to true as a breaking change
cmd.Flags().BoolVar(&o.NewBundleFormat, "new-bundle-format", false,
"output bundle in new format that contains all verification material")

cmd.Flags().BoolVarP(&o.SkipConfirmation, "yes", "y", false,
"skip confirmation prompts for non-destructive operations")

Expand Down
Loading

0 comments on commit c6cdf1b

Please sign in to comment.