From ca7e6d03323934c7ef618c4c1ddadc5ec002c260 Mon Sep 17 00:00:00 2001 From: Simon Eisenmann Date: Mon, 5 May 2014 16:43:05 +0200 Subject: [PATCH] Implemented certificate pass through from SSL frontend proxy via HTTP headers. --- server.conf.in | 22 +++-- src/app/spreed-speakfreely-server/users.go | 93 +++++++++++++++++----- 2 files changed, 84 insertions(+), 31 deletions(-) diff --git a/server.conf.in b/server.conf.in index f5ad0862..36ceb25e 100644 --- a/server.conf.in +++ b/server.conf.in @@ -110,7 +110,7 @@ enabled = false ; REST requests. In mode httpheader, allowRegistration should be false. ; certificate: ; The userid is provided as CommonName with a certificate provided with TLS -; client authentication. When you use this with a front end proxy for TLS +; client authentication. When you use this with a front end proxy for TLS ; termination, that proxy has to validate the certificate and inject certain ; headers into the proxy connection. Furthermode in certificate mode the ; server can act as a CA to sign incoming user requests with a private key @@ -125,31 +125,29 @@ enabled = false ; mode. Keep this commented if you do not want the server to sign certificate ; requests. ;certificate_key = userskey.key -; Full path to PEM encoded certificate to use for user validation in +; Full path to PEM encoded certificate to use for user validation in ; "certificate" mode. When allowRegistration is true and certificate_key is also ; set then the server will act as a CA and sign incoming user registrations and ; return certificates to users as registration. ;certificate_certificate = usersca.crt -; The HTTP header name where to find if the TLS client authentication was -; successfull. The value of this header is matched to -; certificate_verifiedHeaderValue and only if there is a match, the proxy +; The HTTP header name where to find if the TLS client authentication was +; successfull. The value of this header is matched to +; certificate_verifiedHeaderValue and only if there is a match, the proxy ; handled TLS client authentication is accepted as success. Make sure to secure ; these headers with your front end proxy (always set them). Do not use these ; settings when not using a front end proxy. ;certificate_verifiedHeader = x-verified ;certificate_verifiedHeaderValue = SUCCESS -; The HTTP header name where to find the userid extracted by a proxy after -; TLS client authentication. If not set certificate_certificateHeader will be -; used to retrieve the userid from the CommonName field of the certificate. -;certificate_useridHeader = x-userid ; The HTTP header name where to find the PEM encoded certificate authenticated -; by a front end proxy. This configuration this is optional if you have set a -; certificate_useridHeader value and the front end TLS proxy does inject the -; userid into that header. +; by a front end proxy. With Nginx the required value is in $ssl_client_cert. ;certificate_certificateHeader = x-certificate ; The valid duration of generated certificates created in certificate mode when ; allowRegistration is enabled. ;certificate_validForDays = 365 +; Organization to set into the created user certificates. Use a readable public +; name to make the certificate easily recognizable as certificate for your +; server so users can choose the correct certificate when prompted. +;certificate_organization= = My Spreed Server ; If enabled the server can create new userids. Set allowRegistration to true to ; enable userid creation/registration. Users are created according the settings ; of the currently configured mode (see above). diff --git a/src/app/spreed-speakfreely-server/users.go b/src/app/spreed-speakfreely-server/users.go index 0af95855..5ed00dda 100644 --- a/src/app/spreed-speakfreely-server/users.go +++ b/src/app/spreed-speakfreely-server/users.go @@ -31,6 +31,7 @@ import ( "crypto/x509/pkix" "encoding/base64" "encoding/json" + "encoding/pem" "errors" "fmt" "github.com/longsleep/pkac" @@ -128,9 +129,13 @@ func (uh *UsersHTTPHeaderHandler) Create(un *UserNonce, request *http.Request) ( } type UsersCertificateHandler struct { - validFor time.Duration - privateKey crypto.PrivateKey - certificate *x509.Certificate + validFor time.Duration + privateKey crypto.PrivateKey + certificate *x509.Certificate + verifiedHeader string + verifiedHeaderValue string + certificateHeader string + organization []string } func (uh *UsersCertificateHandler) makeTemplate(commonName string) (*x509.Certificate, error) { @@ -146,7 +151,8 @@ func (uh *UsersCertificateHandler) makeTemplate(commonName string) (*x509.Certif return &x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ - CommonName: commonName, + CommonName: commonName, + Organization: uh.organization, }, NotBefore: notBefore, NotAfter: notAfter, @@ -158,19 +164,52 @@ func (uh *UsersCertificateHandler) makeTemplate(commonName string) (*x509.Certif } func (uh *UsersCertificateHandler) Get(request *http.Request) (userid string, err error) { - - if request.TLS == nil || len(request.TLS.VerifiedChains) == 0 { - return - } - chain := request.TLS.VerifiedChains[0] - if len(chain) == 0 { - return + if uh.verifiedHeader != "" && uh.verifiedHeaderValue != "" { + // Use incoming HTTP headers. + if request.Header.Get(uh.verifiedHeader) != uh.verifiedHeaderValue { + // Verify header does not match - ignore incoming userid. + return + } + if uh.certificateHeader != "" { + // Read userid from certificate in header if configured. + var cert tls.Certificate + var certDERBlock *pem.Block + // Whuahah this is an evil fix to get back valid PEM data from Nginx $ssl_client_cert values. + certString := strings.Replace(request.Header.Get(uh.certificateHeader), " ", "\n", -1) + certString = strings.Replace(certString, "BEGIN\n", "BEGIN ", 1) + certString = strings.Replace(certString, "END\n", "END ", 1) + certPEMBlock := []byte(certString) + for { + certDERBlock, certPEMBlock = pem.Decode(certPEMBlock) + if certDERBlock == nil { + break + } + if certDERBlock.Type == "CERTIFICATE" { + cert.Certificate = append(cert.Certificate, certDERBlock.Bytes) + } + } + if len(cert.Certificate) == 0 { + err = errors.New("failed to parse certificate PEM data") + return + } + var certificates []*x509.Certificate + if certificates, err = x509.ParseCertificates(cert.Certificate[0]); err == nil { + userid = certificates[0].Subject.CommonName + } + } + } else { + // Direct TLS termination and authentication. + if request.TLS == nil || len(request.TLS.VerifiedChains) == 0 { + return + } + chain := request.TLS.VerifiedChains[0] + if len(chain) == 0 { + return + } + cert := chain[0] + userid = cert.Subject.CommonName } - - cert := chain[0] - userid = cert.Subject.CommonName log.Printf("Client certificate found for user: %s\n", userid) - return } @@ -268,8 +307,12 @@ func NewUsers(hub *Hub, mode, realm string, runtime phoenix.Runtime) *Users { // Register handler Get at the hub. users.hub.useridRetriever = func(request *http.Request) (userid string, err error) { userid, err = handler.Get(request) - if userid != "" { - log.Printf("Users handler get success: %s\n", userid) + if err != nil { + log.Printf("Failed to get userid from handler: %s", err) + } else { + if userid != "" { + log.Printf("Users handler get success: %s\n", userid) + } } return } @@ -299,12 +342,24 @@ func (users *Users) createHandler(mode string, runtime phoenix.Runtime) (handler handler = &UsersHTTPHeaderHandler{headerName: headerName} case "certificate": var err2 error - uh := &UsersCertificateHandler{} + verifiedHeader, _ := runtime.GetString("users", "certificate_verifiedHeader") + verifiedHeaderValue, _ := runtime.GetString("users", "certificate_verifiedHeaderValue") + certificateHeader, _ := runtime.GetString("users", "certificate_certificateHeader") validForDays, _ := runtime.GetInt("users", "certificate_validForDays") if validForDays == 0 { validForDays = 365 } - uh.validFor = time.Duration(validForDays) * 24 * time.Hour + organization, _ := runtime.GetString("users", "certificate_organization") + if organization == "" { + organization = "My Spreed Server" + } + uh := &UsersCertificateHandler{ + verifiedHeader: verifiedHeader, + verifiedHeaderValue: verifiedHeaderValue, + certificateHeader: certificateHeader, + validFor: time.Duration(validForDays) * 24 * time.Hour, + organization: []string{organization}, + } keyFn, _ := runtime.GetString("users", "certificate_key") certificateFn, _ := runtime.GetString("users", "certificate_certificate") if keyFn != "" && certificateFn != "" {