最近想实现一个旧项目https的防抓包功能,重新学习并且配置了下https相关通信知识,参考了不少文章,有些文章比较旧不全或者有误,走了不少弯路和坑,所以整理出来方便自己巩固以及供大家参考,指出不足或者有误之处互相学习。
本文的服务端环境:
Debian 9.8
Nginx
原创文章,欢迎转载,转载请注明:ifish.site
作者:JaydenZhou
本文需要的前置知识点,这也是我刚接触时候绕得很晕的问题,自己前后端流程走一遍后,就清晰多了。
网络相关的知识点:
https通信,CA发证机构,自签发机构,公钥,私钥,数字证书,数字签名、相关cer,pem,scr,key等关键字定义。
Android相关的知识点:
如何发起网络请求,如何设置单向/双向认证ssl,如何把一些数字证书转成Android识别的bks格式证书等。
后端相关知识点:
Liunx基本知识;如何域名解析(或直接ip)访问;https的CA证书/自签名证书如何生成;如何配置nginx中的https访问;
https的通信,是从非对称加密(RSA)到对称加密(AES)的一个过程,其中数字证书扮演着重要作用。 借助文章:https://juejin.im/post/5c9cbf1df265da60f6731f0a 里面所讲,
数字证书 = 公钥 + 签名 + 申请者和颁发者的信息
签名 = 私钥 + 信息摘要(hash处理过的不可逆的明文信息)
类比于我们的身份证(数字证书) = 证件号(公钥) + 公安盖章(签名) + 个人姓名/发证公安局(申请者和颁发者的信息)。
私钥一般以.key结尾,用它才能跟对应的数字证书(公钥)互相解密。
单向认证: 这里有个简单的理解,凡是你可以直接访问的网站(比如我的域名: https://ifish.site ); 直接请求的https的api等,都是单向认证,因为它只需要client端能够解密出server端的数字证书(分CA和自签发的),操作系统或者浏览器一般都内置了一堆相关CA的证书,所以可以直接访问;若是自签发的,浏览器会提示该证书不受信任。适用场景是站点访问,非高机密数据传输。
双向认证: 顾名思义,就是在单向基础上,添加上了服务端要校验客户端的公钥,客户端要自己保存着自己的私钥来加密,客户端发过来的请求要用该私钥来加密后,才能跟服务器进行完整通信。适用场景:企业间对应机密api接口的数据传输。
配置好Linux环境后,首先我们要生成对应的证书,两种方法如下:
方法一:用CA签的证书(有收费 or 免费),这样浏览器就不会显示不信任提示,前提是要有合法的境内实名域名,然后比如在阿里云服务器管理后台界面进行证书的申请。
方法二:用openssl在自己服务器上,制作自签发的证书,浏览器也可以访问,但是会有不信任提示。
方法一按照对应服务商的提示来操作就行,这里讲下openssl来生成的方法,先抛出一个我自己现在也疑惑的问题:
为何要生成root根CA证书,然后再发布二级server和client证书? 是为了一个根证书可以直接管理多个二级证书?
本文为了简化演示https单向/双向认证,只需要生成对应的 server 和 client 相关证书就行,避免文件太多导致像我这样的新手造成的困惑和配置出错。
1.生成服务端key:
openssl genrsa -out server-key.key 1024
2.生成服务端证书请求文件(这步很关键,弹出信息填写提示时候,“Common Name”一定要填写你自己的域名,其他的可以直接回车):
openssl req -new -out server-req.csr -key server-key.key
3.生成服务端证书cer:
openssl x509 -req -in server-req.csr -out server-cert.cer -signkey server-key.key -CAcreateserial -days 3650
4.生成客户端key(同上面方法一样):
openssl genrsa -out client-key.key 1024
5.生成服务端证书请求文件(Common Name最好一致):
openssl req -new -out client-req.csr -key client-key.key
6.生成客户端证书cer:
openssl x509 -req -in client-req.csr -out client-cert.cer -signkey client-key.key -CAcreateserial -days 3650
7.生成客户端带密码的p12证书(这步很重要,双向认证的话,浏览器访问时候要导入该证书才行;Android请求的时候也需要把它转成bks来请求双向认证):
openssl pkcs12 -export -clcerts -in client-cert.cer -inkey client-key.key -out client.p12
1.将生成的证书,为了方便管理,建议放到nginx相同目录下,比如我是放到“/usr/local/nginx/conf/ssl_cust” 里面;
2.打开对应的 conf 里面要https访问的域名,如果之前有配过443端口,那么只需要指定下对应的证书即可,其中
单向认证是:
ssl_certificate ssl_cust/server-cert.cer;
ssl_certificate_key ssl_cust/server-key.key;
双向认证是:
ssl_certificate ssl_cust/server-cert.cer;
ssl_certificate_key ssl_cust/server-key.key;
ssl_client_certificate ssl_cust/client-cert.cer;
ssl_verify_client on;
3.若完全没有配过https的话,可以参考我的配置:
server
{
listen 443 ssl;
server_name ifish.site www.ifish.site;
ssl on;
ssl_certificate ssl_cust/server-cert.cer;
ssl_certificate_key ssl_cust/server-key.key;
# 双向认证一般不开启, #是注释掉
# ssl_client_certificate ssl_cust/client-cert.cer;
# ssl_verify_client on;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
}
4.保存关闭后,执行以下命令让nginx重启生效:
sudo nginx -s reload
5.如此我们配置就生效了,可以用浏览器来验证下,如果只是单向认证,直接浏览器输入域名来访问即可,会有不安全提示;
若是双向认证,直接访问会出现 400 Bad Requst, 需要我们手动添加证书,这里是Chrome下的截图,我们需要添加之前我们生成的 client.p12 文件。
终于到Android请求的代码写法了,为了简化Demo的独立访问,这里引入了 xUtils库 (https://github.com/wyouflf/xUtils3 ),当然你也可以自己手写或者用比如Retrofit、OkHttp等网络库。
单向认证的写法:
其实单向认证,用xUtils的话可以不需要设置setSslSocketFactory内容,因为库代码DefaultParamsBuilder.java里面判断了如果没有设置自定义ssl,那么就直接用操作系统自带的进行返回,从而来访问https。
我们这里为了演示下具体代码,所以自定义一个ssl的构造出来,操作如下:
把服务端server-cert.cer证书放到 assets 目录下,然后创建一个 SSLHelper.java 的辅助类:
public static SSLSocketFactory getSSLSingleFactory(Context context) {
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
InputStream is = context.getAssets().open("server-cert.cer");
keyStore.setCertificateEntry("0", certificateFactory.generateCertificate(is));
if(is != null) {
is.close();
}
SSLContext sslContext = SSLContext.getInstance("TLS");
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
return sslContext.getSocketFactory();
} catch (CertificateException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
return null;
}
对应的Activity请求里的代码是:
RequestParams params = new RequestParams("https://ifish.site");
params.setSslSocketFactory(SSLHelper.getSSLSingleFactory(this));
// 因为是自签不受权威机构认证,所以绕过不检查域名ssl。
params.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
return true;
}
});
x.http().get(params, new Callback.CommonCallback() {
@Override
public void onSuccess(String result) {
Log.d(TAG, "onSuccess...result = " + result);
}
@Override
public void onError(Throwable ex, boolean isOnCallback) {
Log.d(TAG, "onError...ex = " + ex.getMessage());
}
@Override
public void onCancelled(CancelledException cex) { }
@Override
public void onFinished() { }
});
双向认证的写法:
双向认证要求的是客户端也需要持有一份自己的私钥key、服务端要有一份客户端的公钥证书。但是由于Android系统限制,我们需要把client.p12转成client.bks格式,才能被访问到。介绍一个转化工具,叫做“Portecle”,亲测可用的下载和使用链接如下:https://blog.csdn.net/zhangyong125/article/details/50402183,也可以自己去官网免费下载,如果是Linux系统, 可以用 java -jar protecle.jar 来运行,然后按照上文链接的使用方法,把对应的 client.p12转成client.bks格式。
对应的 SSLHelper.java添加一个双向认证方法:
public static SSLSocketFactory getSSLDoubleFactory(Context context) {
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null);
InputStream is = context.getAssets().open("server-cert.cer");
keyStore.setCertificateEntry("0", certificateFactory.generateCertificate(is));
if(is != null) {
is.close();
}
SSLContext sslContext = SSLContext.getInstance("TLS");
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
// 初始化双向客户端keyStore
KeyStore clientKeyStore = KeyStore.getInstance("BKS");
clientKeyStore.load(context.getAssets().open("client.bks"), "123456".toCharArray());
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(clientKeyStore, "123456".toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
return sslContext.getSocketFactory();
} catch (CertificateException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (UnrecoverableKeyException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
}
return null;
}
然后Activity里面,params.setSslSocketFactory(SSLHelper.getSSLSingleFactory(this)); 换成 params.setSslSocketFactory(SSLHelper.getSSLDoubleFactory(this)); 即可。
疑难点:
1.涉及的知识点比较多,很多不是Android本身的东西;
2.https单向/双向原理不太好理解,可以看该文https://juejin.im/post/5c9cbf1df265da60f6731f0a;
3.仅验证单双向认证来说,没必要生成根CA证书,部分文章生成太多证书会导致配置上容易乱。
调试验证技巧:
原Android网络请求框架庞大,对应的域名是生产环境的域名,绝对不能随便动后台的生产环境配置。因此需要自己的一台服务器,自己搭建一个简单的后台Demo https请求,以及Android的Demo网络请求app,从而方便Debug。
参考:
https://www.zhihu.com/question/29620953 – SSL中,公钥、私钥、证书的后缀名都是些啥?
https://juejin.im/post/5c9cbf1df265da60f6731f0a – 扯一扯HTTPS单向认证、双向认证、抓包原理、反抓包策略
https://zhuanlan.zhihu.com/p/60392573 – 为了抓包某app,我折腾了10天,原来他是用SSL Pinning防抓包的
https://www.cnblogs.com/yelao/p/9486882.html – Nginx https 双向认证
https://blog.csdn.net/jsc702325/article/details/76724010 – Android 用自签名证书实现https请求
https://www.cnblogs.com/guogangj/p/4118605.html – 那些证书相关的玩意儿(SSL,X.509,PEM,DER,CRT,CER,KEY,CSR,P12等)
https://blog.csdn.net/zhangyong125/article/details/50402183 – P12证书转BKS证书