TCP(GRPC)/HTTPS中TLS单向和双向认证

目前,浏览器中开启HTTPS是比较简单的事情。首先我们需要一个域名,然后找一家可信CA机构申请证书并将证书安装到服务器(例如:RapidSSL、Trustwave SSL、Let’s Encrypt等)。但对TCP协议的服务使用self-signed证书我们应该如何完成?

证书的分类

验证方式

  • DV SSL证书(域名验证)
  • OV SSL证书(企业验证)
  • EV SSL证书(企业增强/扩展验证)

功能分类

  • UCC/SAN SSL证书(多域名)
  • SGC SSL证书(强加密)
  • Wildcard SSL证书(通配符)
  • Code Signing SSL证书(代码签名)

认证流程

  • one-way authentication(单向认证)
  • two-way(mutual) authentication(双向认证)

浏览器中大部分应用使用单向认证,即服务器认证客户端,客户端无需认证服务器。大型企业有很多域名一般使用OV SSL和SAN SSL证书,而个人网站则使用DV SSL证书。

证书使用

什么时候使用可信CA签发证书,什么时候使用自签证书? 什么时候使用单向证书或双向证书。

这些都应根据应用的实际业务而定,浏览器应用中使用可信CA签发的证书,这样浏览器访问你的网站时就不会提示not secure错误,这是因浏览器中预置可信CA机构的CA证书。这也不是绝对的,例如12306网站就使用的自签证书,你会看到not secure错误,导入12306网站的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
#!/bin/sh

mkdir -p {certs,crl,newcerts}
touch index.txt
echo 1000 > serial


# CA private key (unencrypted)
openssl genrsa -out ca.key 4096
# Certificate Authority (self-signed certificate)
openssl req -config openssl.cnf -new -x509 -days 3650 -sha256 -key ca.key -extensions v3_ca -out ca.crt -subj "/CN=fake-ca"

# Server private key (unencrypted)
openssl genrsa -out server.key 2048
# Server certificate signing request (CSR)
openssl req -config openssl.cnf -new -sha256 -key server.key -out server.csr -subj "/CN=fake.grpc"
# Certificate Authority signs CSR to grant a certificate
openssl ca -batch -config openssl.cnf -extensions server_cert -days 365 -notext -md sha256 -in server.csr -out server.crt -cert ca.crt -keyfile ca.key

# Client private key (unencrypted)
openssl genrsa -out client.key 2048
# Signed client certificate signing request (CSR)
openssl req -config openssl.cnf -new -sha256 -key client.key -out client.csr -subj "/CN=fake.client"
# Certificate Authority signs CSR to grant a certificate
openssl ca -batch -config openssl.cnf -extensions usr_cert -days 365 -notext -md sha256 -in client.csr -out client.crt -cert ca.crt -keyfile ca.key

#openssl x509 -text -noout -in ca.crt

rm *.csr

自签证书配置

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
70
71
72
73
74
75
76
77
78
SAN =

[ ca ]
# `man ca`
default_ca = CA_default

[ CA_default ]
# Directory and file locations.
dir = .
certs = $dir/certs
crl_dir = $dir/crl
new_certs_dir = $dir/newcerts
database = $dir/index.txt
serial = $dir/serial
# certificate revocation lists.
crlnumber = $dir/crlnumber
crl = $dir/crl/intermediate-ca.crl
crl_extensions = crl_ext
default_crl_days = 30
default_md = sha256

name_opt = ca_default
cert_opt = ca_default
default_days = 375
preserve = no
policy = policy_loose

[ policy_loose ]
# Allow the CA to sign a range of certificates.
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional

[ req ]
# `man req`
default_bits = 4096
distinguished_name = req_distinguished_name
string_mask = utf8only
default_md = sha256

[ req_distinguished_name ]
countryName = Country Name (2 letter code)
stateOrProvinceName = State or Province Name
localityName = Locality Name
0.organizationName = Organization Name
organizationalUnitName = Organizational Unit Name
commonName = Common Name

# Certificate extensions (`man x509v3_config`)

[ v3_ca ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign

[ usr_cert ]
basicConstraints = CA:FALSE
nsCertType = client
nsComment = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth

[ server_cert ]
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = $ENV::SAN

示例代码

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
var (
argAddress string
argCrtFile string
argKeyFile string
argCAFile string
)

var verbose bool
var rootCmd *cobra.Command

func init() {
rootCmd = &cobra.Command{
Use: "grpc",
Short: "demo service",
Long: "Top level command for demo service, it provides GRPC service",
Run: run,
}

rootCmd.Flags().StringVarP(&argAddress, "address", "a", ":3264", "address to listen on")
rootCmd.Flags().StringVar(&argCrtFile, "cert-file", "", "certificate file for gRPC TLS authentication")
rootCmd.Flags().StringVar(&argKeyFile, "key-file", "", "key file for gRPC TLS authentication")
rootCmd.Flags().StringVar(&argCAFile, "ca-file", "", "ca file for gRPC client")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
}

type Service struct {
}

func (s *Service) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, grpc.Errorf(codes.Unauthenticated, "no metadata")
}

