Wie wir bei ZeroTech Apple Safari und Client-Zertifikate mit Websockets verbunden haben

Статья будет полезна тем, кто:

  • weiß, was Client Cert ist und warum es Websockets in der mobilen Safari benötigt;
  • Ich möchte Webdienste für einen begrenzten Personenkreis oder nur für mich selbst veröffentlichen;
  • denkt, dass alles schon von jemandem erledigt wurde und möchte die Welt ein wenig bequemer und sicherer machen.

Die Geschichte der Websockets begann vor etwa 8 Jahren. Bisher wurden Methoden in Form von langen HTTP-Anfragen (eigentlich Antworten) verwendet: Der Browser des Benutzers sendete eine Anfrage an den Server und wartete darauf, dass dieser etwas antwortet. Nach der Antwort stellte er erneut eine Verbindung her und wartete. Doch dann tauchten Websockets auf.

Wie wir bei ZeroTech Apple Safari und Client-Zertifikate mit Websockets verbunden haben

Vor einigen Jahren haben wir eine eigene Implementierung in reinem PHP entwickelt, die keine https-Anfragen verwenden kann, da dies die Linkschicht ist. Vor nicht allzu langer Zeit haben fast alle Webserver gelernt, Anfragen über https weiterzuleiten und Connection:Upgrade zu unterstützen.

Als dies geschah, wurden Websockets fast zum Standarddienst für SPA-Anwendungen, weil es so praktisch ist, dem Benutzer Inhalte auf Initiative des Servers bereitzustellen (eine Nachricht von einem anderen Benutzer übertragen oder eine neue Version eines Bildes, Dokuments oder einer Präsentation herunterladen). dass jemand anderes gerade bearbeitet).

Obwohl es das Client-Zertifikat schon seit geraumer Zeit gibt, wird es immer noch nur unzureichend unterstützt, da es beim Versuch, es zu umgehen, viele Probleme verursacht. Und (möglicherweise :slightly_smiling_face: ) deshalb wollen IOS-Browser (alle außer Safari) es nicht verwenden und fordern es vom lokalen Zertifikatsspeicher an. Zertifikate haben viele Vorteile im Vergleich zu Login/Pass oder SSH-Schlüsseln oder dem Schließen der erforderlichen Ports durch eine Firewall. Aber darum geht es hier nicht.

Unter iOS ist das Verfahren zum Installieren eines Zertifikats recht einfach (nicht ohne Besonderheiten), aber im Allgemeinen erfolgt es nach Anweisungen, von denen es im Internet viele gibt und die nur für den Safari-Browser verfügbar sind. Leider weiß Safari nicht, wie man Client Сert für Web-Sockets verwendet, aber es gibt im Internet viele Anleitungen, wie man ein solches Zertifikat erstellt, aber in der Praxis ist dies unerreichbar.

Wie wir bei ZeroTech Apple Safari und Client-Zertifikate mit Websockets verbunden haben

Um Websockets zu verstehen, haben wir den folgenden Plan verwendet: Problem/Hypothese/Lösung.

Problem: Es gibt keine Unterstützung für Web-Sockets beim Weiterleiten von Anfragen an Ressourcen, die durch ein Client-Zertifikat im Safari-Mobilbrowser für IOS und andere Anwendungen geschützt sind, die die Zertifikatunterstützung aktiviert haben.

Hypothesen:

  1. Es ist möglich, eine solche Ausnahme zu konfigurieren, um Zertifikate (in dem Wissen, dass keine vorhanden sind) für Websockets interner/externer Proxy-Ressourcen zu verwenden.
  2. Für Websockets können Sie mithilfe temporärer Sitzungen, die während einer normalen (nicht Websocket-)Browseranfrage generiert werden, eine eindeutige, sichere und vertretbare Verbindung herstellen.
  3. Temporäre Sitzungen können über einen Proxy-Webserver implementiert werden (nur integrierte Module und Funktionen).
  4. Temporäre Sitzungstoken wurden bereits als vorgefertigte Apache-Module implementiert.
  5. Temporäre Sitzungstoken können durch logische Gestaltung der Interaktionsstruktur implementiert werden.

Sichtbarer Zustand nach der Implementierung.

Zielsetzung: Die Verwaltung von Diensten und Infrastruktur sollte von einem Mobiltelefon aus unter IOS ohne zusätzliche Programme (z. B. VPN) einheitlich und sicher zugänglich sein.

Zusätzliches Ziel: Einsparung von Zeit und Ressourcen/Telefonverkehr (einige Dienste ohne Web-Sockets generieren unnötige Anfragen) durch schnellere Bereitstellung von Inhalten im mobilen Internet.

