当前位置: 首页 > news >正文

Go TLS服务端绑定证书的几种方式

随着互联网的发展,网站提供的服务类型和规模不断扩大,同时也对Web服务的安全性提出了更高的要求。TLS(Transport Layer Security)[1]已然成为Web服务最重要的安全基础设施之一。默认情况下,一个TLS服务器通常只绑定一个证书[2],但当服务复杂度增加时,单一证书已然难以满足需求。这时,服务端绑定多个TLS证书就成为一个非常实用的功能。

Go语言中的net/http包和tls包对TLS提供了强大的支持,在密码学和安全专家Filippo Valsorda[3]的精心设计下,Go提供了多种TLS服务端绑定证书的方式,本文将详细探讨服务端绑定TLS证书的几种方式,包括绑定单个证书、多个证书、自定义证书绑定逻辑等。我会配合示例代码,了解每种方式的使用场景、实现原理和优缺点。

注:本文假设读者已熟悉基本的TLS使用方法[4],并具备Go语言编程经验。如果你不具备Go语言基础知识,可以将学习我撰写的极客时间专栏《Go语言第一课》[5]作为你入门Go的起点。

1. 热身:制作证书

为了后续示例说明方便,我们先来把示例所需的私钥和证书都做出来,本文涉及的证书以及他们之间的签发关系如下图:

02a26ca4d8425a89a48650176f08ea7e.png

注:示例使用的自签名根证书。

从图中我们看到,我们证书分为三个层次,最左边是CA的根证书(root certificate,比如ca-cert.pem),之后是根CA签发的中间CA证书(intermediate certificate,比如inter-cert.pem),从安全和管理角度出发,真正签发服务器证书的都是这些中间CA;最右侧则是由中间CA签发的叶子证书(leaf certificate,比如leaf-server-cert.pem),也就是服务器配置的服务端证书(server certificate),我们为三个不同域名创建了不同的服务器证书。

在这里,我们制作上述证书没有使用类似openssl[6]这样的工具,而是通过Go代码生成的,下面是生成上述证书的代码片段:

// tls-certs-binding/make_certs/main.gofunc main() {// 生成CA根证书密钥对caKey, err := rsa.GenerateKey(rand.Reader, 2048)checkError(err)// 生成CA证书模板caTemplate := x509.Certificate{SerialNumber: big.NewInt(1),Subject: pkix.Name{Organization: []string{"Go CA"},},NotBefore:             time.Now(),NotAfter:              time.Now().Add(time.Hour * 24 * 365),KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,BasicConstraintsValid: true,IsCA:                  true,}// 使用模板自签名生成CA证书caCert, err := x509.CreateCertificate(rand.Reader, &caTemplate, &caTemplate, &caKey.PublicKey, caKey)checkError(err)// 生成中间CA密钥对interKey, err := rsa.GenerateKey(rand.Reader, 2048)checkError(err)// 生成中间CA证书模板interTemplate := x509.Certificate{SerialNumber: big.NewInt(2),Subject: pkix.Name{Organization: []string{"Go Intermediate CA"},},NotBefore:             time.Now(),NotAfter:              time.Now().Add(time.Hour * 24 * 365),KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,BasicConstraintsValid: true,IsCA:                  true,}// 用CA证书签名生成中间CA证书interCert, err := x509.CreateCertificate(rand.Reader, &interTemplate, &caTemplate, &interKey.PublicKey, caKey)checkError(err)// 生成叶子证书密钥对leafKey, err := rsa.GenerateKey(rand.Reader, 2048)checkError(err)// 生成叶子证书模板,CN为server.comleafTemplate := x509.Certificate{SerialNumber: big.NewInt(3),Subject: pkix.Name{Organization: []string{"Go Server"},CommonName:   "server.com",},NotBefore:    time.Now(),NotAfter:     time.Now().Add(time.Hour * 24 * 365),KeyUsage:     x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},IPAddresses:  []net.IP{net.ParseIP("127.0.0.1")},DNSNames:     []string{"server.com"},SubjectKeyId: []byte{1, 2, 3, 4},}// 用中间CA证书签名生成叶子证书leafCert, err := x509.CreateCertificate(rand.Reader, &leafTemplate, &interTemplate, &leafKey.PublicKey, interKey)checkError(err)// 生成server1.com叶子证书leafKey1, _ := rsa.GenerateKey(rand.Reader, 2048)leafTemplate1 := x509.Certificate{SerialNumber: big.NewInt(4),Subject: pkix.Name{CommonName: "server1.com",},NotBefore: time.Now(),NotAfter:  time.Now().Add(time.Hour * 24 * 365),KeyUsage:    x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},DNSNames:    []string{"server1.com"},}leafCert1, _ := x509.CreateCertificate(rand.Reader, &leafTemplate1, &interTemplate, &leafKey1.PublicKey, interKey)// 生成server2.com叶子证书leafKey2, _ := rsa.GenerateKey(rand.Reader, 2048)leafTemplate2 := x509.Certificate{SerialNumber: big.NewInt(5),Subject: pkix.Name{CommonName: "server2.com",},NotBefore: time.Now(),NotAfter:  time.Now().Add(time.Hour * 24 * 365),KeyUsage:    x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},DNSNames:    []string{"server2.com"},}leafCert2, _ := x509.CreateCertificate(rand.Reader, &leafTemplate2, &interTemplate, &leafKey2.PublicKey, interKey)// 将证书和密钥编码为PEM格式caCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert})caKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(caKey)})interCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: interCert})interKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(interKey)})leafCertPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert})leafKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(leafKey)})leafCertPEM1 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert1})leafKeyPEM1 := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(leafKey1)})leafCertPEM2 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert2})leafKeyPEM2 := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(leafKey2)})// 将PEM写入文件writeDataToFile("ca-cert.pem", caCertPEM)writeDataToFile("ca-key.pem", caKeyPEM)writeDataToFile("inter-cert.pem", interCertPEM)writeDataToFile("inter-key.pem", interKeyPEM)writeDataToFile("leaf-server-cert.pem", leafCertPEM)writeDataToFile("leaf-server-key.pem", leafKeyPEM)writeDataToFile("leaf-server1-cert.pem", leafCertPEM1)writeDataToFile("leaf-server1-key.pem", leafKeyPEM1)writeDataToFile("leaf-server2-cert.pem", leafCertPEM2)writeDataToFile("leaf-server2-key.pem", leafKeyPEM2)
}

