ZeroTech 如何通过 Websocket 连接 Apple Safari 和客户端证书

本文对以下人员有用:

  • 知道什么是 Client Cert,并了解为什么它在移动 Safari 上需要 Websocket;
  • 我想向有限的人或只向我自己发布网络服务;
  • 认为一切都已经有人做了,并希望让世界变得更方便、更安全。

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 和客户端证书

来源: habr.com

添加评论