当Http协议变成Https协议时需要CA证书,简单场景下只需在Nginx上配置CA证书即可,后端服务无需更改。
但是当不存在类似Nginx的服务时,就需要后端服务进行调整,加载CA证书,使其支持Https协议。

本篇文章主要介绍下Golang如何进行CA认证。

CA证书生成

因为go 1.15之后废弃了CommonName,所以在生成证书时有所区别,为了方便证书的生成,针对不同的证书制定不同的配置文件。

生成CA根证书

进入ca根证书所在目录,这里是./ca/

  1. 新建ca.conf文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[ req ]
default_bits = 4096
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
countryName = GB
countryName_default = BeiJing
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = BeiJing
localityName = Locality Name (eg, city)
localityName_default = BeiJing
organizationName = Organization Name (eg, company)
organizationName_default = Step
commonName = localhost
commonName_max = 64
commonName_default = localhost
  1. 生成CA密钥
1
openssl genrsa -out ca.key 4096
  1. 生成CA证书
1
openssl req -new -x509 -days 365 -subj "/C=GB/L=Beijing/O=github/CN=localhost" -key ca.key -out ca.crt -config ca.conf

生成服务端证书

进入server证书存放目录,这里是./server/

  1. 新建server.conf文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[ req ]
default_bits = 2048
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = CN
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = BeiJing
localityName = Locality Name (eg, city)
localityName_default = BeiJing
organizationName = Organization Name (eg, company)
organizationName_default = Step
commonName = CommonName (e.g. server FQDN or YOUR name)
commonName_max = 64
commonName_default = XXX(自定义,客户端需要此字段做匹配)
[ req_ext ]
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
IP = 127.0.0.1
  1. 生成server端密钥
1
openssl genrsa -out server.key 2048
  1. 生成CSR
1
openssl req -new  -subj "/C=GB/L=Beijing/O=github/CN=localhost" -key server.key -out server.csr -config server.conf
  1. 根据根证书生成服务端证书
1
openssl x509 -req -sha256 -CA ca.crt -CAkey ../ca/ca.key -CAcreateserial -days 365 -in server.csr -out server.crt -extensions req_ext -extfile server.conf

生成客户端证书

进入client证书存放目录,这里是./client/

  1. 生成client端密钥
1
openssl genrsa -out client.key 2048
  1. 生成CSR
1
openssl req -new -subj "/C=GB/L=Beijing/O=github/CN=localhost"  -key client.key -out client.csr 
  1. 根据根证书生成客户端证书
1
openssl x509 -req -sha256 -CA ca.crt -CAkey ../ca/ca.key -CAcreateserial -days 365 -in client.csr -out client.crt

根证书、服务器端证书和客户端证书生成之后就可以配置CA认证了,客户端证书只有在双向认证时才需要,服务器端证书和根证书是开启CA认证必须的文件。

CA认证Demo

Golang在代码层次开启CA认证功能,需要加载相应证书,根据加载的证书分为单向认证和双向认证。

单向认证

单向认证是在server端加载server证书和CA认证中心的根证书,开启CA认证,核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 加载CA, 添加进 caCertPool
caCert, err := ioutil.ReadFile(dir + "/ca/ca.crt")
if err != nil {
log.Println("try to load ca err", err)
return
}

caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

// 加载服务端证书(生产环境当中, 证书是第三方进行签名的, 而非自定义CA)
srvCert, err := tls.LoadX509KeyPair(dir+"/server/server.crt", dir+"/server/server.key")
if err != nil {
log.Println("try to load key & crt err", err)
return
}

// 配置tls加载证书
config := &tls.Config{
Certificates: []tls.Certificate{srvCert},
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
},
}

// 启动https server
server := &http.Server{
Addr: ":1443",
Handler: mux,
TLSConfig: config,
ErrorLog: log.New(os.Stdout, "", log.Lshortfile|log.Ldate|log.Ltime),
}

http2.ConfigureServer(server, &http2.Server{})

err = server.ListenAndServeTLS("", "")
if err != nil {
log.Println("ListenAndServeTLS err", err)
return
}

在浏览器中输入https://localhost:1443/,由于服务端证书是自有认证中心生成的,浏览器中并没有该服务端证书的根证书,所以浏览器会报错,报错内容如下:


