2022年9月

解密 Java 的 https 流量

最近有个同事发现生产环境中有些服务比较慢, 检查一番后, 发现一个服务调用在tcp 3次握手和 ssl client hello 包之间竟然有200ms 左右的延迟. 接下来, 该同事就想弄清楚: 为什么在3次握手和 client hello之间有200ms 延迟. 他知道有些语言是直接调用的 OpenSSL 的库, 于是问我 Java 里面是不是也是使用 OpenSSL的封装?

我之前写过2篇 Windows 上解密 chrome 的 https 流量 & MAC 上解密 chrome 的 https 流量, 当时就想: 是不是Java 也可以自动把 ssl key 劫持下来, 然后后边自动揭秘. 当时就找到了这个已经持续好多年的项目: jsslkeylog, 这个项目就是通过 Java Agent的方式, 注入相关的代码, 把ssl key 劫持下来.

所以, 当同事问我是不是 OpenSSL 封装的时候, 我立马想到这个项目. 在这个项目里面, 它通过注入的方式把 ssl key 记录下来, 那么肯定是Java 自己原生写的 SSL 的代码处理.

一些核心的代码类: https://github.com/jsslkeylog/jsslkeylog/tree/master/src/main/java/net/sf/jsslkeylog

关于 NSS(Network Security Services)

NSS (Network Security Services) 是由 Mozilla 项目开发的一个模块化、可复用的网络安全服务库,它提供了一系列的安全协议、密钥管理、证书管理、加密算法等安全功能的实现。NSS 被广泛地应用于多种网络应用程序中,包括 Web 浏览器、电子邮件客户端、虚拟专用网络 (VPN) 和移动设备等。

NSS 的功能涵盖了多种协议和技术,包括 SSL、TLS、PKCS#11、S/MIME、X.509 证书、公钥基础设施 (PKI)、安全套接字层 (SSL) 和传输层安全 (TLS) 等。NSS 在这些领域是具有广泛应用的库之一,是开源社区中信任和可靠的安全库。

NSS 还为应用程序提供了一些基本的加密功能,包括随机数生成器、哈希函数、密钥交换、数字签名、加密和解密、证书签名和认证等。这些功能被设计成易于使用和可扩展的,帮助应用程序开发者更快速、更容易地构建安全的网络应用程序。

需要注意的是,NSS 是一个独立的项目,并不仅限于 Mozilla 项目中。它在其他开源项目中也被广泛使用,例如 Red Hat、Fedora、OpenSSL 和 OpenSSH 等。
项目地址: https://github.com/nss-dev/nss
wiki: https://en.wikipedia.org/wiki/Network_Security_Services

关于 NSS 的 key log 格式

官方文档已经找不到了, 这是一个缓存: https://udn.realityripple.com/docs/Mozilla/Projects/NSS/Key_Log_Format
你可以认为这是 NSS 暴露出的一个接口, 这样使用 Wireshark 就能够解密加密传输.
NSS_Key_Log_Format.png

key log 的最新格式

这是 Mozilla 提交给 IEEE 的最新格式, 可以认为之前都不是经过认证, 只是 NSS 和 wireshark 之间商量的一个格式.
https://datatracker.ietf.org/doc/draft-thomson-tls-keylogfile/

wireshark 支持的格式

https://github.com/boundary/wireshark/blob/d029f48e4fd74b09848fc309630e5dfdc5d602f2/epan/dissectors/packet-ssl-utils.c#L4164-L4182
https://github.com/boundary/wireshark/blob/07eade8124fd1d5386161591b52e177ee6ea849f/epan/dissectors/packet-ssl-utils.c#L4204-L4207

两个揭秘 Java TLS 的工具

https://github.com/neykov/extract-tls-secrets/
https://github.com/jsslkeylog/jsslkeylog/tree/master/src/main/java/net/sf/jsslkeylog

使用 -Djavax.net.debug=all 生成的日志直接提取key的工具(TLS 1.3不适用):
https://dimosr.github.io/decrypting-tls-traffic-with-wireshark/
https://gist.github.com/tsaarni/14cc3341d0996e25671f5ca894842ec9

JDK 关于 javax.net.debug 的说明

https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/ReadDebug.html