运行这个程序后,当前目录下就会出现如下私钥文件(xx-key.pem)和证书文件(xx-cert.pem):

$ls *pem
ca-cert.pem  inter-cert.pem  leaf-server-cert.pem leaf-server1-cert.pem leaf-server2-cert.pem
ca-key.pem  inter-key.pem  leaf-server-key.pem leaf-server1-key.pem leaf-server2-key.pem

制作完证书后,我们就来看看日常使用最多的绑定单一TLS证书的情况。

2. 绑定单一TLS证书

做过web应用的读者,想必对绑定单一TLS证书的实现方式并不陌生。服务端只需要加载一对服务端私钥与公钥证书即可对外提供基于TLS的安全网络服务,这里一个echo服务为例,我们来看下服务端的代码:

// tls-certs-binding/bind_single_cert/sever/main.go// 服务端
func startServer(certFile, keyFile string) {// 读取证书和密钥cert, err := tls.LoadX509KeyPair(certFile, keyFile)if err != nil {log.Fatal(err)}// 创建TLS配置config := &tls.Config{Certificates: []tls.Certificate{cert},}// 启动TLS服务器listener, err := tls.Listen("tcp", ":8443", config)if err != nil {log.Fatal(err)}defer listener.Close()log.Println("Server started")for {conn, err := listener.Accept()if err != nil {log.Println(err)continue}handleConnection(conn)}
}func handleConnection(conn net.Conn) {defer conn.Close()// 处理连接...// 循环读取客户端的数据for {buf := make([]byte, 1024)n, err := conn.Read(buf)if err != nil {// 读取失败则退出return}// 回显数据给客户端s := string(buf[:n])fmt.Printf("recv data: %s\n", s)conn.Write(buf[:n])}
}func main() {// 启动服务器startServer("leaf-server-cert.pem", "leaf-server-key.pem")
}

根据TLS的原理,客户端在与服务端的握手过程中,服务端会将服务端证书(leaf-server-cert.pem)发到客户端供后者验证,客户端使用服务器公钥证书校验服务器身份。这一过程的实质是客户端利用CA证书中的公钥或中间CA证书中的公钥对服务端证书中由CA私钥或中间CA私钥签名的数据进行验签

// tls-certs-binding/bind_single_cert/client/main.gofunc main() {caCert, err := ioutil.ReadFile("inter-cert.pem")if err != nil {log.Fatal(err)}caCertPool := x509.NewCertPool()caCertPool.AppendCertsFromPEM(caCert)config := &tls.Config{RootCAs: caCertPool,}conn, err := tls.Dial("tcp", "server.com:8443", config)if err != nil {log.Fatal(err)}defer conn.Close()// 每秒发送信息ticker := time.NewTicker(time.Second)for range ticker.C {msg := "hello, tls"conn.Write([]byte(msg))// 读取回复buf := make([]byte, len(msg))conn.Read(buf)log.Println(string(buf))}}

7c2de011538b0e02aa5a2bc7515e4dac.png

这里我们使用了签发了leaf-server-cert.pem证书的中间CA(inter-cert.pem)来验证服务端证书(leaf-server-cert.pem)的合法性,毫无疑问这是会成功的!

