Extended X509 Certificate Parsing

Problem this snippet solves:

For all of their power and flexibility, there are still a few things that iRules don't do out-of-the-box. Specifically, there are easy commands for fetching tons of information from X509 certificate data, but there still may be a lot of really useful information in an X509 data structure that isn't readily accessible with the pre-built tools.

For example: userPrincipalName, Federal PIV FASC-N ID, and the countryOfCitizenship attributes.

The following iRule employs binary manipulation to find and format certificate data using its ASN.1 number. ASN.1, or Abstract Syntax Notation One, is a standard and flexible notation that describes data structures for representing, encoding, transmitting, and decoding data (http://en.wikipedia.org/wiki/Abstract_Syntax_Notation_One), and is the basis for naming objects in an X509 data structure.

For example, "1.3.6.1.4.1.311.20.2.3" is the ASN.1 Object Identifier (or OID) for the User Principal Name (UPN) field inside the subjectAlternativeName certificate attribute.

Here's the rule:

Code :

#### iRule: Extended X509 parsing RULE ################################
## version 1
## subver .0
## author: kevin@f5.com
## date revised: 05/12/2011
## name: extended.x509.parsing.rule
##
## last modified by: kevin@f5.com
##
## Purpose: Extract otherwise inaccessible attributes from X509 certificate data
##(ex. 
##Subject Alternative Name - User Principal Name (UPN)
##Federal Agency Smartcard Credential Number (FASC-N) 
##Subject Directory Attributes - Country of Citizenship (2.5.29.9)
##)
##
## Tools used to develop solution:
##linux-based OID converter (http://www.rtner.de/software/oid.html)
##ASN.1 Editor (www.lipingshare.com/Asn1Editor)
##Multi-function ascii/binary/decimal/hexidecimal translator (http://home2.paulschou.net/tools/xlate/)
##
## Credit for original implementation goes to Lucas Thompson
##
###########################################################
when CLIENTSSL_CLIENTCERT {
   set strippedcert [findstr [X509::whole [SSL::cert 0]] "-----BEGIN CERTIFICATE-----" 27 "-----END CERTIFICATE-----"]

   if { [catch { b64decode $strippedcert } str_b64dec] == 0 and $str_b64dec ne "" } { 
      ###### UPN ######################################
      if { [catch {
            ## UPN OID=1.3.6.1.4.1.311.20.2.3 - HEX=\x06\x0A\x2B\x06\x01\x04\x01\x82\x37\x14\x02\x03 - LENGTH=15 (add 3 to hex count)
            set offset [string first \x06\x0A\x2B\x06\x01\x04\x01\x82\x37\x14\x02\x03 $str_b64dec]
            set newoffset [expr $offset + 15]
            ## last value in decoded string (up to new offset and converted to integer) indicates length of the UPN value
            binary scan [string index $str_b64dec $newoffset] c upnlengthinteger
            set upn [string range $str_b64dec [expr $newoffset + 1] [expr $upnlengthinteger + $newoffset]]
         } error] 
      } {
         set upn "NO UPN"
      }

      ###### FASC #####################################
      if { [catch {
            ## FASCN OID=2.16.840.1.101.3.6.6 - HEX=\x06\x08\x60\x86\x48\x01\x65\x03\x06\x06 - LENGTH=13 (+3 to hex count)
            set offset [string first \x06\x08\x60\x86\x48\x01\x65\x03\x06\x06 $str_b64dec]
            set newoffset [expr $offset + 13]
            ## last value in decoded string (up to new offset and converted to integer) indicates length of the FASC-N value
            binary scan [string index $str_b64dec $newoffset] c fasclengthinteger

            ## At this point we have the FASC-N value, which could be in either UTF8String or OCTET STRING format.
            ## To get format, look at second to last value in decoded string (up to new offset and converted to integer)
            ## A value of 12 = UTF8String, and 4 = OCTET STRING (http://en.wikipedia.org/wiki/Basic_Encoding_Rules)
            binary scan [string index $str_b64dec [expr $newoffset -1]] c fasctypeint

            if { $fasctypeint == 12 } {
               ## simple UTF8String format
               set fasc [string range $str_b64dec [expr $newoffset + 1] [expr $fasclengthinteger + $newoffset]]
            } elseif { $fasctypeint == 4 } {
               ## OCTET STRING format
               set fasc [binary scan [string range $str_b64dec [expr $newoffset + 1] [expr $fasclengthinteger + $newoffset]] H* test]
               ## Convert hex data to binary
               set t [list 0 0000 1 0001 2 0010 3 0011 4 0100 5 0101 6 0110 7 0111 8 1000 9 1001 a 1010 b 1011 c 1100 d 1101 e 1110 f 1111 A 1010 B 1011 C 1100 D 1101 E 1110 F 1111]
               regsub {^0[xX]} $test {} hex
               set bin [string map -nocase $t $hex]
               ## Convert binary to BCD decimal
               set t [list 00001 0 10000 1 01000 2 11001 3 00100 4 10101 5 01101 6 11100 7 00010 8 10011 9 10110 "" 11010 ""]
               set fasc [string map $t $bin]
            } else {
               ## error
               set fasc "FASC ERROR"
            }
         } error] 
      } {
         set fasc "NO FASC-N"
      }

      ###### Citizenship ###############################
      if { [catch {
            ## CITIZENSHIP OID=1.3.6.1.5.5.7.9.4 - HEX=\x06\x08\x2B\x06\x01\x05\x05\x07\x09\x04 - LENGTH=13 (+3 to hex count)
            set offset [string first \x06\x08\x2B\x06\x01\x05\x05\x07\x09\x04 $str_b64dec]
            set newoffset [expr $offset + 13]
            ## last value in decoded string (up to new offset and converted to integer) indicates length of CITIZENSHIP value
            binary scan [string index $str_b64dec $newoffset] c citlengthinteger
            set citizenship [string range $str_b64dec [expr $newoffset + 1] [expr $citlengthinteger + $newoffset]]
         } error] 
      } {
         set citizenship "NO CITIZENSHIP"
      }

      log local0. "upn = $upn"
      log local0. "fasc = $fasc"
      log local0. "citizenship = $citizenship"

   } else { 
     ## base64 decode failed
   } 
}
}

