物联网网关 QT5 使用 SSL自签名证书访问云端HTTPS REST API 服务

 物联网网关 QT5 使用 SSL自签名证书访问云端HTTPS REST API 服务

老wu最近做的物联网网关项目,网关设备需要调用云端的REST API接口,该云端实现了某些业务逻辑,然后将资源封装成一个个URI接口,供给外部系统调用。

对于REST API来说,作为程序猿的你应该很熟悉了,我们身边的很多基础服务设施都对外提供了REST API 接口,比如我们要查城市天气信息,我们会调用气象服务的REST API,我们要查询导航地图信息,我们会调用高德地图的REST API,我们要查询快递物流信息,我们可以调用顺丰后台的REST API,对于智能设备来说,我们会调用科大讯飞的智能语音REST API、要做人脸识别,我们会求情Face Plus Plus 的 REST API等等。

云端将算法、数据、设施资源等封装成WEB 服务,实现端与端之间的架构解耦,端与端之间不必关心对方是如何实现的,用的什么编程语言来实现,可以彼此独立开发、测试、运行、部署。目前主流的Web服务实现方案中,因为REST模式与复杂的SOAP和XML-RPC相比更加简洁,越来越多的web服务开始采用REST风格设计和实现。

REST(英文:Representational State Transfer,又称具象状态传输)是Roy Thomas Fielding博士于2000年在他的博士论文中提出来的一种万维网软件架构风格,目的是便于不同软件/程序在网络(例如互联网)中互相传递信息。注意,REST是一种架构风格,而不是一种技术规范,它与HTTP、TCP、IP等RCF定义的协议标准不一样,REST并不是一种规范标准而是一种设计风格,REST通常基于使用HTTP,URI,和XML及JSON这些现有的广泛流行的协议和标准。

我们知道,对于基于公网链路的数据传输,数据包是很容易被监听和篡改的,也就是说对于明文传输的数据来说,是无法保证数据是安全可信的,这对于强调数据安全可信的服务(如:网银支付,用户登录,物联网设备控制等等)来说非常重要。为了保障数据传输过程中的安全可信,需要建立加密的数据链路,采用加密后的数据包实现密文传输,我们无法保证公网数据链路无法被监听,但是数据包以密文传输,即使数据包被截取,也无法获取明文语义,可以保证原始数据信息不被窃取和篡改。

传输层安全协议(Transport Layer Security 缩写: TLS),以及其前身安全套接层(Secure Sockets Layer 缩写: SSL)是一种安全协议,目的是为网际网络通信,提供安全及数据完整性保障。为了提供对网络服务器的身份认证,保护交换数据的隐私与完整性,1994年网景公司推出HTTPS协议,以SSL进行加密,这是SSL的起源。IETF将SSL进行标准化,1999年公布第一版TLS标准文件。随后又公布RFC 5246 (2008年8月)与 RFC 6176 (2011年3月)。在浏览器、电子邮件、即时通信、VoIP、网络传真等应用程序中,广泛采用了TLS协议。主要的网站,如Google、Facebook等也以这个协议来创建安全连接,发送数据。目前已成为互联网上保密通讯的工业标准。

SSL包含记录层(Record Layer)和传输层,记录层协议确定传输层数据的封装格式。传输层安全协议使用X.509认证,之后利用非对称加密算法来对通信方做身份认证,之后交换对称密钥作为会话密钥(Session key)。这个会话密钥是用来将通信两方交换的数据做加密,保证两个应用间通信的保密性和可靠性,使客户与服务器应用之间的通信不被攻击者窃听。

超文本传输安全协议(英语:Hypertext Transfer Protocol Secure,缩写:HTTPS,常称为HTTP over TLS,HTTP over SSL或HTTP Secure)是一种网络安全传输协议。在计算机网络上,HTTPS经由超文本传输协议进行通信,但利用SSL/TLS来加密数据包。HTTPS的主要思想是在不安全的网络上创建一安全信道,并可在使用适当的加密包和服务器证书可被验证且可被信任时,对窃听和中间人攻击提供合理的防护。

HTTPS利用SSL/TLS来加密数据包,TLS协议是与应用层协议独立无关的。高层的应用层协议(例如:HTTP、FTP、Telnet等等)能透明的创建于TLS协议之上。TLS协议在应用层协议通信之前就已经完成加密算法、通信密钥的协商以及服务器认证工作。在此之后应用层协议所传送的数据都会被加密,从而保证通信的私密性。