// server
$go run main.go
2023/10/05 22:49:17 Server started// client$go run main.go
2023/10/05 22:49:22 hello, tls
2023/10/05 22:49:23 hello, tls
... ...

注:运行上述代码之前,需修改/etc/hosts文件,添加server.com的IP为127.0.0.1。

不过要注意的是,在这里用CA根证书(ca-cert.pem)直接验证叶子证书(leaf-server-cert.pem)会失败,因为根证书不是叶子证书的直接签发者,必须通过验证证书链来建立根证书和叶子证书之间的信任链。

9e6f51b85c585e8d5ee2b309cac820fd.png

3. 证书链

实际生产中,服务器实体证书和根证书分别只有一张,但中间证书可以有多张,这些中间证书在客户端并不一定存在,这就可能导致客户端与服务端的连接无法建立。通过openssl命令也可以印证这一点:

// 在make_certs目录下// CA根证书无法直接验证叶子证书
$openssl verify -CAfile ca-cert.pem leaf-server-cert.pem
leaf-server-cert.pem: O = Go Server, CN = server.com
error 20 at 0 depth lookup:unable to get local issuer certificate// 证书链不完整,也无法验证
$openssl verify -CAfile inter-cert.pem leaf-server-cert.pem
leaf-server-cert.pem: O = Go Intermediate CA
error 2 at 1 depth lookup:unable to get issuer certificate// 需要用完整证书链来验证
$openssl verify -CAfile ca-cert.pem -untrusted inter-cert.pem leaf-server-cert.pem
leaf-server-cert.pem: OK

为此在建连阶段,服务端不仅要将服务器实体证书发给客户端,还要发送完整的证书链(如下图所示)。

3e87ed1a42b7d305803214c5334c6531.png

证书链的最顶端是CA根证书,它的签名值是自己签名的,验证签名的公钥就包含在根证书中,根证书的签发者(Issuer)与使用者(Subject)是相同的。除了根证书,每个证书的签发者(Issuer)是它的上一级证书的使用者(Subject)。以上图为例,下列关系是成立的:

- ca-cert.pem的Issuer == ca-cert.pem的Subject
- inter1-cert.pem的Issuer == ca-cert.pem的Subject
- inter2-cert.pem的Issuer == inter1-cert.pem的Subject
... ...
- interN-cert.pem的Issuer == interN-1-cert.pem的Subject
- leaf-server-cert.pem的Issuer == interN-cert.pem的Subject

每张证书包含的重要信息是签发者(Issuer)、数字签名算法、签名值、使用者(Subject)域名、使用者公钥。除了根证书,每个证书(比如inter2-cert.pem证书)被它的上一级证书(比如inter1-cert.pem证书)对应的私钥签名,签名值包含在证书中,上一级证书包含的公钥可以用来验证该证书中的签名值(inter2-cert.pem证书可以用来验证inter1-cert.pem证书中的签名值)。

那么如何在服务端返回证书链呢?如何在客户端接收并验证证书链呢?我们来看下面示例。在这个示例中,客户端仅部署了根证书(ca-cert.pem),而服务端需要将服务证书与签发服务证书的中间CA证书以证书链的形式返回给客户端。

我们先来看服务端:

// tls-certs-binding/bind_single_cert/server-with-certs-chain/main.go// 服务端
func startServer(certFile, keyFile string) {// 读取证书和密钥cert, err := tls.LoadX509KeyPair(certFile, keyFile)if err != nil {log.Fatal(err)}interCertBytes, err := os.ReadFile("inter-cert.pem")if err != nil {log.Fatal(err)}interCertblock, _ := pem.Decode(interCertBytes)// 将中间证书添加到证书链cert.Certificate = append(cert.Certificate, interCertblock.Bytes)// 创建TLS配置config := &tls.Config{Certificates: []tls.Certificate{cert},}// 启动TLS服务器listener, err := tls.Listen("tcp", ":8443", config)if err != nil {log.Fatal(err)}defer listener.Close()log.Println("Server started")for {conn, err := listener.Accept()if err != nil {log.Println(err)continue}handleConnection(conn)}
}

我们看到:服务端在加载完服务端证书后,又将中间CA证书inter-cert.pem attach到cert.Certificate,这样cert.Certificate中就构造出了一个证书链,而不单单是一个服务端证书了。

我们要注意证书链构造时的顺序,这里按照的是如下顺序构造证书链的:

- 服务端证书 (leaf certificate)
- 中间CA证书N
- 中间CA证书N-1
... ...
- 中间CA证书2
- 中间CA证书1

如果客户端没有根CA证书 (root certificate),在服务端构造证书链时,需要将根CA证书作为最后一个证书attach到证书链中。

下面则是客户端验证证书链的代码:

