RSA那些坑

(注:因gongji是敏感詞,改爲拼音或者attack了)java

1.背景算法


最近生產上出了兩個由於RSA加解密緻使的性能問題,一個是加密引發的,一個是解密引發的,都是大量線程在進行RSA操做時等鎖,線程被block,請求都被堆在Weblogic隊列裏不能處理。爲此,我就研究了一下RSA加解密在Java裏的實現,發現加解密的問題都出如今同一個地方。由於RSA加解密使用的方法都是同一個,就是計算ae mod n,也就是模冪運算,在作這個運算的時候,爲了防範gongji,須要加入隨機因素,尋找一個blinding parameter,讓gongji者沒法破解私鑰,就是在尋找這個blinding parameter的時候線程出現了堵塞。下面的內容會作詳細闡述。數組


另外,兩個問題出現的jdk版本不同,第一個是1.6,第二個是1.7,都是同一個方法,可是底層實現不太同樣。緩存


2.問題描述
安全



2.1 第一個問題:動態菜單簽名服務器


客戶端的菜單不是寫死在客戶端的,是服務器下發的。客戶端每次啓動時,向服務端請求最新的動態菜單,經過向服務端發一個上次更新菜單的時間,來判斷服務器是否有新的菜單。爲了防止菜單下發後被篡改,對菜單url作了簽名。菜單的內容是基本不變的,可是每一個人過來都要作一次簽名,結果有次對菜單作了較多改動,又遇上作了一次推送,大量用戶打開App,把服務器搞掛了。由於Java提供的簽名方法內部上鎖了,大量線程在等待鎖,請求又多,扛不住了。
網絡

wKiom1ecfDTAnG8BAAH-uEVxoAo931.png

這個問題沒有深刻往下研究,用緩存解決了。由於下發的菜單或者其餘資源幾本是不變的,因此只要按字符串爲key作緩存就好了。好比把對url的簽名,按url爲key緩存。多線程

//先查緩存
String signature=memkv.unsafe_hget(「SIGN_LIST」, url);
//查不到則計算簽名,放入緩存,緩存1800s
if(signature==null) {
 signature = SignatureUtil.sign(url);
 memkv.unsafe_hset(「SIGN_LIST」, url, 1800);
}


2.2 第二個問題:上送報文RSA加密dom


應用開啓了報文全加密,大部分加密都是經過與服務端協商一個對稱密鑰,而後使用非對稱密鑰加密後傳送,後續的報文都是經過對稱加密處理的。可是有部分報文直接用了RSA加密,對性能產生了影響,一樣是等鎖。一共最多1000個線程,900多個都在等這個鎖。ide


wKioL1ecfcKBbxhsAABE7brk1eE823.png


這個問題無法用緩存解決了,由於每一個人上送的信息都是不同的,須要好好研究一下了。



3.問題分析



經過看代碼和查資料,我發現RSA在實際應用中並非簡單的作大數運算,爲了提升安全性,在加解密過程當中都須要添加必定的隨機因素。隨機因素有兩方面,一個是爲了讓同一明文每次加密產生的密文都不同,加密前先填充一部分隨機數,這個不止RSA有,DES等對稱加密也都有,稱爲padding。另外一方面是RSA解密(加密與解密用的同一個方法)的時候在作模冪運算時加入隨機數,似的作運算的時間不固定,讓***沒法統計計算時間, 致盲***,稱爲blinding。 上面出現的問題都是blinding引發的。


3.1 加密隨機數(padding)


加密的時候,通常須要在明文上填充一部分隨機數,這樣每次產生的密文都不同,這個過程稱爲padding,並且加密標準裏有各類類型的padding標準,好比PCKS1。

好比明文是D,經過一些填充,造成一個0011xxxxxxx11D的待加密串,00是開頭,兩個11中間是隨機數,並且這些隨機數不能含有1。這樣同一個D,每次產生的待加密串是不同的,解密後,按照這個格式把D取出來就好了。加密和解密端要使用一樣的padding格式。

如下過程依次爲

原始明文 -> padding後明文 -> 密文 -> padding後明文 -> 原始明文

對明文D作第一次加密

D -> 00112311D -> 123456 -> 00112311D -> D

對明文D作第二次加密

