基本概念:

证书

证书是一个数据结构,其中包含一个 public key 和一个 name; 权威机构对证书进行签名,签名的大概意思是:public key xxx 关联到了 name xx;

对证书进行签名的 entity 称为 issuer(或 certificate authority, CA), 证书中的 entity 称为 subject。 step-certificate-inspect

x509: X.509 标准主要内容:证书的作用、证书文件的结构、证书管理方式、证书校验方式、证书的撤销等。

X.509 构建在 ASN.1 (Abstract Syntax Notation,抽象语法标注)之上,后者是另一个 ITU-T 标准 (X.208 and X.680)。

ASN.1 定义数据类型,

可以将 ASN.1 理解成 X.509 的 JSON,
但实际上更像 protobuf、thrift 或 SQL DDL。
RFC 5280 用 ASN.1 来定义 X.509 证书,其中包括名字、秘钥、签名等信息。

ASN.1 有很多种编码规则, 但用于 X.509 和其他加密相关的,只有一种常见格式:DER —— 虽然有时也会用到 non-canonical 的 basic encoding rules (BER,基础编码规则) 。

PEM (privacy enhanced email):文本格式
DER 是二进制格式,不便复制粘贴。因此大部分证书都是以 PEM 格式打包的

1
2
3
4
5
6
7
8
-----BEGIN CERTIFICATE-----
MIIBwzCCAWqgAwIBAgIRAIi5QRl9kz1wb+SUP20gB1kwCgYIKoZIzj0EAwIwGzEZ
MBcGA1UEAxMQTDVkIFRlc3QgUm9vdCBDQTAeFw0xODExMDYyMjA0MDNaFw0yODEx
BgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBRc+LHppFk8sflIpm/XKpbNMwx3
SDAfBgNVHSMEGDAWgBTirEpzC7/gexnnz7ozjWKd71lz5DAKBggqhkjOPQQDAgNH
ADBEAiAejDEfua7dud78lxWe9eYxYcM93mlUMFIzbWlOJzg+rgIgcdtU9wIKmn5q
FU3iOiRP5VyLNmrsQD3/ItjUN1f1ouY=
-----END CERTIFICATE-----

PEM 编码的证书通常以 .pem.crt.cer 为后缀.

PKCS #7:Java 中常用
你可能会遇到的是一个称为 PKCS(Public Key Cryptography Standards,公钥加密标准)的标准的一部分, 它由 RSA labs 发布。

其中的第一个标准是 PKCS#7,后面被 IETF 重新冠名为 Cryptographic Message Syntax (CMS) ,其中可以包含多个证书(以 full certificate chain 方式编码)。

PKCS#7 在 Java 中使用广泛。常见扩展名是 .p7b and .p7c。

PKCS #12:微软常用
另一个常见的打包格式 PKCS#12, 它能将一个证书链(这一点与 PKCS#7 类似)连同一个(加密之后的)私钥打包到一起。

微软的产品多用这种格式,常见后缀.pfx and .p12。

PKCS#7 和 PKCS#12 envelopes 仍然使用 ASN.1,这意味着 它们都能以原始 DER、BER 或 PEM 的格式编码

公钥、私钥常见扩展名
公钥:.pub or .pem
私钥:.prv, .key, or .pem

DN (distinguished names)
历史上,X.509 使用 X.500 distinguished names (DN) 来命名证书的使用者(name the subject of a certificate),即 subscriber。

SAN (subject alternative name)
常用的 SAN 有四种类型,绑定的都是广泛使用的名字:

  • domain names (DNS)
  • email addresse
  • IP addresse
  • URI

inspect-san-dns

