Forum Discussion

DethMetal_Jeff's avatar
DethMetal_Jeff
Icon for Altostratus rankAltostratus
Aug 04, 2020
Solved

Pass client cert based on POST data

I'm trying to pass along cleint cert information to my backedn server in an http header based on HTTP POST data sent to the login servlet. I've written the below iRule which, in short does the following:

  1. checks for a POST to the login servlet and collects the data for that post
  2. parses the data and grabs the username out of it and then makes a callout to see if this user requires a cert or not
  3. if the user requires a cert, renegotiate the ssl session and require a cert
  4. grab the cert, store it in a variable
  5. send the http message to the backend server enriched with the cert header

I think should work but it appears to fall over after step 3. I looks as though there's an implicit HTTP::release at the end of HTTP_REQUEST_DATA. What I want to happen is for the release to not occur until we have the cert but according to my logs, it happens after the call to SSL::renegotiate and before the CLIENTSSL_CLIENTCERT event. Anyone have any ideas on what I can do to hold the HTTP request until I have the cert?

when RULE_INIT {
  set static::int2_hsl_publisher "/Common/int2-hsl-publisher"
}

when CLIENT_ACCEPTED {
  set login_processed 0
  set default_pool [LB::server pool]
  set hsl [HSL::open -publisher $static::int2_hsl_publisher]
}

when CLIENTSSL_CLIENTCERT {
  #log local0. "ssl handshake"
  if { [SSL::cert count] > 0 } {
    log local0. "Got a cert"
    set client_cert [string map -nocase {"\n" " "} [X509::whole [SSL::cert 0]]]
    #session add ssl [SSL::sessionid] [string map -nocase {"\n" " "} [X509::whole [SSL::cert 0]]]
  }
  catch { HTTP::release }
}

when HTTP_REQUEST {
  set method [HTTP::method]
  set uri [HTTP::uri]
  set host [HTTP::host]
  set request [HTTP::request]
  if { [HTTP::host] ends_with "webtrader.eexchange.com" || [HTTP::host] ends_with "webtrader.fxdnld.com"  } {
    if { not ([HTTP::path] starts_with "/socket/") && not ([HTTP::path] starts_with "/x2") } {
      log local0. "modify path [HTTP::path]"
      set path "/x2[HTTP::path]"
      HTTP::path $path
      # log local0. "added /x2/ to [HTTP::uri]"
    }
    log local0. "added /x2/ to [HTTP::path]"
  }
  if { ( [HTTP::uri] starts_with "/x2" ) || \
  ( [HTTP::uri] starts_with "/socket/x2" ) } {
    if { (  [HTTP::path] equals "/x2/service/login"  ) && \
    ( $login_processed == 0 ) && \
    ( [HTTP::method] equals "POST" ) } {
      # collect login data and (maybe) require a cert
      HTTP::collect [HTTP::header Content-Length]
    }
    
    set vsname [virtual name]
    regexp {^/COMMON/([A-Z0-9]+)} [string toupper [virtual name]] -> stack
    if { [ catch { pool  "$stack-pool-webtrader-static-7026" } ] } {
      HTTP::respond 200 content "X2 has not been configured on $stack"
      return
    }
    set ws1pool $stack-pool-x2-ws1-7027
    set ws2pool $stack-pool-x2-ws2-7028
    set staticpool $stack-pool-x2-static-7026
    
    switch -glob [HTTP::path] {
      "/socket/x2/default/*" {
        SSL::disable serverside
        # log local0. "$vsname -> $stack-pool-x2-ws1-7027"
        pool $ws1pool
      }
      "/socket/x2/marketdata/*" {
        SSL::disable serverside
        # log local0. "$vsname -> $stack-pool-x2-ws2-7028"
        pool $ws2pool
      }
      "/x2/container/*" {
        # log local0. "$vsname -> $default_pool"
        pool $default_pool
      }
      "/x2/*" {
        SSL::disable serverside
        # log local0. "$vsname -> $stack-pool-x2-static-7026"
        pool $staticpool
      }
    }
  }
}

