0%

自签发双向证书认证的一种思路

服务器双向认证,原理上就是相互交换公钥,双方通过公钥认证对方。不同的是服务器双向认证,有一方作为主导方,另一方作为连接方。所以网上讲的自签发证书需要生成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
#!/usr/bin/env bash 

# call this script with an email address (valid or not).
# must install openssl before run this script.
# like:
# ./makecert.sh [email protected]

FULLPATH=$1

rm -f ${FULLPATH}/client.pem ${FULLPATH}/client.key

SUBJECT_SERVER="/C=CN/ST=Shanghai/L=Earth/O=BOX/OU=DC/CN=192.168.10.189/emailAddress"
SUBJECT_CLIENT="/C=CN/ST=Shanghai/L=Earth/O=BOX/OU=DC/CN=192.168.10.190/emailAddress"

EMAIL=${2:[email protected]}
DAYS=${3:-3650}

# 生成服务器证书对,pem和key

openssl req -new -nodes -x509 -days ${DAYS} -out ${FULLPATH}/server.pem -keyout ${FULLPATH}/server.key -extensions v3_req -config openssl.cnf -subj "${SUBJECT_SERVER}=${EMAIL}"

# 生成客户端证书对,pem和key

openssl req -new -nodes -x509 -days ${DAYS} -out ${FULLPATH}/client.pem -keyout ${FULLPATH}/client.key -extensions v3_req -config openssl.cnf -subj "${SUBJECT_CLIENT}=${EMAIL}"

openssl.cnf 配置文件

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
[req] 

distinguished_name = req_distinguished_name

req_extensions = v3_req

[req_distinguished_name]

countryName = Country Name (2 letter code)

countryName_default = CN

stateOrProvinceName = State or Province Name (full name)

stateOrProvinceName_default = Shanghai

localityName = Locality Name (eg, city)

localityName_default = Shanghai

organizationName = Organization Name (eg, company)

organizationName_default = BOXFoundation

organizationalUnitName = Organizational Unit Name (eg, section)

organizationalUnitName_default = BOX

commonName = Common Name (e.g. server FQDN or YOUR name)

commonName_default = server

commonName_max = 64

[v3_req]
basicConstraints = CA:false
subjectAltName = @alt_names

[alt_names]
IP.1 = 192.168.10.190
IP.2 = 192.168.10.189

IP: SANS

默认情况下,证书只针对域名设置。san(subjectAltName)用于进行额外的身份认证,默认的情况下使用主机名(域名)进行身份认证的。在上面列出的普通证书生成方式中,使用 -subj “/CN=server” 发布服务端.csr文件,之后正式通信的时候,就像上面列出的通信过程的第一步,服务端的证书会传过来给客户端,客户端进行验证的时候,发送的ip必须是 https://server:serverport 这种形式,否则就会验证失败。/CN字段是不能直接写成ip值的,可能会报下面的错误:

1
2
3

Get https://10.183.47.206:8081: x509: cannot validate certificate for 10.183.47.206 because it doesn't contain any IP SANs

想要直接通过ip来认证,需要添加一些额外的配置,利用subjectAltName添加ip到openssl证书。

创建openssl.cnf, 内容如下. 其中organizationalUnitName_default是你的组织名,commonName_default是域名,IP.1,IP.2则是想要加进来的IP列表了。

关闭CA证书约束

1
2
[v3_req] 
basicConstraints = CA:false

服务端代码示例

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package main 

import (
"alpha.2se.com/sample/grpc/common"
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"io/ioutil"
"net"
)

//go:generate protoc --proto_path=../common --go_out=plugins=grpc:../common ../common/model.proto

type Echo struct{}

func (echo *Echo) SayHello(ctx context.Context, req *common.HelloRequest) (*common.HelloReply, error) {
name := req.GetName()
reply := &common.HelloReply{}
reply.Message = fmt.Sprintf("Hello %s!", name)
return reply, nil
}

var (
certFile = common.KeyPath + "/server.pem"
keyFile = common.KeyPath + "/server.key"
clientFile = common.KeyPath + "/client.pem"
)

func newTLSFromFile(certFile string) (credentials.TransportCredentials, error) {
b, err := ioutil.ReadFile(clientFile)
if err != nil {
return nil, err
}
// 待认证的客户端证书池
cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM(b) {
return nil, fmt.Errorf("credentials: failed to append certificates")
}
// 服务端证书对,pem和key
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
fmt.Printf("载入证书出错! %v\n", err)
return nil, err
}

// 服务端证书和客户端证书之间可以没有直接必然关系,只是公钥和私钥之间的关系,相互交换公钥,然后进行认证。
return credentials.NewTLS(&tls.Config{
// tls.Config Certificates 放证书链列表,服务器端配置放服务的证书,客户端配置放客户端证书。客户端证书只有在服务器需要认证客户端证书时才需要放入
Certificates: []tls.Certificate{cert},
// ClientCAs 只有在服务端需要认证客户端证书时,才需要将客户端证书放入。
ClientCAs: cp,
// ClientAuth 认证方式,双向认证时,需要打开。
ClientAuth: tls.RequireAndVerifyClientCert}), nil
}

func main() {
fmt.Println("开始服务,监听本地端口::53586")
l, _ := net.Listen("tcp", ":53586")
defer l.Close()
cred, _ := newTLSFromFile(certFile)
svc := grpc.NewServer(grpc.Creds(cred))
echo := &Echo{}
common.RegisterGreeterServer(svc, echo)
svc.Serve(l)
}

客户端示例代码

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package main 

import (
"alpha.2se.com/sample/grpc/common"
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"io/ioutil"
)

var (
// 服务器证书
certFile = common.KeyPath + "/server.pem"
clientFile = common.KeyPath + "/client.pem"
clientKey = common.KeyPath + "/client.key"
)

func newTLSFromFile(certFile string) (credentials.TransportCredentials, error) {
b, err := ioutil.ReadFile(certFile)
if err != nil {
return nil, err
}
cp := x509.NewCertPool()

if !cp.AppendCertsFromPEM(b) {
return nil, fmt.Errorf("credentials: failed to append certificates")
}

cert, err := tls.LoadX509KeyPair(clientFile, clientKey)

if err != nil {
fmt.Printf("载入证书出错! %v\n", err)
return nil, err
}

return credentials.NewTLS(&tls.Config{
// tls.Config Certificates 在客户端配置时,提供给服务端认证的客户端证书列表。
Certificates:[]tls.Certificate{cert},
// RootCAs 服务端证书。
RootCAs: cp}), nil
}

func main() {
tc, _ := newTLSFromFile(certFile)
conn, err := grpc.Dial("localhost:53586", grpc.WithTransportCredentials(tc))
if err != nil {
fmt.Println("----")
fmt.Printf("%v\n", err)
return
}

fmt.Println("echo...")
defer conn.Close()

client := common.NewGreeterClient(conn)
ctx := context.Background()
in := &common.HelloRequest{Name:"alpha"}
if reply, err := client.SayHello(ctx, in); err != nil {
fmt.Printf("%v\n", err)
} else {
fmt.Printf("%s\n", reply.GetMessage())
}
}

model.proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
syntax = "proto3"; 

package common;

service Greeter {
rpc SayHello(HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}