Hur vi på ZeroTech kopplade ihop Apple Safari och klientcertifikat med websockets

Artikeln kommer att vara användbar för dem som:

  • vet vad Client Cert är och förstår varför det behöver websockets på mobil Safari;
  • Jag skulle vilja publicera webbtjänster till en begränsad krets av människor eller bara till mig själv;
  • tror att allt redan har gjorts av någon, och skulle vilja göra världen lite bekvämare och säkrare.

Websockets historia började för cirka 8 år sedan. Tidigare användes metoder i form av långa http-förfrågningar (faktiskt svar): användarens webbläsare skickade en förfrågan till servern och väntade på att den skulle svara på något, efter svaret kopplades den upp igen och väntade. Men så dök websockets upp.

Hur vi på ZeroTech kopplade ihop Apple Safari och klientcertifikat med websockets

För några år sedan utvecklade vi en egen implementering i ren PHP, som inte kan använda https-förfrågningar, eftersom detta är länklagret. För inte så länge sedan lärde sig nästan alla webbservrar att proxyförfrågningar över https och stödja connection:upgrade.

När detta hände blev websockets nästan standardtjänsten för SPA-applikationer, för hur bekvämt det är att tillhandahålla innehåll till användaren på initiativ av servern (sända ett meddelande från en annan användare eller ladda ner en ny version av en bild, dokument, presentation som någon annan för närvarande redigerar).

Även om klientcertifikat har funnits ganska länge, stöds det fortfarande dåligt, eftersom det skapar många problem när man försöker kringgå det. Och (möjligen :slightly_smiling_face: ) det är därför IOS-webbläsare (alla utom Safari) inte vill använda det och begära det från den lokala certifikatarkivet. Certifikat har många fördelar jämfört med inloggning/pass eller ssh-nycklar eller att stänga nödvändiga portar genom en brandvägg. Men det är inte vad det handlar om.

På iOS är proceduren för att installera ett certifikat ganska enkel (inte utan detaljer), men i allmänhet görs det enligt instruktioner, av vilka det finns många på Internet och som endast är tillgängliga för webbläsaren Safari. Tyvärr vet Safari inte hur man använder Client Сert för webbsockets, men det finns många instruktioner på Internet om hur man skapar ett sådant certifikat, men i praktiken är detta ouppnåeligt.

Hur vi på ZeroTech kopplade ihop Apple Safari och klientcertifikat med websockets

För att förstå websockets använde vi följande plan: problem/hypotes/lösning.

Problem: det finns inget stöd för webbsockets vid proxyförfrågningar till resurser som är skyddade av ett klientcertifikat i Safaris mobilwebbläsare för IOS och andra applikationer som har aktiverat certifikatstöd.

Hypoteser:

  1. Det är möjligt att konfigurera ett sådant undantag för att använda certifikat (med vetskap om att det inte kommer att finnas några) till websockets av interna/externa proxyresurser.
  2. För websockets kan du skapa en unik, säker och försvarbar anslutning med hjälp av tillfälliga sessioner som genereras under en normal (icke-websocket) webbläsarförfrågan.
  3. Tillfälliga sessioner kan implementeras med en proxywebbserver (endast inbyggda moduler och funktioner).
  4. Tillfälliga sessionstokens har redan implementerats som färdiga Apache-moduler.
  5. Tillfälliga sessionstokens kan implementeras genom att logiskt utforma interaktionsstrukturen.

Synligt tillstånd efter implementering.

Målet med arbetet: hantering av tjänster och infrastruktur bör vara tillgänglig från en mobiltelefon på IOS utan ytterligare program (som VPN), enhetligt och säkert.

Ytterligare mål: sparar tid och resurser/telefontrafik (vissa tjänster utan webbuttag genererar onödiga förfrågningar) med snabbare leverans av innehåll på mobilt internet.

Hur man kontrollerar?

1. Öppna sidor:

— например, 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. Eller i utvecklarkonsolen:

Hur vi på ZeroTech kopplade ihop Apple Safari och klientcertifikat med websockets

Hypotestestning:

1. Det är möjligt att konfigurera ett sådant undantag för att använda certifikat (med vetskapen om att det inte kommer att finnas några) till webbuttag med interna/externa proxyresurser.

2 lösningar hittades här:

a) På nivån

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

ändra åtkomstnivå.

Denna metod har följande nyanser:

  • Certifikatverifiering sker efter en begäran till proxyresursen, det vill säga handskakning efter begäran. Detta innebär att proxyn först kommer att ladda och sedan avbryta begäran till den skyddade tjänsten. Detta är dåligt, men inte kritiskt;
  • I http2-protokollet. Det är fortfarande i utkast, och webbläsartillverkare vet inte hur man implementerar det #info om tls1.3 http2 post handshake (fungerar inte nu) Implementera RFC 8740 "Using TLS 1.3 with HTTP/2";
  • Det är inte klart hur man förenar denna bearbetning.

b) På grundnivå, tillåt ssl utan certifikat.

SSLVerifyClient require => SSLVerifyClient valfritt, men detta minskar säkerhetsnivån för proxyservern, eftersom en sådan anslutning kommer att behandlas utan certifikat. Du kan dock ytterligare neka åtkomst till proxytjänster med följande direktiv:

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"

Mer detaljerad information finns i artikeln om ssl: Apache Server Client Certificate Authentication

Båda alternativen testades, alternativ "b" valdes för dess mångsidighet och kompatibilitet med http2-protokollet.