SSL/TLS协议采用公钥加密算法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。在客户端和服务器开始交换TLS所保护的加密信息之前,他们必须安全地交换或协定加密密钥和加密数据时要使用的密码。也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。但是,在建立起密文加密之前,数据是以明文传输的,也就是公钥是以明文传输的,如何保证公钥不被篡改?解决方法是:将公钥放在数字证书中。只要证书是可信的,公钥就是可信的。也就是要经过一个“握手阶段(handshake)”。

TLS通过使用一个握手过程协商出一个有状态的连接以传输数据。通过握手,客户端和服务端协商各种参数用于创建安全连接:

  • 当客户端连接到支持TLS协议的服务器要求创建安全连接并列出了受支持的密码组合(加密密码算法和加密哈希函数),握手开始。
  • 服务器从该列表中决定加密和散列函数,并通知客户端。
  • 服务器发回其数字证书,此证书通常包含服务器的名称、受信任的证书颁发机构(CA)和服务器的公钥。
  • 客户端确认其颁发的证书的有效性。
  • 为了生成会话密钥用于安全连接,客户端使用服务器的公钥加密随机生成的密钥,并将其发送到服务器,只有服务器才能使用自己的私钥解密。
  • 利用随机数,双方生成用于加密和解密的对称密钥。这就是TLS协议的握手,握手完毕后的连接是安全的,直到连接(被)关闭。如果上述任何一个步骤失败,TLS握手过程就会失败,并且断开所有的连接。

也就是首先服务器发回其数字证书,此证书通常包含服务器的名称、受信任的证书颁发机构(CA)和服务器的公钥。客户端收到服务器回应以后,首先验证服务器证书。如果证书不是可信机构颁布、或者证书中的域名与实际域名不一致、或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。如果证书没有问题,客户端就会从证书中取出服务器的公钥。

对于高安全要求的应用场合,服务器还会需要确认客户端的身份,就会再包含一项请求,要求客户端提供”客户端证书”。比如,金融机构往往只允许认证客户连入自己的网络,就会向正式客户提供USB密钥,里面就包含了一张客户端证书。

服务器是否是可信的,我们会基于服务器发回的数字证书,验证证书的服务器名称与我们要访问的HTTPS服务域名是否一致,证书颁发机构是否是受信任的以及证书是否在有效期内。

在密码学和计算机安全领域,根证书(root certificate)是一个无签名或自签名的识别根证书颁发机构(CA)的公钥证书。根证书是公开密钥基础建设方案中的一部分。大多数常用的商用方案基于ITU-T X.509标准,其通常包含一个来自证书颁发机构的数字签名。证书颁发机构可以在一个树结构中签发多个证书,根证书就是这个树结构中最顶层的证书,其私钥用于“签名”其他证书。在根证书之后的所有证书都会继承根证书的可信赖性——根证书的签名有点类似“公正”一个现实世界中的身份。进一步向下的证书还依赖于中间机构对它的信任,这通常被称为“从属证书颁发机构”。

我们的操作系统中会自带受信任的根证书办法机构的数字签名证书,如果您的服务器端SSL证书由全球信任的证书颁发机构(CA)验证服务器身份后颁发的,便能很方便的验证其合法性,但是需要额外的费用,需要根据证书的有效年限,向这些证书颁发机构付一定的保护费。

还有一种方式是采用自签名的数字证书,然后手动或者程序自动将数字证书加入到受信任的证书颁发机构中,实现数字证书的授信确认。

老wu这里使用的REST API服务将使用自签名的数字证书,基于TLS 1.2协议,采用服务器端证书和客户端证书双向认证的方式,实现数据的加密可靠传输。

下边老wu分享下实现的一些技术细节和坑点。

使用OpenSSL 生成服务器端及客户端证书

我们使用OpenSSL这款开源工具来生成服务器和客户端SSL连接使用的自签名证书。

如果你使用的是MAC OS 或者 Linux,OpenSSL默认已经安装,如果使用的是Windows系统,可以通过安装Git 工具,使用Git工具自带的shell或者使用OpenSSL的Windows安装版,通过这个链接进行下载:

http://slproweb.com/products/Win32OpenSSL.html

