服务器双向认证,原理上就是相互交换公钥,双方通过公钥认证对方。不同的是服务器双向认证,有一方作为主导方,另一方作为连接方。所以网上讲的自签发证书需要生成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
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}
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}"
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值的,可能会报下面的错误:
想要直接通过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" )
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") } cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { fmt.Printf("载入证书出错! %v\n", err) return nil, err } return credentials.NewTLS(&tls.Config{ Certificates: []tls.Certificate{cert}, ClientCAs: cp, 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{ Certificates:[]tls.Certificate{cert}, 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; }
|