文章中回顾了密码学的一些基本知识,介绍了https的主要功能和基本的原理,整理了通常情况下使用openssl生成证书的情景,还包括带有san的证书生成过程。最后使用golang模拟了客户端和服务端使用https进行验证和通信的基本示例。
关于https的背景知识
密码学的一些基本知识
大致上分为两类,基于key的加密算法与不基于key的加密算法。现在的算法基本都是基于key的,key就以一串随机数数,更换了key之后,算法还可以继续使用。
基于key的加密算法又分为两类,对称加密和不对称加密,比如DES,AES那种的,通信双方一方用key加密之后,另一方用相同的key进行反向的运算就可以解密。
不对称加密比较著名的就是RSA,加密的时候有一个公钥和一个私钥,公钥是可以交给对方的,a给b发送信息,a用自己的私钥加密,b用a的公钥解密,反之,b给a发送信息,b用自己的私钥加密。
在通信之前,需要经过一些握手的过程,双方交换公钥,这个就是key exchange的过程,https最开始的阶段就包含了这个key exchange的过程,大概原理是这样,有些地方还要稍微复杂一些,具体细节可以参考最后列出的参考链接。
数字证书与CA
数字证书相当于是服务器的一个“身份证”,用于唯一标识一个服务器。一般而言,数字证书从受信的权威证书授权机构 (Certification Authority,证书授权机构)买来的(免费的很少),浏览器里面一般就内置好了一些权威的CA,在使用https的时候,只要是这些CA签发的证书,浏览器都是可以认证的,要是在与服务器通信的时候,收到一个没有权威CA认证的证书,就会报出提醒不受信任证书的错误,就像登录12306一样,但是也可以选择接受。
在自己的一些项目中,通常是自己签发一个ca根证书,之后由这个根证书生成一个auth peer: 即server.crt,以及server.key给服务端,server.key是服务端的私钥,server.crt包含了服务端的公钥还有服务端的一些身份信息。在客户端和服务端通信的时候(特别是使用代码编写的客户端访问程序的时候),要指定ca根证书,作用就相当于是浏览器中内置的那些权威证书一样,用于进行服务端的身份检测,当然也可以设置对应的参数,跳过身份验证的哪一步,只是使用https进行传输过程中的信息加密。
证书的格式:
ca证书在为server.crt证书签名时候的原理上的大致流程参考了这个:
数字证书由两部分组成:
1、C:证书相关信息(对象名称+过期时间+证书发布者+证书签名算法….)
2、S:证书的数字签名 (由CA证书通过加密算法生成的)
其中的数字签名是通过公式S = F(Digest©)得到的。
Digest为摘要函数,也就是 md5、sha-1或sha256等单向散列算法,用于将无限输入值转换为一个有限长度的“浓缩”输出值。比如我们常用md5值来验证下载的大文件是否完整。大文件的内容就是一个无限输入。大文件被放在网站上用于下载时,网站会对大文件做一次md5计算,得出一个128bit的值作为大文件的摘要一同放在网站上。用户在下载文件后,对下载后的文件再进行一次本地的md5计算,用得出的值与网站上的md5值进行比较,如果一致,则大 文件下载完好,否则下载过程大文件内容有损坏或源文件被篡改。这里还有一个小技巧常常在机器之间copy或者下载压缩文件的时候也可以用md5sum的命令来进行检验,看看文件是否完整。
F为签名函数。CA自己的私钥是唯一标识CA签名的,因此CA用于生成数字证书的签名函数一定要以自己的私钥作为一个输入参数。在RSA加密系统中,发送端的解密函数就是一个以私钥作为参数的函数,因此常常被用作签名函数使用。因此CA用私钥解密函数作为F,以CA证书中的私钥进行加密,生成最后的数字签名,正如最后一部分实践时候给出的证书生成过程,生成server.crt的时候需要ca.crt(包含根证书的信息)和ca.key(根证书的私钥)都加入进去。
接收端接收服务端数字证书后,如何验证数字证书上携带的签名是这个CA的签名呢?当然接收端首先需要指定对应的CA,接收端会运用下面算法对数字证书的签名进行校验: F’(S) ?= Digest©
接收端进行两个计算,并将计算结果进行比对:
1、首先通过Digest©,接收端计算出证书内容(除签名之外)的摘要,C的内容都是明文可以看到到的。
2、数字证书携带的签名是CA通过CA密钥加密摘要后的结果,因此接收端通过一个解密函数F’对S进行“解密”。就像最开始介绍的那样,在RSA系统中,接收端使用CA公钥(包含在ca.crt中)对S进行“解密”,这恰是CA用私钥对S进行“加密”的逆过程。
将上述两个运算的结果进行比较,如果一致,说明签名的确属于该CA,该证书有效,否则要么证书不是该CA签发的,要么就是中途被人篡改了。
对于self-signed(自签发)证书来说,接收端并没有你这个self-CA的数字证书,也就是没有CA公钥,也就没有办法对数字证书的签名进行验证。因此如果要编写一个可以对self-signed证书进行校验的接收端程序的话,首先我们要做的就是建立一个属于自己的CA,用该CA签发我们的server端证书,之后给客户端发送信息的话,需要对这个根证书进行指定,之后按上面的方式进行验证。
可以使用 openssl req -text -noout -in .csr 查看某个证书请求文件所包含的具体信息。查看证书文件的信息的话使用这个: openssl x509 -noout -text -in .crt
HTTPS基本过程概述
https协议是在http协议的基础上组成的secure的协议。主要功能包含以下两个方面内容:
1 通信双方的身份认证
2 通信双方的通信过程加密
下面通过详细分析https的通信过程来解释这两个功能。
具体参考这两个文章:
http://www.fenesky.com/blog/2014/07/19/how-https-works.html
http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html
1、client 发送 sayhello给server端,说明client所支持的加密套件,还有一个随机数a。
2、server 发送 sayhello给client端,端把server.crt发送给客户端,还有一个随机数b也一起发过来。
3、client端生成preMaster key 这个是随机数c,之后三个随机数结合在一起生成MasterSecret,之后生成session secret,使用指定的ca进行身份认证,就像之前介绍的那样,都正常的话,就切换到加密通信模式。
4、client端使用server.crt中的公钥对preMasterSecret进行加密,如果要进行双向认证的话,client端会把client.crt一并发送过去,server端接受到数据,解密之后,也有了三个随机数,采用同样的方式,三个随机数生成通信所使用的session secret。具体session secret的结构可以参考前面列出的两个博客。server端完成相关工作之后,会发一个ChangeCipherSpec给client,通知client说明自己已经切换到相关的加解密模式,之后发一段加密信息给client看是否正常。
5、client端解密正常,之后就可以按照之前的协议,使用session secret进行加密的通信了。
整体看下,开始的时候建立握手的过程就是身份认证的过程,之后认证完毕之后,就是加密通信的过程了,https的两个主要功能就实现了。
golang中的相关实践
通常使用openssl的证书生成的过程:
openssl genrsa -out ca.key 2048 #这里可以使用 -subj 不用进行交互 当然还可以添加更多的信息 openssl req -x509 -new -nodes -key ca.key -subj "/CN=xxx.com" -days 5000 -out ca.crt openssl genrsa -out server.key 2048 #这里的/cn可以是必须添加的 是服务端的域名 或者是etc/hosts中的ip别名 openssl req -new -key server.key -subj "/CN=server" -out server.csr openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 5000 #查询所生成证书的信息 openssl x509 -noout -text -in ./server.crt
生成client端证书的时候,注意要多添加一个CAcreateserial字段,golang中的server端认证程序会对这个字段进行认证:
openssl genrsa -out client.key 2048 openssl req -new -key client.key -subj "/CN=client" -out client.csr echo extendedKeyUsage=clientAuth > extfile.conf openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -extfile extfile.conf -out client.crt -days 5000
添加了SAN的证书生成的过程:
san(subjectAltName)用于进行额外的身份认证,默认的情况下使用主机名进行身份认证的。在上面列出的普通证书生成方式中,使用 -subj “/CN=server” 发布服务端.csr文件,之后正式通信的时候,就像上面列出的通信过程的第一步,服务端的证书会传过来给客户端,客户端进行验证的时候,发送的ip必须是 https://server:serverport 这种形式,否则就会验证失败。/CN字段是不能直接写成ip值的,可能会报下面的错误:
Get https://10.183.47.206:8081: x509: cannot validate certificate for 10.183.47.206 because it doesn't contain any IP SANs
想要直接通过ip来认证,需要添加一些额外的配置:
首先要创建一个配置文件 openssl.conf,具体的细节内容可以参考最后的链接,个别参数的准确含义也还不太清楚,这个是一个测试可以用的,基本内容如下:
[req]distinguished_name = req_distinguished_namereq_extensions = v3_req[req_distinguished_name]countryName = Country Name (2 letter code)countryName_default = CNstateOrProvinceName = State or Province Name (full name)stateOrProvinceName_default = ZhejianglocalityName = Locality Name (eg, city)localityName_default = HangzhouorganizationalUnitName = Organizational Unit Name (eg, section)organizationalUnitName_default = ZjuselcommonName = xxx.comcommonName_max = 64[ v3_req ]# Extensions to add to a certificate requestbasicConstraints = CA:FALSEkeyUsage = nonRepudiation, digitalSignature, keyEnciphermentsubjectAltName = @alt_names[alt_names]IP.1 = 10.10.105.27
其中[req distinguished name]就是需要输入的常用认证信息,以及默认的值,[alt_names]就是最后添加进来的认证信息,ip可以添加多个,还可以添加dns的信息,比如:
DNS.1 = server1.example.com
之后服务端证书的时候,大致原理是相似的,只不过要使用自签名的方式,这样crt文件中才能包含相关的信息,具体的命令参数也有所不同:
#使用自签名的方式 不用再额外生成ca文件了 openssl genrsa -out server.key 2048 #注意这里要使用新生成的配置文件 openssl req -new -key server.key -subj "/CN=server" -config openssl.conf -out server.csr openssl x509 -req -days 5000 -in server.csr -signkey server.key -out server.crt -extensions v3_req -extfile openssl.conf #查询所生成证书的信息 可以看到里面包含了 ip san openssl x509 -noout -text -in ./server.crt
在这里我们使用自签名证书,就是server.crt同时充当了ca.crt和server.crt的角色。如果还要使用根证书签名server.crt的方式,可以结合上面的操作,使用如下的命令:
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 5000 -extensions v3_req -extfile openssl.conf
还有一些第三方的证书生成工具可以使用,比如k8s中的GCE证书的生成,以及apiserver证书的自动生成,使用的是esyrsa的一个生成工具。
关于自签名证书还是有一些坑的,按照上面的方式生成证书,之后使用golang的https的方式发送请求的话,会报出这个错误,这个也真是折腾了很久:可以参考这个解答。
但是直接使用curl命令的时候,就是可以认证成功的,具体的认证细节貌似在x509的Verifys.go文件中,就没有再具体再深入一步分析了。
于是就还是采用根证书签发的方式添加ipsans,这样使用golang的http客户端链接就可以认证成功了,具体的例子都列在下面,注意在生成server.crt文件的时候要添加上额外的参数:
openssl genrsa -out ca.key 2048 openssl req -x509 -new -nodes -key ca.key -subj "/CN=xxx.com" -days 5000 -out ca.crt openssl genrsa -out server.key 2048 openssl req -new -key server.key -subj "/CN=server" -config openssl.conf -out server.csr #注意相比普通场景 这里要添加额外的参数 openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 5000 -extensions v3_req -extfile openssl.conf
但是坑爹的是mac上面的curl在这种情况下无法认证了,反而使用自签名的方式可以成功,这个证书认证还真是有点麻烦。
ssl中几种不同后缀文件的作用
- .csr这个后缀的全称是(Certificate Signing Request),即是证书签名请求文件。有一些应用可以通过向证书颁发机构(certificate-authorities)提交请求,来生成这个文件。 这个文件实际的格式是 RFC 2986 中所定义的PKCS10 。它包括了证书请求的所有的相关细节信息,包括 subject ,orginataion,state,whatnot,以及证书的公钥,之后csr文件会被提交给ca,ca在其基础上进行数字签名,将签名之后的文件返回过来,这个返回过来的文件就是公钥文件(其中包含了public key不包含private key)。这个返回回来的文件也可以有多种格式。可以用如下的命令查看相关信息openssl req -text -noout -in .csr。
- .pem在RFC1421到1424中有具体的定义,这是一个集合格式(container format)可能包含公有证书,比如在apache安装的时候自带的文件,或者是可以查看在/etc/ssl/certs 文件中的文件。可以用如下的命令查看openssl x509 -in .pem -inform pem -noout -text。.pem文件也可以包含整个的证书认证链,包括公钥,私钥,以及更证书,在haproxy配置的时候,只需要填一个.pem文件的地址,这个.pem文件实际上是公钥证书,私钥证书,以及ca文件放在一起拼接起来的,比如这样 sudo cat /etc/ssl/xx.crt /etc/ssl/xx.key /etc/ssl/ca.crt | sudo tee /etc/ssl/containerfile.pem。pem的全称是Privacy Enhanced Mail 这个协议已经无法保证邮件传输的安全,但是集合格式仍然依赖这个格式。
- .key这个文件是采用PEM处理过的文件,包含一个私钥信息,仅仅是一个传统上的名字,并不是一个标准的命名(是指上还是pem类型的文件),在apache服务器安装的时候,这个文件通常被放在/etc/ssl/private文件夹中。这个文件的权限是很重要的,如果这个文件的权限被设置错误的话,有一些应用就不会去加载这些这个数文件。
- .pkcs12 .pkx .p12最初被RSA定义在 Public-Key Cryptography Stantard 中,变量12表示这个是被Microsoft加强过的。这个格式同时包含公钥和私钥证书对,不同于.pem文件,这个格式的文件是被加密过的,Openssl可以把这个格式的文件转化成为同时包含公钥和私钥信息的文件:openssl pkcs12 -in file-to-convert.p12 -out converted-file.pem -nodes
- der der是另一种证书格式,它根据ASN1 DER 格式来存储,ASN1(Abstract Syntax Notation One)也是一种对于数据表示,编码以及传输的数据格式。.pem实际上是采用base64加密之后的.der文件。openssl也可以把.der文件转化成为.pem文件openssl x509 -inform der -in to-convert.der -out converted.pem http://gagravarr.org/writing/openssl-certs/general.shtml
- .crt .cer .cert 实质上是.pem(准确的说是.der)格式的文件的不同扩展。以这些后缀结尾的文件,会被windows的浏览器默认为是证书文件,而.pem不会被这样认出。
- .pb7这个格式是windows所使用的证书交换格式,Java会支持这个格式,不像.pem格式的文件,这中类型的文件会包含路径的安全信息。
- .crl证书撤回list,ca在期满的时候,可能会通过发行这个文件来解除授权。
https客户端和服务端单向校验
这部分参考了这个(http://www.tuicool.com/articles/aymYbmM ),里面代码部分讲得比较细致。
服务端采用证书,客户端采用普通方式访问:
//server端代码 package main import ( "fmt" "net/http" "os" ) func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hi, This is an example of https service in golang!") } func main() { http.HandleFunc("/", handler) //http.ListenAndServe(":8080", nil) _, err := os.Open("cert_server/server.crt") if err != nil { panic(err) } http.ListenAndServeTLS(":8081", "cert_server/server.crt", "cert_server/server.key", nil) }
client端直接发请求,什么都不加,会报如下错误:
2015/07/11 18:13:50 http: TLS handshake error from 10.183.47.203:58042: remote error: bad certificate
使用浏览器直接访问的话,之后点击信赖证书,这个时候就可以正常get到消息
或者使用(curl -k https:// …) 来进行访问,相当于忽略了第一步的身份验证的工作。 要是不加-k的话 使用curl -v 参数打印出来详细的信息,会看到如下的错误:
curl: (60) SSL certificate problem: Invalid certificate chain
说明是认证没有通过,因为客户端这面并没有提供可以信赖的根证书来对服务端发过来的证书进行验,/CN使用的直接是ip地址,就会报下面的错误:
Get https://10.183.47.206:8081: x509: cannot validate certificate for 10.183.47.206 because it doesn't contain any IP SANs
最好是生成证书的时候使用域名,或者是在/etc/hosts中加上对应的映射,或者直接采用上面所提到的添加了san的证书生成方式,**需要注意的是,由于使用ipsan的时候采用的是自签名的方式,因此客户端发送请求的时候在cacert中指定的根证书就是server.crt。
可以发送请求的客户端的代码如下,注意导入根证书的方式:
package main import ( //"io" //"log" "crypto/tls" "crypto/x509" //"encoding/json" "fmt" "io/ioutil" "net/http" //"strings" ) func main() { //x509.Certificate. pool := x509.NewCertPool() //caCertPath := "etcdcerts/ca.crt" caCertPath := "certs/cert_server/ca.crt" caCrt, err := ioutil.ReadFile(caCertPath) if err != nil { fmt.Println("ReadFile err:", err) return } pool.AppendCertsFromPEM(caCrt) //pool.AddCert(caCrt) tr := &http.Transport{ TLSClientConfig: &tls.Config{RootCAs: pool}, DisableCompression: true, } client := &http.Client{Transport: tr} resp, err := client.Get("https://server:8081") if err != nil { panic(err) } body, _ := ioutil.ReadAll(resp.Body) fmt.Println(string(body)) fmt.Println(resp.Status) }
使用curl命令的话,就加上–cacrt ca.crt证书,这样就相当于添加了可信赖的证书,身份认证的操作就可以成功了。
比如生成服务端证书的时候/CN写的是server 那client发送的时候也发送给 https://server:8081
不过在本地的/etc/hosts中要加上对应的映射。
客户端和服务端的双向校验
按照之前的方式,客户端生成证书,根证书就按之前的那个:
openssl genrsa -out client.key 2048 openssl req -new -key client.key -subj "/CN=client" -out client.csr openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 5000
server端代码进行改进,添加受信任的根证书,注意server端启动的时候的参数,这里的clientauth选择的是ClientAuth: tls.RequireAndVerifyClientCert,可以根据不同的情况选择是否跳过对客户端的身份验证,比如ClientAuth: tls.NoClientCert就不要求客户端提供证书来验证。
// gohttps/6-dual-verify-certs/server.go package main import ( "crypto/tls" "crypto/x509" "fmt" "io/ioutil" "net/http" ) type myhandler struct { } func (h *myhandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hi, This is an example of http service in golang!\n") } func main() { pool := x509.NewCertPool() caCertPath := "cert_server/ca.crt" caCrt, err := ioutil.ReadFile(caCertPath) if err != nil { fmt.Println("ReadFile err:", err) return } pool.AppendCertsFromPEM(caCrt) s := &http.Server{ Addr: ":8081", Handler: &myhandler{}, TLSConfig: &tls.Config{ ClientCAs: pool, ClientAuth: tls.RequireAndVerifyClientCert, }, } err = s.ListenAndServeTLS("cert_server/server.crt", "cert_server/server.key") if err != nil { fmt.Println("ListenAndServeTLS err:", err) } }
客户端代码改进,发送的时候把指定client端的client.crt以及client.key,同样的道理,客户端也可以采用参数InsecureSkipVerify :true,来跳过对服务端的证书身份验证,功能类似于curl命令的-k参数。
// gohttps/6-dual-verify-certs/server.go package main import ( "crypto/tls" "crypto/x509" "fmt" "io/ioutil" "net/http" ) type myhandler struct { } func (h *myhandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hi, This is an example of http service in golang!\n") } func main() { pool := x509.NewCertPool() caCertPath := "cert_server/ca.crt" caCrt, err := ioutil.ReadFile(caCertPath) if err != nil { fmt.Println("ReadFile err:", err) return } pool.AppendCertsFromPEM(caCrt) s := &http.Server{ Addr: ":8081", Handler: &myhandler{}, TLSConfig: &tls.Config{ ClientCAs: pool, ClientAuth: tls.RequireAndVerifyClientCert, }, } err = s.ListenAndServeTLS("cert_server/server.crt", "cert_server/server.key") if err != nil { fmt.Println("ListenAndServeTLS err:", err) } }
客户端代码改进,发送的时候把指定client端的client.crt以及client.key,同样的道理,客户端也可以采用参数InsecureSkipVerify :true,来跳过对服务端的证书身份验证,功能类似于curl命令的-k参数。
// gohttps/6-dual-verify-certs/client.go package main import ( "crypto/tls" "crypto/x509" "fmt" "io/ioutil" "net/http" ) func main() { pool := x509.NewCertPool() caCertPath := "certs/cert_server/ca.crt" caCrt, err := ioutil.ReadFile(caCertPath) if err != nil { fmt.Println("ReadFile err:", err) return } pool.AppendCertsFromPEM(caCrt) cliCrt, err := tls.LoadX509KeyPair("certs/cert_server/client.crt", "certs/cert_server/client.key") if err != nil { fmt.Println("Loadx509keypair err:", err) return } tr := &http.Transport{ TLSClientConfig: &tls.Config{ RootCAs: pool, Certificates: []tls.Certificate{cliCrt}, //InsecureSkipVerify: true, }, } client := &http.Client{Transport: tr} resp, err := client.Get("https://server:8081") if err != nil { fmt.Println("Get error:", err) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) fmt.Println(string(body)) }
但要注意的是,如果按和server端同样的方式生成client端的证书,server端会报这样的错误:
client's certificate's extended key usage doesn't permit it to be used for client authentication
因为client的证书生成方式有一点不一样,向开始提到的那样,goalng对于client端的认证要多一个参数,生成证书的时候,要加上一个单独的认证信息:
openssl genrsa -out client.key 2048 openssl req -new -key client.key -subj "/CN=client" -out client.csr echo extendedKeyUsage=clientAuth > extfile.conf openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -extfile extfile.conf -out client.crt -days 5000
就是多添加一个认证文件的信息,之后使用新的证书就可以实现双向认证了,这样只有那些持有被认证过的证书的客户端才能向服务端发送请求。
单向https加token的使用方式
在实际操作的过程中,有的时候可能用到https的方式更多的是希望用到其安全传输的特性,身份验证的地方可能弱一点,比如在服务端放了server.crt以及server.key的证书,客户端单向使用https发请求的时候,必须还要指定自己的受信根证书,这时候还得把服务端的根证书提前分发给客户端,比较麻烦,可以在配置客户端的Transport的时候,把InsecureSkipVerify参数设置为true,这样就不会对服务端的证书进行身份验证了。服务端对客户端的验证可以通过在Header信息中添加token的参数来进行。但是这种只能用在测试的环境中,由于客户端没有对服务端传递过来的请求进行身份验证,很可能传递回来的请求被进行了篡改或者劫持,具体的细节不太清楚,总之还是有风险的,只适用于某些特殊的场合。
转载请注明:快乐编程 » https原理以及服务端和客户端golang的基本实现