生成服务器证书私钥

在shell里输入命令

openssl genrsa -out server.key 2048

会在当前目录下生成一个server.key文件

接下来使用刚才生成的server.key证书私钥生成生成服务器证书请求(CSR)文件

openssl req -new -key server.key -out server.csr

回车执行命令后提示要输入您的相关信息。填写说明:

1.Country Name:

填您所在国家的 ISO 标准代号,如中国为 CN,美国为 US

2.State or Province Name:

填您单位所在地省/自治区/直辖市,如广东省或 Guangdong

3.Locality Name:

填您单位所在地的市/县/区,如广州市或 Guangzhou

4.Organization Name:

填您单位/机构/企业合法的名称,如吴川斌的博客或www.mr-wu.cn

5.Organizational Unit Name:

填部门名称,如技术支持部或 Technical support

6.Common Name:

填域名,如:api.mr-wu.cn。在多个域名时,填主域名,这项非常重要,我们在验证服务器证书时,会验证Common Name与http请求的url域名是否匹配,如果不匹配,ssl会返回错误。

7.Email Address:

填您的邮件地址,可以不必输入,按回车跳过

8.‘extra’attributes

都可以不填写,按回车跳过直至命令执行完毕。

这里要强调一下,除第 1、6、7、8 项外,2-5 的信息填写请统一使用中文或者英文填写,就是说中英文不要混用。

命令成功执行后,会在当前目录下生成certreq.csr这个文件

接下来生成服务器端证书

openssl x509 -req -days 2000 -in server.csr -signkey server.key -out server.crt

证书采用x509格式,-days 表面证书的有效期 老wu这里设置为2000天。命令成功执行后,在当前目录下生成server.crt证书文件。

接下来生成客户端证书

生成客户端的方法和步骤与上边描述的生成服务器证书步骤是一样的。

执行命令

openssl genrsa -out client.key 2048

openssl req -new -key client.key -out client.csr

openssl x509 -req -days 2000 -in client.csr -signkey client.key -out client.crt

现在,服务器端的私钥及数字证书和客户端的私钥和数字证书我们都成功生成了,接下要我们要把服务器端的私钥和数字证书以及客户端的数字证书上传到服务器端的HTTP Server里,然后进行配置,让HTTP Server 开启SSL连接并监听443端口。

老wu这里使用的Nginx,具体的配置方法以你使用的HTTP Server版本为准

 

[php]
listen 443 ssl http2  开启SSL 加密传输并监听于 443接口

ssl on; 开启ssl 加密传输

ssl_certificate /etc/nginx/ssl/api.mr-wu.cn.key/server.crt; 服务器证书路径

ssl_certificate_key /etc/nginx/ssl/api.mr-wu.cn.key/server.key; 服务器私钥路径

ssl_client_certificate /etc/nginx/ssl/api.mr-wu.cn.key/client.crt; 客户端证书路径

ssl_verify_client on; 开启客户端连接需要证书认证功能
[/php]

然后可以使用 curl 命令进行服务器端SSL连接的测试

[php]

curl -v -s -k –key client.key –cert client.crt https://api.mr-wu.com

[/php]

如果您发现无法访问,如果使用的云系统,例如阿里云,它会对服务器创建安全组,只允许开放的端口通孔,这里要把443接口配置到安全组中。

HTTPS Web Server搭建好了,至于REST API如何去实现,老wu这里就不具体讲了,用PHP框架、NodeJS框架还是JAVA框架都可以,就是配置Nginx的反向代理就好了。

接下来便是QT5 程序通过SSL来连接云端的REST API了,在基于QT5的程序实现中,我们利用QNetworkAccessManager类来管理网络连接请求,利用QNetworkRequest类来进行实现HTTP 的 post、get、delete等操作,利用QSslConfiguration类来实现SSL的连接配置,包括CA证书的配置,客户端证书及客户端私钥的配置,设置SSL 协议的版本,认证模式等等。

QT5这里有一个坑点,就是QT5对OpenSSL的编程实现了支持,但是却没有附带对应的二进制运行库,这就会使得你的程序代码编译是正确通过的,但是在IDE里头运行调试时,会报SSL错误:

[php]

QSslSocket: cannot call unresolved function SSLv23_client_method

QSslSocket: cannot call unresolved function SSL_CTX_new