// tls-certs-binding/bind_single_cert/client-verify-certs-chain/main.gofunc main() {// 加载ca-cert.pemcaCertBytes, err := os.ReadFile("ca-cert.pem")if err != nil {log.Fatal(err)}caCertblock, _ := pem.Decode(caCertBytes)caCert, err := x509.ParseCertificate(caCertblock.Bytes)if err != nil {log.Fatal(err)}// 创建TLS配置config := &tls.Config{InsecureSkipVerify: true, // trigger to call VerifyPeerCertificate// 设置证书验证回调函数VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {// 解析服务端返回的证书链(顺序:server-cert.pem, inter-cert.pem,inter-cert.pem's issuer...)var issuer *x509.Certificatevar cert *x509.Certificatevar err errorif len(rawCerts) == 0 {return errors.New("no server certificate found")}issuer = caCertfor i := len(rawCerts) - 1; i >= 0; i-- {cert, err = x509.ParseCertificate(rawCerts[i])if err != nil {return err}if !verifyCert(issuer, cert) {return errors.New("verifyCert failed")}issuer = cert}return nil},}conn, err := tls.Dial("tcp", "server.com:8443", config)if err != nil {log.Fatal(err)}defer conn.Close()// 每秒发送信息ticker := time.NewTicker(time.Second)for range ticker.C {msg := "hello, tls"conn.Write([]byte(msg))// 读取回复buf := make([]byte, len(msg))conn.Read(buf)log.Println(string(buf))}}// 验证cert是否是issuer的签发
func verifyCert(issuer, cert *x509.Certificate) bool {// 验证证书certPool := x509.NewCertPool()certPool.AddCert(issuer) // okopts := x509.VerifyOptions{Roots: certPool,}_, err := cert.Verify(opts)return err == nil
}

从代码可以看到,我们需要将InsecureSkipVerify设置为true才能触发证书链的自定义校验逻辑(VerifyPeerCertificate)。在VerifyPeerCertificate中,我们先用ca根证书校验位于证书链最后的那个证书,验证成功后,用验证成功的证书验证倒数第二个证书,依次类推,知道全部证书都校验ok,说明证书链是可信任的。

服务端绑定一个证书或一套证书链是最简单的,也是最常见的方案,但在一些场景下,比如考虑支持多个域名、证书轮换等,TLS服务端可能需要绑定多个证书以满足要求。下面我们就来看看如何为TLS服务端绑定多个证书。

4. 绑定多个TLS证书

这个示例的证书绑定情况如下图:

d3fdce64e0cc784060601096500024c7.png

我们在服务端部署并绑定了三个证书,三个证书与域名的对应关系如下:

- 证书leaf-server-cert.pem 对应 server.com 
- 证书leaf-server1-cert.pem 对应 server1.com 
- 证书leaf-server2-cert.pem 对应 server2.com

注:在/etc/hosts中添加server1.com和server2.com对应的ip均为127.0.0.1。

// tls-certs-binding/bind_multi_certs/server/main.gofunc main() {certFiles := []string{"leaf-server-cert.pem", "leaf-server1-cert.pem", "leaf-server2-cert.pem"}keyFiles := []string{"leaf-server-key.pem", "leaf-server1-key.pem", "leaf-server2-key.pem"}// 启动服务器startServer(certFiles, keyFiles)
}// 服务端
func startServer(certFiles, keyFiles []string) {// 读取证书和密钥var certs []tls.Certificatefor i := 0; i < len(certFiles); i++ {cert, err := tls.LoadX509KeyPair(certFiles[i], keyFiles[i])if err != nil {log.Fatal(err)}certs = append(certs, cert)}// 创建TLS配置config := &tls.Config{Certificates: certs,}// 启动TLS服务器listener, err := tls.Listen("tcp", ":8443", config)if err != nil {log.Fatal(err)}defer listener.Close()log.Println("Server started")for {conn, err := listener.Accept()if err != nil {log.Println(err)continue}handleConnection(conn)}
}

我们看到,绑定多个证书与绑定一个证书的原理是完全一样的,tls.Config的Certificates字段原本就是一个切片,可以容纳单个证书,也可以容纳证书链,容纳多个证书也不是问题。

客户端代码变化不大,我们仅是通过下面代码输出了服务端返回的证书的Subject.CN:

// tls-certs-binding/bind_multi_certs/client/main.go// 解析连接的服务器证书
certs := conn.ConnectionState().PeerCertificates
if len(certs) > 0 {log.Println("Server CN:", certs[0].Subject.CommonName)
}

接下来我们通过client连接不同的域名,得到如下执行结果:

// 服务端
$go run main.go
2023/10/06 10:22:38 Server started// 客户端
$go run main.go -server server.com:8443
2023/10/06 10:22:57 Server CN: server.com
2023/10/06 10:22:58 hello, tls$go run main.go -server server1.com:8443
2023/10/06 10:23:02 Server CN: server1.com
2023/10/06 10:23:03 hello, tls
2023/10/06 10:23:04 hello, tls$go run main.go -server server2.com:8443
2023/10/06 10:23:08 Server CN: server2.com
2023/10/06 10:23:09 hello, tls
... ...

