ZeroTech が Apple Safari とクライアント証明書を WebSocket で接続した方法

この記事は次のような人に役立ちます。

  • Client Cert とは何かを知っており、モバイル Safari で WebSocket が必要な理由を理解しています。
  • Web サービスを限られた範囲の人々、または自分だけに公開したいと考えています。
  • すべてはすでに誰かがやっていると考え、世の中を少しでも便利で安全にしたいと考えています。

WebSocket の歴史は約 8 年前に始まりました。 以前は、メソッドは長い http リクエスト (実際にはレスポンス) の形式で使用されていました。ユーザーのブラウザはサーバーにリクエストを送信し、何かの応答を待ち、応答後に再度接続して待機しました。 しかしその後、WebSocket が登場しました。

ZeroTech が Apple Safari とクライアント証明書を WebSocket で接続した方法

数年前、私たちは純粋な PHP で独自の実装を開発しましたが、これはリンク層であるため https リクエストを使用できません。 少し前までは、ほぼすべての Web サーバーが https 経由でリクエストをプロキシし、connection:upgrade をサポートすることを学習しました。

これが起こると、WebSocket が SPA アプリケーションのほぼデフォルトのサービスになりました。これは、サーバーの主導でユーザーにコンテンツを提供する (別のユーザーからのメッセージを送信したり、画像、ドキュメント、プレゼンテーションの新しいバージョンをダウンロードしたりする) ことが非常に便利であるためです。他の人が現在編集中であること)。

クライアント証明書はかなり前から存在していますが、クライアント証明書をバイパスしようとすると多くの問題が発生するため、依然としてサポートが不十分です。 そして (おそらく :slightly_smiling_face: ) それが、IOS ブラウザ (Safari を除くすべて) がそれを使用したくない理由であり、ローカル証明書ストアからそれを要求します。 証明書には、ログイン/パス キーや SSH キー、またはファイアウォールを介して必要なポートを閉じることと比較して、多くの利点があります。 しかし、これはそういうことではありません。

iOS では、証明書をインストールする手順は非常に簡単です (詳細がないわけではありません) が、一般的には、インターネット上に多数の手順があり、Safari ブラウザでのみ利用できる手順に従って実行されます。 残念ながら、Safari は Web ソケットに Client Сert を使用する方法を知りません。そのような証明書を作成する方法についてはインターネット上に多くの説明がありますが、実際にはこれを実現できません。

ZeroTech が Apple Safari とクライアント証明書を WebSocket で接続した方法

WebSocket を理解するために、次の計画を使用しました。 問題/仮説/解決策。

問題: IOS 用の Safari モバイル ブラウザーおよび証明書サポートが有効になっているその他のアプリケーション上のクライアント証明書によって保護されているリソースにリクエストをプロキシする場合、Web ソケットはサポートされません。

仮説:

  1. 内部/外部のプロキシされたリソースの WebSocket に対して証明書 (存在しないことがわかっている) を使用するように、このような例外を構成することが可能です。
  2. WebSocket の場合、通常の (WebSocket 以外の) ブラウザー要求中に生成される一時セッションを使用して、独自の安全で防御可能な接続を確立できます。
  3. 一時セッションは、XNUMX つのプロキシ Web サーバー (組み込みモジュールおよび関数のみ) を使用して実装できます。
  4. 一時セッション トークンは、既製の Apache モジュールとしてすでに実装されています。
  5. 一時的なセッション トークンは、対話構造を論理的に設計することで実装できます。

実装後の状態を表示します。

目的: サービスとインフラストラクチャの管理は、追加のプログラム (VPN など) を必要とせずに、IOS 上の携帯電話から統合され、安全にアクセスできる必要があります。

追加の目標: モバイル インターネット上でのコンテンツの配信が高速化され、時間とリソース/電話トラフィック (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 が Apple Safari とクライアント証明書を WebSocket で接続した方法

仮説検証:

1. 内部/外部のプロキシされたリソースの Web ソケットに対して証明書 (存在しないことがわかっている) を使用するように、このような例外を構成することができます。

ここで 2 つの解決策が見つかりました。

a) レベルで

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

