做个笔记。
对TCP连接进行SSL/TLS加密时,应当注意这个问题。
前言
我们用Java代码示范,来理一下SSL/TLS加密时的常见问题。
// 创建一个ssl上下文,用来管理ssl相关配置
// 这里可以自定义证书的合法性校验,或者直接使用默认的实现
// 想要忽略证书问题,校验时直接返回true即可
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, null, new SecureRandom());
// sslContext.init(getKeyManager(), getTrustManager(), new SecureRandom());
SSLSocketFactory factory = sslContext.getSocketFactory();
// 创建一个socket连接
Socket socket = new Socket();
socket.connect(new InetSocketAddress(host, port), timeout);
// 在现有socket上, 再套一层加密
// 因为有的服务器上绑定了很多域名, 我们需要告知它我们的目标是什么, 以返回正确的域名证书
SSLSocket sslSocket = (SSLSocket)factory.createSocket(socket, sni, port, true);
// 接下来, 进行正常的输入输出
InputStream in = sslSocket.getInputStream();
OutputStream out = sslSocket.getOutputStream();
// ...
sslSocket.close();
综上, 根据经验,常见的操作是:
- 跳过证书有效性检查; 或自定义证书校验, 比如SSL pinning
- SSL双向认证, 客户端也要添加证书
- 设置指定SNI
我想当然地认为证书有效性检查默认就包含了域名与证书是否匹配的验证。
然而并不是。
广义上的证书有效性检查分为两部分:
- 狭义上的证书有效性检查
- 证书和域名是否匹配
所有HTTPS请求工具默认都是广义上的检查,但部分SSL/TLS工具默认只进行狭义上的检查。
这意味着,你不注意的话,很可能经受MITM攻击而不自知。
比如,你使用SSLSocket的默认设置建立到A域名的加密连接, 中间人只要使用有效的B域名的证书, 即可窃听甚至篡改该连接内容。
下面来记一下各种情况下的域名校验实现
Java
SSLSocket
// ...
SSLSocket sslSocket = (SSLSocket)factory.createSocket(socket, sni, port, true);
final Certificate[] certs = sslSocket.getSession().getPeerCertificates();
final X509Certificate x509 = (X509Certificate) certs[0];
for(List<?> entry: x509.getSubjectAlternativeNames()) {
if((Integer)entry.get(0) == 2) {
String san = entry.get(1).toString();
if(san.equals(sni))
return true;
if(san.startsWith("*") && sni.endsWith(san.substring(1)))
return true;
}
}
// 接下来, 进行正常的输入输出
InputStream in = sslSocket.getInputStream();
OutputStream out = sslSocket.getOutputStream();
// ...
Netty SslHandler
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// ...
// 假设已经通过各种方式实现了 SslHandler handler
// https://netty.io/4.1/api/io/netty/handler/ssl/SslContext.html#newHandler-io.netty.buffer.ByteBufAllocator-java.util.concurrent.Executor-
SSLEngine sslEngine = handler.engine();
SSLParameters sslParameters = sslEngine.getSSLParameters();
// only available since Java 7
sslParameters.setEndpointIdentificationAlgorithm("HTTPS");
sslEngine.setSSLParameters(sslParameters);
pipeline.addLast("ssl", handler);
// ...
}
Go
const(
insecureSkipVerify = false
serverName = "www.baidu.com"
remoteAddr = "www.baidu.com:443"
)
conn2server, err := net.Dial("tcp", remoteAddr)
if err != nil {
panic(err)
}
conn2server = tls.Client(conn2server, &tls.Config{
InsecureSkipVerify: insecureSkipVerify,
ServerName: serverName,
// 通过VerifyConnection函数实现域名校验
// 该函数只要不为nil, 总会在默认证书校验完毕后执行
VerifyConnection: func(connState tls.ConnectionState) error {
if insecureSkipVerify {
return nil
}
return connState.PeerCertificates[0].VerifyHostname(serverName)
},
})
Python
就目前看来, Python asyncio的逻辑最符合我的思维习惯。
指定了sni后, 它默认是会检查证书域名是否匹配的。
verify = True
host, port = ("www.baidu.com", 443)
sni = "www.baidu.com"
ssl_context = True if verify else ssl._create_unverified_context()
reader, writer = await asyncio.open_connection(host, port, server_hostname=sni,ssl=ssl_context)
# ...