我们看到,由于绑定多个域名对应的证书,程序可以支持访问不同域名的请求,并根据请求的域名,返回对应域名的证书。

5. 自定义证书选择绑定逻辑

无论是单一TLS证书、证书链还是多TLS证书,他们都有一个共同特点,那就是证书的绑定是事先已知的,是一种“静态”模式的绑定;有些场景下,服务端在初始化启动后并不会绑定某个固定的证书,而是根据客户端的连接需求以及特定规则在证书池中选择某个匹配的证书。在这种情况下,我们需要使用GetCertificate回调从自定义的证书池中选择匹配的证书,而不能在用上面示例中那种“静态”模式了。

我们来看一个自定义证书选择逻辑的示例,下面示意图展示了客户端和服务端的证书部署情况:

ed1f699d065cded7c1678df4a503b4fe.png

我们主要看一下服务端的代码逻辑变动:

// tls-certs-binding/bind_custom_logic/server/main.gofunc startServer(certsPath string) {// 创建TLS配置config := &tls.Config{GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {// 根据clientHello信息选择certcertFile := fmt.Sprintf("%s/leaf-%s-cert.pem", certsPath, info.ServerName[:len(info.ServerName)-4])keyFile := fmt.Sprintf("%s/leaf-%s-key.pem", certsPath, info.ServerName[:len(info.ServerName)-4])// 读取证书和密钥cert, err := tls.LoadX509KeyPair(certFile, keyFile)return &cert, err},  }   ... ...
}

我们看到: tls.Config我们建立了一个匿名函数赋值给了GetCertificate字段,该函数的实现逻辑就是根据客户端clientHello信息(tls握手时发送的信息)按照规则从证书池目录中查找并加载对应的证书与其私钥信息。示例使用ServerName来查找带有同名信息的证书。

例子的运行结果与上面的示例都差不多,这里就不赘述了。

利用这种动态的证书选择逻辑,我们还可以实现通过执行外部命令来获取证书、从数据库加载证书等。

6. 小结

通过本文的介绍,我们全面了解了在Go服务端绑定单个、多个TLS证书的各种方式。我们首先介绍了生成自签名证书的方法,这为我们的示例程序奠定了基础。然后我们详细探讨了绑定单证书、证书链、多证书、定制从证书池取特定证书的逻辑等不同机制的用法、优劣势和适用场景。同时,在介绍每种用法时,我们都用代码示例进一步解释了这些绑定方式的具体实现流程。

单证书TLS简单易理解,运行性能优异。多证书TLS在提高性能、安全性、便利管理等方面有着重要意义。而自定义证书选取逻辑则更加灵活。通过综合运用各种绑定机制,可以使我们的Go语言服务器端更加强大和灵活。

本文示例所涉及的Go源码可以在这里[7]下载。

注:代码仓库中的证书和key文件有效期为一年,大家如发现证书已经过期,可以在make_certs目录下重新生成各种证书和私钥并copy到对应的其他目录中去。

7. 参考资料

  • 《深入浅出 HTTPS:从原理到实战》[8] - https://book.douban.com/subject/30250772/

  • Certificate chains and cross-certification[9] - https://en.wikipedia.org/wiki/X.509#Certificate_chains_and_cross-certification


“Gopher部落”知识星球[10]旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

79a807afe290e92218e0e85d159e5513.jpeg29b248a2865f61fd9cb2959f6c098214.png

afd987baeadd1f1024ad54b9ab8e02b1.pngf15c45cdfbceb9790d3341b6d50ca0af.jpeg

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址[11]:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻) - https://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx

  • 微博2:https://weibo.com/u/6484441286

  • 博客:tonybai.com

  • github: https://github.com/bigwhite

  • Gopher Daily归档 - https://github.com/bigwhite/gopherdaily

45fba7c048b05b5d36a75c7d291cb3b3.jpeg

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

参考资料

[1] 

TLS(Transport Layer Security): https://tonybai.com/2023/01/13/go-and-tls13

[2] 

一个TLS服务器通常只绑定一个证书: https://tonybai.com/2015/04/30/go-and-https

[3] 

Filippo Valsorda: https://filippo.io/

[4] 

TLS使用方法: https://tonybai.com/2015/04/30/go-and-https

[5] 

《Go语言第一课》: http://gk.link/a/10AVZ

[6] 

openssl: https://www.openssl.org

[7] 

这里: https://github.com/bigwhite/experiments/tree/master/tls-certs-binding

[8] 

《深入浅出 HTTPS:从原理到实战》: https://book.douban.com/subject/30250772/

[9] 