För att slutföra verifieringen av denna hypotes krävdes det många experiment med konfigurationen; följande konstruktioner testades:

om = kräver = skriv om

Resultatet är följande grundläggande design:

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>

Med hänsyn till den befintliga behörigheten från certifikatägaren, men med ett saknat certifikat, var jag tvungen att lägga till en obefintlig certifikatägare i form av en av de tillgängliga variablerna SSl_PROTOCOL (istället för SSL_CLIENT_S_DN_CN), mer detaljer i dokumentationen:

Apache-modul mod_ssl

Hur vi på ZeroTech kopplade ihop Apple Safari och klientcertifikat med websockets

2. För websockets kan du skapa en unik, säker och skyddad anslutning med hjälp av tillfälliga sessioner som genereras under en normal (icke-websocket) webbläsarförfrågan.

Baserat på tidigare erfarenheter måste du lägga till ytterligare en sektion till konfigurationen för att förbereda tillfälliga tokens för webbsocket-anslutningar under en vanlig (icke-web-socket) begäran.

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

Tester visade att det fungerar. Det är möjligt att överföra cookies till dig själv via användarens webbläsare.

3. Tillfälliga sessioner kan implementeras med en proxywebbserver (endast inbyggda moduler och funktioner).

Som vi fick reda på tidigare har Apache en hel del kärnfunktioner som låter dig skapa villkorliga konstruktioner. Vi behöver dock medel för att skydda vår information medan den finns i användarens webbläsare, så vi fastställer vad vi ska lagra och varför, och vilka inbyggda funktioner vi kommer att använda:

  • Vi behöver en token som inte lätt kan avkodas.
  • Vi behöver en token som har inkurans inbyggd och möjligheten att kontrollera inkurans på servern.
  • Vi behöver en token som kommer att kopplas till ägaren av certifikatet.

Detta kräver en hashfunktion, ett salt och ett datum för att åldra token. Baserat på dokumentationen Uttryck i Apache HTTP Server vi har allt ur lådan sha1 och %{TIME}.

Resultatet blev denna 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>

Målet har uppnåtts, men det finns problem med serverföråldrad (du kan använda en år gammal Cookie), vilket innebär att tokens, även om de är säkra för internt bruk, är osäkra för industriell (mass)användning.

Hur vi på ZeroTech kopplade ihop Apple Safari och klientcertifikat med websockets

4. Tillfälliga sessionstokens har redan implementerats som färdiga Apache-moduler.

Ett betydande problem återstod från den tidigare iterationen - oförmågan att kontrollera symboliskt åldrande.

Vi letar efter en färdig modul som gör detta, enligt orden: apache token json two factor auth

Ja, det finns färdiga moduler, men de är alla knutna till specifika åtgärder och har artefakter i form av att starta en session och ytterligare cookies. Det vill säga inte på ett tag.
Det tog oss fem timmar att söka, vilket inte gav något konkret resultat.

5. Tillfälliga sessionstokens kan implementeras genom att logiskt utforma strukturen för interaktioner.

Färdiga moduler är för komplexa, eftersom vi bara behöver ett par funktioner.

Som sagt, problemet med datumet är att Apaches inbyggda funktioner inte tillåter generering av ett datum från framtiden, och det finns ingen matematisk addition/subtraktion i de inbyggda funktionerna vid kontroll av inkurans.

Det vill säga, du kan inte skriva:

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

Du kan bara jämföra två siffror.

När jag letade efter en lösning för Safari-problemet hittade jag en intressant artikel: Säkra HomeAssistant med klientcertifikat (fungerar med Safari/iOS)
Den beskriver ett exempel på kod i Lua för Nginx, och som, som det visade sig, i hög grad upprepar logiken i den del av konfigurationen som vi redan har implementerat, med undantag för användningen av hmac-saltningsmetoden för hash ( detta hittades inte i Apache).

Det blev tydligt att Lua är ett språk med tydlig logik, och det är möjligt att göra något enkelt för Apache:

Efter att ha studerat skillnaden med Nginx och Apache:

Och tillgängliga funktioner från Lua-språktillverkaren:
22.1 – Datum och tid

Vi hittade ett sätt att ställa in env-variabler i en liten Lua-fil för att ställa in ett datum från framtiden att jämföra med den nuvarande.

Så här ser ett enkelt Lua-skript ut:

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

Och så här fungerar det totalt, med optimering av antalet cookies och byte av token när halva tiden kommer innan den gamla kakan (token) går ut:

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 

Eftersom LuaHookAccessChecker endast kommer att aktiveras efter åtkomstkontroller baserat på denna information från Nginx.

Hur vi på ZeroTech kopplade ihop Apple Safari och klientcertifikat med websockets

Länk till källa bild.

En annan punkt.

I allmänhet spelar det ingen roll i vilken ordning direktiven är skrivna i Apache-konfigurationen (förmodligen även Nginx), eftersom allt i slutändan kommer att sorteras baserat på ordningen på begäran från användaren, vilket motsvarar schemat för bearbetning Lua manus.

Komplettering:

Synligt tillstånd efter implementering (mål):
hantering av tjänster och infrastruktur är tillgänglig från en mobiltelefon på IOS utan ytterligare program (VPN), enhetlig och säker.

Målet är uppnått, webbuttag fungerar och har en säkerhetsnivå inte mindre än ett certifikat.

Hur vi på ZeroTech kopplade ihop Apple Safari och klientcertifikat med websockets

Källa: will.com

Lägg en kommentar