Skip to content

Commit

Permalink
signing and verification of blobs using time-stamping
Browse files Browse the repository at this point in the history
Signed-off-by: Hector Fernandez <hector@chainguard.dev>
  • Loading branch information
hectorj2f committed Nov 16, 2022
1 parent 5331042 commit e0982e6
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 15 deletions.
2 changes: 2 additions & 0 deletions cmd/cosign/cli/options/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ type KeyOpts struct {
BundlePath string
SkipConfirmation bool
TSAServerURL string
TSABundlePath string
TSACertChainPath string

// FulcioAuthFlow is the auth flow to use when authenticating against
// Fulcio. See https://pkg.go.dev/github.com/sigstore/cosign/cmd/cosign/cli/fulcio#pkg-constants
Expand Down
9 changes: 9 additions & 0 deletions cmd/cosign/cli/options/signblob.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ type SignBlobOptions struct {
BundlePath string
SkipConfirmation bool
TlogUpload bool
TSAServerURL string
TSABundlePath string
}

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

cmd.Flags().BoolVar(&o.TlogUpload, "tlog-upload", false,
"whether or not to upload to the tlog")

cmd.Flags().StringVar(&o.TSAServerURL, "timestamp-server-url", "",
"url to the Timestamp RFC3161 server, default none")

cmd.Flags().StringVar(&o.TSABundlePath, "tsa-bundle", "",
"write everything required to verify the blob to a FILE")
_ = cmd.Flags().SetAnnotation("tsa-bundle", cobra.BashCompFilenameExt, []string{})
}
5 changes: 5 additions & 0 deletions cmd/cosign/cli/options/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ type VerifyBlobOptions struct {
Rekor RekorOptions
Registry RegistryOptions
CommonVerifyOptions CommonVerifyOptions

TSABundlePath string
}

var _ Interface = (*VerifyBlobOptions)(nil)
Expand All @@ -165,6 +167,9 @@ func (o *VerifyBlobOptions) AddFlags(cmd *cobra.Command) {

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

cmd.Flags().StringVar(&o.TSABundlePath, "tsa-bundle", "",
"path to timestamp bundle FILE")
}

