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

The article will be useful to those who:

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

The history of web sockets started about 8 years ago. Previously, methods like long http requests (actually responses) were used: the user's browser sent a request to the server and waited for it to answer something, after the response it reconnected and waited. But then web sockets appeared.

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

A few years ago, we developed our own implementation in pure php, which does not know how to use https requests, since it is a link layer. Not so long ago, almost all web servers learned to proxy requests over https and support connection:upgrade.

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

Although Client Π‘ert has been around for quite some time, it still remains little supported, as it creates a lot of problems with attempts to bypass it. And (perhaps :slightly_smiling_face: ) that's why IOS browsers (everything except Safari) don't want to use it and request it from the local certificate store. Certificates have a lot of advantages over login / pass or ssh keys or closing the necessary ports through the firewall. But it's 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 instructions, which are very numerous on the web and which are only available for the Safari browser. Unfortunately, Safari doesn't 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.

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

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 Safari mobile browser for IOS and other applications that have enabled certificate support.

Hypotheses:

  1. It is possible to set up such an exception to use certificates (knowing that there won't be any) 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. Temporary sessions can be implemented using a single proxy web server (only built-in modules and functions).
  4. Temporary session tokens have already been implemented as ready-made apache modules.
  5. Temporary session tokens can be implemented by logically designing the structure of interactions.

Visible state after implementation.

Objective: service and infrastructure management should be accessible from a mobile phone on 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) with faster 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 console:

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

Hypothesis testing:

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

There are 2 solutions found here:

a) at the level

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

change access level.

This method has the following nuances:

  • The certificate verification occurs after a request to the proxied resource, that is, a 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's still in draft and browser vendors don't 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, allow ssl without a certificate.

SSLVerifyClient require => SSLVerifyClient optional, but this lowers the protection level 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 ssl article: Apache Server Client Certificate Authentication

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

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

if = require = rewrite

The result is the following basic structure:

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>

Taking into account the existing authorization by the certificate owner, but with the missing certificate, I had to add a non-existent certificate owner in the form of one of the available variables SSl_PROTOCOL (instead of SSL_CLIENT_S_DN_CN), more details in the documentation:

Apache Module mod_ssl

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

2. For web sockets, you can make a unique secure and secure connection using temporary sessions that are generated during a normal (non-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 web socket connections on a normal (non-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-names*=s*some-val(;|$)/ >
</If

</If>
</If>

Testing has shown that it works. It is possible to send a Cookie through the user's browser.

3. Temporary sessions can be implemented using a single 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 conditional constructs. However, we need means to protect our information while it is in the user's browser, so we set what and why to store, and what built-in functions we will use:

  • We need a token that cannot be easily decoded.
  • You need a token that has aging hardcoded and the ability to check for aging on the server.
  • We need a token that will be associated with the owner of the certificate.

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

The resulting design is:

#Π½Π΅Ρ‚ сСртификата, ΠΈ ΠΎΠ±Ρ€Π°Ρ‰Π΅Π½ΠΈΠ΅ ΠΊ 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 is achieved, but there are problems with server obsolescence (you can use Cookies from a year ago), which means that tokens, although safe for internal use, are not safe for industrial (mass) use.

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

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

One significant problem remained from the previous iteration - the inability to control the obsolescence 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 session start and additional Cookies. That is not on time.
It took us five hours to search, which did not give a concrete result.

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

Ready-made modules are too complicated, because we only need a couple of functions.

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

That is, you cannot write:

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

You can only compare two numbers.

While looking for a workaround for the Safari issue, I came across an interesting article: Securing HomeAssistant with client certificates (works with Safari/iOS)
It describes an example of Lua code for Nginx, which, as it turned out, very much repeats the logic of that part of the configuration that we have already implemented earlier, with the exception of using the hmac salting method 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:

After learning the difference with Nginx and Apache:

And the available functions from the Lua language maker:
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 against the current one.

This is what 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 this is how it all works in sum, with the optimization of the number of Cookies and the replacement of the token at half time 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 only be activated after access checks based on this information from Nginx.

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

Link to source Image.

Another point.

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

Completion:

Visible state after implementation (target):
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.

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

Source: habr.com

Add a comment