Cách chúng tôi tại ZeroTech kết nối Apple Safari và chứng chỉ ứng dụng khách với websockets

Bài viết sẽ hữu ích cho những ai:

  • biết Chứng chỉ ứng dụng khách là gì và hiểu lý do tại sao nó cần websockets trên Safari di động;
  • Tôi muốn xuất bản các dịch vụ web cho một nhóm người hạn chế hoặc chỉ cho chính tôi;
  • nghĩ rằng mọi thứ đã được ai đó thực hiện và muốn làm cho thế giới trở nên thuận tiện và an toàn hơn một chút.

Lịch sử của websockets bắt đầu khoảng 8 năm trước. Trước đây, các phương thức được sử dụng dưới dạng yêu cầu http dài (thực tế là phản hồi): trình duyệt của người dùng gửi yêu cầu đến máy chủ và đợi nó trả lời điều gì đó, sau phản hồi, nó kết nối lại và chờ. Nhưng sau đó websockets xuất hiện.

Cách chúng tôi tại ZeroTech kết nối Apple Safari và chứng chỉ ứng dụng khách với websockets

Một vài năm trước, chúng tôi đã phát triển triển khai của riêng mình bằng PHP thuần túy, không thể sử dụng các yêu cầu https vì đây là lớp liên kết. Cách đây không lâu, hầu hết tất cả các máy chủ web đều học cách yêu cầu proxy qua https và hỗ trợ kết nối: nâng cấp.

Khi điều này xảy ra, websockets gần như trở thành dịch vụ mặc định cho các ứng dụng SPA, vì việc cung cấp nội dung cho người dùng theo sáng kiến ​​​​của máy chủ sẽ thuận tiện như thế nào (truyền tin nhắn từ người dùng khác hoặc tải xuống phiên bản mới của hình ảnh, tài liệu, bản trình bày). mà người khác hiện đang chỉnh sửa).

Mặc dù Chứng chỉ ứng dụng khách đã xuất hiện được một thời gian nhưng nó vẫn được hỗ trợ kém vì nó tạo ra rất nhiều vấn đề khi cố gắng vượt qua nó. Và (có thể :slightly_smiling_face: ) đó là lý do tại sao các trình duyệt iOS (tất cả ngoại trừ Safari) không muốn sử dụng nó và yêu cầu nó từ kho chứng chỉ cục bộ. Chứng chỉ có nhiều ưu điểm so với khóa đăng nhập/pass hoặc ssh hoặc đóng các cổng cần thiết thông qua tường lửa. Nhưng đó không phải là điều này nói về.

Trên iOS, quy trình cài đặt chứng chỉ khá đơn giản (không phải không có thông tin cụ thể), nhưng nhìn chung nó được thực hiện theo hướng dẫn, trong đó có rất nhiều trên Internet và chỉ có cho trình duyệt Safari. Thật không may, Safari không biết cách sử dụng Client Сert cho ổ cắm web, nhưng có rất nhiều hướng dẫn trên Internet về cách tạo chứng chỉ như vậy, nhưng trên thực tế, điều này là không thể đạt được.

Cách chúng tôi tại ZeroTech kết nối Apple Safari và chứng chỉ ứng dụng khách với websockets

Để hiểu websockets, chúng tôi đã sử dụng kế hoạch sau: vấn đề/giả thuyết/giải pháp.

Vấn đề: không có hỗ trợ cho ổ cắm web khi ủy quyền yêu cầu tới các tài nguyên được bảo vệ bởi chứng chỉ ứng dụng khách trên trình duyệt di động Safari dành cho iOS và các ứng dụng khác đã bật hỗ trợ chứng chỉ.

Các giả thuyết:

  1. Có thể định cấu hình một ngoại lệ như vậy để sử dụng chứng chỉ (biết rằng sẽ không có chứng chỉ nào) cho ổ cắm web của tài nguyên proxy bên trong/bên ngoài.
  2. Đối với websockets, bạn có thể tạo một kết nối duy nhất, an toàn và có khả năng bảo vệ bằng cách sử dụng các phiên tạm thời được tạo trong yêu cầu trình duyệt thông thường (không phải websocket).
  3. Các phiên tạm thời có thể được triển khai bằng một máy chủ web proxy (chỉ các mô-đun và chức năng tích hợp sẵn).
  4. Mã thông báo phiên tạm thời đã được triển khai dưới dạng mô-đun Apache được tạo sẵn.
  5. Mã thông báo phiên tạm thời có thể được triển khai bằng cách thiết kế cấu trúc tương tác một cách hợp lý.

Trạng thái hiển thị sau khi thực hiện.

Khách quan: Việc quản lý các dịch vụ và cơ sở hạ tầng phải có thể truy cập được từ điện thoại di động trên iOS mà không cần các chương trình bổ sung (chẳng hạn như VPN), thống nhất và an toàn.

