Skip to content
Open
129 changes: 16 additions & 113 deletions cmd/cosign/cli/sign/sign_blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ package sign
import (
"context"
"crypto"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
Expand All @@ -34,10 +32,8 @@ import (
"github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa/client"
"github.com/sigstore/cosign/v3/internal/ui"
cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle"
protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1"
"github.com/sigstore/sigstore-go/pkg/sign"
"github.com/sigstore/sigstore/pkg/signature"
"google.golang.org/protobuf/encoding/protojson"
)

func getPayload(ctx context.Context, payloadPath string, hashFunction crypto.Hash) (internal.HashReader, func() error, error) {
Expand All @@ -53,26 +49,19 @@ func getPayload(ctx context.Context, payloadPath string, hashFunction crypto.Has
}

// nolint
func SignBlobCmd(ctx context.Context, ro *options.RootOptions, ko options.KeyOpts, payloadPath, certPath, certChainPath string, b64 bool, outputSignature string, outputCertificate string, tlogUpload bool) ([]byte, error) {
func SignBlobCmd(ctx context.Context, ro *options.RootOptions, ko options.KeyOpts, payloadPath, certPath, certChainPath string) error {
var payload internal.HashReader

ctx, cancel := context.WithTimeout(ctx, ro.Timeout)
defer cancel()

var shouldUpload bool
var err error

if ko.SigningConfig == nil {

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.

In what case will this be nil? Currently, you can either provide a signing config, or specify --use-signing-config to fetch it from TUF. If we're removing that latter flag, then the signing config would always populated, either by TUF or with one explicitly provided (which could be empty).

(this might be answered in a later commit, feel free to point to that if so)

shouldUpload, err = signcommon.ShouldUploadToTlog(ctx, ko, nil, tlogUpload)
if err != nil {
return nil, fmt.Errorf("upload to tlog: %w", err)
}
ko.SigningConfig, err = signcommon.NewSigningConfigFromKeyOpts(ko, shouldUpload)
if err != nil {
return nil, fmt.Errorf("creating signing config: %w", err)
}
} else {
shouldUpload = len(ko.SigningConfig.RekorLogURLs()) > 0
ko.SigningConfig = signcommon.NewEmptySigningConfig()
}

shouldUpload, err := signcommon.ShouldUploadToTlog(ctx, ko, nil, len(ko.SigningConfig.RekorLogURLs()) > 0)
if err != nil {
return fmt.Errorf("should upload to tlog: %w", err)
}

if !shouldUpload {
Expand All @@ -84,7 +73,7 @@ func SignBlobCmd(ctx context.Context, ro *options.RootOptions, ko options.KeyOpt

keypair, certBytes, chainBytes, idToken, err := signcommon.GetKeypairAndToken(ctx, ko, certPath, certChainPath)
if err != nil {
return nil, fmt.Errorf("getting keypair and token: %w", err)
return fmt.Errorf("getting keypair and token: %w", err)
}
if closer, ok := keypair.(interface{ Close() }); ok {
defer closer.Close()
Expand All @@ -93,23 +82,13 @@ func SignBlobCmd(ctx context.Context, ro *options.RootOptions, ko options.KeyOpt
hashFunction := signcommon.ProtoHashAlgoToHash(keypair.GetHashAlgorithm())
payload, closePayload, err := getPayload(ctx, payloadPath, hashFunction)
if err != nil {
return nil, fmt.Errorf("getting payload: %w", err)
return fmt.Errorf("getting payload: %w", err)
}
defer closePayload()

if hashFunction != crypto.SHA256 && !ko.NewBundleFormat && (shouldUpload || (!ko.Sk && ko.KeyRef == "")) {
ui.Infof(ctx, "Non SHA256 hash function is not supported for old bundle format. Use --new-bundle-format to use the new bundle format or use different signing key/algorithm.")
if !ko.SkipConfirmation {
if err := ui.ConfirmContinue(ctx); err != nil {
return nil, err
}
}
ui.Infof(ctx, "Continuing with non SHA256 hash function and old bundle format")
}

data, err := io.ReadAll(&payload)
if err != nil {
return nil, fmt.Errorf("reading payload: %w", err)
return fmt.Errorf("reading payload: %w", err)
}
content := &sign.PlainData{
Data: data,
Expand All @@ -119,94 +98,18 @@ func SignBlobCmd(ctx context.Context, ro *options.RootOptions, ko options.KeyOpt
if ko.TSAClientCACert != "" || (ko.TSAClientCert != "" && ko.TSAClientKey != "") {
tsaClientTransport, err = client.GetHTTPTransport(ko.TSAClientCACert, ko.TSAClientCert, ko.TSAClientKey, ko.TSAServerName, 30*time.Second)
if err != nil {
return nil, fmt.Errorf("getting TSA client transport: %w", err)
return fmt.Errorf("getting TSA client transport: %w", err)
}
}
signOpts := cbundle.SignOptions{TSAClientTransport: tsaClientTransport}
bundleBytes, err := cbundle.SignData(ctx, content, keypair, idToken, certBytes, chainBytes, ko.SigningConfig, ko.TrustedMaterial, signOpts)
if err != nil {
return nil, fmt.Errorf("signing bundle: %w", err)
}

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

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

bundleComponents, err := signcommon.ExtractComponentsFromProtoBundle(&pb)
if err != nil {
return nil, fmt.Errorf("extracting components from bundle: %w", err)
}

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

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

if outputSignature != "" {
bts := bundleComponents.Signature
if b64 {
bts = []byte(base64.StdEncoding.EncodeToString(bundleComponents.Signature))
}
if err := os.WriteFile(outputSignature, bts, 0600); err != nil {
return nil, fmt.Errorf("create signature file: %w", err)
}
ui.Infof(ctx, "Wrote signature to file %s", outputSignature)
} else {
bts := bundleComponents.Signature
if b64 {
bts = []byte(base64.StdEncoding.EncodeToString(bundleComponents.Signature))
fmt.Println(string(bts))
} else {
if _, err := os.Stdout.Write(bts); err != nil {
return nil, err
}
}
}

if outputCertificate != "" && len(bundleComponents.Certificates) > 0 {
certPem, _ := signcommon.EncodeCertificatesToPEM(bundleComponents.Certificates)
var bts []byte
if b64 {
bts = []byte(base64.StdEncoding.EncodeToString(certPem))
} else {
bts = certPem
}
if err := os.WriteFile(outputCertificate, bts, 0600); err != nil {
return nil, fmt.Errorf("create certificate file: %w", err)
}
ui.Infof(ctx, "Wrote certificate to file %s", outputCertificate)
}

if len(bundleComponents.RFC3161Timestamps) > 0 && ko.RFC3161TimestampPath != "" {
legacyTimestamp := cbundle.TimestampToRFC3161Timestamp(bundleComponents.RFC3161Timestamps[0].GetSignedTimestamp())
ts, err := json.Marshal(legacyTimestamp)
if err != nil {
return nil, fmt.Errorf("marshalling timestamp: %w", err)
}
if err := os.WriteFile(ko.RFC3161TimestampPath, ts, 0600); err != nil {
return nil, fmt.Errorf("create timestamp file: %w", err)
}
ui.Infof(ctx, "Wrote timestamp to file %s", ko.RFC3161TimestampPath)
return fmt.Errorf("signing bundle: %w", err)
}

if b64 {
return []byte(base64.StdEncoding.EncodeToString(bundleComponents.Signature)), nil
if err := os.WriteFile(ko.BundlePath, bundleBytes, 0600); err != nil {
return fmt.Errorf("create bundle file: %w", err)
}
return bundleComponents.Signature, nil
ui.Infof(ctx, "Wrote bundle to file %s", ko.BundlePath)
return nil
}
81 changes: 18 additions & 63 deletions cmd/cosign/cli/sign/sign_blob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,7 @@ func TestSignBlobCmd(t *testing.T) {
keyOpts := options.KeyOpts{KeyRef: keyRef, BundlePath: bundlePath}

// Test happy path
_, err := SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, "", "", true, "", "", false)
if err != nil {
t.Fatalf("unexpected error %v", err)
}

// Test file outputs
keyOpts.NewBundleFormat = true
sigPath := filepath.Join(td, "output.sig")
certPath := filepath.Join(td, "output.pem")
_, err = SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, "", "", false, sigPath, certPath, false)
if err != nil {
if err := SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, "", ""); err != nil {
t.Fatalf("unexpected error %v", err)
}

Expand All @@ -81,8 +71,7 @@ func TestSignBlobCmd(t *testing.T) {
certPrivKeyRef := writeFile(t, td, string(pemBytes), "certkey.pem")
keyOpts.KeyRef = certPrivKeyRef

_, err = SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, signCertPath, "", false, "", "", false)
if err != nil {
if err := SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, signCertPath, ""); err != nil {
t.Fatalf("unexpected error %v", err)
}

Expand Down Expand Up @@ -119,14 +108,17 @@ func TestSignBlobCmd(t *testing.T) {
t.Fatal(err)
}
keyOpts.SigningConfig = sc
sigBytes, err := SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, "", "", true, "", "", true)
if err != nil {
if err := SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, "", ""); err != nil {
t.Fatalf("unexpected error %v", err)
}
decodedSig, err := base64.StdEncoding.DecodeString(string(sigBytes))
if err != nil {
t.Fatalf("failed to decode base64 signature: %v", err)
var b1 struct {

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.

nit, a better var name would help readability, even if this is just an inlined struct

MessageSignature struct {
Signature string `json:"signature"`
} `json:"messageSignature"`
}
bytes1, _ := os.ReadFile(bundlePath)

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.

even though it's unnecessary, i'd check the second return val and fail fatally if not nil

json.Unmarshal(bytes1, &b1)
decodedSig, _ := base64.StdEncoding.DecodeString(b1.MessageSignature.Signature)
if !ed25519.Verify(pub, blob, decodedSig) {
errString := "expected ed25519 signature"
if ed25519.VerifyWithOptions(pub, blob, decodedSig, &ed25519.Options{Hash: crypto.SHA512}) == nil {
Expand All @@ -137,14 +129,17 @@ func TestSignBlobCmd(t *testing.T) {

// Test signing using Ed25519 key with default signing config and no transparency log upload
keyOpts = options.KeyOpts{KeyRef: edKeyRef, BundlePath: bundlePath}
sigBytes, err = SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, "", "", true, "", "", false)
if err != nil {
if err := SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, "", ""); err != nil {
t.Fatalf("unexpected error %v", err)
}
decodedSig, err = base64.StdEncoding.DecodeString(string(sigBytes))
if err != nil {
t.Fatalf("failed to decode base64 signature: %v", err)
var b2 struct {
MessageSignature struct {
Signature string `json:"signature"`
} `json:"messageSignature"`
}
bytes2, _ := os.ReadFile(bundlePath)
json.Unmarshal(bytes2, &b2)
decodedSig, _ = base64.StdEncoding.DecodeString(b2.MessageSignature.Signature)
if !ed25519.Verify(pub, blob, decodedSig) {
errString := "expected ed25519 signature"
if ed25519.VerifyWithOptions(pub, blob, decodedSig, &ed25519.Options{Hash: crypto.SHA512}) == nil {
Expand All @@ -162,43 +157,3 @@ func writeFile(t *testing.T, td string, blob string, name string) string {
}
return blobPath
}

func TestSignBlobCmd_LegacyBundleNoCert(t *testing.T) {
td := t.TempDir()
bundlePath := filepath.Join(td, "legacy-bundle.json")

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

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

rootOpts := &options.RootOptions{}
keyOpts := options.KeyOpts{
KeyRef: keyRef,
BundlePath: bundlePath,
NewBundleFormat: false,
}

_, err := SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, "", "", true, "", "", false)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

bundleBytes, err := os.ReadFile(bundlePath)
if err != nil {
t.Fatalf("failed to read written bundle file: %v", err)
}

var payload cosign.LocalSignedPayload
if err := json.Unmarshal(bundleBytes, &payload); err != nil {
t.Fatalf("failed to unmarshal legacy bundle: %v", err)
}

if payload.Cert != "" {
t.Fatalf("expected empty cert field in legacy bundle when signing with a key without certificate, got: %q", payload.Cert)
}
if payload.Base64Signature == "" {
t.Fatal("expected non-empty Base64Signature in legacy bundle")
}
}
Loading