token := md.Get("token")
if len(token) == 0 {
return nil, grpc.Errorf(codes.Unauthenticated, "no token")
}

fmt.Println("requst:", token[0], req.Name)
return &pb.HelloReply{
Message: "hello, " + req.Name,
}, nil
}

func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

func run(cmd *cobra.Command, _ []string) {
listener, err := net.Listen("tcp", argAddress)
if err != nil {
panic(err)
}

// Create the TLS credentials
var opts []grpc.ServerOption
if argCrtFile != "" && argKeyFile != "" {
fmt.Println("enable credentials in the grpc")

if argCAFile == "" {
creds, err := credentials.NewServerTLSFromFile(argCrtFile, argKeyFile)
if err != nil {
panic(err)
}

opts = append(opts, grpc.Creds(creds))
} else {
// Parse certificates from certificate file and key file for server.
cert, err := tls.LoadX509KeyPair(argCrtFile, argKeyFile)
if err != nil {
panic(err)
//return fmt.Errorf("invalid config: error parsing gRPC certificate file: %v", err)
}

// Parse certificates from client CA file to a new CertPool.
cPool := x509.NewCertPool()
clientCert, err := ioutil.ReadFile(argCAFile)
if err != nil {
panic(err)
//return fmt.Errorf("invalid config: reading from client CA file: %v", err)
}
if cPool.AppendCertsFromPEM(clientCert) != true {
panic(err)
//return errors.New("invalid config: failed to parse client CA")
}

tlsConfig := tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: cPool,
}
opts = append(opts,
grpc.Creds(credentials.NewTLS(&tlsConfig)),
)
}
}

server := grpc.NewServer(opts...)
pb.RegisterGreeterServer(server, &Service{})

logrus.WithField("addr", argAddress).Println("Starting server")
server.Serve(listener)
}

客户端代码

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
var (
argGRPCAddr string
argCrtFile string
argKeyFile string
argCAFile string
argCNOverride string
)

var verbose bool
var rootCmd *cobra.Command

func init() {
rootCmd = &cobra.Command{
Use: "grpc-client",
Short: "demo client",
Long: "Top level command for demo client",
Run: run,
}

rootCmd.Flags().StringVarP(&argGRPCAddr, "grpc-addr", "a", "127.0.0.1:3264", "grpc address")
rootCmd.Flags().StringVar(&argCrtFile, "cert-file", "", "certificate file for gRPC TLS authentication")
rootCmd.Flags().StringVar(&argKeyFile, "key-file", "", "key file for gRPC TLS authentication")
rootCmd.Flags().StringVar(&argCAFile, "ca-file", "", "ca file for gRPC client")
rootCmd.Flags().StringVar(&argCNOverride, "cn-override", "", "domain name override")
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
}

// customCredential 自定义认证
type customCredential struct {
token string
security bool
}

func (c customCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
"token": c.token,
}, nil
}

func (c customCredential) RequireTransportSecurity() bool {
return c.security
}

func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}

func run(cmd *cobra.Command, _ []string) {
var opts []grpc.DialOption
customCred := &customCredential{token: "custom-token"}
if argCAFile != "" {
fmt.Println("enable credentials in the grpc")
if argCrtFile != "" && argKeyFile != "" {
cPool := x509.NewCertPool()
caCert, err := ioutil.ReadFile(argCAFile)
if err != nil {
panic(err)
//return nil, fmt.Errorf("invalid CA crt file: %s", caPath)
}
if cPool.AppendCertsFromPEM(caCert) != true {
panic(err)
//return nil, fmt.Errorf("failed to parse CA crt")
}

clientCert, err := tls.LoadX509KeyPair(argCrtFile, argKeyFile)
if err != nil {
panic(err)
//return nil, fmt.Errorf("invalid client crt file: %s", caPath)
}

clientTLSConfig := &tls.Config{
RootCAs: cPool,
Certificates: []tls.Certificate{clientCert},
}
creds := credentials.NewTLS(clientTLSConfig)

opts = append(opts, grpc.WithTransportCredentials(creds))
} else {

// target is common name(host name) in the cert file
creds, err := credentials.NewClientTLSFromFile(argCAFile, argCNOverride)
if err != nil {
panic(err)
}

opts = append(opts, grpc.WithTransportCredentials(creds))
}

customCred.security = true
} else {
opts = append(opts, grpc.WithInsecure())
}

// custom credentials
opts = append(opts, grpc.WithPerRPCCredentials(customCred))

conn, err := grpc.Dial(argGRPCAddr, opts...)
if err != nil {
panic(err)
}
greeterClient := pb.NewGreeterClient(conn)

reply, err := greeterClient.SayHello(context.Background(),
&pb.HelloRequest{Name: "luoji"})
if err != nil {
logrus.WithError(err).Fatal("unable to sayhello")
}

logrus.Info("reply:", reply.Message)
conn.Close()
}