Skip to content

Commit

Permalink
Support for fetching Fulcio certs with self-managed key (#2532)
Browse files Browse the repository at this point in the history
* 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 <anishshah@google.com>

* add tests for newsigner

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>

* Fix lint and nit

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>

* remove commented out code

Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>

* Refactor sign.SignerFromKeyOpts

Signed-off-by: Anish Shah <anishshah@google.com>

* Fix lint issues

Signed-off-by: Anish Shah <anishshah@google.com>

---------

Signed-off-by: Anish Shah <anishshah@google.com>
Signed-off-by: Hayden Blauzvern <hblauzvern@google.com>
Co-authored-by: Hayden Blauzvern <hblauzvern@google.com>
  • Loading branch information
AnishShah and haydentherapper committed Feb 8, 2023
1 parent 66f2b08 commit abb50cf
Show file tree
Hide file tree
Showing 12 changed files with 201 additions and 81 deletions.
58 changes: 23 additions & 35 deletions cmd/cosign/cli/fulcio/fulcio.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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,
}
Expand All @@ -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:
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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{}
Expand Down
56 changes: 55 additions & 1 deletion cmd/cosign/cli/fulcio/fulcio_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,25 @@
package fulcio

import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"errors"
"net/http"
"net/http/httptest"
"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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
}
}
5 changes: 3 additions & 2 deletions cmd/cosign/cli/fulcio/fulcioverifier/fulcioverifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
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.
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
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,
"issue a code signing certificate from Fulcio, even if a key is provided")
}
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,
"issue a code signing certificate from Fulcio, even if a key is provided")
}
33 changes: 17 additions & 16 deletions cmd/cosign/cli/sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 == "" {
Expand Down
Loading

0 comments on commit abb50cf

Please sign in to comment.