From df175da330966b1c96c31698f41d4d7f91518d42 Mon Sep 17 00:00:00 2001
From: Bolke de Bruin <bolke@xs4all.nl>
Date: Wed, 12 Oct 2022 16:32:05 +0200
Subject: [PATCH] Add kdcproxy to support spnego

---
 cmd/rdpgw/config/configuration.go |  16 ++-
 cmd/rdpgw/kdcproxy/proxy.go       | 157 ++++++++++++++++++++++++++++++
 cmd/rdpgw/main.go                 |  17 ++++
 cmd/rdpgw/protocol/gateway.go     |   4 +-
 go.mod                            |  16 ++-
 5 files changed, 201 insertions(+), 9 deletions(-)
 create mode 100644 cmd/rdpgw/kdcproxy/proxy.go

diff --git a/cmd/rdpgw/config/configuration.go b/cmd/rdpgw/config/configuration.go
index 673a9eb..92d08eb 100644
--- a/cmd/rdpgw/config/configuration.go
+++ b/cmd/rdpgw/config/configuration.go
@@ -21,13 +21,15 @@ const (
 	SessionStoreCookie = "cookie"
 	SessionStoreFile   = "file"
 
-	AuthenticationOpenId = "openid"
-	AuthenticationBasic  = "local"
+	AuthenticationOpenId   = "openid"
+	AuthenticationBasic    = "local"
+	AuthenticationKerberos = "kerberos"
 )
 
 type Configuration struct {
 	Server   ServerConfig   `koanf:"server"`
 	OpenId   OpenIDConfig   `koanf:"openid"`
+	Kerberos KerberosConfig `koanf:"kerberos"`
 	Caps     RDGCapsConfig  `koanf:"caps"`
 	Security SecurityConfig `koanf:"security"`
 	Client   ClientConfig   `koanf:"client"`
@@ -50,6 +52,11 @@ type ServerConfig struct {
 	AuthSocket           string   `koanf:"authsocket"`
 }
 
+type KerberosConfig struct {
+	Keytab   string `koanf:"keytab"`
+	Krb5Conf string `koanf:"krb5conf"`
+}
+
 type OpenIDConfig struct {
 	ProviderUrl  string `koanf:"providerurl"`
 	ClientId     string `koanf:"clientid"`
@@ -161,6 +168,7 @@ func Load(configFile string) Configuration {
 	k.UnmarshalWithConf("Caps", &Conf.Caps, koanfTag)
 	k.UnmarshalWithConf("Security", &Conf.Security, koanfTag)
 	k.UnmarshalWithConf("Client", &Conf.Client, koanfTag)
+	k.UnmarshalWithConf("Kerberos", &Conf.Kerberos, koanfTag)
 
 	if len(Conf.Security.PAATokenEncryptionKey) != 32 {
 		Conf.Security.PAATokenEncryptionKey, _ = security.GenerateRandomString(32)
@@ -206,6 +214,10 @@ func Load(configFile string) Configuration {
 		log.Fatalf("openid is configured but tokenauth disabled")
 	}
 
+	if Conf.Server.Authentication == AuthenticationKerberos && Conf.Kerberos.Keytab == "" {
+		log.Fatalf("kerberos is configured but no keytab was specified")
+	}
+
 	// prepend '//' if required for URL parsing
 	if !strings.Contains(Conf.Server.GatewayAddress, "//") {
 		Conf.Server.GatewayAddress = "//" + Conf.Server.GatewayAddress
diff --git a/cmd/rdpgw/kdcproxy/proxy.go b/cmd/rdpgw/kdcproxy/proxy.go
new file mode 100644
index 0000000..86e796e
--- /dev/null
+++ b/cmd/rdpgw/kdcproxy/proxy.go
@@ -0,0 +1,157 @@
+package kdcproxy
+
+import (
+	"fmt"
+	krbconfig "github.com/bolkedebruin/gokrb5/v8/config"
+	"github.com/jcmturner/gofork/encoding/asn1"
+	"io"
+	"log"
+	"net"
+	"net/http"
+	"time"
+)
+
+const (
+	maxLength        = 128 * 1024
+	systemConfigPath = "/etc/krb5.conf"
+	timeout          = 5 * time.Second
+)
+
+type KdcProxyMsg struct {
+	Message []byte `asn1:"tag:0,explicit"`
+	Realm   string `asn1:"tag:1,optional"`
+	Flags   int    `asn1:"tag:2,optional"`
+}
+
+type KerberosProxy struct {
+	krb5Config *krbconfig.Config
+}
+
+func InitKdcProxy(krb5Conf string) KerberosProxy {
+	path := systemConfigPath
+	if krb5Conf != "" {
+		path = krb5Conf
+	}
+	cfg, err := krbconfig.Load(path)
+	if err != nil {
+		log.Fatalf("Cannot load krb5 config %s due to %s", path, err)
+	}
+
+	return KerberosProxy{
+		krb5Config: cfg,
+	}
+}
+
+func (k KerberosProxy) Handler(w http.ResponseWriter, r *http.Request) {
+	if r.Method != "POST" {
+		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+		return
+	}
+
+	length := r.ContentLength
+	if length == -1 {
+		http.Error(w, "Content length required", http.StatusLengthRequired)
+		return
+	}
+
+	if length > maxLength {
+		http.Error(w, "Request entity too large", http.StatusRequestEntityTooLarge)
+		return
+	}
+
+	data := make([]byte, length)
+	_, err := io.ReadFull(r.Body, data)
+	if err != nil {
+		log.Printf("Error reading from stream: %s", err)
+		http.Error(w, "Error reading from stream", http.StatusInternalServerError)
+		return
+	}
+
+	msg, err := decode(data)
+	if err != nil {
+		log.Printf("Cannot unmarshal: %s", err)
+		http.Error(w, "Invalid request", http.StatusBadRequest)
+		return
+	}
+
+	krb5resp, err := k.forward(msg.Realm, msg.Message)
+	if err != nil {
+		log.Printf("cannot forward to kdc due to %s", err)
+		http.Error(w, "Service unavailable", http.StatusServiceUnavailable)
+		return
+	}
+
+	reply, err := encode(krb5resp)
+	if err != nil {
+		log.Printf("unable to encode krb5 message due to %s", err)
+		http.Error(w, "encoding error", http.StatusInternalServerError)
+	}
+
+	w.Header().Set("Content-Type", "application/kerberos")
+	w.Write(reply)
+}
+
+func (k *KerberosProxy) forward(realm string, data []byte) (resp []byte, err error) {
+	if realm == "" {
+		realm = k.krb5Config.LibDefaults.DefaultRealm
+	}
+
+	// load udp first as is the default for kerberos
+	c, kdcs, err := k.krb5Config.GetKDCs(realm, false)
+	if err != nil || c < 1 {
+		return nil, fmt.Errorf("cannot get kdc for realm %s due to %s", realm, err)
+	}
+
+	for i := range kdcs {
+		conn, err := net.Dial("tcp", kdcs[i])
+		if err != nil {
+			log.Printf("error connecting to %s due to %s, trying next if available", kdcs[i], err)
+			continue
+		}
+		conn.SetDeadline(time.Now().Add(timeout))
+
+		_, err = conn.Write(data)
+		if err != nil {
+			log.Printf("cannot write packet data to %s due to %s, trying next if available", kdcs[i], err)
+			conn.Close()
+			continue
+		}
+
+		// todo check header
+		resp, err = io.ReadAll(conn)
+		if err != nil {
+			log.Printf("error reading from kdc %s due to %s, trying next if available", kdcs[i], err)
+			conn.Close()
+			continue
+		}
+		conn.Close()
+
+		return resp, nil
+	}
+
+	return nil, fmt.Errorf("no kdcs found for realm %s", realm)
+}
+
+func decode(data []byte) (msg *KdcProxyMsg, err error) {
+	var m KdcProxyMsg
+	rest, err := asn1.Unmarshal(data, &m)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(rest) > 0 {
+		return nil, fmt.Errorf("trailing data in request")
+	}
+
+	return &m, nil
+}
+
+func encode(krb5data []byte) (r []byte, err error) {
+	m := KdcProxyMsg{Message: krb5data}
+	enc, err := asn1.Marshal(m)
+	if err != nil {
+		log.Printf("cannot marshal due to %s", err)
+		return nil, err
+	}
+	return enc, nil
+}
diff --git a/cmd/rdpgw/main.go b/cmd/rdpgw/main.go
index 4ab1d75..6d91b79 100644
--- a/cmd/rdpgw/main.go
+++ b/cmd/rdpgw/main.go
@@ -4,8 +4,12 @@ import (
 	"context"
 	"crypto/tls"
 	"fmt"
+	"github.com/bolkedebruin/gokrb5/v8/keytab"
+	"github.com/bolkedebruin/gokrb5/v8/service"
+	"github.com/bolkedebruin/gokrb5/v8/spnego"
 	"github.com/bolkedebruin/rdpgw/cmd/rdpgw/common"
 	"github.com/bolkedebruin/rdpgw/cmd/rdpgw/config"
+	"github.com/bolkedebruin/rdpgw/cmd/rdpgw/kdcproxy"
 	"github.com/bolkedebruin/rdpgw/cmd/rdpgw/protocol"
 	"github.com/bolkedebruin/rdpgw/cmd/rdpgw/security"
 	"github.com/bolkedebruin/rdpgw/cmd/rdpgw/web"
@@ -204,6 +208,19 @@ func main() {
 	if conf.Server.Authentication == config.AuthenticationBasic {
 		h := web.BasicAuthHandler{SocketAddress: conf.Server.AuthSocket}
 		http.Handle("/remoteDesktopGateway/", common.EnrichContext(h.BasicAuth(gw.HandleGatewayProtocol)))
+	} else if conf.Server.Authentication == config.AuthenticationKerberos {
+		keytab, err := keytab.Load(conf.Kerberos.Keytab)
+		if err != nil {
+			log.Fatalf("Cannot load keytab: %s", err)
+		}
+		http.Handle("/remoteDesktopGateway/", common.EnrichContext(
+			spnego.SPNEGOKRB5Authenticate(
+				http.HandlerFunc(gw.HandleGatewayProtocol),
+				keytab,
+				service.Logger(log.Default()))),
+		)
+		k := kdcproxy.InitKdcProxy(conf.Kerberos.Krb5Conf)
+		http.HandleFunc("/KdcProxy", k.Handler)
 	} else {
 		// openid
 		oidc := initOIDC(url, store)
diff --git a/cmd/rdpgw/protocol/gateway.go b/cmd/rdpgw/protocol/gateway.go
index 4e17dc5..2a4646b 100644
--- a/cmd/rdpgw/protocol/gateway.go
+++ b/cmd/rdpgw/protocol/gateway.go
@@ -68,9 +68,9 @@ func (g *Gateway) HandleGatewayProtocol(w http.ResponseWriter, r *http.Request)
 		t = &Tunnel{
 			RDGId:      connId,
 			RemoteAddr: ctx.Value(common.ClientIPCtx).(string),
-			UserName:   ctx.Value(common.UsernameCtx).(string),
 		}
-		// username can be nil with openid as it's only available later
+		// username can be nil with openid & kerberos as it's only available later
+		// todo grab kerberos principal now?
 		username := ctx.Value(common.UsernameCtx)
 		if username != nil {
 			t.UserName = username.(string)
diff --git a/go.mod b/go.mod
index 5998bd2..ad14f4b 100644
--- a/go.mod
+++ b/go.mod
@@ -3,18 +3,20 @@ module github.com/bolkedebruin/rdpgw
 go 1.19
 
 require (
+	github.com/bolkedebruin/gokrb5/v8 v8.5.0
 	github.com/coreos/go-oidc/v3 v3.2.0
 	github.com/fatih/structs v1.1.0
 	github.com/go-jose/go-jose/v3 v3.0.0
 	github.com/google/uuid v1.1.2
 	github.com/gorilla/sessions v1.2.1
 	github.com/gorilla/websocket v1.5.0
+	github.com/jcmturner/gofork v1.7.6
 	github.com/knadh/koanf v1.4.2
 	github.com/msteinert/pam v1.0.0
 	github.com/patrickmn/go-cache v2.1.0+incompatible
 	github.com/prometheus/client_golang v1.12.1
 	github.com/thought-machine/go-flags v1.6.1
-	golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
+	golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
 	golang.org/x/oauth2 v0.0.0-20220722155238-128564f6959c
 	google.golang.org/grpc v1.49.0
 	google.golang.org/protobuf v1.28.1
@@ -26,6 +28,11 @@ require (
 	github.com/fsnotify/fsnotify v1.5.4 // indirect
 	github.com/golang/protobuf v1.5.2 // indirect
 	github.com/gorilla/securecookie v1.1.1 // indirect
+	github.com/hashicorp/go-uuid v1.0.3 // indirect
+	github.com/jcmturner/aescts/v2 v2.0.0 // indirect
+	github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
+	github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
+	github.com/jcmturner/rpc/v2 v2.0.3 // indirect
 	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -34,12 +41,11 @@ require (
 	github.com/prometheus/client_model v0.2.0 // indirect
 	github.com/prometheus/common v0.32.1 // indirect
 	github.com/prometheus/procfs v0.7.3 // indirect
-	github.com/stretchr/testify v1.7.1 // indirect
-	golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect
-	golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
+	golang.org/x/net v0.0.0-20220725212005-46097bf591d3 // indirect
+	golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect
 	golang.org/x/text v0.3.7 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
 	google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 // indirect
 	gopkg.in/square/go-jose.v2 v2.6.0 // indirect
-	gopkg.in/yaml.v3 v3.0.0 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
-- 
GitLab