D -> 00117811D -> 783719 -> 00117811D -> D


爲何要使用padding呢?這個比較容易理解,好比有一種gongji類型叫作短消息gongji,若是我已知加解密雙方的明文空間是四個字節的字符串,那我就能夠經過觀察明文對應的密文把全部的對應關係枚舉出來,不須要密鑰我就能知道這個密文對應的明文了。若是一樣的明文每次密文都不同,就無法實現這種gongji了。



3.2 解密隨機數(blinding)


這個須要先從一種特殊的gongji方式提及。


3.2.1 時序attack


RSA的破解從理論上來說是大數質數分解,但是就是有一些人另闢蹊徑,搞出亂七八糟的破解方法。有一種attack方法叫作時序attack(timing attack),根據你解密的時間長短就能破解你的RSA私鑰。這是什麼鬼呢?

舉一個不恰當可是比較容易理解的例子:

A經過B提供的加密裝置把報文加密後發給B,咱們不關心加密是怎麼搞的,由於時序attack也不用關係加密端。

AB發的密文是2048位的01串,B有一個私鑰,也是2048位的01串,解密的時候把兩個串and一下

(如下示例用四位表示)

密文0101

私鑰0110

明文0100

問題的關鍵來了,進行and運算時若是有一個0,那麼運算的時間爲1ms,若是兩個都是1,運算的時間是10ms(只是個假設)。

基於以上假設,A就能夠破解B的私鑰了。A先構造一個0001的密文,獲取B解密的時間,若是是1ms左右,那麼對應的位就是0,若是是10ms左右,對應的1,依次類推,就把整個私鑰推斷出來了。


如何防範這種attack呢?

一種最簡單有效的方法,每次過來一個密文,先用一個隨機數與它and一下,而後在與私鑰and,只要隨機數是真正的隨機數,那麼是沒法破解的。注意,是真正的隨機數而不是僞隨機數。


如今回到RSA,RSA解密的本質就是冪模運算,也就是x = a ^ b  mod  n ,其中a是明文,b是私鑰,n是兩個大質數(p-1)(q-1)的積。因爲這些數都特別大,好比b可能有2048位,直接計算是不可行的。計算x的最經典的算法是蒙哥馬利算法,用代碼表示以下:

int mod(int a,int b,int n){  
    int result = 1;  
    int base = a;  
    while(b>0){  
     if(b & 1==1){  
         result = (result*base) % n;  
      }  
     base = (base*base) %n;  
      b>>=1;  
   }  
   return result;  
}

這個算法從b的最低位循環到最高位,若是是1,須要進行兩次模乘運算,若是是0的話則只須要一次。因爲這個操做是比較耗時的,因此0和1對應的時間差異較大。***能夠經過觀察不一樣輸入對應的解密時間,經過分析統計推斷出私鑰。具體的分析方法也不難,不過我沒怎麼看懂。。。

而防範RSA時序attack的方法也是在解密時加入隨機因素,讓***者沒法獲取準確的解密時間。


3.3 線程阻塞緣由


3.3.1 關於隨機數


真正意義上的隨機數,是很難產生的,由於即便小到原子,它的規律也是有跡可循的。因此咱們產生的隨機數都是僞隨機數,可是僞隨機數的隨機性也是不同的,若是產生的隨機數規律性很強,那就很容易被預測到,而若是產生的隨機數被預測的難度特別大,那麼咱們就能夠認爲它是真隨機數了,只有強度高的隨機數用來加解密等操做上纔是安全的。

 

目前大部分操做系統都會提供兩種隨機數的產生方式,以Linux爲例,它提供了/dev/urandom和/dev/random兩個特殊設備,能夠從裏面讀取必定長度的隨機數。/dev/random是blocking pseudo random number generator (阻塞式僞隨機數產生器),它是經過網絡事件,鍵盤敲擊事件等物理上隨機的事件,收集一些隨機bit到熵池來產生隨機數。這個隨機生成函數可能由於熵池爲空而等待,因此須要大量隨機數的狀況下它會顯得很慢,但諸如產生證書之類的操做須要這種強度的隨機數。 而/dev/urandom就是unblocking,它不會阻塞,可是產生的隨機數不夠高,是以時間戳之類的種子來產生隨機數。

 

