HTTPS安全探秘:黑暗中的证书游戏 | 从自签名到CA机构,解密搭建SSL证书的不为人知之道插图

首先,为什么要陷入这个SSL证书的迷宫呢?或许你会说,搞个自签名证书不就行了吗?答案并非如此。在开发阶段,自签名证书或许足够应付,但一旦进入生产环境,浏览器证书的严格验证使得自签名证书陷入不被信任的状态。这意味着用户每次都需要手动点击才能继续访问,对于即将进行自动化项目的我们而言,这种方式是行不通的。或许你会提到在阿里云、华为云等平台上申请免费证书,但很遗憾,由于项目特殊要求,最终我们的部署环境将完全无法进行外网访问,我们甚至需要自建DNS服务器。或许你会建议使用HTTP而不是HTTPS,但项目中使用WebRTC进行音视频多人会议,而WebRTC只能在HTTPS下运行。

实际上,前述说法有一个需要更正的地方,自签名证书理论上是可行的,但一旦要分发给超过100个客户端,这就变成了一场灾难。为了确保可用性,我们采用了更智慧的方式——通过Windows域控对所有下属计算机进行证书分发。这样一来,不仅解决了自签名证书分发的繁琐问题,也保证了整体系统的可靠性。这正是我们在SSL证书领域中所经历的一场必要而曲折的冒险。

HTTPS安全探秘:黑暗中的证书游戏 | 从自签名到CA机构,解密搭建SSL证书的不为人知之道插图1

1.原理

SSL证书的信任机制其实是非常简单的,第一需要一个机构证书,第二是需要服务端证书,一般来说机构证书被称为CA证书,而服务端证书就称为服务器证书吧。那么为啥https非常安全呢?答案其实不复杂,下面就是一段逻辑性描述来说明为啥https是安全的。

通常情况下我们在给Nginx、Tomcat、IIS上配置的证书便是服务器证书,那么它是怎么保证客户端访问的地址绝对没有被拦截修改的呢?其实也不复杂,当我们的浏览器发起一个请求的时候到服务端上时,对应web服务器会通过证书的秘钥将http响应值进行一次加密,然后将密文与明文同时返回出来,客户端浏览器接收到响应之后会将密文对称解码然后和明文进行对比,这样一来便可以保证响应值没有被串改。

这个时候逻辑上稍微厉害一点都会发现一个问题,客户端是怎么解码的?这里的答案就是服务端在响应的时候同时会将证书的公钥也返回,这个公钥只能解码对应私钥加密的信息,同时这个公钥无法加密只能解密,这样一来如果如果某人想要拦截http请求便必须知道对应的私钥才行,否则浏览器一旦发现解密信息对不上便知道了响应数据已经被拦截修改过了。

如果你反应过来了你会发现一个新的问题,那么假设拦截这自己搞了一对有效的私钥和公钥然后伪装为服务器不就行了,恭喜你盲生发现了华点。这里就需要CA证书来处理了。其实服务器证书的公钥是由CA证书的秘钥配对加密来的,这样一来当请求返回的服务器公钥和通过CA证书进行验证时便会发现这个公钥是不是由机构签发的公钥,一旦对应不上则说明服务器不是原来CA证书签发服务器证书,这就证明你的请求被第三方拦截了。同时CA证书对于浏览器而言只有公钥,也就是说安装证书时本质上就是将CA证书的公钥导入到你的电脑上了,至此除开CA机构的证书发放者没有知道CA证书的秘钥是什么这样一来便可以保证下面几个非常关键的安全性:

  • 你请求的服务绝对是官方的服务器,绝对不是黑客自建的服务器。
  • 服务器响应给你的数据绝对是正确的,期间黑客绝对无法对其进行修改。

证书的结构如下:

HTTPS安全探秘:黑暗中的证书游戏 | 从自签名到CA机构,解密搭建SSL证书的不为人知之道插图2