JDK 使用到的关于TLS 版本的启动参数

  1. -Djdk.tls.client.protocols=TLSv1.2,TLSv1.3
    jdk.tls.client.protocols 是一个 JDK (Java Development Kit) 的系统属性,这个属性可以用来指定与客户端建立安全连接时所支持的 TLS 版本的范围。具体来讲,这个属性可以用来限制客户端连接到服务端时可用的 TLS 协议版本。jdk.tls.client.protocols 属性的值是一个以逗号分隔的 TLS 协议版本列表.
  2. -Dhttps.protocols=TLSv1.2,TLSv1.3
    https.protocols 是一个 Java 系统属性, 可以用来指定在执行 HTTPS 请求时支持的协议版本。特别是当我们使用 Java 编写的程序向使用 HTTPS 存储在远程服务器上的资源发出请求时,这个属性就非常有用了。参数的值表示 Java 程序只会使用 TLS 1.2 和 TLS 1.3 协议版本来发起 HTTPS 请求。如果远程服务器不支持这些协议,则连接将失败。
  3. -Djavax.net.debug=ssl,keygen,handshake

exec user process caused: exec format error

今天开发的app 做了一个新的docker image, 发布到K8S之后, 就报下面的错:

standard_init_linux.go:228: exec user process caused: exec format error

还以为自己写的代码配置出错了, 想登上去看看, 发现进程压根就没起来.

Google 一把, 有人说是entrypoint的shell 文件的 shebang 不对, 我这里没有这个问题.

后来看到有人说是可执行格式的错误, 发现还真是.
首先, 我在本地Mac Pro (ARM) 上去执行这个image, 很正常的执行起来了, 然后我在另外一个Ubuntu 上面执行这个image, 就给我报下面的错误了:

WARNING: The requested image's platform (linux/arm64/v8) does not match the detected host platform (linux/amd64) and no specific platform was requested

所以, 很明显, 这个image 是给 ARM64/v8 用的. 用 docker image inspect <img>去查看, 能看到这个image 是给什么平台机构的.
arch.png

我的 docker build 环境是 MAC ARM 机器, 之前一直没有错, 今天为啥出错了, 原因很有可能是我今天重启了 Docker Desktop.

去看 docker 官方文档: https://docs.docker.com/engine/reference/commandline/build/ 发现他们最近(20220914) 对于 docker build 新加了一个参数 (--platform)
platform.png

到它 change 去看, 发现最新的 v141
https://docs.docker.com/engine/api/version-history/#v140-api-changes

所以, 要在 Mac Pro ARM 芯片上做 x86_64/amd64 的image, 要给一个新参数 --platform linux/amd64

WebSocket 协议

最近有个项目要做一个Slack Robot, 就是根据用户发到Slack 某个Channel的内容, 给出智能回复.

与Slack的交互方式有2种: HTTP连接的方式和WebSocket的方式. HTTP 连接的方式又有2种实现方式:

  1. 我们提供一个公开的endpoint, 当Slack的Channel 有消息的时候, Slack 以webhook的方式及时通知我们, 我们的app 给出回复.
  2. 我们不提供endpoint, 我们连接Slack的endpoint, 以固定时间间隔的方式去poll 消息, 如果有新消息, 我们的app 给出回复.

以上这2种方式都不是很好的方式, 对于第一种, 我们要提供一个公开的endpoint, 在我们的生产环境基本不可能, 对于第二种方式, 不是基于事件的, 不管有没有消息, 都要固定的去poll, 对于一个app 里面有多个实例的情况, 还要控制那个实例去poll.

所以 WebSocket 的方式是最好的, 不过当时就有开发人员提出了意见: 我们的生产环境时不能连外网的, 要连只能通过http 代理, 而我们的生产环境的代理只支持 http 代理, 不支持sock 方式, 所以这种 WebSocket 行不通. 是真的吗?

在我们看看 WebSocket 到底是什么之前, 先做一个 WebSocket 的例子: 使用Node.js 开一个WebSocket 服务, 然后在浏览器开一个 WebSocket 客户端.

WebSocket 服务端

  1. 新建一个文件夹

    mkdir wsServer
    cd wsServer
  2. 新建一个 package.json

    vim package.json

    输入如下代码

    {
     "name": "wsServer",
     "version": "0.0.1",
     "type": "module",
     "dependencies": {
       "ws":"8.8.1"
     }
    }
  3. 新建一个服务端代码文件

    vim server.js

    输入如下代码:

    var WebSocketServer = require('ws').Server
    var fs = require('fs')
    
    const wss = new WebSocketServer({ port: 8080 });
    
    wss.on('connection', function connection(ws) {
     ws.on('message', function message(data) {
     console.log('received: %s', data);
     ws.send(data + " -> server ack");
     });
    
     fs.watch('/tmp/', (eventType, fileName) => {
     console.log('Get event on file ' + fileName + ', type: ' + eventType);
     ws.send('file -> ' + fileName + ' -> ' + eventType);
     })
    
     ws.send('welcome to WebSocket world!');
    });

    上面这段代码做3件事情:

  4. 启动 WebSocket 服务器在8080 端口上, 当有人来连接的时候, 发送欢迎消息;
  5. 当收到客户端消息的时候, 打印收到的消息, 并且发送给客户端 ack 消息;
  6. 监听本地 /tmp 文件夹的文件变动事件, 打印日志, 并推送给客户端;

