OCSP through an outbound explicit proxy
Hey DC community, Kevin Stewart here with a fun little project I'd like to share.
There have been countless questions about this over the years: how to pass LTM or APM OCSP requests through an outbound explicit proxy. We've already established that you can create a simple VIP and iRule that "proxifies" an outbound OCSP HTTP request, and that you can point the OCSP responder configuration URL directly at this layered VIP.
But what if client certificates need to be validated against different OCSP responders, based on the X509 AIA extension? AIA, or "Authority Information Access" is an X509 certificate extension that identifies, among other things, the OCSP responder URL that a given client certificate should be validated against. The LTM and APM OCSP clients (as of 13.1) don't pass an HTTP Host header, and don't allow you to inject any "signaling" information into the outbound OCSP request. That means there's no easy way to get the client's real certificate AIA value over to the proxy VIP, plus you need the OCSP responder to point to the proxy VIP anyway.
The answer is to signal "out-of-band" - at the application VIP, catch the certificate issuer, serial and AIA values into a table,
issuer_hash:serial = AIA
and at the proxy VIP, catch the issuer and serial in the OCSP request and look up the AIA URL from the table. The issuer value in the OCSP request is a SHA-encoding of the issuer value from the DER-formatted certificate. You can't just SHA hash the X509::issuer value. That'd be too easy. It's necessary then to binary parse the issuer directly from the binary certificate blob. The OCSP request is already binary, so you have to binary parse this data anyway.
Before I get into the details, I should warn you that there's a lot of binary processing TCL code in these iRules, which a) isn't for the faint of heart, b) isn't great for performance, and c) isn't going to correct for every type of certificate. I did test this with sha1WithRSAEncryption and sha256WithRSAEncryption certificate signature algorithms, across both RSA and EC certs, and these all worked. It's also worth noting that the performance loss may be neglible given that OCSP itself will be the bigger hit. However, things like TLS session resumption should keep full handshakes and OCSP validation from happening as much over a set of active user sessions.
The configuration looks basically like this:
- Create your application VIP, pool, and client SSL profile as required. Test without client cert auth first to makes sure basic flow and functionality works as expected.
- Create an internal "proxy" VIP and pool that points directly at the outbound explicit proxy (ex. 192.168.2.1:3128), and attach the proxy iRule. You can also create an empty VLAN (no interfaces), attach a unique self-IP to that VLAN (ex. 11.11.11.1), and assign an IP from that subnet to the proxy VIP (ex. 11.11.11.2:80) to keep all OCSP proxy traffic internal. Also make sure the proxy VIP is listening on the correct VLAN, enables address and port translation, and SNAT as required.
- If using LTM OCSP, create the OCSP responder, configuration and profile.
- If using APM OCSP, create the OCSP responder AAA and an access policy VPE that starts with the OCSP authentication agent - the included iRules parse the certificate issuer, serial and AIA from CLIENTSSL_CLIENTCERT, so the client SSL profile needs to request the client certificate (versus the APM On-Demand Cert Auth agent). This could also be done completely within the Access context, but I'm not adding that code right now (maybe later).
- Point the OCSP responder URL at the HTTP proxy VIP URL (ex. http://11.11.11.2) and check "ignore AIA". If APM OCSP, set CertID Digest to SHA-1. LTM OCSP automatically uses SHA-1.
- Attach the application iRule to the application VIP, and attach either the LTM OCSP authentication profile or APM access policy to the application VIP.
Application iRule
when CLIENTSSL_CLIENTCERT { ## Get the SHA1 hash of the DER-formatted issuer value ## X509 certificates are ASN.1-formatted, which is not easy to parse. ## Based on RFC5280 (https://tools.ietf.org/html/rfc5280), the issuer field is a mandatory value in the X509 certificate ## that comes directly after the signature algorithmIdentifier value. So we'll start by finding the object identifier (OID) ## of the signature algorithm, and then walk down the ASN.1 blob to the next item, which is the issuer. if { [SSL::cert count] > 0 } { ## Start with the PEM-formatted certificate, remove the "BEGIN CERTIFICATE" and "END CERTIFICATE" header/footer, ## and base64-decode the remaining data to expose the binary (DER-formatted) certificate. set strippedcert [findstr [X509::whole [SSL::cert 0]] "-----BEGIN CERTIFICATE-----" 27 "-----END CERTIFICATE-----"] if { [catch { b64decode ${strippedcert} } string_b64decoded] == 0 and ${string_b64decoded} ne "" } { if { [catch { ## sigAlg OID (1.2.840.113549.1.1.5) = sha1WithRSAEncryption = HEX \x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x05 ## sigAlg OID (1.2.840.113549.1.1.11) = sha256WithRSAEncryption = HEX \x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x0B ## Find the initial offset at the position of sigAlg OID in the certificate ## Here's an online tool to convert OID to Hex: https://misc.daniel-marschall.de/asn.1/oid-converter/online.php if { [expr [set offset [string first \x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x0B ${string_b64decoded}]] > 100] } { if { [expr [set offset [string first \x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x05 ${string_b64decoded}]] > 100] } { ## Error: unknown signature algorithm } } ## Offset this position by the size of the sigAlg set newoffset [expr ${offset} + 13] ## Get the length of the issuer value - ASN.1 values start with a type and a length, so this extracts the length value. binary scan [string range ${string_b64decoded} ${newoffset} [expr ${newoffset} + 4]] H2H2 type issuer_len_hex ## Convert the length to decimal scan ${issuer_len_hex} %x issuer_len_dec ## SHA1-encode the DER-formatted issuer value - this is the value that will be present in an OCSP request binary scan [sha1 [string range ${string_b64decoded} ${newoffset} [expr ${newoffset} + ${issuer_len_dec} + 1]]] H* issuer_hash ## Store the SHA1 issuer hash:cert serial in a short-lived table with the AIA URL set AIA [findstr [X509::extensions [SSL::cert 0]] "OCSP - URI:" 11 "\n"] set serial [X509::serial_number [SSL::cert 0]] table set -subtable OCSP_AIA "${issuer_hash}:${serial}" "${AIA}" 5 5 } error] } { log local0. "error = $error" } } } }
Proxy iRule
when RULE_INIT { ## Static ocsp responder URL - if the client cert doesn't have an AIA or the app VIP iRule is unable to parse it, ## include this value as a default OCSP URL. You may optionally just choose to fail the connection. set static::ocsp_host "http://ocsp1.f5testlab.local/" } when CLIENT_ACCEPTED { set AIA "" TCP::collect } when CLIENT_DATA { ## Find the first offset as the position of the SHA-1 OID - based on RFC 2560 (https://www.ietf.org/rfc/rfc2560.txt), ## an OCSP request is an ASN.1-formatted binary structure that contains the following: ## - hashAlgorithm - we know this is SHA-1 because the F5 OCSP is configured to use SHA-1. ## - isserNameHash - a SHA-1 hash of the DER-formatted issuer value. This is the first value we need. ## - issuerKeyHash - a SHA-1 hash of the issuer's public key. We don't need this value. ## - serialNumber - the client certificate serial number. This is the second value we need. set offset [string first \x06\x05\x2B\x0E\x03\x02\x1A [TCP::payload]] ## Set new offset after the SHA1 OID set newoffset [expr ${offset} + 7] ## Get the issuerHash length binary scan [string range [TCP::payload] ${newoffset} [expr ${newoffset} + 8]] H2H2H2H2 t1 t2 t3 issuer_len_hex ## convert the issuerHash length to decimal scan ${issuer_len_hex} %x issuer_len_dec ## Get the issuer hash value - this is the issuer hash that we need. binary scan [string range [TCP::payload] [expr ${newoffset} + 4] [expr ${newoffset} + 3 + ${issuer_len_dec}]] H* issuer_hex ## Set new offset after the issuerHash set newoffset [expr ${newoffset} + 4 + ${issuer_len_dec}] ## Get the issuerKeyHash length binary scan [string range [TCP::payload] ${newoffset} [expr ${newoffset} + 4]] H2H2 type issuerKey_len_hex ## convert the issuerKeyHash length to decimal scan ${issuerKey_len_hex} %x issuerKey_len_dec ## Set new offset after the issuerKeyhash set newoffset [expr ${newoffset} + 2 + ${issuerKey_len_dec}] ## Get the serial length binary scan [string range [TCP::payload] ${newoffset} [expr ${newoffset} + 4]] H2H2 type len_hex ## Convert the serial length to decimal scan ${len_hex} %x len_dec ## This is the serial number that we need. binary scan [string range [TCP::payload] [expr ${newoffset} + 2] [expr ${newoffset} + 1 + ${len_dec}]] H* serial ## Look up the table entry for the issuerHash:serial set AIA [table lookup -subtable OCSP_AIA "${issuer_hex}:${serial}"] ## Optionally close the connection or doing something else if a table entry isn't found. Otherwise release. TCP::release } when HTTP_REQUEST { if { ${AIA} ne "" } { ## If AIA exists, dynamically change the URI to map to the client cert OCSP AIA URL ## This is the "proxyification" function to allow unencrypted HTTP to flow through an explicit proxy. ## Most, if not all public OCSP responders perform OCSP over unencrypted HTTP, so there's no need ## to deal with messy HTTP CONNECT logic. HTTP::uri "${AIA}" } else { ## Otherwise use the system default OCSP URL HTTP::uri $static::ocsp_host } }
To recap, an OCSP responder configuration (LTM or APM) points to a local "proxy" VIP (and ignores AIA). The application VIP iRule first grabs the client certificate AIA URL, serial number, and issuer hash, and throws those values into a table. Upon client certificate authentication, the OCSP responder client issues an OCSP request that flows to the proxy VIP. The proxy VIP grabs the issuer hash and serial number from the OCSP request, looks up the AIA URL in the table, and dynamically changes the HTTP URI value in the outbound request to the explicit proxy, to effectively proxify the OCSP request.
As stated, this should work with sha1WithRSAEncryption and sha256WithRSAEncryption certificates. If you come across a certificate that uses something else, please let me know.
Thanks.
- Kevin
Edit:
If you want to do this completely inside APM, using an On-Demand Cert Auth agent:
- Insert an iRule event agent between the On-Demand Cert Auth agent and the OCSP Auth agent in the VPE. Use "GETISSUER" as the iRule event agent ID.
- Switch out the application VIP with the following:
Access Application VIP
when ACCESS_POLICY_AGENT_EVENT { if { [ACCESS::policy agent_id] eq "GETISSUER" } { set strippedcert [findstr [ACCESS::session data get session.ssl.cert.whole] "-----BEGIN CERTIFICATE-----" 27 "-----END CERTIFICATE-----"] if { [catch { b64decode ${strippedcert} } string_b64decoded] == 0 and ${string_b64decoded} ne "" } { if { [catch { ## sigAlg OID (1.2.840.113549.1.1.5) = sha1WithRSAEncryption = HEX \x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x05 ## sigAlg OID (1.2.840.113549.1.1.11) = sha256WithRSAEncryption = HEX \x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x0B ## find the position of sigAlg in the certificate if { [expr [set offset [string first \x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x0B ${string_b64decoded}]] > 100] } { if { [expr [set offset [string first \x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x05 ${string_b64decoded}]] > 100] } { ## Error: unknown signature algorithm } } ## offset this position by the size of the sigAlg set newoffset [expr ${offset} + 13] ## get the length of the DER-formatted issuer value binary scan [string range ${string_b64decoded} ${newoffset} [expr ${newoffset} + 4]] H2H2 type issuer_len_hex ## convert the length to decimal scan ${issuer_len_hex} %x issuer_len_dec ## SHA1-encode the DER-formatted issuer value binary scan [sha1 [string range ${string_b64decoded} ${newoffset} [expr ${newoffset} + ${issuer_len_dec} + 1]]] H* issuer_hash ## Store the SHA1 issuer hash:cert serial in a short-lived table with the AIA URL set AIA [findstr [ACCESS::session data get session.ssl.cert.x509extension] "OCSP - URI:" 11 "\n"] set serial [ACCESS::session data get session.ssl.cert.serial] table set -subtable OCSP_AIA "${issuer_hash}:${serial}" "${AIA}" 10 10 } error] } { log local0. "error = $error" } } } }
- Piotr_BiesiadaNimbostratus
Hi,
small improvement about issuer object length in Application iRule
when CLIENTSSL_CLIENTCERT { ## Get the SHA1 hash of the DER-formatted issuer value ## X509 certificates are ASN.1-formatted, which is not easy to parse. ## Based on RFC5280 (https://tools.ietf.org/html/rfc5280), the issuer field is a mandatory value in the X509 certificate ## that comes directly after the signature algorithmIdentifier value. So we'll start by finding the object identifier (OID) ## of the signature algorithm, and then walk down the ASN.1 blob to the next item, which is the issuer. if { [SSL::cert count] > 0 } { if { [catch { ## sigAlg OID (1.2.840.113549.1.1.5) = sha1WithRSAEncryption = HEX \x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x05 ## sigAlg OID (1.2.840.113549.1.1.11) = sha256WithRSAEncryption = HEX \x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01\x0B ## Find the initial offset at the position of sigAlg OID in the certificate ## Here's an online tool to convert OID to Hex: https://misc.daniel-marschall.de/asn.1/oid-converter/online.php # do not care about last byte x05 or x0B if { [expr [set offset [string first \x06\x09\x2A\x86\x48\x86\xF7\x0D\x01\x01 [SSL::cert 0]]] > 100] } { log local0. "ERR:SigAlg-unknown" } ## Offset this position by the size of the sigAlg #13 bytes, size of this ASN.1 object set newoffset [expr {$offset + 13}] ## Get the length of the issuer value - ASN.1 values start with a type and a length, so this extracts the length value. binary scan [string range [SSL::cert 0] ${newoffset} [expr {$newoffset + 5}]] c1c1c1c1 type issuer_len_dec issuer_len_dec_ext1 issuer_len_dec_ext2 #RFC https://tools.ietf.org/html/rfc5280 p.124 issuer length could be more than 256 bytes (322 + asn1 encoding) #c1 is signed byte, we need to & 0xFF set issuer_len_dec [expr {$issuer_len_dec & 0xff}] #x81 if {${issuer_len_dec} == 129 } { set issuer_len_dec_ext1 [expr {$issuer_len_dec_ext1 & 0xff}] set issuer_len_dec [expr {$issuer_len_dec_ext1 + 1}] } elseif { ${issuer_len_dec} == 130 } { #x82 set issuer_len_dec_ext1 [expr {$issuer_len_dec_ext1 & 0xff}] set issuer_len_dec_ext2 [expr {$issuer_len_dec_ext2 & 0xff}] set issuer_len_dec [expr { $issuer_len_dec_ext1 << 8 + $issuer_len_dec_ext2 + 2} ] } ## SHA1-encode the DER-formatted issuer value - this is the value that will be present in an OCSP request binary scan [sha1 [string range [SSL::cert 0] ${newoffset} [expr {${newoffset} + ${issuer_len_dec} + 1}]]] H* issuer_hash ## Store the SHA1 issuer hash:cert serial in a short-lived table with the AIA URL set AIA [findstr [X509::extensions [SSL::cert 0]] "OCSP - URI:" 11 "\n"] #remove : from serial set serial [string map { ":" "" } [X509::serial_number [SSL::cert 0]]] table set -subtable OCSP_AIA "${issuer_hash}:${serial}" "${AIA}" 300 300 } error] } { log local0. "error = $error" } } }
- thegeneralmillsNimbostratus
Thanks for putting this together. We used part of this method to support some F5s that are out in a PoP where we were not planning on adding SNAT capabilities to the access our OCSP stapling endpoint, it did, however, have access to a data center that did have SNAT enabled. We took the "Proxy iRule" you created and applied it to a Virtual Server in the RFC1918 space with a Pool that consisted of the DNS record of our OCSP stapling endpoint. SNAT from the data center works like a champ, and we didn't have to find a different work around to solve this issue.
Thanks a bunch, very helpful and informative!