Skip to content

Commit

Permalink
add GRPC interface (#472)
Browse files Browse the repository at this point in the history
* initial grpc impl

Signed-off-by: Bob Callaway <bcallaway@google.com>

* fix cmd line arg with viper alias

Signed-off-by: Bob Callaway <bcallaway@google.com>

* add debugging, install protoc via apt

Signed-off-by: Bob Callaway <bcallaway@google.com>

* remove debugging and trailing newline

Signed-off-by: Bob Callaway <bcallaway@google.com>

* write header and value together

Signed-off-by: Bob Callaway <bcallaway@google.com>

* fix docker-compose, revert last fix

Signed-off-by: Bob Callaway <bob.callaway@gmail.com>

* fix logging, add grpc mw

Signed-off-by: Bob Callaway <bcallaway@google.com>

* update protobuf defs, implement legacy interface

Signed-off-by: Bob Callaway <bcallaway@google.com>

* fix spacing

Signed-off-by: Bob Callaway <bcallaway@google.com>

* use abstract unix domain socket

Signed-off-by: Bob Callaway <bcallaway@google.com>

* send 201 and SCT with response

Signed-off-by: Bob Callaway <bcallaway@google.com>

* address code review comments

Signed-off-by: Bob Callaway <bcallaway@google.com>

* debugging k8s failure

Signed-off-by: Bob Callaway <bcallaway@google.com>

* trim space on chain before sending

Signed-off-by: Bob Callaway <bcallaway@google.com>

* trim space on CSC call as well

Signed-off-by: Bob Callaway <bcallaway@google.com>

* fix prom and address review comments

Signed-off-by: Bob Callaway <bcallaway@google.com>

* s/@/:/

Signed-off-by: Bob Callaway <bcallaway@google.com>

* update version num, tweak cert chain nomenclature

Signed-off-by: Bob Callaway <bcallaway@google.com>

* modify api_test to use grpc server

Signed-off-by: Bob Callaway <bcallaway@google.com>

* remove unused code

Signed-off-by: Bob Callaway <bcallaway@google.com>

* remove ctl logger, fix newlines

Signed-off-by: Bob Callaway <bcallaway@google.com>

* fix test to not send nil challenge

Signed-off-by: Bob Callaway <bcallaway@google.com>

* address review comments

Signed-off-by: Bob Callaway <bcallaway@google.com>

* commit generated file

Signed-off-by: Bob Callaway <bcallaway@google.com>

* opt for message with 0 fields over empty for forward compatibility

Signed-off-by: Bob Callaway <bcallaway@google.com>
  • Loading branch information
bobcallaway authored Apr 14, 2022
1 parent 2605dbf commit d464219
Show file tree
Hide file tree
Showing 45 changed files with 4,490 additions and 769 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/pkg/generated/protobuf/** linguist-generated
4 changes: 3 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ jobs:
- uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v3.0.0
with:
go-version: ${{ env.GOVERSION }}
- name: Install protobuf
run: sudo apt update -y && sudo apt install -y -q protobuf-compiler

- name: Build
run: make -C $GITHUB_WORKSPACE all
Expand All @@ -43,4 +45,4 @@ jobs:
- name: Upload Coverage Report
uses: codecov/codecov-action@e3c560433a6cc60aec8812599b7844a7b4fa0d71 # v3.0.0
- name: Ensure no files were modified as a result of the build
run: git update-index --refresh && git diff-index --quiet HEAD -- || git diff --exit-code
run: git update-index --refresh && git diff-index --quiet -I"^\/\/\s+protoc(-gen-go)?\s+v[0-9]+\.[0-9]+\.[0-9]+$" HEAD -- || git diff -I"^\/\/\s+protoc(-gen-go)?\s+v[0-9]+\.[0-9]+\.[0-9]+$" --exit-code
5 changes: 5 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ issues:
linters:
- errcheck
- gosec
# the following section is due to the legacy API being deprecated
- path: pkg/api/legacy_server.go
linters:
- staticcheck
text: SA1019
max-issues-per-linter: 0
max-same-issues: 0
run:
Expand Down
49 changes: 47 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,16 @@ all: fulcio
# Ensure Make is run with bash shell as some syntax below is bash-specific
SHELL:=/usr/bin/env bash

SRCS = $(shell find cmd -iname "*.go") $(shell find pkg -iname "*.go")
SRCS = $(shell find cmd -iname "*.go") $(shell find pkg -iname "*.go"|grep -v pkg/generated) $(GENSRC)
TOOLS_DIR := hack/tools
TOOLS_BIN_DIR := $(abspath $(TOOLS_DIR)/bin)
BIN_DIR := $(abspath $(ROOT_DIR)/bin)

GO_MODULE=$(shell head -1 go.mod | cut -f2 -d ' ')

GENSRC = pkg/generated/protobuf/%.go
PROTOBUF_DEPS = $(shell find . -iname "*.proto" | grep -v "third_party")

# Set version variables for LDFLAGS
GIT_VERSION ?= $(shell git describe --tags --always --dirty)
GIT_HASH ?= $(shell git rev-parse HEAD)
Expand All @@ -39,7 +46,7 @@ ifeq ($(DIFF), 1)
GIT_TREESTATE = "dirty"
endif

FULCIO_PKG=github.com/sigstore/fulcio/cmd/app
FULCIO_PKG=github.com/sigstore/fulcio/pkg/api
LDFLAGS=-X $(FULCIO_PKG).gitVersion=$(GIT_VERSION) -X $(FULCIO_PKG).gitCommit=$(GIT_HASH) -X $(FULCIO_PKG).gitTreeState=$(GIT_TREESTATE) -X $(FULCIO_PKG).buildDate=$(BUILD_DATE)

KO_PREFIX ?= gcr.io/projectsigstore
Expand All @@ -49,12 +56,31 @@ GHCR_PREFIX ?= ghcr.io/sigstore

FULCIO_YAML ?= fulcio-$(GIT_TAG).yaml

# Binaries
PROTOC-GEN-GO := $(TOOLS_BIN_DIR)/protoc-gen-go
PROTOC-GEN-GO-GRPC := $(TOOLS_BIN_DIR)/protoc-gen-go-grpc
PROTOC-GEN-GRPC-GATEWAY := $(TOOLS_BIN_DIR)/protoc-gen-grpc-gateway
PROTOC-API-LINTER := $(TOOLS_BIN_DIR)/api-linter

$(GENSRC): $(PROTOC-GEN-GO) $(PROTOC-GEN-GO-GRPC) $(PROTOC-GEN-GRPC-GATEWAY) $(PROTOC-API-LINTER) $(PROTOBUF_DEPS)
mkdir -p pkg/generated/protobuf
$(PROTOC-API-LINTER) -I third_party/googleapis/ -I . $(PROTOBUF_DEPS) #--set-exit-status # TODO: add strict checking
protoc --plugin=protoc-gen-go=$(TOOLS_BIN_DIR)/protoc-gen-go \
--go_opt=module=$(GO_MODULE) --go_out=. \
--plugin=protoc-gen-go-grpc=$(TOOLS_BIN_DIR)/protoc-gen-go-grpc \
--go-grpc_opt=module=$(GO_MODULE) --go-grpc_out=. \
--plugin=protoc-gen-grpc-gateway=$(TOOLS_BIN_DIR)/protoc-gen-grpc-gateway \
--grpc-gateway_opt=module=$(GO_MODULE) --grpc-gateway_opt=logtostderr=true --grpc-gateway_out=. \
-I third_party/googleapis/ -I . $(PROTOBUF_DEPS)

lint: ## Runs golangci-lint
$(GOBIN)/golangci-lint run -v ./...

gosec: ## Runs gosec
$(GOBIN)/gosec ./...

gen: $(GENSRC)

fulcio: $(SRCS) ## Build Fulcio for local tests
go build -trimpath -ldflags "$(LDFLAGS)"

Expand All @@ -65,6 +91,9 @@ clean: ## Clean the workspace
rm -rf dist
rm -rf fulcio

clean-gen: clean
rm -rf $(shell find pkg/generated -iname "*.go")

up: ## Start docker compose
docker-compose -f docker-compose.yml build
docker-compose -f docker-compose.yml up
Expand All @@ -73,6 +102,22 @@ debug: ## Start docker compose in debug mode
docker-compose -f docker-compose.yml -f docker-compose.debug.yml build fulcio-server-debug
docker-compose -f docker-compose.yml -f docker-compose.debug.yml up fulcio-server-debug

## --------------------------------------
## Tooling Binaries
## --------------------------------------

$(PROTOC-GEN-GO): $(TOOLS_DIR)/go.mod
cd $(TOOLS_DIR); go build -trimpath -tags=tools -o $(TOOLS_BIN_DIR)/protoc-gen-go google.golang.org/protobuf/cmd/protoc-gen-go

$(PROTOC-GEN-GO-GRPC): $(TOOLS_DIR)/go.mod
cd $(TOOLS_DIR); go build -trimpath -tags=tools -o $(TOOLS_BIN_DIR)/protoc-gen-go-grpc google.golang.org/grpc/cmd/protoc-gen-go-grpc

$(PROTOC-GEN-GRPC-GATEWAY): $(TOOLS_DIR)/go.mod
cd $(TOOLS_DIR); go build -trimpath -tags=tools -o $(TOOLS_BIN_DIR)/protoc-gen-grpc-gateway github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway

$(PROTOC-API-LINTER): $(TOOLS_DIR)/go.mod
cd $(TOOLS_DIR); go build -trimpath -tags=tools -o $(TOOLS_BIN_DIR)/api-linter github.com/googleapis/api-linter/cmd/api-linter

## --------------------------------------
## Images with ko
## --------------------------------------
Expand Down
151 changes: 151 additions & 0 deletions cmd/app/grpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// 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 app

import (
"context"
"fmt"
"net"

"github.com/goadesign/goa/grpc/middleware"
ctclient "github.com/google/certificate-transparency-go/client"
grpcmw "github.com/grpc-ecosystem/go-grpc-middleware"
grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"
grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery"
grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus"
"github.com/prometheus/client_golang/prometheus"
"github.com/sigstore/fulcio/pkg/api"
"github.com/sigstore/fulcio/pkg/ca"
"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/log"
"github.com/spf13/viper"
"google.golang.org/grpc"
)

const (
LegacyUnixDomainSocket = "@fulcio-legacy-grpc-socket"
)

type grpcServer struct {
*grpc.Server
grpcServerEndpoint string
caService gw.CAServer
}

func passFulcioConfigThruContext(cfg *config.FulcioConfig) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// For each request, infuse context with our snapshot of the FulcioConfig.
// TODO(mattmoor): Consider periodically (every minute?) refreshing the ConfigMap
// from disk, so that we don't need to cycle pods to pick up config updates.
// Alternately we could take advantage of Knative's configmap watcher.
ctx = config.With(ctx, cfg)
ctx, cancel := context.WithCancel(ctx)
defer cancel()

// Calls the inner handler
return handler(ctx, req)
}
}

func createGRPCServer(cfg *config.FulcioConfig, ctClient *ctclient.LogClient, baseca ca.CertificateAuthority) (*grpcServer, error) {
logger, opts := log.SetupGRPCLogging()

myServer := grpc.NewServer(grpc.UnaryInterceptor(
grpcmw.ChainUnaryServer(
grpc_recovery.UnaryServerInterceptor(grpc_recovery.WithRecoveryHandlerContext(panicRecoveryHandler)), // recovers from per-transaction panics elegantly, so put it first
middleware.UnaryRequestID(middleware.UseXRequestIDMetadataOption(true), middleware.XRequestMetadataLimitOption(128)),
grpc_zap.UnaryServerInterceptor(logger, opts...),
passFulcioConfigThruContext(cfg),
grpc_prometheus.UnaryServerInterceptor,
)),
grpc.MaxRecvMsgSize(int(maxMsgSize)))

grpcCAServer := api.NewGRPCCAServer(ctClient, baseca)
// Register your gRPC service implementations.
gw.RegisterCAServer(myServer, grpcCAServer)

grpcServerEndpoint := fmt.Sprintf("%s:%s", viper.GetString("grpc-host"), viper.GetString("grpc-port"))
return &grpcServer{myServer, grpcServerEndpoint, grpcCAServer}, nil
}

func (g *grpcServer) setupPrometheus(reg *prometheus.Registry) {
grpcMetrics := grpc_prometheus.DefaultServerMetrics
grpcMetrics.EnableHandlingTimeHistogram()
reg.MustRegister(grpcMetrics, api.MetricLatency, api.RequestsCount)
grpc_prometheus.Register(g.Server)
}

func (g *grpcServer) startTCPListener() {
go func() {
lis, err := net.Listen("tcp", g.grpcServerEndpoint)
if err != nil {
log.Logger.Fatal(err)
}
defer lis.Close()

tcpAddr := lis.Addr().(*net.TCPAddr)
g.grpcServerEndpoint = fmt.Sprintf("%v:%d", tcpAddr.IP, tcpAddr.Port)
log.Logger.Infof("listening on grpc at %s", g.grpcServerEndpoint)

log.Logger.Fatal(g.Server.Serve(lis))
}()
}

func (g *grpcServer) startUnixListener() {
go func() {
unixAddr, err := net.ResolveUnixAddr("unix", LegacyUnixDomainSocket)
if err != nil {
log.Logger.Fatal(err)
}
lis, err := net.ListenUnix("unix", unixAddr)
if err != nil {
log.Logger.Fatal(err)
}
defer lis.Close()

log.Logger.Infof("listening on grpc at %s", unixAddr.String())

log.Logger.Fatal(g.Server.Serve(lis))
}()
}

func createLegacyGRPCServer(cfg *config.FulcioConfig, v2Server gw.CAServer) (*grpcServer, error) {
logger, opts := log.SetupGRPCLogging()

myServer := grpc.NewServer(grpc.UnaryInterceptor(
grpcmw.ChainUnaryServer(
grpc_recovery.UnaryServerInterceptor(grpc_recovery.WithRecoveryHandlerContext(panicRecoveryHandler)), // recovers from per-transaction panics elegantly, so put it first
middleware.UnaryRequestID(middleware.UseXRequestIDMetadataOption(true), middleware.XRequestMetadataLimitOption(128)),
grpc_zap.UnaryServerInterceptor(logger, opts...),
passFulcioConfigThruContext(cfg),
grpc_prometheus.UnaryServerInterceptor,
)),
grpc.MaxRecvMsgSize(int(maxMsgSize)))

legacyGRPCCAServer := api.NewLegacyGRPCCAServer(v2Server)

// Register your gRPC service implementations.
gw_legacy.RegisterCAServer(myServer, legacyGRPCCAServer)

return &grpcServer{myServer, LegacyUnixDomainSocket, v2Server}, nil
}

func panicRecoveryHandler(ctx context.Context, p interface{}) error {
log.ContextLogger(ctx).Error(p)
return fmt.Errorf("panic: %v", p)
}
118 changes: 118 additions & 0 deletions cmd/app/http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// 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 app

import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"time"

"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/sigstore/fulcio/pkg/api"
gw "github.com/sigstore/fulcio/pkg/generated/protobuf"
legacy_gw "github.com/sigstore/fulcio/pkg/generated/protobuf/legacy"
"github.com/sigstore/fulcio/pkg/log"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/proto"
)

type httpServer struct {
*http.Server
httpServerEndpoint string
}

func extractOIDCTokenFromAuthHeader(ctx context.Context, req *http.Request) metadata.MD {
token := strings.Replace(req.Header.Get("Authorization"), "Bearer ", "", 1)
return metadata.Pairs(api.MetadataOIDCTokenKey, token)
}

func createHTTPServer(ctx context.Context, serverEndpoint string, grpcServer, legacyGRPCServer *grpcServer) httpServer {
mux := runtime.NewServeMux(runtime.WithMetadata(extractOIDCTokenFromAuthHeader),
runtime.WithForwardResponseOption(setResponseCodeModifier))

opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
if err := gw.RegisterCAHandlerFromEndpoint(ctx, mux, grpcServer.grpcServerEndpoint, opts); err != nil {
log.Logger.Fatal(err)
}

if legacyGRPCServer != nil {
endpoint := fmt.Sprintf("unix:%v", legacyGRPCServer.grpcServerEndpoint)
if err := legacy_gw.RegisterCAHandlerFromEndpoint(ctx, mux, endpoint, opts); err != nil {
log.Logger.Fatal(err)
}
}

// Limit request size
handler := api.WithMaxBytes(mux, maxMsgSize)
handler = promhttp.InstrumentHandlerDuration(api.MetricLatency, handler)
handler = promhttp.InstrumentHandlerCounter(api.RequestsCount, handler)

api := http.Server{
Addr: serverEndpoint,
Handler: handler,

// Timeouts
ReadTimeout: 60 * time.Second,
ReadHeaderTimeout: 60 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 60 * time.Second,
}
return httpServer{&api, serverEndpoint}
}

func (h httpServer) startListener() {
log.Logger.Infof("listening on http at %s", h.httpServerEndpoint)
go func() {
if err := h.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Logger.Fatal(err)
}
}()
}

func setResponseCodeModifier(ctx context.Context, w http.ResponseWriter, _ proto.Message) error {
md, ok := runtime.ServerMetadataFromContext(ctx)
if !ok {
return nil
}

// set SCT if present ahead of modifying response code
if vals := md.HeaderMD.Get(api.SCTMetadataKey); len(vals) > 0 {
delete(md.HeaderMD, api.SCTMetadataKey)
delete(w.Header(), "Grpc-Metadata-sct")
w.Header().Set("SCT", vals[0])
}

// set http status code
if vals := md.HeaderMD.Get(api.HTTPResponseCodeMetadataKey); len(vals) > 0 {
code, err := strconv.Atoi(vals[0])
if err != nil {
return err
}
// delete the headers to not expose any grpc-metadata in http response
delete(md.HeaderMD, api.HTTPResponseCodeMetadataKey)
delete(w.Header(), "Grpc-Metadata-X-Http-Code")
w.WriteHeader(code)
}

return nil
}
Loading

0 comments on commit d464219

Please sign in to comment.