这里还有一个问题便是这些CA证书是哪来的,自己的电脑上又重来没有导入过什么证书。这里便是一个非常无耻躺着赚钱的商业模式了,微软、谷歌、苹果等公司提供了操作系统和浏览器,他们便是第一方的CA机构,他们的系统自己肯定信任自己对吧?所以系统安装的时候他们的CA公钥已经安装到你们的系统里面了,然后这几家巨头合伙说那么这些CA公钥在每种系统都有,然后就是一写第三方公司和这些巨头打成了合作,这些公司的机构证书也被巨头们信任所以理所当然的入库了,这些三方机构便是大名鼎鼎的SymantecGeoTrustLet's Encrypt几个巨头,这些机构一个单域名的签名证书都敢直接拿出来卖,一年好几千,对他们而言无法就是给下发的证书进行一次签名而已,真正的躺着赚钱。

2.开始制作证书

这里我使用的证书工具是openssl,经典工具,坦白的说非常难用。

2.1创建CA证书

首先第一步肯定是制作一个机构证书也就是CA证书出来,这里有两种方案,第一是直接用openssl创建CA证书,另一种是windows域控生成域组织的CA证书,我们分开说。

2.1.1通过openssl创建CA证书

第一步是创建一个秘钥,这个便是CA证书的根本,之后所有的东西都来自这个秘钥:

# 通过rsa算法生成2048位长度的秘钥

openssl genrsa -out myCA.key 2048

第二步是通过秘钥加密机构信息形成公钥:

# 公钥包含了机构信息,在输入下面的指令之后会有一系列的信息输入,这些信息便是机构信息,公司名称地址什么的

# 这里还有一个过期信息,CA证书也会过期,openssl默认是一个月,我们直接搞到100年

openssl req -new -x509 -key myCA.key -out myCA.cer -days 36500

这一步需要输入的机构信息有点,分别说一下:

参数名称参数值
Country Name国家代码,比如中国就是CN
State or Province Name省名称
Locality Name城市名称
Organization Name机构名称
Organizational Unit Name机构单位名称
Common Name重点参数:授权给什么,因为机构是根节点所以是授权给自己
Email Address邮件地址

2.1.2通过windows域控创建CA证书

这种便是我采用的方案,执行上比直接用openssl创建证书复杂多了,但是好处也非常多,一方面域控下级的所有计算机天然对域控服务就是信任状态,第二是域控制器能够通过组策略域内同步CA证书,本质上来讲相对于多了一个CA证书同步与分发的机制。我这边使用的Windows Server 2016,其他版本区别也不大。

第一步是在域控上启用证书服务

HTTPS安全探秘:黑暗中的证书游戏 | 从自签名到CA机构,解密搭建SSL证书的不为人知之道插图3
HTTPS安全探秘:黑暗中的证书游戏 | 从自签名到CA机构,解密搭建SSL证书的不为人知之道插图4

第二步是安装完毕之后配置证书

这里非常简单,我都不想说了,直接根据提示输入相关信息就行了,在过期时间那一步最好将时间拉长,我还是使用的100年。

第三步是通过组策略进行分发

策略路径是:计算机策略/Windows设置/安全设置/公钥策略/受信任的根证书颁发机构计算机策略/Windows设置/安全设置/公钥策略/受信任的发布者证书。将上面创建的证书导出之后,在这里导入即可。

2.2创建服务器证书

在得到CA证书之后,需要通过openssl工具对证书进行转换得到公钥(.crt文件)和密钥(.key文件),无论CA证书是怎么来的到这里之后就没有任何区别了,服务器证书的制作流程相较CA证书要复杂一点点。

第一步通过openssl工具创建服务器的秘钥:

# 通过RSA算法生成长度2048位的秘钥

openssl genrsa -out server.key 2048

第二步这里是创建一个签名请求

需要将服务器信息写入到请求文件之中,然后通过CA机构证书对请求签名形成服务器证书公钥,这一步要复杂一些,很多网上的教程在这里都GG了主要原因没有把原理搞清楚。

首先https证书的公钥不同于自定义情况下的加密证书,这里需要安装浏览器标准进行配置,首先openssl默认的证书版本是V1,V1在支持https时部分浏览器依旧会认为不安全,所以需要使用V3版本;同时openssl即便是使用V3版本依旧没有附带V3的subjectAltName字段数据(这里是证书对应的IP地址或者域名,可以用通配符)。但是这些东西命令行没法指定所以需要配置文件,我这里准备了一个:

tsa_policy2 = 1.2.3.4.5.6
tsa_policy3 = 1.2.3.4.5.7
[ ca ]
default_ca = CA_default  # The default ca section
[ CA_default ]
dir  = ./demoCA  # Where everything is kept
certs  = $dir/certs  # Where the issued certs are kept
crl_dir  = $dir/crl  # Where the issued crl are kept
database = $dir/index.txt # database index file.
new_certs_dir = $dir/newcerts  # default place for new certs.
certificate = $dir/cacert.pem  # The CA certificate
serial  = $dir/serial   # The current serial number
crlnumber = $dir/crlnumber # the current crl number
crl  = $dir/crl.pem   # The current CRL
private_key = $dir/private/cakey.pem# The private key
RANDFILE = $dir/private/.rand # private random number file
x509_extensions = usr_cert  # The extentions to add to the cert
name_opt  = ca_default  # Subject Name options
cert_opt  = ca_default  # Certificate field options
default_days = 365   # how long to certify for
default_crl_days= 30   # how long before next CRL
default_md = default  # use public key default MD
preserve = no   # keep passed DN ordering
policy  = policy_match
[ policy_match ]
countryName  = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = optional
commonName  = supplied
emailAddress  = optional
[ policy_anything ]
countryName  = optional
stateOrProvinceName = optional
localityName  = optional
organizationName = optional
organizationalUnitName = optional
commonName  = supplied
emailAddress  = optional
[ req ]
default_bits  = 1024
default_keyfile  = privkey.pem
distinguished_name = req_distinguished_name
attributes  = req_attributes
x509_extensions = v3_ca # The extentions to add to the self signed cert
string_mask = utf8only
req_extensions = v3_req # The extensions to add to a certificate request
[ req_distinguished_name ]
countryName   = Country Name (2 letter code)
countryName_default  = CN
countryName_min   = 2
countryName_max   = 2
stateOrProvinceName  = State or Province Name (full name)
stateOrProvinceName_default = BeiJing
localityName   = Locality Name (eg, city)
0.organizationName  = Organization Name (eg, company)
0.organizationName_default = myca
organizationalUnitName  = Organizational Unit Name (eg, section)
commonName   = Common Name (e.g. server FQDN or YOUR name)
commonName_max   = 64
emailAddress   = Email Address
emailAddress_max  = 64
[ req_attributes ]
challengePassword  = A challenge password
challengePassword_min  = 4
challengePassword_max  = 20
unstructuredName  = An optional company name
[ usr_cert ]
basicConstraints=CA:FALSE
nsCertType = client, email, objsign
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
nsComment   = "OpenSSL Generated Certificate"
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
[ svr_cert ]
basicConstraints=CA:FALSE
nsCertType   = server
keyUsage = nonRepudiation, digitalSignature, keyEncipherment, dataEncipherment, keyAgreement
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
extendedKeyUsage = serverAuth,clientAuth
[ v3_req ]
subjectAltName = @alt_names
# 这里是重点,需要将里面配置为最终服务端需要的域名或者IP
# 这里可以写多个,能够自行添加DNS.X = XXXXXX
[ alt_names ]
DNS.1 = xunshi.com
DNS.2 = *.xunshi.com
[ v3_ca ]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
basicConstraints = CA:true
[ crl_ext ]
authorityKeyIdentifier=keyid:always
[ proxy_cert_ext ]
basicConstraints=CA:FALSE
nsComment   = "OpenSSL Generated Certificate"
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo
[ tsa ]
default_tsa = tsa_config1 # the default TSA section
[ tsa_config1 ]
dir  = ./demoCA  # TSA root directory
serial  = $dir/tsaserial # The current serial number (mandatory)
crypto_device = builtin  # OpenSSL engine to use for signing
signer_cert = $dir/tsacert.pem  # The TSA signing certificate
     # (optional)
certs  = $dir/cacert.pem # Certificate chain to include in reply
     # (optional)
signer_key = $dir/private/tsakey.pem # The TSA private key (optional)
default_policy = tsa_policy1  # Policy if request did not specify it
     # (optional)
other_policies = tsa_policy2, tsa_policy3 # acceptable policies (optional)
digests  = md5, sha1  # Acceptable message digests (mandatory)
accuracy = secs:1, millisecs:500, microsecs:100 # (optional)
clock_precision_digits  = 0 # number of digits after dot. (optional)
ordering  = yes # Is ordering defined for timestamps?
    # (optional, default: no)
