Client Certificate Request by URI with OCSP Checking (v10.1 - v10.2.x)

Problem this snippet solves:

This iRule requests a client cert for specific URIs and then validates the client cert against the client SSL profile's trusted CA cert bundle. If that succeeds, then the client cert is validated against an OCSP server (or pool of servers). Invalid certs are redirected to a URL with the Openssl verify code appended.

Note: See below for a sample bigip.conf showing the profile and virtual server definitions.

Warning: This is an anonymized version of an OCSP iRule that was tested in a customer implementation. I have not done any testing of the iRule since anonymizing it. So test, test, test!

Code :

# client_cert_ocsp_rule_v1.0.3_for_10.1.0
#
# Requires 10.1.0 or higher for fixes to:
#CR125264 - HTTP::respond should be allowed in CLIENTSSL_HANDSHAKE
#CR126501 - OCSP AUTH iRules need to detect server down vs. bad cert
#CR111646 - SSL 'peer cert mode request' broken
#
# v1.0.1 - 2010-06-08 - Aaron Hooley - Integralis - Updated for v10.1.0 and CMP compatibility
# v1.0.2 - 2010-06-23 - Aaron Hooley - Integralis - Invalidate SSL session on logout responses
# v1.0.3 - 2010-11-17 - Aaron Hooley - F5 - Handle SSL::sessionid returning null string instead of 64 0's

