Skip to content

Commit

Permalink
Remove dependency on cosign/cli/fulcio.
Browse files Browse the repository at this point in the history
This reimplements much of the behavior in
https://github.com/sigstore/cosign/blob/v1.9.0/cmd/cosign/cli/fulcio/fulcio.go
to remove the dependency on cosign for fulcio operations.

We may want to upstream this library to sigstore/sigstore, but starting
off here to get a feel for other changes we might want to make first.

Signed-off-by: Billy Lynch <[email protected]>
  • Loading branch information
wlynch committed Jun 3, 2022
1 parent c72bc2c commit 6eb3af8
Show file tree
Hide file tree
Showing 4 changed files with 340 additions and 112 deletions.
167 changes: 55 additions & 112 deletions internal/fulcio/fulcio.go
Original file line number Diff line number Diff line change
@@ -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]
}
128 changes: 128 additions & 0 deletions internal/fulcio/fulcio_test.go
Original file line number Diff line number Diff line change
@@ -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 := "[email protected]"

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)
}
}
Loading

0 comments on commit 6eb3af8

Please sign in to comment.