用 Charles 截取一些 Android 和 iOS应用的请求数据,发现好多 https 的请求无线显示内容了,只是显示个红色的 unknown
这是因为 Android 从 6.0 之后加强了系统安全性,ssl 证书的验证由系统级别精确到了应用级别,所以即使安装了 Charles 的根证书依然无法看到 https 的内容
但 iOS 还没有像 Android 这么做,但也有一些应用无法看到 https 了,一番搜索后了解到原来有些应用采用了 SSL Pinning 的技术,简单来说就是在建立 ssl 连接的时候对证书做了验证,如果发现证书和请求的域名不匹配的时候就拒绝连接了,所以看到的 unkown 其实是说明这次请求根本没有发送成功,这就牵涉到了 ssl 三次握手的一些知识,可以理解为在建立 ssl 连接的时候服务器和客户端之间会进行几次身份验证,采用的是 RSA 加密, 其中私钥在服务器容器中配置好了,证书信息(包含公钥)会在客户端请求建立 https 连接的时候拿到,至于具体的握手建立连接的过程这里就不详谈了,有兴趣的可以去看其他同学的文章
既然了解了问题的原因,我们可以在本地进行一个简单的测试来验证一下,用 java写一个http客户端和线上的 https 服务建立连接,然后验证服务器公钥,就拿https://www.baidu.com来测试吧,首先我们可以先看一下百度的 https 证书,通过 chrome 访问https://www.baidu.com,点击地址栏左上角的小锁,然后点击证书,就能看到证书信息了
接下来我们把证书拖到一个文件夹里,这样就获取到一个 .cer 格式的证书文件,这个证书包含了很多信息,我们主要是要取到证书里的公钥信息,通过 openssl 工具来获取,命令如下
openssl x509 -inform der -in /xxx/xxx.cer -pubkey -noout > public_key.pem
这样就把证书里的公钥导出来了,文件打开后就是下面这个样子:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtMa/2lMgD+pA87hSF2Y7
NgGNErSZDdObbBhTsRkIsPpzRz4NOnlieGEuVDxJfFbawL5hVdVCcGoQvvW9jWSW
IQCTYwmHtxm6DiA+SchT7QKPRgHroQeTc7vt8bPJ4vvd8Dkqg630QZi8huq6dKim
49DlxY6zC7LSrJF0Dv+AECM2YmUItIf1VwwlxwDY9ahduDNBpypf2/pwniG7rkIW
Zgdp/hwmKoEPq3Pj1lIgpG2obNRmSKRv8mgKxWWhTr8EekBDHNN1+3WsGdZKNQVu
z9Vl0UTKawxYBMSFTx++LDLR8cYo+/kmNrVt+suWoqDQvPhR3wdEvY9vZ8DUr9nN
wwIDAQAB
-----END PUBLIC KEY-----
这个就是百度证书的公钥了,然后我们接下来写一个 http 客户端
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import com.bedrock.util.encrypt.Base64;
/**
* HttpKit
*/
public class HttpsTest {
private static final SSLSocketFactory sslSocketFactory = initSSLSocketFactory();
private static SSLSocketFactory initSSLSocketFactory() {
try {
TrustManager[] tm = { new HttpsTest().new OptionTrustManager() };
SSLContext sslContext = SSLContext.getInstance("TLS", "SunJSSE");
sslContext.init(null, tm, new java.security.SecureRandom());
return sslContext.getSocketFactory();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* https 证书管理
*/
private class OptionTrustManager implements X509TrustManager {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
for (X509Certificate x509Certificate : chain) {
String principal = x509Certificate.getSubjectX500Principal().getName();
String pubkey = new String(Base64.encode(x509Certificate.getPublicKey().getEncoded(), Base64.DEFAULT));
if (principal.contains("baidu.com")) {
if (!pubkey.contains("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtMa/2lMgD+p")) {
throw new CertificateException("error public key");
}
}
System.out.println("domain:" + principal + " public key:\n" + pubkey);
}
}
}
public static void main(String[] args) {
String responseStr = get("https://www.baidu.com");
System.out.println(responseStr.length());
}
public static HttpURLConnection getHttpConnection(String url)
throws IOException, NoSuchAlgorithmException, NoSuchProviderException, KeyManagementException {
URL _url = new URL(url);
HttpURLConnection conn = (HttpURLConnection) _url.openConnection();
if (conn instanceof HttpsURLConnection) {
((HttpsURLConnection) conn).setSSLSocketFactory(sslSocketFactory);
}
conn.setRequestMethod("GET");
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setConnectTimeout(30000);
conn.setReadTimeout(30000);
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
conn.setRequestProperty("User-Agent",
"Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.146 Safari/537.36");
return conn;
}
/**
* Send GET request
*/
public static String get(String url) {
HttpURLConnection conn = null;
try {
conn = getHttpConnection(url);
conn.connect();
return readResponseString(conn);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
private static String readResponseString(HttpURLConnection conn) {
StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
try {
inputStream = conn.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
String line = null;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
}
return sb.toString();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
主要关注下 * OptionTrustManager 的 checkServerTrusted * 这个方法,我们在自定义的 X509TrustManager 中对证书进行了简单的验证,取了证书的一部分内容做校验(这个只是例子,线上使用的话最好做全部字符的校验)
好了,客户端也有了,接下来我们用 Charles 来验证下,看加上这个验证后 charles 还能不能截取到我们的通信内容了(这里 java 代码相当于一个客户端,baidu 相当于服务器)
首先,我们开启 Charles 的 macOS Proxy来获取系统的 http 通信内容.如下图(开启 macOS Proxy 需要安装根证书,安装方法看下面第二张图)
首先开启 macOS Proxy,接下来我们运行下 main 方法,发现现在访问 baidu.com 显示的是 unknown 了,再看 console 输出了异常信息,公钥验证不通过
这是因为 Charles 是作为中间人来劫持我们的请求的,我们访问 baidu.com 实际是先访问了 Charles 服务,然后 Charles 服务再去跟 baidu.com 进行交互,完了再把请求数据返回给我们,所以我们拿到的公钥信息是 Charles 的根证书的,自然是跟 baidu 的证书不匹配的,所以就不会再发起请求了
然后关闭 macOS Proxy,再次运行 main 方法,可以看到这次没有抛异常,请求到了 baidu.com 的页面内容,而且打印了两个证书信息,其中第一个证书是 baidu 的,第二个是 CA 的证书
有一点需要注意,这里验证的是证书的公钥信息,正式情况下,还需要验证证书的过期时间等信息,验证公钥是为了再证书过期后不至于客户端留的证书和新的证书不一致,只要我们的服务器私钥不变,生成的证书的公钥信息也就不会变
到此,我们已经对 unknown 有了比较清晰的认识,以上只是自己个人浅显的理解,如有纰漏还望指正