Skip to content

Commit 3b22395

Browse files
committed
Fix cert attachment for new bundle with signing config
Without this change, if an image is signed with `--certificate` and also using signing config and the new bundle format (the defaults), the logic routes to sigstore-go signing which does not natively understand signing with a cert that is not issued by Fulcio. This means that only the public key is uploaded to the bundle, so the image cannot be verified using the certificate. This change ensures the certificate is passed through to the bundle if it is provided with `sign`, `attest`, and `attest-blob` . The `sign-blob` command does not support `--certificate`. Signed-off-by: Colleen Murphy <[email protected]>
1 parent f550963 commit 3b22395

File tree

4 files changed

+191
-18
lines changed

4 files changed

+191
-18
lines changed

cmd/cosign/cli/sign/sign_blob.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ func SignBlobCmd(ctx context.Context, ro *options.RootOptions, ko options.KeyOpt
7777
}
7878

7979
if ko.SigningConfig != nil {
80-
keypair, idToken, err := signcommon.GetKeypairAndToken(ctx, ko, "", "")
80+
keypair, _, idToken, err := signcommon.GetKeypairAndToken(ctx, ko, "", "")
8181
if err != nil {
8282
return nil, fmt.Errorf("getting keypair and token: %w", err)
8383
}
@@ -95,7 +95,7 @@ func SignBlobCmd(ctx context.Context, ro *options.RootOptions, ko options.KeyOpt
9595
Data: data,
9696
}
9797

98-
bundle, err := cbundle.SignData(ctx, content, keypair, idToken, ko.SigningConfig, ko.TrustedMaterial)
98+
bundle, err := cbundle.SignData(ctx, content, keypair, idToken, nil, ko.SigningConfig, ko.TrustedMaterial)
9999
if err != nil {
100100
return nil, fmt.Errorf("signing bundle: %w", err)
101101
}

cmd/cosign/cli/signcommon/common.go

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,30 +96,32 @@ func getEphemeralKeypairOptions(signingAlgorithm string) (*sign.EphemeralKeypair
9696

9797
// GetKeypairAndToken creates a keypair object from provided key or cert flags or generates an ephemeral key.
9898
// 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, string, error) {
99+
func GetKeypairAndToken(ctx context.Context, ko options.KeyOpts, cert, certChain string) (sign.Keypair, []byte, string, error) {
100100
var keypair sign.Keypair
101101
var ephemeralKeypair bool
102102
var idToken string
103103
var sv *SignerVerifier
104+
var certBytes []byte
104105
var err error
105106

106107
if ko.Sk || ko.Slot != "" || ko.KeyRef != "" || cert != "" {
107108
sv, _, err = signerFromKeyOpts(ctx, cert, certChain, ko)
108109
if err != nil {
109-
return nil, "", fmt.Errorf("getting signer: %w", err)
110+
return nil, nil, "", fmt.Errorf("getting signer: %w", err)
110111
}
111112
keypair, err = key.NewSignerVerifierKeypair(sv, ko.DefaultLoadOptions)
112113
if err != nil {
113-
return nil, "", fmt.Errorf("creating signerverifier keypair: %w", err)
114+
return nil, nil, "", fmt.Errorf("creating signerverifier keypair: %w", err)
114115
}
116+
certBytes = sv.Cert
115117
} else {
116118
ephemeralKeypairOptions, err := getEphemeralKeypairOptions(ko.SigningAlgorithm)
117119
if err != nil {
118-
return nil, "", fmt.Errorf("getting ephemeral keypair options: %w", err)
120+
return nil, nil, "", fmt.Errorf("getting ephemeral keypair options: %w", err)
119121
}
120122
keypair, err = sign.NewEphemeralKeypair(ephemeralKeypairOptions)
121123
if err != nil {
122-
return nil, "", fmt.Errorf("generating keypair: %w", err)
124+
return nil, nil, "", fmt.Errorf("generating keypair: %w", err)
123125
}
124126
ephemeralKeypair = true
125127
}
@@ -142,11 +144,11 @@ func GetKeypairAndToken(ctx context.Context, ko options.KeyOpts, cert, certChain
142144
RedirectURL: ko.OIDCRedirectURL,
143145
})
144146
if err != nil {
145-
return nil, "", fmt.Errorf("retrieving ID token: %w", err)
147+
return nil, nil, "", fmt.Errorf("retrieving ID token: %w", err)
146148
}
147149
}
148150

149-
return keypair, idToken, nil
151+
return keypair, certBytes, idToken, nil
150152
}
151153

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

