Repository: sigstore/fulcio
Pull request #1938 (2025-02-05 17:30:06)

Allow configurable client signing algorithms
#### Summary
This PR adds a --client-signing-algorithms flag to Fulcio to restrict what key/hash combinations are allowed.

Closes https://github.com/sigstore/fulcio/issues/1388
This is based on https://github.com/sigstore/fulcio/pull/1517, but I could not mark it as Ready because done by @tetsuo-cpp .

#### Release Note


#### Documentation


ret2libc: cc @haydentherapper 
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/sigstore/fulcio/pull/1938?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) Report
Attention: Patch coverage is `50.70423%` with `35 lines` in your changes missing coverage. Please review.
> Project coverage is 51.77%. Comparing base [(`cf238ac`)](https://app.codecov.io/gh/sigstore/fulcio/commit/cf238accd68ef13e1e3425a5d87db2f352de173c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) to head [(`277af89`)](https://app.codecov.io/gh/sigstore/fulcio/commit/277af893394e35447182fad2b24aa396b03dc53b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore).
> Report is 296 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/sigstore/fulcio/pull/1938?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) | Patch % | Lines |
|---|---|---|
| [pkg/server/grpc\_server.go](https://app.codecov.io/gh/sigstore/fulcio/pull/1938?src=pr&el=tree&filepath=pkg%2Fserver%2Fgrpc_server.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-cGtnL3NlcnZlci9ncnBjX3NlcnZlci5nbw==) | 52.63% | [15 Missing and 3 partials :warning: ](https://app.codecov.io/gh/sigstore/fulcio/pull/1938?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) |
| [cmd/app/serve.go](https://app.codecov.io/gh/sigstore/fulcio/pull/1938?src=pr&el=tree&filepath=cmd%2Fapp%2Fserve.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore#diff-Y21kL2FwcC9zZXJ2ZS5nbw==) | 45.16% | [16 Missing and 1 partial :warning: ](https://app.codecov.io/gh/sigstore/fulcio/pull/1938?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1938      +/-   ##
==========================================
- Coverage   57.93%   51.77%   -6.16%     
==========================================
  Files          50       73      +23     
  Lines        3119     5680    +2561     
==========================================
+ Hits         1807     2941    +1134     
- Misses       1154     2463    +1309     
- Partials      158      276     +118     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/sigstore/fulcio/pull/1938?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=sigstore).


Patch:
diff --git a/cmd/app/grpc.go b/cmd/app/grpc.go
index c2f5358..4ec902f 100644
--- a/cmd/app/grpc.go
+++ b/cmd/app/grpc.go
@@ -26,6 +26,8 @@ import (
 	"sync"
 	"syscall"
 
+	"github.com/sigstore/sigstore/pkg/signature"
+
 	"github.com/fsnotify/fsnotify"
 	"github.com/goadesign/goa/grpc/middleware"
 	ctclient "github.com/google/certificate-transparency-go/client"
@@ -155,7 +157,7 @@ func (c *cachedTLSCert) GRPCCreds() grpc.ServerOption {
 	}))
 }
 
