hmac
3 TopicsHigh Performance HMAC Cookie Signing
Problem this snippet solves: General Information The outlined iRule implements a Hash-based Message Authentication Code (see RFC 2104) cookie signing functionality, to provide an additional security layer for sensitive session cookie information. The iRule computes an HMAC verification token based on X509 certificate- or TCP-Connection information and a given session cookie and then passes the results as an additional HTTP cookie to the client. If the HMAC cookie is missing on subsequent requests, or a mismatch between the X509 certificate-, TCP-Connection- or session cookie information and the HMAC verification token is identified, then the given session cookie would become silently removed from the HTTP request to the backend system. Thus would then result in re-initialization and/or a re-authentication of the user session on the backend. Security benefits of HMAC cookie signing The provided HMAC based cookie signing mechanism has some very unique advantages. Providing tamper resistance to cookie information without utilizing CPU intensive per-request cookie decryption. Transparently binding of X509 certificate information to sensitive session cookies, so that ANY KNOWN FORM of cookie hijacking on smartcard authenticated connections can be securely mitigated. Even those between two legitimate smartcard users! Transparently binding of client IP address or F5 GeoLocation information to sensitive session cookies, so that an additional protection layer against cookie hijacking or general misuse can be provided. Performance consideration The iRule is highly performance optimized to provide an as fast as possible request throughput. The iRule utilizes a RAM cache for previously calculated HMAC tokens, to shrink the required CPU cycles for the per-request cookie verification to an absolute minimum. The RAM caches are based on run-time modified $static::variable(arrays) to avoid any form of cross TMM communications and TMM connection parking situations like the [table] command would do, while still being fully CMP-compliant. The RAM caches are built and maintained on each TMM core independently and do support a RULE_INIT driven and also configurable garbage collection interval using the [after -periodic] syntax and a maximum cache size limiter to optimize and protect the memory/health of the plattform. When a garbage collection occurs, then all previously cached HMAC tokens are getting flushed as a whole and recalculated on demand without interrupting the active sessions. The performance test used to measure and optimize the performance of the iRule was built on a two core LTM-1600 unit, with synthetically pre-filled RAM caches containing 100.000 unique HMAC tokens on each TMM core. The cookies where based on random ASP.NET session ids in combination with X509 CNAME information and resulted to a memory footprint of roughly 17,5 Mbyte per TMM core. The test scenario contains a single user session requesting consecutive 25 page impressions on a website with 40 web objects (1000 requests) using four independent HTTP keep-alive connections. -------------------------------------------- Ltm::Rule Event: iRule_2_Delete:HTTP_REQUEST -------------------------------------------- Priority 500 Executions Total 1.0K Failures 0 Aborts 0 CPU Cycles on Executing Average 67.6K Maximum 189.4K Minimum 27.2K --------------------------------------------- Ltm::Rule Event: iRule_2_Delete:HTTP_RESPONSE --------------------------------------------- Priority 500 Executions Total 1.0K Failures 0 Aborts 0 CPU Cycles on Executing Average 29.9K Maximum 212.6K Minimum 19.4K Note: The rather high maximum CPU values are cause by a single Set-Cookie HMAC calculation in the HTTP_RESPONSE event, and an additional HMAC calculation on the second TMM core on its first HTTP_REQUEST event. Based on the differences of the Average and Maximum CPU cycles needed for the different code paths of the HMAC verification, the offloading factor of the RAM cache can be specified at round about 65% saved CPU cycles per HTTP_REQUEST event. Credits Special thanks goes to the DevCentral member Devon Twesten from "Booz Allen Hamilton", which came up with the idea of HMAC signing session cookies based on X509 certificate information as an additional protection layer. You may read his original posting here. Cheers, Kai How to use this snippet: Usage: Create a new iRule and copy/paste the provided iRule into it. Identify your session cookie name and modify the RULE_INIT section to reflect your environment and configure the desired cache garbage collection interval and maximum cache size limits as needed. Modify the $hmac_input variable creation examples as needed to include X509 information, Client IP address or F5 GeoLocation information. Attach the iRule to your Vitual Server. Test the iRule by trying to modify the HMAC or session cookie. Keep an eye on the provided HMAC cache garbage collection log entires and tweak the garbage collection interval as needed. Code : # # Deployment specific iRule events # # Note: Use just one of the outlined deployment specific iRule events. Combining them would be possible by changing [set hmac_input] to [append hmac_input], but it wouldn't make much sense to do so. when CLIENT_ACCEPTED { # log -noname local0.debug "HMAC: CLIENT_ACCEPTED: Setting client IP addr as HMAC token input." set hmac_input [IP::client_addr] } when CLIENT_ACCEPTED { # log -noname local0.debug "HMAC: CLIENT_ACCEPTED: Setting GeoLocation country data as HMAC token input." set hmac_input [whereis [IP::client_addr] country] } when CLIENTSSL_HANDSHAKE { # log -noname local0.debug "HMAC: CLIENTSSL_HANDSHAKE: Checking if SSL handshake used client certificate." if { ( [SSL::cert count] > 0 ) and ( [set x509_subject [X509::subject [SSL::cert 0]]] ne "" ) } then { # log -noname local0.debug "HMAC: CLIENTSSL_HANDSHAKE: Setting the X509 subject name as HMAC token input." if { [set hmac_input [X509::subject [SSL::cert 0]]] eq "" } then { # log -noname local0.debug "HMAC: CLIENTSSL_HANDSHAKE: The certificate does not contain a X509 subject name. Rejecting the session..." reject } } else { # log -noname local0.debug "HMAC: CLIENTSSL_HANDSHAKE: The SSL handshake does not used any client certificate. Rejecting the session..." reject } } # # HMAC verification related iRule events # when RULE_INIT { # Modify the settings below to reflect your application and insert a strong enougth HMAC signing key. set static::hmac_signing_key "AzaZ5678901234567XYZ34567XYZ" ;# The HMAC256 signing key should be ideally a 256bit random key set static::session_cookie "ASP.NET_SessionId";# Name of the Session Cookie set static::hmac_cookie "ASP.NET_SessionId_HMAC";# Name of the HMAC Cookie set static::hmac_cache_maxsize 100000;# Maximum number of HMAC cache entries per TMM core # Initialize the HMAC cache array unset -nocomplain static::hmac_cache set static::hmac_cache(count) 0 # Tweak the -periodic interval of the HMAC garbage collection as needed (3600000 msec = 1hour, 86400000 msec = 1day) after 86400000 -periodic { if { [array exists static::hmac_cache] } then { log -noname local0.debug "HMAC: Cache Array Recycling: Clearing the HMAC cache array on TMM [TMM::cmp_unit] with currently $static::hmac_cache(count) entries" unset -nocomplain static::hmac_cache set static::hmac_cache(count) 0 } } } when HTTP_REQUEST { if { [catch { # log -noname local0.debug "HMAC: HTTP_REQUEST: Query the HMAC cache array on TMM[TMM::cmp_unit] for existence of pre-computed HMAC values." if { $static::hmac_cache([HTTP::cookie value $static::session_cookie]:$hmac_input) eq [HTTP::cookie value $static::hmac_cookie] } then { # log -noname local0.debug "HMAC: HTTP_REQUEST: HMAC cookie is verified using a cached HMAC token on TMM[TMM::cmp_unit]. Allowing the session cookie \"$static::session_cookie\" to pass." } else { # log -noname local0.debug "HMAC: HTTP_REQUEST: HMAC cookie is NOT verified using a cached HMAC token on TMM[TMM::cmp_unit]. Removing session cookie \"$static::session_cookie\" from current request." HTTP::cookie remove $static::session_cookie } }]} then { # log -noname local0.debug "HMAC: HTTP_REQUEST: The cache on TMM[TMM::cmp_unit] didn't contain a pre-computed HMAC value. Extracting received session_cookie and hmac_cookie values." if { ( [set session_cookie [HTTP::cookie value $static::session_cookie]] ne "" ) and ( [set hmac_cookie [HTTP::cookie value $static::hmac_cookie]] ne "" ) } then { # log -noname local0.debug "HMAC: HTTP_REQUEST: The request contains the app cookie \"$static::session_cookie\" = \"$session_cookie\" and HMAC cookie \"$static::hmac_cookie\" = \"$hmac_cookie\"." # log -noname local0.debug "HMAC: HTTP_REQUEST: Compute a fresh HMAC token for message \"$session_cookie:$hmac_input\" using Key \"$static::hmac_signing_key\"." set hmac_token [b64encode [CRYPTO::sign -alg hmac-sha256 -key $static::hmac_signing_key "$session_cookie:$hmac_input"]] # log -noname local0.debug "HMAC: HTTP_REQUEST: Compare the computed HMAC token \"$hmac_token\" with received HMAC cookie \"$static::hmac_cookie\" = \"$hmac_cookie\"." if { $hmac_token eq $hmac_cookie } then { # log -noname local0.debug "HMAC: HTTP_REQUEST: The HMAC cookie $static::hmac_cookie is verified using the computed HMAC token. Allowing the session cookie \"$static::session_cookie\" to pass." if { [incr static::hmac_cache(count)] > $static::hmac_cache_maxsize } then { # log -noname local0.debug "HMAC: HTTP_REQUEST: The HMAC cache has reached the maximum size of $static::hmac_cache_maxsize. Clearing the HMAC cache on TMM [TMM::cmp_unit]." unset -nocomplain static::hmac_cache set static::hmac_cache(count) 1 } # log -noname local0.debug "HMAC: HTTP_REQUEST: Storing the computed HMAC token \"$hmac_token\" into the HMAC cache array." set static::hmac_cache($session_cookie:$hmac_input) $hmac_token } else { # log -noname local0.debug "HMAC: HTTP_REQUEST: The HMAC cookie $static::hmac_cookie is NOT verified using the computed HMAC token. Removing session cookie \"$static::session_cookie\" from current request." HTTP::cookie remove $static::session_cookie } } else { # log -noname local0.debug "HMAC: HTTP_REQUEST: Current request didn't contain session_cookie and/or hmac_cookie values. Removing session cookie \"$static::session_cookie\" from current request." HTTP::cookie remove $static::session_cookie } } } when HTTP_RESPONSE { # log -noname local0.debug "HMAC: HTTP_RESPONSE: Checking if HTTP response sets the app cookie \"$static::session_cookie\"." if { [set session_cookie [HTTP::cookie value $static::session_cookie]] ne "" } then { # log -noname local0.debug "HMAC: HTTP_RESPONSE: The response contains the app cookie \"$static::session_cookie\" = \"$session_cookie\"." if { [catch { # log -noname local0.debug "HMAC: HTTP_RESPONSE: Inserting cached HMAC cookie \"$static::hmac_cookie\" = \"$static::hmac_cache($session_cookie:$hmac_input)\" to the response." HTTP::header insert "Set-Cookie" "$static::hmac_cookie=$static::hmac_cache($session_cookie:$hmac_input); HttpOnly; Secure; Path=/" }]} then { # log -noname local0.debug "HMAC: HTTP_RESPONSE: The cache on TMM[TMM::cmp_unit] didn't contain a pre-computed HMAC value." if { [incr static::hmac_cache(count)] > $static::hmac_cache_maxsize } then { # log -noname local0.debug "HMAC: HTTP_RESPONSE: The HMAC cache has reached the maximum size of $static::hmac_cache_maxsize. Clearing the HMAC cache on TMM [TMM::cmp_unit]." unset -nocomplain static::hmac_cache set static::hmac_cache(count) 1 } # log -noname local0.debug "HMAC: HTTP_RESPONSE: Compute a fresh HMAC token for message \"$session_cookie:$hmac_input\" using Key \"$static::hmac_signing_key\". HTTP::header insert "Set-Cookie" "$static::hmac_cookie=[set static::hmac_cache($session_cookie:$hmac_input) [b64encode [CRYPTO::sign -alg hmac-sha256 -key $static::hmac_signing_key "$session_cookie:$hmac_input"]]]; HttpOnly; Secure; Path=/" # log -noname local0.debug "HMAC: HTTP_RESPONSE: Inserting computed HMAC cookie \"$static::hmac_cookie\" = \"$static::hmac_cache($session_cookie:$hmac_input)\" to the response and storing the computed HMAC token into the HMAC cache array." } } } Tested this on version: 12.01.1KViews0likes0CommentsHigh Performance HMAC Cookie Signing (for v10.x)
Problem this snippet solves: General Information The outlined iRule implements a Hash-based Message Authentication Code (see RFC 2104) cookie signing functionality, to provide an additional security layer for sensitive session cookie information. The iRule computes an HMAC verification token based on X509 certificate- or TCP-Connection information and a given session cookie and then passes the results as an additional HTTP cookie to the client. If the HMAC cookie is missing on subsequent requests, or a mismatch between the X509 certificate-, TCP-Connection- or session cookie information and the HMAC verification token is identified, then the given session cookie would become silently removed from the HTTP request to the backend system. Thus would then result in re-initialization and/or a re-authentication of the user session on the backend. Security benefits of HMAC cookie signing The provided HMAC based cookie signing mechanism has some very unique advantages. Providing tamper resistance to cookie information without utilizing CPU intensive per-request cookie decryption. Transparently binding of X509 certificate information to sensitive session cookies, so that ANY KNOWN FORM of cookie hijacking on smartcard authenticated connections can be securely mitigated. Even those between two legitimate smartcard users! Transparently binding of client IP address or F5 GeoLocation information to sensitive session cookies, so that an additional protection layer against cookie hijacking or general misuse can be provided. Performance consideration The iRule is highly performance optimized to provide an as fast as possible request throughput. The iRule utilizes a RAM cache for previously calculated HMAC tokens, to shrink the required CPU cycles for the per-request cookie verification to an absolute minimum. The RAM caches are based on run-time modified $static::variable(arrays) to avoid any form of cross TMM communications and TMM connection parking situations like the [table] command would do, while still being fully CMP-compliant. The RAM caches are built and maintained on each TMM core independently and do support a RULE_INIT driven and also configurable garbage collection interval using the [after -periodic] syntax and a maximum cache size limiter to optimize and protect the memory/health of the plattform. When a garbage collection occurs, then all previously cached HMAC tokens are getting flushed as a whole and recalculated on demand without interrupting the active sessions. The performance test used to measure and optimize the performance of the iRule was built on a two core LTM-1600 unit, with synthetically pre-filled RAM caches containing 100.000 unique HMAC tokens on each TMM core. The cookies where based on random ASP.NET session ids in combination with X509 CNAME information and resulted to a memory footprint of roughly 17,5 Mbyte per TMM core. The test scenario contains a single user session requesting consecutive 25 page impressions on a website with 40 web objects (1000 requests) using four independent HTTP keep-alive connections. -------------------------------------------- Ltm::Rule Event: iRule_2_Delete:HTTP_REQUEST -------------------------------------------- Priority 500 Executions Total 1.0K Failures 0 Aborts 0 CPU Cycles on Executing Average 69.7K Maximum 486.0K Minimum 32.8K --------------------------------------------- Ltm::Rule Event: iRule_2_Delete:HTTP_RESPONSE --------------------------------------------- Priority 500 Executions Total 1.0K Failures 0 Aborts 0 CPU Cycles on Executing Average 30.6K Maximum 564.6K Minimum 17.7K Note: The rather high maximum CPU values are cause by a single Set-Cookie HMAC calculation in the HTTP_RESPONSE event, and an additional HMAC calculation on the second TMM core on its first HTTP_REQUEST event. Based on the differences of the Average and Maximum CPU cycles needed for the different HMAC verification code paths, the offloading factor of the RAM cache can be specified at round about 85% saved CPU cycles per HTTP_REQUEST event. Warning: TMOS version 10.x don't have a specialized iRule command to compute HMAC tokens. The used HMAC token generator of this iRule is purely written in TCL and provided by F5 as a workaround for v10.x platforms. The performance of this TCL code is somewhat slower than the on-purpose HMAC token generator which is available in v11.x and later platforms. Although the RAM caches are doing a really great job to offload the fresh HMAC generation to a great extent , the interval based garbage collection of the RAM cache or certain flooding attacks may still cause some CPU spikes. Credits Special thanks goes to the DevCentral member Devon Twesten from "Booz Allen Hamilton", which came up with the idea of HMAC signing session cookies based on X509 certificate information as an additional protection layer. You may read his original posting here. Cheers, Kai How to use this snippet: Usage: Create a new iRule and copy/paste the provided iRule into it. Identify your session cookie name and modify the RULE_INIT section to reflect your environment and configure the desired cache garbage collection interval and maximum cache size limits as needed. Modify the $hmac_input variable creation examples as needed to include X509 information, Client IP address or F5 GeoLocation information. Attach the iRule to your Vitual Server. Test the iRule by trying to modify the HMAC or session cookie. Keep an eye on the provided HMAC cache garbage collection log entires and tweak the garbage collection interval as needed. Code : # # Deployment specific iRule events # # Note: Use just one of the outlined deployment specific iRule events. Combining them would be possible by changing [set hmac_input] to [append hmac_input], but it wouldn't make much sense to do so. when CLIENT_ACCEPTED { # log -noname local0.debug "HMAC: CLIENT_ACCEPTED: Setting client IP addr as HMAC token input." set hmac_input [IP::client_addr] } when CLIENT_ACCEPTED { # log -noname local0.debug "HMAC: CLIENT_ACCEPTED: Setting GeoLocation country data as HMAC token input." set hmac_input [whereis [IP::client_addr] country] } when CLIENTSSL_HANDSHAKE { # log -noname local0.debug "HMAC: CLIENTSSL_HANDSHAKE: Checking if SSL handshake used client certificate." if { ( [SSL::cert count] > 0 ) and ( [set x509_subject [X509::subject [SSL::cert 0]]] ne "" ) } then { # log -noname local0.debug "HMAC: CLIENTSSL_HANDSHAKE: Setting the X509 subject name as HMAC token input." if { [set hmac_input [X509::subject [SSL::cert 0]]] eq "" } then { # log -noname local0.debug "HMAC: CLIENTSSL_HANDSHAKE: The certificate does not contain a X509 subject name. Rejecting the session..." reject } } else { # log -noname local0.debug "HMAC: CLIENTSSL_HANDSHAKE: The SSL handshake does not used any client certificate. Rejecting the session..." reject } } # # HMAC verification related iRule events # when RULE_INIT { # Modify the settings below to reflect your application and insert a strong enougth HMAC signing key. set static::hmac_signing_key "AzaZ5678901234567XYZ34567XYZ" ;# The HMAC256 signing key should be ideally a 256bit random key set static::session_cookie "ASP.NET_SessionId";# Name of the Session Cookie set static::hmac_cookie "ASP.NET_SessionId_HMAC";# Name of the HMAC Cookie set static::hmac_cache_maxsize 100000;# Maximum number of HMAC cache entries per TMM core # Initialize the HMAC cache array unset -nocomplain static::hmac_cache set static::hmac_cache(count) 0 # Tweak the -periodic interval of the HMAC garbage collection as needed (3600000 msec = 1hour, 86400000 msec = 1day) after 86400000 -periodic { if { [array exists static::hmac_cache] } then { log -noname local0.debug "HMAC: Cache Array Recycling: Clearing the HMAC cache array on TMM [TMM::cmp_unit] with currently $static::hmac_cache(count) entries" unset -nocomplain static::hmac_cache set static::hmac_cache(count) 0 } } # TCL Macro to support HMAC token generation on v10.X plattforms (see https://devcentral.f5.com/s/articles/hmac) set static::genarate_hmac_token { set bsize 64 if { [string length $static::hmac_signing_key] > $bsize } { set key [sha256 $static::hmac_signing_key] } else { set key $static::hmac_signing_key } set ipad "" set opad "" for { set j 0 }{ $j < [string length $key] }{ incr j }{ binary scan $key @${j}H2 k set o [expr 0x$k ^ 0x5c] set i [expr 0x$k ^ 0x36] append ipad [format %c $i] append opad [format %c $o] } for { }{ $j < $bsize }{ incr j }{ append ipad 6 append opad \\ } set hmac_token [b64encode [sha256 $opad[sha256 "${ipad}$session_cookie:$hmac_input"]]] } } when HTTP_REQUEST { if { [catch { # log -noname local0.debug "HMAC: HTTP_REQUEST: Query the HMAC cache array on TMM[TMM::cmp_unit] for existence of pre-computed HMAC values." if { $static::hmac_cache([HTTP::cookie value $static::session_cookie]:$hmac_input) eq [HTTP::cookie value $static::hmac_cookie] } then { # log -noname local0.debug "HMAC: HTTP_REQUEST: HMAC cookie is verified using a cached HMAC token on TMM[TMM::cmp_unit]. Allowing the session cookie \"$static::session_cookie\" to pass." } else { # log -noname local0.debug "HMAC: HTTP_REQUEST: HMAC cookie is NOT verified using a cached HMAC token on TMM[TMM::cmp_unit]. Removing session cookie \"$static::session_cookie\" from current request." HTTP::cookie remove $static::session_cookie } }]} then { # log -noname local0.debug "HMAC: HTTP_REQUEST: The cache on TMM[TMM::cmp_unit] didn't contain a pre-computed HMAC value. Extracting received session_cookie and hmac_cookie values." if { ( [set session_cookie [HTTP::cookie value $static::session_cookie]] ne "" ) and ( [set hmac_cookie [HTTP::cookie value $static::hmac_cookie]] ne "" ) } then { # log -noname local0.debug "HMAC: HTTP_REQUEST: The request contains the app cookie \"$static::session_cookie\" = \"$session_cookie\" and HMAC cookie \"$static::hmac_cookie\" = \"$hmac_cookie\"." # log -noname local0.debug "HMAC: HTTP_REQUEST: Compute a fresh HMAC token for message \"$session_cookie:$hmac_input\" using Key \"$static::hmac_signing_key\"." eval $static::genarate_hmac_token # log -noname local0.debug "HMAC: HTTP_REQUEST: Compare the computed HMAC token \"$hmac_token\" with received HMAC cookie \"$static::hmac_cookie\" = \"$hmac_cookie\"." if { $hmac_token eq $hmac_cookie } then { # log -noname local0.debug "HMAC: HTTP_REQUEST: The HMAC cookie $static::hmac_cookie is verified using the computed HMAC token. Allowing the session cookie \"$static::session_cookie\" to pass." if { [incr static::hmac_cache(count)] > $static::hmac_cache_maxsize } then { log -noname local0.debug "HMAC: HTTP_REQUEST: The HMAC cache has reached the maximum size of $static::hmac_cache_maxsize. Clearing the HMAC cache on TMM [TMM::cmp_unit]." unset -nocomplain static::hmac_cache set static::hmac_cache(count) 1 } # log -noname local0.debug "HMAC: HTTP_REQUEST: Storing the computed HMAC token \"$hmac_token\" into the HMAC cache array." set static::hmac_cache($session_cookie:$hmac_input) $hmac_token } else { # log -noname local0.debug "HMAC: HTTP_REQUEST: The HMAC cookie $static::hmac_cookie is NOT verified using the computed HMAC token. Removing session cookie \"$static::session_cookie\" from current request." HTTP::cookie remove $static::session_cookie } } else { # log -noname local0.debug "HMAC: HTTP_REQUEST: Current request didn't contain session_cookie and/or hmac_cookie values. Removing session cookie \"$static::session_cookie\" from current request." HTTP::cookie remove $static::session_cookie } } } when HTTP_RESPONSE { # log -noname local0.debug "HMAC: HTTP_RESPONSE: Checking if HTTP response sets the app cookie \"$static::session_cookie\"." if { [set session_cookie [HTTP::cookie value $static::session_cookie]] ne "" } then { # log -noname local0.debug "HMAC: HTTP_RESPONSE: The response contains the app cookie \"$static::session_cookie\" = \"$session_cookie\"." if { [catch { HTTP::header insert "Set-Cookie" "$static::hmac_cookie=$static::hmac_cache($session_cookie:$hmac_input); HttpOnly; Secure; Path=/" # log -noname local0.debug "HMAC: HTTP_RESPONSE: Inserting cached HMAC cookie \"$static::hmac_cookie\" = \"$static::hmac_cache($session_cookie:$hmac_input)\" to the response." }]} then { # log -noname local0.debug "HMAC: HTTP_RESPONSE: The cache on TMM[TMM::cmp_unit] didn't contain a pre-computed HMAC value." if { [incr static::hmac_cache(count)] > $static::hmac_cache_maxsize } then { log -noname local0.debug "HMAC: HTTP_RESPONSE: The HMAC cache has reached the maximum size of $static::hmac_cache_maxsize. Clearing the HMAC cache on TMM [TMM::cmp_unit]." unset -nocomplain static::hmac_cache set static::hmac_cache(count) 1 } # log -noname local0.debug "HMAC: HTTP_RESPONSE: Compute a fresh HMAC token for message \"$session_cookie:$hmac_input\" using Key \"$static::hmac_signing_key\"." eval $static::genarate_hmac_token # log -noname local0.debug "HMAC: HTTP_RESPONSE: Storing the computed HMAC token \"$hmac_token\" into the HMAC cache array." set static::hmac_cache($session_cookie:$hmac_input) $hmac_token # log -noname local0.debug "HMAC: HTTP_RESPONSE: Inserting computed HMAC cookie \"$static::hmac_cookie\" = \"$static::hmac_cache($session_cookie:$hmac_input)\" to the response." HTTP::header insert "Set-Cookie" "$static::hmac_cookie=$hmac_token; HttpOnly; Secure; Path=/" } } } Tested this on version: 12.0698Views0likes0CommentsGoogle Authenticator Verification iRule (TMOS v11.1+ optimized)
Problem this snippet solves: Hi Folks, the provided iRule contains a TMOS v11.1+ compatible fork of the already existing and very lovely Google Authenticator verification iRules here on CodeShare. The iRule adds support for the full TOTP algorithm standard (see RFC 6238 as well as RFC 4226) by including a support for longer shared key sizes and the more complex HMAC-SHA256 as well as HMAC-SHA512 algorithms. In addition, the core functionality of the provided iRule is a complete revamp and contains lot of performance optimizations to reduce the required CPU cycles on TMOS v11.1+ platforms by a great degree. The performance optimizations of this iRule are achieved by: Using the TMOS v11.1+ compatible [CRYPTO::sign -alg hmac-...] syntax to calculate HMAC values. Using a less complex and very minimalistic base32-to-binary [string map] conversation to decode the Google Authenticator keys. Using a serialized verification of multiple clock skews in a relative increasing/decreasing order to Unix epoch time. Using slightly optimized [expr { }] syntaxes. Avoiding unnecessary $variable usage. Avoiding calls into (rather slow) TCL procedures. Performance comparison: The performance data below was gathered by using a maximum allowed clock skew value of +/-5 minutes between clients and the F5, resulting in a calculation of either 1 verfication value (ideal case) or up to 21 verfication values (worst case) for a single token verification . Test Name | This iRule | Previous iRule(s)| Savings Valid verification via Unix epoch time (ideal case) | 191.078 cycles | 5.497.482 cycles | 96,6% Valid verification via -2,5 min clock skew (median case)| 511.067 cycles | 5.504.262 cycles | 90,8% Valid verification via -5 min clock skew (worst case) | 816.676 cycles | 5.502.650 cycles | 85,2% Invalid verification (always worst case) | 849.217 cycles | 5.464.924 cycles | 84,5% Note: The results of the "Previous iRule(s)" was gathered by using a striped down version (e.g. using fixed keys, tokens and clock values with disabled logging and HTTP responses) of Stanislas latest Google Authenticator HTTP API iRule, since it was (until today) the only published version that includes a support to handle token verification using multiple clock skews. The core functionality of Stanislas Google Authenticator HTTP API iRule is heavely based on the original Google Authenticator iRule published by George Watkins, but Stanislas already uses a slightly performance optimized syntax for certain [expr {}] syntaxes. Cheers, Kai Footnote: Special thanks goes to George Watkins for publishing the very first Google Authenticator iRule back in 2012. And also to Stanislas for publishing his Google Authenticator HTTP API iRule, which has introduced a handy HTTP API support for APM Policies and a very useful addition to handle clock skews. How to use this snippet: Integrate the core functionality of this Google Authenticator Verification iRule in your own Google Authenticator solution. Tweak the $static::ga_key_size and $static::ga_hmac_mode settings as needed. Tweak the $static::allowed_clock_skew_units to set the maximum allowed clock skew units between your LTM and the end user devices. The variable $ga(key) is used to set the provisioned Google Authenticator shared user key. The variable $ga(token) is used to set the user provided Google Authenticator token. The variable $result stores the verification results. Enjoy! Code : when RULE_INIT { ############################################################################################## # Configure the Google Authenticator key sizes and HMAC operation modes. # # Note: Google Authenticator uses a hardcoded 80 bit key length with HMAC-SHA1 verification. # The underlying HOTP algorithm (see RFC 4226) and TOTP algorithm (RFC 6238) standards # require at least 128 bit and even recommend a 160 bit key length. In addition, both # RFC standards include HMAC-SHA256 and HMAC-SHA512 operation modes. # So if the Google Authenticator code is changed in the future to match the official # requirements or even recommendations, then you have to change the variables below. # set static::ga_key_size 80 ;# Shared key size in bits set static::ga_hmac_mode "hmac-sha1" ;# Options "hmac-sha1", "hmac-sha256" or "hmac-sha512" ############################################################################################## # Configure the allowed clock skew units (1 unit = +/-30 seconds in both directions) # set static::ga_allowed_clock_skew_units 10 ############################################################################################## # Initialize the Base32 alphabet to binary conversation (see RFC 4648) # set static::b32_to_binary [list \ A 00000 B 00001 C 00010 D 00011 \ E 00100 F 00101 G 00110 H 00111 \ I 01000 J 01001 K 01010 L 01011 \ M 01100 N 01101 O 01110 P 01111 \ Q 10000 R 10001 S 10010 T 10011 \ U 10100 V 10101 W 10110 X 10111 \ Y 11000 Z 11001 2 11010 3 11011 \ 4 11100 5 11101 6 11110 7 11111 \ 0 "" 1 "" = "" " " "" \ ] } when HTTP_REQUEST { ############################################################################################## # Defining the user provided token code and provisioned user key # set ga(token) "000000" set ga(key) "ZVZG5UZU4D7MY4DH" ############################################################################################## # Initialization of the Google Authentication iRule # # Map the Base32 encoded ga(key) to binary string representation and check length >= $static::ga_key_size if { [string length [set ga(key) [string map -nocase $static::b32_to_binary $ga(key)]]] >= $static::ga_key_size } then { # Convert the translated ga(key) binary string representation to binary set ga(key) [binary format B$static::ga_key_size $ga(key)] # Initialize ga(clock) timeframe based on Unix epoch time in seconds / 30 set ga(clock) [expr { [clock seconds] / 30 } ] ############################################################################################## # Perform verification of the provided ga(token) for current time frame ga(clock) # # Calculate hex encoded HMAC checksum value for wide-int value of time frame ga(clock) using ga(key) as secret binary scan [CRYPTO::sign -alg $static::ga_hmac_mode -key $ga(key) [binary format W* $ga(clock)]] H* ga(verify) # Parse ga(offset) based on the last nibble (= 4 bits / 1 hex) of the ga(verify) HMAC checksum and multiply with 2 for byte to hex conversation set ga(offset) [expr { "0x[string index $ga(verify) end]" * 2 } ] # Parse (= 4 bytes / 8 hex) from ga(verify) starting at the ga(offset) value, then remove the most significant bit, perform the modulo 1000000 and format the result to a 6 digit number set ga(verify) [format %06d [expr { ( "0x[string range $ga(verify) $ga(offset) [expr { $ga(offset) + 7 } ]]" & 0x7FFFFFFF ) % 1000000 } ]] # Compare ga(verify) with user provided ga(token) value if { $ga(verify) equals $ga(token) } then { # The provided ga(token) is valid" set result "valid" } elseif { $static::ga_allowed_clock_skew_units > 0 } then { ############################################################################################## # Perform verification of the provided ga(token) for additional clock skew units # # Note: The order is increasing/decreasing according to ga(clock) (aka. Unix epoch time +30sec, -30sec, +60sec, -60sec, etc.) # set result "invalid" for { set x 1 } { $x <= $static::ga_allowed_clock_skew_units } { incr x } { ############################################################################################## # Perform verification of the provided ga(token) for time frame ga(clock) + $x # # Calculate hex encoded HMAC checksum value for wide-int value of time frame ga(clock) + x using ga(key) as secret binary scan [CRYPTO::sign -alg $static::ga_hmac_mode -key $ga(key) [binary format W* [expr { $ga(clock) + $x }]]] H* ga(verify) # Parse ga(offset) based on the last nibble (= 4 bits / 1 hex) of the ga(verify) HMAC checksum and multiply with 2 for byte to hex conversation set ga(offset) [expr { "0x[string index $ga(verify) end]" * 2 } ] # Parse (= 4 bytes / 8 hex) from ga(verify) starting at the ga(offset) value, then remove the most significant bit, perform the modulo 1000000 and format the result to a 6 digit number set ga(verify) [format %06d [expr { ( "0x[string range $ga(verify) $ga(offset) [expr { $ga(offset) + 7 } ]]" & 0x7FFFFFFF ) % 1000000 } ]] # Compare ga(verify) with user provided ga(token) value if { $ga(verify) equals $ga(token) } then { # The provided ga(token) is valid" set result "valid" break } ############################################################################################## # Perform verification of the provided ga(token) for time frame ga(clock) - $x # # Calculate hex encoded HMAC checksum value for wide-int value of time frame ga(clock) - $x using ga(key) as secret binary scan [CRYPTO::sign -alg $static::ga_hmac_mode -key $ga(key) [binary format W* [expr { $ga(clock) - $x }]]] H* ga(verify) # Parse ga(offset) based on the last nibble (= 4 bits / 1 hex) of the ga(verify) HMAC checksum and multiply with 2 for byte to hex conversation set ga(offset) [expr { "0x[string index $ga(verify) end]" * 2 } ] # Parse (= 4 bytes / 8 hex) from ga(verify) starting at the ga(offset) value, then remove the most significant bit, perform the modulo 1000000 and format the result to a 6 digit number set ga(verify) [format %06d [expr { ( "0x[string range $ga(verify) $ga(offset) [expr { $ga(offset) + 7 } ]]" & 0x7FFFFFFF ) % 1000000 } ]] # Compare ga(verify) with user provided ga(token) value if { $ga(verify) equals $ga(token) } then { # The provided ga(token) is valid" set result "valid" break } } } else { # The provided ga(token) is invalid" set result "invalid" } } else { #The provided ga(key) is malformated set result "error: malformated shared key" } unset -nocomplain ga ############################################################################################## # Handle for token verification results # # log local0.debug "Verification Result: $result" HTTP::respond 200 content "Verification Result: $result" # return $result } Tested this on version: 12.01.5KViews0likes6Comments