第三节:HTTPS数字证书和数字证书链

什么是数字证书和数字证书链

上一节说到SSL/TLS协议是为了解决三大风险而设计,第三点就是防止身份被冒充,防止身份被冒充的核心关键就是数字证书和数字证书链。下面”直白”的方式说下数字证书和数字证书链。

为了保证信息在传输中的安全和双方的身份不被冒充,HTTPS在建立安全链接阶段使用了公钥、私钥两把“钥匙”——非对称加密。非对称意思是:公钥加密的内容,只有私钥才能解密。私钥加密的内容,只有公钥才能解密。公钥是公开的,私钥保存在服务器端。

第三节:HTTPS数字证书和数字证书链 - 图1

由上图可看出只有拥有了“私钥”的服务器才能解密出“公钥”加密的对话内容。如果客户端获取到的公钥确实是由真正服务器生成的,那么就能确保了服务器的身份不是伪造的。现在问题来了,因为公私钥的生成算法是开源的,每个服务器都能提供并生成自己的一对公私钥,客户端如何确认拿到的公钥不是伪造的。

这里引出了另外一个安全机制,就是数字证书链。证书链的核心是证书中心(certificate authority,简称CA),合法CA的公钥是预存在操作系统和浏览器里的,只有通过了CA认证的服务器公钥才被浏览器客户端认为是可信的公钥。认证的原理很简单,依然是公私钥原理。CA拿自己的私钥去给需要认证的服务器公钥签名,生成一个“数字证书”。数字证书是包含了CA的签名,服务器自身公钥等等信息的集合体。浏览器拿着CA的公钥去验证该签名。只有被CA公钥验证通过的证书才是可信任的证书。有了这个逻辑,整个安全证书链信任系统就构成了。

客户端验证服务器证书
第三节:HTTPS数字证书和数字证书链 - 图2

如何取得“信任”

回到最初的思路分析:建立一个可以同时与客户端和服务端进行通信的网络服务。

现在需要解决的是如何得到客户端的信任,才能建立与客户端的通信。经过上面的分析,突破口就是CA证书。只要自定义的CA证书得到了客户端的信任,我就能用CA证书签发各种“伪造”的服务器证书。简单说就是让客户端系统安装上我们自定义的CA证书。

如何生成CA根证书

由于生成证书的方法是开源的,这里用到的是一个Node.js的库forge。但需要注意的是,使用什么样的方式生成CA根证书并不影响我们最终实现一个HTTPS中间人代理,如果你对openssl生成证书的方式比较熟悉,用openssl完成这一步也是可行的。

生成CA证书代码核心部分:

  1. const forge = require('node-forge');
  2. const pki = forge.pki;
  3. const fs = require('fs');
  4. const path = require('path');
  5. const mkdirp = require('mkdirp');
  6. var keys = pki.rsa.generateKeyPair(1024);
  7. var cert = pki.createCertificate();
  8. cert.publicKey = keys.publicKey;
  9. cert.serialNumber = (new Date()).getTime() + '';
  10. // 设置CA证书有效期
  11. cert.validity.notBefore = new Date();
  12. cert.validity.notBefore.setFullYear(cert.validity.notBefore.getFullYear() - 5);
  13. cert.validity.notAfter = new Date();
  14. cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 20);
  15. var attrs = [{
  16. name: 'commonName',
  17. value: 'https-mitm-proxy-handbook'
  18. }, {
  19. name: 'countryName',
  20. value: 'CN'
  21. }, {
  22. shortName: 'ST',
  23. value: 'GuangDong'
  24. }, {
  25. name: 'localityName',
  26. value: 'ShenZhen'
  27. }, {
  28. name: 'organizationName',
  29. value: 'https-mitm-proxy-handbook'
  30. }, {
  31. shortName: 'OU',
  32. value: 'https://github.com/wuchangming/https-mitm-proxy-handbook'
  33. }];
  34. cert.setSubject(attrs);
  35. cert.setIssuer(attrs);
  36. cert.setExtensions([{
  37. name: 'basicConstraints',
  38. critical: true,
  39. cA: true
  40. }, {
  41. name: 'keyUsage',
  42. critical: true,
  43. keyCertSign: true
  44. }, {
  45. name: 'subjectKeyIdentifier'
  46. }]);
  47. // 用自己的私钥给CA根证书签名
  48. cert.sign(keys.privateKey, forge.md.sha256.create());
  49. var certPem = pki.certificateToPem(cert);
  50. var keyPem = pki.privateKeyToPem(keys.privateKey);
  51. console.log('公钥内容:\n');
  52. console.log(certPem);
  53. console.log('私钥内容:\n');
  54. console.log(keyPem);

完整源码:../code/chapter3/createRootCA.js

npm script运行方式

  1. npm run createRootCA

执行完npm run createRootCA后,CA根证书的公私钥会生成到项目根路径的rootCA文件夹下:

公钥文件:rootCA/rootCA.crt
私钥文件:rootCA/rootCA.key.pem

