How we at ZeroTech made Apple Safari friends and client certificates with websockets

The article will be useful to those who:



  • knows what Client Cert is, and understands why he needs websockets on mobile Safari;
  • would like to publish web services to a limited circle of people or only to myself;
  • He thinks that everything has already been done by someone, and would like to make the world a little more convenient and secure.


The history of web sockets began about 8 years ago. Previously, methods were used in the form of long http-requests (in fact, responses): the user's browser sent a request to the server and waited for it to respond to it, after the response it connected again and waited. But then websockets came along.







Several years ago, we developed our own pure php implementation, which does not know how to use https requests, since this is the data link layer. Not so long ago, almost all web servers learned to proxy requests via https and support connection: upgrade.



When this happened, web sockets became almost the default service for SPA applications, because it’s convenient to provide the user with content on the server’s initiative (send a message from another user or download a new version of an image, document, presentation that someone else is editing now) ...



Although Client Cert has been around for quite some time, it still remains poorly supported, as it creates a lot of problems with trying to get around it. And (possibly: slightly_smiling_face :), so IOS browsers (everyone except Safari) do not want to use it and request it from the local certificate store. Certificates have many advantages over login / pass or ssh keys or firewalling the correct ports. But this is not about that.



On iOS, the procedure for installing a certificate is quite simple (not without specifics), but in general it is done according to the instructions, which are very numerous on the network and which are available only for the Safari browser. Unfortunately, Safari does not know how to use Client ert for web sockets, but there are many instructions on the Internet on how to make such a certificate, but in practice this is unattainable.







To understand web sockets, we used the following plan: problem / hypothesis / solution.



Problem: There is no support for web sockets when proxying requests to resources that are protected by a client certificate on the mobile Safari browser for IOS and other applications that have included certificate support.



Hypotheses:



  1. It is possible to configure such an exception to use certificates (knowing that they will not be available) to web sockets of internal / external proxied resources.
  2. For web sockets, you can make a unique secure and secure connection using temporary sessions that are generated by a normal (non-web socket) browser request.
  3. Transient sessions can be implemented using a single proxy web server (built-in modules and functions only).
  4. Temporary session tokens have already been implemented as ready-made apache modules.
  5. Temporary session tokens can be implemented by logically designing the interaction structure.


Visible state after deployment.



Purpose of work: the management of services and infrastructure should be accessible from a mobile phone to IOS without additional programs (such as VPN), unified and secure.



Additional goal: saving time and resources / phone traffic (some services without web sockets generate unnecessary requests) while accelerating content delivery on the mobile Internet.



How to check?



1. Opening pages:



— , https://teamcity.yourdomain.com    Safari (    ) —     -.
— , https://teamcity.yourdomain.com/admin/admin.html?item=diagnostics&tab=webS…—  ping/pong.
— , https://rancher.yourdomain.com/p/c-84bnv:p-vkszd/workload/deployment:danidb:ph…-> viewlogs —   .


2. Or in the developer's console:







Testing hypotheses:



1. It is possible to configure such an exception to use certificates (knowing that they will not be available) to web sockets of internal / external proxied resources.



Here 2 solutions were found:



a) At the level of



<Location sock*> SSLVerifyClient optional </Location>
<Location /> SSLVerifyClient require </Location>


change the access level.



This method has the following nuances:



  • The certificate is checked after a request to the proxied resource, that is, post request handshake. This means that the proxy will first load and then cut off the request to the protected service. This is bad, but not critical;
  • In the http2 protocol. It is still in draft, and browser manufacturers do not know how to implement it #info about tls1.3 http2 post handshake (not working now) Implement RFC 8740 "Using TLS 1.3 with HTTP / 2" ;
  • It is not clear how to unify this processing.


b) At a basic level, enable ssl without a certificate.



SSLVerifyClient require => SSLVerifyClient optional, but this reduces the level of protection of the proxy server, since such a connection will be processed without a certificate. However, you can further deny access to proxied services with the following directive:



RewriteEngine        on
RewriteCond     %{SSL:SSL_CLIENT_VERIFY} !=SUCCESS
RewriteRule     .? - [F]
ErrorDocument 403 "You need a client side certificate issued by CAcert to access this site"


For more information, see the article about ssl: Apache Server Client Certificate Authentication



Both options were tested, option "b" was chosen for universality and compatibility with the http2 protocol.



To complete the verification of this hypothesis, it took a lot of experiments with the configuration, the constructions were tested:



if = require = rewrite



We got the following basic construction:
SSLVerifyClient optional
RewriteEngine on
RewriteCond %{SSL:SSL_CLIENT_VERIFY} !=SUCCESS
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
RewriteRule     .? - [F]
#ErrorDocument 403 "You need a client side certificate issued by CAcert to access this site"

#websocket for safari without cert auth
<If "%{SSL:SSL_CLIENT_VERIFY} != 'SUCCESS'">
<If "%{HTTP:Upgrade} = 'websocket'">
...
    #         
    SSLUserName SSl_PROTOCOL
