证书的双向认证与自动化签发

为了保证网络程序间的通信安全,需要对程序间的网络通道进行数据加密。在数据加密之前,还需要对通信双方的身份进行验证。通过一个双方互信的证书签发中心,分别签发证书,在双方首次握手过程中,对对方的证书进行校验,校验通过则继续通信,否则断开链接。这就是基础的 SSL/TLS 双向验证过程。如下图:

除了双向验证以外,还可以只对提供服务的服务方进行单向验证,如常规的网站服务。

证书签发中心

在开始双向验证之前,需要从一个知名的 CA 证书签发中心,签发各端的安全证书。通常这一步是需要付费的。如果仅仅是针对内部的服务间通信安全,可以自己充当这个角色即可。

自签名 CA 证书

生成自签名证书传统工具是OpenSSL。不过OpenSSL不论是其复杂的命令选项,还是更加复杂配置都会让人头皮发麻。这里介绍一个更简单的生成自签名证书的工具: certstrap, 项目地址:square/certstrap.具体安装请参考其文档。

certstrap 操作命令如下:

$: certstrap init --common-name "ExampleCA" --expires "20 years"

命令完成后,会在当前目录下创建一个新的out目录,生成的证书都在该目录下.

$: tree out
out
├── ExampleCA.crl
├── ExampleCA.crt
└── ExampleCA.key

服务端证书

首先创建CSR, 即证书签名请求。

$: certstrap request-cert -cn server -ip 127.0.0.1 -domain "*.example.com"
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Created out/server.key
Created out/server.csr

生成CSR之后,通过刚刚生成的CA证书进行签名.

$: certstrap sign server --CA ExampleCA
Enter passphrase for CA key (empty for no passphrase):
Created out/server.crt from out/server.csr signed by out/ExampleCA.key

这样就完成了服务端证书的签名,签名后的证书就是:out/server.crt.

客户端证书

如果需要进行双向验证,还需生成相应的客户端证书。客户端证书的生成过程同服务端类似,更简单一点,不需要提供证书的IP与域名信息。

$: certstrap request-cert -cn client
$: certstrap sign client --CA ca

查看证书

生成完的证书是否正确,可以通过certigo工具进行查询。项目地址: square/certigo

安装完成后,通过以下命令查询证书的具体信息。

$: certigo dump out/server.crt
** CERTIFICATE 1 **
Valid: 2019-08-26 09:34 UTC to 2021-08-26 09:34 UTC
Subject:
	CN=server
Issuer:
	CN=ExampleCA
DNS Names:
	*.example.com
IP Addresses:
	127.0.0.1

PKCS 格式证书

生成PKCS格式的证书可以直接点击安装到系统证书簇中,方便一些应用(浏览器等)的使用。具体生成PKCS 格式证书,使用OpenSSL命令如下:

$: openssl pkcs12 -export -out client.p12 -inkey out/client.key -in out/client.crt -certfile out/ExampleCA.crt

双向认证

完成证书的签发后,在编码过程中,就需要启用相应的证书。为了更加方便的启用证书,x-mod/tlsconfig 做了很多辅助工作,可以快速的在代码中启用证书。

服务端设置

服务端开启TLS,同时开启客户端验证:


import "github.com/x-mod/tlsconfig"

cf := tlsconfig.New(
    //服务端 TLS 证书
    tlsconfig.CertKeyPair("out/server.crt", "out/server.key"), 
    //客户端 TLS 证书签名 CA
    tlsconfig.ClientCA("out/exampleCA.crt"), 
    //验证客户端证书
    tlsconfig.ClientAuthVerified(),
)

客户端设置

客户端 TLS 设置:


import "github.com/x-mod/tlsconfig"

cf := tlsconfig.New(
    //服务端 TLS 证书签名 CA
    tlsconfig.CA("out/exampleCA.crt"), 
    //客户端证书 TLS 证书
    tlsconfig.CertKeyPair("out/client.crt", "out/client.key"), 
)

以上代码是简单的C/S各端的tls.Config对象的设置,C/S程序可以是tcp/http/grpc等各类实现,可自行代码验证。

证书自动化

除了自签发证书,以及购买商业证书外。还可以使用 letsencrypt.org 签发的免费证书。虽然免费证书的实效通常只有三个月,但是可以通过程序实现自动化更新。

自动化签发原理

简单解释一下 letsencrypt.org 签发证书的原理。 letsencrypt.org 共提供了 4 种校验(challenge)方式, 分别是:

  • HTTP-01 challenge
  • DNS-01 challenge
  • TLS-SNI-01 challenge
  • TLS-ALPN-01 challenge

其中校验方式(TLS-SNI-01)由于安全原因已废弃,代替方案就是 TLS-ALPN-01。虽然有多种校验(challenge)方式,但是其基本原理是相同的,即 验证所声明域名资源的可写权

HTTP-01 challenge 过程,首先 acme 客户端向 letsencrypt.org 服务请求一个验证令牌(token), 再将该令牌写入 http://<YOUR_DOMAIN>/.well-known/acme-challenge/<TOKEN>路径。这样 letsencrypt.org 服务通过访问该路径来确认 http 资源的可写权。

DNS-01 challenge 过程,首先 acme 客户端向 letsencrypt.org 服务请求一个验证具体的 DNS TXT 记录值, 并再将记录值添加到_acme-challenge.<YOUR_DOMAIN>解析记录中。这样 letsencrypt.org 服务通过请求 _acme-challenge.<YOUR_DOMAIN>的 TXT 记录值来验证 DNS 资源的可写权。

TLS-ALPN-01 challenge 过程,ALPN (Application Layer Protocol Negotiation)是TLS的扩展,我也不熟不冒充专家,留给读者自己了。不过基础原理是相同的。

每种校验方式的优缺点,可以参考官方文档: challenge-types.

自动化签发代码

对于开发人员而言,快速实现证书自动化,通常会选择 HTTP-01 challenge 方式。具体实现代码非常简单:

package main

import (
	"context"
	"io"
	"log"
	"net/http"
	"syscall"

	"github.com/x-mod/httpserver"
	"github.com/x-mod/routine"
	"github.com/x-mod/tlsconfig"
	"golang.org/x/crypto/acme/autocert"
)

func main() {
	certs := &autocert.Manager{
		Prompt:     autocert.AcceptTOS,
		HostPolicy: autocert.HostWhitelist("your-domain"),
		Cache:      autocert.DirCache("your-local-certs-cache-dir"),
		Email:      "your-email-address",
	}
	srv := httpserver.New(
		httpserver.Address(":80"),
		httpserver.HTTPHandler(certs.HTTPHandler(nil)),
	)

	srvs := httpserver.New(
		httpserver.Address(":443"),
		httpserver.TLSConfig(tlsconfig.New(
			tlsconfig.GetCertificate(certs.GetCertificate),
		)),
		httpserver.HTTPHandler(
			http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
				io.WriteString(w, "Hello, world!\n")
			}),
		),
	)
	if err := routine.Main(
		context.TODO(),
		routine.ExecutorFunc(srvs.Serve),
		routine.Go(routine.ExecutorFunc(srv.Serve)),
		routine.Signal(syscall.SIGINT, routine.SigHandler(func() {
			srv.Close()
			srvs.Close()
		}))); err != nil {
		log.Println(err)
	}
}
    

将以上代码相关配置参数更改为具体配置即可,当然服务运行前,请提前设置好公网IP以及对应的域名指向。

None 命令行 →

评论