Certificate chains and cross-certification: https://en.wikipedia.org/wiki/X.509#Certificate_chains_and_cross-certification

[10] 

“Gopher部落”知识星球: https://public.zsxq.com/groups/51284458844544

[11] 

链接地址: https://m.do.co/c/bff6eed92687

相关文章:

Go TLS服务端绑定证书的几种方式

随着互联网的发展&#xff0c;网站提供的服务类型和规模不断扩大&#xff0c;同时也对Web服务的安全性提出了更高的要求。TLS(Transport Layer Security)[1]已然成为Web服务最重要的安全基础设施之一。默认情况下&#xff0c;一个TLS服务器通常只绑定一个证书[2]&#xff0c;但…...

【算法与数据结构】--高级算法和数据结构--排序和搜索

一、常见排序算法 以下是一些常见的排序算法&#xff0c;包括冒泡排序、选择排序、插入排序、快速排序和归并排序。每种排序算法的讲解以及附带C#和Java示例&#xff1a; 1.1 冒泡排序 (Bubble Sort) 讲解&#xff1a; 冒泡排序是一种简单的比较排序算法。它多次遍历待排序的…...

【Java】jvm 元空间、常量池(了解)

JDK1.8 以前的 HotSpot JVM 有方法区&#xff0c;也叫永久代&#xff08;permanent generation&#xff09;方法区用于存放已被虚拟机加载的类信息&#xff0c;常量、静态遍历&#xff0c;即编译器编译后的代码JDK1.7 开始了方法区的部分移除&#xff1a;符号引用&#xff08;S…...

Spring Boot自动加载

问&#xff1a;自动装配如何实现的&#xff1f; 答&#xff1a;简单来说就是自动去把第三方组件的Bean装载到IOC容器中&#xff0c;不需要开发人员再去写Bean相关的配置&#xff0c;在springboot应用里面只需要在启动类上去加上SpringBootApplication注解&#xff0c;就可以去实…...

MPNN 模型:GNN 传递规则的实现

首先&#xff0c;假如我们定义一个极简的传递规则 A是邻接矩阵&#xff0c;X是特征矩阵&#xff0c; 其物理意义就是 通过矩阵乘法操作&#xff0c;批量把图中的相邻节点汇聚到当前节点。 但是由于A的对角线都是 0.因此自身的节点特征会被过滤掉。 图神经网络的核心是 吸周围…...

Flink kafka 数据汇不指定分区器导致的问题

背景 在flink中&#xff0c;我们经常使用kafka作为flink的数据汇&#xff0c;也就是目标数据的存储地&#xff0c;然而当我们使用FlinkKafkaProducer作为数据汇连接器时&#xff0c;我们需要注意一些注意事项&#xff0c;本文就来记录一下 使用kafka数据汇连接器 首先我们看…...

【软考】14.1 面向对象基本概念/分析设计测试

《面向对象开发》 对象 现实生活中实际存在的一个实体&#xff1b;构成系统的一个基本单位由对象名、属性和方法组成 类 实体的形式化描述&#xff1b;对象是类的实例&#xff0c;类是对象的模板可分为&#xff1a;实体类&#xff1a;现实世界中真实的实体接口类&#xff08;边…...

MFC-对话框

目录 1、模态和非模态对话框&#xff1a; &#xff08;1&#xff09;、对话框的创建 &#xff08;2&#xff09;、更改默认的对话框名称 &#xff08;3&#xff09;、创建模态对话框 1&#xff09;、创建按钮跳转的界面 2&#xff09;、在跳转的窗口添加类 3&#xff0…...

Essential Steps in Natural Language Processing (NLP)

&#x1f497;&#x1f497;&#x1f497;欢迎来到我的博客&#xff0c;你将找到有关如何使用技术解决问题的文章&#xff0c;也会找到某个技术的学习路线。无论你是何种职业&#xff0c;我都希望我的博客对你有所帮助。最后不要忘记订阅我的博客以获取最新文章&#xff0c;也欢…...

Flink中KeyBy、分区、分组的正确理解

1.Flink中的KeyBy 在Flink中&#xff0c;KeyBy作为我们常用的一个聚合类型算子&#xff0c;它可以按照相同的Key对数据进行重新分区&#xff0c;分区之后分配到对应的子任务当中去。 源码解析 keyBy 得到的结果将不再是 DataStream&#xff0c;而是会将 DataStream 转换为 Key…...

QT6集成CEF3--01 准备工作

QT6集成CEF3--01 准备工作 一、所有使用到的工具软件清单:二、准备工作三、cefclient示例程序四、特别注意 一、所有使用到的工具软件清单: CEF 二进制发行包 cef_binary_117.2.5gda4c36achromium-117.0.5938.152_windows64.tar.bz2 CMake 编译工具 cmake-3.22.6-windows-x86_…...

随机误差理论与测量