Certificate signing requests(证书签名请求,PKCS#10)

Subscriber 请求一个证书时,会向 CA 会提交一个 certificate signing request (CSR)。

CSR 也是一个 ASN.1 结构,定义在 PKCS#10。 与证书类似,CSR 数据结构包括一个公钥、一个名字和一个签名。 CSR 是自签名的,用与 CRS 中公钥对应的私钥自签名。

这个签名用于证明该 subscriber 有对应的私钥,能对任何用其公钥加密的东西进行解密。 还使即使 CSR 被复制或转发,都没有能篡改其中的内容(篡改无效)。 CSR 中包括了很多证书细节配置项。但在实际中,大部分配置项都会被 CA 忽略。大部分 CA 都使用自己的固定模板, 或提供一个 administrative 接口来收集这些信息。

TLS身份认证原理

以单向的客户端认证服务端身份为例

首先我们需要生成一个用于签发数字证书的CA私钥和CA数字证书

1
2
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -days 5000 -out ca.crt -subj "/CN=fridayhub.cn"

然后需要生成服务端私钥和数字证书,并用CA私钥给服务端数字证书做签名。

1
2
3
4
5
openssl genrsa -out server.key 2048
# 生成服务端证书的 CSR
openssl req -new -key server.key -subj "/CN=localhost" -out server.csr
# 通过 CSR 向 CA 签发服务端证书
openssl x509 -req -in server.csr -CA ../root/ca.crt -CAkey ../root/ca.key -CAcreateserial -out server.crt -days 5000

单向认证流程如下:

  1. 客户端发送连接请求
  2. 服务将自己的数字证书(证书中包含服务端公钥和签名等信息)发给客户端
  3. 客户端利用本地的CA数字证书验证服务端下发的数字证书,以验证服务端身份
  4. 客户端随机生成一个session秘钥,并用服务端下发的数字证书中的公钥对这个秘钥加密,并发给服务端
  5. 服务端用自身私钥解密,然后服务端和客户端以后就用这个session秘钥进行加密通信 以上流程中忽略了双方协商加密算法之类的步骤。如果是双向认证,则还有服务端要求客户端发送证书的过程[1]。

实现TLS双向认证
要实现双向认证,还需要生成客户端的私钥和数字证书,并用CA私钥给服务端数字证书做签名。

1
2
3
4
5
6
# 生成客户端秘钥
openssl genrsa -out client.key 2048
生成客户端 CSR
openssl req -new -key client.key -subj "/CN=localhost" -out client.csr
# 通过 CSR 向 CA 签发客户端证书
openssl x509 -req -in client.csr -CA ../root/ca.crt -CAkey ../root/ca.key -CAcreateserial -out client.crt -days 5000

证书与秘钥管理

要以TLS方式接入我们系统,设备端需要三个文件,CA数字证书,设备私钥,设备数字证书。
CA数字证书必须由我们颁发,没有保密要求。
设备私钥由厂商自己生成,设备私钥不能泄露。
设备数字证书须由我们颁发,需要厂商根据设备私钥先生成一个数字证书,再将该证书传给我们,由我们的CA私钥和CA证书对其签发生成真正的设备数字证书,在保证设备私钥不泄露的情况下,设备数字证书没有保密要求。

后端需要四个文件: CA私钥,CA数字证书,服务端私钥,服务端数字证书

CA私钥用来和CA数字证书一起签发服务端数字证书和设备端数字证书,必须保密。
CA数字证书还被服务端用来验证设备数字证书的真假,没有保密要求。
服务端私钥必须保密
服务端数字证书没有保密要求

双向认证流程:

  1. 浏览器发送一个连接请求给安全服务器。
  2. 服务器将自己的证书,以及同证书相关的信息发送给客户浏览器。
  3. 客户浏览器检查服务器送过来的证书是否是由自己信赖的CA中心所签发的。如果是,就继续执行协议;如果不是,客户浏览器就给客户一个警告消息:警告客户这个证书不是可以信赖的,询问客户是否需要继续。
  4. 接着客户浏览器比较证书里的消息,例如域名和公钥,与服务器刚刚发送的相关消息是否一致,如果是一致的,客户浏览器认可这个服务器的合法身份。
  5. 服务器要求客户发送客户自己的证书。收到后,服务器验证客户的证书,如果没有通过验证,拒绝连接;如果通过验证,服务器获得用户的公钥。
  6. 客户浏览器告诉服务器自己所能够支持的通讯对称密码方案。
  7. 服务器从客户发送过来的密码方案中,选择一种加密程度最高的密码方案,用客户的公钥加过密后通知浏览器。
  8. 浏览器针对这个密码方案,选择一个通话密钥,接着用服务器的公钥加过密后发送给服务器。
  9. 服务器接收到浏览器送过来的消息,用自己的私钥解密,获得通话密钥。
  10. 服务器、浏览器接下来的通讯都是用对称密码方案,对称密钥是加过密的。

服务端代码:

 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
 // 加载服务端证书
 srvCert, err := tls.LoadX509KeyPair("server.crt", "server.key")
 if err != nil {
     log.Panic("try to load key & crt got error", err)
 }

 // 加载 CA,并且添加进 caCertPool,这个 caCertPool 就是用来校验客户端证书的根证书列表
 log.Info("load ca ca.crt")
 caCrt, err := ioutil.ReadFile("ca.crt")
 if err != nil {
     log.Panic("try to load ca got error:",err)
 }
 caCertPool := x509.NewCertPool()
 caCertPool.AppendCertsFromPEM(caCrt)

 // https tls config
 tlsConfig := &tls.Config{
     Certificates:       []tls.Certificate{srvCert},
     ClientCAs:          caCertPool,  // 专用于校验客户端证书的 CA 池, 如果是ca机构,此项可以不填,通过加载操作系统的ca 证书。
     InsecureSkipVerify: false,       // 不接受不安全的连接
     ClientAuth:         tls.RequireAndVerifyClientCert,  // 校验客户端证书
 }
 tlsConfig.BuildNameToCertificate()

// 监听连接
ln, err := tls.Listen("tcp", ":443", tlsConfig)
if err != nil {
   log.Errorf("try to listen tcp got error:%v", err)
   time.Sleep(3 * time.Second)
   continue LISTEN_LOOP
}
log.Info("listening on 443...")

客户端代码:

 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
// 加载客户端证书
 cliCert, err := tls.LoadX509KeyPair("client.crt", "client.key")
 if err != nil {
     log.Panic("try to load key & crt got error", err)
 }

 // 加载 CA 根证书
 log.Info("load ca ca.crt")
 caCrt, err := ioutil.ReadFile("ca.crt") // 这里server和client使用同一个ca
 if err != nil {
     log.Panic("try to load ca got error", err)
 }
 caCertPool := x509.NewCertPool()
 caCertPool.AppendCertsFromPEM(caCrt)

 // https tls config
 tlsConfig := &tls.Config{
     Certificates:       []tls.Certificate{cliCert},
     RootCAs:            caCertPool,  // 校验服务端证书的 CA 池
     InsecureSkipVerify: false,
 }
 tlsConfig.BuildNameToCertificate()

 // 发起连接
 conn, err := tls.Dial("tcp", "127.0.0.1:443", tlsConfig)
 if err != nil {
     log.Panic("try to dial tcp got error", err)
 }
 defer conn.Close()
 log.Info("connected to remote:%s",  conn.RemoteAddr().String())

使用openssl查看和验证证书:
查看证书详细信息: openssl x509 -noout -text -in server.crt

校验证书:

使用本地ca证书校验由自己颁发的服务器证书server.crt和客户端证书client.crt

1
2
openssl verify -CAfile ca.crt server.crt
openssl verify -CAfile ca.crt client.crt

kubeedge证书交换过程分析

流程梳理

  1. 云端生成ca 公私钥,保存在 kubeedge namespace的 casecret secret中
  2. 使用生成云端公私钥,并用ca生成服务端证书,都保存在kubeedge namespace的 cloudcoresecret secret中
  3. 在云端获取tokensecret configmap中的token,这个token默认每12小时更新一次
  4. edgecore 通过 serverip:10002/edge.crt 获取ca证书,需要带上token
  5. edgecore 生成 私钥 server.key
  6. edgecore 生成csr,请求cloucore serverip:10002/edge.crt 获取edgecore证书
  7. cloudcore 接收到crs请求,使用同一套ca签发证书
  8. 云端保存证书为 server.crt

cloudcore:

证书功能在clodhub中 kubeedge/cloud/pkg/cloudhub/cloudhub.go http server 配置: CloudHubHTTPS, 默认端口10002

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type CloudHubHTTPS struct {
	// Enable indicates whether enable Https protocol
	// default true
	Enable bool `json:"enable"`
	// Address indicates server ip address
	// default 0.0.0.0
	Address string `json:"address,omitempty"`
	// Port indicates the open port for HTTPS server
	// default 10002
	Port uint32 `json:"port,omitempty"`
}

http handler

1
2
	ws.Route(ws.GET(constants.DefaultCertURL).To(edgeCoreClientCert)) // /edge.crt 用于颁发边缘的证书
	ws.Route(ws.GET(constants.DefaultCAURL).To(getCA)) // /ca.crt 请求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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// kubeedge/cloud/pkg/cloudhub/servers/httpserver/signcerts.go
// 边缘证书颁发流程
func edgeCoreClientCert(request *restful.Request, response *restful.Response) {
    signEdgeCert(response, request.Request)
}

func signEdgeCert(w http.ResponseWriter, r *http.Request) {
    // 获取csr
    r.Body = http.MaxBytesReader(w, r.Body, constants.MaxRespBodyLength)
	csrContent, err := io.ReadAll(r.Body)
    csr, err := x509.ParseCertificateRequest(csrContent)

    // 签发
    clientCertDER, err := signCerts(csr.Subject, csr.PublicKey, usages)
}


func signCerts(subInfo pkix.Name, pbKey crypto.PublicKey, usages []x509.ExtKeyUsage) ([]byte, error) {
    // 准备cert config
	cfgs := &certutil.Config{
		CommonName:   subInfo.CommonName,
		Organization: subInfo.Organization,
		Usages:       usages,
	}

    // 读取ca证书
    caCert, err := x509.ParseCertificate(ca)

    // 读取ca私钥
    caKey, err := x509.ParseECPrivateKey(caKeyDER)

    // 签发
    certDER, err := NewCertFromCa(cfgs, caCert, clientKey, caKey, edgeCertSigningDuration)
}

// 生成证书
func NewCertFromCa(cfg *certutil.Config, caCert *x509.Certificate, serverKey crypto.PublicKey, caKey crypto.Signer, validalityPeriod time.Duration) ([]byte, error) {
    // 拼装模板
    certTmpl := x509.Certificate{
		Subject: pkix.Name{
			CommonName:   cfg.CommonName,
			Organization: cfg.Organization,
		},
		DNSNames:     cfg.AltNames.DNSNames,
		IPAddresses:  cfg.AltNames.IPs,
		SerialNumber: serial,
		NotBefore:    time.Now().UTC(),
		NotAfter:     time.Now().Add(time.Hour * 24 * validalityPeriod),
		KeyUsage:     x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
		ExtKeyUsage:  cfg.Usages,
	}

    // 创建证书
    certDERBytes, err := x509.CreateCertificate(rand.Reader, &certTmpl, caCert, serverKey, caKey)
}

edgecore:

edgecore主要在edgehub中 kubeedge/edge/pkg/edgehub/certificate/certmanager.go

 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
// 如果本地没有,则请求ca
func (cm *CertManager) applyCerts() error {
    cacert, err := GetCACert(cm.caURL) // http 请求不要验证

    // 使用token校验ca
    ok, hash, newHash := ValidateCACerts(cacert, tokenParts[0]) // sha256校验hash值

    // 保存ca为文件 ca.crt
    ca, err := x509.ParseCertificate(cacert)
    certutil.WriteCert(cm.caFile, ca) 

    // 获取证书 server.crt
    pk, edgeCert, err := cm.GetEdgeCert(cm.certURL, caPem, tls.Certificate{}, strings.Join(tokenParts[1:], "."))

    // 保存 edge.crt server.crt server.key
    crt, _ := x509.ParseCertificate(edgeCert)
    certutil.WriteKeyAndCert(cm.keyFile, cm.certFile, pk, crt)
}

// 生成私钥,并生成crs请求cloudcore签发证书
func (cm *CertManager) GetEdgeCert(url string, capem []byte, cert tls.Certificate, token string) (*ecdsa.PrivateKey, []byte, error) {
    // 生成私钥和crs
    pk, csr, err := cm.getCSR() 

    // 请求并返回证书 
    req, err := http.BuildRequest(nethttp.MethodGet, url, bytes.NewReader(csr), token, cm.NodeName)
    res, err := http.SendRequest(req, client)
    content, err := io.ReadAll(io.LimitReader(res.Body, constants.MaxRespBodyLength))
    return pk, content, nil    
}

服务连接过程:

cloudcore

 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
//kubeedge/cloud/pkg/cloudhub/servers/server.go
// 服务启动
func StartCloudHub(messageq *channelq.ChannelMessageQueue)  {
    go startWebsocketServer()
    // go startQuicServer() 根据配置启动websocket或者 quic
}

// 启动websocket服务
func startWebsocketServer() {
    // 读取证书相关
    tlsConfig := createTLSConfig(hubconfig.Config.Ca, hubconfig.Config.Cert, hubconfig.Config.Key)

    svc := server.Server{
		Type:       api.ProtocolTypeWS,
		TLSConfig:  &tlsConfig,
		AutoRoute:  true,
		ConnNotify: handler.CloudhubHandler.OnRegister, // 连接会掉函数
		Addr:       fmt.Sprintf("%s:%d", hubconfig.Config.WebSocket.Address, hubconfig.Config.WebSocket.Port), // 默认10000端口
		ExOpts:     api.WSServerOption{Path: "/"},
	}

    klog.Exit(svc.ListenAndServeTLS("", "")) // 监听端口开始服务
}

// 准备ca证书,服务端证书
func createTLSConfig(ca, cert, key []byte) tls.Config {
	// init certificate
	pool := x509.NewCertPool()
    // 指定ca
	ok := pool.AppendCertsFromPEM(pem.EncodeToMemory(&pem.Block{Type: certutil.CertificateBlockType, Bytes: ca}))
	if !ok {
		panic(fmt.Errorf("fail to load ca content"))
	}

    // 服务端证书和私钥
	certificate, err := tls.X509KeyPair(pem.EncodeToMemory(&pem.Block{Type: certutil.CertificateBlockType, Bytes: cert}), pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: key}))
	if err != nil {
		panic(err)
	}
	return tls.Config{
		ClientCAs:    pool,
		ClientAuth:   tls.RequireAndVerifyClientCert,
		Certificates: []tls.Certificate{certificate},
		MinVersion:   tls.VersionTLS12, // tls版本
		// has to match cipher used by NewPrivateKey method, currently is ECDSA
		CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}, // 加密算法
	}
}

