用Go语言写HTTPS程序

这篇文字基本是Tony Bai的这篇博客tony的翻版;只是使
内容和前两篇介绍TLS原理的OpenSSL操作的文字衔接。

单向验证身份

一般的HTTPS服务都是只需要客户端验证服务器的身份就好了。比如我们想访问
银行的网站,我们得确认那个网站真是我们要访问的银行的网站,而不是一个界
面类似的用来诱骗我们输入银行账号和密码的钓鱼网站。而银行网站并不需要通
过TLS验证我们的身份,因为我们会通过在网页里输入账号的密码向服务器展示
我们的用户身份。

HTTPS服务器程序

上文中我们贴了一个用Go语言写的HTTPS
server程序[./server.go](./server.go)

  1. package main
  2. import (
  3. "io"
  4. "log"
  5. "net/http"
  6. )
  7. func main() {
  8. http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
  9. io.WriteString(w, "hello, world!\n")
  10. })
  11. if e := http.ListenAndServeTLS(":443", "server.crt", "server.key", nil); e != nil {
  12. log.Fatal("ListenAndServe: ", e)
  13. }
  14. }

我们可以用[create_tls_asserts.bash](./create_tls_asserts.bash)创建私
server.key和身份证server.crt

  1. openssl genrsa -out server.key 2048
  2. openssl req -nodes -new -key server.key -subj "/CN=localhost" -out server.csr
  3. openssl x509 -req -sha256 -days 365 -in server.csr -signkey server.key -out server.crt

并且启动服务程序:

  1. sudo go run server.go &

不验证服务器身份的客户端程序

上文中我们展示了可以给curl一个-k参数,
让它不验证服务器身份即访问。我们自己也写一个类似curl -k的client程序
[unsecure-client.go](./unsecure-client.go)来坚持访问一个不一定安全的
HTTPS server:

  1. package main
  2. import (
  3. "crypto/tls"
  4. "io"
  5. "log"
  6. "net/http"
  7. "os"
  8. )
  9. func main() {
  10. c := &http.Client{
  11. Transport: &http.Transport{
  12. TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
  13. }}
  14. if resp, e := c.Get("https://localhost"); e != nil {
  15. log.Fatal("http.Client.Get: ", e)
  16. } else {
  17. defer resp.Body.Close()
  18. io.Copy(os.Stdout, resp.Body)
  19. }
  20. }

用以下命令编译和启动这个客户端程序:

  1. $ go run unsecure-client.go
  2. hello, world!

用自签署的身份证验证服务器身份

上文中我们还展示了可以把服务器的身份证
server.crt通过--cacert参数传给curl,让curl用服务器自己的身份证验证
它自己。类似的,我们也可以写一个类似curl --cacert server.crt的Go程序
[secure-client.go](./secure-client.go)来访问HTTPS server。这个程序和
上一个的区别仅仅在于 TLSClientConfig 的配置方式:

  1. c := &http.Client{
  2. Transport: &http.Transport{
  3. TLSClientConfig: &tls.Config{RootCAs: loadCA("server.crt")},
  4. }}

其中 loadCA 的实现很简单:

  1. func loadCA(caFile string) *x509.CertPool {
  2. pool := x509.NewCertPool()
  3. if ca, e := ioutil.ReadFile(caFile); e != nil {
  4. log.Fatal("ReadFile: ", e)
  5. } else {
  6. pool.AppendCertsFromPEM(ca)
  7. }
  8. return pool
  9. }

双方认证对方身份

有的时候,客户端通过输入账号和密码向服务器端展示自己的身份的方式太过繁
琐。尤其是在如果客户端并不是一个人,而只是一个程序的时候。这时,我们希
望双方都利用一个身份证(certificate)通过TLS协议向对方展示自己的身份。
比如这个关于Kubernetes的例子

创建CA并签署server以及client的身份证

我们可以按照上文中例子展示的:让通
信双方互相交换身份证,这样既可互相验证。但是如果一个分布式系统里有多方,
任意两房都要交换身份证太麻烦了。我们通常创建一个
自签署的根身份证,然后用它来签署分布式系
统中各方的身份证。这样每一方都只要有这个根身份证即可验证所有其他通信方。
这里解释了用OpenSSL生成根身份证和签署其他身
份证的过程。针对我们的例子,具体过程如下:

  1. 创建我们自己CA的私钥:

    1. openssl genrsa -out ca.key 2048

    创建我们自己CA的CSR,并且用自己的私钥自签署之,得到CA的身份证:

    1. openssl req -x509 -new -nodes -key ca.key -days 10000 -out ca.crt -subj "/CN=we-as-ca"
  2. 创建server的私钥,CSR,并且用CA的私钥自签署server的身份证:

    1. openssl genrsa -out server.key 2048
    2. openssl req -new -key server.key -out server.csr -subj "/CN=localhost"
    3. openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365
  3. 创建client的私钥,CSR,以及用ca.key签署client的身份证:

    1. openssl genrsa -out client.key 2048
    2. openssl req -new -key client.key -out client.csr -subj "/CN=localhost"
    3. openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 365

Server

相对于上面的例子,server的源码
[./bidirectional/server.go](./bidirectional/server.go)稍作了一些修改:
增加了一个 http.Server 变量s,并且调用s.ListenAndServeTLS,而不
是像之前那样直接调用http.ListenAndServeTLS了:

  1. func main() {
  2. s := &http.Server{
  3. Addr: ":443",
  4. Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  5. fmt.Fprintf(w, "Hello World!\n")
  6. }),
  7. TLSConfig: &tls.Config{
  8. ClientCAs: loadCA("ca.crt"),
  9. ClientAuth: tls.RequireAndVerifyClientCert,
  10. },
  11. }
  12. e := s.ListenAndServeTLS("server.crt", "server.key")
  13. if e != nil {
  14. log.Fatal("ListenAndServeTLS: ", e)
  15. }
  16. }

Client

客户端程序[./bidirectional/client.go](./bidirectional/client.go)相对
于上面提到的unsecure-client.gosecure-client.go的变化主要在于

  1. 调用tls.LoadX509KeyPair读取client.keyclient.crt,并返回一个
    tls.Certificate变量,
  2. 把这个变量传递给http.Client变量,然后调用其Get函数。
  1. func main() {
  2. pair, e := tls.LoadX509KeyPair("client.crt", "client.key")
  3. if e != nil {
  4. log.Fatal("LoadX509KeyPair:", e)
  5. }
  6. client := &http.Client{
  7. Transport: &http.Transport{
  8. TLSClientConfig: &tls.Config{
  9. RootCAs: loadCA("ca.crt"),
  10. Certificates: []tls.Certificate{pair},
  11. },
  12. }}
  13. resp, e := client.Get("https://localhost")
  14. if e != nil {
  15. log.Fatal("http.Client.Get: ", e)
  16. }
  17. defer resp.Body.Close()
  18. io.Copy(os.Stdout, resp.Body)
  19. }

运行和测试

  1. cd bidirectional
  2. ./create_tls_asserts.bash # 创建各种TLS资源
  3. sudo go run ./server.go & # 启动服务器
  4. go run ./client.go # 尝试连接服务器

应当看到屏幕上打印出来 Hello World!

在这篇博客tony中提到,需要创建一个client.ext文件,
使得client的身份证里包含ExtKeyUsage字段。但是我并没有这么做,得到的
程序也可以运行。

参考文献