</If>
</If>




Given the existing authorization for the certificate holder, but with a missing certificate, I had to add a nonexistent certificate holder as one of the available SSl_PROTOCOL variables (instead of SSL_CLIENT_S_DN_CN), more in the documentation:



Apache Module mod_ssl







2. For web sockets, you can make a unique safe and secure connection to using temporary sessions that are generated during a regular (not a web socket) browser request.



Based on previous experience, you need to add an additional section to the configuration in order to prepare temporary tokens for a web socket connection with a normal (not a web socket) request.



#   ookie   
<If "%{SSL:SSL_CLIENT_VERIFY} = 'SUCCESS'">
<If "%{HTTP:Upgrade} != 'websocket'">
Header set Set-Cookie "websocket-allowed=true; path=/; Max-Age=100"
</If>
</If>

# Cookie   - 
<source lang="javascript">
<If "%{SSL:SSL_CLIENT_VERIFY} != 'SUCCESS'">
<If "%{HTTP:Upgrade} = 'websocket'">
#check for exists cookie

#get and check
SetEnvIf Cookie "websocket-allowed=(.*)" env-var-name=$1

#or rewrite rule
RewriteCond %{HTTP_COOKIE} !^.*mycookie.*$

#or if
<If "%{HTTP_COOKIE} =~ /(^|; )cookie-name\s*=\s*some-val(;|$)/ >
</If

</If>
</If>


Testing has shown that it works. It is possible to transmit a cookie to the user's browser.



3. Transient sessions can be implemented using one proxy web server (only built-in modules and functions).



As we found out earlier, Apache has quite a lot of core functionality that allows you to create conditionals. However, we need a means of protecting our information while it is in the user's browser, so we set what and for what to store, and what built-in functions we will use:



  • We need a token that defies simple decoding.
  • You need a token that has obsolescence and the ability to check for obsolescence on the server.
  • We need a token that will be associated with the owner of the certificate.


This requires a hash function, salt and a date to expire the token. Based on the Expressions in Apache HTTP Server documentation , we have all this out of the box sha1 and% {TIME}.



The result is such a design:
# ,    websocket
<If "%{SSL:SSL_CLIENT_VERIFY} != 'SUCCESS'">
<If "%{HTTP:Upgrade} = 'websocket'">
    SetEnvIf Cookie "zt-cert-sha1=([^;]+)" zt-cert-sha1=$1
    SetEnvIf Cookie "zt-cert-uid=([^;]+)" zt-cert-uid=$1
    SetEnvIf Cookie "zt-cert-date=([^;]+)" zt-cert-date=$1

#     ,   env-    ,         (  ,   ,     )
    <RequireAll>
        Require expr %{sha1:salt1%{env:zt-cert-date}salt3%{env:zt-cert-uid}salt2} == %{env:zt-cert-sha1}
        Require expr %{env:zt-cert-sha1} =~ /^.{40}$/
    </RequireAll>
</If>
</If>

# ,   websocket
<If "%{SSL:SSL_CLIENT_VERIFY} = 'SUCCESS'">
<If "%{HTTP:Upgrade} != 'websocket'">
    SetEnvIf Cookie "zt-cert-sha1=([^;]+)" HAVE_zt-cert-sha1=$1

    SetEnv zt_cert "path=/; HttpOnly;Secure;SameSite=Strict"
#  ,   
    Header add Set-Cookie "expr=zt-cert-sha1=%{sha1:salt1%{TIME}salt3%{SSL_CLIENT_S_DN_CN}salt2};%{env:zt_cert}" env=!HAVE_zt-cert-sha1
    Header add Set-Cookie "expr=zt-cert-uid=%{SSL_CLIENT_S_DN_CN};%{env:zt_cert}" env=!HAVE_zt-cert-sha1
    Header add Set-Cookie "expr=zt-cert-date=%{TIME};%{env:zt_cert}" env=!HAVE_zt-cert-sha1
</If>
</If>




The goal has been achieved, but there are problems with server obsolescence (you can use a cookie a year ago), which means tokens, although safe for internal use, but unsafe for industrial (mass).







4. Temporary session tokens have already been implemented as ready-made Apache modules.



From the previous iteration, one significant problem remained - the inability to control the expiration of the token.



We are looking for a ready-made module that does this, according to the words: apache token json two factor auth





Yes, there are ready-made modules, but all are tied to specific actions and have artifacts in the form of a session start and additional Cookies. That is, not for a while.

It took us five hours to search, which did not produce any concrete result.



5. Temporary session-tokens can be implemented by logically designing the structure of interactions.



The ready-made modules are too complicated because we only need a couple of functions.



At the same time, the problem with the date is that the Apache built-in functions do not allow generating a date from the future, and when checking for obsolescence in built-in functions there is no mathematical addition / subtraction.



That is, you can not write:



(%{env:zt-cert-date} + 30) > %{DATE}


Only two numbers can be compared.



While looking for a workaround for the Safari problem, an interesting article was found: Securing HomeAssistant with client certificates (works with Safari / iOS)

It describes an example of Lua code for Nginx, and which, as it turned out, very much repeats the logic of that part of the configuration that we already implemented earlier. except for the use of the hmac-method of arranging the salt for hashing (this was not found in Apache).



It became clear that Lua is a language with clear logic, it is possible to do something simple for Apache:





Having studied the difference with Nginx and Apache:





And the available functions from the manufacturer of the Lua language:

22.1 - Date and Time



Found a way to set env variables in a small Lua file in order to set a date from the future to check with the current one.



This is how a simple Lua script looks like:
require 'apache2'

function handler(r)
    local fmt = '%Y%m%d%H%M%S'
    local timeout = 3600 -- 1 hour

    r.notes['zt-cert-timeout'] = timeout
    r.notes['zt-cert-date-next'] = os.date(fmt,os.time()+timeout)
    r.notes['zt-cert-date-halfnext'] = os.date(fmt,os.time()+ (timeout/2))
    r.notes['zt-cert-date-now'] = os.date(fmt,os.time())

    return apache2.OK
end




And so it all works in total, with optimization of the number of cookies and replacing the token when half the time has passed before the expiration of the old cookies (token):
SSLVerifyClient optional

#LuaScope thread
#generate event variables zt-cert-date-next
LuaHookAccessChecker /usr/local/etc/apache24/sslincludes/websocket_token.lua handler early

#   - ,  webscoket
RewriteEngine on
RewriteCond %{SSL:SSL_CLIENT_VERIFY} !=SUCCESS
RewriteCond %{HTTP:Upgrade} !=websocket [NC]
RewriteRule     .? - [F]
#ErrorDocument 403 "You need a client side certificate issued by CAcert to access this site"

#websocket for safari without certauth
<If "%{SSL:SSL_CLIENT_VERIFY} != 'SUCCESS'">
<If "%{HTTP:Upgrade} = 'websocket'">
    SetEnvIf Cookie "zt-cert=([^,;]+),([^,;]+),[^,;]+,([^,;]+)" zt-cert-sha1=$1 zt-cert-date=$2 zt-cert-uid=$3

    <RequireAll>
        Require expr %{sha1:salt1%{env:zt-cert-date}salt3%{env:zt-cert-uid}salt2} == %{env:zt-cert-sha1}
        Require expr %{env:zt-cert-sha1} =~ /^.{40}$/
        Require expr %{env:zt-cert-date} -ge %{env:zt-cert-date-now}
    </RequireAll>
   
    #         
    SSLUserName SSl_PROTOCOL
    SSLOptions -FakeBasicAuth
</If>
</If>

<If "%{SSL:SSL_CLIENT_VERIFY} = 'SUCCESS'">
<If "%{HTTP:Upgrade} != 'websocket'">
    SetEnvIf Cookie "zt-cert=([^,;]+),[^,;]+,([^,;]+)" HAVE_zt-cert-sha1=$1 HAVE_zt-cert-date-halfnow=$2
    SetEnvIfExpr "env('HAVE_zt-cert-date-halfnow') -ge %{TIME} && env('HAVE_zt-cert-sha1')=~/.{40}/" HAVE_zt-cert-sha1-found=1

    Define zt-cert "path=/;Max-Age=%{env:zt-cert-timeout};HttpOnly;Secure;SameSite=Strict"
    Define dates_user "%{env:zt-cert-date-next},%{env:zt-cert-date-halfnext},%{SSL_CLIENT_S_DN_CN}"
    Header set Set-Cookie "expr=zt-cert=%{sha1:salt1%{env:zt-cert-date-next}sal3%{SSL_CLIENT_S_DN_CN}salt2},${dates_user};${zt-cert}" env=!HAVE_zt-cert-sha1-found
</If>
</If>

SetEnvIfExpr "env('HAVE_zt-cert-date-halfnow') -ge %{TIME} && env('HAVE_zt-cert-sha1')=~/.{40}/" HAVE_zt-cert-sha1-found=1
,

    
SetEnvIfExpr "env('HAVE_zt-cert-date-halfnow') -ge  env('zt-cert-date-now') && env('HAVE_zt-cert-sha1')=~/.{40}/" HAVE_zt-cert-sha1-found=1 




Because LuaHookAccessChecker will be activated only after access checks based on this information from Nginx.







Link to the image source .



One more point.



In general, it doesn’t matter in what order the directives are written in the Apache configuration (probably Nginx), as in the end everything will be sorted based on the order of the request from the user, which corresponds to the scheme for working out Lua-scripts.



Completion:



Visible state after implementation (goal):

service and infrastructure management is available from a mobile phone on IOS without additional programs (VPN), unified and secure.



The goal is achieved, web sockets work and have no less security than a certificate.






All Articles