Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions client/pkg/transport/listener_tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,29 @@ func (l *tlsListener) acceptLoop() {
}
}

// ConfigureCRLVerification appends a VerifyConnection hook to cfg that
// rejects any peer certificate whose serial number appears in the CRL file
// configured on info. It is a no-op when CRLFile is empty. Any existing
// VerifyConnection hook is called first and its error short-circuits.
func (info TLSInfo) ConfigureCRLVerification(cfg *tls.Config) {
if len(info.CRLFile) == 0 {
return
}
crlFile := info.CRLFile
prev := cfg.VerifyConnection
cfg.VerifyConnection = func(cs tls.ConnectionState) error {
if prev != nil {
if err := prev(cs); err != nil {
return err
}
}
if len(cs.PeerCertificates) == 0 {
return nil
}
return checkCRL(crlFile, cs.PeerCertificates)
}
}

func checkCRL(crlPath string, cert []*x509.Certificate) error {
// TODO: cache
crlBytes, err := os.ReadFile(crlPath)
Expand Down
8 changes: 8 additions & 0 deletions server/embed/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,14 @@ func (sctx *serveCtx) serve(
return tlsErr
}

// In gRPC-only mode the gRPC stack owns the TLS handshake (via grpc.Creds).
// Wrapping sctx.l with a second TLS listener would cause a double-TLS failure,
// so inject CRL checking into the TLS config before the gRPC server is created
// (gRPC clones the config at creation time).
if onlyGRPC {
tlsinfo.ConfigureCRLVerification(tlscfg)
}

if grpcEnabled {
gs = v3rpc.Server(s, tlscfg, nil, gopts...)
v3electionpb.RegisterElectionServer(gs, servElection)
Expand Down
16 changes: 16 additions & 0 deletions tests/e2e/ctl_v3_kv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,22 @@ func TestCtlV3GetRevokedCRL(t *testing.T) {
testCtl(t, testGetRevokedCRL, withCfg(*cfg))
}

// TestCtlV3GetRevokedCRLGRPCOnly is the same as TestCtlV3GetRevokedCRL but uses
// --listen-client-http-urls to split HTTP and gRPC onto separate ports. In that
// mode the gRPC server owns the TLS handshake directly (onlyGRPC=true in
// serve.go) and CRL checking must be injected via ConfigureCRLVerification
// rather than through the wrapping TLS listener used in the combined path.
func TestCtlV3GetRevokedCRLGRPCOnly(t *testing.T) {
cfg := e2e.NewConfig(
e2e.WithClusterSize(1),
e2e.WithClientConnType(e2e.ClientTLS),
e2e.WithClientRevokeCerts(true),
e2e.WithClientCertAuthority(true),
e2e.WithClientHTTPSeparate(true),
)
testCtl(t, testGetRevokedCRL, withCfg(*cfg))
}

func testGetRevokedCRL(cx ctlCtx) {
// test reject
require.ErrorContains(cx.t, ctlV3Put(cx, "k", "v", ""), "context deadline exceeded")
Expand Down