Google 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.5KViews0likes6CommentsTCL procedures for Base32 encoding/decoding
Problem this snippet solves: Hi Folks, the iRule below contains two TCL procedures to support Base32 encoding and decoding (see RFC 4648 as well as RFC 3548) within iRules. The procedures are based on a rather simple but extensive [string map] syntax to translate or untranslate the Base32 alphabet on a given input data stream via its binary string representation. Compared to other Base32 libraries, which may convert the input on a per-charater/quantum basis, the single step [string map] translation will require significant less CPU cycles to handle the base32 encodings / decodings. Note: The provided Base32 decoder uses a liberal input validation (see RFC 4648 Section 3.3), by ignoring incorrect "=" paddings, accepting upper as well as lower case base32 alphabet characters, automatically translating "0" (zero) to "O", "1" (one) to "I", "8" (eight) to "B" and silently removing any WHITESPACE, TAB and "CRLF" sequences from the input. If the input string contains any other non-Base32 alphabet characters, an internal error will be raised and the output will become an empty string. Cheers, Kai How to use this snippet: The iRule below contains a RULE_INIT event which outlines the procedure usage. Enjoy! Code : when RULE_INIT { set string "Hello World!" set output [call b32encode $string] log local0.debug "Base32 encoded the input \"$string\" to \"$output\"" set string "JBSWY3DPEBLW64TMMQQQ====" set output [call b32decode $string] log local0.debug "Base32 decoded the input \"$string\" to \"$output\"" } proc b32decode { input } { set bin [string map -nocase [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 01110 1 01000 8 00001 \ " " "" "" "" "\n" ""] $input] if { [catch { set output [binary format B[expr { int( [string length $bin] / 8 ) * 8 }] $bin] }] } then { set output "" } return $output } proc b32encode { input } { binary scan $input B* bin return [string map [list 00000 A 00001 B 00010 C 00011 D \ 00100 E 00101 F 00110 G 00111 H \ 01000 I 01001 J 01010 K 01011 L \ 01100 M 01101 N 01110 O 01111 P \ 10000 Q 10001 R 10010 S 10011 T \ 10100 U 10101 V 10110 W 10111 X \ 11000 Y 11001 Z 11010 2 11011 3 \ 11100 4 11101 5 11110 6 11111 7 \ 0000 A=== 0001 C=== 0010 E=== 0011 G=== \ 0100 I=== 0101 K=== 0110 M=== 0111 O=== \ 1000 Q=== 1001 S=== 1010 U=== 1011 W=== \ 1100 Y=== 1101 2=== 1110 4=== 1111 6=== \ 000 A====== 001 E====== 010 I====== 011 M====== \ 100 Q====== 101 U====== 110 Y====== 111 4====== \ 00 A= 01 I= 10 Q= 11 Y= \ 0 A==== 1 Q==== ] $bin] } Tested this on version: 12.0694Views0likes0CommentsBase32 Encoder and Decoder
Problem this snippet solves: I was in the process of working on another code sample that was dependent on the ability to decode Base32 strings. iRules has no built-in support for Base32 encoding, so I wrote my own. The code will only handle UTF-8 text. Unicode text will become fragmented and will not be encoded correctly, but the code will not blow up when it encounters Unicode (important security consideration). If you must encode Unicode, convert to Base64 first. Code : when RULE_INIT { set b32_tests { JBSWY3DPEBLW64TMMQQQ==== KRUGS4ZANFZSAYLON52GQZLSEBZXI4TJNZTSC=== KRUGS4ZANFZSAYJAON2HE2LOM4QHO2LUNAQGCIDQMFSCA33GEA2A==== KRUGS4ZANFZSAYJAON2HE2LOM4QHO2LUNBXXK5BAMEQGM2LOMFWCA4LVMFXHI5LN } array set b32_alphabet { A 0 B 1 C 2 D 3 E 4 F 5 G 6 H 7 I 8 J 9 K 10 L 11 M 12 N 13 O 14 P 15 Q 16 R 17 S 18 T 19 U 20 V 21 W 22 X 23 Y 24 Z 25 2 26 3 27 4 28 5 29 6 30 7 31 } foreach input $b32_tests { log local0. " input = $input" set input [string toupper $input] set input [string trim $input =] set l [string length $input] set output "" set n 0 set j 0 # decode loop is outlined in RFC 4648 (http://tools.ietf.org/html/rfc4648#page-8) for { set i 0 } { $i < $l } { incr i } { set n [expr $n << 5] set n [expr $n + $b32_alphabet([string index $input $i])] set j [incr j 5] if { $j >= 8 } { set j [incr j -8] append output [format %c [expr ($n & (0xFF << $j)) >> $j]] } } log local0. "output/input = $output" # flip b32_alphabet so that base32 characters can be indexed by an 8-bit integer foreach { key value } [array get b32_alphabet] { array set b32_alphabet_inv "$value $key" } set input $output set output "" set l [string length $input] set n 0 set j 0 # encode loop is outlined in RFC 4648 (http://tools.ietf.org/html/rfc4648#page-8) for { set i 0 } { $i < $l } { incr i } { set n [expr $n << 8] set n [expr $n + [scan [string index $input $i] %c]] set j [incr j 8] while { $j >= 5 } { set j [incr j -5] append output $b32_alphabet_inv([expr ($n & (0x1F << $j)) >> $j]) } } # pad final input group with zeros to form an integral number of 5-bit groups, then encode if { $j > 0 } { append output $b32_alphabet_inv([expr $n << (5 - $j) & 0x1F]) } # if the final quantum is not an integral multiple of 40, append "=" padding set pad [expr 8 - [string length $output] % 8] if { ($pad > 0) && ($pad < 8) } { append output [string repeat = $pad] } log local0. " output = $output" log local0. [string repeat - 20] } } Tested this on version: 10.0527Views0likes1Comment