tsa_name  = yes # Must the TSA name be included in the reply?
    # (optional, default: no)
ess_cert_id_chain = no # Must the ESS cert id chain be included?
    # (optional, default: no)

将上面的配置内容保存为openssl.cnf放到生成的服务器证书文件的目录下(注意:修改alt_names里面的域名或者IP为最终部署需要的地址,支持通配符),然后执行创建签名申请文件即可,执行运行:

# 和创建CA时一样这里需要输入一堆服务器信息,输入项也是相同的。

# 不过在输入Common Name(CN)最好直接输入服务器的IP地址或者域名。

openssl req -config openssl.cnf -new -out server.req -key server.key

第三步通过CA机构证书对服务器证书进行签名认证

# 这里没有什么需要说的,本质上就是将签名请求文件进行签名最终得到服务器的公钥

openssl x509 -req -extfile openssl.cnf -extensions v3_req -<strong>in</strong> server.req -out server.cer -CAkey myCA.key -CA myCA.cer -days 36500 -CAcreateserial -CAserial serial

第四步部署证书

这里应该没有什么需要说的了,我们通过Nginx部署,最终得到server.key就是秘钥,server.cer文件就是公钥只需要配置给Nginx就行了。

3.信任CA机构证书

如果通过Windows域控创建的CA证书,其证书本身通过组策略便可以给每一个域下计算机添加机构信任。如果你没有域控只是通过openssl创建的CA证书也没有关系,只需要将CA证书的公钥(myCA.cer文件)导入到系统信任的根证书颁发机构里面就行了:

HTTPS安全探秘:黑暗中的证书游戏 | 从自签名到CA机构,解密搭建SSL证书的不为人知之道插图5

这个界面在windows的internet选型->内容->证书可以打开,导入即可,也可以直接双击cer文件进行证书安装,最终不光是windows系统,任何操作系统都可以安装证书来进行对CA机构的进行信任操作。

在对证书进行信任之后通过https打开浏览器进入内网DNS或者host配置的域名便可以得到没有任何警告的内容的安全连接:

HTTPS安全探秘:黑暗中的证书游戏 | 从自签名到CA机构,解密搭建SSL证书的不为人知之道插图6

如果是Mac系统访问逻辑也是一样的通过安装CA证书并且在钥匙串内添加信任之后依然可以正常访问:

HTTPS安全探秘:黑暗中的证书游戏 | 从自签名到CA机构,解密搭建SSL证书的不为人知之道插图7

在Android手机上也是一样,安装并且信任证书之后可以正常访问:

HTTPS安全探秘:黑暗中的证书游戏 | 从自签名到CA机构,解密搭建SSL证书的不为人知之道插图8

4.总结

深入学习了HTTPS的认证逻辑,发现这个领域充斥着大量垃圾内容和误导。在我的学习过程中,我对openssl有了更深的了解,至少在理论上算得上及格。但是,我必须承认,这个过程让我感到非常不愉快。

事实上,理解HTTPS并不是什么难事,我只花了一天半的时间就搞定了。然而,中文互联网上充满了充斥着一些所谓“干货”的博客,内容千篇一律,大多数都是抄袭而来,浪费了大家的时间。在整个中文互联网检索体系下,我竟然找不到一篇文章详细描述HTTPS搭建的逻辑与流程,这真是太离谱了。最终,我只能从HTTPS的原理和openssl的官方文档开始自己摸索。

说到HTTPS的原理,理解之后并不意味着绝对安全。我想请各位思考一下,如何伪造一个页面?假设我是黑客,只需要一个小小的脚本就能实现入侵。我们可以自行制作CA和服务证书,通过修改HOST文件对域名解析进行劫持,将用户引导到我们自己的服务器。然后,将我们自己制作的CA证书注入目标电脑的受信任证书组。这样一来,被入侵者看到的是安全连接,但实际上,他们的请求已经被我们拦截了。

因此,不要因为看到了HTTPS就觉得安全了。一旦你的电脑本身被入侵,HTTPS也就形同虚设。在执行高风险操作时,最好还是查看站点的证书,确认对应的CA机构是否被修改过。这是一个黑暗而真实的世界,保持警惕才能真正守护自己的安全。