《GO语言高级编程》设计中案例,仅作为笔记进行收藏。gRPC建⽴在HTTP/2协议之上,对TLS提供了很好的⽀持。客户端在链接服务器中通过 grpc.WithInsecure() 选项跳过了对服务器证书的验证,没有启⽤证书的gRPC服务在和客户端进⾏的是明⽂通讯,信息⾯临被任何第三⽅监听的⻛险。为了保障gRPC通信不被第三⽅监听篡改或伪造,可以对服务器启动TLS加密特性。

1.结构目录

2.为服务器和客户端分别生成私钥和证书,存放在 tls-config/ 目录下,命令如下:

// Makefile
default:
	# ca
	openssl genrsa -out ca.key 2048
	openssl req -new -x509 -days 3650 \
		-subj "/C=GB/L=China/O=gobook/CN=github.com" \
		-key ca.key -out ca.crt

	# server
	openssl genrsa \
		-out server.key 2048
	openssl req -new \
		-subj "/C=GB/L=China/O=server/CN=server.io" \
		-key server.key \
		-out server.csr
	openssl x509 -req -sha256 \
		-CA ca.crt -CAkey ca.key -CAcreateserial -days 3650 \
		-in server.csr \
		-out server.crt

	# client
	openssl genrsa \
		-out client.key 2048
	openssl req -new \
		-subj "/C=GB/L=China/O=client/CN=client.io" \
		-key client.key \
		-out client.csr
	openssl x509 -req -sha256 \
		-CA ca.crt -CAkey ca.key -CAcreateserial -days 3650 \
		-in client.csr \
		-out client.crt

clean:
	-rm *.key *.crt *.csr *.srl

3.Helloworld.proto

syntax = "proto3";

package main;

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

message HelloRequest{
  string name = 1;
}

message HelloReply{
  string message = 1;
}

4.Helloworld.pb.go

// Code generated by protoc-gen-go. DO NOT EDIT.
// source: helloworld.proto

package main

import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"

import (
	context "golang.org/x/net/context"
	grpc "google.golang.org/grpc"
)

// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf

// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package

type HelloRequest struct {
	Name                 string   `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
	XXX_NoUnkeyedLiteral struct{} `json:"-"`
	XXX_unrecognized     []byte   `json:"-"`
	XXX_sizecache        int32    `json:"-"`
}

func (m *HelloRequest) Reset()         { *m = HelloRequest{} }
func (m *HelloRequest) String() string { return proto.CompactTextString(m) }
func (*HelloRequest) ProtoMessage()    {}
func (*HelloRequest) Descriptor() ([]byte, []int) {
	return fileDescriptor_helloworld_04dfe859c9b956ba, []int{0}
}
func (m *HelloRequest) XXX_Unmarshal(b []byte) error {
	return xxx_messageInfo_HelloRequest.Unmarshal(m, b)
}
func (m *HelloRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
	return xxx_messageInfo_HelloRequest.Marshal(b, m, deterministic)
}
func (dst *HelloRequest) XXX_Merge(src proto.Message) {
	xxx_messageInfo_HelloRequest.Merge(dst, src)
}
func (m *HelloRequest) XXX_Size() int {
	return xxx_messageInfo_HelloRequest.Size(m)
}
func (m *HelloRequest) XXX_DiscardUnknown() {
	xxx_messageInfo_HelloRequest.DiscardUnknown(m)
}

var xxx_messageInfo_HelloRequest proto.InternalMessageInfo

func (m *HelloRequest) GetName() string {
	if m != nil {
		return m.Name
	}
	return ""
}

type HelloReply struct {
	Message              string   `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
	XXX_NoUnkeyedLiteral struct{} `json:"-"`
	XXX_unrecognized     []byte   `json:"-"`
	XXX_sizecache        int32    `json:"-"`
}

