最近工作中有相关项目有安全要求,需要用到 HTTPS 的双向数字证书校验,以增强应用的数据交换安全。
数字签名
数字证书由两部分组成:
- C:证书相关信息(对象名称 + 过期时间 + 证书发布者 + 证书签名算法….)
- S:证书的数字签名
其中的数字签名是通过公式 S = F(Digest(C))
得到的。
Digest 为摘要函数,也就是 md5、sha-1 或 sha256 等单向散列算法,用于将无限输入值转换为一个有限长度的“浓缩”输出值。比如我们常用 md5 值来验证下载的大文件是否完 整。大文件的内容就是一个无限输入。大文件被放在网站上用于下载时,网站会对大文件做一次 md5 计算,得出一个 128bit 的值作为大文件的 摘要一同放在网站上。用户在下载文件后,对下载后的文件再进行一次本地的 md5 计算,用得出的值与网站上的 md5 值进行比较,如果一致,则大 文件下载完好,否则下载过程大文件内容有损坏或源文件被篡改。
F 为签名函数。CA 自己的私钥是唯一标识 CA 签名的,因此 CA 用于生成数字证书的签名函数一定要以自己的私钥作为一个输入参数。在 RSA 加密 系统中,发送端的解密函数就是一个以私钥作 为参数的函数,因此常常被用作签名函数使用。签名算法是与证书一并发送给接收 端的,比如 apple 的一个服务的证书中关于签名算法的描述是“带 RSA 加密的 SHA-256 ( 1.2.840.113549.1.1.11 )
”。因此 CA 用私钥解密函数作为 F,对 C 的摘要进行运算得到了客户数字证书的签名,好比大学毕业证上的校长签名,所有毕业证都是校长签发的。
签名校验
接收端接收服务端数字证书后,如何验证数字证书上携带的签名是这个 CA 的签名呢?接收端会运用下面算法对数字证书的签名进行校验:
F'(S) ?= Digest(C)
接收端进行两个计算,并将计算结果进行比对:
- 首先通过 Digest(C),接收端计算出证书内容(除签名之外)的摘要。
- 数字证书携带的签名是 CA 通过 CA 密钥加密摘要后的结果,因此接收端通过一个解密函数 F’对 S 进行“解密”。RSA 系统中,接收端使用 CA 公钥对 S 进行“解密”,这恰是 CA 用私钥对 S 进行“加密”的逆过程。
将上述两个运算的结果进行比较,如果一致,说明签名的确属于该 CA,该证书有效,否则要么证书不是该 CA 的,要么就是中途被人篡改了。
Golang 使用 HTTPS 双向证书验证步骤
-
签发 CA 根证书
-
签发服务端证书
-
签发客户端证书
-
编写服务端代码
-
编写客户端代码
-
进行交互验证
-
签发 CA 根证书
- 生成 CA 自己的私钥 ca.key
- 根据 CA 自己的私钥生成自签发的数字证书,该证书里包含 CA 自己的公钥。
openssl genrsa -out rootCA.key 2048 openssl req -x509 -new -nodes -key ca.key -subj "/CN=vsjclub.com" -days 5000 -out ca.pem 或者生成 csr,然后再生成证书 openssl req -new -sha256 -out ca.csr -key ca.key openssl x509 -req -in ca.csr -signkey ca.key -days 5000 -out ca.crt
-
签发服务端证书
- 生成服务端私钥;
- 生成 Certificate Sign Request,CSR,证书签名请求;
- 自 CA 用自己的 CA 私钥对服务端提交的 csr 进行签名处理,得到服务端的数字证书 server.crt。
openssl genrsa -out server.key 2048 openssl req -new -key server.key -subj "/CN=localhost" -out server.csr openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 5000
- 从证书中提取公钥
openssl x509 -in server.crt -pubkey -noout > public.key
-
签发客户端证书
openssl genrsa -out client.key 2048 openssl req -new -key client.key -subj "/CN=vsjclub.com" -out client.csr openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 5000
服务端程序
// golnag/https/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 := "rsa/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("rsa/server.crt", "rsa/server.key")
if err != nil {
fmt.Println("ListenAndServeTLS err:", err)
}
}
客户端程序
// golang/https_client.go
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net/http"
)
func main() {
pool := x509.NewCertPool()
caCertPath := "rsa/ca.crt"
caCrt, err := ioutil.ReadFile(caCertPath)
if err != nil {
fmt.Println("ReadFile err:", err)
return
}
pool.AppendCertsFromPEM(caCrt)
cliCrt, err := tls.LoadX509KeyPair("rsa/client.crt", "rsa/client.key")
if err != nil {
fmt.Println("Loadx509keypair err:", err)
return
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: pool,
Certificates: []tls.Certificate{cliCrt},
},
}
client := &http.Client{Transport: tr}
resp, err := client.Get("https://localhost:8081")
if err != nil {
fmt.Println("Get error:", err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
}
结语
通过上面的例子可以看到,Golang 做 HTTPS 相关的开发是非常便利的,Golang 标准库已经实现了 TLS 1.2 版本协议。