QSslSocket: cannot call unresolved function SSL_library_init

QSslSocket: cannot call unresolved function ERR_get_error

QSslSocket: cannot call unresolved function ERR_get_error

[/php]

解决的方法是复制OpenSSL的运行库到QT中,对于windows版本来说是这两个dll链接库:

ssleay32.dll

libeay32.dll

你可以到下载OpenSSL已经编译好的安装包,然后拷贝这两个文件到QT中,或者像老wu这样,使用的是minGW的QT,在安装QT时选择附带安装minGW环境,minGW里头已经包含了opensll的运行库了。

老wu安装的QT 5.9.0 minGW版,QT安装到了D盘下的Qt目录下,则ssleay32.dll libeay32.dll这两个文件位于“D:\Qt\Qt5.9.0\Tools\mingw530_32\opt\bin”目录下,将他们拷贝到QT的bin目录下:“D:\Qt\Qt5.9.0\5.9\mingw53_32\bin”。

运行QT 的SSL连接程序不再报错,当然,你的程序编译发布后,也要记得带上openssl的链接库文件。

对于QT在ARM下运行,我们基于QT 的源文件进行交叉编译,在编译的时候,要将openssl也要编译进去。

下边是QT5 SSL 请求 REST API 的示例代码片段:

首选,得在QT的项目文件.pro中加入network的支持

QT += network

然后就是C++源码:

[php]

//判断系统是否支持OpenSSL
qDebug() << "支持OpenSSL: " << QSslSocket::supportsSsl();

// 创建客户端证书
QFile fileCrt("client.crt");
fileCrt.open(QIODevice::ReadOnly);
const QSslCertificate certificate(&fileCrt, QSsl::Pem);
fileCrt.close();

// 创建客户端私钥
QFile fileKey("client.key");
fileKey.open(QIODevice::ReadOnly);
const QSslKey prvateKey(&fileKey, QSsl::Rsa);
fileKey.close();

[/php]

这里QSslCertificate、QSslKey类构造的时候,传递的是QIODevice,这里是直接读取保存在磁盘中的key及crt文件,这样你的程序在发布是,也要在磁盘目录里附上key及crt文件,这样的做法并不太好,我们可以将key及crt文件加入到qt的资源文件中,或者将key及crt直接转换成QByteArray常量,直接在类构造时传入QByteArray即可,这样可以避免key和crt文件意外丢失或者被改动。

[php]

//通过QSslConfiguration 类进行SSL连接配置
QSslConfiguration config ;
//设置SSL验证模式 老wu这里设置为QSslSocket::VerifyPeer 在SSL握手是验证服务器端证书是否正确
config.setPeerVerifyMode(QSslSocket::VerifyPeer);
//使用TLS 1.2 协议版本 这得看你服务器端的支持情况
config.setProtocol(QSsl::TlsV1_2);
//将之前创建的客户端私钥prvateKey及客户端证书certificate加入到config中
config.setPrivateKey(prvateKey);
config.setLocalCertificate(certificate);

//由于是自签名的服务器端证书,我们还得将服务器端的证书叫入到CA证书数据库中,否则服务器端证书将验证失败。
QList<QSslCertificate> caCerList;
// 创建服务器端证书
QFile fileServCrt("server.crt");
fileServCrt.open(QIODevice::ReadOnly);
const QSslCertificate cACertificate(&fileServCrt, QSsl::Pem);
//将服务证书加入到CA列表中
caCerList << cACertificate;
config.setCaCertificates(caCerList);

//网络连接管路
QNetworkAccessManager *manager = new QNetworkAccessManager(this);

QUrl url("https://api.mr-wu.cn/v1/");
QNetworkRequest request(url);

//加入ssl配置信息
request.setSslConfiguration(config);
//设置rest api 数据内容为json格式
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");

connect(manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(syncRequestFinished(QNetworkReply*)));

connect(manager, SIGNAL(sslErrors(QNetworkReply*,QList<QSslError> )), this, SLOT(sslErrors(QNetworkReply*,QList<QSslError> )));

manager->get(request);

[/php]

感谢您耐心看完老wu的额这篇文章,如文中有错误的地方,欢迎留言指正,同时也希望这篇文章能办得到您,SSL的坑咱不睬,O(∩_∩)O~

 

吴川斌

吴川斌

Leave a Reply