From 32515707f62c740dcf86a183a195587f0eac9bdb Mon Sep 17 00:00:00 2001 From: Anish Shah Date: Sat, 10 Dec 2022 09:07:29 +0000 Subject: [PATCH] Support for fetching Fulcio certs with self-managed key Added a new flag --issue-certificate to sign commands that allows users to fetch Fulcio certificate with self-managed key Signed-off-by: Anish Shah --- cmd/cosign/cli/fulcio/fulcio.go | 94 +++++++++++++++++++--------- cmd/cosign/cli/fulcio/fulcio_test.go | 8 ++- cmd/cosign/cli/options/key.go | 1 + cmd/cosign/cli/options/sign.go | 4 ++ cmd/cosign/cli/options/signblob.go | 4 ++ cmd/cosign/cli/sign.go | 1 + cmd/cosign/cli/sign/sign.go | 4 ++ cmd/cosign/cli/signblob.go | 1 + doc/cosign_sign-blob.md | 1 + doc/cosign_sign.md | 1 + 10 files changed, 89 insertions(+), 30 deletions(-) diff --git a/cmd/cosign/cli/fulcio/fulcio.go b/cmd/cosign/cli/fulcio/fulcio.go index e4f514b83b1d..3048b10ad43c 100644 --- a/cmd/cosign/cli/fulcio/fulcio.go +++ b/cmd/cosign/cli/fulcio/fulcio.go @@ -19,7 +19,9 @@ import ( "context" "crypto" "crypto/ecdsa" + "crypto/ed25519" "crypto/rand" + "crypto/rsa" "crypto/sha256" "crypto/x509" "errors" @@ -33,6 +35,7 @@ import ( "github.com/sigstore/cosign/internal/pkg/cosign/fulcio/fulcioroots" "github.com/sigstore/cosign/pkg/cosign" "github.com/sigstore/cosign/pkg/providers" + key "github.com/sigstore/cosign/pkg/signature" "github.com/sigstore/fulcio/pkg/api" "github.com/sigstore/sigstore/pkg/oauthflow" "github.com/sigstore/sigstore/pkg/signature" @@ -62,27 +65,54 @@ 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) - if err != nil { - return nil, err - } - +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 } - // Sign the email address as part of the request h := sha256.Sum256([]byte(tok.Subject)) - proof, err := ecdsa.SignASN1(rand.Reader, priv, h[:]) - if err != nil { - return nil, err + + var pubBytes, proof []byte + var algorithm string + switch sv := sv.(type) { + case *signature.RSAPSSSignerVerifier: + algorithm = "rsa" + pubBytes = x509.MarshalPKCS1PublicKey(sv.Public().(*rsa.PublicKey)) + // Sign the email address as part of the request + proof, err = sv.Sign(rand.Reader, h[:], signature.SignerOpts{Hash: crypto.SHA256}) + if err != nil { + return nil, err + } + case *signature.ECDSASignerVerifier: + algorithm = "ecdsa" + pubBytes, err = x509.MarshalPKIXPublicKey(sv.Public().(*ecdsa.PublicKey)) + if err != nil { + return nil, err + } + // Sign the email address as part of the request + proof, err = sv.Sign(rand.Reader, h[:], nil) + if err != nil { + return nil, err + } + case *signature.ED25519SignerVerifier: + algorithm = "ed25519" + pubBytes, err = x509.MarshalPKIXPublicKey(sv.Public().(*ed25519.PublicKey)) + if err != nil { + return nil, err + } + // Sign the email address as part of the request + proof, err = sv.Sign(nil, h[:], nil) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("unsupported key type: %T", sv) } cr := api.CertificateRequest{ PublicKey: api.Key{ - Algorithm: "ecdsa", + Algorithm: algorithm, Content: pubBytes, }, SignedEmailAddress: proof, @@ -92,7 +122,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: @@ -105,15 +135,14 @@ 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) { @@ -140,13 +169,21 @@ 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 + var signer signature.SignerVerifier + if ko.IssueCertificate && ko.KeyRef != "" { + signer, err = key.SignerVerifierFromKeyRef(ctx, ko.KeyRef, ko.PassFunc) + if err != nil { + return nil, err + } + } else { + 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...") @@ -172,24 +209,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 b19baa53d021..aa81f83cd2cf 100644 --- a/cmd/cosign/cli/fulcio/fulcio_test.go +++ b/cmd/cosign/cli/fulcio/fulcio_test.go @@ -16,6 +16,7 @@ package fulcio import ( + "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" @@ -28,6 +29,7 @@ import ( "github.com/sigstore/cosign/cmd/cosign/cli/options" "github.com/sigstore/fulcio/pkg/api" "github.com/sigstore/sigstore/pkg/oauthflow" + "github.com/sigstore/sigstore/pkg/signature" ) type testFlow struct { @@ -64,6 +66,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 +124,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 { diff --git a/cmd/cosign/cli/options/key.go b/cmd/cosign/cli/options/key.go index c4e9496ebaa3..f5d29620e70d 100644 --- a/cmd/cosign/cli/options/key.go +++ b/cmd/cosign/cli/options/key.go @@ -36,6 +36,7 @@ type KeyOpts struct { TSAServerURL string RFC3161TimestampPath string TSACertChainPath string + IssueCertificate bool // 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 diff --git a/cmd/cosign/cli/options/sign.go b/cmd/cosign/cli/options/sign.go index 860f1fdd931d..62296f436d28 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, + "whether or not to issue code signing certificate from Fulcio") } diff --git a/cmd/cosign/cli/options/signblob.go b/cmd/cosign/cli/options/signblob.go index 8cc23e23a358..b66dbf462eca 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, + "whether or not to issue code signing certificate from Fulcio") } diff --git a/cmd/cosign/cli/sign.go b/cmd/cosign/cli/sign.go index dbd61b2a1ed6..c026e4da6c7c 100644 --- a/cmd/cosign/cli/sign.go +++ b/cmd/cosign/cli/sign.go @@ -111,6 +111,7 @@ race conditions or (worse) malicious tampering. OIDCProvider: o.OIDC.Provider, SkipConfirmation: o.SkipConfirmation, TSAServerURL: o.TSAServerURL, + IssueCertificate: 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 000e3fc9810b..b934d97acb9e 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -498,6 +498,10 @@ 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.IssueCertificate { + return keylessSigner(ctx, ko) + } + if ko.Sk { return signerFromSecurityKey(ko.Slot) } diff --git a/cmd/cosign/cli/signblob.go b/cmd/cosign/cli/signblob.go index e4ee87105a3f..b0c086822b75 100644 --- a/cmd/cosign/cli/signblob.go +++ b/cmd/cosign/cli/signblob.go @@ -83,6 +83,7 @@ func SignBlob() *cobra.Command { SkipConfirmation: o.SkipConfirmation, TSAServerURL: o.TSAServerURL, RFC3161TimestampPath: o.RFC3161TimestampPath, + IssueCertificate: o.IssueCertificate, } for _, blob := range args { diff --git a/doc/cosign_sign-blob.md b/doc/cosign_sign-blob.md index 4c67fb1853c0..03c3b9c7c9ce 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 --insecure-skip-verify [EXPERIMENTAL] skip verifying fulcio published to the SCT (this should only be used for testing). + --issue-certificate whether or not to issue code signing certificate from Fulcio --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 13dcd6000f90..3e70f4ee0ce1 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 --insecure-skip-verify [EXPERIMENTAL] skip verifying fulcio published to the SCT (this should only be used for testing). + --issue-certificate whether or not to issue code signing certificate from Fulcio --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")