527529
// WriteNewBundleWithSigningConfig uses signing config and trusted root to fetch responses from services for the bundle and writes the bundle to the OCI remote layer.
528530
func WriteNewBundleWithSigningConfig(ctx context.Context, ko options.KeyOpts, cert, certChain string, bundleOpts CommonBundleOpts, signingConfig *root.SigningConfig, trustedMaterial root.TrustedMaterial) error {
529-
keypair, idToken, err := GetKeypairAndToken(ctx, ko, cert, certChain)
531+
keypair, certBytes, idToken, err := GetKeypairAndToken(ctx, ko, cert, certChain)
530532
if err != nil {
531533
return fmt.Errorf("getting keypair and token: %w", err)
532534
}
@@ -535,7 +537,7 @@ func WriteNewBundleWithSigningConfig(ctx context.Context, ko options.KeyOpts, ce
535537
Data: bundleOpts.Payload,
536538
PayloadType: "application/vnd.in-toto+json",
537539
}
538-
bundle, err := cbundle.SignData(ctx, content, keypair, idToken, signingConfig, trustedMaterial)
540+
bundle, err := cbundle.SignData(ctx, content, keypair, idToken, certBytes, signingConfig, trustedMaterial)
539541
if err != nil {
540542
return fmt.Errorf("signing bundle: %w", err)
541543
}

pkg/cosign/bundle/sign.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@ import (
2929
"google.golang.org/protobuf/encoding/protojson"
3030
)
3131

32-
func SignData(ctx context.Context, content sign.Content, keypair sign.Keypair, idToken string, signingConfig *root.SigningConfig, trustedMaterial root.TrustedMaterial) ([]byte, error) {
32+
func SignData(ctx context.Context, content sign.Content, keypair sign.Keypair, idToken string, cert []byte, signingConfig *root.SigningConfig, trustedMaterial root.TrustedMaterial) ([]byte, error) {
3333
var opts sign.BundleOptions
3434

3535
if trustedMaterial != nil {
3636
opts.TrustedRoot = trustedMaterial
3737
}
3838

39-
if idToken != "" {
39+
switch {
40+
case idToken != "":
4041
if len(signingConfig.FulcioCertificateAuthorityURLs()) == 0 {
4142
return nil, fmt.Errorf("no fulcio URLs provided in signing config")
4243
}
@@ -53,7 +54,9 @@ func SignData(ctx context.Context, content sign.Content, keypair sign.Keypair, i
5354
opts.CertificateProviderOptions = &sign.CertificateProviderOptions{
5455
IDToken: idToken,
5556
}
56-
} else {
57+
case cert != nil:
58+
opts.CertificateProvider = &localCertProvider{cert}
59+
default:
5760
publicKeyPem, err := keypair.GetPublicKeyPem()
5861
if err != nil {
5962
return nil, err
@@ -130,3 +133,15 @@ type verifyTrustedMaterial struct {
130133
func (v *verifyTrustedMaterial) PublicKeyVerifier(hint string) (root.TimeConstrainedVerifier, error) {
131134
return v.keyTrustedMaterial.PublicKeyVerifier(hint)
132135
}
136+
137+
type localCertProvider struct {
138+
cert []byte
139+
}
140+
141+
func (c *localCertProvider) GetCertificate(_ context.Context, _ sign.Keypair, _ *sign.CertificateProviderOptions) ([]byte, error) {
142+
certBlock, _ := pem.Decode(c.cert)
143+
if certBlock == nil {
144+
return nil, fmt.Errorf("could not decode cert")
145+
}
146+
return certBlock.Bytes, nil
147+
}

test/e2e_test.go

Lines changed: 160 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,20 @@ import (
2121
"bytes"
2222
"context"
2323
"crypto"
24+
"crypto/ecdsa"
2425
"crypto/ed25519"
26+
"crypto/elliptic"
2527
"crypto/rand"
2628
"crypto/sha256"
2729
"crypto/x509"
30+
"crypto/x509/pkix"
2831
"encoding/base64"
2932
"encoding/hex"
3033
"encoding/json"
3134
"encoding/pem"
3235
"fmt"
3336
"io"
37+
"math/big"
3438
"net/http"
3539
"net/http/httptest"
3640
"net/url"
@@ -572,8 +576,7 @@ func downloadTSACerts(downloadDirectory string, tsaServer string) (string, strin
572576
return leafPath, intermediatePath, rootPath, nil
573577
}
574578

575-
func prepareTrustedRoot(t *testing.T, tsaURL string) string {
576-
downloadDirectory := t.TempDir()
579+
func trustedRootCmd(t *testing.T, downloadDirectory, tsaURL string) *trustedroot.CreateCmd {
577580
caPath := filepath.Join(downloadDirectory, "fulcio.crt.pem")
578581
caFP, err := os.Create(caPath)
579582
must(err, t)
@@ -602,8 +605,22 @@ func prepareTrustedRoot(t *testing.T, tsaURL string) string {
602605
must(downloadFile(tsaURL+"/api/v1/timestamp/certchain", tsaFP), t)
603606
cmd.TSACertChainPath = []string{tsaPath}
604607
}
608+
return cmd
609+
}
610+
611+
func prepareTrustedRoot(t *testing.T, tsaURL string) string {
612+
downloadDirectory := t.TempDir()
613+
cmd := trustedRootCmd(t, downloadDirectory, tsaURL)
605614
must(cmd.Exec(context.TODO()), t)
606-
return out
615+
return cmd.Out
616+
}
617+
618+
func prepareTrustedRootWithSelfSignedCertificate(t *testing.T, certPath, tsaURL string) string {
619+
td := t.TempDir()
620+
cmd := trustedRootCmd(t, td, tsaURL)
621+
cmd.CertChain = append(cmd.CertChain, certPath)
622+
must(cmd.Exec(context.TODO()), t)
623+
return cmd.Out
607624
}
608625

609626
func TestSignVerifyWithTUFMirror(t *testing.T) {
@@ -889,8 +906,12 @@ func TestSignAttestVerifyBlobWithSigningConfig(t *testing.T) {
889906
mirror := tufServer.URL
890907
trustedRoot := prepareTrustedRoot(t, tsaServer.URL)
891908
signingConfigStr := prepareSigningConfig(t, fulcioURL, rekorURL, "unused", tsaServer.URL+"/api/v1/timestamp")
909+
sc, err := os.ReadFile(signingConfigStr)
910+
must(err, t)
911+
fmt.Println(string(sc))
912+
fmt.Println(fulcioURL)
892913

893-
_, err := newTUF(tufMirror, []targetInfo{
914+
_, err = newTUF(tufMirror, []targetInfo{
894915
{
895916
name: "trusted_root.json",
896917
source: trustedRoot,
@@ -1094,6 +1115,113 @@ func TestSignAttestVerifyContainerWithSigningConfig(t *testing.T) {
10941115
must(verifyAttestation.Exec(ctx, []string{imgName}), t)
10951116
}
10961117

1118+
func TestSignVerifyContainerWithSigningConfigWithCertificate(t *testing.T) {
1119+
tufLocalCache := t.TempDir()
1120+
t.Setenv("TUF_ROOT", tufLocalCache)
1121+
viper.Set("timestamp-signer", "memory")
1122+
viper.Set("timestamp-signer-hash", "sha256")
1123+
tsaAPIServer := server.NewRestAPIServer("localhost", 0, []string{"http"}, false, 10*time.Second, 10*time.Second)
1124+
tsaServer := httptest.NewServer(tsaAPIServer.GetHandler())
1125+
t.Cleanup(tsaServer.Close)
1126+
tufMirror := t.TempDir()
1127+
tufServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1128+
http.FileServer(http.Dir(tufMirror)).ServeHTTP(w, r)
1129+
}))
1130+
mirror := tufServer.URL
1131+
1132+
cert, privKey, err := selfSignedCertificate()
1133+
must(err, t)
1134+
keysDir := t.TempDir()
1135+
privKeyPath := filepath.Join(keysDir, "priv.key")
1136+
privDer, err := x509.MarshalECPrivateKey(privKey)
1137+
must(err, t)
1138+
keyWriter, err := os.OpenFile(privKeyPath, os.O_WRONLY|os.O_CREATE, 0o600)
1139+
must(err, t)
1140+
defer keyWriter.Close()
1141+
block := &pem.Block{
1142+
Type: "EC PRIVATE KEY",
1143+
Bytes: privDer,
1144+
}
1145+
must(pem.Encode(keyWriter, block), t)
1146+
1147+
certPath := filepath.Join(keysDir, "cert.pem")
1148+
certWriter, err := os.OpenFile(certPath, os.O_WRONLY|os.O_CREATE, 0o600)
1149+
must(err, t)
1150+
defer certWriter.Close()
1151+
block = &pem.Block{
1152+
Type: "CERTIFICATE",
1153+
Bytes: cert.Raw,
1154+
}
1155+
must(pem.Encode(certWriter, block), t)
1156+
1157+
keys, err := cosign.ImportKeyPair(privKeyPath, passFunc)
1158+
must(err, t)
1159+
importKeyPath := filepath.Join(keysDir, "import-priv.key")
1160+
must(os.WriteFile(importKeyPath, keys.PrivateBytes, 0o600), t)
1161+
1162+
trustedRoot := prepareTrustedRootWithSelfSignedCertificate(t, certPath, tsaServer.URL)
1163+
signingConfigStr := prepareSigningConfig(t, fulcioURL, rekorURL, "unused", tsaServer.URL+"/api/v1/timestamp")
1164+
1165+
_, err = newTUF(tufMirror, []targetInfo{
1166+
{
1167+
name: "trusted_root.json",
1168+
source: trustedRoot,
1169+
},
1170+
{
1171+
name: "signing_config.v0.2.json",
1172+
source: signingConfigStr,
1173+
},
1174+
})
1175+
must(err, t)
1176+
1177+
repo, stop := reg(t)
1178+
defer stop()
1179+
imgName := path.Join(repo, "cosign-e2e")
1180+
1181+
_, _, cleanup := mkimage(t, imgName)
1182+
defer cleanup()
1183+
1184+
ctx := context.Background()
1185+
1186+
rootPath := filepath.Join(tufMirror, "1.root.json")
1187+
must(initialize.DoInitialize(ctx, rootPath, mirror), t)
1188+
1189+
ko := options.KeyOpts{
1190+
NewBundleFormat: true,
1191+
SkipConfirmation: true,
1192+
KeyRef: importKeyPath,
1193+
PassFunc: passFunc,
1194+
}
1195+
trustedMaterial, err := cosign.TrustedRoot()
1196+
must(err, t)
1197+
ko.TrustedMaterial = trustedMaterial
1198+
signingConfig, err := cosign.SigningConfig()
1199+
must(err, t)
1200+
ko.SigningConfig = signingConfig
1201+
1202+
// Sign image with cert in bundle format
1203+
so := options.SignOptions{
1204+
Upload: true,
1205+
NewBundleFormat: true,
1206+
Key: importKeyPath,
1207+
Cert: certPath,
1208+
TlogUpload: false,
1209+
}
1210+
must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t)
1211+
1212+
// Verify image
1213+
cmd := cliverify.VerifyCommand{
1214+
CertVerifyOptions: options.CertVerifyOptions{
1215+
CertOidcIssuerRegexp: ".*",
1216+
CertIdentity: "[email protected]",
1217+
},
1218+
NewBundleFormat: true,
1219+
IgnoreSCT: true,
1220+
}
1221+
args := []string{imgName}
1222+
must(cmd.Exec(ctx, args), t)
1223+
}
1224+
10971225
func TestSignVerifyWithSigningConfigWithKey(t *testing.T) {
10981226
tufLocalCache := t.TempDir()
10991227
t.Setenv("TUF_ROOT", tufLocalCache)
@@ -4369,3 +4497,31 @@ func TestAttestVerifyUploadFalse(t *testing.T) {
43694497
must(err, t)
43704498
assert.Contains(t, out.String(), fmt.Sprintf("sha256:%s", hex.EncodeToString(h.Sum(nil))))
43714499
}
4500+
4501+
func selfSignedCertificate() (*x509.Certificate, *ecdsa.PrivateKey, error) {
4502+
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
4503+
if err != nil {
4504+
return nil, nil, err
4505+
}
4506+
ct := &x509.Certificate{
4507+
SerialNumber: big.NewInt(1),
4508+
Subject: pkix.Name{
4509+
CommonName: "self.signed.cert",
4510+
Organization: []string{"dev"},
4511+
},
4512+
EmailAddresses: []string{"[email protected]"},
4513+
NotBefore: time.Now().Add(-1 * time.Minute),
4514+
NotAfter: time.Now().Add(24 * time.Hour),
4515+
KeyUsage: x509.KeyUsageDigitalSignature,
4516+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},
4517+
}
4518+
certBytes, err := x509.CreateCertificate(rand.Reader, ct, ct, &priv.PublicKey, priv)
4519+
if err != nil {
4520+
return nil, nil, err
4521+
}
4522+
cert, err := x509.ParseCertificate(certBytes)
4523+
if err != nil {
4524+
return nil, nil, err
4525+
}
4526+
return cert, priv, nil
4527+
}

0 commit comments

Comments
 (0)