Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for fetching Fulcio certs with self-managed key #2532

Merged
merged 6 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
haydentherapper marked this conversation as resolved.
Show resolved Hide resolved
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
AnishShah marked this conversation as resolved.
Show resolved Hide resolved
// 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