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

Add --bundle flag to sign-blob and verify-blob #1306

Merged
merged 3 commits into from
Jan 14, 2022
Merged
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
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 @@ -232,10 +232,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.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 @@ -265,9 +265,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, sig
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, sig
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 @@ -153,7 +176,7 @@ func VerifyBlobCmd(ctx context.Context, ko sign.KeyOpts, certRef, certEmail, sig
}

// 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 @@ -166,6 +189,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 @@ -197,9 +226,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 @@ -234,6 +271,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