when HTTP_REQUEST_SEND {
  clientside {
    log local0. "inside request send, [HTTP::path]"
    #set id [SSL::sessionid]
    #set thecert [session lookup ssl $id]
      if { [info exists client_cert] && ( $client_cert ne "" ) && ( [HTTP::path] equals "/x2/service/login" )  } {
        log local0. "cert -> $client_cert ->  [HTTP::path]"
        # log local0. "setting cert header to [HTTP::path] $client_cert"
        HTTP::header insert "X-Client-Cert" $client_cert
      }
    HTTP::header insert X-Forwarded-For [IP::remote_addr]
  }
}

when HTTP_REQUEST_DATA {
  if { (  [HTTP::path] equals "/x2/service/login"  ) && \
  ( $login_processed == 0 ) && \
  ( [HTTP::method] equals "POST" ) } {
    set login_processed 1
    set vsname [virtual name]
    set payload [HTTP::payload]
    set userId ""
    regexp {^/COMMON/([A-Z0-9]+)} [string toupper [virtual name]] -> stack
    set userId [call cxLib::getPostVar $payload "userId"]
    # does the user need a cert? non-zero return from check results in prompt
    if { [call cxLib::checkRequiresCert $stack $userId] } {
      log local0. "$userId requires client cert"
      SSL::session invalidate
      session delete ssl [SSL::sessionid]
      SSL::authenticate always
      SSL::authenticate depth 9
      SSL::cert mode require
      SSL::renegotiate
      } else {
        log local0. "$userId does not require client cert"
        HTTP::release
      }
    }
  }
 
  when HTTP_RESPONSE {
    set now [clock format [clock seconds] -format "%d/%b/%Y:%H:%M:%S %z"]
    set contentlength [HTTP::header "Content-Length"]
    if {[HTTP::is_redirect]} {  
      # log local0. "rewriting Location header [string map {"/http:/" "/https:/"} [HTTP::header Location]]"
      HTTP::header replace Location [string map {"http:" "https:"} [HTTP::header Location]]
      if { [HTTP::header Location] contains "webtrader.eexchange.com" || [HTTP::header Location] contains "webtrader.fxdnld.com"  } {
        if { [HTTP::header Location] contains "com/x2/" } {
          HTTP::header replace Location [string map {"/x2/" "/"} [HTTP::header Location]]
        }
      }
    }
    if { $uri starts_with "/x2/" } {
      HSL::send $hsl "<190>x2-access: [IP::local_addr] [IP::client_addr] $host - \[$now\] \"$method $uri HTTP/[HTTP::version]\" [HTTP::status] $contentlength\n"
    }
  }
  • Just to update anyone else who is looking to do the same thing. I inserted an HTTP::collect at line 111 after the SSL::renegotiate and this appears to force the iRule to hold the http data instead of invoking the implict release at the end of HTTP_REQUEST_DATA. So now it hits the CLIENTSSL_CLIENTCERT event and the HTTP::release is called there. Since I'm setting SSL::cert mode require this should always result in either the handshake failing or the HTTP::release being called from CLIENTSSL_CERT.

     

3 Replies

  • Why not set the client SSL profile to always request a cert (not require), and then you would already have it to hand for those cases where it's needed, without forcing a renegotiation?

  • That's what I've been doing but apparently that's less than desirable behavior from a client/product perspective. We have scenarios where a user may have a valid certificate installed but isn't required to use it for this particular application. If i set it to request they'll get prompted to select a cert regardless of which app they're trying to use. So, while technically ok as in it works, the end user experience isn't desirable.

     

  • Just to update anyone else who is looking to do the same thing. I inserted an HTTP::collect at line 111 after the SSL::renegotiate and this appears to force the iRule to hold the http data instead of invoking the implict release at the end of HTTP_REQUEST_DATA. So now it hits the CLIENTSSL_CLIENTCERT event and the HTTP::release is called there. Since I'm setting SSL::cert mode require this should always result in either the handshake failing or the HTTP::release being called from CLIENTSSL_CERT.