アクセスレベルを変更します。

この方法には次のようなニュアンスがあります。

  • 証明書の検証は、プロキシされたリソースへのリクエストの後、つまりリクエスト後のハンドシェイク後に行われます。 これは、プロキシが最初にロードしてから、保護されたサービスへのリクエストを遮断することを意味します。 これは悪いことではありますが、重大ではありません。
  • http2 プロトコル内。 これはまだドラフト段階であり、ブラウザのメーカーはそれを実装する方法を知りません #tls1.3 http2 ポスト ハンドシェイクに関する情報 (現在は機能していません) RFC 8740「HTTP/1.3 での TLS 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サーバークライアント証明書認証

両方のオプションがテストされましたが、汎用性と http2 プロトコルとの互換性を考慮してオプション「b」が選択されました。

この仮説の検証を完了するには、構成に関して多くの実験が必要となり、次の設計がテストされました。

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 ではなく) の XNUMX つの形式で追加する必要がありました。詳細については、ドキュメントを参照してください。

Apache モジュール mod_ssl

ZeroTech が Apple Safari とクライアント証明書を WebSocket で接続した方法

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. 一時セッションは、XNUMX つのプロキシ 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>

目標は達成されましたが、サーバーの陳腐化に関する問題があります (XNUMX 年前の Cookie を使用できます)。つまり、トークンは内部使用には安全ですが、産業 (大量) 使用には安全ではありません。

ZeroTech が Apple Safari とクライアント証明書を WebSocket で接続した方法

4. 一時セッション トークンは、既製の Apache モジュールとしてすでに実装されています。

前回の反復から XNUMX つの重大な問題が残りました。それは、トークンのエージングを制御できないことです。

「apache token json two-factor auth」という言葉に従って、これを行う既製のモジュールを探しています。

はい、既製のモジュールがありますが、それらはすべて特定のアクションに関連付けられており、セッションの開始や追加の Cookie の形式でアーティファクトが含まれています。 つまり、しばらくはありません。
検索には XNUMX 時間かかりましたが、具体的な結果は得られませんでした。

5. 一時的なセッション トークンは、インタラクションの構造を論理的に設計することで実装できます。

必要な機能はいくつかだけであるため、既製のモジュールは複雑すぎます。

そうは言っても、日付に関する問題は、Apache の組み込み関数では将来の日付を生成できないこと、また、陳腐化をチェックするときに組み込み関数には数学的な加算/減算が存在しないことです。

つまり、次のように書くことはできません。

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

比較できるのは XNUMX つの数値のみです。

Safari の問題の回避策を探しているときに、興味深い記事を見つけました。 クライアント証明書による HomeAssistant の保護 (Safari/iOS で動作)
これは、Nginx 用の Lua のコードの例を説明しています。これは、ハッシュ化のための hmac ソルティング メソッドの使用を除いて、既に実装した構成のその部分のロジックを非常に繰り返していることが判明しました (これは 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 が Apple Safari とクライアント証明書を WebSocket で接続した方法

ソースへのリンク 画像.

もう一つのポイント。

一般に、Apache (おそらく Nginx も) 設定でディレクティブがどのような順序で記述されるかは問題ではありません。最終的には、処理スキームに対応するユーザーからのリクエストの順序に基づいてすべてが並べ替えられるためです。 Lua スクリプト。

完了:

実装後の目に見える状態 (目標):
サービスとインフラストラクチャの管理は、追加のプログラム (VPN) なしで IOS 上の携帯電話から利用でき、統合され安全です。

目標は達成され、Web ソケットは機能し、証明書と同等のセキュリティ レベルを備えています。

ZeroTech が Apple Safari とクライアント証明書を WebSocket で接続した方法

出所: habr.com

コメントを追加します