Https通信过程
1 客户端发起一个https的请求,把自身支持的一系列Cipher Suite(密钥算法套件,简称Cipher)发送给服务端
2 服务端,接收到客户端所有的Cipher后与自身支持的对比,如果不支持则连接断开,反之则会从中选出一种加密算法和HASH算法
以证书的形式返回给客户端 证书中还包含了 公钥 颁证机构 网址 失效日期等等。
3 客户端收到服务端响应后会做以下几件事
验证证书的合法性
颁发证书的机构是否合法与是否过期,证书中包含的网站地址是否与正在访问的地址一致等证书验证通过后,在浏览器的地址栏会加上一把小锁(每家浏览器验证通过后的提示不一样 不做讨论)生成随机密码
如果证书验证通过,或者用户接受了不授信的证书,此时浏览器会生成一串随机数,然后用证书中的公钥加密。-
HASH握手信息
用最开始约定好的HASH方式,把握手消息取HASH值, 然后用 随机数加密 “握手消息+握手消息HASH值(签名)” 并一起发送给服务端在这里之所以要取握手消息的HASH值,主要是把握手消息做一个签名,用于验证握手消息在传输过程中没有被篡改过。
服务端拿到客户端传来的密文,用自己的私钥来解密握手消息取出随机数密码,再用随机数密码 解密 握手消息与HASH值,并与传过来的HASH值做对比确认是否一致。然后用随机密码加密一段握手消息(握手消息+握手消息的HASH值 )给客户端。
客户端用随机数解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,之后所有的通信数据将由之前浏览器生成的随机密码并利用对称加密算法进行加密因为这串密钥只有客户端和服务端知道,所以即使中间请求被拦截也是没法解密数据的,以此保证了通信的安全
非对称加密算法:RSA,DSA/DSS 在客户端与服务端相互验证的过程中用的是对称加密
对称加密算法:AES,RC4,3DES 客户端与服务端相互验证通过后,以随机数作为密钥时,就是对称加密
HASH算法:MD5,SHA1,SHA256 在确认握手消息没有被篡改时
Android OKHttp Https配置
X509TrustManager:
在JSSE中,证书信任管理器类就是实现了接口X509TrustManager的类。
接口X509TrustManager有下述三个公有的方法需要我们实现:
⑴ void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException
该方法检查客户端的证书,若不信任该证书则抛出异常。由于我们不需要对客户端进行认证,因此我们只需要执行默认的信任管理器的这个方法。JSSE中,默认的信任管理器类为TrustManager。
⑵ void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException
该方法检查服务器的证书,若不信任该证书同样抛出异常。通过自己实现该方法,可以使之信任我们指定的任何证书。在实现该方法时,也可以简单的不做任何处理,即一个空的函数体,由于不会抛出异常,它就会信任任何证书。
⑶ X509Certificate[] getAcceptedIssuers()
返回受信任的X509证书数组。
- 若server使用的证书是由认证的CA机构颁发的,则如此配置即可(OkHttp等均已支持):
private X509TrustManager systemDefaultTrustManager() {
try {
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:"
+ Arrays.toString(trustManagers));
}
return (X509TrustManager) trustManagers[0];
} catch (GeneralSecurityException e) {
throw new AssertionError(); // The system has no TLS. Just give up.
}
}
private SSLSocketFactory systemDefaultSslSocketFactory(X509TrustManager trustManager) {
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] { trustManager }, null);
return sslContext.getSocketFactory();
} catch (GeneralSecurityException e) {
throw new AssertionError(); // The system has no TLS. Just give up.
}
}
- 若使用自签名证书,则客户端需要将server生成的xx.cer放置到assets目录下:
/**
* 设置https certificate
*
* @param context
* @param certificateName must be in assets directory such as "abf.cer"
*/
public void setSSLSocketFactory(Context context, String certificateName) {
try {
CertificateFactory tCertFactory = CertificateFactory.getInstance("X.509");
X509Certificate tJDBCert = readJDBCertFromAsset(context, certificateName, tCertFactory);
X509TrustManager tTrustMgr = newTrustManager(tJDBCert);
SSLContext tSSLContext = newSSLContext(tTrustMgr);
mOkHttpClient.newBuilder().sslSocketFactory(new NoSSLv3SocketFactory(tSSLContext.getSocketFactory()), tTrustMgr);
} catch (CertificateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
}
/**
* xxx.cer证书 -> X509Certificate
*/
private X509Certificate readJDBCertFromAsset(Context context, String certificateName, CertificateFactory pCertFactory) throws IOException, CertificateException {
InputStream caInput = context.getAssets().open(certificateName);
if (caInput == null) {
return null;
}
try {
X509Certificate tCert = (X509Certificate)pCertFactory.generateCertificate(caInput);
tCert.checkValidity();
return tCert;
} finally {
caInput.close();
}
}
/**
* X509TrustManager实例化 Android 自带的一套https认证机制
*/
private X509TrustManager newTrustManager(final X509Certificate privateCert) {
// Accepted CA is only the one installed in the store.
return new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
if (null == chain || 0 == chain.length) {
throw new CertificateException("Certificate chain is invalid.");
}
if (null == authType || 0 == authType.length()) {
throw new CertificateException("Authentication type is invalid.");
}
for (X509Certificate cert : chain) {
// Make sure that it hasn't expired.
cert.checkValidity();
// Verify the certificate's public key chain.
try {
cert.verify(privateCert.getPublicKey());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (NoSuchProviderException e) {
e.printStackTrace();
} catch (SignatureException e) {
e.printStackTrace();
}
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[]{};
}
};
}
/**
* X509TrustManager -> SSLContext
*/
private SSLContext newSSLContext(X509TrustManager pTrustManager) throws NoSuchAlgorithmException, KeyManagementException {
SSLContext tSSLContext = SSLContext.getInstance("TLS");
tSSLContext.init(null, new TrustManager[]{pTrustManager}, null);
return tSSLContext;
}
解释一下代理如何抓取Https请求:
一般我们可以抓取的https请求,server端的证书都是由认证CA颁发的,所以
X509TrustManager可以使用默认的那套,从系统根证书信任列表中去挨个认证。
所以那Charles为例,如何抓取https信息呢? 首先电脑上(由于电脑是代理,相当于服务器)
信任Charles证书,然后在手机上安装Charles证书,两端证书的指纹是一样的。当手机发出
https请求时,根据https通信过程,电脑会把charles的证书下发给手机,而手机上由于之前
安装了Charles证书显然可以验证通过,所以手机端会生成对称秘钥,从此以它进行加解密。但
是,对于自签名的证书,由于使用的X509TrustManager是自定义的,也就是只能验证自签名
证书,不会向手机的根信任列表去挨个校验,所以这种方式无法抓取https。