func (m *HelloReply) Reset()         { *m = HelloReply{} }
func (m *HelloReply) String() string { return proto.CompactTextString(m) }
func (*HelloReply) ProtoMessage()    {}
func (*HelloReply) Descriptor() ([]byte, []int) {
	return fileDescriptor_helloworld_04dfe859c9b956ba, []int{1}
}
func (m *HelloReply) XXX_Unmarshal(b []byte) error {
	return xxx_messageInfo_HelloReply.Unmarshal(m, b)
}
func (m *HelloReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
	return xxx_messageInfo_HelloReply.Marshal(b, m, deterministic)
}
func (dst *HelloReply) XXX_Merge(src proto.Message) {
	xxx_messageInfo_HelloReply.Merge(dst, src)
}
func (m *HelloReply) XXX_Size() int {
	return xxx_messageInfo_HelloReply.Size(m)
}
func (m *HelloReply) XXX_DiscardUnknown() {
	xxx_messageInfo_HelloReply.DiscardUnknown(m)
}

var xxx_messageInfo_HelloReply proto.InternalMessageInfo

func (m *HelloReply) GetMessage() string {
	if m != nil {
		return m.Message
	}
	return ""
}

func init() {
	proto.RegisterType((*HelloRequest)(nil), "main.HelloRequest")
	proto.RegisterType((*HelloReply)(nil), "main.HelloReply")
}

// Reference imports to suppress errors if they are not otherwise used.
var _ context.Context
var _ grpc.ClientConn

// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
const _ = grpc.SupportPackageIsVersion4

// GreeterClient is the client API for Greeter service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type GreeterClient interface {
	SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
}

type greeterClient struct {
	cc *grpc.ClientConn
}

func NewGreeterClient(cc *grpc.ClientConn) GreeterClient {
	return &greeterClient{cc}
}

func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
	out := new(HelloReply)
	err := c.cc.Invoke(ctx, "/main.Greeter/SayHello", in, out, opts...)
	if err != nil {
		return nil, err
	}
	return out, nil
}

// GreeterServer is the server API for Greeter service.
type GreeterServer interface {
	SayHello(context.Context, *HelloRequest) (*HelloReply, error)
}

func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) {
	s.RegisterService(&_Greeter_serviceDesc, srv)
}

func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
	in := new(HelloRequest)
	if err := dec(in); err != nil {
		return nil, err
	}
	if interceptor == nil {
		return srv.(GreeterServer).SayHello(ctx, in)
	}
	info := &grpc.UnaryServerInfo{
		Server:     srv,
		FullMethod: "/main.Greeter/SayHello",
	}
	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
		return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest))
	}
	return interceptor(ctx, in, info, handler)
}

var _Greeter_serviceDesc = grpc.ServiceDesc{
	ServiceName: "main.Greeter",
	HandlerType: (*GreeterServer)(nil),
	Methods: []grpc.MethodDesc{
		{
			MethodName: "SayHello",
			Handler:    _Greeter_SayHello_Handler,
		},
	},
	Streams:  []grpc.StreamDesc{},
	Metadata: "helloworld.proto",
}

func init() { proto.RegisterFile("helloworld.proto", fileDescriptor_helloworld_04dfe859c9b956ba) }

var fileDescriptor_helloworld_04dfe859c9b956ba = []byte{
	// 142 bytes of a gzipped FileDescriptorProto
	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0xc8, 0x48, 0xcd, 0xc9,
	0xc9, 0x2f, 0xcf, 0x2f, 0xca, 0x49, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0xc9, 0x4d,
	0xcc, 0xcc, 0x53, 0x52, 0xe2, 0xe2, 0xf1, 0x00, 0xc9, 0x04, 0xa5, 0x16, 0x96, 0xa6, 0x16, 0x97,
	0x08, 0x09, 0x71, 0xb1, 0xe4, 0x25, 0xe6, 0xa6, 0x4a, 0x30, 0x2a, 0x30, 0x6a, 0x70, 0x06, 0x81,
	0xd9, 0x4a, 0x6a, 0x5c, 0x5c, 0x50, 0x35, 0x05, 0x39, 0x95, 0x42, 0x12, 0x5c, 0xec, 0xb9, 0xa9,
	0xc5, 0xc5, 0x89, 0xe9, 0x30, 0x45, 0x30, 0xae, 0x91, 0x35, 0x17, 0xbb, 0x7b, 0x51, 0x6a, 0x6a,
	0x49, 0x6a, 0x91, 0x90, 0x01, 0x17, 0x47, 0x70, 0x62, 0x25, 0x58, 0x97, 0x90, 0x90, 0x1e, 0xc8,
	0x26, 0x3d, 0x64, 0x6b, 0xa4, 0x04, 0x50, 0xc4, 0x0a, 0x72, 0x2a, 0x93, 0xd8, 0xc0, 0xae, 0x32,
	0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0x18, 0x38, 0x13, 0xf1, 0xa9, 0x00, 0x00, 0x00,
}