edgecore:

 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
// /kubeedge/edge/pkg/edgehub/clients/factory.go
func GetClient() (Adapter, error) {
websocketConf := wsclient.WebSocketConfig{
			URL:              config.WebSocketURL,
			CertFilePath:     config.TLSCertFile,
			KeyFilePath:      config.TLSPrivateKeyFile,
			HandshakeTimeout: time.Duration(config.WebSocket.HandshakeTimeout) * time.Second,
			ReadDeadline:     time.Duration(config.WebSocket.ReadDeadline) * time.Second,
			WriteDeadline:    time.Duration(config.WebSocket.WriteDeadline) * time.Second,
			ProjectID:        config.ProjectID,
			NodeID:           config.NodeName,
		}
		return wsclient.NewWebSocketClient(&websocketConf), nil
}

// /kubeedge/edge/pkg/edgehub/clients/wsclient/websocket.go
// websocket 初始化  双向tls 证书相关的都在这
func (wsc *WebSocketClient) Init() error {
    // 证书和私钥
    cert, err := tls.LoadX509KeyPair(wsc.config.CertFilePath, wsc.config.KeyFilePath)

    // ca 证书
    caCert, err := os.ReadFile(config.Config.TLSCAFile)

    // 将ca加入到rootCAs pool,之后验证证书就能找到私钥了
    pool := x509.NewCertPool()
	if ok := pool.AppendCertsFromPEM(caCert); !ok {
		return fmt.Errorf("cannot parse the certificates")
	}

	tlsConfig := &tls.Config{
		RootCAs:            pool,
		Certificates:       []tls.Certificate{cert},
		InsecureSkipVerify: false,
	}

    option := wsclient.Options{
		HandshakeTimeout: wsc.config.HandshakeTimeout,
		TLSConfig:        tlsConfig,
		Type:             api.ProtocolTypeWS,
		Addr:             wsc.config.URL,
		AutoRoute:        false,
		ConnUse:          api.UseTypeMessage,
	}

    // new client
    client := &wsclient.Client{Options: option, ExOpts: exOpts}

    // 向服务端发起连接
    connection, err := client.Connect()
}

