ZeroTech 如何透過 Websocket 連接 Apple Safari 和用戶端證書

本文對以下人員有用:

  • 知道什麼是 Client Cert,並了解為什麼它在行動 Safari 上需要 Websockets;
  • 我想向有限的人或只向我自己發佈網路服務;
  • 認為一切都已經有人做了,並希望讓世界變得更方便、更安全。

Websocket 的歷史大約始於 8 年前。 以前,方法以長http請求(實際上是回應)的形式使用:使用者的瀏覽器向伺服器發送請求並等待它回答某些內容,在回應之後它再次連接並等待。 但後來 websocket 出現了。

ZeroTech 如何透過 Websocket 連接 Apple Safari 和用戶端證書

幾年前,我們用純 PHP 開發了自己的實現,它不能使用 https 請求,因為這是連結層。 不久前,幾乎所有的Web伺服器都學會了透過https代理請求並支援connection:upgrade。

當這種情況發生時,websockets 幾乎成為 SPA 應用程式的預設服務,因為在伺服器主動向用戶提供內容(傳輸來自另一個用戶的訊息或下載新版本的圖像、文件、簡報)是多麼方便其他人目前正在編輯)。

儘管客戶端憑證已經存在相當長一段時間了,但它仍然得不到很好的支持,因為它在嘗試繞過它時會產生很多問題。 並且(可能是 :slightly_smiling_face: )這就是為什麼 IOS 瀏覽器(除了 Safari 之外的所有瀏覽器)不想使用它並從本地證書存儲請求它。 與登入/通行證或 ssh 金鑰或透過防火牆關閉必要的連接埠相比,憑證具有許多優點。 但這不是重點。

在iOS上,安裝憑證的過程非常簡單(並非沒有細節),但通常是根據說明完成的,其中網路上有很多,並且僅適用於Safari瀏覽器。 不幸的是,Safari 不知道如何使用 Client Сert 進行 Web 套接字,但 Internet 上有很多關於如何建立此類憑證的說明,但實際上這是無法實現的。

ZeroTech 如何透過 Websocket 連接 Apple Safari 和用戶端證書

為了理解 websocket,我們使用了以下計劃: 問題/假設/解決方案。

問題: 對於 IOS 和其他已啟用憑證支援的應用程序,在 Safari 行動瀏覽器上將請求代理到受客戶端憑證保護的資源時,不支援 Web 套接字。

假設:

  1. 可以配置這樣的例外,以對內部/外部代理資源的 Websocket 使用憑證(知道不會有憑證)。
  2. 對於 Websocket,您可以使用在正常(非 Websocket)瀏覽器請求期間產生的臨時會話來建立唯一、安全且可防禦的連線。
  3. 可以使用一台代理 Web 伺服器(僅限內建模組和功能)來實現臨時會話。
  4. 臨時會話令牌已作為現成的 Apache 模組實作。
  5. 臨時會話令牌可以透過互動結構的邏輯設計來實現。

實施後可見狀態。

客觀的: 服務和基礎設施的管理應該可以透過IOS上的手機進行訪問,無需額外的程序(例如VPN),統一且安全。

附加目標: 透過在行動互聯網上更快地交付內容,節省時間和資源/電話流量(某些沒有 Web 套接字的服務會產生不必要的請求)。

如何檢查?

1.開啟頁面:

— например, 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. 或在開發者控制台中:

ZeroTech 如何透過 Websocket 連接 Apple Safari 和用戶端證書

假設檢定:

1. 可以設定這樣的例外,以對內部/外部代理資源的 Web 套接字使用憑證(知道不會有憑證)。

在這裡找到了2個解決方案:

a) 在水平上

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

更改存取等級。

此方法有以下細微差別:

  • 憑證驗證發生在對代理資源發出請求之後,即請求後握手之後。 這意味著代理將首先加載然後切斷對受保護服務的請求。 這很糟糕,但並不重要;
  • 在http2協定中。 目前仍處於草案階段,瀏覽器製造商不知道如何實作#info about tls1.3 http2 post handshake(現在不工作) 實作 RFC 8740“將 TLS 1.3 與 HTTP/2 結合使用”;
  • 目前還不清楚如何統一這個處理。

b) 在基本層級上,允許在沒有憑證的情況下使用 ssl。

SSLVerifyClient require => SSLVerifyClient 可選,但這會降低代理伺服器的安全級別,因為這樣的連線將在沒有憑證的情況下進行處理。 但是,您可以使用以下指令進一步拒絕對代理服務的存取:

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"

更詳細的資訊可以在關於ssl的文章中找到: Apache 伺服器用戶端憑證驗證

