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. 热身:制作证书
为了后续示例说明方便,我们先来把示例所需的私钥和证书都做出来,本文涉及的证书以及他们之间的签发关系如下图:
注:示例使用的自签名根证书。
从图中我们看到,我们证书分为三个层次,最左边是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))}}
这里我们使用了签发了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)会失败,因为根证书不是叶子证书的直接签发者,必须通过验证证书链来建立根证书和叶子证书之间的信任链。
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
为此在建连阶段,服务端不仅要将服务器实体证书发给客户端,还要发送完整的证书链(如下图所示)。
证书链的最顶端是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证书
这个示例的证书绑定情况如下图:
我们在服务端部署并绑定了三个证书,三个证书与域名的对应关系如下:
- 证书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回调从自定义的证书池中选择匹配的证书,而不能在用上面示例中那种“静态”模式了。
我们来看一个自定义证书选择逻辑的示例,下面示意图展示了客户端和服务端的证书部署情况:
我们主要看一下服务端的代码逻辑变动:
// 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核心技术,并继续加强与星友的互动。欢迎大家加入!
著名云主机服务厂商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

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。
参考资料
[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服务端绑定证书的几种方式
随着互联网的发展,网站提供的服务类型和规模不断扩大,同时也对Web服务的安全性提出了更高的要求。TLS(Transport Layer Security)[1]已然成为Web服务最重要的安全基础设施之一。默认情况下,一个TLS服务器通常只绑定一个证书[2],但…...
【算法与数据结构】--高级算法和数据结构--排序和搜索
一、常见排序算法 以下是一些常见的排序算法,包括冒泡排序、选择排序、插入排序、快速排序和归并排序。每种排序算法的讲解以及附带C#和Java示例: 1.1 冒泡排序 (Bubble Sort) 讲解: 冒泡排序是一种简单的比较排序算法。它多次遍历待排序的…...

【Java】jvm 元空间、常量池(了解)
JDK1.8 以前的 HotSpot JVM 有方法区,也叫永久代(permanent generation)方法区用于存放已被虚拟机加载的类信息,常量、静态遍历,即编译器编译后的代码JDK1.7 开始了方法区的部分移除:符号引用(S…...

Spring Boot自动加载
问:自动装配如何实现的? 答:简单来说就是自动去把第三方组件的Bean装载到IOC容器中,不需要开发人员再去写Bean相关的配置,在springboot应用里面只需要在启动类上去加上SpringBootApplication注解,就可以去实…...
MPNN 模型:GNN 传递规则的实现
首先,假如我们定义一个极简的传递规则 A是邻接矩阵,X是特征矩阵, 其物理意义就是 通过矩阵乘法操作,批量把图中的相邻节点汇聚到当前节点。 但是由于A的对角线都是 0.因此自身的节点特征会被过滤掉。 图神经网络的核心是 吸周围…...
Flink kafka 数据汇不指定分区器导致的问题
背景 在flink中,我们经常使用kafka作为flink的数据汇,也就是目标数据的存储地,然而当我们使用FlinkKafkaProducer作为数据汇连接器时,我们需要注意一些注意事项,本文就来记录一下 使用kafka数据汇连接器 首先我们看…...

【软考】14.1 面向对象基本概念/分析设计测试
《面向对象开发》 对象 现实生活中实际存在的一个实体;构成系统的一个基本单位由对象名、属性和方法组成 类 实体的形式化描述;对象是类的实例,类是对象的模板可分为:实体类:现实世界中真实的实体接口类(边…...

MFC-对话框
目录 1、模态和非模态对话框: (1)、对话框的创建 (2)、更改默认的对话框名称 (3)、创建模态对话框 1)、创建按钮跳转的界面 2)、在跳转的窗口添加类 3࿰…...

Essential Steps in Natural Language Processing (NLP)
💗💗💗欢迎来到我的博客,你将找到有关如何使用技术解决问题的文章,也会找到某个技术的学习路线。无论你是何种职业,我都希望我的博客对你有所帮助。最后不要忘记订阅我的博客以获取最新文章,也欢…...

Flink中KeyBy、分区、分组的正确理解
1.Flink中的KeyBy 在Flink中,KeyBy作为我们常用的一个聚合类型算子,它可以按照相同的Key对数据进行重新分区,分区之后分配到对应的子任务当中去。 源码解析 keyBy 得到的结果将不再是 DataStream,而是会将 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节 多次测量结果的精度指标算数平均值的分布特性与标准差算数平均值的置信度算数平均值的精度指标(常用的有4个) 第5节 非等精度测量 第1节 随机误差的性…...

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

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

Stream流中的 max()和 sorted()方法
需求:某个公司的开发部门,分为开发 一部 和 二部 ,现在需要进行年中数据结算。分析: 员工信息至少包含了(名称、性别、工资、奖金、处罚记录)开发一部有 4 个员工、开发二部有 5 名员工分别筛选出 2 个部门…...

云上攻防-云原生篇Docker安全权限环境检测容器逃逸特权模式危险挂载
文章目录 前言1、Docker是干嘛的?2、Docker对于渗透测试影响?3、Docker渗透测试点有那些?4、前渗透-判断在Docker中方式一:查询cgroup信息方式二:检查/.dockerenv文件方式三:检查mount信息方式四࿱…...

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文件流 这个地方区分是请求后端接口还是…...

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

格式工厂怎么把两个视频合并在一起
免费的工具谁不喜欢呢,今天为大家介绍的是格式工厂这款多功能视频转换软件,然而今天主要为大家介绍的是格式工厂的视频合并功能。 是的,你没有听错,格式工厂除了转换之外,还可以视频合适、视频剪辑、视频分割、去水印…...
在鸿蒙HarmonyOS 5中实现抖音风格的点赞功能
下面我将详细介绍如何使用HarmonyOS SDK在HarmonyOS 5中实现类似抖音的点赞功能,包括动画效果、数据同步和交互优化。 1. 基础点赞功能实现 1.1 创建数据模型 // VideoModel.ets export class VideoModel {id: string "";title: string ""…...

中南大学无人机智能体的全面评估!BEDI:用于评估无人机上具身智能体的综合性基准测试
作者:Mingning Guo, Mengwei Wu, Jiarun He, Shaoxian Li, Haifeng Li, Chao Tao单位:中南大学地球科学与信息物理学院论文标题:BEDI: A Comprehensive Benchmark for Evaluating Embodied Agents on UAVs论文链接:https://arxiv.…...
在HarmonyOS ArkTS ArkUI-X 5.0及以上版本中,手势开发全攻略:
在 HarmonyOS 应用开发中,手势交互是连接用户与设备的核心纽带。ArkTS 框架提供了丰富的手势处理能力,既支持点击、长按、拖拽等基础单一手势的精细控制,也能通过多种绑定策略解决父子组件的手势竞争问题。本文将结合官方开发文档,…...
线程与协程
1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指:像函数调用/返回一样轻量地完成任务切换。 举例说明: 当你在程序中写一个函数调用: funcA() 然后 funcA 执行完后返回&…...

UDP(Echoserver)
网络命令 Ping 命令 检测网络是否连通 使用方法: ping -c 次数 网址ping -c 3 www.baidu.comnetstat 命令 netstat 是一个用来查看网络状态的重要工具. 语法:netstat [选项] 功能:查看网络状态 常用选项: n 拒绝显示别名&#…...
Nginx server_name 配置说明
Nginx 是一个高性能的反向代理和负载均衡服务器,其核心配置之一是 server 块中的 server_name 指令。server_name 决定了 Nginx 如何根据客户端请求的 Host 头匹配对应的虚拟主机(Virtual Host)。 1. 简介 Nginx 使用 server_name 指令来确定…...

现代密码学 | 椭圆曲线密码学—附py代码
Elliptic Curve Cryptography 椭圆曲线密码学(ECC)是一种基于有限域上椭圆曲线数学特性的公钥加密技术。其核心原理涉及椭圆曲线的代数性质、离散对数问题以及有限域上的运算。 椭圆曲线密码学是多种数字签名算法的基础,例如椭圆曲线数字签…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
Element Plus 表单(el-form)中关于正整数输入的校验规则
目录 1 单个正整数输入1.1 模板1.2 校验规则 2 两个正整数输入(联动)2.1 模板2.2 校验规则2.3 CSS 1 单个正整数输入 1.1 模板 <el-formref"formRef":model"formData":rules"formRules"label-width"150px"…...

如何更改默认 Crontab 编辑器 ?
在 Linux 领域中,crontab 是您可能经常遇到的一个术语。这个实用程序在类 unix 操作系统上可用,用于调度在预定义时间和间隔自动执行的任务。这对管理员和高级用户非常有益,允许他们自动执行各种系统任务。 编辑 Crontab 文件通常使用文本编…...