Mosquitto官方提供了一个测试地址供我们使用。其中8883和8884端口是支持TLS/SSL的。区别在于端口8883只需要客户端验证服务端的链接即可,端口8884需要双向验证客户端也需要向服务器上传证书。
两个端口的测试代码我在之前的文章iOS开发中MQTTKit的TLS/SSL支持方案已经提供。关于客户端证书,我们需要向Generate a TLS client certificate for test.mosquitto.org请求。根据官网的提示,我们可以借助openssl来实现。
- 生成密钥
openssl genrsa -out client.key
- 生成CSR文件
openssl req -out client.csr -key client.key -new
执行完这两个命令之后终端当前所在路径下会生成两个文件
用记事本将csr文件的内容拷贝进Generate a TLS client certificate for test.mosquitto.org的输入框,验证合法之后就会自动下载生成的客户端证书client.crt。我们将client.key和client.crt导入工程,可是一直验证的结果是身份验证失败。但是端口8883是没有问题成功建立连接的。
先简单捋一下TLS/SSL的流程
双向验证多出的部分就在于对客户端证书的请求和验证。我们首先应该排除密钥和证书不匹配的问题,因为在SSL_Connect()
之前配置客户端密钥和证书的时候我们已经通过SSL_CTX_check_private_key
验证过了。那么客户端证书和密钥没有问题的情况下,链接究竟是被谁主动断开的?
- 服务端断开
最大的可能是客户端证书验证不通过 - 客户端断开
配置代码出现问题/服务端证书验证不过/参数设置出错/openssl的问题...
这个时候我们可以借助一下Wireshark来帮助我们查看一下客户端和服务端之间通讯的情况究竟是怎样。首先我们查看一下连接成功的端口8883整个通讯过程
我们简单分析一下。No.73之后的报文是PINGREQ和PINGRES的过程,查看Time也可以发现No.72和No.73两个报文之间间隔了60s的实现,这一部分我们不需要关心。No.34 - No.72是TCP的三次握手,TLS/SSL连接验证,MQTT协议CONNECT和ACK的整个流程。
以下是作者手画的流程,已经过滤了TCP的三次握手
图中标示的包的大小包含了IP首部和TCP首部,所以不是MQTT协议报文的真实大小,为了大家对比方便我就按照Wireshark显示的大小来标注了。这个流程对比上面标准的TLS/SSL流程少了很多的步骤。报文中我们可以看到
PSH
这个标志位,这是告诉当前网络立刻将当前的数据发送出去,其中一些步骤可能合并成一个报文发出。因为TCP协议本身有一个特性是ACK捎带,所以报文看起来比较混杂而不是清清楚楚的Request和ACK一一对应。MQTT协议本身的内容因为被加密无法被Wireshark识别出来,所以识别成了TCP报文。
分析完端口8883,让我们看一下端口8884的通讯情况
这个看起来就简单多了。
- TCP三次握手
- 大小为359的报文发出,这是Client Hello
- 1480是Server Hello的部分
- 再往下看就和端口8883不一样了,之前端口8883服务端发送了大小为976的报文,但是端口8884发送的报文大小是1018。为什么报文的大小不一样呢?因为双向验证中服务器会多发送Cerificate Request的部分,这对于单向验证的端口8883是可选略过的。
- No.25是No.24的确认,No.26客户端发送给服务端的报文FIN置1了。
也就是说连接是在客户端接收了服务端证书和请求之后,被客户端主动关闭的。我尝试在源码添加了打印信息,着重在证书验证的部分,在对服务端证书验证的回调int _mosquitto_server_certificate_verify(int preverify_ok, X509_STORE_CTX *ctx)
里我找到了一个返回错误码的地方。
return _mosquitto_verify_certificate_hostname(cert, mosq->host);
作者在这添加了一个FIXME,提示我们如果你的openssl版本足够新的话(>=1.1.x),这里请使用X509_check_host()
。在修改代码之后这一部分验证成功,但是...
客户端还是发送了一个FIN关闭了连接。 = 。=
因为Client Hello是成功发送并接收到Server Hello的,这说明SSL这一部分的代码,SSL_Connect()
之前配置的部分是没有问题的。我尝试打印了SSL_Connect()
的返回码,果然出现了问题,返回了SSL_ERROR_SSL = 1
。我获取了具体出错的原因
char msg[1024];
ERR_error_string_n(ERR_get_error(), msg, sizeof(msg));
_mosquitto_log_printf(mosq, MOSQ_LOG_ERR, "SSL Connect Error: %s", msg);
rsa routines:RSA_sign:digest too big for rsa key"
这里有一个具体的讨论OpenSSL - What is the reason for error "SSL negotiation failed: error:04075070:rsa routines:RSA_sign:digest too big for rsa key"。如果你有兴趣可以自行查看一下讨论,我在这里简单总结一下。
记得Mosquitto测试地址提示我们可以使用Openssl来生成私钥,使用的Command是openssl genrsa -out client.key
。缺省情况下生成的是RSA-512。但是
Or use another hash type for signature which can produce not more than 53 bytes of hashed data. (i.e MD5, SHA1, SHA256, SHA384) while using 512-bit keys. OpenSSL by default uses SHA512 hash for signature. Change the code to use any other hash.
512 bit(64 byte) RSA key can only encrypt 53 bytes at max. 64 - 11 byte padding and SHA512 produces 64 bytes of hashed data.
Openssl默认使用的是SHA512来签名。RSA-512包含64个字节,移除了padding部分能够编码的最大长度是64 - 11 = 53个字节,但是SHA512生成了64个字节。我们可以使用其它的哈希类型来签名比如说(MD5, SHA1, SHA256, SHA384) ,或者使用RSA-1024及其它更长的密钥。
所以我们生成证书的时候后面要添加一个长度参数
openssl genrsa -out client.key 1024
重新生成客户端密钥和证书以后,端口8884的测试顺利通过了 :)