启动服务端代码:

node server.js

WebSocket 客户端

在 Chrome 浏览器打开任意页面的控制台, 输入如下JavaScript 代码

const ws = new WebSocket('ws://10.249.64.103:8080');

// Listen for messages
ws.addEventListener('message', (event) => {
    console.log('Message from server: ', event.data);
});

document.addEventListener('click', (event) => {
    console.log("just clicked");
    ws.send('click on (' + event.x + ', ' + event.y + ')');
});

上面的代码做下面的事情:

  1. 连接服务端 WebSocket;
  2. 然后当收到服务器端消息的时候, 打印收到的消息;
  3. 当页面上收到点击事件的时候, 推送给服务端点击事件的坐标;

运行效果

客户端效果:
client.png

服务器端效果:
server.png

使用代理

因为客户端使用的是浏览器, 可以设置在chrome 设置对于这个IP 启用代理, 使用http 代理, 依然能联通server 端, 正常运转.

使用curl

使用curl 只能被动的接受服务器传来的消息, 不能发送任何消息. (这个 Sec-WebSocket-Key 是从浏览器刚才发送的历史中复制过来的)

curl \
    --include \
    --no-buffer \
    --header "Connection: Upgrade" \
    --header "Upgrade: websocket" \
    --header "Host: 10.249.64.103:8080" \
    --header "Origin: http://10.249.64.103:8080" \
    --header "Sec-WebSocket-Key: 1TFTcjPQ7iG2XvsZ83WgZg==" \
    --header "Sec-WebSocket-Version: 13" \
    http://10.249.64.103:8080

效果(curl 对回应消息只能拼接, 不换行):
server.png

JavaScript WebSocket API 文档

官方文档在这里: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket
这个 WebSocket 类很简单:

  1. 构造函数给出服务器的地址, 包括ws协议, host 加 port;
  2. 有几个字段: 比如 url, readyState, protocol, bufferedAcmount, binaryType等, 有些是只读的;
  3. 客户端只有发送 send() 和 close() 方法;
  4. event handler:

    1. message: 当收到消息时;
    2. open: 当连接建立时;
    3. error: 当发送错误时;
    4. close: 当关闭连接时;

Node.js 的 WebSocket 实现库 ws 的文档

官方文档: https://www.npmjs.com/package/ws#sending-and-receiving-text-data
它不仅仅包含一个server端的API实现, 还包含一个做为 Node.js 客户端端代码实现

WebSocket 协议

官方文档: https://www.rfc-editor.org/rfc/rfc6455
当我们看过上面的 WebSocket 的例子之后, 再来看这个RFC 文档, 就不是那么难了.

为什么需要 WebSocket 协议

在 web 通信等某些场景下, 如果服务端发生了某些事件, 需要实时推送给客户端. 在传统的基于web的技术下, 需要客户端不断的去poll消息, 不管服务端到底有没有事件更新, 每次poll 都需要客户端发送一个http request, 并且如果发送的频率过低, 可能不能及时收到服务端事件更新, 如果频率过高, 又会对网络和服务端造成一些压力.

所以, 如果建立一个连接的情况下, 服务端事件变更主动推送客户端, 客户端只要等待就好了, 就完美解决了这个问题, 于是就有了 WebSocket 协议.

WebSocket 基本介绍

  1. 它可以使客户端和服务端在一个连接里面双向不间断通信;
  2. 通信过程分2阶段, 先是使用http协议握手连接, 然后以数据帧的方式双向发送数据;
  3. 广泛使用在游戏, 股票等需要实时消息通讯等软件中;
  4. WebSocket 可以使用现有web的 proxy和认证等成熟的机制;
  5. 通常开在80或443, 可以通过防火墙;

通常的 连接握手协议:

The handshake from the client looks as follows:

        GET /chat HTTP/1.1
        Host: server.example.com
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
        Origin: http://example.com
        Sec-WebSocket-Protocol: chat, superchat
        Sec-WebSocket-Version: 13

   The handshake from the server looks as follows:

        HTTP/1.1 101 Switching Protocols
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
        Sec-WebSocket-Protocol: chat