diff --git a/cmd/cosign/cli/fulcio/fulcio.go b/cmd/cosign/cli/fulcio/fulcio.go index d84c6192984..2e20c53a655 100644 --- a/cmd/cosign/cli/fulcio/fulcio.go +++ b/cmd/cosign/cli/fulcio/fulcio.go @@ -18,26 +18,23 @@ package fulcio import ( "context" "crypto" - "crypto/ecdsa" - "crypto/rand" - "crypto/sha256" "crypto/x509" "fmt" "net/url" "os" - - "go.step.sm/crypto/jose" - "golang.org/x/term" + "strings" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/cmd/cosign/cli/sign/privacy" "github.com/sigstore/cosign/v2/internal/pkg/cosign/fulcio/fulcioroots" "github.com/sigstore/cosign/v2/internal/ui" - "github.com/sigstore/cosign/v2/pkg/cosign" "github.com/sigstore/cosign/v2/pkg/providers" "github.com/sigstore/fulcio/pkg/api" + "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/oauthflow" "github.com/sigstore/sigstore/pkg/signature" + "go.step.sm/crypto/jose" + "golang.org/x/term" ) const ( @@ -58,28 +55,29 @@ func (rf *realConnector) OIDConnect(url, clientID, secret, redirectURL string) ( return oauthflow.OIDConnect(url, clientID, secret, redirectURL, rf.flow) } -func getCertForOauthID(priv *ecdsa.PrivateKey, fc api.LegacyClient, connector oidcConnector, oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL string) (*api.CertificateResponse, error) { - pubBytes, err := x509.MarshalPKIXPublicKey(&priv.PublicKey) +func getCertForOauthID(sv signature.SignerVerifier, fc api.LegacyClient, connector oidcConnector, oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL string) (*api.CertificateResponse, error) { + tok, err := connector.OIDConnect(oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL) if err != nil { return nil, err } - tok, err := connector.OIDConnect(oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL) + publicKey, err := sv.PublicKey() + if err != nil { + return nil, err + } + pubBytes, err := cryptoutils.MarshalPublicKeyToPEM(publicKey) if err != nil { return nil, err } - // Sign the email address as part of the request - h := sha256.Sum256([]byte(tok.Subject)) - proof, err := ecdsa.SignASN1(rand.Reader, priv, h[:]) + proof, err := sv.SignMessage(strings.NewReader(tok.Subject)) if err != nil { return nil, err } cr := api.CertificateRequest{ PublicKey: api.Key{ - Algorithm: "ecdsa", - Content: pubBytes, + Content: pubBytes, }, SignedEmailAddress: proof, } @@ -88,7 +86,7 @@ func getCertForOauthID(priv *ecdsa.PrivateKey, fc api.LegacyClient, connector oi } // GetCert returns the PEM-encoded signature of the OIDC identity returned as part of an interactive oauth2 flow plus the PEM-encoded cert chain. -func GetCert(ctx context.Context, priv *ecdsa.PrivateKey, idToken, flow, oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL string, fClient api.LegacyClient) (*api.CertificateResponse, error) { +func GetCert(ctx context.Context, sv signature.SignerVerifier, idToken, flow, oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL string, fClient api.LegacyClient) (*api.CertificateResponse, error) { c := &realConnector{} switch flow { case flowDevice: @@ -101,18 +99,17 @@ func GetCert(ctx context.Context, priv *ecdsa.PrivateKey, idToken, flow, oidcIss return nil, fmt.Errorf("unsupported oauth flow: %s", flow) } - return getCertForOauthID(priv, fClient, c, oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL) + return getCertForOauthID(sv, fClient, c, oidcIssuer, oidcClientID, oidcClientSecret, oidcRedirectURL) } type Signer struct { Cert []byte Chain []byte SCT []byte - pub *ecdsa.PublicKey - *signature.ECDSASignerVerifier + signature.SignerVerifier } -func NewSigner(ctx context.Context, ko options.KeyOpts) (*Signer, error) { +func NewSigner(ctx context.Context, ko options.KeyOpts, signer signature.SignerVerifier) (*Signer, error) { fClient, err := NewClient(ko.FulcioURL) if err != nil { return nil, fmt.Errorf("creating Fulcio client: %w", err) @@ -139,14 +136,6 @@ func NewSigner(ctx context.Context, ko options.KeyOpts) (*Signer, error) { } } - priv, err := cosign.GeneratePrivateKey() - if err != nil { - return nil, fmt.Errorf("generating cert: %w", err) - } - signer, err := signature.LoadECDSASignerVerifier(priv, crypto.SHA256) - if err != nil { - return nil, err - } fmt.Fprintln(os.Stderr, "Retrieving signed certificate...") var flow string @@ -175,24 +164,23 @@ func NewSigner(ctx context.Context, ko options.KeyOpts) (*Signer, error) { } flow = flowNormal } - Resp, err := GetCert(ctx, priv, idToken, flow, ko.OIDCIssuer, ko.OIDCClientID, ko.OIDCClientSecret, ko.OIDCRedirectURL, fClient) // TODO, use the chain. + Resp, err := GetCert(ctx, signer, idToken, flow, ko.OIDCIssuer, ko.OIDCClientID, ko.OIDCClientSecret, ko.OIDCRedirectURL, fClient) // TODO, use the chain. if err != nil { return nil, fmt.Errorf("retrieving cert: %w", err) } f := &Signer{ - pub: &priv.PublicKey, - ECDSASignerVerifier: signer, - Cert: Resp.CertPEM, - Chain: Resp.ChainPEM, - SCT: Resp.SCT, + SignerVerifier: signer, + Cert: Resp.CertPEM, + Chain: Resp.ChainPEM, + SCT: Resp.SCT, } return f, nil } func (f *Signer) PublicKey(opts ...signature.PublicKeyOption) (crypto.PublicKey, error) { - return &f.pub, nil + return f.SignerVerifier.PublicKey() } var _ signature.Signer = &Signer{} diff --git a/cmd/cosign/cli/fulcio/fulcio_test.go b/cmd/cosign/cli/fulcio/fulcio_test.go index e35a38e51fc..c36f28b0816 100644 --- a/cmd/cosign/cli/fulcio/fulcio_test.go +++ b/cmd/cosign/cli/fulcio/fulcio_test.go @@ -16,9 +16,12 @@ package fulcio import ( + "context" + "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/x509" "encoding/pem" "errors" "net/http" @@ -26,8 +29,12 @@ import ( "testing" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" + "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/cosign/v2/test" "github.com/sigstore/fulcio/pkg/api" + "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/oauthflow" + "github.com/sigstore/sigstore/pkg/signature" ) type testFlow struct { @@ -64,6 +71,10 @@ func TestGetCertForOauthID(t *testing.T) { if err != nil { t.Fatalf("Could not generate ecdsa keypair for test: %v", err) } + sv, err := signature.LoadECDSASignerVerifier(testKey, crypto.SHA256) + if err != nil { + t.Fatalf("Could not create a signer: %v", err) + } testCases := []struct { desc string @@ -118,7 +129,7 @@ func TestGetCertForOauthID(t *testing.T) { err: tc.tokenGetterErr, } - resp, err := getCertForOauthID(testKey, tscp, &tf, "", "", "", "") + resp, err := getCertForOauthID(sv, tscp, &tf, "", "", "", "") if err != nil { if !tc.expectErr { @@ -173,3 +184,46 @@ func TestNewClient(t *testing.T) { t.Fatal("no requests were received") } } + +func TestNewSigner(t *testing.T) { + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, _, _ := test.GenerateLeafCert("subject", "oidc-issuer", rootCert, rootKey) + pemChain, _ := cryptoutils.MarshalCertificatesToPEM([]*x509.Certificate{leafCert, rootCert}) + + testServer := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusCreated) + _, _ = w.Write(pemChain) + })) + defer testServer.Close() + + // success: Generate a random key and create a corresponding + // SignerVerifier. + ctx := context.TODO() + ko := options.KeyOpts{ + OIDCDisableProviders: true, + // random test token + IDToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", + FulcioURL: testServer.URL, + FulcioAuthFlow: "token", + } + privKey, err := cosign.GeneratePrivateKey() + if err != nil { + t.Fatal(err) + } + sv, err := signature.LoadECDSASignerVerifier(privKey, crypto.SHA256) + if err != nil { + t.Fatal(err) + } + signer, err := NewSigner(ctx, ko, sv) + if err != nil { + t.Fatalf("unexpected error creating signer: %v", err) + } + responsePEMChain := string(signer.Cert) + string(signer.Chain) + if responsePEMChain != string(pemChain) { + t.Fatalf("response certificates not equal, got %v, expected %v", responsePEMChain, pemChain) + } + if signer.SignerVerifier == nil { + t.Fatalf("missing signer/verifier") + } +} diff --git a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go index f52b473bb4c..146bff483a4 100644 --- a/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go +++ b/cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go @@ -23,10 +23,11 @@ import ( "github.com/sigstore/cosign/v2/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/sigstore/pkg/signature" ) -func NewSigner(ctx context.Context, ko options.KeyOpts) (*fulcio.Signer, error) { - fs, err := fulcio.NewSigner(ctx, ko) +func NewSigner(ctx context.Context, ko options.KeyOpts, signer signature.SignerVerifier) (*fulcio.Signer, error) { + fs, err := fulcio.NewSigner(ctx, ko, signer) if err != nil { return nil, err } diff --git a/cmd/cosign/cli/options/key.go b/cmd/cosign/cli/options/key.go index b9783a0b591..1100ca083d1 100644 --- a/cmd/cosign/cli/options/key.go +++ b/cmd/cosign/cli/options/key.go @@ -36,6 +36,9 @@ type KeyOpts struct { TSAServerURL string RFC3161TimestampPath string TSACertChainPath string + // IssueCertificate controls whether to issue a certificate when a key is + // provided. + IssueCertificateForExistingKey bool // FulcioAuthFlow is the auth flow to use when authenticating against // Fulcio. See https://pkg.go.dev/github.com/sigstore/cosign/v2/cmd/cosign/cli/fulcio#pkg-constants diff --git a/cmd/cosign/cli/options/sign.go b/cmd/cosign/cli/options/sign.go index 860f1fdd931..eea06d8d660 100644 --- a/cmd/cosign/cli/options/sign.go +++ b/cmd/cosign/cli/options/sign.go @@ -34,6 +34,7 @@ type SignOptions struct { SkipConfirmation bool TlogUpload bool TSAServerURL string + IssueCertificate bool Rekor RekorOptions Fulcio FulcioOptions @@ -98,4 +99,7 @@ func (o *SignOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.TSAServerURL, "timestamp-server-url", "", "url to the Timestamp RFC3161 server, default none") + + cmd.Flags().BoolVar(&o.IssueCertificate, "issue-certificate", false, + "issue a code signing certificate from Fulcio, even if a key is provided") } diff --git a/cmd/cosign/cli/options/signblob.go b/cmd/cosign/cli/options/signblob.go index 8cc23e23a35..654c7ff8209 100644 --- a/cmd/cosign/cli/options/signblob.go +++ b/cmd/cosign/cli/options/signblob.go @@ -37,6 +37,7 @@ type SignBlobOptions struct { TlogUpload bool TSAServerURL string RFC3161TimestampPath string + IssueCertificate bool } var _ Interface = (*SignBlobOptions)(nil) @@ -82,4 +83,7 @@ func (o *SignBlobOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.RFC3161TimestampPath, "rfc3161-timestamp", "", "write the RFC3161 timestamp to a file") _ = cmd.Flags().SetAnnotation("rfc3161-timestamp", cobra.BashCompFilenameExt, []string{}) + + cmd.Flags().BoolVar(&o.IssueCertificate, "issue-certificate", false, + "issue a code signing certificate from Fulcio, even if a key is provided") } diff --git a/cmd/cosign/cli/sign.go b/cmd/cosign/cli/sign.go index 81d02162b99..879c46cf6e8 100644 --- a/cmd/cosign/cli/sign.go +++ b/cmd/cosign/cli/sign.go @@ -95,22 +95,23 @@ race conditions or (worse) malicious tampering. return err } ko := options.KeyOpts{ - KeyRef: o.Key, - PassFunc: generate.GetPass, - Sk: o.SecurityKey.Use, - Slot: o.SecurityKey.Slot, - FulcioURL: o.Fulcio.URL, - IDToken: o.Fulcio.IdentityToken, - InsecureSkipFulcioVerify: o.Fulcio.InsecureSkipFulcioVerify, - RekorURL: o.Rekor.URL, - OIDCIssuer: o.OIDC.Issuer, - OIDCClientID: o.OIDC.ClientID, - OIDCClientSecret: oidcClientSecret, - OIDCRedirectURL: o.OIDC.RedirectURL, - OIDCDisableProviders: o.OIDC.DisableAmbientProviders, - OIDCProvider: o.OIDC.Provider, - SkipConfirmation: o.SkipConfirmation, - TSAServerURL: o.TSAServerURL, + KeyRef: o.Key, + PassFunc: generate.GetPass, + Sk: o.SecurityKey.Use, + Slot: o.SecurityKey.Slot, + FulcioURL: o.Fulcio.URL, + IDToken: o.Fulcio.IdentityToken, + InsecureSkipFulcioVerify: o.Fulcio.InsecureSkipFulcioVerify, + RekorURL: o.Rekor.URL, + OIDCIssuer: o.OIDC.Issuer, + OIDCClientID: o.OIDC.ClientID, + OIDCClientSecret: oidcClientSecret, + OIDCRedirectURL: o.OIDC.RedirectURL, + OIDCDisableProviders: o.OIDC.DisableAmbientProviders, + OIDCProvider: o.OIDC.Provider, + SkipConfirmation: o.SkipConfirmation, + TSAServerURL: o.TSAServerURL, + IssueCertificateForExistingKey: o.IssueCertificate, } if err := sign.SignCmd(ro, ko, *o, args); err != nil { if o.Attachment == "" { diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index eed2b0d21e2..71c3834fe41 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -18,6 +18,7 @@ package sign import ( "bytes" "context" + "crypto" "crypto/x509" "encoding/pem" "errors" @@ -483,18 +484,33 @@ func signerFromKeyRef(ctx context.Context, certPath, certChainPath, keyRef strin return certSigner, nil } -func keylessSigner(ctx context.Context, ko options.KeyOpts) (*SignerVerifier, error) { +func signerFromNewKey() (*SignerVerifier, error) { + privKey, err := cosign.GeneratePrivateKey() + if err != nil { + return nil, fmt.Errorf("generating cert: %w", err) + } + sv, err := signature.LoadECDSASignerVerifier(privKey, crypto.SHA256) + if err != nil { + return nil, err + } + + return &SignerVerifier{ + SignerVerifier: sv, + }, nil +} + +func keylessSigner(ctx context.Context, ko options.KeyOpts, sv *SignerVerifier) (*SignerVerifier, error) { var ( k *fulcio.Signer err error ) if ko.InsecureSkipFulcioVerify { - if k, err = fulcio.NewSigner(ctx, ko); err != nil { + if k, err = fulcio.NewSigner(ctx, ko, sv); err != nil { return nil, fmt.Errorf("getting key from Fulcio: %w", err) } } else { - if k, err = fulcioverifier.NewSigner(ctx, ko); err != nil { + if k, err = fulcioverifier.NewSigner(ctx, ko, sv); err != nil { return nil, fmt.Errorf("getting key from Fulcio: %w", err) } } @@ -507,17 +523,28 @@ func keylessSigner(ctx context.Context, ko options.KeyOpts) (*SignerVerifier, er } func SignerFromKeyOpts(ctx context.Context, certPath string, certChainPath string, ko options.KeyOpts) (*SignerVerifier, error) { - if ko.Sk { - return signerFromSecurityKey(ctx, ko.Slot) + var sv *SignerVerifier + var err error + genKey := false + switch { + case ko.Sk: + sv, err = signerFromSecurityKey(ctx, ko.Slot) + case ko.KeyRef != "": + sv, err = signerFromKeyRef(ctx, certPath, certChainPath, ko.KeyRef, ko.PassFunc) + default: + genKey = true + ui.Infof(ctx, "Generating ephemeral keys...") + sv, err = signerFromNewKey() + } + if err != nil { + return nil, err } - if ko.KeyRef != "" { - return signerFromKeyRef(ctx, certPath, certChainPath, ko.KeyRef, ko.PassFunc) + if ko.IssueCertificateForExistingKey || genKey { + return keylessSigner(ctx, ko, sv) } - // Default Keyless! - ui.Infof(ctx, "Generating ephemeral keys...") - return keylessSigner(ctx, ko) + return sv, nil } type SignerVerifier struct { diff --git a/cmd/cosign/cli/signblob.go b/cmd/cosign/cli/signblob.go index d8c2cfc0e83..b09d6ee3b31 100644 --- a/cmd/cosign/cli/signblob.go +++ b/cmd/cosign/cli/signblob.go @@ -66,23 +66,24 @@ func SignBlob() *cobra.Command { return err } ko := options.KeyOpts{ - KeyRef: o.Key, - PassFunc: generate.GetPass, - Sk: o.SecurityKey.Use, - Slot: o.SecurityKey.Slot, - FulcioURL: o.Fulcio.URL, - IDToken: o.Fulcio.IdentityToken, - InsecureSkipFulcioVerify: o.Fulcio.InsecureSkipFulcioVerify, - RekorURL: o.Rekor.URL, - OIDCIssuer: o.OIDC.Issuer, - OIDCClientID: o.OIDC.ClientID, - OIDCClientSecret: oidcClientSecret, - OIDCRedirectURL: o.OIDC.RedirectURL, - OIDCDisableProviders: o.OIDC.DisableAmbientProviders, - BundlePath: o.BundlePath, - SkipConfirmation: o.SkipConfirmation, - TSAServerURL: o.TSAServerURL, - RFC3161TimestampPath: o.RFC3161TimestampPath, + KeyRef: o.Key, + PassFunc: generate.GetPass, + Sk: o.SecurityKey.Use, + Slot: o.SecurityKey.Slot, + FulcioURL: o.Fulcio.URL, + IDToken: o.Fulcio.IdentityToken, + InsecureSkipFulcioVerify: o.Fulcio.InsecureSkipFulcioVerify, + RekorURL: o.Rekor.URL, + OIDCIssuer: o.OIDC.Issuer, + OIDCClientID: o.OIDC.ClientID, + OIDCClientSecret: oidcClientSecret, + OIDCRedirectURL: o.OIDC.RedirectURL, + OIDCDisableProviders: o.OIDC.DisableAmbientProviders, + BundlePath: o.BundlePath, + SkipConfirmation: o.SkipConfirmation, + TSAServerURL: o.TSAServerURL, + RFC3161TimestampPath: o.RFC3161TimestampPath, + IssueCertificateForExistingKey: o.IssueCertificate, } for _, blob := range args { diff --git a/doc/cosign_sign-blob.md b/doc/cosign_sign-blob.md index 414ec2c6ad7..6a3b7f09e74 100644 --- a/doc/cosign_sign-blob.md +++ b/doc/cosign_sign-blob.md @@ -39,6 +39,7 @@ cosign sign-blob [flags] -h, --help help for sign-blob --identity-token string [EXPERIMENTAL] identity token to use for certificate from fulcio. the token or a path to a file containing the token is accepted. --insecure-skip-verify [EXPERIMENTAL] skip verifying fulcio published to the SCT (this should only be used for testing). + --issue-certificate issue a code signing certificate from Fulcio, even if a key is provided --key string path to the private key file, KMS URI or Kubernetes Secret --oidc-client-id string [EXPERIMENTAL] OIDC client ID for application (default "sigstore") --oidc-client-secret-file string [EXPERIMENTAL] Path to file containing OIDC client secret for application diff --git a/doc/cosign_sign.md b/doc/cosign_sign.md index 9a7229e225f..11fb0dd25d5 100644 --- a/doc/cosign_sign.md +++ b/doc/cosign_sign.md @@ -77,6 +77,7 @@ cosign sign [flags] -h, --help help for sign --identity-token string [EXPERIMENTAL] identity token to use for certificate from fulcio. the token or a path to a file containing the token is accepted. --insecure-skip-verify [EXPERIMENTAL] skip verifying fulcio published to the SCT (this should only be used for testing). + --issue-certificate issue a code signing certificate from Fulcio, even if a key is provided --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the private key file, KMS URI or Kubernetes Secret --oidc-client-id string [EXPERIMENTAL] OIDC client ID for application (default "sigstore") diff --git a/test/e2e_test.go b/test/e2e_test.go index e7530396a84..abe9b1cc647 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -75,6 +75,7 @@ import ( const ( serverEnv = "REKOR_SERVER" rekorURL = "https://rekor.sigstore.dev" + fulcioURL = "https://fulcio.sigstore.dev" ) var keyPass = []byte("hello") @@ -847,6 +848,40 @@ func TestRekorBundle(t *testing.T) { must(verify(pubKeyPath, imgName, true, nil, ""), t) } +func TestFulcioBundle(t *testing.T) { + repo, stop := reg(t) + defer stop() + td := t.TempDir() + + imgName := path.Join(repo, "cosign-e2e") + + _, _, cleanup := mkimage(t, imgName) + defer cleanup() + + _, privKeyPath, pubKeyPath := keypair(t, td) + + ko := options.KeyOpts{ + KeyRef: privKeyPath, + PassFunc: passFunc, + RekorURL: rekorURL, + FulcioURL: fulcioURL, + } + so := options.SignOptions{ + Upload: true, + IssueCertificate: true, + } + + // Sign the image + must(sign.SignCmd(ro, ko, so, []string{imgName}), t) + // Make sure verify works + must(verify(pubKeyPath, imgName, true, nil, ""), t) + + // Make sure offline verification works with bundling + // use rekor prod since we have hardcoded the public key + os.Setenv(serverEnv, "notreal") + must(verify(pubKeyPath, imgName, true, nil, ""), t) +} + func TestRFC3161Timestamp(t *testing.T) { // TSA server needed to create timestamp viper.Set("timestamp-signer", "memory")