-func createGRPCServer(cfg *config.FulcioConfig, ctClient *ctclient.LogClient, baseca ca.CertificateAuthority, ip identity.IssuerPool) (*grpcServer, error) {
+func createGRPCServer(cfg *config.FulcioConfig, ctClient *ctclient.LogClient, baseca ca.CertificateAuthority, algorithmRegistry *signature.AlgorithmRegistryConfig, ip identity.IssuerPool) (*grpcServer, error) {
 	logger, opts := log.SetupGRPCLogging()
 
 	serverOpts := []grpc.ServerOption{
@@ -186,7 +188,7 @@ func createGRPCServer(cfg *config.FulcioConfig, ctClient *ctclient.LogClient, ba
 
 	myServer := grpc.NewServer(serverOpts...)
 
-	grpcCAServer := server.NewGRPCCAServer(ctClient, baseca, ip)
+	grpcCAServer := server.NewGRPCCAServer(ctClient, baseca, algorithmRegistry, ip)
 
 	health.RegisterHealthServer(myServer, grpcCAServer)
 	// Register your gRPC service implementations.
diff --git a/cmd/app/serve.go b/cmd/app/serve.go
index ebec9d5..0932e97 100644
--- a/cmd/app/serve.go
+++ b/cmd/app/serve.go
@@ -33,6 +33,9 @@ import (
 	"syscall"
 	"time"
 
+	v1 "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1"
+	"github.com/sigstore/sigstore/pkg/signature"
+
 	"chainguard.dev/go-grpc-kit/pkg/duplex"
 	"github.com/goadesign/goa/grpc/middleware"
 	ctclient "github.com/google/certificate-transparency-go/client"
@@ -113,6 +116,15 @@ func newServeCmd() *cobra.Command {
 	cmd.Flags().String("grpc-tls-key", "", "the private key file to use for secure connections (without passphrase) - only applies to grpc-port")
 	cmd.Flags().Duration("idle-connection-timeout", 30*time.Second, "The time allowed for connections (HTTP or gRPC) to go idle before being closed by the server")
 	cmd.Flags().String("ct-log.tls-ca-cert", "", "Path to TLS CA certificate used to connect to ct-log")
+	cmd.Flags().StringSlice("client-signing-algorithms", buildDefaultClientSigningAlgorithms([]v1.PublicKeyDetails{
+		v1.PublicKeyDetails_PKIX_ECDSA_P256_SHA_256,
+		v1.PublicKeyDetails_PKIX_ECDSA_P384_SHA_384,
+		v1.PublicKeyDetails_PKIX_ECDSA_P521_SHA_512,
+		v1.PublicKeyDetails_PKIX_RSA_PKCS1V15_2048_SHA256,
+		v1.PublicKeyDetails_PKIX_RSA_PKCS1V15_3072_SHA256,
+		v1.PublicKeyDetails_PKIX_RSA_PKCS1V15_4096_SHA256,
+		v1.PublicKeyDetails_PKIX_ED25519,
+	}), "the list of allowed client signing algorithms")
 
 	// convert "http-host" flag to "host" and "http-port" flag to be "port"
 	cmd.Flags().SetNormalizeFunc(func(_ *pflag.FlagSet, name string) pflag.NormalizedName {
@@ -211,6 +223,20 @@ func runServeCmd(cmd *cobra.Command, args []string) { //nolint: revive
 	// Setup the logger to dev/prod
 	log.ConfigureLogger(viper.GetString("log_type"))
 
+	algorithmStrings := viper.GetStringSlice("client-signing-algorithms")
+	var algorithmConfig []v1.PublicKeyDetails
+	for _, s := range algorithmStrings {
+		algorithmValue, err := signature.ParseSignatureAlgorithmFlag(s)
+		if err != nil {
+			log.Logger.Fatal(err)
+		}
+		algorithmConfig = append(algorithmConfig, algorithmValue)
+	}
+	algorithmRegistry, err := signature.NewAlgorithmRegistryConfig(algorithmConfig)
+	if err != nil {
+		log.Logger.Fatalf("error loading --client-signing-algorithms=%s: %v", algorithmConfig, err)
+	}
+
 	// from https://github.com/golang/glog/commit/fca8c8854093a154ff1eb580aae10276ad6b1b5f
 	_ = flag.CommandLine.Parse([]string{})
 
@@ -324,7 +350,7 @@ func runServeCmd(cmd *cobra.Command, args []string) { //nolint: revive
 		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, ip); err != http.ErrServerClosed {
+		if err := StartDuplexServer(ctx, cfg, ctClient, baseca, algorithmRegistry, viper.GetString("host"), port, metricsPort, ip); err != http.ErrServerClosed {
 			log.Logger.Fatal(err)
 		}
 		return
@@ -337,7 +363,7 @@ func runServeCmd(cmd *cobra.Command, args []string) { //nolint: revive
 
 	reg := prometheus.NewRegistry()
 
-	grpcServer, err := createGRPCServer(cfg, ctClient, baseca, ip)
+	grpcServer, err := createGRPCServer(cfg, ctClient, baseca, algorithmRegistry, ip)
 	if err != nil {
 		log.Logger.Fatal(err)
 	}
@@ -415,7 +441,7 @@ func checkServeCmdConfigFile() error {
 	return nil
 }
 
-func StartDuplexServer(ctx context.Context, cfg *config.FulcioConfig, ctClient *ctclient.LogClient, baseca certauth.CertificateAuthority, host string, port, metricsPort int, ip identity.IssuerPool) error {
+func StartDuplexServer(ctx context.Context, cfg *config.FulcioConfig, ctClient *ctclient.LogClient, baseca certauth.CertificateAuthority, algorithmRegistry *signature.AlgorithmRegistryConfig, host string, port, metricsPort int, ip identity.IssuerPool) error {
 	logger, opts := log.SetupGRPCLogging()
 
 	d := duplex.New(
@@ -437,7 +463,7 @@ func StartDuplexServer(ctx context.Context, cfg *config.FulcioConfig, ctClient *
 	)
 
 	// GRPC server
-	grpcCAServer := server.NewGRPCCAServer(ctClient, baseca, ip)
+	grpcCAServer := server.NewGRPCCAServer(ctClient, baseca, algorithmRegistry, ip)
 	protobuf.RegisterCAServer(d.Server, grpcCAServer)
 	if err := d.RegisterHandler(ctx, protobuf.RegisterCAHandlerFromEndpoint); err != nil {
 		return fmt.Errorf("registering grpc ca handler: %w", err)
@@ -470,3 +496,15 @@ func StartDuplexServer(ctx context.Context, cfg *config.FulcioConfig, ctClient *
 	}
 	return nil
 }
+
+func buildDefaultClientSigningAlgorithms(allowedAlgorithms []v1.PublicKeyDetails) []string {
+	var algorithmStrings []string
+	for _, algorithm := range allowedAlgorithms {
+		algorithmString, err := signature.FormatSignatureAlgorithmFlag(algorithm)
+		if err != nil {
+			log.Logger.Fatal(err)
+		}
+		algorithmStrings = append(algorithmStrings, algorithmString)
+	}
+	return algorithmStrings
+}
diff --git a/go.mod b/go.mod
index 38f1abf..68ce3de 100644
--- a/go.mod
+++ b/go.mod
@@ -19,14 +19,15 @@ require (
 	github.com/google/go-cmp v0.6.0
 	github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
 	github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99
-	github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.0
 	github.com/hashicorp/golang-lru v1.0.2
 	github.com/magiconair/properties v1.8.9
 	github.com/prometheus/client_golang v1.20.5
 	github.com/prometheus/client_model v0.6.1
 	github.com/prometheus/common v0.62.0
 	github.com/rs/cors v1.11.1
-	github.com/sigstore/sigstore v1.8.12
+	github.com/sigstore/protobuf-specs v0.4.0
+	github.com/sigstore/sigstore v1.8.13-0.20250204232249-4b62818325b7
 	github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.12
 	github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.12
 	github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.12
@@ -42,9 +43,9 @@ require (
 	go.step.sm/crypto v0.57.0
 	go.uber.org/zap v1.27.0
 	google.golang.org/api v0.218.0
-	google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb
+	google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f
 	google.golang.org/grpc v1.70.0
-	google.golang.org/protobuf v1.36.3
+	google.golang.org/protobuf v1.36.4
 	gopkg.in/yaml.v3 v3.0.1
 	sigs.k8s.io/release-utils v0.9.0
 )
diff --git a/go.sum b/go.sum
index bf23363..5cfeb5d 100644
--- a/go.sum
+++ b/go.sum
@@ -175,8 +175,8 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDa
 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99 h1:JYghRBlGCZyCF2wNUJ8W0cwaQdtpcssJ4CgC406g+WU=
 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20210315223345-82c243799c99/go.mod h1:3bDW6wMZJB7tiONtC/1Xpicra6Wp5GgbTbQWCbI5fkc=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.0 h1:VD1gqscl4nYs1YxVuSdemTrSgTKrwOWDK0FVFMqm+Cg=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.0/go.mod h1:4EgsQoS4TOhJizV+JTFg40qx1Ofh3XmXEQNBpgvNT40=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
 github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -307,8 +307,10 @@ github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c
 github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
 github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
 github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
-github.com/sigstore/sigstore v1.8.12 h1:S8xMVZbE2z9ZBuQUEG737pxdLjnbOIcFi5v9UFfkJFc=
-github.com/sigstore/sigstore v1.8.12/go.mod h1:+PYQAa8rfw0QdPpBcT+Gl3egKD9c+TUgAlF12H3Nmjo=
+github.com/sigstore/protobuf-specs v0.4.0 h1:yoZbdh0kZYKOSiVbYyA8J3f2wLh5aUk2SQB7LgAfIdU=
+github.com/sigstore/protobuf-specs v0.4.0/go.mod h1:FKW5NYhnnFQ/Vb9RKtQk91iYd0MKJ9AxyqInEwU6+OI=
+github.com/sigstore/sigstore v1.8.13-0.20250204232249-4b62818325b7 h1:dKgngAj90pDWGKB/yBt/AmSvNFzLOPvYGfEgEKRQWc4=
+github.com/sigstore/sigstore v1.8.13-0.20250204232249-4b62818325b7/go.mod h1:2lXojNsjZjkqu1//FWxq7qUcPB8Lq1KsR5hc+GkcC/4=
 github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.12 h1:EC3UmIaa7nV9sCgSpVevmvgvTYTkMqyrRbj5ojPp7tE=
 github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.12/go.mod h1:aw60vs3crnQdM/DYH+yF2P0MVKtItwAX34nuaMrY7Lk=
 github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.12 h1:FPpliDTywSy0woLHMAdmTSZ5IS/lVBZ0dY0I+2HmnSY=
@@ -516,8 +518,8 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
 google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk=
 google.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc=
-google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb h1:B7GIB7sr443wZ/EAEl7VZjmh1V6qzkt5V+RYcUYtS1U=
-google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:E5//3O5ZIG2l71Xnt+P/CYUY8Bxs8E7WMoZ9tlcMbAY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA=
+google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
 google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
@@ -528,8 +530,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8
 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
 google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
 google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
-google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
-google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
+google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
diff --git a/pkg/server/grpc_server.go b/pkg/server/grpc_server.go
index 9b0cb59..d569d0e 100644
--- a/pkg/server/grpc_server.go
+++ b/pkg/server/grpc_server.go
@@ -18,10 +18,14 @@ package server
 import (
 	"context"
 	"crypto"
+	"crypto/ed25519"
+	"crypto/x509"
 	"encoding/json"
 	"errors"
 	"fmt"
 
+	"github.com/sigstore/sigstore/pkg/signature"
+
 	ctclient "github.com/google/certificate-transparency-go/client"
 	health "google.golang.org/grpc/health/grpc_health_v1"
 
@@ -44,11 +48,12 @@ type GRPCCAServer interface {
 	health.HealthServer
 }
 
-func NewGRPCCAServer(ct *ctclient.LogClient, ca certauth.CertificateAuthority, ip identity.IssuerPool) GRPCCAServer {
+func NewGRPCCAServer(ct *ctclient.LogClient, ca certauth.CertificateAuthority, algorithmRegistry *signature.AlgorithmRegistryConfig, ip identity.IssuerPool) GRPCCAServer {
 	return &grpcaCAServer{
-		ct:         ct,
-		ca:         ca,
-		IssuerPool: ip,
+		ct:                ct,
+		ca:                ca,
+		algorithmRegistry: algorithmRegistry,
+		IssuerPool:        ip,
 	}
 }
 
@@ -58,8 +63,9 @@ const (
 
 type grpcaCAServer struct {
 	fulciogrpc.UnimplementedCAServer
-	ct *ctclient.LogClient
-	ca certauth.CertificateAuthority
+	ct                *ctclient.LogClient
+	ca                certauth.CertificateAuthority
+	algorithmRegistry *signature.AlgorithmRegistryConfig
 	identity.IssuerPool
 }
 
@@ -88,6 +94,7 @@ func (g *grpcaCAServer) CreateSigningCertificate(ctx context.Context, request *f
 	}
 
 	var publicKey crypto.PublicKey
+	var hashFunc crypto.Hash
 	// Verify caller is in possession of their private key and extract
 	// public key from request.
 	if len(request.GetCertificateSigningRequest()) > 0 {
@@ -106,6 +113,11 @@ func (g *grpcaCAServer) CreateSigningCertificate(ctx context.Context, request *f
 		if err := csr.CheckSignature(); err != nil {
 			return nil, handleFulcioGRPCError(ctx, codes.InvalidArgument, err, invalidSignature)
 		}
+
+		hashFunc, err = getHashFuncForSignatureAlgorithm(csr.SignatureAlgorithm)
+		if err != nil {
+			return nil, handleFulcioGRPCError(ctx, codes.InvalidArgument, err, err.Error())
+		}
 	} else {
 		// Option 2: Check the signature for proof of possession of a private key
 		var (
@@ -133,6 +145,22 @@ func (g *grpcaCAServer) CreateSigningCertificate(ctx context.Context, request *f
 		if err := challenges.CheckSignature(publicKey, proofOfPossession, principal.Name(ctx)); err != nil {
 			return nil, handleFulcioGRPCError(ctx, codes.InvalidArgument, err, invalidSignature)
 		}
+
+		// The proof of possession signature always uses SHA-256, unless the key algorithm is ED25519
+		hashFunc = crypto.SHA256
+		if _, ok := publicKey.(ed25519.PublicKey); ok {
+			hashFunc = crypto.Hash(0)
+		}
+	}
+
+	// Check whether the public-key/hash algorithm combination is allowed
+	isPermitted, err := g.algorithmRegistry.IsAlgorithmPermitted(publicKey, hashFunc)
+	if err != nil {
+		return nil, handleFulcioGRPCError(ctx, codes.InvalidArgument, err, err.Error())
+	}
+	if !isPermitted {
+		err = fmt.Errorf("Signing algorithm not permitted: %T, %s", publicKey, hashFunc)
+		return nil, handleFulcioGRPCError(ctx, codes.InvalidArgument, err, err.Error())
 	}
 
 	var csc *certauth.CodeSigningCertificate
@@ -280,3 +308,23 @@ func (g *grpcaCAServer) Check(_ context.Context, _ *health.HealthCheckRequest) (
 func (g *grpcaCAServer) Watch(_ *health.HealthCheckRequest, _ health.Health_WatchServer) error {
 	return status.Error(codes.Unimplemented, "unimplemented")
 }
+
+func getHashFuncForSignatureAlgorithm(signatureAlgorithm x509.SignatureAlgorithm) (crypto.Hash, error) {
+	switch signatureAlgorithm {
+	case x509.ECDSAWithSHA256:
+		return crypto.SHA256, nil
+	case x509.ECDSAWithSHA384:
+		return crypto.SHA384, nil
+	case x509.ECDSAWithSHA512:
+		return crypto.SHA512, nil
+	case x509.SHA256WithRSA:
+		return crypto.SHA256, nil
+	case x509.SHA384WithRSA:
+		return crypto.SHA384, nil
+	case x509.SHA512WithRSA:
+		return crypto.SHA512, nil
+	case x509.PureEd25519:
+		return crypto.Hash(0), nil
+	}
+	return crypto.Hash(0), fmt.Errorf("unrecognized signature algorithm: %s", signatureAlgorithm)
+}


Test patch:
diff --git a/cmd/app/http_test.go b/cmd/app/http_test.go
index b93b71b..ed34ada 100644
--- a/cmd/app/http_test.go
+++ b/cmd/app/http_test.go
@@ -32,6 +32,8 @@ import (
 
 	"github.com/sigstore/fulcio/pkg/ca"
 	"github.com/sigstore/fulcio/pkg/identity"
+	v1 "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1"
+	"github.com/sigstore/sigstore/pkg/signature"
 	"github.com/spf13/viper"
 
 	"google.golang.org/grpc"
@@ -47,7 +49,11 @@ func setupHTTPServer(t *testing.T) (httpServer, string) {
 
 	viper.Set("grpc-host", "")
 	viper.Set("grpc-port", 0)
-	grpcServer, err := createGRPCServer(nil, nil, &TrivialCertificateAuthority{}, nil)
+	algorithmRegistry, err := signature.NewAlgorithmRegistryConfig([]v1.PublicKeyDetails{})
+	if err != nil {
+		t.Error(err)
+	}
+	grpcServer, err := createGRPCServer(nil, nil, &TrivialCertificateAuthority{}, algorithmRegistry, nil)
 	if err != nil {
 		t.Error(err)
 	}
@@ -93,7 +99,11 @@ func setupHTTPServerWithGRPCTLS(t *testing.T) (httpServer, string) {
 
 	viper.Set("grpc-host", "")
 	viper.Set("grpc-port", 0)
-	grpcServer, err := createGRPCServer(nil, nil, &TrivialCertificateAuthority{}, nil)
+	algorithmRegistry, err := signature.NewAlgorithmRegistryConfig([]v1.PublicKeyDetails{})
+	if err != nil {
+		t.Error(err)
+	}
+	grpcServer, err := createGRPCServer(nil, nil, &TrivialCertificateAuthority{}, algorithmRegistry, nil)
 	if err != nil {
 		t.Error(err)
 	}
diff --git a/cmd/app/serve_test.go b/cmd/app/serve_test.go
index fe1f6b8..10ce349 100644
--- a/cmd/app/serve_test.go
+++ b/cmd/app/serve_test.go
@@ -31,6 +31,8 @@ import (
 	"github.com/sigstore/fulcio/pkg/ca/ephemeralca"
 	"github.com/sigstore/fulcio/pkg/config"
 	"github.com/sigstore/fulcio/pkg/generated/protobuf"
+	v1 "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1"
+	"github.com/sigstore/sigstore/pkg/signature"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials/insecure"
 )
@@ -48,9 +50,13 @@ func TestDuplex(t *testing.T) {
 		t.Fatal(err)
 	}
 	metricsPort := 2114
+	algorithmRegistry, err := signature.NewAlgorithmRegistryConfig([]v1.PublicKeyDetails{})
+	if err != nil {
+		t.Error(err)
+	}
 
 	go func() {
-		if err := StartDuplexServer(ctx, config.DefaultConfig, nil, ca, "localhost", port, metricsPort, nil); err != nil {
+		if err := StartDuplexServer(ctx, config.DefaultConfig, nil, ca, algorithmRegistry, "localhost", port, metricsPort, nil); err != nil {
 			log.Fatalf("error starting duplex server: %v", err)
 		}
 	}()
diff --git a/pkg/server/grpc_server_test.go b/pkg/server/grpc_server_test.go
index 56f05b7..6b27eb1 100644
--- a/pkg/server/grpc_server_test.go
+++ b/pkg/server/grpc_server_test.go
@@ -57,7 +57,9 @@ import (
 	"github.com/sigstore/fulcio/pkg/config"
 	"github.com/sigstore/fulcio/pkg/generated/protobuf"
 	"github.com/sigstore/fulcio/pkg/identity"
+	v1 "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1"
 	"github.com/sigstore/sigstore/pkg/cryptoutils"
+	"github.com/sigstore/sigstore/pkg/signature"
 	"google.golang.org/grpc/resolver"
 )
 
@@ -93,7 +95,11 @@ func setupGRPCForTest(t *testing.T, cfg *config.FulcioConfig, ctl *ctclient.LogC
 	lis = bufconn.Listen(bufSize)
 	s := grpc.NewServer(grpc.UnaryInterceptor(passFulcioConfigThruContext(cfg)))
 	ip := NewIssuerPool(cfg)
-	protobuf.RegisterCAServer(s, NewGRPCCAServer(ctl, ca, ip))
+	algorithmRegistry, err := signature.NewAlgorithmRegistryConfig([]v1.PublicKeyDetails{v1.PublicKeyDetails_PKIX_ECDSA_P256_SHA_256, v1.PublicKeyDetails_PKIX_RSA_PKCS1V15_2048_SHA256})
+	if err != nil {
+		t.Error(err)
+	}
+	protobuf.RegisterCAServer(s, NewGRPCCAServer(ctl, ca, algorithmRegistry, ip))
 	go func() {
 		if err := s.Serve(lis); err != nil && !errors.Is(err, grpc.ErrServerStopped) {
 			t.Errorf("Server exited with error: %v", err)
@@ -1639,7 +1645,7 @@ func TestAPIWithChainguard(t *testing.T) {
 		Audience: jwt.Audience{"sigstore"},
 	}).Claims(&claims).Serialize()
 	if err != nil {
-		t.Fatalf("CompactSerialize() = %v", err)
+		t.Fatalf("Serialize() = %v", err)
 	}
 
 	ctClient, eca := createCA(cfg, t)
@@ -1791,6 +1797,94 @@ func TestAPIWithIssuerClaimConfig(t *testing.T) {
 	}
 }
 
+// Tests API with an RSA key
+func TestAPIWithRSA(t *testing.T) {
+	emailSigner, emailIssuer := newOIDCIssuer(t)
+
+	// Create a FulcioConfig that supports these issuers.
+	cfg, err := config.Read([]byte(fmt.Sprintf(`{
+		"OIDCIssuers": {
+			%q: {
+				"IssuerURL": %q,
+				"ClientID": "sigstore",
+				"Type": "email"
+			}
+		}
+	}`, emailIssuer, emailIssuer)))
+	if err != nil {
+		t.Fatalf("config.Read() = %v", err)
+	}
+
+	emailSubject := "foo@example.com"
+
+	// Create an OIDC token using this issuer's signer.
+	tok, err := jwt.Signed(emailSigner).Claims(jwt.Claims{
+		Issuer:   emailIssuer,
+		IssuedAt: jwt.NewNumericDate(time.Now()),
+		Expiry:   jwt.NewNumericDate(time.Now().Add(30 * time.Minute)),
+		Subject:  emailSubject,
+		Audience: jwt.Audience{"sigstore"},
+	}).Claims(customClaims{Email: emailSubject, EmailVerified: true}).Serialize()
+	if err != nil {
+		t.Fatalf("Serialize() = %v", err)
+	}
+
+	ctClient, eca := createCA(cfg, t)
+	ctx := context.Background()
+	server, conn := setupGRPCForTest(t, cfg, ctClient, eca)
+	defer func() {
+		server.Stop()
+		conn.Close()
+	}()
+
+	client := protobuf.NewCAClient(conn)
+
+	priv, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		t.Fatalf("GenerateKey() = %v", err)
+	}
+	pubBytes, err := x509.MarshalPKIXPublicKey(&priv.PublicKey)
+	if err != nil {
+		t.Fatalf("x509.MarshalPKIXPublicKey() = %v", err)
+	}
+	hash := sha256.Sum256([]byte(emailSubject))
+	proof, err := rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA256, hash[:])
+	if err != nil {
+		t.Fatalf("SignPKCS1v15() = %v", err)
+	}
+	pemBytes := string(cryptoutils.PEMEncode(cryptoutils.PublicKeyPEMType, pubBytes))
+
+	// Hit the API to have it sign our certificate.
+	resp, err := client.CreateSigningCertificate(ctx, &protobuf.CreateSigningCertificateRequest{
+		Credentials: &protobuf.Credentials{
+			Credentials: &protobuf.Credentials_OidcIdentityToken{
+				OidcIdentityToken: tok,
+			},
+		},
+		Key: &protobuf.CreateSigningCertificateRequest_PublicKeyRequest{
+			PublicKeyRequest: &protobuf.PublicKeyRequest{
+				PublicKey: &protobuf.PublicKey{
+					Content: pemBytes,
+				},
+				ProofOfPossession: proof,
+			},
+		},
+	})
+	if err != nil {
+		t.Fatalf("SigningCert() = %v", err)
+	}
+
+	leafCert := verifyResponse(resp, eca, emailIssuer, t)
+
+	// Expect email subject
+	if len(leafCert.EmailAddresses) != 1 {
+		t.Fatalf("unexpected length of leaf certificate URIs, expected 1, got %d", len(leafCert.URIs))
+	}
+	if leafCert.EmailAddresses[0] != emailSubject {
+		t.Fatalf("subjects do not match: Expected %v, got %v", emailSubject, leafCert.EmailAddresses[0])
+	}
+}
+
 // Tests API with challenge sent as CSR
 func TestAPIWithCSRChallenge(t *testing.T) {
 	emailSigner, emailIssuer := newOIDCIssuer(t)
@@ -1873,6 +1967,88 @@ func TestAPIWithCSRChallenge(t *testing.T) {
 	}
 }
 
+// Tests API with challenge sent as CSR with an RSA key
+func TestAPIWithCSRChallengeRSA(t *testing.T) {
+	emailSigner, emailIssuer := newOIDCIssuer(t)
+
+	// Create a FulcioConfig that supports these issuers.
+	cfg, err := config.Read([]byte(fmt.Sprintf(`{
+		"OIDCIssuers": {
+			%q: {
+				"IssuerURL": %q,
+				"ClientID": "sigstore",
+				"Type": "email"
+			}
+		}
+	}`, emailIssuer, emailIssuer)))
+	if err != nil {
+		t.Fatalf("config.Read() = %v", err)
+	}
+
+	emailSubject := "foo@example.com"
+
+	// Create an OIDC token using this issuer's signer.
+	tok, err := jwt.Signed(emailSigner).Claims(jwt.Claims{
+		Issuer:   emailIssuer,
+		IssuedAt: jwt.NewNumericDate(time.Now()),
+		Expiry:   jwt.NewNumericDate(time.Now().Add(30 * time.Minute)),
+		Subject:  emailSubject,
+		Audience: jwt.Audience{"sigstore"},
+	}).Claims(customClaims{Email: emailSubject, EmailVerified: true}).Serialize()
+	if err != nil {
+		t.Fatalf("Serialize() = %v", err)
+	}
+
+	ctClient, eca := createCA(cfg, t)
+	ctx := context.Background()
+	server, conn := setupGRPCForTest(t, cfg, ctClient, eca)
+	defer func() {
+		server.Stop()
+		conn.Close()
+	}()
+
+	client := protobuf.NewCAClient(conn)
+
+	priv, err := rsa.GenerateKey(rand.Reader, 2048)
+	if err != nil {
+		t.Fatalf("GenerateKey() = %v", err)
+	}
+	csrTmpl := &x509.CertificateRequest{Subject: pkix.Name{CommonName: "test"}}
+	derCSR, err := x509.CreateCertificateRequest(rand.Reader, csrTmpl, priv)
+	if err != nil {
+		t.Fatalf("error creating CSR: %v", err)
+	}
+	pemCSR := pem.EncodeToMemory(&pem.Block{
+		Type:  "CERTIFICATE REQUEST",
+		Bytes: derCSR,
+	})
+
+	// Hit the API to have it sign our certificate.
+	resp, err := client.CreateSigningCertificate(ctx, &protobuf.CreateSigningCertificateRequest{
+		Credentials: &protobuf.Credentials{
+			Credentials: &protobuf.Credentials_OidcIdentityToken{
+				OidcIdentityToken: tok,
+			},
+		},
+		Key: &protobuf.CreateSigningCertificateRequest_CertificateSigningRequest{
+			CertificateSigningRequest: pemCSR,
+		},
+	})
+	if err != nil {
+		t.Fatalf("SigningCert() = %v", err)
+	}
+
+	leafCert := verifyResponse(resp, eca, emailIssuer, t)
+
+	// Expect email subject
+	if len(leafCert.EmailAddresses) != 1 {
+		t.Fatalf("unexpected length of leaf certificate URIs, expected 1, got %d", len(leafCert.URIs))
+	}
+	if leafCert.EmailAddresses[0] != emailSubject {
+		t.Fatalf("subjects do not match: Expected %v, got %v", emailSubject, leafCert.EmailAddresses[0])
+	}
+}
+
 // Tests API with insecure pub key
 func TestAPIWithInsecurePublicKey(t *testing.T) {
 	emailSigner, emailIssuer := newOIDCIssuer(t)
@@ -2092,6 +2268,87 @@ func TestAPIWithInvalidChallenge(t *testing.T) {
 	}
 }
 
+// Tests API with an ECDSA key with an unpermitted curve
+func TestAPIWithInvalidPublicKey(t *testing.T) {
+	emailSigner, emailIssuer := newOIDCIssuer(t)
+
+	// Create a FulcioConfig that supports these issuers.
+	cfg, err := config.Read([]byte(fmt.Sprintf(`{
+		"OIDCIssuers": {
+			%q: {
+				"IssuerURL": %q,
+				"ClientID": "sigstore",
+				"Type": "email"
+			}
+		}
+	}`, emailIssuer, emailIssuer)))
+	if err != nil {
+		t.Fatalf("config.Read() = %v", err)
+	}
+
+	emailSubject := "foo@example.com"
+
+	// Create an OIDC token using this issuer's signer.
+	tok, err := jwt.Signed(emailSigner).Claims(jwt.Claims{
+		Issuer:   emailIssuer,
+		IssuedAt: jwt.NewNumericDate(time.Now()),
+		Expiry:   jwt.NewNumericDate(time.Now().Add(30 * time.Minute)),
+		Subject:  emailSubject,
+		Audience: jwt.Audience{"sigstore"},
+	}).Claims(customClaims{Email: emailSubject, EmailVerified: true}).Serialize()
+	if err != nil {
+		t.Fatalf("Serialize() = %v", err)
+	}
+
+	ctClient, eca := createCA(cfg, t)
+	ctx := context.Background()
+	server, conn := setupGRPCForTest(t, cfg, ctClient, eca)
+	defer func() {
+		server.Stop()
+		conn.Close()
+	}()
+
+	client := protobuf.NewCAClient(conn)
+
+	// Generate an ECDSA key with an unpermitted curve
+	priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
+	if err != nil {
+		t.Fatalf("GenerateKey() = %v", err)
+	}
+	pubBytes, err := x509.MarshalPKIXPublicKey(&priv.PublicKey)
+	if err != nil {
+		t.Fatalf("x509.MarshalPKIXPublicKey() = %v", err)
+	}
+	hash := sha256.Sum256([]byte(emailSubject))
+	proof, err := ecdsa.SignASN1(rand.Reader, priv, hash[:])
+	if err != nil {
+		t.Fatalf("SignASN1() = %v", err)
+	}
+	pemBytes := string(cryptoutils.PEMEncode(cryptoutils.PublicKeyPEMType, pubBytes))
+
+	_, err = client.CreateSigningCertificate(ctx, &protobuf.CreateSigningCertificateRequest{
+		Credentials: &protobuf.Credentials{
+			Credentials: &protobuf.Credentials_OidcIdentityToken{
+				OidcIdentityToken: tok,
+			},
+		},
+		Key: &protobuf.CreateSigningCertificateRequest_PublicKeyRequest{
+			PublicKeyRequest: &protobuf.PublicKeyRequest{
+				PublicKey: &protobuf.PublicKey{
+					Content: pemBytes,
+				},
+				ProofOfPossession: proof,
+			},
+		},
+	})
+	if err == nil || !strings.Contains(err.Error(), "Signing algorithm not permitted") {
+		t.Fatalf("expected signing algorithm not permitted, got %v", err)
+	}
+	if status.Code(err) != codes.InvalidArgument {
+		t.Fatalf("expected invalid argument, got %v", status.Code(err))
+	}
+}
+
 // Tests API with an invalid CSR.
 func TestAPIWithInvalidCSR(t *testing.T) {
 	emailSigner, emailIssuer := newOIDCIssuer(t)
@@ -2231,6 +2488,81 @@ func TestAPIWithInvalidCSRSignature(t *testing.T) {
 	}
 }
 
+// Tests API with CSR, containing ECDSA key with unpermitted curve
+func TestAPIWithInvalidCSRPublicKey(t *testing.T) {
+	emailSigner, emailIssuer := newOIDCIssuer(t)
+
+	// Create a FulcioConfig that supports this issuer.
+	cfg, err := config.Read([]byte(fmt.Sprintf(`{
+		"OIDCIssuers": {
+			%q: {
+				"IssuerURL": %q,
+				"ClientID": "sigstore",
+				"Type": "email"
+			}
+		}
+	}`, emailIssuer, emailIssuer)))
+	if err != nil {
+		t.Fatalf("config.Read() = %v", err)
+	}
+
+	emailSubject := "foo@example.com"
+
+	// Create an OIDC token using this issuer's signer.
+	tok, err := jwt.Signed(emailSigner).Claims(jwt.Claims{
+		Issuer:   emailIssuer,
+		IssuedAt: jwt.NewNumericDate(time.Now()),
+		Expiry:   jwt.NewNumericDate(time.Now().Add(30 * time.Minute)),
+		Subject:  emailSubject,
+		Audience: jwt.Audience{"sigstore"},
+	}).Claims(customClaims{Email: emailSubject, EmailVerified: true}).Serialize()
+	if err != nil {
+		t.Fatalf("Serialize() = %v", err)
+	}
+
+	ctClient, eca := createCA(cfg, t)
+	ctx := context.Background()
+	server, conn := setupGRPCForTest(t, cfg, ctClient, eca)
+	defer func() {
+		server.Stop()
+		conn.Close()
+	}()
+
+	client := protobuf.NewCAClient(conn)
+
+	priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
+	if err != nil {
+		t.Fatalf("error generating private key: %v", err)
+	}
+	csrTmpl := &x509.CertificateRequest{Subject: pkix.Name{CommonName: "test"}}
+	derCSR, err := x509.CreateCertificateRequest(rand.Reader, csrTmpl, priv)
+	if err != nil {
+		t.Fatalf("error creating CSR: %v", err)
+	}
+	pemCSR := pem.EncodeToMemory(&pem.Block{
+		Type:  "CERTIFICATE REQUEST",
+		Bytes: derCSR,
+	})
+
+	// Hit the API to have it sign our certificate.
+	_, err = client.CreateSigningCertificate(ctx, &protobuf.CreateSigningCertificateRequest{
+		Credentials: &protobuf.Credentials{
+			Credentials: &protobuf.Credentials_OidcIdentityToken{
+				OidcIdentityToken: tok,
+			},
+		},
+		Key: &protobuf.CreateSigningCertificateRequest_CertificateSigningRequest{
+			CertificateSigningRequest: pemCSR,
+		},
+	})
+	if err == nil || !strings.Contains(err.Error(), "Signing algorithm not permitted") {
+		t.Fatalf("expected signing algorithm not permitted, got %v", err)
+	}
+	if status.Code(err) != codes.InvalidArgument {
+		t.Fatalf("expected invalid argument, got %v", status.Code(err))
+	}
+}
+
 // Stand up a very simple OIDC endpoint.
 func newOIDCIssuer(t *testing.T) (jose.Signer, string) {
 	t.Helper()