mirror of
https://github.com/go-gost/x.git
synced 2026-10-19 16:16:32 +00:00
feat(handler): forward mTLS client cert identity to auth plugins
Extracts the verified mTLS peer certificate (CN, SANs, SHA-256 fingerprint) from the TLS connection in the HTTP handler after the handshake completes, stores it in the request context, and forwards it to both gRPC and HTTP auth plugins via new AuthenticateRequest fields (client_cn, client_san, client_cert_fingerprint).
This commit is contained in:
+12
-7
@@ -60,13 +60,18 @@ func (p *grpcPlugin) Authenticate(ctx context.Context, user, password string, op
|
||||
if v := xctx.SrcAddrFromContext(ctx); v != nil {
|
||||
clientAddr = v.String()
|
||||
}
|
||||
r, err := p.client.Authenticate(ctx,
|
||||
&proto.AuthenticateRequest{
|
||||
Service: options.Service,
|
||||
Username: user,
|
||||
Password: password,
|
||||
Client: clientAddr,
|
||||
})
|
||||
req := &proto.AuthenticateRequest{
|
||||
Service: options.Service,
|
||||
Username: user,
|
||||
Password: password,
|
||||
Client: clientAddr,
|
||||
}
|
||||
if v := xctx.PeerCertFromContext(ctx); v != nil {
|
||||
req.ClientCn = v.CN
|
||||
req.ClientSan = v.SANs
|
||||
req.ClientCertFingerprint = v.Fingerprint
|
||||
}
|
||||
r, err := p.client.Authenticate(ctx, req)
|
||||
if err != nil {
|
||||
p.log.Error(err)
|
||||
return "", false
|
||||
|
||||
+12
-4
@@ -14,10 +14,13 @@ import (
|
||||
)
|
||||
|
||||
type httpPluginRequest struct {
|
||||
Service string `json:"service"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Client string `json:"client"`
|
||||
Service string `json:"service"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Client string `json:"client"`
|
||||
ClientCn string `json:"clientCn,omitempty"`
|
||||
ClientSan []string `json:"clientSan,omitempty"`
|
||||
ClientCertFingerprint string `json:"clientCertFingerprint,omitempty"`
|
||||
}
|
||||
|
||||
type httpPluginResponse struct {
|
||||
@@ -71,6 +74,11 @@ func (p *httpPlugin) Authenticate(ctx context.Context, user, password string, op
|
||||
Password: password,
|
||||
Client: clientAddr,
|
||||
}
|
||||
if v := xctx.PeerCertFromContext(ctx); v != nil {
|
||||
rb.ClientCn = v.CN
|
||||
rb.ClientSan = v.SANs
|
||||
rb.ClientCertFingerprint = v.Fingerprint
|
||||
}
|
||||
v, err := json.Marshal(&rb)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -101,6 +101,26 @@ func ClientIDFromContext(ctx context.Context) ClientID {
|
||||
return v
|
||||
}
|
||||
|
||||
type peerCertKey struct{}
|
||||
|
||||
// PeerCert carries the verified mTLS client certificate identity.
|
||||
type PeerCert struct {
|
||||
CN string
|
||||
SANs []string
|
||||
Fingerprint string // SHA-256 hex of cert.Raw
|
||||
}
|
||||
|
||||
// ContextWithPeerCert returns a copy of ctx that carries the peer cert.
|
||||
func ContextWithPeerCert(ctx context.Context, cert *PeerCert) context.Context {
|
||||
return context.WithValue(ctx, peerCertKey{}, cert)
|
||||
}
|
||||
|
||||
// PeerCertFromContext returns the peer cert stored in ctx, or nil.
|
||||
func PeerCertFromContext(ctx context.Context) *PeerCert {
|
||||
v, _ := ctx.Value(peerCertKey{}).(*PeerCert)
|
||||
return v
|
||||
}
|
||||
|
||||
// labelsKey saves the static service labels.
|
||||
type labelsKey struct{}
|
||||
|
||||
|
||||
@@ -150,6 +150,49 @@ func TestClientIDFromContext_WrongType(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerCertContext(t *testing.T) {
|
||||
cert := &PeerCert{
|
||||
CN: "client.example.com",
|
||||
SANs: []string{"san1.example.com", "san2.example.com"},
|
||||
Fingerprint: "abcdef1234567890",
|
||||
}
|
||||
ctx := ContextWithPeerCert(context.Background(), cert)
|
||||
|
||||
got := PeerCertFromContext(ctx)
|
||||
if got == nil {
|
||||
t.Fatal("PeerCertFromContext() = nil, want non-nil")
|
||||
}
|
||||
if got.CN != cert.CN {
|
||||
t.Errorf("PeerCertFromContext().CN = %q, want %q", got.CN, cert.CN)
|
||||
}
|
||||
if len(got.SANs) != len(cert.SANs) || got.SANs[0] != cert.SANs[0] {
|
||||
t.Errorf("PeerCertFromContext().SANs = %v, want %v", got.SANs, cert.SANs)
|
||||
}
|
||||
if got.Fingerprint != cert.Fingerprint {
|
||||
t.Errorf("PeerCertFromContext().Fingerprint = %q, want %q", got.Fingerprint, cert.Fingerprint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerCertFromContext_Empty(t *testing.T) {
|
||||
if got := PeerCertFromContext(context.Background()); got != nil {
|
||||
t.Errorf("PeerCertFromContext(empty) = %v, want nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerCertFromContext_WrongType(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), peerCertKey{}, "not-a-cert")
|
||||
if got := PeerCertFromContext(ctx); got != nil {
|
||||
t.Errorf("PeerCertFromContext(wrong type) = %v, want nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerCertFromContext_Nil(t *testing.T) {
|
||||
ctx := ContextWithPeerCert(context.Background(), nil)
|
||||
if got := PeerCertFromContext(ctx); got != nil {
|
||||
t.Errorf("PeerCertFromContext(nil) = %v, want nil", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextWithLabels(t *testing.T) {
|
||||
labels := map[string]string{"tenant": "acme", "region": "eu"}
|
||||
ctx := ContextWithLabels(context.Background(), labels)
|
||||
|
||||
@@ -190,6 +190,9 @@ package http
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -353,6 +356,9 @@ func (h *httpHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler
|
||||
})
|
||||
log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr())
|
||||
|
||||
// Save the raw conn before stats wrapping to preserve TLS metadata.
|
||||
rawConn := conn
|
||||
|
||||
pStats := xstats.Stats{}
|
||||
conn = stats_wrapper.WrapConn(conn, &pStats)
|
||||
|
||||
@@ -386,6 +392,11 @@ func (h *httpHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler
|
||||
}
|
||||
defer req.Body.Close()
|
||||
|
||||
// Extract mTLS peer certificate from the raw connection.
|
||||
if peerCert := getTLSPeerCert(rawConn); peerCert != nil {
|
||||
ctx = xctx.ContextWithPeerCert(ctx, peerCert)
|
||||
}
|
||||
|
||||
if clientIP := xhttp.GetClientIP(req); clientIP != nil {
|
||||
ro.ClientIP = clientIP.String()
|
||||
}
|
||||
@@ -546,6 +557,39 @@ func buildHTTPRecorder(req *http.Request) *xrecorder.HTTPRecorderObject {
|
||||
}
|
||||
}
|
||||
|
||||
// getTLSPeerCert extracts the verified mTLS client certificate identity from
|
||||
// a connection by walking wrapper layers (traffic limiter, etc.) to reach the
|
||||
// underlying *tls.Conn, then reading ConnectionState().VerifiedChains.
|
||||
// It returns nil if the connection has no TLS peer certificate.
|
||||
func getTLSPeerCert(conn net.Conn) *xctx.PeerCert {
|
||||
for {
|
||||
if tc, ok := conn.(interface{ ConnectionState() tls.ConnectionState }); ok {
|
||||
cs := tc.ConnectionState()
|
||||
if cs.HandshakeComplete && len(cs.VerifiedChains) > 0 && len(cs.VerifiedChains[0]) > 0 {
|
||||
cert := cs.VerifiedChains[0][0]
|
||||
fpr := sha256.Sum256(cert.Raw)
|
||||
sans := make([]string, 0, len(cert.DNSNames)+len(cert.EmailAddresses)+len(cert.URIs))
|
||||
sans = append(sans, cert.DNSNames...)
|
||||
sans = append(sans, cert.EmailAddresses...)
|
||||
for _, u := range cert.URIs {
|
||||
sans = append(sans, u.String())
|
||||
}
|
||||
return &xctx.PeerCert{
|
||||
CN: cert.Subject.CommonName,
|
||||
SANs: sans,
|
||||
Fingerprint: hex.EncodeToString(fpr[:]),
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if uw, ok := conn.(interface{ UnwrapConn() net.Conn }); ok {
|
||||
conn = uw.UnwrapConn()
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// observeStats periodically publishes per-client traffic stats to the
|
||||
// configured Observer. It runs in a background goroutine started by Init.
|
||||
// Events that fail to send are retried on the next tick.
|
||||
|
||||
Reference in New Issue
Block a user