when RULE_INIT {

# URL to redirect clients to for failed authentication
# The error code is appended to the URL in the iRule
set static::auth_failure_url "https://www.example.com/cert_error.asp?errCode="

# Session timeout. Length of time (in seconds) to store the client cert in the session table.
set static::ocsp_session_timeout 1800

# Log debug messages? (0=none, 1=minimal, 2=verbose, 3=everything)
set static::ocsp_debug 3

# Enable audit logging? (0=none, 1=unvalidated requests only, 2=all requests)
set static::ocsp_audit_log_level 2

# OCSP VIP's default pool name
set static::ocsp_pool "ca_ocsp_http_pool"

# Enable OCSP checking? (0=assume all client certs have valid OCSP status, 1=check OCSP status)
# Set to 1 by default to use the OCSP result. Disable to test client certs and ignore bad OCSP results.
set static::check_ocsp 1

# Pages to require a client cert for (replace with datagroup post-testing)
#This is now configured in the ocsp_pages_to_require_cert_class datagroup

# Prefix to use when inserting the certificate details in the HTTP headers
set static::header_prefix "CRT_"

# SSL::sessionid returns 64 0's if the session ID doesn't exist, so set a variable to check for this
set static::ocsp_null_sessionid [string repeat 0 64]
}
when CLIENT_ACCEPTED {

# Initialise the TMM session id and variables tracking the auth status on each new connection
set tmm_auth_ssl_ocsp_sid 0
set invalidate_session 0
set need_cert 0
set inserted_headers 0

# Save the client IP:port and VIP name to shorten the log lines
set log_prefix "client IP:port=[IP::client_addr]:[TCP::client_port]; VIP=[virtual name]"
if {$static::ocsp_debug > 0}{log local0. "$log_prefix: New TCP connection to [IP::local_addr]:[TCP::local_port]"}
}
when CLIENTSSL_CLIENTCERT {

# This event is triggered when LTM requests/requires a cert, even if the client doesn't present a cert.
if {$static::ocsp_debug > 0}{log local0. "$log_prefix: Cert count: [SSL::cert count], SSL sessionid: [SSL::sessionid]"}

# Exit this event if we didn't request a cert
if {$need_cert == 0}{
if {$static::ocsp_debug > 2}{log local0. "$log_prefix: Exiting event as \$need_cert is 0"}
return
}
# Check if client presented a cert after it was requested
if {[SSL::cert count] == 0}{

# No client cert received.  Use -1 to track this (0 will be used to indicate no error by SSL::verify_result)
set ssl_status_code "-1"

# $ssl_status_desc is only used in this rule for debug logging.
set ssl_status_desc "Required client certificate not present for resource."

# The app requirement is to send a 403.17 for cert date issues, 403.13 for OCSP failure and all else 403.7
set app_auth_status 403.7

if {$static::ocsp_debug > 0}{log local0. "$log_prefix: No cert for protected resource. Invalidating session."}
set invalidate_session 1

# Audit logging
if {$static::ocsp_audit_log_level > 0}{catch {log -noname local0. "DC_audit: status=403.17; $log_prefix; status_text=No cert for secured URI; URI=$requested_uri;"}}

} else {

# Client presented at least one cert.  The actual client cert should always be first.
if {$static::ocsp_debug > 1}{

# Loop through each cert and log the cert subject
for {set i 0} {$i < [SSL::cert count]} {incr i}{

log local0. "$log_prefix: cert $i, subject: [X509::subject [SSL::cert $i]], issuer: [X509::issuer [SSL::cert $i]], cert_serial=[X509::serial_number [SSL::cert $i]]"
}
}
if {$static::ocsp_debug > 2}{log local0. "$log_prefix: Received cert with SSL session ID: [SSL::sessionid]. Base64 encoded cert: [b64encode [SSL::cert 0]]"}

# Save the SSL status code (defined here: http://www.openssl.org/docs/apps/verify.html#DIAGNOSTICS)
set ssl_status_code [SSL::verify_result]
set ssl_status_desc [X509::verify_cert_error_string [SSL::verify_result]]

# Check if there was no error in validating the client cert against LTM's server cert
if { $ssl_status_code == 0 }{
if {$static::ocsp_debug > 0}{log local0. "$log_prefix: Certificate validation against root cert OK. status: $ssl_status_desc. Checking against OCSP."}

######################################################################################################
##### If the OCSP responder is an LTM VIP (used for load balancing multiple OCSP servers)
##### use this check here of the OCSP server pool before attempting the OCSP validation.
##### Just change ocsp_pool to the actual OCSP server pool name.

# Check if the OCSP server pool does not have any
if {[active_members $static::ocsp_pool] == 0}{

# OCSP servers are not available!!
log local0.emerg "$log_prefix: OCSP auth pool is down! Resuming SSL handshake and blocking HTTP request."

# Audit logging
if {$static::ocsp_audit_log_level > 0}{
catch {log -noname local0. "cc_audit: $log_prefix; status_text=OCSP server pool is unavailable. Blocking request."}
}

# We could send an HTTP response from this event, but it doesn't actually get sent until
# the CLIENTSSL_HANDSHAKE event anyhow.  So track that this is an invalid request and set the app auth status code
# to indicate OCSP validation of the cert failed.
set app_auth_status 403.7
set invalidate_session 1
SSL::handshake resume
return
}
##### OCSP pool check END:
######################################################################################################

# Check if there isn't already a TMM authentication OCSP session ID
if {$tmm_auth_ssl_ocsp_sid == 0}{

# [AUTH::start pam default_ssl_ocsp] returns an authentication session ID
set tmm_auth_ssl_ocsp_sid [AUTH::start pam default_ssl_ocsp]

if {$static::ocsp_debug > 2}{log local0. "$log_prefix: \$tmm_auth_ssl_ocsp_sid was 0, \$tmm_auth_ssl_ocsp_sid: $tmm_auth_ssl_ocsp_sid"}

if {[info exists tmm_auth_subscription]} {
if {$static::ocsp_debug > 1}{log local0. "$log_prefix: Subscribing to \$tmm_auth_ssl_ocsp_sid: $tmm_auth_ssl_ocsp_sid"}
AUTH::subscribe $tmm_auth_ssl_ocsp_sid
}
}
AUTH::cert_credential $tmm_auth_ssl_ocsp_sid [SSL::cert 0]
AUTH::cert_issuer_credential $tmm_auth_ssl_ocsp_sid [SSL::cert issuer 0]
AUTH::authenticate $tmm_auth_ssl_ocsp_sid

# Hold the SSL handshake until the auth result is returned from OCSP
# The AUTH::authenticate command triggers an OCSP lookup and then the AUTH_RESULT event.
# In AUTH_RESULT, SSL::handshake resume triggers CLIENTSSL_HANDSHAKE.
if {$static::ocsp_debug > 0}{log local0. "$log_prefix: Holding SSL handshake for OCSP check"}
SSL::handshake hold

} else {

# Client cert validation against the CA's root server cert failed.

# Translate the Openssl status code to an HTTP status code for the application
# http://www.openssl.org/docs/apps/verify.html#DIAGNOSTICS
# The app requirement is to send a 403.17 for cert date issues, 403.13 for OCSP failure and all else 403.7
switch -- $ssl_status_code {
"9" -
"10" {
set app_auth_status 403.17
}
"23" {
set app_auth_status 403.13
}
default {
set app_auth_status 403.7
}
}
if {$static::ocsp_debug > 0}{log local0. "$log_prefix: Certificate validation not ok. Status: $ssl_status_code, $ssl_status_desc, $app_auth_status."}

# Audit logging
if {$static::ocsp_audit_log_level > 0}{catch {log -noname local0. "DC_audit: status=$app_auth_status; $log_prefix; status_text=Invalid cert for secured URI; openssl_code=$ssl_status_code; openssl_desc=$ssl_status_desc; cert_subject=[X509::subject [SSL::cert 0]]; cert_issuer=[X509::issuer [SSL::cert 0]]; cert_serial=[X509::serial_number [SSL::cert 0]]; URI=$requested_uri"}}

# Delete the SSL session from the session table
if {$static::ocsp_debug > 1}{log local0. "$log_prefix: Invalidating SSL session [SSL::sessionid]"}
session delete ssl [SSL::sessionid]
SSL::session invalidate
set invalidate_session 1

# Release the request flow as we want to send an HTTP response to clients who don't send a valid cert
if {$static::ocsp_debug > 0}{log local0. "$log_prefix: Invalid cert. Releasing HTTP."}
}
}
}
when AUTH_RESULT {

# AUTH::status values:
# https://devcentral.f5.com/s/wiki/default.aspx/iRules/AUTH__status.html
#  0 = success
#  1 = failure
# -1 = error
#  2 = not-authed

if {$static::ocsp_debug > 0}{log local0. "$log_prefix: \[AUTH::status\]: [AUTH::status]; (0=success, 1=failure, -1=error, 2=not-authed)"}

# Check if there is an existing TMM SSL OCSP session ID
if {[info exists tmm_auth_ssl_ocsp_sid] and ($tmm_auth_ssl_ocsp_sid == [AUTH::last_event_session_id])} {

# Save the auth status
set tmm_auth_status [AUTH::status]

# Use OCSP status?
if {$static::check_ocsp == 0}{
set tmm_auth_status 0
}

# Check if auth was successful
if {$tmm_auth_status == 0 } {

# OCSP auth was successful, so resume the SSL handshake.  This will trigger the CLIENTSSL_HANDSHAKE event next.
if {$static::ocsp_debug > 0}{log local0. "$log_prefix: Valid cert per OCSP. Resuming SSL handshake"}
SSL::handshake resume

} else {

# OCSP auth failed

# Audit logging
if {$static::ocsp_audit_log_level > 0}{
catch {log -noname local0. "DC_audit: status=403.13. $log_prefix; status_text=Invalid cert per OCSP for secured URI; URI=$requested_uri"}
}

# We could send an HTTP response from this event, but it doesn't actually get sent until
# the CLIENTSSL_HANDSHAKE event anyhow.  So track that this is an invalid request and set the app auth status code
# to indicate OCSP validation of the cert failed.
set invalidate_session 1
set app_auth_status 403.17

if {$static::ocsp_debug > 0}{log local0. "$log_prefix: Invalid cert per OCSP. \[AUTH::response_data\]: [AUTH::response_data]. Resuming SSL handshake."}
SSL::handshake resume
}
}
}
when CLIENTSSL_HANDSHAKE {

# This event is triggered when the SSL handshake with the client completes

# Log SSL cipher details
if {$static::ocsp_debug > 2}{log local0. "$log_prefix: Cipher name, version, bits: [SSL::cipher name], [SSL::cipher version], [SSL::cipher bits]"}

# Exit this event if cert isn't required
if {$need_cert == 0}{
if {$static::ocsp_debug > 2}{log local0. "$log_prefix: \$need_cert is 0, exiting event."}
return
}

# Check if OCSP auth was already successful
if {[info exists tmm_auth_status] and $tmm_auth_status == 0}{

if {$static::ocsp_debug > 1}{log local0. "$log_prefix: Auth succeeded, parsing cert fields and adding session table entry."}

# The application expects the following fields:
#IssuerIssuerCN
#IssuerO
#Serial numberSerialNumber
#Valid from dateValidFrom
#Valid to dateValidUntil
#Organisation DetailsSubjectO
#Individual DetailsSubjectOU
#Issued toSubjectCN
#Email addressSubjectEmail

# Parse the IssuerCN and IssuerO.  Use catch to handle any errors.
# Example: CN=OSIS Customer CA,O=Origo Secure Internet Services Ltd.
if {[catch {scan [X509::issuer [SSL::cert 0]] {CN=%[^,], O=%[^,]} IssuerCN IssuerO} result]}{

# Error parsing IssuerCN and IssuerO, log an error and set the values to error strings.
log local0. "$log_prefix: Error parsing IssuerCN and IssuerO from [X509::issuer [SSL::cert 0]], $result"

set IssuerCN "ERROR- Could not parse IssuerCN from [X509::issuer [SSL::cert 0]]"
set IssuerO "ERROR- Could not parse IssuerO from [X509::issuer [SSL::cert 0]]"
}

# Parse the Subject fields
# Example: emailAddress=user@example.com,CN=User Name,OU=post code,OU=Company Name,OU=EmployeeID00,OU=Warning text,OU=CPS - www.example.com/cps,O=FirmID00,C=GB
set SubjectCN ""
set SubjectO ""
if {[catch {scan [X509::subject [SSL::cert 0]]  {emailAddress=%[^,], CN=%[^,], OU=%[^,], OU=%[^,], OU=%[^,], OU=%[^,], OU=%[^,], O=%[^,], C=%[^,]}  SubjectEmail SubjectCN SubjectOU1 SubjectOU2 SubjectOU3 SubjectOU4 SubjectOU5 SubjectO SubjectC} result]}{

# Error parsing Subject, log an error and set the values to error strings.
log local0. "$log_prefix: Error parsing subject from [X509::subject [SSL::cert 0]], $result"

foreach cert_token [list SubjectEmail SubjectCN SubjectOU1 SubjectOU2 SubjectOU3 SubjectOU4 SubjectOU5 SubjectO SubjectC] {
set cert_token "ERROR- Could not parse $cert_token from [X509::subject [SSL::cert 0]]"
}
}

# Add the client cert fields as a list to the session table
if {$static::ocsp_debug > 0}{log local0. "$log_prefix: Saving client cert details in session using SSL sessionid [SSL::sessionid]."}

#session add ssl [SSL::sessionid] [list \
#${static::header_prefix}IssuerCN $IssuerCN \
#${static::header_prefix}IssuerO $IssuerO \
#${static::header_prefix}SerialNumber [X509::serial_number [SSL::cert 0]] \
#${static::header_prefix}ValidFrom [X509::not_valid_before [SSL::cert 0]] \
#${static::header_prefix}ValidUntil [X509::not_valid_after [SSL::cert 0]] \
#${static::header_prefix}SubjectCN $SubjectCN \
#${static::header_prefix}SubjectOU "$SubjectOU1|$SubjectOU2|$SubjectOU3|$SubjectOU4|$SubjectOU5" \
#${static::header_prefix}SubjectO $SubjectO \
#${static::header_prefix}SubjectEmail $SubjectEmail \
#] $static::ocsp_session_timeout

session add ssl [SSL::sessionid] [list ${static::header_prefix}IssuerCN $IssuerCN ${static::header_prefix}IssuerO $IssuerO  ${static::header_prefix}SerialNumber [X509::serial_number [SSL::cert 0]] ${static::header_prefix}ValidFrom [X509::not_valid_before [SSL::cert 0]] ${static::header_prefix}ValidUntil [X509::not_valid_after [SSL::cert 0]]  ${static::header_prefix}SubjectCN $SubjectCN ${static::header_prefix}SubjectOU "$SubjectOU1|$SubjectOU2|$SubjectOU3|$SubjectOU4|$SubjectOU5" ${static::header_prefix}SubjectO $SubjectO  ${static::header_prefix}SubjectEmail $SubjectEmail] $static::ocsp_session_timeout

# Audit logging
if {$static::ocsp_audit_log_level > 1}{
catch {log -noname local0. "DC_audit: status=okay; $log_prefix; status_text=Valid cert per OCSP for secured URI (new SSL session); cert_subject=$SubjectCN; cert_issuer=$IssuerCN; cert_serial=[X509::serial_number [SSL::cert 0]]; URI=$requested_uri"}
}

if {$static::ocsp_debug > 1}{log local0. "$log_prefix: Auth was successful, releasing HTTP"}
HTTP::release

} elseif {$invalidate_session}{

if {$static::ocsp_debug > 0}{log local0. "$log_prefix: No/invalid cert received, sending response. \$app_auth_status: $app_auth_status"}

# Send response to client for invalid request
HTTP::respond 302 Location "${static::auth_failure_url}${app_auth_status}" Connection Close Cache-Control No-Cache Pragma No-Cache
#HTTP::respond 200 content "${static::auth_failure_url}${app_auth_status}" Connection Close Cache-Control No-Cache Pragma No-Cache
HTTP::release
if {[SSL::sessionid] ne ""}{session delete ssl [SSL::sessionid]}
SSL::session invalidate
#TCP::close
} else {
if {$static::ocsp_debug > 2}{log local0. "$log_prefix: default case."}

if {$static::ocsp_debug > 0}{log local0. "$log_prefix: No/invalid cert received, sending response. \$app_auth_status: 403.7"}

# Send response to client for invalid request
#HTTP::respond 302 Location "${static::auth_failure_url}403.7" Connection Close Cache-Control No-Cache Pragma No-Cache
HTTP::respond 200 content "${static::auth_failure_url}403.7" Connection Close Cache-Control No-Cache Pragma No-Cache

HTTP::release
if {[SSL::sessionid] ne ""}{session delete ssl [SSL::sessionid]}
SSL::session invalidate
#TCP::close
}
}
when HTTP_REQUEST {

if {$static::ocsp_debug > 0}{log local0. "$log_prefix: URI: [HTTP::uri], SSL session ID: [SSL::sessionid], session lookup llength: [llength [session lookup ssl [SSL::sessionid]]], string len: [string length [session lookup ssl [SSL::sessionid]]] User-Agent: [HTTP::header User-Agent]"}

# Check if this is a request to a logout URI.
#If so, we'll invalidate the SSL session ID on the response.
if {[matchclass [string tolower [HTTP::uri]] ends_with }{
set logout 1
} else {
set logout 0
}

# Double check that the session is valid
if {[info exists invalidate_session] and $invalidate_session == 1}{

if {$static::ocsp_debug > 0}{log local0. "$log_prefix: Invalidating SSL session ID: [SSL::sessionid]"}
session delete ssl [SSL::sessionid]
SSL::session invalidate
}
# Check if request is to a page which requires a client SSL certificate
if {[matchclass [string tolower [HTTP::path]] starts_with ocsp_pages_to_require_cert_class]}{

# Save the requested URI for logging in subsequent events
set requested_uri [HTTP::uri]

set need_cert 1
if {$static::ocsp_debug > 0}{log local0. "$log_prefix: Request to restricted path: [HTTP::path]. \$need_cert: $need_cert"}

# Check if there is an existing SSL session ID and if the cert is in the session table
#This condition should only be true on resumed SSL sessions.
#if {[SSL::sessionid] ne $static::ocsp_null_sessionid and [SSL::sessionid] ne "" and !([catch {session lookup ssl [SSL::sessionid]} ssl_session_data]) and $ssl_session_data ne ""}
if {[SSL::sessionid] ne $static::ocsp_null_sessionid and [session lookup ssl [SSL::sessionid]] ne ""}{

if {$static::ocsp_debug > 0}{
log local0. "$log_prefix: Allowed request to [HTTP::host][HTTP::uri]. Inserting SSL cert details in HTTP headers."

# Debug logging of each session table list item
if {$static::ocsp_debug > 2}{
foreach session_element [session lookup ssl [SSL::sessionid]] {
log local0. "$log_prefix: $session_element"
}
}
}

# Remove any HTTP header which starts with "crt_"
foreach a_header [HTTP::header names] {

# Check if this header name starts with "crt_"
if {[string match -nocase ${static::header_prefix}* $a_header]}{
HTTP::header remove $a_header

# If there is a header which starts with crt_, it is probably someone attacking the application!
log local0.emerg "$log_prefix: Client with possible spoofed client cert header [HTTP::request]"
}
}

# Insert SSL cert details in the HTTP headers
# HTTP::header insert $ssl_session_data
HTTP::header insert [session lookup ssl [SSL::sessionid]]

# Track that we've inserted the HTTP headers, so we don't do it again in HTTP_REQUEST_SEND
set inserted_headers 1

if {$static::ocsp_debug > 0}{log local0. "$log_prefix: Valid request"}

# Audit logging
if {$static::ocsp_audit_log_level > 1}{

# Get the cert details from the session table for the audit logging
set session_list [session lookup ssl [SSL::sessionid]]

catch {log -noname local0. "DC_audit: status=okay; $log_prefix; status_text=Valid cert per OCSP for secured URI (resumed SSL session); cert_subject=[lindex $session_list 11]; cert_issuer=[lindex $session_list 1]; cert_serial=[lindex $session_list 5]; URI=$requested_uri"}
}

} else {

# Hold the HTTP request until the SSL re-negotiation is complete
HTTP::collect

# Force renegotiation of the SSL connection with a cert requested
SSL::session invalidate
SSL::authenticate always
SSL::authenticate depth 9
SSL::cert mode request
SSL::renegotiate

if {$static::ocsp_debug > 0}{log local0. "$log_prefix: Restricted path, [HTTP::uri], with no client cert. Collecting HTTP and renegotiating SSL handshake to request client cert."}
}
} else {
if {$static::ocsp_debug > 1}{log local0. "$log_prefix: Request to unrestricted path: [HTTP::path]"}
set need_cert 0
}
}
when HTTP_REQUEST_SEND {

# This event is relevant only on the initial request of a secured URI (non-resumed SSL sessions).
# The insertion of cert details for resumed SSL sessions is handled in HTTP_REQUEST.

# Force evaluation in clientside context as HTTP_REQUEST_SEND is a serverside event
clientside {

if {$static::ocsp_debug > 0}{log local0. "$log_prefix: \$invalidate_session: $invalidate_session, \$need_cert: $need_cert, \[SSL::sessionid\]: [SSL::sessionid], \[session lookup ssl \[SSL::sessionid\]\]: [session lookup ssl [SSL::sessionid]], URI: [clientside {HTTP::uri}]"}

# Check if request was to a restricted URI and the headers weren't inserted already in HTTP_REQUEST
if {$need_cert==1 and $inserted_headers==0}{

# Check if the session is still valid, there is an existing SSL session ID and that the cert is in the session table
if {$invalidate_session == 0 and [SSL::sessionid] ne $static::ocsp_null_sessionid and [session lookup ssl [SSL::sessionid]] ne ""}{

# Remove any HTTP header which starts with "crt_"
foreach a_header [HTTP::header names] {

# Check if this header name starts with "crt_"
if {[string match -nocase ${static::header_prefix}* $a_header]}{

HTTP::header remove $a_header

# If there is a header which starts with crt_, it is probably someone attacking the application!
log local0.emerg "$log_prefix: Client with possible spoofed client cert header [HTTP::host][HTTP::uri], [HTTP::header User-Agent"]"
}
}

if {$static::ocsp_debug > 0}{log local0. "$log_prefix: Inserting SSL cert details in HTTP headers."}

# Insert SSL cert details from the session table in the HTTP headers
HTTP::header insert [session lookup ssl [SSL::sessionid]]

} else {

# Client request for secured URI wasn't valid
log local0. "$log_prefix: Rejection connection for invalid request to [HTTP::host][HTTP::uri] ([IP::local_addr]:[TCP::local_port]) with session ID: [SSL::sessionid]"

# Reject the connection as we should never get here
reject
}
}
}
}
when HTTP_RESPONSE {

# Check if we're invalidating the SSL session ID for logout requests
if {$logout}{
if {$static::ocsp_debug > 0}{log local0. "$log_prefix: Invalidating SSL session ID: [SSL::sessionid] for logout."}
session delete ssl [SSL::sessionid]
SSL::session invalidate
}
}
Published Mar 17, 2015
Version 1.0
No CommentsBe the first to comment