這兩個選項都經過測試,選擇選項「b」是因為其多功能性以及與 http2 協定的兼容性。

為了完成這一假設的驗證,我們對配置進行了大量實驗;測試了以下設計:

if = 要求 = 重寫

結果是以下基本設計:

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>

考慮到證書所有者的現有授權,但缺少證書,我必須以可用變數 SSl_PROTOCOL 之一(而不是 SSL_CLIENT_S_DN_CN)的形式添加不存在的證書所有者,文檔中的更多詳細資訊:

Apache 模組 mod_ssl

ZeroTech 如何透過 Websocket 連接 Apple Safari 和用戶端證書

2. 對於 Websocket,您可以使用正常(非 Websocket)瀏覽器請求期間產生的臨時會話建立唯一、安全且受保護的連線。

根據先前的經驗,您需要在配置中新增一個附加部分,以便在常規(非 Web 套接字)請求期間為 Web 套接字連線準備臨時令牌。

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

測試表明它有效。 可以透過使用者的瀏覽器將 Cookie 傳輸給您自己。

3. 可以使用一台代理Web伺服器實現臨時會話(僅內建模組和功能)。

正如我們之前發現的,Apache 有許多核心功能,可讓您建立條件建構。 然而,我們需要採取措施來保護使用者瀏覽器中的信息,因此我們確定要儲存的內容和原因,以及我們將使用哪些內建功能:

  • 我們需要一個不容易被解碼的令牌。
  • 我們需要一個內建過時功能的令牌,並且能夠在伺服器上檢查過時情況。
  • 我們需要一個與憑證擁有者關聯的令牌。

這需要雜湊函數、鹽和令牌老化日期。 根據文件 Apache HTTP 伺服器中的表達式 我們擁有開箱即用的 sha1 和 %{TIME}。

結果是這樣的設計:

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

目標已經實現,但是存在伺服器過時的問題(您可以使用一年前的 Cookie),這意味著這些代幣雖然對於內部使用是安全的,但對於工業(大眾)使用來說是不安全的。

ZeroTech 如何透過 Websocket 連接 Apple Safari 和用戶端證書

4. 臨時會話令牌已作為現成的 Apache 模組實作。

上一次迭代仍然存在一個重大問題—無法控制令牌時效。

我們正在尋找一個現成的模組來執行此操作,根據以下內容:apache token json Two Factor auth

是的,有現成的模組,但它們都與特定操作相關,並且具有啟動會話和附加 Cookie 形式的工件。 也就是說,暫時還沒有。
我們花了五個小時尋找,卻沒有得到具體結果。

5. 可以透過邏輯設計互動結構來實現臨時會話令牌。

現成的模組太複雜,因為我們只需要幾個功能。

話雖如此,日期的問題在於 Apache 的內建函數不允許產生未來的日期,並且在檢查是否過時時,內建函數中沒有數學加法/減法。

也就是說,你不能寫:

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

您只能比較兩個數字。

在尋找 Safari 問題的解決方法時,我發現了一篇有趣的文章: 使用客戶端憑證保護 HomeAssistant(適用於 Safari/iOS)
它描述了 Nginx 的 Lua 程式碼範例,事實證明,它非常重複我們已經實現的那部分配置的邏輯,除了使用 hmac salting 方法進行雜湊(這在 Apache 中沒有找到)。

很明顯,Lua 是一種邏輯清晰的語言,可以為 Apache 做一些簡單的事情:

研究了 Nginx 和 Apache 的差異:

Lua語言製造商提供的可用函數:
22.1 – 日期和時間

我們找到了一種在小 Lua 檔案中設定環境變數的方法,以便設定未來的日期與當前日期進行比較。

一個簡單的 Lua 腳本如下:

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

這就是整個過程的工作原理,優化 Cookie 的數量,並在舊 Cookie(令牌)過期之前更換令牌:

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 

因為 LuaHookAccessChecker 只有在根據來自 Nginx 的資訊進行存取檢查後才會啟動。

ZeroTech 如何透過 Websocket 連接 Apple Safari 和用戶端證書

連結到來源 圖像.

還有一件事。

一般來說,在 Apache(也可能是 Nginx)配置中以什麼順序編寫指令並不重要,因為最終所有內容都會根據使用者請求的順序進行排序,這對應於處理方案盧阿腳本。

完成:

實施後的可見狀態(目標):
服務和基礎設施的管理可以透過 IOS 上的手機進行,無需額外的程序 (VPN),統一且安全。

目標已經實現,Web 套接字可以工作並且具有不低於憑證的安全性等級。

ZeroTech 如何透過 Websocket 連接 Apple Safari 和用戶端證書

來源: www.habr.com

添加評論