F为签名函数。CA自己的私钥是唯一标识CA签名的,因此CA用于生成数字证书的签名函数一定要以自己的私钥作为一个输入参数。在RSA加密 系统中,发送端的解密函数就是一个以私钥作 为参数的函数,因此常常被用作签名函数使用。签名算法是与证书一并发送给接收 端的,比如apple的一个服务的证书中关于签名算法的描述是“带 RSA 加密的 SHA-256 ( 1.2.840.113549.1.1.11 )”。因此CA用私钥解密函数作为F,对C的摘要进行运算得到了客户数字证书的签名,好比大学毕业证上的校长签名,所有毕业证都是校长签发的。
接收端接收服务端数字证书后,如何验证数字证书上携带的签名是这个CA的签名呢?接收端会运用下面算法对数字证书的签名进行校验: F'(S) ?= Digest(C)
接收端进行两个计算,并将计算结果进行比对: 1、首先通过Digest(C),接收端计算出证书内容(除签名之外)的摘要。 2、数字证书携带的签名是CA通过CA密钥加密摘要后的结果,因此接收端通过一个解密函数F'对S进行“解密”。RSA系统中,接收端使用 CA公钥对S进行“解密”,这恰是CA用私钥对S进行“加密”的逆过程。
将上述两个运算的结果进行比较,如果一致,说明签名的确属于该CA,该证书有效,否则要么证书不是该CA的,要么就是中途被人篡改了。
但对于self-signed(自签发)证书来说,接收端并没有你这个self-CA的数字证书,也就是没有CA公钥,也就没有办法对数字证 书的签名进行验证。因此如果要编写一个可以对self-signed证书进行校验的接收端程序的话,首先我们要做的就是建立一个属于自己的 CA,用该CA签发我们的server端证书,并将该CA自身的数字证书随客户端一并发布。
这让我想起了在《搭建自己的ngrok服务》一文中为ngrok服务端、客户端生成证书的那几个步骤,我们来重温并分析一下每一步都在做什么。
(1)openssl genrsa -out rootCA.key 2048 (2)openssl req -x509 -new -nodes -key rootCA.key -subj "/CN=*.tunnel.tonybai.com" -days 5000 -out rootCA.pem
(3)openssl genrsa -out device.key 2048 (4)openssl req -new -key device.key -subj "/CN=*.tunnel.tonybai.com" -out device.csr (5)openssl x509 -req -in device.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out device.crt -days 5000
(6)cp rootCA.pem assets/client/tls/ngrokroot.crt (7)cp device.crt assets/server/tls/snakeoil.crt (8)cp device.key assets/server/tls/snakeoil.key
自己搭建ngrok服务,客户端要验证服务端证书,我们需要自己做CA,因此步骤(1)和步骤(2)就是生成CA自己的相关信息。 步骤(1) ,生成CA自己的私钥 rootCA.key 步骤(2),根据CA自己的私钥生成自签发的数字证书,该证书里包含CA自己的公钥。
步骤(3)~(5)是用来生成ngrok服务端的私钥和数字证书(由自CA签发)。 步骤(3),生成ngrok服务端私钥。 步骤(4),生成Certificate Sign Request,CSR,证书签名请求。 步骤(5),自CA用自己的CA私钥对服务端提交的csr进行签名处理,得到服务端的数字证书device.crt。
步骤(6),将自CA的数字证书同客户端一并发布,用于客户端对服务端的数字证书进行校验。 步骤(7)和步骤(8),将服务端的数字证书和私钥同服务端一并发布。
接下来我们来验证一下客户端对服务端数字证书进行验证(gohttps/5-verify-server-cert)!
首先我们来建立我们自己的CA,需要生成一个CA私钥和一个CA的数字证书:
$openssl genrsa -out ca.key 2048 Generating RSA private key, 2048 bit long modulus ……….+++ ………………………….+++ e is 65537 (0×10001)
$openssl req -x509 -new -nodes -key ca.key -subj "/CN=tonybai.com" -days 5000 -out ca.crt
接下来,生成server端的私钥,生成数字证书请求,并用我们的ca私钥签发server的数字证书:
openssl genrsa -out server.key 2048 Generating RSA private key, 2048 bit long modulus ….+++ …………………….+++ e is 65537 (0×10001)
$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 Signature ok subject=/CN=localhost Getting CA Private Key
现在我们的工作目录下有如下一些私钥和证书文件: CA: 私钥文件 ca.key 数字证书 ca.crt
Server: 私钥文件 server.key 数字证书 server.crt
接下来,我们就来完成我们的程序。
Server端的程序几乎没有变化:
// gohttps/5-verify-server-cert/server.go package main
import ( "fmt" "net/http" )
func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hi, This is an example of http service in golang!") }
func main() { http.HandleFunc("/", handler) http.ListenAndServeTLS(":8081", "server.crt", "server.key", nil) }
client端程序变化较大,由于client端需要验证server端的数字证书,因此client端需要预先加载ca.crt,以用于服务端数字证书的校验:
// gohttps/5-verify-server-cert/client.go package main
import ( "crypto/tls" "crypto/x509" "fmt" "io/ioutil" "net/http" )
func main() { pool := x509.NewCertPool() caCertPath := "ca.crt"
caCrt, err := ioutil.ReadFile(caCertPath) if err != nil { fmt.Println("ReadFile err:", err) return } pool.AppendCertsFromPEM(caCrt)
tr := &http.Transport{ TLSClientConfig: &tls.Config{RootCAs: pool}, } 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)) }
运行server和client:
$go run server.go
go run client.go Hi, This is an example of http service in golang!
六、对客户端的证书进行校验(双向证书校验)
服务端可以要求对客户端的证书进行校验,以更严格识别客户端的身份,限制客户端的访问。
要对客户端数字证书进行校验,首先客户端需要先有自己的证书。我们以上面的例子为基础,生成客户端的私钥与证书。
$openssl genrsa -out client.key 2048 Generating RSA private key, 2048 bit long modulus ………………..+++ ………………..+++ e is 65537 (0×10001) $openssl req -new -key client.key -subj "/CN=tonybai_cn" -out client.csr $openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 5000 Signature ok subject=/CN=tonybai_cn Getting CA Private Key
接下来我们来改造我们的程序,首先是server端。
首先server端需要要求校验client端的数字证书,并且加载用于校验数字证书的ca.crt,因此我们需要对server进行更加灵活的控制:
// 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 := "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("server.crt", "server.key") if err != nil { fmt.Println("ListenAndServeTLS err:", err) } }
可以看出代码通过将tls.Config.ClientAuth赋值为tls.RequireAndVerifyClientCert来实现Server强制校验client端证书。ClientCAs是用来校验客户端证书的ca certificate。
Client端变化也很大,需要加载client.key和client.crt用于server端连接时的证书校验:
// 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 := "ca.crt"
caCrt, err := ioutil.ReadFile(caCertPath) if err != nil { fmt.Println("ReadFile err:", err) return } pool.AppendCertsFromPEM(caCrt)
cliCrt, err := tls.LoadX509KeyPair("client.crt", "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)) }
好了,让我们来试着运行一下这两个程序,结果如下:
$go run server.go 2015/04/30 22:13:33 http: TLS handshake error from 127.0.0.1:53542: tls: client's certificate's extended key usage doesn't permit it to be used for client authentication
$go run client.go Get error: Get https://localhost:8081: remote error: handshake failure
失败了!从server端的错误日志来看,似乎是client端的client.crt文件不满足某些条件。
根据server端的错误日志,搜索了Golang的源码,发现错误出自crypto/tls/handshake_server.go。
k := false for _, ku := range certs[0].ExtKeyUsage { if ku == x509.ExtKeyUsageClientAuth { ok = true break } } if !ok { c.sendAlert(alertHandshakeFailure) return nil, errors.New("tls: client's certificate's extended key usage doesn't permit it to be used for client authentication") }
大致判断是证书中的ExtKeyUsage信息应该包含clientAuth。翻看openssl的相关资料,了解到自CA签名的数字证书中包含的都是一些basic的信息,根本没有ExtKeyUsage的信息。我们可以用命令来查看一下当前client.crt的内容:
$ openssl x509 -text -in client.crt -noout Certificate: Data: Version: 1 (0×0) Serial Number: d6:e3:f6:fa:ae:65:ed:df Signature Algorithm: sha1WithRSAEncryption Issuer: CN=tonybai.com Validity Not Before: Apr 30 14:11:34 2015 GMT Not After : Jan 6 14:11:34 2029 GMT Subject: CN=tonybai_cn Subject Public Key Info: Public Key Algorithm: rsaEncryption RSA Public Key: (2048 bit) Modulus (2048 bit): 00:e4:12:22:50:75:ae:b2:8a:9e:56:d5:f3:7d:31: 7b:aa:75:5d:3f:90:05:4e:ff:ed:9a:0a:2a:75:15: … … Exponent: 65537 (0×10001) Signature Algorithm: sha1WithRSAEncryption 76:3b:31:3e:9d:b0:66:ad:c0:03:d4:19:c6:f2:1a:52:91:d6: 13:31:3a:c5:d5:58:ea:42:1d:b7:33:b8:43:a8:a8:28:91:ac: … …
而偏偏golang的tls又要校验ExtKeyUsage,如此我们需要重新生成client.crt,并在生成时指定extKeyUsage。经过摸索,可以用如下方法重新生成client.crt:
1、创建文件client.ext 内容: extendedKeyUsage=clientAuth
2、重建client.crt
$openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -extfile client.ext -out client.crt -days 5000 Signature ok subject=/CN=tonybai_cn Getting CA Private Key
再通过命令查看一下新client.crt:
看到输出的文本中多了这么几行: X509v3 extensions: X509v3 Extended Key Usage: TLS Web Client Authentication
这说明client.crt的extended key usage已经添加成功了。我们再来执行一下server和client:
$ go run client.go Hi, This is an example of http service in golang!
client端证书验证成功,也就是说双向证书验证均ok了。
七、小结
通过上面的例子可以看出,使用golang开发https相关程序十分便利,Golang标准库已经实现了TLS 1.2版本协议。上述所有example代码均放在我的github上的experiments/gohttps中。
本文来自:Tony Bai
感谢作者:bigwhite
查看原文:Go和HTTPS