文章目录 第1节 随机误差的性质和特点第2节 随机误差的数字特性标准差的估计 第3节 单次测量结果的精度指标第4节 多次测量结果的精度指标算数平均值的分布特性与标准差算数平均值的置信度算数平均值的精度指标&#xff08;常用的有4个) 第5节 非等精度测量 第1节 随机误差的性…...

树莓派4b配置通过smbus2使用LCD灯

出现报错&#xff1a; FileNotFoundError: [Errno 2] No such file or directory: ‘/dev/i2c-1’ 则说明没有打开I2C&#xff0c;可通过如下步骤进行设置 1、打开树莓派配置 sudo raspi-config2、进入Interface Options&#xff0c;配置I2C允许 目前很多python3版本已经不…...

UPS 原理和故障案例分享

摘要:不间断电源UPS (Uninterruptible Power System)&#xff0c;主要是由整流器、 逆变器、静态旁路和储能装置等组成;具备高可靠性、高可用性和高质量的独立 电源。通过对收集的 UPS 故障案例进行分析&#xff0c;从施工&#xff0c;调试和运行三个方面筛选 出四个故障案例与…...

Stream流中的 max()和 sorted()方法

需求&#xff1a;某个公司的开发部门&#xff0c;分为开发 一部 和 二部 &#xff0c;现在需要进行年中数据结算。分析&#xff1a; 员工信息至少包含了&#xff08;名称、性别、工资、奖金、处罚记录&#xff09;开发一部有 4 个员工、开发二部有 5 名员工分别筛选出 2 个部门…...

云上攻防-云原生篇Docker安全权限环境检测容器逃逸特权模式危险挂载

文章目录 前言1、Docker是干嘛的&#xff1f;2、Docker对于渗透测试影响&#xff1f;3、Docker渗透测试点有那些&#xff1f;4、前渗透-判断在Docker中方式一&#xff1a;查询cgroup信息方式二&#xff1a;检查/.dockerenv文件方式三&#xff1a;检查mount信息方式四&#xff1…...

PDE数值解中,为什么要引入弱解(weak solution)的概念?

See https://www.zhihu.com/question/24243246?utm_sourceqq&utm_mediumsocial&utm_oi1315073218793488384...

使用pdfjs实现在线预览pdf

在工作中可能会遇到前端展示pdf文件进行预览并提供下载的需求场景,例如操作指引,这个时候需要寻找一款实现该功能的插件,以pdjjs举例子 1. 安装pdf.js npm install pdfjs-dist2. 引入pdf.js import pdfjsLib from pdfjs-dist3.加载pdf文件流 这个地方区分是请求后端接口还是…...

汇编语言基础

引言 汇编语言是直接在硬件之上工作的编程语言&#xff0c;首先要了解硬件系统的结构&#xff0c;才能有效的应用汇编语言对其编程。汇编课程的研究重点放在如何利用硬件系统的编程结构和指令集有效灵活的控制系统进行工作。 基础知识 1.1机器语言 机器语言是机器指令的集合…...

格式工厂怎么把两个视频合并在一起

免费的工具谁不喜欢呢&#xff0c;今天为大家介绍的是格式工厂这款多功能视频转换软件&#xff0c;然而今天主要为大家介绍的是格式工厂的视频合并功能。 是的&#xff0c;你没有听错&#xff0c;格式工厂除了转换之外&#xff0c;还可以视频合适、视频剪辑、视频分割、去水印…...

2.MySQL表的操作

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 表的操作 (1)表的创建 CREATE TABLE table_name ( field1 datatype, field2 datatype, field3 datatype ) character set 字符集 collate 校验规则 engine 存储引擎; 存储引擎的不同会导致创建表的文件不同。 换个引擎。 t…...

网络安全之应急流程

近期需要弄一个网络安全应急的流程&#xff0c;其实对于网络安全应急并不陌生&#xff0c;只是在一些特定的环境上会遇到一些难以解决的问题或者缺少某个岗位的技术人员&#xff0c;因为不同运营商的应急小队也是不同的岗位&#xff0c;如今有着安全设备的告警和预警&#xff0…...

[Python进阶] 操纵鼠标:pyuserinput

6.2 操纵鼠标&#xff1a;pyuserinput 6.2.1 说明 在安装pyuserinput库时会自动安装PyMouse和PyKeyboard库。前者主要用来操作鼠标&#xff0c;包括鼠标的点击、移动等。后者主要用来操作键盘&#xff0c;包括键盘按键的按下、弹起等。 这两个库还可以同时对鼠标和键盘的事件…...

【LeetCode】每日一题两数之和寻找正序数组的中位数找出字符串中第一个匹配项的下标在排序数组中查找元素的第一个和最后一个位置