Mục tiêu bổ sung: tiết kiệm thời gian và tài nguyên/lưu lượng điện thoại (một số dịch vụ không có ổ cắm web tạo ra các yêu cầu không cần thiết) với khả năng phân phối nội dung nhanh hơn trên Internet di động.

Làm thế nào để kiểm tra?

1. Trang mở đầu:

— например, 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. Hoặc trong bảng điều khiển dành cho nhà phát triển:

Cách chúng tôi tại ZeroTech kết nối Apple Safari và chứng chỉ ứng dụng khách với websockets

Kiểm định giả thuyết:

1. Có thể định cấu hình một ngoại lệ như vậy để sử dụng chứng chỉ (biết rằng sẽ không có) cho ổ cắm web của tài nguyên proxy bên trong/bên ngoài.

2 giải pháp đã được tìm thấy ở đây:

a) Ở cấp độ

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

thay đổi cấp độ truy cập.

Phương pháp này có các sắc thái sau:

  • Xác minh chứng chỉ xảy ra sau khi yêu cầu tài nguyên được ủy quyền, nghĩa là bắt tay yêu cầu đăng. Điều này có nghĩa là proxy sẽ tải trước và sau đó cắt yêu cầu tới dịch vụ được bảo vệ. Điều này tệ nhưng không quá nghiêm trọng;
  • Trong giao thức http2. Nó vẫn còn ở bản nháp và các nhà sản xuất trình duyệt không biết cách triển khai nó #thông tin về bắt tay bài đăng tls1.3 http2 (hiện không hoạt động) Triển khai RFC 8740 "Sử dụng TLS 1.3 với HTTP/2";
  • Không rõ làm thế nào để thống nhất quá trình xử lý này.

b) Ở mức cơ bản, cho phép ssl không cần chứng chỉ.

SSLVerifyClient yêu cầu => SSLVerifyClient tùy chọn, nhưng điều này làm giảm mức độ bảo mật của máy chủ proxy vì kết nối như vậy sẽ được xử lý mà không cần chứng chỉ. Tuy nhiên, bạn có thể từ chối thêm quyền truy cập vào các dịch vụ được ủy quyền bằng chỉ thị sau:

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"

Thông tin chi tiết hơn có thể được tìm thấy trong bài viết về ssl: Xác thực chứng chỉ máy khách Apache

Cả hai tùy chọn đều đã được thử nghiệm, tùy chọn “b” được chọn vì tính linh hoạt và khả năng tương thích với giao thức http2.

Để hoàn tất việc kiểm chứng giả thuyết này, phải thực hiện rất nhiều thử nghiệm với cấu hình, các thiết kế sau đã được thử nghiệm:

nếu = yêu cầu = viết lại

Kết quả là thiết kế cơ bản sau:

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>

Tính đến ủy quyền hiện có của chủ sở hữu chứng chỉ, nhưng bị thiếu chứng chỉ, tôi đã phải thêm chủ sở hữu chứng chỉ không tồn tại dưới dạng một trong các biến có sẵn SSl_PROTOCOL (thay vì SSL_CLIENT_S_DN_CN), chi tiết hơn trong tài liệu:

Mô-đun Apache mod_ssl

Cách chúng tôi tại ZeroTech kết nối Apple Safari và chứng chỉ ứng dụng khách với websockets

2. Đối với websockets, bạn có thể tạo kết nối duy nhất, an toàn và được bảo vệ bằng cách sử dụng các phiên tạm thời được tạo trong yêu cầu trình duyệt thông thường (không phải websocket).

