最近工作中有相关项目有安全要求,需要用到 HTTPS 的双向数字证书校验,以增强应用的数据交换安全。

数字签名

数字证书由两部分组成:

  1. C:证书相关信息(对象名称 + 过期时间 + 证书发布者 + 证书签名算法….)
  2. S:证书的数字签名
S = F(Digest(C))

Digest 为摘要函数,也就是 md5、sha-1 或 sha256 等单向散列算法,用于将无限输入值转换为一个有限长度的“浓缩”输出值。比如我们常用 md5 值来验证下载的大文件是否完 整。大文件的内容就是一个无限输入。大文件被放在网站上用于下载时,网站会对大文件做一次 md5 计算,得出一个 128bit 的值作为大文件的 摘要一同放在网站上。用户在下载文件后,对下载后的文件再进行一次本地的 md5 计算,用得出的值与网站上的 md5 值进行比较,如果一致,则大 文件下载完好,否则下载过程大文件内容有损坏或源文件被篡改。

SHA-256 ( 1.2.840.113549.1.1.11 )

签名校验

F'(S) ?= Digest(C)

接收端进行两个计算,并将计算结果进行比对:

  1. 首先通过 Digest(C),接收端计算出证书内容(除签名之外)的摘要。
  2. 数字证书携带的签名是 CA 通过 CA 密钥加密摘要后的结果,因此接收端通过一个解密函数 F’对 S 进行“解密”。RSA 系统中,接收端使用 CA 公钥对 S 进行“解密”,这恰是 CA 用私钥对 S 进行“加密”的逆过程。

将上述两个运算的结果进行比较,如果一致,说明签名的确属于该 CA,该证书有效,否则要么证书不是该 CA 的,要么就是中途被人篡改了。

Golang 使用 HTTPS 双向证书验证步骤

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
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 版本协议。