ssl自签名证书生成步骤及概念原理简介

2,117 阅读16分钟

前言

现如今在公网上访问的大多数网站都是https协议了,https协议可以说是现在互联网的安全的基石。但是安全算法中的各种概念和流程总让人晕头转向,网上的博文也多如牛毛,但质量参差不齐,很多文章互相矛盾甚至自相矛盾,本文是笔者基于自己的理解,试图对https背后的协议进行简单描述,以求建立起对整个https协议的大体直觉,最终我们将自签名证书应用在自己的服务上,并且让我们的浏览器认可这个证书。

握手流程

首先https协议在网络的osi 7层架构中处于最顶层应用层,他依赖于ssl协议(Secure Socket Layer安全套接层), 而ssl工作在应用层之下(大概在传输层,各种说法都有,不必纠结),而ssl协议是通过一系列握手过程保证连接安全性的。

ssl的大概思路是,使用安全但复杂的非对称加密方式(RSA)让客户端传输一个加解密口令给服务端,收到以后服务端就用这个口令与客户端进行高效的对称加密通信。那么这里面就有一个关键点——非对称加密必需安全。非对称加密的流程中,客户端需要获取一个服务端指定的公钥(实际中的载体是证书),用这个公钥给口令加密,然后发给服务端,服务端用私钥解密后就开始对称加密的阶段了。

中间人问题与基于CA机构的解决方式

那么如果现在有个“中间人”拿着自己生成的公钥欺骗客户端说:“我手里这个就是服务端的公钥,给你用吧。”客户端使用这个公钥传上去口令信息就会被“中间人”知道,“中间人”知道口令后用真正的公钥替客户端走完了与服务端的握手环节,那么后续客户端在网络上的信息对于“中间人”来说就都是可以解密出来的,这调链路上的信息就全都被“中间人”监听了。这就是“中间人”攻击。那客户端直接找服务端要公钥行不行?也不行,因为客户端并不“认识”服务端,他没有能力判断这个IP地址就是服务端的地址。

那么如何避免这个问题呢?简单说就是不相信来历不明的公钥。具体来说,在网络上有几家有公信力的平台(CA机构),任何提供服务的服务器都可以把自己的公钥放在这里(要钱的),而客户端如果想访问哪个服务,就问这个平台要公钥,平台负责把公钥发出去。就好比大家都为了保险,都把钥匙挂在天安门的国旗旗杆上,因为天安门国旗旗杆只有一个,且大家都认识,所以没办法造假。CA机构(有公信力的大平台)实际的操作方式是用自己的私钥对服务器的证书(公钥)的全文进行签名,然后把签名CA机构自己的公钥以及有https需求的服务端证书一起打包成一个经过CA机构签发的证书。用前面的例子就是——把旗杆上的每一把钥匙又放进单独的盒子里,每个盒子都可以用国旗下面的同一把钥匙打开取出来。这里面国旗下的钥匙,也就是CA机构自己的公钥及相关信息,因为没人替他提供托管服务了处于这整个链路的根源处,所以叫根证书。因为权威的CA机构也就那么几家,所以很多浏览器直接内置了大部分CA机构的根证书,相当于就那几把钥匙天天要使,于是自己配了一套,需要的时候直接用。

简单总结就是,客户端用对少数几个“一定不是中间人”的CA机构的信任来规避中间人问题。

CA证书签发流程及客户端校验流程

从前面的介绍可以知道,如果想在互联网上用https协议,就得找一个CA机构来签发自己的证书,大概步骤如下:

1.在业务服务器上生成一对RSA密钥,openssl生成的密钥通常以.key结尾,其中同时包含了公钥和私钥信息。
2.业务服务器使用密钥对生成一个网站证书,其中的核心内容是公钥,后缀名通常是.crt。
3.使用网站证书向某个CA机构发出一个签发申请,这个申请通常以一个后缀名是.csr的文件表示。其中包含了.crt的全部信息以及发出申请的公司信息等信息。
4.CA机构收到签发申请后,用CA机构自己的私钥对申请中的网站证书生成一个签名,然后把这个签名以及原网站证书打包生成一个新的CA签发证书。业务服务器拿到这个CA签发的证书就可以把这个证书挂在nginx或者别的网关上提供https服务了。

证书签发完成后,客户端如何使用呢?流程如下:

1.当客户端需要访问业务服务器的https接口时,他从这个服务器上请求下来一个证书。
2.客户端看到这个证书上面有某个CA机构的签名,于是他从CA机构请求一个CA根证书(如果自己已经内置了改CA机构的根证书,就不需要再请求了)的来尝试对签名进行解密,解密成功,说明这张证书确实是这个CA机构签发的,可以信赖。
3.客户端生成一个对称加密的密钥,然后从第一步中的证书上把公钥提取出来,用这个公钥对对称密钥加密。把这个报文发给业务服务器。
4.业务服务器接收到对称加密的密钥后,客户端就可以开始业务数据的传输了。

自建CA证书与自签名证书

对于正规的企业来说,因为有稳定的网络出口IP和域名,以及充足的资金支持,为SSL证书支付一笔费用是完全可以接收且容易办到的。但对于个人应用来说就不同了,个人用户很少会拥有固定IP地址,对于费用也非常敏感,那么难道个人搭建的系统就不配用https吗?当然不是,因为CA机构本质上也只是一对密钥加上一个证书在起作用而已,我们可以自己生成这两样东西,然后给自己的证书进行签发。虽然CA机构不是公认的,但只要浏览器安装了我们自己生成的根证书,那他的效力与正常CA机构的效力是基本相同的。

这种自己构建一个CA机构的证书在国内几乎所有的博客中都直接将上述这种自建CA机构签发的证书称作自签名证书,但如果你看国外论坛,就会发现,自签名证书指的并不是这种证书。

自签名证书指的是另一种更极端的签发形式。因为业务服务器自己就有一对公私钥,而签发其实只是需要用私钥对包含公钥的证书进行一个签名即可,那我完全可以直接用自己的私钥给自己签名,自己充当自己的CA机构,这才是真正的自签名证书。而且这恰恰就是最顶级的CA机构签发自己根证书的方式。因为找不到其他机构给自己背书了,所以他只能自己给自己签发证书。

常见概念及术语

RSA算法:RSA非对称加密算法是现代互联网加密技术的基石。甚至可以说现代加密算法约等于RSA算法。以RSA为代表的非对称加密算法可以把公钥暴露在外界,把私钥保留在本地,从根本上消除了密钥传输过程被窃取的可能(因为私钥永远不需要传递)。且其所基于的数学原理——大质数因数分解问题,目前没有可快速解决的方法。

密钥对:RSA算法的输出结果是一对密钥——公钥、私钥。通常来讲,公钥负责发送给外界进行加密,私钥负责保留在本地进行解密。但私钥加密的数据也可以被公钥解密,CA证书的签发过程中的CA机构签名就使用了CA机构的私钥进行加密,然后外部的客户端可以用手中的公钥进行解密验签。在实际的命令行操作中,使用openssl genrsa 命令会生成一个密钥文件,这个文件通常约定其后缀名为.key,文件中同时包含了公钥和私钥,由于整个ssl认证流程中只有这个文件会包含私钥信息,所以这个文件经常被直接称作“私钥”。

证书签发申请:由上文的介绍可以知道,一个业务服务器如果想让某个CA机构对他的进行证书签发就需要发送一个签发申请,这个申请以一个文件的形式存在,文件的后缀名通常是.csr(Certificate Signing Request)。生成签发申请文件时需要使用公钥,生成过程中程序会要求申请人填写其公司的有关信息,包括国家、省份、城市、公司、部门以及CommonName信息,CommonName信息尤其重要,因为它决定了这个证书的使用范围,通常来讲会填写公司的域名(如www.baidu.com),如果CA机构支持通配符证书,则可以填写域名的通配符表达式(如*.baidu.com)。

证书:CA机构接受“证书签发申请”后,就会使用申请中的信息进行签发,并生成一个签发后的证书文件,通常后缀名为.crt(certificate),证书中的核心信息有三部分——服务器公钥、签发机构的信息、签发机构针对公钥生成的签名。签发后的证书就可以挂在https服务器上(如nginx)向外分发了。

根证书:CA机构也有自己的数字证书,这个证书有可能是另一个更权威的CA机构签发的,也有可能是他自己给自己签发的,这个CA机构的数字证书叫做根证书。

通配符证书:所有证书都必需指定其适用的范围,这个范围可以是(1)一个域名或IP地址(2)多个域名或IP地址(3)一族符合某种规则的域名(4)多个情况1到3的组合。通配符证书通常指第(3)种情况。通配符通常写在csr的CommonName字段上如果在这个字段的值中写*.baidu.com,那么这就是一个可以适配任何xxx.baidu.com的证书,它可以用在baidu.com这个域名下的任何三级域名上。顺便说一下,你不能写一个诸如..com的通配符以期待能获取到一个超广范围的证书,因为现代的浏览器会认为这种通配符无效。另外再提示一点,证书适用范围在匹配时并不考虑端口号,也就是说不管你的https服务在哪个端口上,只要证书里面有这台机器的域名或者IP地址,就算匹配成功。

