From 037a09f45a929561870499b1c224675a2ba0ede2 Mon Sep 17 00:00:00 2001 From: Priya Wadhwa Date: Mon, 6 Mar 2023 12:57:07 -0500 Subject: [PATCH] Use Issuer interface to allow for custom issuers This plumbs through IssuerPool and starts using it to set up Fulcio. Signed-off-by: Priya Wadhwa --- cmd/app/grpc.go | 47 ++++++++++++- cmd/app/grpc_test.go | 133 ++++++++++++++++++++++++++++++++++++ cmd/app/http_test.go | 2 +- cmd/app/serve.go | 10 +-- cmd/app/serve_test.go | 2 +- pkg/identity/base/issuer.go | 6 ++ pkg/server/grpc_server.go | 17 ++--- 7 files changed, 198 insertions(+), 19 deletions(-) create mode 100644 cmd/app/grpc_test.go diff --git a/cmd/app/grpc.go b/cmd/app/grpc.go index 6a41ab4e2..a059a671d 100644 --- a/cmd/app/grpc.go +++ b/cmd/app/grpc.go @@ -33,6 +33,14 @@ import ( "github.com/sigstore/fulcio/pkg/config" gw "github.com/sigstore/fulcio/pkg/generated/protobuf" gw_legacy "github.com/sigstore/fulcio/pkg/generated/protobuf/legacy" + "github.com/sigstore/fulcio/pkg/identity" + "github.com/sigstore/fulcio/pkg/identity/buildkite" + "github.com/sigstore/fulcio/pkg/identity/email" + "github.com/sigstore/fulcio/pkg/identity/github" + "github.com/sigstore/fulcio/pkg/identity/kubernetes" + "github.com/sigstore/fulcio/pkg/identity/spiffe" + "github.com/sigstore/fulcio/pkg/identity/uri" + "github.com/sigstore/fulcio/pkg/identity/username" "github.com/sigstore/fulcio/pkg/log" "github.com/sigstore/fulcio/pkg/server" "github.com/spf13/viper" @@ -64,7 +72,7 @@ func PassFulcioConfigThruContext(cfg *config.FulcioConfig) grpc.UnaryServerInter } } -func createGRPCServer(cfg *config.FulcioConfig, ctClient *ctclient.LogClient, baseca ca.CertificateAuthority) (*grpcServer, error) { +func createGRPCServer(cfg *config.FulcioConfig, ctClient *ctclient.LogClient, baseca ca.CertificateAuthority, ip identity.IssuerPool) (*grpcServer, error) { logger, opts := log.SetupGRPCLogging() myServer := grpc.NewServer(grpc.UnaryInterceptor( @@ -77,7 +85,7 @@ func createGRPCServer(cfg *config.FulcioConfig, ctClient *ctclient.LogClient, ba )), grpc.MaxRecvMsgSize(int(maxMsgSize))) - grpcCAServer := server.NewGRPCCAServer(ctClient, baseca) + grpcCAServer := server.NewGRPCCAServer(ctClient, baseca, ip) // Register your gRPC service implementations. gw.RegisterCAServer(myServer, grpcCAServer) @@ -85,6 +93,41 @@ func createGRPCServer(cfg *config.FulcioConfig, ctClient *ctclient.LogClient, ba return &grpcServer{myServer, grpcServerEndpoint, grpcCAServer}, nil } +func NewIssuerPool(cfg *config.FulcioConfig) identity.IssuerPool { + var ip identity.IssuerPool + for _, i := range cfg.OIDCIssuers { + ip = append(ip, getIssuer("", i)) + } + for meta, i := range cfg.MetaIssuers { + ip = append(ip, getIssuer(meta, i)) + } + return ip +} + +func getIssuer(meta string, i config.OIDCIssuer) identity.Issuer { + issuerURL := i.IssuerURL + if meta == "" { + issuerURL = meta + } + switch i.Type { + case config.IssuerTypeEmail: + return email.Issuer(issuerURL) + case config.IssuerTypeGithubWorkflow: + return github.Issuer(issuerURL) + case config.IssuerTypeBuildkiteJob: + return buildkite.Issuer(issuerURL) + case config.IssuerTypeKubernetes: + return kubernetes.Issuer(issuerURL) + case config.IssuerTypeSpiffe: + return spiffe.Issuer(issuerURL) + case config.IssuerTypeURI: + return uri.Issuer(issuerURL) + case config.IssuerTypeUsername: + return username.Issuer(issuerURL) + } + return nil +} + func (g *grpcServer) setupPrometheus(reg *prometheus.Registry) { grpcMetrics := grpc_prometheus.DefaultServerMetrics grpcMetrics.EnableHandlingTimeHistogram() diff --git a/cmd/app/grpc_test.go b/cmd/app/grpc_test.go new file mode 100644 index 000000000..4f2deea95 --- /dev/null +++ b/cmd/app/grpc_test.go @@ -0,0 +1,133 @@ +// Copyright 2023 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 app + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/sigstore/fulcio/pkg/config" + "github.com/sigstore/fulcio/pkg/identity" + "github.com/sigstore/fulcio/pkg/identity/base" + "github.com/sigstore/fulcio/pkg/identity/email" + "github.com/sigstore/fulcio/pkg/identity/github" + "github.com/sigstore/fulcio/pkg/identity/kubernetes" + "github.com/sigstore/fulcio/pkg/identity/spiffe" + "github.com/sigstore/fulcio/pkg/identity/uri" + "github.com/sigstore/fulcio/pkg/identity/username" +) + +func TestIssuerPool(t *testing.T) { + // Test the issuer pool with the OIDCIssuers + cfg := &config.FulcioConfig{ + OIDCIssuers: map[string]config.OIDCIssuer{ + "https://oauth2.sigstore.dev/auth": { + IssuerURL: "https://oauth2.sigstore.dev/auth", + ClientID: "sigstore", + IssuerClaim: "$.federated_claims.connector_id", + Type: config.IssuerTypeEmail, + }, + }, + } + // Build the expected issuer pool + expected := identity.IssuerPool{ + email.Issuer("https://oauth2.sigstore.dev/auth"), + } + ignoreOpts := []cmp.Option{base.CmpOptions} + got := NewIssuerPool(cfg) + if d := cmp.Diff(expected, got, ignoreOpts...); d != "" { + t.Fatal(d) + } + + // Test the issuer pool with a MetaIssuer + cfg = &config.FulcioConfig{ + MetaIssuers: map[string]config.OIDCIssuer{ + "https://oidc.eks.*.amazonaws.com/id/*": { + ClientID: "bar", + Type: "kubernetes", + }, + }, + } + expected = identity.IssuerPool{ + kubernetes.Issuer("https://oidc.eks.*.amazonaws.com/id/*"), + } + got = NewIssuerPool(cfg) + if d := cmp.Diff(expected, got, ignoreOpts...); d != "" { + t.Fatal(d) + } +} + +func TestGetIssuer(t *testing.T) { + tests := []struct { + description string + issuer config.OIDCIssuer + expected identity.Issuer + }{ + { + description: "email", + issuer: config.OIDCIssuer{ + IssuerURL: "email.com", + Type: "email", + }, + expected: email.Issuer("email.com"), + }, { + description: "github", + issuer: config.OIDCIssuer{ + IssuerURL: "github.com", + Type: "github-workflow", + }, + expected: github.Issuer("github.com"), + }, { + description: "spiffe", + issuer: config.OIDCIssuer{ + IssuerURL: "spiffe.com", + Type: "spiffe", + }, + expected: spiffe.Issuer("spiffe.com"), + }, { + description: "kubernetes", + issuer: config.OIDCIssuer{ + IssuerURL: "kubernetes.com", + Type: "kubernetes", + }, + expected: kubernetes.Issuer("kubernetes.com"), + }, { + description: "uri", + issuer: config.OIDCIssuer{ + IssuerURL: "uri.com", + Type: "uri", + }, + expected: uri.Issuer("uri.com"), + }, { + description: "username", + issuer: config.OIDCIssuer{ + IssuerURL: "username.com", + Type: "username", + }, + expected: username.Issuer("username.com"), + }, + } + + ignoreOpts := []cmp.Option{base.CmpOptions} + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + got := getIssuer("", test.issuer) + if d := cmp.Diff(got, test.expected, ignoreOpts...); d != "" { + t.Fatal(d) + } + }) + } +} diff --git a/cmd/app/http_test.go b/cmd/app/http_test.go index b8027e702..bf463f0fc 100644 --- a/cmd/app/http_test.go +++ b/cmd/app/http_test.go @@ -44,7 +44,7 @@ func setupHTTPServer(t *testing.T) (httpServer, string) { viper.Set("grpc-host", "") viper.Set("grpc-port", 0) - grpcServer, err := createGRPCServer(nil, nil, &TrivialCertificateAuthority{}) + grpcServer, err := createGRPCServer(nil, nil, &TrivialCertificateAuthority{}, nil) if err != nil { t.Error(err) } diff --git a/cmd/app/serve.go b/cmd/app/serve.go index 554d823d5..326a0e15b 100644 --- a/cmd/app/serve.go +++ b/cmd/app/serve.go @@ -50,6 +50,7 @@ import ( "github.com/sigstore/fulcio/pkg/config" "github.com/sigstore/fulcio/pkg/generated/protobuf" "github.com/sigstore/fulcio/pkg/generated/protobuf/legacy" + "github.com/sigstore/fulcio/pkg/identity" "github.com/sigstore/fulcio/pkg/log" "github.com/sigstore/fulcio/pkg/server" "github.com/sigstore/sigstore/pkg/cryptoutils" @@ -269,6 +270,7 @@ func runServeCmd(cmd *cobra.Command, args []string) { log.Logger.Fatal(err) } } + ip := NewIssuerPool(cfg) portsMatch := viper.GetString("port") == viper.GetString("grpc-port") hostsMatch := viper.GetString("host") == viper.GetString("grpc-host") @@ -276,7 +278,7 @@ func runServeCmd(cmd *cobra.Command, args []string) { port := viper.GetInt("port") metricsPort := viper.GetInt("metrics-port") // StartDuplexServer will always return an error, log fatally if it's non-nil - if err := StartDuplexServer(ctx, cfg, ctClient, baseca, viper.GetString("host"), port, metricsPort); err != http.ErrServerClosed { + if err := StartDuplexServer(ctx, cfg, ctClient, baseca, viper.GetString("host"), port, metricsPort, ip); err != http.ErrServerClosed { log.Logger.Fatal(err) } return @@ -286,7 +288,7 @@ func runServeCmd(cmd *cobra.Command, args []string) { reg := prometheus.NewRegistry() - grpcServer, err := createGRPCServer(cfg, ctClient, baseca) + grpcServer, err := createGRPCServer(cfg, ctClient, baseca, ip) if err != nil { log.Logger.Fatal(err) } @@ -342,7 +344,7 @@ func checkServeCmdConfigFile() error { return nil } -func StartDuplexServer(ctx context.Context, cfg *config.FulcioConfig, ctClient *ctclient.LogClient, baseca ca.CertificateAuthority, host string, port, metricsPort int) error { +func StartDuplexServer(ctx context.Context, cfg *config.FulcioConfig, ctClient *ctclient.LogClient, baseca ca.CertificateAuthority, host string, port, metricsPort int, ip identity.IssuerPool) error { logger, opts := log.SetupGRPCLogging() d := duplex.New( @@ -361,7 +363,7 @@ func StartDuplexServer(ctx context.Context, cfg *config.FulcioConfig, ctClient * ) // GRPC server - grpcCAServer := server.NewGRPCCAServer(ctClient, baseca) + grpcCAServer := server.NewGRPCCAServer(ctClient, baseca, ip) protobuf.RegisterCAServer(d.Server, grpcCAServer) if err := d.RegisterHandler(ctx, protobuf.RegisterCAHandlerFromEndpoint); err != nil { return fmt.Errorf("registering grpc ca handler: %w", err) diff --git a/cmd/app/serve_test.go b/cmd/app/serve_test.go index 3d7b58fca..b3e82fa43 100644 --- a/cmd/app/serve_test.go +++ b/cmd/app/serve_test.go @@ -50,7 +50,7 @@ func TestDuplex(t *testing.T) { metricsPort := 2114 go func() { - if err := StartDuplexServer(ctx, config.DefaultConfig, nil, ca, "localhost", port, metricsPort); err != nil { + if err := StartDuplexServer(ctx, config.DefaultConfig, nil, ca, "localhost", port, metricsPort, nil); err != nil { log.Fatalf("error starting duplex server: %v", err) } }() diff --git a/pkg/identity/base/issuer.go b/pkg/identity/base/issuer.go index d4085a676..49f313550 100644 --- a/pkg/identity/base/issuer.go +++ b/pkg/identity/base/issuer.go @@ -20,9 +20,15 @@ import ( "regexp" "strings" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/sigstore/fulcio/pkg/identity" ) +var ( + // For testing + CmpOptions = cmpopts.IgnoreUnexported(baseIssuer{}) +) + type baseIssuer struct { issuerURL string } diff --git a/pkg/server/grpc_server.go b/pkg/server/grpc_server.go index 87ef78a2a..2b43fc1ea 100644 --- a/pkg/server/grpc_server.go +++ b/pkg/server/grpc_server.go @@ -39,12 +39,14 @@ type grpcCAServer struct { fulciogrpc.UnimplementedCAServer ct *ctclient.LogClient ca certauth.CertificateAuthority + identity.IssuerPool } -func NewGRPCCAServer(ct *ctclient.LogClient, ca certauth.CertificateAuthority) fulciogrpc.CAServer { +func NewGRPCCAServer(ct *ctclient.LogClient, ca certauth.CertificateAuthority, ip identity.IssuerPool) fulciogrpc.CAServer { return &grpcCAServer{ - ct: ct, - ca: ca, + ct: ct, + ca: ca, + IssuerPool: ip, } } @@ -70,14 +72,7 @@ func (g *grpcCAServer) CreateSigningCertificate(ctx context.Context, request *fu } // Authenticate OIDC ID token by checking signature - idtoken, err := identity.Authorize(ctx, token) - if err != nil { - return nil, handleFulcioGRPCError(ctx, codes.Unauthenticated, err, invalidCredentials) - } - // Parse authenticated ID token into principal - // TODO:(nsmith5) replace this and authorize call above with - // just identity.IssuerPool.Authenticate() - principal, err := challenges.PrincipalFromIDToken(ctx, idtoken) + principal, err := g.IssuerPool.Authenticate(ctx, token) if err != nil { return nil, handleFulcioGRPCError(ctx, codes.InvalidArgument, err, invalidIdentityToken) }