其它操做系統的實現方式可能不一樣,可是本質都是同樣的。總之,要想得到一個真隨機數,無論用什麼語言,都須要等。在Java裏面,能夠用SecureRandom產生隨機數,並且能夠在JVM參數裏配置是使用/dev/random仍是/dev/urandom,若是安全性要求改,就用/dev/random,可是性能是就會有折扣。


3.3.2 Java的Cipher類


Java的加解密庫是經過spi的方式,底層有不一樣的provider實現,默認的是sun官方的,比較有名的第三方的是bouncy castle。而上層的統一接口就是Cipher。


使用方法:

//獲取Cipher示例,注意這個並非單例的,它有線程安全問題
//第一個參數是算法以及填充之類的,第二個是Provider,好比BC就是bouncy castle
Cipher cipher = Cipher.getInstance("RSA/CBC/PCKS1", "BC");
//必須初始化
cipher.init(Cipher.DECRYPT_MODE, privateKey);
//傳入byte[]數組進行加解密
cipher.doFinal(miwen);

我在看咱們應用的代碼時,發現全局居然只有一個Cipher,這個是有問題的,它有線程安全問題,看代碼以及多線程作實驗都發現了,因爲初始化一個Cipher比較慢,因此最好用ThreadLocal來解決它的線程安全問題。


3.3.3 getBlindingParameterPair函數


經過堵塞的線程堆棧來看,兩個問題都出如今RsaCore的getBlindingParameterPair上,第一個行數是261,第二個是417,這是由於兩個jdk不同。


在jdk1.6裏面,getBlindingParameterPair用的是隨機數的方法,堵在了SecureRandom.nextBytes上,這個函數在底層是讀的/dev/random,若是產生的隨機數不夠用,就要堵塞。


在jdk1.7_80這個版本里,getBlingdingParameterPair沒有用隨機數,而是經過計算獲得了一個pair,具體代碼以下:

synchronized (this) {
  if ((!this.u.equals(BigInteger.ZERO)) && (!this.v.equals(BigInteger.ZERO)))
 {
  localBlindingRandomPair = new RSACore.BlindingRandomPair(this.u, this.v);
  if ((this.u.compareTo(BigInteger.ONE)<=0)||(this.v.compareTo(BigInteger.ONE)<=0))
  {
   this.u = BigInteger.ZERO;
   this.v = BigInteger.ZERO;
  } else {
   this.u = this.u.modPow(BIG_TWO, paramBigInteger3);
   this.v = this.v.modPow(BIG_TWO, paramBigInteger3);
  }
 }
}

在同步塊裏進行大數運算,爲何要上鎖我沒有深刻研究,可是1.7確實比1.6快了。


上面都是用jdk自帶的provider實現的,我又試了下把provider換乘bouncy castle,比1.6自帶的快不少,可是比1.7的慢,用多個線程解密的時候發現也是堵在SecureRandom.nextBytes上,因此bouncy castle也是用的隨機數。

wKiom1ecqinC_hF-AAA7x2YiB_E692.png



問題的緣由就是jdk自帶的RSA解密方法爲了防範時序attack致使的,可是也沒有好的解決辦法。在早期的jdk裏,代碼中有if(ENABLE_BLINDING)這個判斷,因此是能夠把blinding關掉的,可是新的已經沒有選項了,都須要使用blinding,由於時序attack確實是挺容易實施的。


4.問題解決方案


第一個問題用緩存解決了,每一個菜單半個小時只作一次簽名,與原來的每一個請求都簽名相比,性能好太多。


第二個問題除了鎖的問題,Cipher類自己有線程安全問題,經過ThreadLocal解決線程安全問題。可是鎖的問題很差解決,短時間內經過使用計數器對線程進行保護,好比最多容許300個線程同時調用這個函數,多了直接拒絕,由於使用RSA加解密的交易並很少,出現擁堵估計是特殊狀況。長期內仍是把用RSA加密報文的方式完全換掉,RSA只加密對稱密鑰。


總之,RSA做爲一種低效的加密方法,用在加密大量數據上面是不合適的,即便是簽名之類的地方,能儘可能少用也要少用,不然對性能影響很大。

相關文章
相關標籤/搜索