Dựa trên kinh nghiệm trước đó, bạn cần thêm một phần bổ sung vào cấu hình để chuẩn bị mã thông báo tạm thời cho các kết nối ổ cắm web trong một yêu cầu thông thường (không phải ổ cắm 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>

Thử nghiệm cho thấy rằng nó hoạt động. Có thể chuyển Cookies cho chính bạn thông qua trình duyệt của người dùng.

3. Các phiên tạm thời có thể được triển khai bằng cách sử dụng một máy chủ web proxy (chỉ các mô-đun và chức năng tích hợp sẵn).

Như chúng tôi đã tìm hiểu trước đó, Apache có khá nhiều chức năng cốt lõi cho phép bạn tạo các cấu trúc có điều kiện. Tuy nhiên, chúng tôi cần các phương tiện để bảo vệ thông tin của mình khi thông tin đó ở trong trình duyệt của người dùng, vì vậy, chúng tôi thiết lập những gì cần lưu trữ và lý do lưu trữ cũng như những chức năng tích hợp nào chúng tôi sẽ sử dụng:

  • Chúng tôi cần một mã thông báo không thể giải mã dễ dàng.
  • Chúng tôi cần một mã thông báo đã lỗi thời được tích hợp sẵn và khả năng kiểm tra lỗi thời trên máy chủ.
  • Chúng tôi cần mã thông báo sẽ được liên kết với chủ sở hữu chứng chỉ.

Điều này yêu cầu hàm băm, muối và ngày tháng để làm cũ mã thông báo. Dựa trên tài liệu Biểu thức trong Máy chủ HTTP Apache chúng tôi có sẵn tất cả sha1 và %{TIME}.

Kết quả là thiết kế này:

#нет сертификата, и обращение к 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ục tiêu đã đạt được nhưng có vấn đề về lỗi thời của máy chủ (bạn có thể sử dụng Cookie cũ), điều đó có nghĩa là các mã thông báo, mặc dù an toàn khi sử dụng nội bộ, nhưng lại không an toàn khi sử dụng trong công nghiệp (đại trà).

Cách chúng tôi tại ZeroTech kết nối Apple Safari và chứng chỉ ứng dụng khách với websockets

4. Mã thông báo phiên tạm thời đã được triển khai dưới dạng mô-đun Apache được tạo sẵn.

Một vấn đề quan trọng vẫn còn tồn tại từ lần lặp trước - không có khả năng kiểm soát quá trình lão hóa của mã thông báo.

Chúng tôi đang tìm kiếm một mô-đun làm sẵn để thực hiện việc này, theo dòng chữ: apache token json twofactor auth

Có, có những mô-đun được tạo sẵn, nhưng tất cả chúng đều gắn liền với các hành động cụ thể và có các tạo phẩm dưới dạng bắt đầu một phiên và các Cookie bổ sung. Đó là, không phải trong một thời gian.
Chúng tôi mất XNUMX giờ để tìm kiếm nhưng không đưa ra kết quả cụ thể.

5. Mã thông báo phiên tạm thời có thể được triển khai bằng cách thiết kế hợp lý cấu trúc tương tác.

Các mô-đun làm sẵn quá phức tạp vì chúng ta chỉ cần một vài chức năng.

Như đã nói, vấn đề với ngày tháng là các hàm dựng sẵn của Apache không cho phép tạo ngày từ tương lai và không có phép cộng/trừ toán học trong các hàm dựng sẵn khi kiểm tra độ lỗi thời.

Tức là bạn không thể viết:

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

Bạn chỉ có thể so sánh hai số.

Trong khi tìm cách giải quyết vấn đề Safari, tôi tìm thấy một bài viết thú vị: Bảo mật HomeAssistant bằng chứng chỉ ứng dụng khách (hoạt động với Safari/iOS)
Nó mô tả một ví dụ về mã trong Lua cho Nginx, và hóa ra là lặp lại rất nhiều logic của phần cấu hình mà chúng tôi đã triển khai, ngoại trừ việc sử dụng phương pháp tạo muối hmac để băm ( cái này không được tìm thấy trong Apache).

Rõ ràng rằng Lua là một ngôn ngữ có logic rõ ràng và có thể làm điều gì đó đơn giản cho Apache:

Đã nghiên cứu sự khác biệt với Nginx và Apache:

Và các chức năng có sẵn từ nhà sản xuất ngôn ngữ Lua:
22.1 – Ngày và Giờ

Chúng tôi đã tìm ra cách đặt các biến env trong một tệp Lua nhỏ để đặt ngày trong tương lai để so sánh với ngày hiện tại.

Tập lệnh Lua đơn giản trông như thế này:

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

Và đây là cách tất cả hoạt động tổng thể, với việc tối ưu hóa số lượng Cookie và thay thế mã thông báo khi còn một nửa thời gian trước khi Cookie (mã thông báo) cũ hết hạn:

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 

Bởi vì LuaHookAccessChecker sẽ chỉ được kích hoạt sau khi kiểm tra quyền truy cập dựa trên thông tin này từ Nginx.

Cách chúng tôi tại ZeroTech kết nối Apple Safari và chứng chỉ ứng dụng khách với websockets

Liên kết tới nguồn Hình ảnh.

Một điểm.

Nói chung, thứ tự các lệnh được viết trong cấu hình Apache (có thể cả Nginx) không quan trọng, vì cuối cùng mọi thứ sẽ được sắp xếp dựa trên thứ tự yêu cầu từ người dùng, tương ứng với sơ đồ xử lý Tập lệnh Lua.

Hoàn thành:

Trạng thái hiển thị sau khi thực hiện (mục tiêu):
quản lý dịch vụ và cơ sở hạ tầng có sẵn từ điện thoại di động trên iOS mà không cần chương trình bổ sung (VPN), thống nhất và an toàn.

Mục tiêu đã đạt được, các ổ cắm web hoạt động và có mức độ bảo mật không kém một chứng chỉ.

Cách chúng tôi tại ZeroTech kết nối Apple Safari và chứng chỉ ứng dụng khách với websockets

Nguồn: www.habr.com

Thêm một lời nhận xét