Skip to content

Commit

Permalink
Add --bundle flag to sign-blob and verify-blob (#1306)
Browse files Browse the repository at this point in the history
* Add --bundle flag to sign-blob and verify-blob

Signed-off-by: Priya Wadhwa <[email protected]>

* Add TUF timestamp when signing

Signed-off-by: Priya Wadhwa <[email protected]>
  • Loading branch information
priyawadhwa authored Jan 14, 2022
1 parent 079e28d commit bad18e5
Show file tree
Hide file tree
Showing 14 changed files with 230 additions and 16 deletions.
4 changes: 4 additions & 0 deletions cmd/cosign/cli/options/signblob.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type SignBlobOptions struct {
OIDC OIDCOptions
Registry RegistryOptions
Timeout time.Duration
BundlePath string
}

var _ Interface = (*SignBlobOptions)(nil)
Expand Down Expand Up @@ -64,4 +65,7 @@ func (o *SignBlobOptions) AddFlags(cmd *cobra.Command) {

cmd.Flags().DurationVar(&o.Timeout, "timeout", time.Second*30,
"HTTP Timeout defaults to 30 seconds")

cmd.Flags().StringVar(&o.BundlePath, "bundle", "",
"write everything required to verify the blob to a FILE")
}
8 changes: 6 additions & 2 deletions cmd/cosign/cli/options/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,9 @@ func (o *VerifyAttestationOptions) AddFlags(cmd *cobra.Command) {

// VerifyBlobOptions is the top level wrapper for the `verify blob` command.
type VerifyBlobOptions struct {
Key string
Signature string
Key string
Signature string
BundlePath string

SecurityKey SecurityKeyOptions
CertVerify CertVerifyOptions
Expand All @@ -132,6 +133,9 @@ func (o *VerifyBlobOptions) AddFlags(cmd *cobra.Command) {

cmd.Flags().StringVar(&o.Signature, "signature", "",
"signature content or path or remote URL")

cmd.Flags().StringVar(&o.BundlePath, "bundle", "",
"path to bundle FILE")
}

// VerifyBlobOptions is the top level wrapper for the `verify blob` command.
Expand Down
24 changes: 24 additions & 0 deletions cmd/cosign/cli/sign/sign_blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"time"

"github.com/pkg/errors"
cbundle "github.com/sigstore/cosign/pkg/cosign/bundle"
"github.com/sigstore/cosign/pkg/cosign/tuf"

"github.com/sigstore/cosign/cmd/cosign/cli/options"
"github.com/sigstore/cosign/cmd/cosign/cli/rekor"
Expand All @@ -44,6 +47,7 @@ type KeyOpts struct {
OIDCIssuer string
OIDCClientID string
OIDCClientSecret string
BundlePath string

// Modeled after InsecureSkipVerify in tls.Config, this disables
// verifying the SCT.
Expand Down Expand Up @@ -82,6 +86,8 @@ func SignBlobCmd(ctx context.Context, ko KeyOpts, regOpts options.RegistryOption
return nil, errors.Wrap(err, "signing blob")
}

signedPayload := cosign.LocalSignedPayload{}

if options.EnableExperimental() {
rekorBytes, err = sv.Bytes(ctx)
if err != nil {
Expand All @@ -96,6 +102,24 @@ func SignBlobCmd(ctx context.Context, ko KeyOpts, regOpts options.RegistryOption
return nil, err
}
fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex)
signedPayload.Bundle = cbundle.EntryToBundle(entry)
ts, err := tuf.GetTimestamp(ctx)
if err != nil {
return nil, err
}
signedPayload.Timestamp = ts
}

// if bundle is specified, just do that and ignore the rest
if ko.BundlePath != "" {
signedPayload.Base64Signature = base64.StdEncoding.EncodeToString(sig)
signedPayload.Cert = base64.StdEncoding.EncodeToString(rekorBytes)

contents, err := json.Marshal(signedPayload)
if err != nil {
return nil, err
}
return []byte(signedPayload.Base64Signature), os.WriteFile(ko.BundlePath, contents, 0600)
}

if outputSignature != "" {
Expand Down
1 change: 1 addition & 0 deletions cmd/cosign/cli/signblob.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ func SignBlob() *cobra.Command {
OIDCIssuer: o.OIDC.Issuer,
OIDCClientID: o.OIDC.ClientID,
OIDCClientSecret: o.OIDC.ClientSecret,
BundlePath: o.BundlePath,
}
for _, blob := range args {
// TODO: remove when the output flag has been deprecated
Expand Down
9 changes: 5 additions & 4 deletions cmd/cosign/cli/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,10 +234,11 @@ The blob may be specified as a path to a file or - for stdin.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ko := sign.KeyOpts{
KeyRef: o.Key,
Sk: o.SecurityKey.Use,
Slot: o.SecurityKey.Slot,
RekorURL: o.Rekor.URL,
KeyRef: o.Key,
Sk: o.SecurityKey.Use,
Slot: o.SecurityKey.Slot,
RekorURL: o.Rekor.URL,
BundlePath: o.BundlePath,
}
if err := verify.VerifyBlobCmd(cmd.Context(), ko, o.CertVerify.Cert,
o.CertVerify.CertEmail, o.CertVerify.CertOidcIssuer, o.Signature, args[0]); err != nil {
Expand Down
5 changes: 4 additions & 1 deletion cmd/cosign/cli/verify/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,12 @@ func loadCertFromFileOrURL(path string) (*x509.Certificate, error) {
if err != nil {
return nil, err
}
return loadCertFromPEM(pems)
}

func loadCertFromPEM(pems []byte) (*x509.Certificate, error) {
var out []byte
out, err = base64.StdEncoding.DecodeString(string(pems))
out, err := base64.StdEncoding.DecodeString(string(pems))
if err != nil {
// not a base64
out = pems
Expand Down
71 changes: 68 additions & 3 deletions cmd/cosign/cli/verify/verify_blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, cer
var verifier signature.Verifier
var cert *x509.Certificate

if !options.OneOf(ko.KeyRef, ko.Sk, certRef) && !options.EnableExperimental() {
if !options.OneOf(ko.KeyRef, ko.Sk, certRef) && !options.EnableExperimental() && ko.BundlePath == "" {
return &options.PubKeyParseError{}
}

sig, b64sig, err := signatures(sigRef)
sig, b64sig, err := signatures(sigRef, ko.BundlePath)
if err != nil {
return err
}
Expand Down Expand Up @@ -102,6 +102,29 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, cer
if err != nil {
return err
}
case ko.BundlePath != "":
b, err := cosign.FetchLocalSignedPayloadFromPath(ko.BundlePath)
if err != nil {
return err
}
if b.Cert == "" {
return fmt.Errorf("bundle does not contain cert for verification, please provide public key")
}
// cert can either be a cert or public key
certBytes := []byte(b.Cert)
if isb64(certBytes) {
certBytes, _ = base64.StdEncoding.DecodeString(b.Cert)
}
cert, err = loadCertFromPEM(certBytes)
if err != nil {
// check if cert is actually a public key
verifier, err = sigs.LoadPublicKeyRaw(certBytes, crypto.SHA256)
} else {
verifier, err = signature.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256)
}
if err != nil {
return err
}
case options.EnableExperimental():
rClient, err := rekor.NewClient(ko.RekorURL)
if err != nil {
Expand Down Expand Up @@ -154,7 +177,7 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, cer
}

// signatures returns the raw signature and the base64 encoded signature
func signatures(sigRef string) (string, string, error) {
func signatures(sigRef string, bundlePath string) (string, string, error) {
var targetSig []byte
var err error
switch {
Expand All @@ -167,6 +190,12 @@ func signatures(sigRef string) (string, string, error) {
}
targetSig = []byte(sigRef)
}
case bundlePath != "":
b, err := cosign.FetchLocalSignedPayloadFromPath(bundlePath)
if err != nil {
return "", "", err
}
targetSig = []byte(b.Base64Signature)
default:
return "", "", fmt.Errorf("missing flag '--signature'")
}
Expand Down Expand Up @@ -198,9 +227,17 @@ func payloadBytes(blobRef string) ([]byte, error) {
}

func verifyRekorEntry(ctx context.Context, ko sign.KeyOpts, pubKey signature.Verifier, cert *x509.Certificate, b64sig string, blobBytes []byte) error {
// If we have a bundle with a rekor entry, let's first try to verify offline
if ko.BundlePath != "" {
if err := verifyRekorBundle(ctx, ko.BundlePath, cert); err == nil {
fmt.Fprintf(os.Stderr, "tlog entry verified offline\n")
return nil
}
}
if !options.EnableExperimental() {
return nil
}

rekorClient, err := rekor.NewClient(ko.RekorURL)
if err != nil {
return err
Expand Down Expand Up @@ -235,6 +272,34 @@ func verifyRekorEntry(ctx context.Context, ko sign.KeyOpts, pubKey signature.Ver
return cosign.CheckExpiry(cert, time.Unix(*e.IntegratedTime, 0))
}

func verifyRekorBundle(ctx context.Context, bundlePath string, cert *x509.Certificate) error {
b, err := cosign.FetchLocalSignedPayloadFromPath(bundlePath)
if err != nil {
return err
}
if b.Bundle == nil {
return fmt.Errorf("rekor entry is not available")
}
pub, err := cosign.GetRekorPub(ctx)
if err != nil {
return errors.Wrap(err, "retrieving rekor public key")
}

rekorPubKey, err := cosign.PemToECDSAKey(pub)
if err != nil {
return errors.Wrap(err, "pem to ecdsa")
}

if err := cosign.VerifySET(b.Bundle.Payload, b.Bundle.SignedEntryTimestamp, rekorPubKey); err != nil {
return err
}
if cert == nil {
return nil
}
it := time.Unix(b.Bundle.Payload.IntegratedTime, 0)
return cosign.CheckExpiry(cert, it)
}

func extractCerts(e *models.LogEntryAnon) ([]*x509.Certificate, error) {
b, err := base64.StdEncoding.DecodeString(e.Body.(string))
if err != nil {
Expand Down
40 changes: 38 additions & 2 deletions cmd/cosign/cli/verify/verify_blob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@
package verify

import (
"encoding/json"
"io/ioutil"
"path/filepath"
"testing"

"github.com/sigstore/cosign/pkg/cosign"
)

func TestSignatures(t *testing.T) {
func TestSignaturesRef(t *testing.T) {
sig := "a=="
b64sig := "YT09"
tests := []struct {
Expand All @@ -41,7 +46,7 @@ func TestSignatures(t *testing.T) {

for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
gotSig, gotb64Sig, err := signatures(test.sigRef)
gotSig, gotb64Sig, err := signatures(test.sigRef, "")
if test.shouldErr && err != nil {
return
}
Expand All @@ -57,3 +62,34 @@ func TestSignatures(t *testing.T) {
})
}
}

func TestSignaturesBundle(t *testing.T) {
td := t.TempDir()
fp := filepath.Join(td, "file")

sig := "a=="
b64sig := "YT09"

// save as a LocalSignedPayload to the file
lsp := cosign.LocalSignedPayload{
Base64Signature: b64sig,
}
contents, err := json.Marshal(lsp)
if err != nil {
t.Fatal(err)
}
if err := ioutil.WriteFile(fp, contents, 0644); err != nil {
t.Fatal(err)
}

gotSig, gotb64Sig, err := signatures("", fp)
if err != nil {
t.Fatal(err)
}
if gotSig != sig {
t.Fatalf("unexpected signature, expected: %s got: %s", sig, gotSig)
}
if gotb64Sig != b64sig {
t.Fatalf("unexpected encoded signature, expected: %s got: %s", b64sig, gotb64Sig)
}
}
1 change: 1 addition & 0 deletions doc/cosign_sign-blob.md

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

1 change: 1 addition & 0 deletions doc/cosign_verify-blob.md

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

22 changes: 22 additions & 0 deletions pkg/cosign/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ import (
"crypto/x509"
"encoding/json"
"fmt"
"io/ioutil"
"runtime"

"github.com/google/go-containerregistry/pkg/name"
"github.com/pkg/errors"
"github.com/sigstore/cosign/pkg/cosign/bundle"
"github.com/sigstore/cosign/pkg/cosign/tuf"
ociremote "github.com/sigstore/cosign/pkg/oci/remote"
"knative.dev/pkg/pool"
)
Expand All @@ -37,6 +39,13 @@ type SignedPayload struct {
Bundle *bundle.RekorBundle
}

type LocalSignedPayload struct {
Base64Signature string `json:"base64Signature"`
Cert string `json:"cert,omitempty"`
Bundle *bundle.RekorBundle `json:"rekorBundle,omitempty"`
Timestamp *tuf.Timestamp `json:"timestamp,omitempty"`
}

type Signatures struct {
KeyID string `json:"keyid"`
Sig string `json:"sig"`
Expand Down Expand Up @@ -147,3 +156,16 @@ func FetchAttestationsForReference(ctx context.Context, ref name.Reference, opts

return attestations, nil
}

// FetchLocalSignedPayloadFromPath fetches a local signed payload from a path to a file
func FetchLocalSignedPayloadFromPath(path string) (*LocalSignedPayload, error) {
contents, err := ioutil.ReadFile(path)
if err != nil {
return nil, errors.Wrapf(err, "reading %s", path)
}
var b *LocalSignedPayload
if err := json.Unmarshal(contents, &b); err != nil {
return nil, err
}
return b, nil
}
Loading

0 comments on commit bad18e5

Please sign in to comment.