传输层加密实战:证书生成与双向认证

讲传输层加密,光看理论是不行的。我早年刚接触TLS时,照着文档折腾了一整天,结果客户端连不上,报了个“tls: bad certificate”——其实就是证书链没配对。从那以后我做任何加密项目,一定把证书生成、配置、验证这套流程手打一遍。今天咱们就来走完全程:从OpenSSL命令到双向认证服务器。

1. 理解传输层加密与双向认证

单向认证大家很熟:客户端验证服务器证书,确保连接对象的身份。双向认证就是反过来再加一道——服务端也要验证客户端证书。说白了,你们俩得互相亮身份证。

特性单向认证双向认证
客户端验证服务器✅✅
服务器验证客户端❌✅
适用场景HTTPS网站API间通信、IT设备、金融接口
客户端需要证书不需要需要

我个人习惯在Tokenization接口中用双向认证——既能加密信道,又能确保调用方身份。当然,证书管理成本也高一些,不过安全嘛,值得。

2. 准备OpenSSL环境

基本上你的Linux/Mac都自带OpenSSL。检查版本:

openssl version

我建议至少1.1.1以上,支持TLS 1.3。如果版本太旧,记得升级——我以前被旧版OpenSSL坑过,生出的证书兼容性出问题。

3. 生成CA根证书

证书链的起点。CA相当于“发证机关”,自己给自己签。步骤很简单:

  1. 生成CA私钥
  2. 用私钥自签名根证书
# 生成CA私钥(2048位RSA)
openssl genrsa -out ca.key 2048

# 自签名根证书,有效期10年
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
  -out ca.crt \
  -subj "/C=CN/ST=Beijing/L=Beijing/O=MyOrg/CN=MyRootCA"
避坑:我曾经把CA证书有效期设成1年,结果一年后所有子证书都失效——CA过期意味着用它签的所有证书都不可信。所以CA证书有效期最好设长一些,比如10年,甚至更长。

4. 生成服务器证书

服务器证书用于“证明你是这个域名/IP”。步骤:生成私钥 → 生成证书签名请求(CSR) → 用CA给CSR签名。

# 生成服务器私钥
openssl genrsa -out server.key 2048

# 生成CSR(注意CN填服务器域名或IP)
openssl req -new -key server.key -out server.csr \
  -subj "/C=CN/ST=Beijing/L=Beijing/O=MyOrg/CN=api.myservice.com"

# 用CA签名,生成服务器证书(有效期2年)
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out server.crt -days 730 -sha256

嗯,这里要注意SAN(Subject Alternative Name)。老浏览器只认CN的时代过去了,现在必须加SAN。我习惯用扩展配置文件来加SAN:

# server.ext 内容
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = api.myservice.com
DNS.2 = localhost
IP.1 = 127.0.0.1
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out server.crt -days 730 -sha256 -extfile server.ext

5. 生成客户端证书(双向认证)

客户端证书和服务器流程几乎一样,区别在于用途是clientAuth。我一般把它们放在一个目录下方便管理。

# 客户端私钥
openssl genrsa -out client.key 2048

# 客户端CSR
openssl req -new -key client.key -out client.csr \
  -subj "/C=CN/ST=Beijing/L=Beijing/O=MyOrg/CN=client-001"

# 签名客户端证书
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key \
  -CAcreateserial -out client.crt -days 365 -sha256 \
  -extfile <(echo "extendedKeyUsage=clientAuth")
小贴士:客户端证书的CN可以放唯一标识,比如设备ID或用户名。这样服务端收到证书后,直接从CN里取出身份信息,省得再传。

6. 配置TLS服务器(双向认证)

我拿Go写个简单示例,因为Go标准库crypto/tls支持双向认证很直接。Nginx、Gin、Spring Boot也都类似,原理一样。

package main

import (
    "crypto/tls"
    "crypto/x509"
    "io/ioutil"
    "log"
    "net/http"
)

func main() {
    // 加载CA证书,用于验证客户端
    caCert, _ := ioutil.ReadFile("ca.crt")
    caCertPool := x509.NewCertPool()
    caCertPool.AppendCertsFromPEM(caCert)

    // 加载服务器证书和私钥
    serverCert, _ := tls.LoadX509KeyPair("server.crt", "server.key")

    // TLS配置:强制双向认证
    tlsConfig := &tls.Config{
        Certificates: []tls.Certificate{serverCert},
        ClientCAs:    caCertPool,
        ClientAuth:   tls.RequireAndVerifyClientCert,
        MinVersion:   tls.VersionTLS12,
    }

    // 启动HTTPS服务器
    server := &http.Server{
        Addr:      ":8443",
        TLSConfig: tlsConfig,
        Handler:   http.HandlerFunc(handler),
    }

    log.Fatal(server.ListenAndServeTLS("", "")) // 证书已加载到TLSConfig,传空
}

func handler(w http.ResponseWriter, r *http.Request) {
    // 从请求中提取客户端证书信息
    if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
        cert := r.TLS.PeerCertificates[0]
        w.Write([]byte("Hello, " + cert.Subject.CommonName))
    } else {
        w.Write([]byte("No client cert"))
    }
}

关键参数:ClientAuth = tls.RequireAndVerifyClientCert。少了这行,服务端就不会要求客户端出示证书,双向认证就废了。

7. 验证双向认证

用curl测试,必须带上客户端证书和私钥,同时指定信任的CA(用来验证服务器证书):

curl -v --cacert ca.crt \
  --cert client.crt \
  --key client.key \
  https://localhost:8443/

如果一切正常,你会看到服务端返回“Hello, client-001”。我遇到过最坑的情况是客户端证书路径写错了,curl报“curl: (58) unable to set private key file”。另一个常见问题是服务器证书没带SAN,现代Go/tls会直接拒绝——你想想看,产品环境里这得多尴尬。

双向认证TLS握手流程 客户端 服务端 ClientHello(支持的密码套件) ServerHello + 服务器证书 + CertificateRequest 验证服务器证书 客户端证书 + ClientKeyExchange 验证客户端证书 Finished(加密通信建立) 加密通道(AES-GCM / ChaCha20-Poly1305)
核心总结:双向认证证书链是CA自签 → 服务器证书/客户端证书。服务器配置RequireAndVerifyClientCert是关键。我曾经在生产环境把ClientAuth写成了RequestClientCert,结果客户端不传证书也能访问——排查了半天。所以你配置完一定用curl手动验证,确保不传客户端证书时服务端会拒绝。