Skip to content

Commit

Permalink
Support for fetching Fulcio certs with self-managed key
Browse files Browse the repository at this point in the history
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 <anishshah@google.com>
  • Loading branch information
AnishShah committed Dec 13, 2022
1 parent 6cb723f commit 8c39c6b
Show file tree
Hide file tree
Showing 11 changed files with 107 additions and 32 deletions.
77 changes: 48 additions & 29 deletions cmd/cosign/cli/fulcio/fulcio.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,22 @@ package fulcio
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
"crypto/x509"
"errors"
"fmt"
"net/url"
"os"
"strings"

"github.com/sigstore/cosign/pkg/cosign/pivkey"
"github.com/sigstore/sigstore/pkg/cryptoutils"
"golang.org/x/term"

"github.com/sigstore/cosign/cmd/cosign/cli/options"
"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"
Expand Down Expand Up @@ -62,28 +63,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,
}
Expand All @@ -92,7 +94,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:
Expand All @@ -105,15 +107,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) {
Expand All @@ -140,13 +141,32 @@ 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
switch {
case ko.IssueCertificate && ko.KeyRef != "":
signer, err = key.SignerVerifierFromKeyRef(ctx, ko.KeyRef, ko.PassFunc)
if err != nil {
return nil, err
}
case ko.IssueCertificate && ko.Sk:
sk, err := pivkey.GetKeyWithSlot(ko.Slot)
if err != nil {
return nil, err
}
signer, err = sk.SignerVerifier()
if err != nil {
sk.Close()
return nil, err
}
default:
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...")

Expand All @@ -172,24 +192,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{}
Expand Down
8 changes: 7 additions & 1 deletion cmd/cosign/cli/fulcio/fulcio_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package fulcio

import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions cmd/cosign/cli/options/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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
Expand Down
4 changes: 4 additions & 0 deletions cmd/cosign/cli/options/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type SignOptions struct {
SkipConfirmation bool
TlogUpload bool
TSAServerURL string
IssueCertificate bool

Rekor RekorOptions
Fulcio FulcioOptions
Expand Down Expand Up @@ -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")
}
4 changes: 4 additions & 0 deletions cmd/cosign/cli/options/signblob.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type SignBlobOptions struct {
TlogUpload bool
TSAServerURL string
RFC3161TimestampPath string
IssueCertificate bool
}

var _ Interface = (*SignBlobOptions)(nil)
Expand Down Expand Up @@ -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,
"when set and a key is provided, issue a code signing certificate from Fulcio")
}
1 change: 1 addition & 0 deletions cmd/cosign/cli/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 == "" {
Expand Down
4 changes: 2 additions & 2 deletions cmd/cosign/cli/sign/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,11 +498,11 @@ 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 {
if ko.Sk && !ko.IssueCertificate {
return signerFromSecurityKey(ko.Slot)
}

if ko.KeyRef != "" {
if ko.KeyRef != "" && !ko.IssueCertificate {
return signerFromKeyRef(ctx, certPath, certChainPath, ko.KeyRef, ko.PassFunc)
}

Expand Down
1 change: 1 addition & 0 deletions cmd/cosign/cli/signblob.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func SignBlob() *cobra.Command {
SkipConfirmation: o.SkipConfirmation,
TSAServerURL: o.TSAServerURL,
RFC3161TimestampPath: o.RFC3161TimestampPath,
IssueCertificate: o.IssueCertificate,
}

for _, blob := range args {
Expand Down
1 change: 1 addition & 0 deletions doc/cosign_sign-blob.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions doc/cosign_sign.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions test/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import (
const (
serverEnv = "REKOR_SERVER"
rekorURL = "https://rekor.sigstore.dev"
fulcioURL = "https://fulcio.sigstore.dev"
)

var keyPass = []byte("hello")
Expand Down Expand Up @@ -619,6 +620,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) {
// turn on the tlog
defer setenv(t, env.VariableExperimental.String(), "1")()
Expand Down

0 comments on commit 8c39c6b

Please sign in to comment.