// kubeedge/vendor/github.com/kubeedge/viaduct/pkg/client/client.go
// 连接处理
func (c *Client) Connect() (conn.Connection, error) {
    protoClient = NewWSClient(c.Options, c.ExOpts) // 选用websocket

    protoConn, err := protoClient.Connect() // 发起ws连接
}

// kubeedge/staging/src/github.com/kubeedge/viaduct/pkg/client/ws.go
func (c *WSClient) Connect() (conn.Connection, error) {
    wsConn, resp, err := c.dialer.Dial(c.options.Addr, header)
}

问题 x509: certificate is valid for 2.2.2.2, not for 1.1.1.1

这个是因为客户端请求到服务端的证书后,会做校验,其中有一项就是校验dns或者ip,这个是在证书生成的时候配置的。 以kubeedge为例: 生成服务端证书:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// /kubeedge/cloud/pkg/cloudhub/servers/httpserver/signcerts.go
// SignCerts creates server's certificate and key
func SignCerts() ([]byte, []byte, error) {
	cfg := &certutil.Config{
		CommonName:   constants.ProjectName,
		Organization: []string{constants.ProjectName},
		Usages:       []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		AltNames: certutil.AltNames{
			DNSNames: hubconfig.Config.DNSNames, //以上报错验证的项
			IPs:      getIps(hubconfig.Config.AdvertiseAddress), //以上报错验证的项
		},
	}

	certDER, keyDER, err := NewCloudCoreCertDERandKey(cfg)
	if err != nil {
		return nil, nil, err
	}

	return certDER, keyDER, nil
}

