本篇文章我们讲解下关于https及证书的相关知识,因为我发现不理解证书感觉很难理解https,所以本篇会花大量的篇幅来讲证书,我们一开始会讲解一些相关概念帮助大家理解下,然后我们自己签发一个证书来理解这个过程,最后通过实例和抓包帮助大家理解https。因为本篇文章有很多代码及配置文件相关,最好通过电脑打开查看,篇幅很长,感谢耐心。
见到https时就会有很多不知道的概念,我们这里来区分下:
HTTPS:https即为http及其下边一层的可靠的安全加密的传输协议(tls/ssl),它主要思想是传输的双发验证对端的证书是否可以通过(被信任),以此生成加密的密钥,通过此密钥加密传输的内容,后边会详解。
SSL:Secure Socket Layer, 安全套接字层,http层下新增加的这一层构成了https
TLS:Transport Layer Security,同样是为了保证数据安全的加密协议层,是SSL的增强版,SSL有1.0,2.0,3.0版本,TLS目前1.0,1.1,1.2,1.3,TLS的1.0版本就是SSL的3.0
Key:https中有公钥和私钥,用公钥加密的内容,可以使用私钥解密,反之亦然,不过我们平常所说的key文件是指私钥文件
CRT: certificate证书文件,是证书机构颁发的保证安全通信的文件,由域名、公司信息、序列号和签名信息等组成
CER:也是证书文件,和CRT相比只是缩写不同,CRT缩写常见于类uninx系统,CER缩写常见于windows系统
X.509:这里特指颁发的证书的格式,而其根据不同的编码格式分为PEM和DER:
CSR:Certificate Signing Request 证书签名请求,里面包含公钥等个体信息,这个发给公证机构作为申请,通过这个公证机构颁发证书给你
CA:Catificate Authority 证书颁发机构,它的作用就是给各个用户签发证书等,比如说Symantec、Comodo、Godaddy、GolbalSign 和 Digicert等
openssl:相当于SSL的一个实现,如果把SSL规范看成OO中的接口,那么OpenSSL则认为是接口的实现,个人理解openssl是作为针对SSL/TLS的一个工具,包括对证书的解析,个人颁发,证书编码转化等
使用https进行通信,就需要一个证书,那么证书哪里来,就是来自于CA(证书颁发机构)颁发。所谓证书就是比较有公信的机构颁发给你,然后你用他来进行通信,另外一端验证通过后就会信任你,进行信息通信。举例来说如果你的服务器没有证书,浏览器访问你的网址时,会提示不安全。
那么如果有一个服务器,如何才能获取到证书呢,如果要从证书颁发机构申请的话,需要提供CSR文件及钱,然后机构就会颁发证书给你。如果你想要自己给自己颁发,只是用来本地玩玩,或者局域网各个机器之间的相互交流,便可以自己建立一个CA,使用这个CA来给自己颁发证书。我们下面以给自己颁发证书的全过程来描述下,我们使用openssl来进行构建。
CA角色也是需要一个自身的pair,pair包括key(private)和certificate,而最原始的被称为root pair,通常情况下,root CA 不会直接为服务器或者客户端签证,它们会先为自己生成几个中间 CA(intermediate CAs),这几个中间 CA 作为 root CA 的代表为服务器和客户端签证。自己实操的过程中其实也可以直接使用root pair来给服务器签发,为了演示全面,我们来使用intermediate CA来签发证书。
$ cd /etc/pki/
$ mkdir leap && cd leap
$ mkdir ca && cd ca
生成root key
$ mkdir root && cd root
$ mkdir certs crl newcerts private
$ openssl genrsa -aes256 -out private/leap_ca_key.pem 4096
Enter pass phrase for root_ca.key.pem: [pass]
Verifying - Enter pass phrase for root_ca.key.pem: [passwd]
$ chmod 400 private/leap_ca_key.pem
生成root cert
首先复制cnf文件内容到openssl.cnf
HOME = .
RANDFILE = .rand
[ca]
default_ca = ca_default
[ca_default]
dir = .
certs = $dir/certs
crl_dir = $dir/crl
database = $dir/index.txt
new_certs_dir = $dir/newcerts
certificate = $dir/certs/%s_ca_cert.pem
private_key = $dir/private/%s_ca_key.pem
serial = $dir/serial
crl = $dir/crl.pem
x509_extensions = usr_cert
name_opt = ca_default
cert_opt = ca_default
default_days = 3650
default_crl_days = 30
default_md = sha256
preserve = no
policy = policy_match
[policy_match]
# The root CA should only sign intermediate certificates that match.
# See the POLICY FORMAT section of `man ca`.
countryName = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ policy_loose ]
# Allow the intermediate CA to sign a more diverse range of certificates.
# See the POLICY FORMAT section of the `ca` man page.
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[req]
default_bits = 2048
default_keyfile = privkey.pem
default_md = sha256
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_distinguished_name]
countryName = Country Name (2 letter code)
countryName_min = 2
countryName_max = 2
stateOrProvinceName = State or Province Name (full name)
localityName = Locality Name (eg, city)
0.organizationName = Organization Name (eg, company)
organizationalUnitName = Organizational Unit Name (eg, section)
commonName = Common Name (eg, your name or your server hostname)
commonName_max = 64
emailAddress = Email Address
emailAddress_max = 64
# Optionally, 修改下这里填写自己的相关信息
countryName_default = CN
stateOrProvinceName_default = Beijing
localityName_default = Beijing
0.organizationName_default = Leap
organizationalUnitName_default =
emailAddress_default =
[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:always
[v3_ca]
# Extensions for a typical CA (`man x509v3_config`).
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer:always
basicConstraints = critical, CA:true
[ v3_intermediate_ca ]
# Extensions for a typical intermediate CA (`man x509v3_config`).
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign
自己使用的话,可以修改[req_distinguished_name]下的指定的内容为CA的相关信息
然后我们再建立cnf中需要的文件([ca_default]下):
$ touch index.txt
$ echo 1000 > serial
准备工作完成,我们来生成CA的root cert
$ openssl req -config openssl.cnf -new -x509 -days 3650 -sha256 \
-key private/leap_ca_key.pem -extensions v3_ca \
-out certs/leap_ca_cert.pem
Enter pass phrase for private/leap_ca_key.pem:
Can't load .rand into RNG
139906699818816:error:2406F079:random number generator:RAND_load_file:Cannot open file:crypto/rand/randfile.c:98:Filename=.rand
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [CN]:
State or Province Name (full name) [Beijing]:
Locality Name (eg, city) [Beijing]:
Organization Name (eg, company) [Leap]:
Organizational Unit Name (eg, section) []:LeapCA
Common Name (eg, your name or your server's hostname) []:leap.cn
Email Address []:
这里会出现交互界面,让你来填写,上面有一部分我们已经设定了默认值,除此之外我们需要设定其他的值,比较要注意的是Common Name一般就是指你的域名或者host名
然后我们验证看下生成root证书:
$ openssl x509 -noout -text -in certs/leap_ca_cert.pem
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
7d:f5:69:17:10:8b:67:13:83:44:a1:71:c7:fb:05:8a:f1:c5:47:59
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = CN, ST = Beijing, L = Beijing, O = Leap, OU = LeapCA, CN = leap.cn
Validity
Not Before: Oct 25 07:05:32 2021 GMT
Not After : Oct 23 07:05:32 2031 GMT
Subject: C = CN, ST = Beijing, L = Beijing, O = Leap, OU = LeapCA, CN = leap.cn
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (4096 bit)
Modulus:
...
Signature Algorithm: sha256WithRSAEncryption
2c:e9:ff:57:2c:ca:4a:e0:bc:35:dc:e4:13:89:16:6b:4c:44:
d8:32:d7:cd:fe:41:88:76:eb:c9:35:92:34:20:98:a2:f0:4d:
23:f5:1c:80:79:75:14:e6:a0:
...
这里就是和我们平常看到的证书是一样的了,可以看到颁发机构(Issuer)是LeapCA,颁发给(Subject)LeapCA。
包含:
现在已经有了root pair,已经可以证书发放了,不过还是通过中间(intermediate)pair作为root pair的代理来颁发证书。
新建目录及生成intermediate key:
$ ls
certs crl newcerts openssl.cnf private
$ pwd
/etc/pki/leap/ca/root
$ cd ..
$ mkdir intermediate && cd intermediate
$ mkdir certs crl csr newcerts private
$ openssl genrsa -aes256 \
-out private/leap_intermediate_key.pem 4096
$ chmod 400 private/leap_intermediate_key.pem
生成intermediate key
最开始还是要设置配置文件,将上一个openssl.cnf拷贝到intermediate目录下,我们修改一下:
# ...
[ca_default]
# ...
certificate = $dir/certs/%s_intermediate_cert.pem
private_key = $dir/private/%s_intermediate_key.pem
# ...
policy = policy_loose
# ...
# 增加下边
[ server_cert ]
# Extensions for server certificates (`man x509v3_config`).
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth, clientAuth # 如签发的证书单向认证,只需要serverAuth
同样也还是建立需要的文件:
$ touch index.txt
$ echo 1000 > serial
生成csr文件:
$ openssl req -config openssl.cnf -new -sha256 \
-key private/leap_intermediate_key.pem \
-out csr/leap_intermediate_csr.pem
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [CN]:
State or Province Name (full name) [Beijing]:
Locality Name (eg, city) [Beijing]:
Organization Name (eg, company) [Leap]:
Organizational Unit Name (eg, section) []:LeapIntermediate
Common Name (eg, your name or your server's hostname) []:leap.sub.cn
Email Address []:
Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:[passwd] # passwd
An optional company name []:
这里生成csr文件需要填写内容和之前很相似,除了需要设置csr的一些信息,要注意的是CommanName不要和CA一样。
接下来通过root pair来为intermediate pair生成证书:
# 用到刚刚生成csr文件
$ cd ../root
$ openssl ca -config openssl.cnf -extensions v3_intermediate_ca \
-keyfile private/leap_ca_key.pem \
-cert certs/leap_ca_cert.pem \
-days 3650 -notext -md sha256 \
-in ../intermediate/csr/leap_intermediate_csr.pem \
-out ../intermediate/certs/leap_intermediate_cert.pem
# ...
Signature ok
Certificate Details:
Serial Number: 4096 (0x1000)
Validity
Not Before: Oct 26 08:54:20 2021 GMT
Not After : Oct 24 08:54:20 2031 GMT
Subject:
countryName = CN
stateOrProvinceName = Beijing
organizationName = Leap
organizationalUnitName = LeapIntermediate
commonName = leap.sub.cn
# ...
Certificate is to be certified until Oct 24 08:54:20 2031 GMT (3650 days)
Sign the certificate? [y/n]:y
1 out of 1 certificate requests certified, commit? [y/n]y
Write out database with 1 new entries
Data Base Updated
表示生成成功,看下index.txt多一条记录
V 311024085420Z 1000 unknown /C=CN/ST=Beijing/O=Leap/OU=LeapIntermediate/CN=leap.sub.cn
再来验证下生成的证书:
$ openssl verify -CAfile certs/leap_ca_cert.pem \
../intermediate/certs/leap_intermediate_cert.pem
../intermediate/certs/leap_intermediate_cert.pem: OK
浏览器在验证中间证书的时候,同时也会去验证它的上一级证书是否靠谱,创建证书链,将 root cert 和 intermediate cert 合并到一起,可以让浏览器一并验证:
$ cat ../intermediate/certs/leap_intermediate_cert.pem \
certs/leap_ca_cert.pem > ../intermediate/certs/leap_ca_chain_cert.pem
$ chmod 444 ../intermediate/certs/leap_ca_chain_cert.pem
CA的角色既然已经创建完成了,下一步就是为通信的双方创建证书了,有一点要明确的是tls/ssl分为双向认证和单向认证。如果双向认证,客户端和服务端都要有自己的证书,如果单向认证,只需要服务端有自己的证书,平常浏览器访问就是单向认证,为什么单向认证?其实原因也很简单,总不能给每个人(浏览器端)都颁发一个证书吧,所以https这里采用的是单向认证。这里客户端和服务器都生成自己的证书。
为客户端生成pair:
$ cd ..
$ pwd
/etc/pki/leap/ca
$ mkdir leap.client.cn
# 生成key
$ openssl genrsa -aes256 -out leap.client.cn/leap_client_key.pem 2048
# 生成csr,Common Name设置为leap.client.cn
$ openssl req -config intermediate/openssl.cnf \
-key leap.client.cn/leap_client_key.pem \
-new -sha256 -out leap.client.cn/leap_client_csr.pem
# 使用intermediate pair为client生成cert
$ cd intermediate
$ openssl ca -config openssl.cnf \
-extensions server_cert -days 375 -notext -md sha256 \
-keyfile private/leap_intermediate_key.pem \
-cert certs/leap_intermediate_cert.pem \
-in ../leap.client.cn/leap_client_csr.pem \
-out ../leap.client.cn/leap_client_cert.pem
为服务端生成pair:
$ cd ..
$ pwd
/etc/pki/leap/ca
$ mkdir leap.server.cn
# 生成key
$ openssl genrsa -aes256 -out leap.server.cn/leap_server_key.pem 2048
# 生成csr, Common Name设置为leap.server.cn
$ openssl req -config intermediate/openssl.cnf \
-key leap.server.cn/leap_server_key.pem \
-new -sha256 -out leap.server.cn/leap_server_csr.pem
# 使用intermediate pair为server生成cert
$ cd intermediate
$ openssl ca -config openssl.cnf \
-extensions server_cert -days 375 -notext -md sha256 \
-keyfile private/leap_intermediate_key.pem \
-cert certs/leap_intermediate_cert.pem \
-in ../leap.server.cn/leap_server_csr.pem \
-out ../leap.server.cn/leap_server_cert.pem
到这里我们就为server和client分别生成了pair,接下来我们就通过实例来验证下生成的证书是否正确。
这里我们就使用上面生成的证书密钥等文件来通过实例来演示看下双向认证(go语言):
首先第一步将生成证书链文件,服务器的key和cert文件,客户端的key和cert文件拷贝到工程中以备使用,新建一个客户端程序文件和服务端程序文件,结构如图所示:
证书文件我给重命名了下(去掉leap_前缀),这里使用因为我们private key都是加了密的(没有加密忽略此步骤),所以使用时候需要解密才能使用,指令为:
openssl rsa -in leap_server_key.pem -out server_key.pem -passin pass:[your passwd]
openssl rsa -in leap_client_key.pem -out client_key.pem -passin pass:[your passwd]
package main
import (
// ...(略)
)
func main() {
pool := x509.NewCertPool()
if ca, e := ioutil.ReadFile("ca_chain_cert.pem"); e != nil {
log.Fatal("ReadFile: ", e)
} else {
pool.AppendCertsFromPEM(ca)
}
s := &http.Server{
Addr: ":443",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!\n")
}),
TLSConfig: &tls.Config{
ClientCAs: pool,
ClientAuth: tls.RequireAndVerifyClientCert,
},
}
e := s.ListenAndServeTLS("server_cert.pem", "server_key.pem")
if e != nil {
log.Fatal("ListenAndServeTLS: ", e)
}
}
程序也比较简单,初始化一个http.Server,进行一些配置,里面初始化CA,然后设置cert和key并监听启动服务
package main
import (
// ...(略)
)
func main() {
pool := x509.NewCertPool()
if ca, e := ioutil.ReadFile("ca_chain_cert.pem"); e != nil {
log.Fatal("ReadFile: ", e)
} else {
pool.AppendCertsFromPEM(ca)
}
pair, e := tls.LoadX509KeyPair("client_cert.pem", "client_key.pem")
if e != nil {
log.Fatal("LoadX509KeyPair:", e)
}
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: pool,
Certificates: []tls.Certificate{pair},
},
}}
resp, e := client.Get("https://leap.server.cn")
if e != nil {
log.Fatal("http.Client.Get: ", e)
}
defer resp.Body.Close()
io.Copy(os.Stdout, resp.Body)
}
代码比较类似,新建client对象,将ca,cert,key等进行配置,不过要注意的一点是域名要使用服务端生成证书里面的域名: leap.server.cn,即我们在生成csr的时候Common Name。
本例中跑在一台机器,所以需要设置hosts文件:
$ vim /etc/hosts
127.0.0.1 leap.server.cn
然后我们来运行下:
# 启动服务器
$ go build tls_server.go
$ ./tls_server
# 启动客户端
$ go build tls_client.go
$ ./tls_client
2021/10/27 19:15:45 http.Client.Get: Get "https://leap.server.cn": x509: certificate relies on legacy Common Name field, use SANs or temporarily enable Common Name matching with GODEBUG=x509ignoreCN=0
发现启动客户端时报错,原因是因为go 1.15 版本开始废弃 CommonName,推荐使用 SAN 证书。 如果想兼容之前的方式,需要设置环境变量 GODEBUG 为 x509ignoreCN=0。两种解决方案,要么重新生成SAN证书,要么就是设置环境变量 GODEBUG 为 x509ignoreCN=0,首先第二种解决方案如下:
$ GODEBUG=x509ignoreCN=0 ./tls_client
Hello World!
不过浏览器使用的话,也会验证服务端证书的这个字段,所以还是重新生成下服务端的证书,需要修改intermediate/openssl.conf下的[ server_cert ]下增加一个item并增加[alt_names]的group:
[ server_cert ]
subjectAltName = @alt_names
[alt_names]
DNS.1 = leap.server.cn
用这个cnf重新执行生成服务端证书的步骤,这样就不需要加GODEBUG了:
$ ./tls_client
Hello World!
好的验证成功,已完成。
我们平时使用的https还是单向认证的,就是平常浏览器访问服务器这种情况,单向认证只是比双向认证少服务端验证客户端证书的步骤,借此来抓包,详细的分析下https的协议。
首先我们先跑起来,然后再来观察现象,把之前生成证书链复制到系统中,这里使用windows系统,然后把他安装到计算机中。步骤如下:
服务端的程序还需要修改下,因为之前那个服务端程序是会去验证客户端的,需要写一个不验证客户端证书的服务端程序,代码超级简单:
func handler(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "https world!")
}
func main() {
http.HandleFunc("/", handler)
if err := http.ListenAndServeTLS(":443", "server_cert.pem", "server_key.pem", nil); err != nil {
log.Fatal("ListenAndServe:", err)
}
}
最后使用浏览器访问下:
访问成功,网址那里也有一个锁头表示此网站是安全的。
接下来用wireshark抓包看下先:
192.168.77.1是客户端浏览器,192.168.77.133是服务器, 简单过下流程:
首先完成tcp握手后,client发起Client hello,这里会传送客户端支持的tls版本,一个随机数(后期生成密钥使用),支持的加密算法,支持的压缩算法等
服务端收到请求后,发送Service Hello,这里包含选择的tls版本,服务器生成的随机数(后期生成密钥使用),选中的加密算法,选中的压缩算法。
然后服务端会发送证书给客户端,发送使用证书签名的握手信息等
客户端验证证书完成,发送使用加密信息发送的通知,并结束
之后就是客户端请求服务端的消息了(黄色部分)
这里抓包的tls是1.3版本,并未深入讲解,着重讲解两个点:
$ openssl verify -CAfile certs/leap_ca_cert.pem \
../intermediate/certs/leap_intermediate_cert.pem
../intermediate/certs/leap_intermediate_cert.pem: OK
不过这里做了什么呢?我们仔细看下证书的部分分为摘要信息和签名信息(Signature),当在签发的时候,CA会把持有者的公钥、用途、颁发者、有效时间等信息进行Hash计算,得到一个Hash值,Hash值使用CA的私钥加密,生成Certificate Signature,也就是签名信息。这样当客户端验证的时候使用CA的公钥解密,并将服务端的公钥的摘要信息使用Hash计算判断二者是否相等,从而能够验证证书的完整性和正确性
整体来说我们把https和证书颁发这块知识讲解了下,https使用tls/ssl来进行加密,其中https分为双向验证和单向验证,双向验证比如说银行的一些交易客户这边需要U盾或者其他就是验证客户端的证书,而我们平常浏览器访问不需要验证客户端就是单向认证。篇幅原因tls详细没有展开讲,感谢阅读