Wie überprüft man?

1. Seiten öffnen:

— например, 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. Oder in der Entwicklerkonsole:

Wie wir bei ZeroTech Apple Safari und Client-Zertifikate mit Websockets verbunden haben

Hypothesentest:

1. Es ist möglich, eine solche Ausnahme zu konfigurieren, um Zertifikate (in dem Wissen, dass keine vorhanden sind) für Web-Sockets interner/externer Proxy-Ressourcen zu verwenden.

Hier wurden 2 Lösungen gefunden:

a) Auf der Ebene

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

Zugriffsebene ändern.

Diese Methode hat die folgenden Nuancen:

  • Die Zertifikatsüberprüfung erfolgt nach einer Anfrage an die Proxy-Ressource, also nach dem Handshake nach der Anfrage. Das bedeutet, dass der Proxy die Anfrage an den geschützten Dienst zunächst lädt und dann unterbricht. Das ist schlecht, aber nicht kritisch;
  • Im http2-Protokoll. Es befindet sich noch im Entwurf und Browserhersteller wissen nicht, wie sie es implementieren sollen. #Info über tls1.3 http2 Post Handshake (funktioniert derzeit nicht) Implementieren Sie RFC 8740 „Verwenden von TLS 1.3 mit HTTP/2“;
  • Es ist nicht klar, wie diese Verarbeitung vereinheitlicht werden kann.

b) Erlauben Sie grundsätzlich SSL ohne Zertifikat.

SSLVerifyClient require => SSLVerifyClient optional, allerdings verringert sich dadurch die Sicherheitsstufe des Proxyservers, da eine solche Verbindung ohne Zertifikat verarbeitet wird. Sie können den Zugriff auf Proxy-Dienste jedoch mit der folgenden Anweisung weiter verweigern:

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"

Genauere Informationen finden Sie im Artikel über SSL: Authentifizierung des Apache-Server-Client-Zertifikats

Beide Optionen wurden getestet, Option „b“ wurde aufgrund ihrer Vielseitigkeit und Kompatibilität mit dem http2-Protokoll ausgewählt.

Um die Verifizierung dieser Hypothese abzuschließen, waren zahlreiche Experimente mit der Konfiguration erforderlich; die folgenden Designs wurden getestet:

if = require = umschreiben

Das Ergebnis ist folgender Grundentwurf:

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>

Unter Berücksichtigung der vorhandenen Autorisierung durch den Zertifikatsinhaber, aber bei fehlendem Zertifikat, musste ich einen nicht vorhandenen Zertifikatsinhaber in Form einer der verfügbaren Variablen SSl_PROTOCOL (anstelle von SSL_CLIENT_S_DN_CN) hinzufügen, weitere Details in der Dokumentation:

Apache-Modul mod_ssl

Wie wir bei ZeroTech Apple Safari und Client-Zertifikate mit Websockets verbunden haben

2. Für Websockets können Sie mithilfe temporärer Sitzungen, die während einer normalen (nicht Websocket-)Browseranfrage generiert werden, eine eindeutige, sichere und geschützte Verbindung herstellen.

Aufgrund früherer Erfahrungen müssen Sie der Konfiguration einen zusätzlichen Abschnitt hinzufügen, um temporäre Token für Web-Socket-Verbindungen während einer regulären (nicht Web-Socket-)Anfrage vorzubereiten.

#подготовка передача себе С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>

Tests haben gezeigt, dass es funktioniert. Es ist möglich, Cookies über den Browser des Benutzers an Sie selbst zu übertragen.

3. Temporäre Sitzungen können über einen Proxy-Webserver implementiert werden (nur integrierte Module und Funktionen).

Wie wir bereits herausgefunden haben, verfügt Apache über zahlreiche Kernfunktionen, mit denen Sie bedingte Konstrukte erstellen können. Wir benötigen jedoch Mittel, um unsere Informationen zu schützen, während sie sich im Browser des Benutzers befinden. Daher legen wir fest, was wir speichern und warum und welche integrierten Funktionen wir verwenden werden:

  • Wir brauchen einen Token, der nicht einfach entschlüsselt werden kann.
  • Wir benötigen einen Token, in den die Obsoleszenz integriert ist und der die Möglichkeit bietet, die Obsoleszenz auf dem Server zu überprüfen.
  • Wir benötigen einen Token, der dem Besitzer des Zertifikats zugeordnet wird.

