RADIUS authentication packet manipulation library
Problem this snippet solves:
RADIUS authentication library that facilitates development of complex full proxy RADIUS auth solutions. Links a RADIUS request with a response and validates the RADIUS response authenticator as well as any Message-Authenticator attribute if present.
This started out as an attempt to refactor "RADIUS server using APM to authenticate users". It has since proved really powerful so I wanted to share it in case others gain ideas from it or have any feedback.
It's not specifically tuned for performance since you could say I initially set about to untune Stanislas' work in order to make some iRules easier to maintain. It should still perform reasonably well considering there's not a whole lot of code in there. The focus was to ensure it would be robust and secure.
How to use this snippet:
Save this irule in /Common/RADIUS_AUTH. It doesn't require a radius profile.
Read the comments for usage details. If you are ever expecting an error condition, be sure to wrap your calls in a "catch" command and check the global $::errorCode value.
Increase the timeout variable if it's too short for your RADIUS client.
Code :
################################################################################ # # RADIUS_AUTH - Version 1.0.1 - 2018-11-01 # ################################################################################ # # Procedures used to manipulate RADIUS authentication packets with support # for Authenticator validation as well as Message-Authenticator. # # Tested on BIG-IP LTM 12.1.2 # # Supported RADIUS codes: # 1: Access-Request # 2: Access-Accept # 3: Access-Reject # 11: Access-Challenge # # Installation: Create this irule as /Common/RADIUS_AUTH # # Usage: # Generally, when CLIENT_ACCEPTED call "parse_packet" to get information about # an Access-Request from a RADIUS client. The AVP array can then be manipulated # and a new packet generated in order to either replace the payload sent to the # server or drop the packet and respond directly. # # AVP array now uses a floating point multiple value index. The type code is # the integer component, the value after the decimal point is the sequence in # which it was read from the payload. In this manner, you could use for example # [array get AVP 18.*] to list all Reply-Message attributes in case there are # multiples. If you only care about the first instance of an attribute, # remember to use the float values like AVP(18.0) as AVP(18) would not be set. # # When working with Message-Authenticator, a key must be provided. Failure to # validate a Message-Authenticator will result in an error. # # There is no need to supply the Request Authenticator via the optional req_auth # parameter as long as the Access-Request went through parse_packet and you # generate a response before the timeout removes the entry from the table. # # You can use RADIUS_AUTH to some extent in conjunction with RADIUS::avp. Avoid # using RADIUS::avp to change values on packets that use Message-Authenticator. # # Shortcomings: # This library is concerned mostly with validating the packet structure. # In terms of RFC compliance, this library doesn't do any validation of AVP # values (other than Message-Authenticator). The table used to store request # authenticators assumes you are only using this with a single RADIUS virtual # server. If you use this library with many RADIUS virtual servers, move the # "static::RA_table" declaration out of here and into irules specific to # those servers with a unique value for each. # # The F5 supplied RADIUS library has good support for vendor AVPs. However, if # you do have a need to use RADIUS_AUTH for this, here's an example that # iterates through all the vendor specific AVPs: # # foreach {idx val} [array get AVP 26.*] { # binary scan $val Ia* vid bin_data # set output " Vendor($vid) " # call RADIUS_AUTH::avp_bin2array $bin_data VAVP # if {[array size VAVP] > 0} { # foreach vi [array names VAVP] { # append output "VAVP($vi): $VAVP($vi); " # } # } else { # append output "VAVP(raw): $bin_data; " # } # log local0. $output # } # # The ability for me to assemble this library was very much thanks to the APM # RADIUS server published by Stanislas Piron (whome in turn thanks John McInnes # for his prior work and Kai Wilke for further assistance). # # Project home: https://github.com/CharlesDarwinUniversity/RADIUS_AUTH # ################################################################################ when RULE_INIT { set static::RA_timeout 10 set static::RA_table "RADIUS_AUTH" set static::RA_pp_err "RADIUS_AUTH::parse_packet ERROR" set static::RA_gp_err "RADIUS_AUTH::generate_packet ERROR" set static::RA_errm(0) "OK" set static::RA_errm(1) "Malformed RADIUS packet" set static::RA_errm(2) "Malformed RADIUS AVP data" set static::RA_errm(3) "Key is requred to validate RADIUS response packet" set static::RA_errm(4) "Request Authenticator not found, can not validate response packet" set static::RA_errm(5) "Failed to validate RADIUS response authenticator" set static::RA_errm(6) "Invalid Message-Authenticator or wrong key" set static::RA_errm(7) "Invalid or unsupported RADIUS auth code" } ################################################################################ # # RADIUS_AUTH::parse_packet payload ?key? ?out_avp? ?out_username? ?req_auth? # ################################################################################ # # Validates the payload and returns the RADIUS code when valid. Populates an # output parameter with the AVP data as an array of multi-value type codes. # # Note: Doesn't return the packet ID becuase you can simply use RADIUS::id or # binary scan. # # "out_username" is populated with User-Name. If this is a response code, and # User-Name was not supplied it will lookup the stored Access-Request User-Name # value (RFC2865 hints that the response User-Name could be preferred). # # Example usage: # set code [call RADIUS_AUTH::parse_packet [UDP::payload] $key avp] # ################################################################################ proc parse_packet { payload {key ""} {out_avp ""} {out_username ""} { req_auth "" } } { if {$out_avp ne ""} { upvar $out_avp AVP } if {![array exists AVP]} { array set AVP {} } if {$out_username ne ""} { upvar $out_username USER_NAME } if { [catch { if { [binary scan $payload ccSa16 CODE IDENTIFIER PKT_LEN AUTHENTICATOR] != 4 \ || [set PKT_LEN [expr {$PKT_LEN & 0xFFFF}]] > [string length $payload] || $PKT_LEN > 4096} { error $static::RA_pp_err $static::RA_errm(1) 1 } else { # Length field is valid (less than 4096 and less than payload length). # Octets outside the range of the Length field MUST be treated as padding and ignored on reception. set PAYLOAD [string range $payload 0 $PKT_LEN] set IDENTIFIER [expr {$IDENTIFIER & 0xFF}] ;# Unsigned (compatible with RADIUS:id) } # Support RADIUS auth codes only... switch -- $CODE { 1 - 2 - 3 - 11 { if {$PKT_LEN > 20} { # Stores all attribute in AVP array with multiple value index of the format TYPE.SEQUENCE... call RADIUS_AUTH::avp_bin2array [string range $PAYLOAD 20 end] AVP msg_auth_offset if { [array size AVP] == 0 } { error $static::RA_pp_err $static::RA_errm(2) 2 } if { $msg_auth_offset > -1 } { set record_offset [expr {$msg_auth_offset+20}] binary scan [string replace $PAYLOAD $record_offset \ [expr {$record_offset + 18}] [binary format ccH32 80 18 [string repeat 0 32]]] a* UNSIGNED_PAYLOAD } } elseif { [array exists AVP] } { array unset AVP } if {![array exists AVP]} { array set AVP {} } ;# Make sure we have an AVP array, even if it's empty set USER_NAME [expr {[info exists AVP(1.0)] ? $AVP(1.0) : ""}] # Process the authenticator... if { $CODE == 1 } { # Access-Request code... set req_auth $AUTHENTICATOR # Store Request Authenticator and User-Name for later... table set "$static::RA_table.[IP::client_addr].$IDENTIFIER.RA" $AUTHENTICATOR $static::RA_timeout $static::RA_timeout table set "$static::RA_table.[IP::client_addr].$IDENTIFIER.UN" $USER_NAME $static::RA_timeout $static::RA_timeout } else { # RADIUS response codes... if { $key eq "" } { error $static::RA_pp_err $static::RA_errm(3) 3 } if { $req_auth eq "" } { set req_auth [table lookup "$static::RA_table.[IP::client_addr].$IDENTIFIER.RA"] if { $req_auth eq ""} { error $static::RA_pp_err $static::RA_errm(4) 4 } } set resp_auth [md5 [binary format ccSa16a[expr {$PKT_LEN-20}]a[string length $key] $CODE $IDENTIFIER \ $PKT_LEN $req_auth [string range $PAYLOAD 20 end] $key ]] if { $resp_auth ne $AUTHENTICATOR } { error $static::RA_pp_err $static::RA_errm(5) 5 } if { [info exists AVP(80.0)] } { # Patch Request Authenticator into UNSIGNED_PAYLOAD (see rfc2869, page 34) binary scan [string replace $UNSIGNED_PAYLOAD 4 19 $req_auth] a* UNSIGNED_PAYLOAD } } # If supplied, validate the Message-Authenticator... if { [info exists AVP(80.0)] && \ ($key eq "" || ![CRYPTO::verify -alg hmac-md5 -key $key -signature $AVP(80.0) $UNSIGNED_PAYLOAD]) } { error $static::RA_pp_err $static::RA_errm(6) 6 } # Response doesn't contain User-Name, populate it from the Access-Request value... if { $CODE != 1 && $USER_NAME eq "" } { set USER_NAME [table lookup "$static::RA_table.[IP::client_addr].$IDENTIFIER.UN"] } } default { error $static::RA_pp_err $static::RA_errm(7) 7 } } }]}{ # Clean the out parameters on error array unset AVP * set USER_NAME "" error $static::RA_pp_err $::errorInfo $::errorCode } return $CODE } ################################################################################ # # RADIUS_AUTH::generate_packet code id key ?in_avp? ?force_msg_auth? ?req_auth? # ################################################################################ # # Returns a valid RADIUS packet payload. # # Example usage: # set payload [call RADIUS_AUTH::generate_packet $code $id $key avp 1] # ################################################################################ proc generate_packet { code id key {in_avp ""} {force_msg_auth 0} { req_auth "" } } { if {$in_avp ne ""} { upvar $in_avp AVP } else { array set AVP {} } if { $req_auth eq "" } { set req_auth [table lookup "$static::RA_table.[IP::client_addr].$id.RA"] if { $req_auth eq "" } { error $static::RA_gp_err $static::RA_errm(4) 4 } } if { [info exists AVP(80.0)] } { # Remove the Message-Authenticator, setting the flag to reinsert it later set force_msg_auth 1 unset AVP(80.0) } set bin_AVP [call RADIUS_AUTH::avp_array2bin AVP] set packet_length [expr { [string length $bin_AVP] + 20 }] if { $force_msg_auth==1 } { set UNSIGNED_AVP $bin_AVP[binary format ccH32 80 18 [string repeat 0 32]] incr packet_length 18 append bin_AVP [binary format cc 80 18][CRYPTO::sign -alg hmac-md5 -key $key [binary format ccSa16a* \ $code $id $packet_length $req_auth $UNSIGNED_AVP]] } if { $code==1 } { # Access-Request code... set authenticator $req_auth } else { # Assuming RADIUS response codes ( we could add $code validation here ) set authenticator [md5 [binary format ccSa16a[expr {$packet_length-20}]a[string length $key] \ $code $id $packet_length $req_auth $bin_AVP $key ]] } return [binary format ccSa16a* $code $id $packet_length $authenticator $bin_AVP] } ################################################################################ # # RADIUS_AUTH::avp_array2bin in_avp ?unsign? # ################################################################################ # # Returns the RADIUS binary string formatted representation of an AVP array. # # Unless "unsign" is set to 0, any Message-Authenticator value will be # replaced with zeros as any value would be meaningless in this context. # # Example usage: # set binary_avp [call RADIUS_AUTH::avp_array2bin avp] # ################################################################################ proc avp_array2bin { in_avp {unsign 1} } { upvar $in_avp AVP set binary "" foreach mv_idx [array names AVP] { set avp_type [expr {int($mv_idx)}] if { $avp_type != 80 || $unsign == 0 } { set avp_len [expr {[string length $AVP($mv_idx)]+2}] append binary [binary format cca* $avp_type $avp_len $AVP($mv_idx)] } } if { [info exists AVP(80.0)] && $unsign == 1 } { # Insert the unsigned Message-Authenticator append binary [binary format ccH32 80 18 [string repeat 0 32]] } return $binary } ################################################################################ # # RADIUS_AUTH::avp_bin2array binary out_avp ?out_msg_auth_offset? # ################################################################################ # # Interprets "binary" as RADIUS AVP data and populates the array specified by # the output parameter "out_avp". # # "out_msg_auth_offset" is used to get the offset of the Message-Authenticator # in the binary data incase you want to know if it exists or unsign it. This # feature is really only here to make packet parsing a little more efficient. # # Example usage: # set binary_avp [call RADIUS_AUTH::avp_array2bin avp] # ################################################################################ proc avp_bin2array { binary out_avp { out_msg_auth_offset "" } } { upvar $out_avp AVP if {[array exists AVP]} { array unset AVP } if { $out_msg_auth_offset ne "" } { upvar $out_msg_auth_offset msg_auth_offset } set msg_auth_offset -1 set bin_len [string length $binary] array set multi_value_index {} for {set avp_offset 0} {$avp_offset < $bin_len } {incr avp_offset $avp_len} { if {([binary scan $binary @${avp_offset}cc avp_type avp_len] != 2) || ([set avp_len [expr {$avp_len & 0xFF}]] < 3) \ || ($avp_offset+$avp_len > $bin_len) } { # This is not valid AVP data, trash the array and bail... array unset AVP break } set avp_type [expr {$avp_type & 0xFF}] if { $avp_type==80 } { set msg_auth_offset $avp_offset } if {![info exists multi_value_index($avp_type)]} { set multi_value_index($avp_type) -1 } set mv_idx "$avp_type.[incr multi_value_index($avp_type)]" binary scan $binary @${avp_offset}x2a[expr {$avp_len -2}] AVP($mv_idx) } if { not([array exists AVP]) }{ array set AVP {} } } ################################################################################ # # RADIUS_AUTH::pw_decrypt key req_auth encoded_password # ################################################################################ # # Decryption algorithm for User-Password attribute, contributed by Stanislas # Piron. # # Returns the decrypted password. # # Example usage: # binary scan [UDP::payload] @4a16 req_auth # set pw [call RADIUS_AUTH::pw_decrypt $key $req_auth $AVP(2)] # ################################################################################ proc pw_decrypt { key req_auth encoded_password } { binary scan [md5 $key$req_auth] WW bx_64bits_1 bx_64bits_2 binary scan $encoded_password W* encoded_password_w_list set password_list [list] foreach {cx_64bits_1 cx_64bits_2} $encoded_password_w_list { lappend password_list [expr { $cx_64bits_1 ^ $bx_64bits_1 }] [expr { $cx_64bits_2 ^ $bx_64bits_2 }] binary scan [md5 $key[binary format WW $cx_64bits_1 $cx_64bits_2]] WW bx_64bits_1 bx_64bits_2 } binary scan [binary format W* $password_list] A* password return $password } ################################################################################ # # RADIUS_AUTH::pw_encrypt key req_auth password # ################################################################################ # # Encryption algorithm for User-Password attribute, contributed by Stanislas # Piron. # # Returns the encrypted password. # # Example usage: # binary scan [UDP::payload] @4a16 req_auth # set AVP(2) [call RADIUS_AUTH::pw_encrypt $key $req_auth "my password"] # ################################################################################ proc pw_encrypt { key req_auth password } { binary scan [md5 $key$req_auth] WW bx_64bits_1 bx_64bits_2 binary scan [binary format a[expr {[string length $password] + 16 - [string length $password]%16}] $password ] W* password_w_list set encoded_password_list [list] foreach {px_64bits_1 px_64bits_2} $password_w_list { lappend encoded_password_list [expr { $px_64bits_1 ^ $bx_64bits_1 }] [expr { $px_64bits_2 ^ $bx_64bits_2 }] binary scan [md5 $key[binary format W2 [lrange $encoded_password_list end-1 end]]] WW bx_64bits_1 bx_64bits_2 } binary scan [binary format W* $encoded_password_list] A* encoded_password return $encoded_password } ################################################################################ # # RADIUS_AUTH::ipv4_to_octets ip # ################################################################################ # # Useful for manipulating IPv4 attributes such as NAS-IP-Address # # Returns the 4 byte representation of an IPv4 string # ################################################################################ proc ipv4_to_octets {ip} { set octets [split $ip .] foreach oct $octets { if {$oct < 0 || $oct > 255} { set octets [list 0 0 0 0] break } } if { [llength $octets] != 4 } { set octets [list 0 0 0 0] } return [binary format c4 $octets] }
Tested this on version:
12.1- Stanislas_Piro2Cumulonimbus
Hi,
I suggest this code for encoding / decoding
proc pw_decrypt { KEY Q_AUTHENTICATOR ENCODED_PASSWORD } { binary scan [md5 $KEY$Q_AUTHENTICATOR] WW bx_64bits_1 bx_64bits_2 binary scan $ENCODED_PASSWORD W* ENCODED_PASSWORD_W_LIST set PASSWORD_LIST [list] foreach {cx_64bits_1 cx_64bits_2} $ENCODED_PASSWORD_W_LIST { lappend PASSWORD_LIST [expr { $cx_64bits_1 ^ $bx_64bits_1 }] [expr { $cx_64bits_2 ^ $bx_64bits_2 }] binary scan [md5 $KEY[binary format WW $cx_64bits_1 $cx_64bits_2]] WW bx_64bits_1 bx_64bits_2 } binary scan [binary format W* $PASSWORD_LIST] A* PASSWORD return $PASSWORD } proc pw_encrypt { KEY Q_AUTHENTICATOR PASSWORD } { binary scan [md5 $KEY$Q_AUTHENTICATOR] WW bx_64bits_1 bx_64bits_2 binary scan [binary format a[expr {[string length $PASSWORD] + 16 - [string length $PASSWORD]%16}] $PASSWORD ] W* PASSWORD_W_LIST set ENCODED_PASSWORD_LIST [list] foreach {px_64bits_1 px_64bits_2} $PASSWORD_W_LIST { log local0. "$px_64bits_1 ^ $bx_64bits_1" lappend ENCODED_PASSWORD_LIST [expr { $px_64bits_1 ^ $bx_64bits_1 }] [expr { $px_64bits_2 ^ $bx_64bits_2 }] binary scan [md5 $KEY[binary format W2 [lrange $ENCODED_PASSWORD_LIST end-1 end]]] WW bx_64bits_1 bx_64bits_2 } binary scan [binary format W* $ENCODED_PASSWORD_LIST] A* ENCODED_PASSWORD return $ENCODED_PASSWORD }
and to get AUTHENTICATOR , use this command: instead if string range...
binary scan [UDP::payload] @4a16 Q_AUTHENTICATOR
- Sam_HallNimbostratus
Thanks Stanislas, very elegant. Does binary scan always perform better than string range? If not, when wouldn't I replace string range with a binary scan? Or is this just about binary string data as opposed to text.