node-tls/ssl

Node - TLS/SSL

在网络中,数据在服务端和客户端之间传递,由于是明文传递的内容,是很容易被第三方窃取的。当时NetSpace提出了SSL(Secure Sockets Layer, 安全套接层)。SSL作为一种安全协议,它在传输层提供对网络连接加密的功能。对于应用层而言,它是透明的,数据在传递到应用层之前就已经完成了加解密的过程。最初的SSL应用在Web上,被服务器端和浏览器端同时支持,随后IETF将其标准化,成为TLS(Transport Layer Security,安全传输层协议)。

Node在网络安全上提供了3个模块,分别为crypto、tls、https、其中crypto主要用于加解密;tls模块提供了与net模块类似的功能,区别在于它建立在TLS/SSL加密的TCP连接上;https与http接口一致,区别也仅在于它建立在安全的连接之上。

TLS/SSL是一个公钥与私钥的非对称性结构。每个服务器和客户端都有自己的公私钥。公钥用来加密要传输的数据,私钥用来加密收到的数据。公私钥是成配对的,通过公钥加密的数据,只能通过它对应的私钥来解密。故在建立安全传输之前,客户端和服务端需要互换公钥。客户端发送数据时需要服务端的公钥加密传输,服务器通过自己的私钥去解密数据。服务器发送、客户端接收,同理。

Node底层采用的openssl实现TLS/SSL的,为此要生成公私钥,可以通过openssl完成。
生成私钥

1
$ openssl genrsa -out server.key 1024

生成公钥

1
$ openssl rsa -in server.key -pubout -out server.pem

公私钥的对称加密虽好,但网络中依然可能存在中间人攻击窃密。在客户端与服务端交换公钥的过程中,中间人对客户端扮演服务端的角色,对服务端扮演客户端的角色,这样客户端与服务器端几乎感受不到中间人的存在。为了解决这个问题,TSL/SSL引入数字证书来进行验证。这个证书是第三方CA(Certificate Authority, 数字证书认证中心)颁发给站点的,且这个证书中具有CA通过自己的公钥和私钥实现的签名。为了得到签名证书,服务器端需要通过自己的私钥生成CSR(Cdrtificate Signing Request,证书签名请求)文件。CA机构通过这个文件颁发属于该服务器端的签名证书。通过CA机构颁发证书需要付出一定的精力和费用,对于中小型企业而言,采用自签名证书是中不错的选择。所谓自签名证书,就是自己扮演CA机构,给自己的服务器端颁发证书。下面重点说下自签名证书颁发的流程:

1
2
3
4
5
6
# 生成私钥
$ openssl genrsa -out ca.key 1024
# 生成CSR文件
$ openssl req -new -key ca.key -out ca.csr
# 通过私钥生成证书
$ openssl x509 -req -in ca.csr -signkey ca.key -out ca.crt

上述步骤完成了扮演CA角色需要的文件,接下来是服务端向CA机构申请证书

1
2
3
4
# 生成CSR文件
$ openssl req -new -key server.key -out server.csr
# 向CA机构申请签名,签名过程需要CA的证书和私钥参与,最终颁发一个带有CA签名的证书
$ openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.crt

CA机构将证书颁发给服务器端后,证书在请求的过程中会发送给客户端,客户端通过CA的证书验证真伪。如果是知名的CA机构,他们的证书一般预装在浏览器中。如果是自签名证书,客户端需要获取到CA的证书才能进行验证。

  • TLS服务
    构建证书都备齐之后,我们试通过Node的tls模块来创建一个安全的简单的服务:

    TlsServer.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    let tls = require('tls');
    let fs = require('fs');
    let options = {
    key: fs.readFileSync('./keys/server.key'),
    cert: fs.readFileSync('./keys/server.crt'),
    requestCert: true,
    ca: [fs.readFileSync('./keys/ca.crt')]
    };

    let server = tls.createServer(options, function (socket) {
    console.log('server connected', socket.authorized);
    socket.write('Welcome\n');
    socket.setEncoding('utf-8');
    socket.pipe(socket);
    });
    server.listen(8000, function () {
    console.log('server start');
    });

    TlsClient.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    let tls = require('tls');
    let fs = require('fs');

    let options = {
    key: fs.readFileSync('./keys/client.key'),
    cert: fs.readFileSync('./keys/client.crt'),
    rejectUnauthorized: false, // 自签名证书rejectUnauthorized为false
    ca : [fs.readFileSync('./keys/ca.crt')]
    };

    let client = tls.connect(8000, options, function () {
    console.log('client connected', client.authorized);
    process.stdin.pipe(client);
    });

    client.setEncoding('utf-8');
    client.on('data', function (data) {
    console.log('client data', data);
    });
    client.on('end', function () {
    console.log('client end');
    });
  • HTTPS
    HTTPS服务就是工作在TLS/SSL上的HTTP。了解了TLS后,创建HTTPS就简单了。接下来让我们了解下HTTPS是如何构建的:

    HttpsServer.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const https = require('https');
    const fs = require('fs');
    const options = {
    key: fs.readFileSync('./keys/server.key'),
    cert: fs.readFileSync('./keys/server.crt'),
    ca: [fs.readFileSync('./keys/ca.crt')]
    };

    https.createServer(options, function (req, res) {
    res.writeHead(200);
    res.end('Hello Https\n');
    }).listen(8000);

    HttpsClient.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    const https = require('https');
    const fs = require('fs');
    const options = {
    hostname: 'localhost',
    port: 8000,
    method: 'GET',
    path: '/',
    rejectUnauthorized: false, // 自签名证书rejectUnauthorized为false
    key: fs.readFileSync('./keys/client.key'),
    cert: fs.readFileSync('./keys/client.crt'),
    ca: [fs.readFileSync('./keys/ca.crt')]
    };

    let req = https.request(options, function (res) {
    res.setEncoding('utf-8');
    res.on('data', function (d) {
    console.log('client data', d) ;
    });
    });

    req.on('error', function (err) {
    console.log('client error', err);
    });
    req.end();