然后点击最下方的继续浏览localhost即可访问。

双向认证

双向认证开启后,客户端在请求时需携带客户端证书和根证书,服务端还需开启验证客户端证书配置,相关代码如下:

服务端只需修改tls.config,增加客户端证书验证配置,然后重启服务即可。

1
2
3
4
5
6
7
8
9
10
11
   // 配置tls加载证书
config := &tls.Config{
Certificates: []tls.Certificate{srvCert},
ClientCAs: caCertPool,
InsecureSkipVerify: true,
ClientAuth: tls.RequireAndVerifyClientCert,
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
},
}

客户端加载客户端证书,这里直接使用golang写了个客户端,也可以使用curl访问。核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 加载CA证书(生产环境不需要这一步)
caCrt, err := ioutil.ReadFile(dir + "/ca/ca.crt")
if err != nil {
panic("try to load ca err, " + err.Error())
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCrt)

// 加载客户端证书
cliCert, err := tls.LoadX509KeyPair(dir+"/client/client.crt", dir+"/client/client.key")
if err != nil {
panic("try to load key & key err, " + err.Error())
}

config := &tls.Config{
Certificates: []tls.Certificate{cliCert},
RootCAs: caCertPool,
InsecureSkipVerify: false,
ServerName: "localhost",
}
config.BuildNameToCertificate()

client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: config,
},
Timeout: 10 * time.Second,
}

此时如果在浏览器中访问https://localhost:1443/,会无法访问,而且也不能忽略证书,因为服务端要求认证客户端证书,而浏览器中又没有客户端证书,所以无法访问,页面显示如下:

到此,关于CA认证的简单使用就结束了,熟悉了单向认证和双向认证的使用效果。
但是我调研CA认证需要解决的场景是多个客户端向服务端发送数据,服务端验证客户端证书是否合法,比较不同的是多个客户端的证书并不是由同一个认证中心颁发的,这就需要服务端在接收到客户端请求时,根据客户端的证书去动态的加载对应认证中心生成的服务端证书。

动态加载证书

这个场景的需求有两个:

  1. 验证客户端的证书
  2. 验证客户端证书时需加载对应的服务端证书
    要想满足需求1则必须开启双向认证,其次要想加载对应的服务端证书就得根据客户端的证书动态加载服务端证书,动态加载的功能通过GetConfigForClientServerName来实现。其核心代码如下:

服务端也是只需修改tls.config即可,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
config := &tls.Config{
// 加载server自己的证书
Certificates: []tls.Certificate{srvCert}, // 服务器证书

CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
},

GetConfigForClient: func(info *tls.ClientHelloInfo) (*tls.Config, error) {
if info.ServerName == "localhost" {
srvCert, _ = tls.LoadX509KeyPair(dir+"/server/server.crt", dir+"/server/server.key")

caCert, err := ioutil.ReadFile(dir + "/ca/ca.crt")
if err != nil {
log.Println("try to load ca err", err)
}
caCertPool.AppendCertsFromPEM(caCert)
} else if info.ServerName == "localhost1" {
srvCert, _ = tls.LoadX509KeyPair(dir+"/server1/server.crt", dir+"/server1/server.key")

caCert, err := ioutil.ReadFile(dir + "/ca1/ca.crt")
if err != nil {
log.Println("try to load ca err", err)
}
caCertPool.AppendCertsFromPEM(caCert)
}

serverConf := &tls.Config{
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
return &srvCert, nil
},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: caCertPool,
InsecureSkipVerify: true,
//VerifyPeerCertificate: getClientValidator(hi),
}
return serverConf, nil
},
}

服务端通过GetConfigForClientClientHelloInfo中获取客户端请求中的ServerName,根据其加载不同的服务端证书即可。

在客户端,则需要不同的客户端加载各自的证书即可,需要注意的就是在tls.Config中的ServerName属性配置自己向CA认证中心时申请证书时所标注的CommonName,服务端就是根据ServerName加载对应的服务端证书的(同样生成服务端证书时CommonName需与ServerName一致)。

1
2
3
4
5
6
config := &tls.Config{
Certificates: []tls.Certificate{cliCert},
RootCAs: caCertPool,
InsecureSkipVerify: false,
ServerName: "localhost",
}

这样服务端就可以认证不同的客户端证书了。