Dies erfordert eine Hashing-Funktion, ein Salt und ein Datum zum Altern des Tokens. Basierend auf der Dokumentation Ausdrücke im Apache HTTP Server Wir haben alles sofort einsatzbereit, sha1 und %{TIME}.

Das Ergebnis war dieser Entwurf:

#нет сертификата, и обращение к 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>

Das Ziel wurde erreicht, es gibt jedoch Probleme mit der Serververalterung (Sie können ein ein Jahr altes Cookie verwenden), was bedeutet, dass die Token zwar für den internen Gebrauch sicher, für den industriellen (Massen-)Gebrauch jedoch unsicher sind.

Wie wir bei ZeroTech Apple Safari und Client-Zertifikate mit Websockets verbunden haben

4. Temporäre Sitzungstoken wurden bereits als vorgefertigte Apache-Module implementiert.

Ein wesentliches Problem blieb aus der vorherigen Iteration bestehen – die Unfähigkeit, die Token-Alterung zu kontrollieren.

Wir suchen ein fertiges Modul, das dies tut, nach den Worten: Apache Token JSON Two Factor Auth

Ja, es gibt vorgefertigte Module, aber alle sind an bestimmte Aktionen gebunden und verfügen über Artefakte in Form des Startens einer Sitzung und zusätzlicher Cookies. Das heißt, für eine Weile nicht.
Die Suche dauerte fünf Stunden, brachte aber kein konkretes Ergebnis.

5. Temporäre Sitzungstoken können durch logische Gestaltung der Interaktionsstruktur implementiert werden.

Vorgefertigte Module sind zu komplex, da wir nur ein paar Funktionen benötigen.

Allerdings besteht das Problem mit dem Datum darin, dass die integrierten Funktionen von Apache die Generierung eines Datums aus der Zukunft nicht zulassen und es in den integrierten Funktionen keine mathematische Addition/Subtraktion bei der Überprüfung auf Veralterung gibt.

Das heißt, Sie können nicht schreiben:

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

Sie können nur zwei Zahlen vergleichen.

Auf der Suche nach einem Workaround für das Safari-Problem bin ich auf einen interessanten Artikel gestoßen: HomeAssistant mit Client-Zertifikaten sichern (funktioniert mit Safari/iOS)
Es beschreibt ein Beispiel für Code in Lua für Nginx, der, wie sich herausstellte, die Logik des Teils der Konfiguration, den wir bereits implementiert haben, weitgehend wiederholt, mit Ausnahme der Verwendung der hmac-Salting-Methode für das Hashing ( Dies wurde in Apache nicht gefunden.

Es wurde deutlich, dass Lua eine Sprache mit klarer Logik ist und es möglich ist, etwas Einfaches für Apache zu tun:

Nachdem ich den Unterschied zwischen Nginx und Apache untersucht habe:

Und verfügbare Funktionen des Lua-Sprachherstellers:
22.1 – Datum und Uhrzeit

Wir haben eine Möglichkeit gefunden, Umgebungsvariablen in einer kleinen Lua-Datei festzulegen, um ein Datum in der Zukunft festzulegen, das mit dem aktuellen Datum verglichen werden kann.

So sieht ein einfaches Lua-Skript aus:

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

Und so funktioniert das Ganze insgesamt, mit Optimierung der Anzahl der Cookies und Austausch des Tokens, wenn die Hälfte der Zeit vergeht, bis das alte Cookie (Token) abläuft:

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 

Denn LuaHookAccessChecker wird erst nach Zugriffsprüfungen basierend auf diesen Informationen von Nginx aktiviert.

Wie wir bei ZeroTech Apple Safari und Client-Zertifikate mit Websockets verbunden haben

Link zur Quelle Bild.

Ein weiterer Punkt.

Im Allgemeinen spielt es keine Rolle, in welcher Reihenfolge die Anweisungen in der Apache- (wahrscheinlich auch Nginx-)Konfiguration geschrieben werden, da am Ende alles nach der Reihenfolge der Anfrage des Benutzers sortiert wird, was dem Schema für die Verarbeitung entspricht Lua-Skripte.

Fertigstellung:

Sichtbarer Zustand nach Umsetzung (Ziel):
Die Verwaltung von Diensten und Infrastruktur ist von einem Mobiltelefon auf IOS ohne zusätzliche Programme (VPN) einheitlich und sicher möglich.

Das Ziel wurde erreicht, Web-Sockets funktionieren und verfügen über ein Sicherheitsniveau, das nicht geringer ist als ein Zertifikat.

Wie wir bei ZeroTech Apple Safari und Client-Zertifikate mit Websockets verbunden haben

Source: habr.com

Kommentar hinzufügen