# Notice the list of tools in the header:

{
   linux-based OID converter (http://www.rtner.de/software/oid.html)
      This tool converts the ASN.1 numbers into their corresponding hexidecimal values.

   ASN.1 Editor (www.lipingshare.com/Asn1Editor)
      This Windows-based GUI tool allows you to examine the ASN.1 values and structure of a certificate.

   Multi-function ascii/binary/decimal/hexidecimal translator (http://home2.paulschou.net/tools/xlate/)
      Not critical, but a great utility nonetheless.
}

# Explanation of binary functions:

{
  ## UPN OID=1.3.6.1.4.1.311.20.2.3 - HEX=\x06\x0A\x2B\x06\x01\x04\x01\x82\x37\x14\x02\x03 - LENGTH=15 (+3 to hex count)
  set offset [string first \x06\x0A\x2B\x06\x01\x04\x01\x82\x37\x14\x02\x03 $str_b64dec]
  set newoffset [expr $offset + 15]
  ## last value in decoded string (up to the new offset and converted to an integer) indicates length of the UPN value
  binary scan [string index $str_b64dec $newoffset] c upnlengthinteger
  set upn [string range $str_b64dec [expr $newoffset + 1] [expr $upnlengthinteger + $newoffset]]
}

# The userPrincipalName value has an ASN.1 OID assignment of 1.3.6.1.4.1.311.20.2.3. Converted to hexadecimal, that's \x06\x0A\x2B\x06\x01\x04\x01\x82\x37\x14\x02\x03. The "string first" command finds the numerical index at which this hexidecimal pattern occurs. We then add 15 to that number (the number of hexadecimal values plus 3). This is the new offset. The last value in the pattern, before the actual UPN value, indicates the length of the UPN. We'll use "string index" to find this value and then convert it to decimal. The final statement does a "string range" command to fetch the data between the new offset (plus 1) and the length of the UPN (plus the new offset). Do the same thing for the countryOfCitizenship value (but with a different ASN.1 OID).

# Pretty simple, no?

{
  binary scan [string index $str_b64dec [expr $newoffset -1]] c fasctypeint

  if { $fasctypeint == 12 } {
     ## simple UTF8String format
     set fasc [string range $str_b64dec [expr $newoffset + 1] [expr $fasclengthinteger + $newoffset]]
  } elseif { $fasctypeint == 4 } {
     ## OCTET STRING format
     set fasc [binary scan [string range $str_b64dec [expr $newoffset + 1] [expr $fasclengthinteger + $newoffset]] H* test]
     ## Convert hex data to binary
     set t [list 0 0000 1 0001 2 0010 3 0011 4 0100 5 0101 6 0110 7 0111 8 1000 9 1001 a 1010 b 1011 c 1100 d 1101 e 1110 f 1111 A 1010 B 1011 C 1100 D 1101 E 1110 F 1111]
     regsub {^0[xX]} $test {} hex
     set bin [string map -nocase $t $hex]
     ## Convert binary to BCD decimal
     set t [list 00001 0 10000 1 01000 2 11001 3 00100 4 10101 5 01101 6 11100 7 00010 8 10011 9 10110 "" 11010 ""]
     set fasc [string map $t $bin]
  } else {
     ## error
     set fasc "FASC ERROR"
  }

# The FASC-N value is a little trickier. Depending on the agency this value can be in either UTF8String or OCTET STRING format. Per the included link, each has a corresponding DER encoding type which just happens to be the second to last value in the data before the actual FASC-N value (remember the last value was the length of the data). The first statement retrieves this value ([expr $newoffset -1]). If it's "12" (UTF8String), then a simple string parse is in order. If it's "4" (OCTET STRING) though, we have to retrieve the binary data, convert it to hex, then to binary, and then to Packed Binary Coded Decimal (BCD). I'll leave the reason for all of this as another academic discussion. There's probably also a cleaner way to do all of these conversions, but I chose a pair of simple mapping methods to get the job done.

# And there you have it. Everything is wrapped nicely inside a few nested catch statements (since there's no guarantee every certificate will have all of the values you're looking for); and it resides in the CLIENTSSL_CLIENTCERT event. You could just as easily put this code anywhere you have access to [SSL::cert 0] data though, like an ACCESS_POLICY_AGENT_EVENT event.

# Enjoy!
Published Mar 17, 2015
Version 1.0

Was this article helpful?