ZeroTech我们如何通过websockets来与Apple Safari朋友和客户证书

本文将对以下人员有用:



  • 知道客户证书是什么,并了解为什么他需要在移动Safari上使用Websocket;
  • 希望将网络服务发布给少数人或只发布给自己;
  • 认为一切都已经由某人完成,并希望使世界更加方便和安全。


Web套接字的历史始于大约8年前。以前,使用长http请求(实际上是答复)形式的方法:用户的浏览器向服务器发送了一个请求,并等待他回答一些,然后再次连接并等待。但是随后出现了网络套接字。







几年前,我们开发了自己的纯PHP实现,因为这是数据链接层,所以不知道如何使用https请求。不久前,几乎所有Web服务器都学会了通过https代理请求并支持连接:升级。



发生这种情况时,Web套接字几乎成为SPA应用程序的默认服务,因为它可以方便地向用户提供服务器主动性的内容(发送来自其他用户的消息或下载其他人正在编辑的图像,文档,演示文稿的新版本) ...



尽管Clientert已经存在了很长时间,但它仍然受支持不佳,因为尝试绕过它会产生很多问题。并且(可能是:稍微_smiling_face :),因此IOS浏览器(除Safari之外的所有浏览器)都不想使用它并询问本地证书存储。与登录/通过或ssh密钥或对正确的端口进行防火墙相比,证书具有许多优点。但这不是重点。



在iOS上,安装证书的过程非常简单(并非没有具体说明),但通常是根据说明完成的,该说明在网络上非常多,并且仅适用于Safari浏览器。不幸的是,Safari不知道如何将客户端证书用于Web套接字,但是Internet上有很多说明如何制作这样的证书,但是实际上这是无法实现的。







为了理解websocket,我们使用以下概述:问题/假设/解决方案。



问题:在 IOS和其他包含证书支持的应用程序的移动Safari浏览器上将请求代理到受客户端证书保护的资源时,不支持websocket。



假设:



  1. 可以配置这样的例外,以对内部/外部代理资源的Web套接字使用证书(知道它们将不可用)。
  2. 对于Web套接字,您可以使用在常规(非Web套接字)浏览器请求期间生成的临时会话建立唯一的安全连接。
  3. 可以使用一台代理Web服务器(仅内置模块和功能)实现临时会话。
  4. 临时会话令牌已作为现成的Apache模块实现。
  5. 可以通过逻辑设计交互结构来实现临时会话令牌。


部署后可见状态。



工作目的:应该可以通过IOS上的移动电话进行服务和基础架构的管理,而无需附加程序(例如VPN),并且统一而安全。



附加目标:节省时间和资源/电话流量(某些没有Web套接字的服务会生成不必要的请求),同时加快移动Internet上的内容交付。



怎么检查?



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.或在开发人员的控制台中:







假设测试:



1.可以为内部/外部代理资源的Web套接字配置使用证书的此类例外(知道它们将不存在)。



在这里找到2个解决方案:



a)在一级



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


更改访问级别。



此方法具有以下细微差别:



  • 在对代理资源的请求(即请求后握手)之后检查证书。这意味着代理将首先加载,然后切断对受保护服务的请求。这是不好的,但不是关键。
  • 在http2中。它仍处于草稿中,浏览器制造商不知道如何实现有关tls1.3 http2 post握手的#info(现在无法正常工作)实施RFC 8740“在HTTP / 2中使用TLS 1.3”
  • 目前尚不清楚如何统一此处理。


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 = require = rewrite



我们得到以下基本构造:
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 Module mod_ssl2







。对于Web套接字,您可以建立一个唯一的安全连接使用在常规(不是Web套接字)浏览器请求期间生成的临时会话。



根据以前的经验,您需要在配置中添加一个附加部分,以便在常规(而非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-name\s*=\s*some-val(;|$)/ >
</If

</If>
</If>


测试表明它有效。可以通过用户浏览器传输cookie。



3.临时会话可以使用一台代理Web服务器(仅内置模块和功能)实现。



如前所述,Apache具有很多核心功能,可让您创建条件语句。但是,我们需要一种在用户浏览器中保护我们的信息的方法,因此我们设置要存储的内容和存储的内容以及将使用的内置函数:



  • 我们需要一个无法进行简单解码的令牌。
  • 我们需要一个令牌,在令牌中可以保护服务器的过时和检查过时的能力。
  • 您需要一个与证书所有者关联的令牌。


这需要散列函数,盐和令牌过期的日期。根据Apache HTTP Server文档中的表达式,我们提供了现成的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),这意味着令牌虽然对于内部使用是安全的,但对于工业(批量)使用却是不安全的。







4.临时会话令牌已经作为现成的Apache模块实现。



从上一次迭代开始,仍然存在一个重要的问题-无法控制令牌过时。



我们正在寻找一个可以做到这一点的现成模块,根据:apache token json two factor auth





是的,有现成的模块,但是所有模块都与特定操作相关,并且具有会话开始和其他cookie形式的工件。也就是说,暂时不会。

我们花了五个小时进行搜索,但未给出具体结果。



5.临时会话令牌可以通过逻辑设计交互结构来实现。



现成的模块太复杂了,因为我们只需要几个功能。



同时,日期的问题是Apache内置函数不允许从将来生成日期,并且在检查内置函数中的过时值时,没有数学上的加减法。



也就是说,您不能写:



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


只能比较两个数字。



在寻找Safari问题的解决方法时,发现了一篇有趣的文章:使用客户端证书保护HomeAssistant(可与Safari / iOS一起使用)

它描述了Nginx的Lua代码示例,事实证明,它非常重复了我们之前已经实现的那部分配置的逻辑。除了使用安排盐用于哈希的hmac方法外(在Apache中找不到)。



显而易见,Lua是一种逻辑清晰的语言,可以为Apache做一些简单的事情:





研究了Nginx和Apache的区别:





以及Lua语言制造商提供的可用功能:

22.1-日期和时间



找到了一种在小型Lua文件中设置env变量的方法,以便设置从未来开始检查当前日期的日期。



这是一个简单的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的信息进行访问检查后才被激活。







链接到图像



还有一点。



通常,在Apache配置(可能是Nginx)中编写指令的顺序无关紧要,因为最终所有内容都将根据用户的请求顺序进行排序,这与编制Lua脚本的方案相对应。



完成:



实施后可见的状态(目标):

可通过IOS上的移动电话进行服务和基础架构管理,而无需附加程序(VPN),统一和安全。



该目标得以实现,WebSocket可以正常工作,并且安全性不亚于证书。






All Articles