SAN证书:最新版本的证书标准中加入了一个功能,将证书的适用范围配置又进一步扩展了,这就是Subject Alternative Name属性,这个属性是一个列表,证书申请者可以添加多个域名、IP地址或通配符表达式。这个配置方式可以说是几乎可以让你把一个证书用在任何地址上了。

X.509:上面所说的密钥文件、申请文件、证书文件都有一套标准格式,这一套格式标准的名称就叫做X.509。

openssl:openssl是一个开源的命令行工具,它实现了上述的所有流程所需要的功能。我们平时但凡需要进行非对称加密相关的操作时,都会使用这个工具。顺带说一下,openssl命令完成同一件工作可以使用很多种方式来实现,但对于标准的ssl流程来说,它的x509模块(通过添加openssl x509来调用)集成了大量一步到位的功能(如可以一行命令生成SAN自签名证书),所以如果你看到网上的教程有的步骤很多,有的步骤很少,那多半是因为有的人使用了这个模块,有的人没有使用。

密钥格式(pem/der) :通常来讲openssl生成的各种文件的后缀名是可以随意取的,而正是这种自由导致了网上各个openssl教程中对名称的产出物有各种写法,搞得初学者一头雾水,各种写法包括不限于.key,.crt,.cer,.pem以及前三者与.pem的各种组合。实际上不管文件后缀写的是什么,openssl可以输出的文件格式只有两种——pem与der。由于X.509标准中的各种文件本身是以二进制方式定义的,所以openssl生成的各种文件不是直接人眼可读的,而二进制的表示方式除了原始的二进制方式还有base64编码的形式,这种形式相较与纯二进制更加易于传递,所以openssl选择base64的形式作为默认的文件输出格式,这个格式的名称就叫pem,而二进制方式的名称就叫der。在命令行中可以用-outform参数进行指定。如果你看到命令中没有明确指定这个参数,那么输出产物就是pem格式的。

passphase:因为整个加密证书的核心是一对RSA密钥对,而这对密钥在一些场合(如涉及国家安全等场景)可能极其重要,这时就可以对这个密钥对本身再进行一次基于对称加密算法的DES加密,这样一来即使一个外部人员得到了私钥,如果他不知道密码,还是无法使用这套密钥,这也是openssl生成密钥时的默认方式。不过对于小企业或者个人来说,这种二次加密会使得各种操作非常繁琐且二次加密的密码极其容易忘记,所以特别是个人场景下,一般会使用-nodes参数(no des的合写)关闭这个功能。

challenge password:在证书签发申请的过程中的最后一步会提示你输入一个challenge password,经过我的搜索得出的结论是,这个参数已经废弃,不需要填写直接按回车跳过。

自建CA证书生成脚本

自建CA脚本的过程比较麻烦,而且不知道是否是浏览器设置问题,在安装了根证书后报签名无效,但整个命令的思路是清晰的,贴出来作为参考,如果哪位知道问题所在,还请不吝指正。