安装CA根证书

⚠️注意:

1.必须要按照上面步骤先生成CA证书相关文件
2.每一次生成的证书和密钥都是独一无二的。

Windows

第一步:

首先双击打开证书文件rootCA/rootCA.crt

第二步:

第三节:HTTPS数字证书和数字证书链 - 图3

第三步:

第三节:HTTPS数字证书和数字证书链 - 图4

第四步:

第三节:HTTPS数字证书和数字证书链 - 图5

检查证书安装

命令行输入certmgr.msc,如下图可以看到新安装的证书

第三节:HTTPS数字证书和数字证书链 - 图6

Mac

项目根路径下执行下面命令

sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain rootCA/rootCA.crt

也可以直接运行npm script

  1. npm run installCAForMac

输入用户密码后即可安装成功。

检查证书安装

输入命令open /Library/Keychains/System.keychain 可查看安装情况如下图

第三节:HTTPS数字证书和数字证书链 - 图7

根据CA根证书生成对应不同域名的子证书

生成一个伪造的github的证书

  1. const forge = require('node-forge');
  2. const pki = forge.pki;
  3. const fs = require('fs');
  4. const path = require('path');
  5. const mkdirp = require('mkdirp');
  6. // CNanme
  7. var domain = 'github.com';
  8. var caCertPem = fs.readFileSync(path.join(__dirname, '../../rootCA/rootCA.crt'));
  9. var caKeyPem = fs.readFileSync(path.join(__dirname, '../../rootCA/rootCA.key.pem'));
  10. var caCert = forge.pki.certificateFromPem(caCertPem);
  11. var caKey = forge.pki.privateKeyFromPem(caKeyPem);
  12. var keys = pki.rsa.generateKeyPair(1024);
  13. var cert = pki.createCertificate();
  14. cert.publicKey = keys.publicKey;
  15. cert.serialNumber = (new Date()).getTime() + '';
  16. cert.validity.notBefore = new Date();
  17. cert.validity.notBefore.setFullYear(cert.validity.notBefore.getFullYear() - 1);
  18. cert.validity.notAfter = new Date();
  19. cert.validity.notAfter.setFullYear(cert.validity.notAfter.getFullYear() + 1);
  20. var attrs = [{
  21. name: 'commonName',
  22. value: domain
  23. }, {
  24. name: 'countryName',
  25. value: 'CN'
  26. }, {
  27. shortName: 'ST',
  28. value: 'GuangDong'
  29. }, {
  30. name: 'localityName',
  31. value: 'ShengZhen'
  32. }, {
  33. name: 'organizationName',
  34. value: 'https-mitm-proxy-handbook'
  35. }, {
  36. shortName: 'OU',
  37. value: 'https://github.com/wuchangming/https-mitm-proxy-handbook'
  38. }];
  39. cert.setIssuer(caCert.subject.attributes);
  40. cert.setSubject(attrs);
  41. cert.setExtensions([{
  42. name: 'basicConstraints',
  43. critical: true,
  44. cA: false
  45. }, {
  46. name: 'keyUsage',
  47. critical: true,
  48. digitalSignature: true,
  49. contentCommitment: true,
  50. keyEncipherment: true,
  51. dataEncipherment: true,
  52. keyAgreement: true,
  53. keyCertSign: true,
  54. cRLSign: true,
  55. encipherOnly: true,
  56. decipherOnly: true
  57. }, {
  58. name: 'subjectKeyIdentifier'
  59. }, {
  60. name: 'extKeyUsage',
  61. serverAuth: true,
  62. clientAuth: true,
  63. codeSigning: true,
  64. emailProtection: true,
  65. timeStamping: true
  66. }, {
  67. name: 'authorityKeyIdentifier'
  68. }]);
  69. cert.sign(caKey, forge.md.sha256.create());
  70. var certPem = pki.certificateToPem(cert);
  71. var keyPem = pki.privateKeyToPem(keys.privateKey);
  72. console.log(certPem);
  73. console.log(keyPem);

npm script运行方式

  1. npm run createCertByRootCA

执行完npm run createCertByRootCA后,CA根证书的公私钥会生成到项目根路径的cert文件夹下:

公钥文件:cert/my.crt
私钥文件:cert/my.key.pem

通过证书链的原理可以了解,获取CA认证的方法就是用CA证书的私钥给需要认证的子证书签名。上面的代码即是根据这个原理伪造了一个github.com域名的子证书。

证书中的Common Name字段表明了该证书对应的域名
第三节:HTTPS数字证书和数字证书链 - 图8

如果需要代表多个域名时需要用到扩展字段Subject Alternative Name
第三节:HTTPS数字证书和数字证书链 - 图9

另外和CA根证书最大的不同是,该子证书是用CA根证书的私钥签名,而CA根证书是用自己的私钥自签名。这也从代码的角度认识到了证书链的原理

  1. // 用CA根证书私钥签名
  2. cert.sign(caKey, forge.md.sha256.create());

第四节:一个简易的HTTPS代理