// VerifyDockerfileOptions is the top level wrapper for the `dockerfile verify` command.
Expand Down
30 changes: 30 additions & 0 deletions cmd/cosign/cli/sign/sign_blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import (
"os"
"path/filepath"

"github.com/sigstore/cosign/internal/pkg/cosign/tsa"
cbundle "github.com/sigstore/cosign/pkg/cosign/bundle"
tsaclient "github.com/sigstore/timestamp-authority/pkg/client"

"github.com/sigstore/cosign/cmd/cosign/cli/options"
"github.com/sigstore/cosign/cmd/cosign/cli/rekor"
Expand Down Expand Up @@ -65,6 +67,20 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, regOpts options.Re

signedPayload := cosign.LocalSignedPayload{}

if ko.TSAServerURL != "" {
clientTSA, err := tsaclient.GetTimestampClient(ko.TSAServerURL)
if err != nil {
return nil, fmt.Errorf("failed to create TSA client: %w", err)
}
b64Sig := []byte(base64.StdEncoding.EncodeToString(sig))

respBytes, err := tsa.GetTimestampedSignature(b64Sig, clientTSA)
if err != nil {
return nil, err
}

signedPayload.TSABundle = cbundle.TimestampToTSABundle(respBytes)
}
if ShouldUploadToTlog(ctx, ko, nil, ko.SkipConfirmation, tlogUpload) {
rekorBytes, err = sv.Bytes(ctx)
if err != nil {
Expand All @@ -82,6 +98,20 @@ func SignBlobCmd(ro *options.RootOptions, ko options.KeyOpts, regOpts options.Re
signedPayload.Bundle = cbundle.EntryToBundle(entry)
}

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

contents, err := json.Marshal(signedPayload)
if err != nil {
return nil, err
}
if err := os.WriteFile(ko.TSABundlePath, contents, 0600); err != nil {
return nil, fmt.Errorf("create tsa bundle file: %w", err)
}
fmt.Printf("TSA bundle wrote in the file %s\n", ko.TSABundlePath)
}

// if bundle is specified, just do that and ignore the rest
if ko.BundlePath != "" {
signedPayload.Base64Signature = base64.StdEncoding.EncodeToString(sig)
Expand Down
4 changes: 4 additions & 0 deletions cmd/cosign/cli/signblob.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,17 @@ func SignBlob() *cobra.Command {
OIDCDisableProviders: o.OIDC.DisableAmbientProviders,
BundlePath: o.BundlePath,
SkipConfirmation: o.SkipConfirmation,
TSAServerURL: o.TSAServerURL,
TSABundlePath: o.TSABundlePath,
}

for _, blob := range args {
// TODO: remove when the output flag has been deprecated
if o.Output != "" {
fmt.Fprintln(os.Stderr, "WARNING: the '--output' flag is deprecated and will be removed in the future. Use '--output-signature'")
o.OutputSignature = o.Output
}

if _, err := sign.SignBlobCmd(ro, ko, o.Registry, blob, o.Base64Output, o.OutputSignature, o.OutputCertificate, o.TlogUpload); err != nil {
return fmt.Errorf("signing %s: %w", blob, err)
}
Expand Down
13 changes: 8 additions & 5 deletions cmd/cosign/cli/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,11 +276,14 @@ The blob may be specified as a path to a file or - for stdin.`,
PersistentPreRun: options.BindViper,
RunE: func(cmd *cobra.Command, args []string) error {
ko := options.KeyOpts{
KeyRef: o.Key,
Sk: o.SecurityKey.Use,
Slot: o.SecurityKey.Slot,
RekorURL: o.Rekor.URL,
BundlePath: o.BundlePath,
KeyRef: o.Key,
Sk: o.SecurityKey.Use,
Slot: o.SecurityKey.Slot,
RekorURL: o.Rekor.URL,
BundlePath: o.BundlePath,
TSAServerURL: o.CommonVerifyOptions.TSAServerURL,
TSABundlePath: o.TSABundlePath,
TSACertChainPath: o.CommonVerifyOptions.TSACertChainPath,
}
verifyBlobCmd := &verify.VerifyBlobCmd{
KeyOpts: ko,
Expand Down
123 changes: 114 additions & 9 deletions cmd/cosign/cli/verify/verify_blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import (
"github.com/sigstore/cosign/cmd/cosign/cli/rekor"
"github.com/sigstore/cosign/pkg/blob"
"github.com/sigstore/cosign/pkg/cosign"
"github.com/sigstore/cosign/pkg/cosign/bundle"
cbundle "github.com/sigstore/cosign/pkg/cosign/bundle"
"github.com/sigstore/cosign/pkg/cosign/pivkey"
"github.com/sigstore/cosign/pkg/cosign/pkcs11key"
sigs "github.com/sigstore/cosign/pkg/signature"
Expand All @@ -59,6 +59,8 @@ import (
"github.com/sigstore/sigstore/pkg/cryptoutils"
"github.com/sigstore/sigstore/pkg/signature/dsse"
signatureoptions "github.com/sigstore/sigstore/pkg/signature/options"
tclient "github.com/sigstore/timestamp-authority/pkg/client"
tsaverification "github.com/sigstore/timestamp-authority/pkg/verification"
)

func isb64(data []byte) bool {
Expand Down Expand Up @@ -88,14 +90,21 @@ type VerifyBlobCmd struct {
// nolint
func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error {
var cert *x509.Certificate
var bundle *bundle.RekorBundle
var bundle *cbundle.RekorBundle
var tsaBundle *cbundle.TSABundle

// Require a certificate/key OR a local bundle file that has the cert.
if options.NOf(c.KeyRef, c.Sk) > 1 {
return &options.KeyParseError{}
}

sig, err := signatures(c.SigRef, c.BundlePath)
var sig string
var err error
if c.KeyOpts.TSABundlePath != "" {
sig, err = signatures(c.SigRef, c.KeyOpts.TSABundlePath)
} else {
sig, err = signatures(c.SigRef, c.BundlePath)
}
if err != nil {
return err
}
Expand All @@ -116,7 +125,21 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error {
CertGithubWorkflowRef: c.CertGithubWorkflowRef,
IgnoreSCT: c.IgnoreSCT,
Offline: c.Offline,
TSACertChainPath: c.KeyOpts.TSACertChainPath,
}
if c.KeyOpts.TSAServerURL != "" {
co.TSAClient, err = tclient.GetTimestampClient(c.KeyOpts.TSAServerURL)
if err != nil {
return fmt.Errorf("failed to create TSA client: %w", err)
}
if c.KeyOpts.TSACertChainPath != "" {
_, err := os.Stat(c.KeyOpts.TSACertChainPath)
if err != nil {
return fmt.Errorf("unable to open timestamp certificate chain file '%s': %w", c.KeyOpts.TSACertChainPath, err)
}
}
}

if keylessVerification(c.KeyRef, c.Sk) {
if c.RekorURL != "" {
rekorClient, err := rekor.NewClient(c.RekorURL)
Expand Down Expand Up @@ -240,12 +263,59 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error {
return fmt.Errorf("loading verifier from bundle: %w", err)
}
bundle = b.Bundle
case c.TSABundlePath != "":
b, err := cosign.FetchLocalSignedPayloadFromPath(c.TSABundlePath)
if err != nil {
return err
}
if b.Cert == "" {
return fmt.Errorf("bundle does not contain cert for verification, please provide public key")
}
// b.Cert can either be a certificate 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
co.SigVerifier, err = sigs.LoadPublicKeyRaw(certBytes, crypto.SHA256)
} else {
if c.CertChain == "" {
co.RootCerts, err = fulcio.GetRoots()
if err != nil {
return fmt.Errorf("getting Fulcio roots: %w", err)
}
co.IntermediateCerts, err = fulcio.GetIntermediates()
if err != nil {
return fmt.Errorf("getting Fulcio intermediates: %w", err)
}
co.SigVerifier, err = cosign.ValidateAndUnpackCert(cert, co)
if err != nil {
return fmt.Errorf("verifying certificate from bundle: %w", err)
}
} else {
// Verify certificate with chain
chain, err := loadCertChainFromFileOrURL(c.CertChain)
if err != nil {
return err
}
co.SigVerifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co)
if err != nil {
return fmt.Errorf("verifying certificate from bundle with chain: %w", err)
}
}
}
if err != nil {
return fmt.Errorf("loading verifier from bundle: %w", err)
}
tsaBundle = b.TSABundle
default:
return fmt.Errorf("please provide a cert to verify against via --certificate or a bundle via --bundle")
}

// Performs all blob verification.
if err := verifyBlob(ctx, co, blobBytes, sig, cert, bundle); err != nil {
if err := verifyBlob(ctx, co, blobBytes, sig, cert, bundle, tsaBundle); err != nil {
return err
}

Expand All @@ -265,7 +335,7 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error {
// clean up the args into CheckOpts or use KeyOpts here to resolve different KeyOpts.
func verifyBlob(ctx context.Context, co *cosign.CheckOpts,
blobBytes []byte, sig string, cert *x509.Certificate,
bundle *bundle.RekorBundle) error {
bundle *cbundle.RekorBundle, tsaBundle *cbundle.TSABundle) error {
if cert != nil {
// This would have already be done in the main entrypoint, but do this for robustness.
var err error
Expand Down Expand Up @@ -315,7 +385,18 @@ func verifyBlob(ctx context.Context, co *cosign.CheckOpts,
}
validityTime = time.Unix(bundle.IntegratedTime, 0)
fmt.Fprintf(os.Stderr, "tlog entry verified offline\n")
// b. We can make an online lookup to the transparency log since we don't have an entry.
// b. We can make an online lookup to the transparency log since we don't have an entry.
case tsaBundle != nil:
// TODO: Verify TSA only and return the result with the tsa bundle
if co.TSAClient != nil {
err := verifyTSABundle(tsaBundle, sig, co.TSACertChainPath)
if err != nil {
// Return when the provided bundle fails verification. (Do not fallback).
return err
}
fmt.Fprintf(os.Stderr, "tsa bundle entry verified\n")
return nil
}
case co.RekorClient != nil:
var tlogFindErr error
var e *models.LogEntryAnon
Expand Down Expand Up @@ -454,8 +535,32 @@ func payloadBytes(blobRef string) ([]byte, error) {
return blobBytes, nil
}

func verifyRekorBundle(ctx context.Context, bundle *bundle.RekorBundle,
blobBytes []byte, sig string, pubKeyBytes []byte) (*bundle.RekorPayload, error) {
func verifyTSABundle(bundle *cbundle.TSABundle, sig string, tsaCertChainPath string) error {
fmt.Println("Verifying TSA Bundle")

sigBytes, err := base64.StdEncoding.DecodeString(sig)
if err != nil {
return fmt.Errorf("reading DecodeString: %w", err)
}
pemBytes, err := os.ReadFile(filepath.Clean(tsaCertChainPath))
if err != nil {
return fmt.Errorf("error reading certification chain path file: %w", err)
}
certPool := x509.NewCertPool()
ok := certPool.AppendCertsFromPEM(pemBytes)
if !ok {
return fmt.Errorf("error parsing response into Timestamp while appending certs from PEM")
}

err = tsaverification.VerifyTimestampResponse(bundle.SignedRFC3161Timestamp, bytes.NewReader(sigBytes), certPool)
if err != nil {
return fmt.Errorf("unable verifyTSRWithPEM: %w", err)
}
return nil
}

func verifyRekorBundle(ctx context.Context, bundle *cbundle.RekorBundle,
blobBytes []byte, sig string, pubKeyBytes []byte) (*cbundle.RekorPayload, error) {
if err := verifyBundleMatchesData(ctx, bundle, blobBytes, pubKeyBytes, []byte(sig)); err != nil {
return nil, err
}
Expand All @@ -480,7 +585,7 @@ func verifyRekorBundle(ctx context.Context, bundle *bundle.RekorBundle,
return &bundle.Payload, nil
}

func verifyBundleMatchesData(ctx context.Context, bundle *bundle.RekorBundle, blobBytes, certBytes, sigBytes []byte) error {
func verifyBundleMatchesData(ctx context.Context, bundle *cbundle.RekorBundle, blobBytes, certBytes, sigBytes []byte) error {
eimpl, kind, apiVersion, err := unmarshalEntryImpl(bundle.Payload.Body.(string))
if err != nil {
return err
Expand Down
2 changes: 1 addition & 1 deletion cmd/cosign/cli/verify/verify_blob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@ func TestVerifyBlob(t *testing.T) {
bundle = b.Bundle
}

err = verifyBlob(ctx, co, tt.blob, tt.signature, tt.cert, bundle)
err = verifyBlob(ctx, co, tt.blob, tt.signature, tt.cert, bundle, nil)
if (err != nil) != tt.shouldErr {
t.Fatalf("verifyBlob()= %s, expected shouldErr=%t ", err, tt.shouldErr)
}
Expand Down
2 changes: 2 additions & 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.

1 change: 1 addition & 0 deletions pkg/cosign/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type LocalSignedPayload struct {
Base64Signature string `json:"base64Signature"`
Cert string `json:"cert,omitempty"`
Bundle *bundle.RekorBundle `json:"rekorBundle,omitempty"`
TSABundle *bundle.TSABundle `json:"tsaBundle,omitempty"`
}

type Signatures struct {
Expand Down

0 comments on commit e0982e6

Please sign in to comment.