上一篇用netty實現的http代理服務器還無法對https報文進行解密,原因也説了,就是服務器的私鑰不在我們這,根據RSA公鑰加密私鑰解密的特性,如果我們沒有私鑰的話是不可能獲取到https的真實內容的,那有沒有什麼辦法解密https的報文呢,當然有通過代理服務器偽造ssl證書就可以達到目的,那麼具體是什麼原理的,下面一步一步來分析。
https協議
首先來回顧下https協議的ssl握手

簡單敍述下ssl握手中,只説説ssl單向驗證過程
- 客户端向服務器發出ssl握手,發送client-random隨機數。
- 服務器返回ssl證書和server-randon隨機數。
- 客户端校驗ssl證書,校驗通過,再生成一個premaster-secret隨機數用服務器證書裏的公鑰加密發送,這個時候客户端已經可以通過三個隨機數算出對稱加密的密鑰了。
- 服務器用私鑰解密premaster-secret,也拿到了三個隨機數算出對稱加密的密鑰。
- 兩邊都用算出來的對稱密鑰進行報文加密和解密
注意第三步非常關鍵,ssl證書是採用信任鏈的方式來驗證ssl證書是否有效,在瀏覽器中都會內置好許多受信任的CA證書,而CA證書下籤發的ssl證書來訪問瀏覽器才會驗證通過,不然就會提示證書不安全(12306典型的例子,因為12306是自己簽發的CA根證書,不存在於瀏覽器受信任的證書列表中,所以瀏覽器會提示不安全)。所以就是要製作一個服務器證書,然後還要讓瀏覽器安全校驗通過才行。
動態替換ssl證書
在代理服務器拿到目標服務器ssl證書的時候,不返回給瀏覽器,而是使用我們自己製作的ssl證書,這個時候有個問題,就是自己製作的證書並不是瀏覽器中CA根證書籤發的,所以我們需要把自己製作的CA根證書加入瀏覽器受信任的根證書頒發機構。

代理服務器返回的ssl證書中是由剛剛的CA證書籤發的,這樣就形成了一個受信任鏈,其實代理服務器就相當於一個瀏覽器訪問目標目標服務器,獲取到明文之後在通過客户端和代理服務器建立的ssl連接再轉發給瀏覽器,就可以捕獲並攔截到明文了。
openssl製作CA根證書
生成java支持的私鑰
openssl genrsa -des3 -out ca.key 2048
openssl pkcs8 -topk8 -nocrypt -inform PEM -outform DER -in ca.key -out ca_private.pem
再通過CA私鑰生成CA證書
openssl req -sha256 -new -x509 -days 365 -key ca.key -out ca.crt \
-subj "/C=CN/ST=GD/L=SZ/O=lee/OU=study/CN=ProxyeeRoot"
上面我們就製作了一個自己的CA證書,打開ca.crt來看一看
這裏我們就擁有了CA根證書的私鑰,只要在程序中通過這個私鑰就能向下簽發服務器證書了,如圖代理服務器簽發了一個www.baidu.com的ssl證書

java動態簽發ssl證書
JAVA自帶的SSL以及X509庫只能使用SSL證書,不能生成SSL證書。因此我們使用“Bouncy Castle”這個算法庫來實現SSL證書的生成。
//maven
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.49</version>
</dependency>
//註冊bouncycastle
Security.addProvider(new BouncyCastleProvider());
//生成ssl證書公鑰和私鑰
KeyPairGenerator caKeyPairGen = KeyPairGenerator.getInstance("RSA", "BC");
caKeyPairGen.initialize(2048, new SecureRandom());
PrivateKey serverPriKey = keyPair.getPrivate();
PublicKey serverPubKey = keyPair.getPublic();
//通過CA私鑰動態簽發ssl證書
public static X509Certificate genCert(String issuer, PublicKey serverPubKey, PrivateKey caPriKey, String host) throws Exception {
X509V3CertificateGenerator v3CertGen = new X509V3CertificateGenerator();
String issuer = "C=CN, ST=GD, L=SZ, O=lee, OU=study, CN=ProxyeeRoot";
String subject = "C=CN, ST=GD, L=SZ, O=lee, OU=study, CN=" + host;
v3CertGen.reset();
v3CertGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
v3CertGen.setIssuerDN(new X509Principal(issuer));
v3CertGen.setNotBefore(new Date(System.currentTimeMillis() - 10 * ONE_DAY));
v3CertGen.setNotAfter(new Date(System.currentTimeMillis() + 3650 * ONE_DAY));
v3CertGen.setSubjectDN(new X509Principal(subject));
v3CertGen.setPublicKey(serverPubKey);
//SHA256 Chrome需要此哈希算法否則會出現不安全提示
v3CertGen.setSignatureAlgorithm("SHA256WithRSAEncryption");
//SAN擴展 Chrome需要此擴展否則會出現不安全提示
GeneralNames subjectAltName = new GeneralNames(new GeneralName(GeneralName.dNSName, host));
v3CertGen.addExtension(X509Extensions.SubjectAlternativeName, false, subjectAltName);
X509Certificate cert = v3CertGen.generateX509Certificate(caPriKey);
return cert;
}
至此我們最重要的功能已經實現了,接着就是拿着ssl證書返回給客户端即可捕獲明文了。
效果
這裏我攔截了訪問https://www.baidu.com的請求,並在加上了一對響應頭。

這樣就以動態簽發ssl證書方法,解決了https明文捕獲的問題。此外還添加了對websocket的支持,並且提供攔截器對外使用,實現上面效果的代碼如下:
new NettyHttpProxyServer().initProxyInterceptFactory(() -> new HttpProxyIntercept() {
@Override
public boolean afterResponse(Channel clientChannel, Channel proxyChannel, HttpResponse httpResponse) {
//攔截響應,添加一個響應頭
httpResponse.headers().add("intercept","test");
return true;
}
}).start(9999);
後記
希望通過一個sdk的方式把http代理服務器開源出去供大家使用,源碼託管在github,歡迎start。
筆者現在用這個sdk嗅探http下載請求,然後對於支持http斷點下載的文件,採用多連接分段下載,想體驗可以在這裏看看。
當然也可以做到類似fiddler的自動替換請求體、響應體功能,具體就看大家的業務需求了!