5.main.go

package main

import (
	"log"
	"net"
	"time"

	"crypto/tls"
	"crypto/x509"
	"io/ioutil"

	"golang.org/x/net/context"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
)

var (
	port = ":5000"

	tlsDir        = "./tls-config"
	tlsServerName = "server.io"

	ca         = tlsDir + "/ca.crt"
	server_crt = tlsDir + "/server.crt"
	server_key = tlsDir + "/server.key"
	client_crt = tlsDir + "/client.crt"
	client_key = tlsDir + "/client.key"
)

type myGrpcServer struct{}

func (s *myGrpcServer) SayHello(ctx context.Context, in *HelloRequest) (*HelloReply, error) {
	return &HelloReply{Message: "Hello " + in.Name}, nil
}

func main() {
	go startServer()
	time.Sleep(time.Second)

	doClientWork()
}

func startServer() {
	certificate, err := tls.LoadX509KeyPair(server_crt, server_key)
	if err != nil {
		log.Panicf("could not load server key pair: %s", err)
	}

	certPool := x509.NewCertPool()
	ca, err := ioutil.ReadFile(ca)
	if err != nil {
		log.Panicf("could not read ca certificate: %s", err)
	}

	// Append the client certificates from the CA
	if ok := certPool.AppendCertsFromPEM(ca); !ok {
		log.Panic("failed to append client certs")
	}

	// Create the TLS credentials
	creds := credentials.NewTLS(&tls.Config{
		ClientAuth:   tls.RequireAndVerifyClientCert, // NOTE: this is optional!
		Certificates: []tls.Certificate{certificate},
		ClientCAs:    certPool,
	})

	server := grpc.NewServer(grpc.Creds(creds))
	RegisterGreeterServer(server, new(myGrpcServer))

	lis, err := net.Listen("tcp", port)
	if err != nil {
		log.Panicf("could not list on %s: %s", port, err)
	}

	if err := server.Serve(lis); err != nil {
		log.Panicf("grpc serve error: %s", err)
	}
}

func doClientWork() {
	certificate, err := tls.LoadX509KeyPair(client_crt, client_key)
	if err != nil {
		log.Panicf("could not load client key pair: %s", err)
	}

	certPool := x509.NewCertPool()
	ca, err := ioutil.ReadFile(ca)
	if err != nil {
		log.Panicf("could not read ca certificate: %s", err)
	}
	if ok := certPool.AppendCertsFromPEM(ca); !ok {
		log.Panic("failed to append ca certs")
	}

	creds := credentials.NewTLS(&tls.Config{
		InsecureSkipVerify: false,         // NOTE: this is required!
		ServerName:         tlsServerName, // NOTE: this is required!
		Certificates:       []tls.Certificate{certificate},
		RootCAs:            certPool,
	})

	conn, err := grpc.Dial("localhost"+port, grpc.WithTransportCredentials(creds))
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	c := NewGreeterClient(conn)

	r, err := c.SayHello(context.Background(), &HelloRequest{Name: "gopher"})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("doClientWork: %s", r.Message)
}

6.OpenSSL

6.1 genrsa

