JAVA寫HTTP代理服務器(三)-https明文捕獲

上一篇用netty實現的http代理服務器還沒法對https報文進行解密,緣由也說了,就是服務器的私鑰不在咱們這,根據RSA公鑰加密私鑰解密的特性,若是咱們沒有私鑰的話是不可能獲取到https的真實內容的,那有沒有什麼辦法解密https的報文呢,固然有經過代理服務器僞造ssl證書就能夠達到目的,那麼具體是什麼原理的,下面一步一步來分析。java

## https協議
首先來回顧下https協議的ssl握手git

ssl握手

簡單敘述下ssl握手中,只說說ssl單向驗證過程

  1. 客戶端向服務器發出ssl握手,發送client-random隨機數。
  2. 服務器返回ssl證書和server-randon隨機數。
  3. 客戶端校驗ssl證書,校驗經過,再生成一個premaster-secret隨機數用服務器證書裏的公鑰加密發送,這個時候客戶端已經能夠經過三個隨機數算出對稱加密的密鑰了。
  4. 服務器用私鑰解密premaster-secret,也拿到了三個隨機數算出對稱加密的密鑰。
  5. 兩邊都用算出來的對稱密鑰進行報文加密和解密

注意第三步很是關鍵,ssl證書是採用信任鏈的方式來驗證ssl證書是否有效,在瀏覽器中都會內置好許多受信任的CA證書,而CA證書下籤發的ssl證書來訪問瀏覽器纔會驗證經過,否則就會提示證書不安全(12306典型的例子,由於12306是本身簽發的CA根證書,不存在於瀏覽器受信任的證書列表中,因此瀏覽器會提示不安全)。因此就是要製做一個服務器證書,而後還要讓瀏覽器安全校驗經過才行。github

動態替換ssl證書

動態替換ssl證書
在代理服務器拿到目標服務器ssl證書的時候,不返回給瀏覽器,而是使用咱們本身製做的ssl證書,這個時候有個問題,就是本身製做的證書並非瀏覽器中CA根證書籤發的,因此咱們須要把本身製做的CA根證書加入瀏覽器受信任的根證書頒發機構。web

QQ截圖20171031155636.png

代理服務器返回的ssl證書中是由剛剛的CA證書籤發的,這樣就造成了一個受信任鏈,其實代理服務器就至關於一個瀏覽器訪問目標目標服務器,獲取到明文以後在經過客戶端和代理服務器創建的ssl鏈接再轉發給瀏覽器,就能夠捕獲並攔截到明文了。算法

openssl製做CA根證書

生成java支持的私鑰segmentfault

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證書服務器

QQ截圖20171031172306.png

java動態簽發ssl證書

JAVA自帶的SSL以及X509庫只能使用SSL證書,不能生成SSL證書。所以咱們使用「Bouncy Castle」這個算法庫來實現SSL證書的生成。websocket

//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的請求,並在加上了一對響應頭。

QQ截圖20171031173455.png

這樣就以動態簽發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的自動替換請求體、響應體功能,具體就看你們的業務需求了!

相關文章
相關標籤/搜索