Skip to content

Commit 1e49088

Browse files
authored
Updating sign-blob to also support signing with a certificate (#4547)
--------- Signed-off-by: Zach Steindler <steiza@github.com>
1 parent 38bf9e6 commit 1e49088

File tree

10 files changed

+110
-129
lines changed

10 files changed

+110
-129
lines changed

cmd/cosign/cli/options/attest_blob.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,14 @@ func (o *AttestBlobOptions) AddFlags(cmd *cobra.Command) {
7272
_ = cmd.MarkFlagFilename("key", privateKeyExts...)
7373

7474
cmd.Flags().StringVar(&o.Cert, "certificate", "",
75-
"path to the X.509 certificate in PEM format to include in the OCI Signature")
75+
"path to the X.509 certificate for signing attestation")
7676
_ = cmd.MarkFlagFilename("certificate", certificateExts...)
7777

7878
cmd.Flags().StringVar(&o.CertChain, "certificate-chain", "",
7979
"path to a list of CA X.509 certificates in PEM format which will be needed "+
80-
"when building the certificate chain for the signing certificate. "+
80+
"when building the certificate chain for the signed attestation. "+
8181
"Must start with the parent intermediate CA certificate of the "+
82-
"signing certificate and end with the root certificate. Included in the OCI Signature")
82+
"signing certificate and end with the root certificate.")
8383
_ = cmd.MarkFlagFilename("certificate-chain", certificateExts...)
8484