openssl.cnf全文(去掉了大块的注释)。openssl.cnf需要把默认的openssl.cnf(ubuntu默认地址是/etc/ssl/openssl.cnf考过来进行修改,下面是去掉了大段注释的openssl.cnf全文。

HOME			= .
oid_section		= new_oids

[ new_oids ]
tsa_policy1 = 1.2.3.4.1
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.
                    # several certs with same subject.
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
                    # must be commented out to leave a V1 CRL
crl		= $dir/crl.pem 		# The current CRL
private_key	= $dir/private/cakey.pem# The private key
x509_extensions	= usr_cert		# The extensions to add to the cert
name_opt 	= ca_default		# Subject Name options
cert_opt 	= ca_default		# Certificate field options
copy_extensions = copy
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		= 2048
default_keyfile 	= privkey.pem
distinguished_name	= req_distinguished_name
attributes		= req_attributes
x509_extensions	= v3_ca	# The extensions to add to the self signed cert
string_mask = utf8only
req_extensions = v3_req # The extensions to add to a certificate request

[ alt_names ]
DNS.1 = newsmetro.net
DNS.2 = *.newsmetro.net
DNS.3 = aquar.site
DNS.4 = *.aquar.site
IP.1 = 127.0.0.1
IP.2 = 192.168.0.131
IP.3 = 192.168.30.1
IP.4 = 10.0.30.242
IP.5 = 39.xxx.xxx.xxx

[ 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	= TJ
localityName			= Locality Name (eg, city)
0.organizationName		= Organization Name (eg, company)
0.organizationName_default	= Aqaur
organizationalUnitName		= Organizational Unit Name (eg, section)
commonName			= Common Name (e.g. server FQDN or YOUR name)
commonName_default		= *.aquar.site
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
nsComment			= "OpenSSL Generated Certificate"
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer

[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @alt_names

[ v3_ca ]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
basicConstraints = critical,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)
signer_digest  = sha256			# Signing digest to use. (Optional)
default_policy	= tsa_policy1		# Policy if request did not specify it
                    # (optional)
other_policies	= tsa_policy2, tsa_policy3	# acceptable policies (optional)
digests     = sha1, sha256, sha384, sha512  # 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)
ess_cert_id_alg		= sha1	# algorithm to compute certificate
                # identifier (optional, default: sha1)

命令行执行记录:

# openssl对于自建CA的情况要求你必需有一个单独且有特定结构目录来存放CA的私钥、证书以及一些状态信息,所以需要提前手动建一个符合要求的目录。这些目录的定义在openssl的默认配置文件openssl.cnf文件中可以看到,这个文件在ubuntu的默认目录是/etc/ssl/openssl.cnf。
mkdir -p ./demoCA/{private,newcerts}
touch ./demoCA/index.txt
echo 01 > ./demoCA/serial

#使用x509模块生成一套自签名的CA证书,产出物包含了密钥文件以及一个使用这套密钥签发好了的证书,机构名称及SAN配置在openssl.cnf中
openssl req -x509 -nodes -newkey rsa:4096 -keyout ./demoCA/private/ca.key.pem -out ./demoCA/ca.crt.pem -sha256 -days 36500 -config openssl.cnf
# 生成一套业务服务器密钥
openssl genrsa -out aquar.key.pem 4096
# 用刚生成的这套密钥生成对应的证书签发请求文件aquar.csr.pem
openssl req -new -days 36500 -key aquar.key.pem -out aquar.csr.pem -config openssl.cnf 
# 用刚才自建的CA证书以及CA密钥对上一步生成的证书签发请求进行签发,生成一个签发后的证书文件aquar.crt.pem
openssl x509 -req -in aquar.csr.pem -CA ./demoCA/ca.crt.pem -CAkey ./demoCA/private/ca.key.pem -CAcreateserial -extfile ./openssl.cnf -extensions v3_req -days 36500 -sha256 -out aquar.crt.pem

# 查看证书签发请求信息
openssl req -text -in aquar.csr.pem
# 查看最终签发的业务服务证书信息
openssl x509 -in aquar.crt.pem -text -noout

自签名证书生成脚本

自签名证书相较自建CA的方式要容易得多,且很容易被浏览器正确接受。下面就是一行代码生成自签名SAN证书的命令:

openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \
  -keyout aquar.key -out aquar.crt -subj "/CN=*.aquar.site" \
  -addext "subjectAltName=DNS:newsmetro.net,DNS:*.newsmetro.net,DNS:aquar.site,DNS:*.aquar.site,IP:127.0.0.1,IP:192.168.0.131,IP:192.168.0.117,IP:192.168.30.1,IP:10.0.30.242,IP:39.xxx.xxx.xxx"

这个命令会生成两个文件:aquar.crt以及aquar.key,把这两个文件拷贝到合适的位置,配置到nginx的https配置中,然后nginx -s reload,就可以拥有一个自签名的https服务了。

上面这步做完以后,访问服务,浏览器会报错并提示证书不安全。查看证书详情,会看到下图的提示。

cdf55497b0f7444396380cb5fbe10f89.png

把前面生成的aquar.crt文件从服务器中拷贝下来,然后打开证书管理功能(chrome的设置位置是:设置->隐私设置和安全性->安全->管理证书。

d77215be5969467d996a82c4943bc338.png

点击后选择“受信任的根证书颁发机构标签页,点击下面的导入按钮,导入下载下来的aquar.crt文件。

8481097c66204857a7672486230b224e.png

6aeb306e8a7d48be93e19d15f2ac4101.png

导入成功后关闭证书管理功能,刷新服务页面,就可以看到https的红叉消失了,浏览器接收了这个自签名证书。

4143d19957474202a1d670d714c8fbbf.png

到此,我们终于建立起一个被浏览器承认的https服务。