主页点击直达&#xff1a;个人主页 我的小仓库&#xff1a;代码仓库 C语言偷着笑&#xff1a;C语言专栏 数据结构挨打小记&#xff1a;初阶数据结构专栏 Linux被操作记&#xff1a;Linux专栏 LeetCode刷题掉发记&#xff1a;LeetCode刷题 算法&#xff1a;算法专栏 C头…...

与HTTP相关的各种协议

TCP/IP TCP/IP协议是目前网络世界“事实上”的标准通信协议&#xff0c;实际上是一系列网络通信协议的统称&#xff0c;其中最核心的两个协议是 TCP和IP&#xff0c;其他的还有 UDP、ICMP、ARP 等等&#xff0c;共同构成了一个复杂但有层次的协议栈。 这个协议栈有四层&#x…...

常见的网络攻击手段

网络攻击对个人、组织和整个社会都带来了严重的威胁&#xff0c;因此必须采取有效的安全措施来保护网络系统和用户的信息安全。网站是攻击者经常瞄准的目标&#xff0c;以下是一些常见的攻击方式&#xff1a; 1. DDoS攻击&#xff08;分布式拒绝服务攻击&#xff09;&#xff1…...

学习笔记---超基础+详细+新手的顺序表~~

目录 1.顺序表的前言 1.1 顺序表--->通讯录&#x1f4c7; 1.2 数据结构的相关概念&#x1f3c7; 1.2.1 什么是数据结构 1.2.1 为什么需要数据结构 2. 顺序表概念及分类 2.1 顺序表的概念&#x1f419; 2.2 顺序表的分类&#x1f42b; 2.2.1 顺序表和数组的区别 2.…...

Java高级-CompletableFuture并发编程利器

CompletableFuture核心Api 1.概述2.Async2.a) supplyAsync2.b) runAsync 3.Then3.a) thenApply()3.b) thenApplyAsync() 1.概述 Future可以在并发编程中异步获取结果 CompletableFuture实现了Future接口&#xff0c;肯定也会有Future的功能&#xff0c;也相当于是Future的一个…...

python、java、c++哪一个前景比较好?

Python是一种广泛使用的高级编程语言&#xff0c;适用于数据分析、人工智能、机器学习等领域。Java是一种通用的编程语言&#xff0c;适用于企业级应用开发、网站开发、软件开发、嵌入式领域等。C是一种系统编程语言&#xff0c;适用于嵌入式开发、游戏开发、音视频、服务端开发…...

【排序算法】详解直接插入排序和希尔排序原理及其性能分析

文章目录 插入排序算法原理细节分析代码实现复杂度分析:稳定性分析:与冒泡排序的对比 希尔排序算法原理细节分析代码实现复杂度分析稳定性分析 总结对比 插入排序 算法原理 插入排序又或者说直接插入排序,是一种和冒泡排序类似的并且比较简单的排序方法&#xff0c; 基本思想…...

如何在别人网站挂黑链/教育培训机构加盟十大排名

php中文网最新课程每日17点准时技术干货分享直接写入cache模块中&#xff0c;生成控制器namespace app\cache\controller;use think\Controller;use think\Cache;具体方法如下&#xff1a;public function Index(){return $this->fetch();}//清除模版缓存不删除cache目录;pu…...

软件工程技术学什么/网站性能优化的方法有哪些

效果图 静态图 ​ 动态图 ​ 代码及详解: 代码很简单,让我们直接来看代码和注释 varying vec2 texcoord;// uniform float iGlobalTime; // uniform vec2 iResolution;...

网站建设成本计划书/知识付费网站搭建

第一部分 简 介 一. 硬盘结构简介 1. 硬盘参数释疑 到目前为止&#xff0c;人们常说的硬盘参数还是古老的 CHS (Cylinder/Head/Sector)参数。那么为什么要使用这些参数&#xff0c;它们的意义是什么&#xff1f;它们的取值范围是什么&#xff1f; 很久以前(long long ago .…...

餐饮加盟网站建设/苏州关键词优化怎样

2019独角兽企业重金招聘Python工程师标准>>> /*** 根据反射传入不同的List<T>动态生成日志文件* param list 数据集合* param writeTxtPath 日志文件生成路径* throws IllegalArgumentException* throws IllegalAccessException* throws FileNotF…...

网站建设的难处/今日军事新闻报道

如果在安装CentOS的时候没有选择中文&#xff0c;可以通过以下方式安装中文语言支持。 # yum install "Chinese Support"也可以通过 yum grouplist来列出所有的group和languages...

怎么删除网站里的死链接/免费建站免费推广的网站

网页开发最最重要最最基本的就是富文本编辑器和文件上传&#xff0c;开始我迷信百度的ueditor和webupload&#xff0c;结果总是别扭&#xff0c;看来不能迷信BAT啊。富文本用了froala&#xff0c;文件上传早点用bootstrap fileinput那多炫啊。 参考网上的文章&#xff0c;走了…...