功能:
用于生成RSA私钥,不会生成公钥,因为公钥提取自私钥
参数:
openssl genrsa [-help] [-out filename] [-passout arg] [-aes128] [-aes192] [-aes256] [-aria128] [-aria192] [-aria256] [-camellia128] [-camellia192] [-camellia256] [-des] [-des3] [-idea] [-f4] [-3] [-rand file(s)] [-engine id] [numbits]
说明:
-help:打印出使用情况消息。
-out filename:将密钥输出到指定文件。如果未指定此参数,则使用标准输出。
-passout arg:加密私钥文件时,传递密码的格式,如果要加密私钥文件时单未指定该项,则提示输入密码。传递密码的args的格式如下:
    pass: password表示传递的明文密码,仅应在安全性不重要的地方使用。
    env:从环境变量var获取密码。由于其他进程的环境在某些平台上可见,因此应谨慎使用此选项。
    file:filename的第一行是密码。如果为-passin和-passout参数提供了相同的路径名参数,则第一行将用于输入密码,下一行将用于输出密码。filename不必引用常规文件:例如可以引用设备或命名管道。
    stdin:从标准输入中读取密码。
-aes128|-aes192|-aes256|-aria128|-aria192|-aria256|-camellia128|-camellia192|-camellia256|-des|-des3|-idea:在输出私钥之前使用指定的密码对其进行加密。如果未指定,则不使用加密;如果使用加密,如果密码没有通过-passout参数提供,则将提示您输入密码。
-F4 | -3:要使用的公共指数,即65537或3。默认值为65537。
-rand file(s):一个或多个文件,这些文件包含用于为随机数生成器提供种子的随机数据,或者是EGD套接字。可以指定多个文件,并由与操作系统相关的字符分隔。 
-engine id:指定引擎(通过其唯一的ID字符串)将使genrsa尝试获取对指定引擎的功能引用,从而在需要时对其进行初始化。然后,引擎将被设置为所有可用算法的默认引擎。
-numbits:生成的私钥的大小(以位为单位)。这必须是指定的最后一个选项。默认值为2048。

例如:

openssl genrsa -des -passout pass:"1234546" -out prikey.pem 2048

6.2 req

功能:
创建并在PKCS#10格式的过程的证书请求。例如:可以创建自签名证书以用作根CA。
参数:
openssl req [-help] [-inform PEM|DER] [-outform PEM|DER] [-in filename] [-passin arg] [-out filename] [-passout arg] [-text] [-pubkey] [-noout] [-verify] [-modulus] [-new] [-rand file(s)] [-newkey rsa:bits] [-newkey alg:file] [-nodes] [-key filename] [-keyform PEM|DER] [-keyout filename] [-keygen_engine id] [-[digest]] [-config filename] [-multivalue-rdn] [-x509] [-days n] [-set_serial n] [-newhdr] [-extensions section] [-reqexts section] [-utf8] [-nameopt] [-reqopt] [-subject] [-subj arg] [-batch] [-verbose] [-engine id]
说明:
-out filename:证书请求或自签署证书的输出文件,也可以是其他内容的输出文件,默认指定要写入的输出文件名或标准输出(stdout)。
-in filename:如果未指定此选项,则它指定从中读取请求的输入文件名或标准输入。如果未指定创建选项(-new和-newkey),则仅读取请求。
-passin arg:输入文件密码源,传递解密密码。
-passout arg:输出文件密码源,指定加密输出文件时的密码。
-text:以文本形式打印出证书申请。
-verify:验证请求上的签名。
-new:此选项生成一个新的证书请求。它将提示用户输入相关的字段值。提示的实际字段及其最大和最小大小在配置文件和任何请求的扩展名中指定。如果未使用-key选项,它将使用配置文件中指定的信息生成新的RSA私钥。
-key filename:这指定从中读取私钥的文件。它还接受PEM格式文件的PKCS#8格式私钥。
-subj arg:替换或自定义证书请求时需要输入的信息,并输出修改后的请求信息。arg 参数:C是Country、ST是state、L是localcity、O是Organization、OU是Organization Unit、CN是common name