为了保证网络程序间的通信安全,需要对程序间的网络通道进行数据加密。在数据加密之前,还需要对通信双方的身份进行验证。通过一个双方互信的证书签发中心,分别签发证书,在双方首次握手过程中,对对方的证书进行校验,校验通过则继续通信,否则断开链接。这就是基础的 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以及对应的域名指向。