diff --git a/internal/fulcio/fulcio.go b/internal/fulcio/fulcio.go index 071a5ef5..0be8a621 100644 --- a/internal/fulcio/fulcio.go +++ b/internal/fulcio/fulcio.go @@ -1,145 +1,88 @@ -// -// Copyright 2022 The Sigstore Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - package fulcio import ( - "context" "crypto" - "crypto/ecdsa" - "crypto/elliptic" "crypto/rand" + "crypto/sha256" "crypto/x509" - "encoding/pem" - "fmt" - "io" - "os" + "net/url" + "reflect" + "strings" - "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" - "github.com/sigstore/cosign/cmd/cosign/cli/sign" - "github.com/sigstore/cosign/pkg/providers" - "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/fulcio/pkg/api" + "github.com/sigstore/sigstore/pkg/oauthflow" ) -type Identity struct { - sv *sign.SignerVerifier - stderr io.Writer +// Client provides a fulcio client with helpful options for configuring OIDC +// flows. +type Client struct { + api.Client + oidc OIDCOptions } -func NewIdentity(ctx context.Context, w io.Writer) (*Identity, error) { - clientID := envOrValue("GITSIGN_OIDC_CLIENT_ID", "sigstore") - idToken := "" - authFlow := fulcio.FlowNormal - if providers.Enabled(ctx) { - var err error - idToken, err = providers.Provide(ctx, clientID) - if err != nil { - fmt.Fprintln(w, "error getting id token:", err) - } - authFlow = fulcio.FlowToken - } +// OIDCOptions contains settings for OIDC operations. +type OIDCOptions struct { + Issuer string + ClientID string + ClientSecret string + RedirectURL string + TokenGetter oauthflow.TokenGetter +} - priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) +func NewClient(fulcioURL string, opts OIDCOptions) (*Client, error) { + u, err := url.Parse(fulcioURL) if err != nil { - return nil, fmt.Errorf("generating private key: %w", err) + return nil, err } + client := api.NewClient(u, api.WithUserAgent("gitsign")) + return &Client{ + Client: client, + oidc: opts, + }, nil +} - fClient, err := fulcio.NewClient(envOrValue("GITSIGN_FULCIO_URL", "https://fulcio.sigstore.dev")) +// GetCert exchanges the given private key for a Fulcio certificate. +func (c *Client) GetCert(priv crypto.Signer) (*api.CertificateResponse, error) { + pubBytes, err := x509.MarshalPKIXPublicKey(priv.Public()) if err != nil { - return nil, fmt.Errorf("error creating Fulcio client: %w", err) + return nil, err } - issuer := envOrValue("GITSIGN_OIDC_ISSUER", "https://oauth2.sigstore.dev/auth") - redirectURL := os.Getenv("GITSIGN_OIDC_REDIRECT_URL") - - cert, err := fulcio.GetCert(ctx, priv, idToken, authFlow, issuer, clientID, "", redirectURL, fClient) + tok, err := oauthflow.OIDConnect(c.oidc.Issuer, c.oidc.ClientID, c.oidc.ClientSecret, c.oidc.RedirectURL, c.oidc.TokenGetter) if err != nil { - fmt.Fprintln(w, "error getting signer:", err) return nil, err } - sv, err := signature.LoadECDSASignerVerifier(priv, crypto.SHA256) + // Sign the email address as part of the request + h := sha256.Sum256([]byte(tok.Subject)) + proof, err := priv.Sign(rand.Reader, h[:], nil) if err != nil { return nil, err } - return &Identity{ - sv: &sign.SignerVerifier{ - Cert: cert.CertPEM, - Chain: cert.ChainPEM, - SignerVerifier: sv, + cr := api.CertificateRequest{ + PublicKey: api.Key{ + Algorithm: keyAlgorithm(priv), + Content: pubBytes, }, - stderr: w, - }, nil -} - -func envOrValue(env, value string) string { - if v := os.Getenv(env); v != "" { - return v - } - return value -} - -// Certificate gets the identity's certificate. -func (i *Identity) Certificate() (*x509.Certificate, error) { - p, _ := pem.Decode(i.sv.Cert) - cert, err := x509.ParseCertificate(p.Bytes) - return cert, err -} - -// CertificateChain attempts to get the identity's full certificate chain. -func (i *Identity) CertificateChain() ([]*x509.Certificate, error) { - p, _ := pem.Decode(i.sv.Chain) - chain, err := x509.ParseCertificates(p.Bytes) - if err != nil { - return nil, err - } - // the cert itself needs to be appended to the chain - cert, err := i.Certificate() - if err != nil { - return nil, err + SignedEmailAddress: proof, } - return append([]*x509.Certificate{cert}, chain...), nil + return c.SigningCert(cr, tok.RawString) } -// Signer gets a crypto.Signer that uses the identity's private key. -func (i *Identity) Signer() (crypto.Signer, error) { - s, ok := i.sv.SignerVerifier.(crypto.Signer) - if !ok { - return nil, fmt.Errorf("could not use signer %T as crypto.Signer", i.sv.SignerVerifier) +// keyAlgorithm returns a string representation of the type of signer. +// Currently this is dervived from the package name - +// e.g. crypto/ecdsa.PrivateKey -> ecdsa. +// if Signer is nil, "" is returned. +func keyAlgorithm(signer crypto.Signer) string { + // This is a bit of a hack, but let's us use the package name as an approximation for + // algorithm type. + // e.g. *ecdsa.PrivateKey -> ecdsa + t := reflect.TypeOf(signer) + if t == nil { + return "" } - - return s, nil -} - -// Delete deletes this identity from the system. -func (i *Identity) Delete() error { - // Does nothing - keys are ephemeral - return nil -} - -// Close any manually managed memory held by the Identity. -func (i *Identity) Close() { - // noop -} - -func (i *Identity) PublicKey() (crypto.PublicKey, error) { - return i.sv.SignerVerifier.PublicKey() -} - -func (i *Identity) SignerVerifier() *sign.SignerVerifier { - return i.sv + s := strings.Split(strings.TrimPrefix(t.String(), "*"), ".") + return s[0] } diff --git a/internal/fulcio/fulcio_test.go b/internal/fulcio/fulcio_test.go new file mode 100644 index 00000000..49190304 --- /dev/null +++ b/internal/fulcio/fulcio_test.go @@ -0,0 +1,128 @@ +package fulcio + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/coreos/go-oidc/v3/oidc" + "github.com/google/go-cmp/cmp" + "github.com/sigstore/fulcio/pkg/api" + "github.com/sigstore/sigstore/pkg/oauthflow" + "golang.org/x/oauth2" +) + +type fakeSigner struct { + crypto.Signer +} + +func TestKeyAlgorithm(t *testing.T) { + key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + for _, tc := range []struct { + signer crypto.Signer + want string + }{ + { + signer: key, + want: "ecdsa", + }, + { + signer: fakeSigner{}, + want: "fulcio", + }, + { + signer: nil, + want: "", + }, + } { + t.Run(tc.want, func(t *testing.T) { + got := keyAlgorithm(tc.signer) + if got != tc.want { + t.Errorf("want %s, got %s", tc.want, got) + } + }) + } +} + +type fakeFulcio struct { + api.Client + signer *ecdsa.PrivateKey + email string +} + +func (f *fakeFulcio) SigningCert(cr api.CertificateRequest, token string) (*api.CertificateResponse, error) { + if want := keyAlgorithm(f.signer); want != cr.PublicKey.Algorithm { + return nil, fmt.Errorf("want algorithm %s, got %s", want, cr.PublicKey.Algorithm) + } + pem, err := x509.MarshalPKIXPublicKey(f.signer.Public()) + if err != nil { + return nil, err + } + want := api.Key{ + Algorithm: keyAlgorithm(f.signer), + Content: pem, + } + if diff := cmp.Diff(want, cr.PublicKey); diff != "" { + return nil, errors.New(diff) + } + + // Verify checksum separately since this is non-deterministic. + h := sha256.Sum256([]byte(f.email)) + if !ecdsa.VerifyASN1(&f.signer.PublicKey, h[:], cr.SignedEmailAddress) { + return nil, errors.New("signed email did not match!") + } + + return &api.CertificateResponse{}, nil +} + +type fakeTokenGetter struct { + email string +} + +func (f *fakeTokenGetter) GetIDToken(*oidc.Provider, oauth2.Config) (*oauthflow.OIDCIDToken, error) { + return &oauthflow.OIDCIDToken{ + Subject: f.email, + }, nil +} + +func TestGetCert(t *testing.T) { + // Implements a fake OIDC discovery. + oidc := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(map[string]interface{}{ + "issuer": fmt.Sprintf("http://%s", r.Host), + }) + })) + defer oidc.Close() + + key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + email := "foo@example.com" + + client := &Client{ + // fakeFulcio is what will be doing the validation. + Client: &fakeFulcio{ + signer: key, + email: email, + }, + oidc: OIDCOptions{ + Issuer: oidc.URL, + TokenGetter: &fakeTokenGetter{ + email: email, + }, + }, + } + + // fakeFulcio is returning a bogus response, so only check if we returned + // error. + if _, err := client.GetCert(key); err != nil { + t.Fatalf("GetCert: %v", err) + } +} diff --git a/internal/fulcio/identity.go b/internal/fulcio/identity.go new file mode 100644 index 00000000..28a66841 --- /dev/null +++ b/internal/fulcio/identity.go @@ -0,0 +1,146 @@ +// +// Copyright 2022 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package fulcio + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "fmt" + "io" + "os" + + "github.com/sigstore/cosign/pkg/providers" + "github.com/sigstore/sigstore/pkg/oauthflow" + "github.com/sigstore/sigstore/pkg/signature" +) + +type Identity struct { + sv *CertSignerVerifier + stderr io.Writer +} + +func NewIdentity(ctx context.Context, w io.Writer) (*Identity, error) { + clientID := envOrValue("GITSIGN_OIDC_CLIENT_ID", "sigstore") + var authFlow oauthflow.TokenGetter = oauthflow.DefaultIDTokenGetter + if providers.Enabled(ctx) { + var err error + idToken, err := providers.Provide(ctx, clientID) + if err != nil { + fmt.Fprintln(w, "error getting id token:", err) + } + authFlow = &oauthflow.StaticTokenGetter{RawToken: idToken} + } + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("generating private key: %w", err) + } + + client, err := NewClient(envOrValue("GITSIGN_FULCIO_URL", "https://fulcio.sigstore.dev"), + OIDCOptions{ + Issuer: envOrValue("GITSIGN_OIDC_ISSUER", "https://oauth2.sigstore.dev/auth"), + ClientID: clientID, + RedirectURL: os.Getenv("GITSIGN_OIDC_REDIRECT_URL"), + TokenGetter: authFlow, + }) + if err != nil { + return nil, fmt.Errorf("error creating Fulcio client: %w", err) + } + + cert, err := client.GetCert(priv) + if err != nil { + fmt.Fprintln(w, "error getting signer:", err) + return nil, err + } + + sv, err := signature.LoadECDSASignerVerifier(priv, crypto.SHA256) + if err != nil { + return nil, err + } + + return &Identity{ + sv: &CertSignerVerifier{ + SignerVerifier: sv, + Cert: cert.CertPEM, + Chain: cert.ChainPEM, + }, + stderr: w, + }, nil +} + +func envOrValue(env, value string) string { + if v := os.Getenv(env); v != "" { + return v + } + return value +} + +// Certificate gets the identity's certificate. +func (i *Identity) Certificate() (*x509.Certificate, error) { + p, _ := pem.Decode(i.sv.Cert) + cert, err := x509.ParseCertificate(p.Bytes) + return cert, err +} + +// CertificateChain attempts to get the identity's full certificate chain. +func (i *Identity) CertificateChain() ([]*x509.Certificate, error) { + p, _ := pem.Decode(i.sv.Chain) + chain, err := x509.ParseCertificates(p.Bytes) + if err != nil { + return nil, err + } + // the cert itself needs to be appended to the chain + cert, err := i.Certificate() + if err != nil { + return nil, err + } + + return append([]*x509.Certificate{cert}, chain...), nil +} + +// Signer gets a crypto.Signer that uses the identity's private key. +func (i *Identity) Signer() (crypto.Signer, error) { + s, ok := i.sv.SignerVerifier.(crypto.Signer) + if !ok { + return nil, fmt.Errorf("could not use signer %T as crypto.Signer", i.sv.SignerVerifier) + } + + return s, nil +} + +// Delete deletes this identity from the system. +func (i *Identity) Delete() error { + // Does nothing - keys are ephemeral + return nil +} + +// Close any manually managed memory held by the Identity. +func (i *Identity) Close() { + // noop +} + +func (i *Identity) PublicKey() (crypto.PublicKey, error) { + return i.sv.SignerVerifier.PublicKey() +} + +func (i *Identity) SignerVerifier() *CertSignerVerifier { + return i.sv +} diff --git a/internal/fulcio/signer.go b/internal/fulcio/signer.go new file mode 100644 index 00000000..41aaf791 --- /dev/null +++ b/internal/fulcio/signer.go @@ -0,0 +1,11 @@ +package fulcio + +import "github.com/sigstore/sigstore/pkg/signature" + +// CertSignerVerifier wraps a SignerVerifier with a Certificate. +type CertSignerVerifier struct { + signature.SignerVerifier + + Cert []byte + Chain []byte +}