For more information regarding the security incident at F5, the actions we are taking to address it, and our ongoing efforts to protect our customers, click here.

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
Published Oct 26, 2018
Version 1.0

2 Comments

  • 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
    
  • 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.