8585
cmd.Flags().StringVar(&o.OutputSignature, "output-signature", "",

cmd/cosign/cli/options/signblob.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import (
2929
// The new output-certificate flag is only in use when COSIGN_EXPERIMENTAL is enabled
3030
type SignBlobOptions struct {
3131
Key string
32+
Cert string
33+
CertChain string
3234
Base64Output bool
3335
Output string // deprecated: TODO remove when the output flag is fully deprecated
3436
OutputSignature string // TODO: this should be the root output file arg.
@@ -69,6 +71,17 @@ func (o *SignBlobOptions) AddFlags(cmd *cobra.Command) {
6971
"path to the private key file, KMS URI or Kubernetes Secret")
7072
_ = cmd.MarkFlagFilename("key", privateKeyExts...)
7173

74+
cmd.Flags().StringVar(&o.Cert, "certificate", "",
75+
"path to the X.509 certificate for signing attestation")
76+
_ = cmd.MarkFlagFilename("certificate", certificateExts...)
77+
78+
cmd.Flags().StringVar(&o.CertChain, "certificate-chain", "",
79+
"path to a list of CA X.509 certificates in PEM format which will be needed "+
80+
"when building the certificate chain for the signed attestation. "+
81+
"Must start with the parent intermediate CA certificate of the "+
82+
"signing certificate and end with the root certificate.")
83+
_ = cmd.MarkFlagFilename("certificate-chain", certificateExts...)
84+
7285
cmd.Flags().BoolVar(&o.Base64Output, "b64", true,
7386
"whether to base64 encode the output")
7487

cmd/cosign/cli/sign/sign_blob.go

Lines changed: 18 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -58,44 +58,41 @@ func getPayload(ctx context.Context, payloadPath string, hashFunction crypto.Has
5858
}
5959

6060
// nolint
61-
func SignBlobCmd(ctx context.Context, ro *options.RootOptions, ko options.KeyOpts, payloadPath string, b64 bool, outputSignature string, outputCertificate string, tlogUpload bool) ([]byte, error) {
61+
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) {
6262
var payload internal.HashReader
6363

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

67-
shouldUpload, err := signcommon.ShouldUploadToTlog(ctx, ko, nil, tlogUpload)
68-
if err != nil {
69-
return nil, fmt.Errorf("upload to tlog: %w", err)
70-
}
71-
72-
if !shouldUpload {
67+
// TODO - this does not take ko.SigningConfig into account
68+
if !tlogUpload {
7369
// To maintain backwards compatibility with older cosign versions,
7470
// we do not use ed25519ph for ed25519 keys when the signatures are not
7571
// uploaded to the Tlog.
7672
ko.DefaultLoadOptions = &[]signature.LoadOption{}
7773
}
7874

79-
if ko.SigningConfig != nil {
80-
keypair, _, idToken, err := signcommon.GetKeypairAndToken(ctx, ko, "", "")
81-
if err != nil {
82-
return nil, fmt.Errorf("getting keypair and token: %w", err)
83-
}
75+
keypair, sv, certBytes, idToken, err := signcommon.GetKeypairAndToken(ctx, ko, certPath, certChainPath)
76+
if err != nil {
77+
return nil, fmt.Errorf("getting keypair and token: %w", err)
78+
}
8479

85-
payload, closePayload, err := getPayload(ctx, payloadPath, protoHashAlgoToHash(keypair.GetHashAlgorithm()))
86-
if err != nil {
87-
return nil, fmt.Errorf("getting payload: %w", err)
88-
}
89-
defer closePayload()
80+
hashFunction := protoHashAlgoToHash(keypair.GetHashAlgorithm())
81+
payload, closePayload, err := getPayload(ctx, payloadPath, hashFunction)
82+
if err != nil {
83+
return nil, fmt.Errorf("getting payload: %w", err)
84+
}
85+
defer closePayload()
86+
87+
if ko.SigningConfig != nil {
9088
data, err := io.ReadAll(&payload)
9189
if err != nil {
9290
return nil, fmt.Errorf("reading payload: %w", err)
9391
}
9492
content := &sign.PlainData{
9593
Data: data,
9694
}
97-
98-
bundle, err := cbundle.SignData(ctx, content, keypair, idToken, nil, ko.SigningConfig, ko.TrustedMaterial)
95+
bundle, err := cbundle.SignData(ctx, content, keypair, idToken, certBytes, ko.SigningConfig, ko.TrustedMaterial)
9996
if err != nil {
10097
return nil, fmt.Errorf("signing bundle: %w", err)
10198
}
@@ -106,15 +103,9 @@ func SignBlobCmd(ctx context.Context, ro *options.RootOptions, ko options.KeyOpt
106103
return bundle, nil
107104
}
108105

109-
sv, closeSV, err := signcommon.GetSignerVerifier(ctx, "", "", ko)
110-
if err != nil {
111-
return nil, fmt.Errorf("getting signer: %w", err)
112-
}
113-
defer closeSV()
114-
115-
hashFunction, err := getHashFunction(sv, ko)
106+
shouldUpload, err := signcommon.ShouldUploadToTlog(ctx, ko, nil, tlogUpload)
116107
if err != nil {
117-
return nil, err
108+
return nil, fmt.Errorf("upload to tlog: %w", err)
118109
}
119110

120111
if hashFunction != crypto.SHA256 && !ko.NewBundleFormat && (shouldUpload || (!ko.Sk && ko.KeyRef == "")) {
@@ -127,12 +118,6 @@ func SignBlobCmd(ctx context.Context, ro *options.RootOptions, ko options.KeyOpt
127118
ui.Infof(ctx, "Continuing with non SHA256 hash function and old bundle format")
128119
}
129120

130-
payload, closePayload, err := getPayload(ctx, payloadPath, hashFunction)
131-
if err != nil {
132-
return nil, err
133-
}
134-
defer closePayload()
135-
136121
sig, err := sv.SignMessage(&payload, signatureoptions.WithContext(ctx))
137122
if err != nil {
138123
return nil, fmt.Errorf("signing blob: %w", err)
@@ -277,35 +262,6 @@ func extractCertificate(ctx context.Context, sv *signcommon.SignerVerifier) ([]b
277262
return nil, nil
278263
}
279264

280-
func getHashFunction(sv *signcommon.SignerVerifier, ko options.KeyOpts) (crypto.Hash, error) {
281-
if ko.Sk || ko.KeyRef != "" {
282-
pubKey, err := sv.PublicKey()
283-
if err != nil {
284-
return crypto.Hash(0), fmt.Errorf("error getting public key: %w", err)
285-
}
286-
287-
defaultLoadOptions := cosign.GetDefaultLoadOptions(ko.DefaultLoadOptions)
288-
289-
// TODO: Ideally the SignerVerifier should have a method to get the hash function
290-
algo, err := signature.GetDefaultAlgorithmDetails(pubKey, *defaultLoadOptions...)
291-
if err != nil {
292-
return crypto.Hash(0), fmt.Errorf("error getting default algorithm details: %w", err)
293-
}
294-
return algo.GetHashType(), nil
295-
}
296-
297-
// New key was generated, using the signing algorithm specified by the user
298-
keyDetails, err := signcommon.ParseSignatureAlgorithmFlag(ko.SigningAlgorithm)
299-
if err != nil {
300-
return crypto.Hash(0), fmt.Errorf("parsing signature algorithm: %w", err)
301-
}
302-
algo, err := signature.GetAlgorithmDetails(keyDetails)
303-
if err != nil {
304-
return crypto.Hash(0), fmt.Errorf("getting algorithm details: %w", err)
305-
}
306-
return algo.GetHashType(), nil
307-
}
308-
309265
func hashFuncToProtoBundle(hashFunc crypto.Hash) protocommon.HashAlgorithm {
310266
switch hashFunc {
311267
case crypto.SHA256:

cmd/cosign/cli/sign/sign_blob_test.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,15 @@
1515
package sign
1616

1717
import (
18+
"crypto/x509"
19+
"encoding/pem"
1820
"os"
1921
"path/filepath"
2022
"testing"
2123

24+
"github.com/secure-systems-lab/go-securesystemslib/encrypted"
2225
"github.com/sigstore/cosign/v3/cmd/cosign/cli/options"
26+
"github.com/sigstore/cosign/v3/internal/test"
2327
"github.com/sigstore/cosign/v3/pkg/cosign"
2428
)
2529

@@ -37,7 +41,7 @@ func TestSignBlobCmd(t *testing.T) {
3741
keyOpts := options.KeyOpts{KeyRef: keyRef, BundlePath: bundlePath}
3842

3943
// Test happy path
40-
_, err := SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, true, "", "", false)
44+
_, err := SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, "", "", true, "", "", false)
4145
if err != nil {
4246
t.Fatalf("unexpected error %v", err)
4347
}
@@ -46,7 +50,32 @@ func TestSignBlobCmd(t *testing.T) {
4650
keyOpts.NewBundleFormat = true
4751
sigPath := filepath.Join(td, "output.sig")
4852
certPath := filepath.Join(td, "output.pem")
49-
_, err = SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, false, sigPath, certPath, false)
53+
_, err = SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, "", "", false, sigPath, certPath, false)
54+
if err != nil {
55+
t.Fatalf("unexpected error %v", err)
56+
}
57+
58+
// Test signing with a certificate
59+
rootCert, rootKey, _ := test.GenerateRootCa()
60+
cert, certPrivKey, _ := test.GenerateLeafCert("subject", "oidc-issuer", rootCert, rootKey)
61+
certPemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
62+
signCertPath := writeFile(t, td, string(certPemBytes), "cert.pem")
63+
x509Encoded, err := x509.MarshalPKCS8PrivateKey(certPrivKey)
64+
if err != nil {
65+
t.Fatalf("unexpected error %v", err)
66+
}
67+
encBytes, err := encrypted.Encrypt(x509Encoded, nil)
68+
if err != nil {
69+
t.Fatalf("unexpected error %v", err)
70+
}
71+
pemBytes := pem.EncodeToMemory(&pem.Block{
72+
Bytes: encBytes,
73+
Type: cosign.SigstorePrivateKeyPemType,
74+
})
75+
certPrivKeyRef := writeFile(t, td, string(pemBytes), "certkey.pem")
76+
keyOpts.KeyRef = certPrivKeyRef
77+
78+
_, err = SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, signCertPath, "", false, "", "", false)
5079
if err != nil {
5180
t.Fatalf("unexpected error %v", err)
5281
}

cmd/cosign/cli/signblob.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ func SignBlob() *cobra.Command {
127127
o.OutputSignature = o.Output
128128
}
129129

130-
if _, err := sign.SignBlobCmd(cmd.Context(), ro, ko, blob, o.Base64Output, o.OutputSignature, o.OutputCertificate, o.TlogUpload); err != nil {
130+
if _, err := sign.SignBlobCmd(cmd.Context(), ro, ko, blob, o.Cert, o.CertChain, o.Base64Output, o.OutputSignature, o.OutputCertificate, o.TlogUpload); err != nil {
131131
return fmt.Errorf("signing %s: %w", blob, err)
132132
}
133133
}

cmd/cosign/cli/signcommon/common.go

Lines changed: 27 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -83,72 +83,53 @@ func (c *SignerVerifier) Bytes(ctx context.Context) ([]byte, error) {
8383
return pemBytes, nil
8484
}
8585

86-
func getEphemeralKeypairOptions(signingAlgorithm string) (*sign.EphemeralKeypairOptions, error) {
87-
keyDetails, err := ParseSignatureAlgorithmFlag(signingAlgorithm)
88-
if err != nil {
89-
return nil, fmt.Errorf("parsing signature algorithm: %w", err)
90-
}
91-
92-
return &sign.EphemeralKeypairOptions{
93-
Algorithm: keyDetails,
94-
}, nil
95-
}
96-
9786
// GetKeypairAndToken creates a keypair object from provided key or cert flags or generates an ephemeral key.
9887
// For an ephemeral key, it also uses the key to fetch an OIDC token, the pair of which are later used to get a Fulcio cert.
99-
func GetKeypairAndToken(ctx context.Context, ko options.KeyOpts, cert, certChain string) (sign.Keypair, []byte, string, error) {
88+
func GetKeypairAndToken(ctx context.Context, ko options.KeyOpts, cert, certChain string) (sign.Keypair, *SignerVerifier, []byte, string, error) {
10089
var keypair sign.Keypair
10190
var ephemeralKeypair bool
10291
var idToken string
10392
var sv *SignerVerifier
10493
var certBytes []byte
10594
var err error
10695

107-
if ko.Sk || ko.Slot != "" || ko.KeyRef != "" || cert != "" {
108-
sv, _, err = signerFromKeyOpts(ctx, cert, certChain, ko)
109-
if err != nil {
110-
return nil, nil, "", fmt.Errorf("getting signer: %w", err)
111-
}
112-
keypair, err = key.NewSignerVerifierKeypair(sv, ko.DefaultLoadOptions)
113-
if err != nil {
114-
return nil, nil, "", fmt.Errorf("creating signerverifier keypair: %w", err)
115-
}
116-
certBytes = sv.Cert
117-
} else {
118-
ephemeralKeypairOptions, err := getEphemeralKeypairOptions(ko.SigningAlgorithm)
119-
if err != nil {
120-
return nil, nil, "", fmt.Errorf("getting ephemeral keypair options: %w", err)
121-
}
122-
keypair, err = sign.NewEphemeralKeypair(ephemeralKeypairOptions)
123-
if err != nil {
124-
return nil, nil, "", fmt.Errorf("generating keypair: %w", err)
125-
}
126-
ephemeralKeypair = true
96+
sv, ephemeralKeypair, err = signerFromKeyOpts(ctx, cert, certChain, ko)
97+
if err != nil {
98+
return nil, nil, nil, "", fmt.Errorf("getting signer: %w", err)
12799
}
100+
keypair, err = key.NewSignerVerifierKeypair(sv, ko.DefaultLoadOptions)
101+
if err != nil {
102+
return nil, nil, nil, "", fmt.Errorf("creating signerverifier keypair: %w", err)
103+
}
104+
certBytes = sv.Cert
128105
defer func() {
129106
if sv != nil {
130107
sv.Close()
131108
}
132109
}()
133110

134111
if ephemeralKeypair || ko.IssueCertificateForExistingKey {
135-
idToken, err = auth.RetrieveIDToken(ctx, auth.IDTokenConfig{
136-
TokenOrPath: ko.IDToken,
137-
DisableProviders: ko.OIDCDisableProviders,
138-
Provider: ko.OIDCProvider,
139-
AuthFlow: ko.FulcioAuthFlow,
140-
SkipConfirm: ko.SkipConfirmation,
141-
OIDCServices: ko.SigningConfig.OIDCProviderURLs(),
142-
ClientID: ko.OIDCClientID,
143-
ClientSecret: ko.OIDCClientSecret,
144-
RedirectURL: ko.OIDCRedirectURL,
145-
})
112+
if ko.SigningConfig == nil {
113+
sv, err = keylessSigner(ctx, ko, sv)
114+
} else {
115+
idToken, err = auth.RetrieveIDToken(ctx, auth.IDTokenConfig{
116+
TokenOrPath: ko.IDToken,
117+
DisableProviders: ko.OIDCDisableProviders,
118+
Provider: ko.OIDCProvider,
119+
AuthFlow: ko.FulcioAuthFlow,
120+
SkipConfirm: ko.SkipConfirmation,
121+
OIDCServices: ko.SigningConfig.OIDCProviderURLs(),
122+
ClientID: ko.OIDCClientID,
123+
ClientSecret: ko.OIDCClientSecret,
124+
RedirectURL: ko.OIDCRedirectURL,
125+
})
126+
}
146127
if err != nil {
147-
return nil, nil, "", fmt.Errorf("retrieving ID token: %w", err)
128+
return nil, nil, nil, "", fmt.Errorf("retrieving ID token: %w", err)
148129
}
149130
}
150131

151-
return keypair, certBytes, idToken, nil
132+
return keypair, sv, certBytes, idToken, nil
152133
}
153134

154135
func keylessSigner(ctx context.Context, ko options.KeyOpts, sv *SignerVerifier) (*SignerVerifier, error) {
@@ -528,7 +509,7 @@ func WriteBundle(ctx context.Context, sv *SignerVerifier, rekorEntry *models.Log
528509

529510
// WriteNewBundleWithSigningConfig uses signing config and trusted root to fetch responses from services for the bundle and writes the bundle to the OCI remote layer.
530511
func WriteNewBundleWithSigningConfig(ctx context.Context, ko options.KeyOpts, cert, certChain string, bundleOpts CommonBundleOpts, signingConfig *root.SigningConfig, trustedMaterial root.TrustedMaterial) error {
531-
keypair, certBytes, idToken, err := GetKeypairAndToken(ctx, ko, cert, certChain)
512+
keypair, _, certBytes, idToken, err := GetKeypairAndToken(ctx, ko, cert, certChain)
532513
if err != nil {
533514
return fmt.Errorf("getting keypair and token: %w", err)
534515
}

doc/cosign_attest-blob.md

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

doc/cosign_sign-blob.md

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)