就是当证书是在2.2.2.2生成的,然后把服务放在1.1.1.1运行,当客户端请求到证书以后,校验就会出错。

具体校验流程如下,逻辑比较长。

 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
func (wsc *WebSocketClient) Init() error // edge/pkg/edgehub/clients/wsclient/websocket.go
	=> option := wsclient.Options{
			HandshakeTimeout: wsc.config.HandshakeTimeout,
			TLSConfig:        tlsConfig,
			Type:             api.ProtocolTypeWS,
			Addr:             wsc.config.URL,
			AutoRoute:        false,
			ConnUse:          api.UseTypeMessage,
		}
	=> client := &wsclient.Client{Options: option, ExOpts: exOpts} // 
	=> connection, err := client.Connect()
	func (c *Client) Connect() (conn.Connection, error)  // staging/src/github.com/kubeedge/viaduct/pkg/client/client.go
		=> protoClient = NewWSClient(c.Options, c.ExOpts)
		func NewWSClient(options Options, exOpts interface{}) *WSClient  // staging/src/github.com/kubeedge/viaduct/pkg/client/ws.go
		    => TLSClientConfig:  options.TLSConfig
		=> protoConn, err := protoClient.Connect()
			func (c *WSClient) Connect() (conn.Connection, error) { // staging/src/github.com/kubeedge/viaduct/pkg/client/ws.go
			=> wsConn, resp, err := c.dialer.Dial(c.options.Addr, header)
				=> func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) // github.com/gorilla/websocket@v1.4.2/client.go
					=> func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) 
						=> u, err := url.Parse(urlStr)
						=> hostPort, hostNoPort := hostPortNoPort(u)
							=> func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) 
								=> hostNoPort = u.Host
						=> if u.Scheme == "https"
							=> cfg.ServerName = hostNoPort
						=> cfg := cloneTLSConfig(d.TLSClientConfig)
						=> tlsConn := tls.Client(netConn, cfg)
						=> doHandshake(tlsConn, cfg)
							func doHandshake(tlsConn *tls.Conn, cfg *tls.Config) error // github.com/gorilla/websocket@v1.4.2/client.go
								=> tlsConn.Handshake()
								=> if !cfg.InsecureSkipVerify
								=> tlsConn.VerifyHostname(cfg.ServerName) 
									=> func (c *Conn) VerifyHostname(host string) error  // /opt/go/src/crypto/tls/conn.go
										=> return c.peerCertificates[0].VerifyHostname(host)
											=> func (c *Certificate) VerifyHostname(h string) error  // /opt/go/src/crypto/x509/verify.go
												=> candidateIP := h
												=> if ip := net.ParseIP(candidateIP);  // 一般是内网情况 用ip
												=> return HostnameError{c, candidateIP}
													=> func (h HostnameError) Error() string 
														=> return "x509: certificate is valid for " + valid + ", not " + h.Host
												=> for _, match := range c.DNSNames  // 一般是外网情况 用dns
												=> return HostnameError{c, h}


func doHandshake(tlsConn *tls.Conn, cfg *tls.Config) // /opt/go/src/crypto/tls/conn.go
	=> tlsConn.Handshake()
		=> c.HandshakeContext(context.Background())
			=> c.handshakeContext(ctx)
				=> func (c *Conn) handshakeContext(ctx context.Context) (ret error)
					=> c.handshakeFn(handshakeCtx)
						=> func (c *Conn) clientHandshake(ctx context.Context) (err error) 
							=> func (hs *clientHandshakeState) handshake() error 
								=> hs.doFullHandshake()
								=> func (hs *clientHandshakeState) doFullHandshake() error 
									=> c.verifyServerCertificate(certMsg.certificates)
									=> func (c *Conn) verifyServerCertificate(certificates [][]byte) error
										=> c.peerCertificates = certs

参考:
http://arthurchiao.art/blog/everything-about-pki-zh/