2009/08/31

resin-4.0 session的設定

我們
這在做管理介面時遇到鬼打牆
看session是啟用的
存活時間是30分鐘
但只要一換頁面session就會變空的
翻來翻去翻resin的說明

有這玩意兒可以來調session的設定
請參考這裡
前面講了一大推沒在用的我還去試他orz
最後試一試是只要設這個

<!-- enable persistent sessions -->

<session-config>
<save-mode>after-request</save-mode>
<use-persistent-store/>
</session-config>

<!--

增加save-mode這個child
並設定其質為after-request
一有request就存進去
解決這問題

(題外 管理介面有做一些小變更 包含登入驗證等頁面改變 可以去看看?)

2009/08/30

PKI Research #4.5 - Digital Signature 再臨

最近要實作檔案傳輸的部分,由於一開始作使用者登入時,驗證的部分是在本機端 (Client) 作的,伺服器端 (Server) 根本無法得知真實性。所以當有人聲稱某甲要求檔案或傳輸檔案過來時,我們也不能肯定他就是本人。因此,有必要在伺服器端再作一遍。

數位簽章的實作,其實在研究 #4 中已經有了,但是主要是驗證簽章的部分,要在伺服器中執行。搭配接收 POST 上傳檔案的設計,我採用 PHP 來實作。查了一下 PHP 也有 OpenSSL 套件支援 (API),所以我只要使用它提供的函式就可以驗證簽章,關鍵函式 openssl_verify。

openssl_verify 能接受一些參數 (原訊息、數位簽章、公鑰) 來驗證簽章正確性,其中簽章演算法只支援到 SHA1,沒有先前研究的 SHA256,只好把簽章的部分也改用 SHA1 以配合。公鑰的部分,得先從憑證裡抽出來,這有 openssl_get_publickey 可以使用。

大概像這樣:
<?php
$message = 'test'; // Original Message

try {
    $dbh = new PDO('mysql:dbname=FAST;host=127.0.0.1', 'user', 'pass');
} catch (PDOException $e) {
    echo 'Connection failed: ' . $e->getMessage();
}

$sth = $dbh->prepare('SELECT `Public_Key` FROM XXX WHERE ID = ?');
$sth->bindParam(1, $tagID, PDO::PARAM_STR, 8);
$sth->execute();

$row = $sth->fetch();
$pubkeyid = openssl_get_publickey($row['Public_Key']);

// state whether signature is okay or not
$ok = openssl_verify($message, base64_decode($_POST['sigData']), $pubkeyid);
if ($ok == 1) {
    echo "good";
} elseif ($ok == 0) {
    echo "bad";
} else {
    echo "ugly, error checking signature";
}

// free the key from memory
openssl_free_key($pubkeyid);
?>

sigData 是數位簽章內容,為了傳輸方便已先行以 BASE64 編碼,所以這邊再解碼還原。

實際測試時,發現結果一直回傳 bad 即簽章不符,因為 C# 實作驗證都一切正常,所以我懷疑是不是 PHP 的問題,改換使用 JSP 來實作簽章驗證的部分,結果一樣失敗,不同的是 Java 跳出簽章結構錯誤的訊息,我開始懷疑起實作簽章的部分了。在 PHP 上使用 key 私鑰檔實作簽名,跑出來的東西跟 C# 的完全不一樣。C# 簽章驗證的結果,兩種結果都能通過驗證,不過 C# 的結果丟給 PHP 是失敗的,百思不得其解。

回頭想想,會不是會當初實作簽章的時候,想法錯了呢?還記得 .NET Framework 不許我們使用公鑰解密,取而代之的提供 Verify 方法,找看看 Bouncy Castle 有沒有提供類似的 Sign 方法,結果真的有。
public static byte[] signature(byte[] data){
try{
AsymmetricCipherKeyPair keyPair;
using(var reader = File.OpenText(@"D:\Programs\OpenSSL\user1.key")) { // Direct read pem format RSA private key file
keyPair = (AsymmetricCipherKeyPair) new PemReader(reader).ReadObject();
}

RsaDigestSigner rds = new RsaDigestSigner(new Sha1Digest());
rds.Init(true, keyPair.Private);
rds.BlockUpdate(data, 0, data.Length);
return rds.GenerateSignature();
}catch(Exception e){
throw new IOException("problem creating private key: " + e.ToString());
}
}

public static bool verify(byte[] message, byte[] sig){
var crt = new X509Certificate2(@"D:\Programs\OpenSSL\user1.crt"); // Read X509 Certificate file
var rsa = (RSACryptoServiceProvider) crt.PublicKey.Key; // Get public key

return rsa.VerifyData(message, "SHA1", sig);
}

Bouncy Castle 提供了 Org.BouncyCastle.Crypto.Signer 類別來處理數位簽章,RsaDigestSigner 就是給 RSA 公開金鑰演算法使用的數位簽章處理類別,由於 PHP 方的 Hash 演算法只支援到 SHA1,所以我簽章的部分也僅使用 SHA1。使用這個 Signer 類別不必自己手動 Hash 訊息,交給類別去作。

驗證簽章的部分跟上次基本上沒很大的差別,只是我也捨棄 VerifyHash 方法改使用 VerifyData 方法,直接把訊息 Hash 的動作交給了方法去處理,我完全不必實作相關 Hash 方法。

這次修改後,產生簽章的結果跟 PHP 的一模一樣,這樣就可以由 C#產生簽章,由 PHP 接收來驗證,進而確認使用者身份,伺服器端也可以利用 SESSION 暫時紀錄結果,就不必每次都要驗證。預設 30 分鐘 SESSION 會過期,到時候再驗證一次即可。

2009/08/26

mod_jk (Apache + Tomcat) 在 Ubuntu 下的設定方式

因為組員只會寫JSP,所以原本的Apache以外勢必還要再架設一個跑JSP的服務,我就想到了 Tomcat。因為 Ubuntu 的套件庫很多已經編譯好的玩意,連相依性都幫我考慮好了,很方便。所以我就打開 Synaptic 套件管理工具來找尋 Tomcat 安裝。

安裝的是 Tomcat6,連帶安裝了 OpenJDK 等 Java 函式庫。我突然看到還有 mod_jk 可以安裝,說明是說可以把 Tomcat 跟 Apache 連接起來,透過 Apache 就可以執行 JSP (說穿了就是扮演代理角色)。因為 Apache 的效能比 Tomcat 還要好,所以透過 mod_jk 不但可以除去網址老是有 8080 Port 的不便,也可以獲得 Apache 的代理效能。

全部利用 Synaptic 套件管理工具安裝完後,mod_jk還需要手動設定,而且還滿多地方需要手動設定不然按照預設值是跑不起來的地方,不知道是不是版本的關係。我使用 Ubuntu 9.04 Server 版。

首先,要先去修改 /etc/libapache2-mod-jk/workers.properties 這個檔案。這個檔案是 mod_jk 使用,作為 Apache 跟 Tomcat 溝通的工人 (Worker)。使用 vi 打開後主要修改幾處地方:

workers.tomcat_home=/usr/share/tomcat6
這個值預設是安裝 Tomcat 5 的位置,因為我是安裝新版的 6 版,不修改一定不能用。改成 /usr/share/tomcat6 就可以順利指定 Tomcat6 的目錄。

workers.java_home=/usr/lib/jvm/default-java
這個值預設是 JavaVM 沒錯,但是現在已經有開放原始碼的 OpenJRE/OpenJDK,如果你也是安裝了 OpenJRE,那記得改一下位置: /usr/lib/jvm/default-java,這個路徑是一個 Symbolic Link,不知道其他版本是不是也有這個 link。


接下來請到 /etc/apache2/mods-available,打 ls 查看一下可以發現有 jk.load,這是載入 mod_jk 模組的設定檔,但卻沒看到對應的 jk.conf (照理說是 mod_jk 的設定),查了一下發現在 /usr/share/doc/libapache2-mod-jk 目錄下有 httpd_example_apache2.conf 範例設定檔,將這個檔案 cp 到 /etc/apache2/mods-available 並改名為 jk.conf 以方便作業。接著,打開檔案編輯。

這個檔案預設上是差不多可以用了,但是還是需要修改:

加上一行 JkMountCopy All
加這行的用意是因為預設 mod_jk 只會運作在 Apache 主網站,其他的虛擬網站 (VirtualHost) 設定不會套用。但是 Ubuntu 預設的結構是,主網站也是使用虛擬網站設定,並沒有主網站設定,所以按照預設值 mod_jk 會無法對應到正確的檔案 (這可以從 log 看的出來)。而這行就是將設定複製到全部的虛擬網站。

因為我的網站設定只有 80 port 跟 443 port,而且網站內容一模一樣,這樣的設定沒有什麼問題。但是如果你的虛擬網站很複雜,各自有不同內容,那就請只在想要連接到 Tomcat 的網站設定檔內修改加入一行 JkMountCopy On,讓你指定的虛擬網站能夠正常運作 mod_jk 設定。


結束編輯後,我們還要建立一個 Symbolic Link 讓 Apache 讀取。到 /etc/apache2/mods-enabled 執行以下指令:

ln -s ../mods-available/jk.conf jk.conf


最後重新啟動 Apache (執行 /etc/init.d/apache2 restart 指令),就大功告成了。

結果就是 http://127.0.0.1/*.jsp 會自動對應到 http://127.0.0.1:8080/*.jsp,如果你想要更特別的設定,打開 /etc/apache2/mods-available/jk.conf 就可以自由新增。

JkMount /*.jsp ajp13_worker
JkMount /*/servlet/ ajp13_worker

這是預設的設定,用意就是將符合的條件送給 Tomcat 處理。所以我打 /test.jsp 就會送到 :8080/test.jsp

2009/08/24

員工資料庫 介面 申請單 JSP上做MD5

員工資料庫完成
增(給員工填的申請表單)
刪(與列表、查合一)


介面皆讓大家看過
就不截圖了...
外觀上或功能上有意見請在此篇回應


再來說到身份ID做hash部份
使用到java.security內的方法MessagerDigest
我們得先import java.security.*後
執行如此下的方法(參考資料)
MessageDigest mdAlgorithm = MessageDigest.getInstance("MD5");
mdAlgorithm.update(User_ID.getBytes());
byte[] digest = mdAlgorithm.digest();
StringBuffer hexString = new StringBuffer();
for (int i = 0; i < digest.length; i++) {
User_ID = Integer.toHexString(0xFF & digest[i]);
if (User_ID.length() < 2) {
User_ID = "0" + User_ID;
}
hexString.append(User_ID);
}
User_ID=hexString.toString();


就可以成功執行hash動作

2009/08/18

PKI Research #5 - 使用 OpenCA 產生的憑證跟加密過的私鑰

本篇是研究最卡的部分,足足卡了三天。欲知原因以下見曉。(關鍵字:PKCS#8, PKCS#5)

因為伊達說已經完成研究 OpenCA 發憑證的部分,因此我們使用 OpenCA 的最大目的「產生憑證、私鑰檔和 CRL 憑證廢止清冊」就已經達成,再來就是要研究如何拿來用。

OpenCA 主要使用 MySQL 來儲存相關資料,舉凡憑證申請、憑證存放、CA 憑證、CRL 憑證廢止清冊等全部都在資料庫內,而不是一般的檔案。以 phpMyAdmin 登入後可以看到。





最重要的就是 certificate 資料表了,data 欄位就是重點。裡面存放著憑證以及私鑰檔,我們只要取出來應該就可以利用了吧。我當初是這麼想,但是他的私鑰檔卻經過 DES 加密 (「私鑰經過 PKCS#5 密碼加密標準 "PBEwithMD5andDES-CBC" 加密過的」PKCS#8 私鑰格式標準),檔案格式看起來像這樣:

-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIBgTAbBgkqhkiG9w0BBQMwDgQIjgXxhGkmgGcCAggABIIBYEXZfHjLUnXJ8h+F
...
mtuOc9U=
-----END ENCRYPTED PRIVATE KEY-----


看得出跟一般 OpenSSL 產生的未加密的私鑰檔不同吧,如下 (純粹私鑰,非 PKCS#8 格式):

-----BEGIN RSA PRIVATE KEY-----
MIIBOQIBAAJBAOWeK9TSYVnPEkmK45TeI7NrC8MzvwabSVg1aEuTmcNkLRg/Qibv
...
Ft4zOc8TnbXnGxXRfGVG0RZfXmxcQNv2yX2G4Vc=
-----END RSA PRIVATE KEY-----


如果以為 PKCS#8 私鑰 key 檔能跟 OpenSSL 的私鑰 key 檔一樣讀取操作 (PKI Research #3 - RSA 加解密),那你就錯了。你會發現 keyPair 永遠是 null,因為察看原始碼才發現,PemReader 沒有特別處理 BEGIN (ENCRYPTED) PRIVATE KEY 的加密私鑰檔 (90 行 switch 開始),更簡單的說法就是 PemReader 不支援 PKCS#8 格式私鑰檔,所以只好自己找方法了。

由於中文極度缺乏相關資源,英文的資源雖然有找到類似的討論串,但也是跟我有著一樣的疑問,而沒有人回答。例如:



其實還有很多,但是我一時忘了。嘗試用 PBEwithMD5andDES-CBC 當關鍵字知道 Bouncy Castle 的 Org.BouncyCastle.Security.PbeUtilities 有支援,但是它的說明手冊不夠完整 (或者該說,沒這種玩意,只有 API),到底是怎樣的使用法也沒人提到。於是我們就來看 PbeUtilities.cs 的原始碼吧:

如果你想知道為什麼是 PKCS#5 PBEwithMD5andDES-CBC 加密格式,用 OpenSSL asn1parse -in 檔案路徑 就可以查看 ASN.1 結構,就會寫到。


原始碼果然定義一堆加密的方法,這下子有希望了。看了一下大概是像這樣子使用:
using System;
using System.IO;
using System.Text;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;

const String pemp8header = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
const String pemp8footer = "-----END ENCRYPTED PRIVATE KEY-----";
char[] password = "xxxxxx".ToCharArray(); // 解密密碼

// 取出檔案中 BASE64 編碼的部分,還原
StringBuilder sb = new StringBuilder(File.ReadAllText(@"B:\tmp.key"));
sb.Replace(pemp8header, "");
sb.Replace(pemp8footer, "");


String pubstr = sb.ToString().Trim();
byte[] binkey = Convert.FromBase64String(pubstr);
var info = EncryptedPrivateKeyInfo.GetInstance(Asn1Object.FromByteArray(binkey));

// 開始建立解密引擎,準備進行 DES-CBC 解密
var algId = info.EncryptionAlgorithm;
var cipher = (BufferedBlockCipher) PbeUtilities.CreateEngine(algId.ObjectID);
var param = PbeUtilities.GenerateCipherParameters(algId.ObjectID, password, algId.Parameters);
cipher.Init(false, param);

// 取出加密資料,進行解密
byte[] data = info.GetEncryptedData();
byte[] outBytes = new byte[cipher.GetOutputSize(data.Length)];
int len = cipher.ProcessBytes(data, 0, data.Length, outBytes, 0);

try {
len += cipher.DoFinal(outBytes, len);
} catch(Exception e) {
throw new Exception("failed DoFinal - exception " + e.ToString());
}

// outBytes 為解密完的 PrivateKeyInfo (PKCS#8),接著取出 PrivateKey
var p = PrivateKeyInfo.GetInstance(Asn1Object.FromByteArray(outBytes));
var rsa = new RsaPrivateKeyStructure((Asn1Sequence) p.PrivateKey);
// privSpec 可用於解密
var privSpec = new RsaKeyParameters(true, rsa.Modulus, rsa.PrivateExponent);


手工實作法的步驟很繁雜,有沒有更好的方法呢?

想想可能還有其他已經包好的方法用了 PbeUtilities 來讀取加密私鑰了,就查查看。果不其然又讓我發現了 PrivateKeyInfoFactory.cs 原始碼,原來只要幾行:
using System;
using System.IO;
using System.Text;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;

const String pemp8header = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
const String pemp8footer = "-----END ENCRYPTED PRIVATE KEY-----";
char[] password = "xxxxxx".ToCharArray(); // 解密密碼

// 取出檔案中 BASE64 編碼的部分,還原
StringBuilder sb = new StringBuilder(File.ReadAllText(@"B:\tmp.key"));
sb.Replace(pemp8header, "");
sb.Replace(pemp8footer, "");

String pubstr = sb.ToString().Trim();
byte[] binkey = Convert.FromBase64String(pubstr);
var info = EncryptedPrivateKeyInfo.GetInstance(Asn1Object.FromByteArray(binkey));

// 重點方法: PrivateKeyInfoFactory.CreatePrivateKeyInfo() 一行抵十行
var p = PrivateKeyInfoFactory.CreatePrivateKeyInfo(password, info);
var rsa = new RsaPrivateKeyStructure((Asn1Sequence) p.PrivateKey);
// privSpec 可用於解密
var privSpec = new RsaKeyParameters(true, rsa.Modulus, rsa.PrivateExponent);


總結:開放原始碼是有必要的,畢竟最後還是得看原始碼找答案的嘛 (大誤)

2009/08/14

OpenCA 新增憑證 & 匯出


今天來說說那個OpenCA弄新憑證&匯出我們要的資料的方法



到PKI Init & Config的Initialization>CA administrator
到Pub頁面,下方就有Request a Certificate

去request一個新的憑證
需要填入的資料表單如下


基本上,只有名稱跟email是一定必填的
若要做給使用者填寫的表格,要再思考
其他欄位是否有必要性

精過一番精密...喔不 是簡單的過程後
這個"Request"就成功發出了 接下來就到CA operation去做 憑‧證 承~~認!


來這 選NEW(即能看到新進的憑證伸請要求)
然後承認後
可以到Information > CA Certificates > valid (合法的Certificates)

看到列表入下


那麼點選就能進入該觀看Certificate資料
並選擇下載(格式可選)


選我們要的格式就OK了w
Got it! Go to next.

2009/08/13

流程



可能還是要先多想想需要甚麼功能吧!

各部分研究目標

OpenCA
- 功能: 輸入使用者個人資訊以產生伺服器簽發憑證跟私鑰檔,並能產生廢止清冊 CRLs
- 目的: 產生憑證檔跟私鑰檔來驗證,還有 CRLs 讓程式判斷憑證是否可使用
- 研究: 如何申請使用者憑證,並能取得憑證檔

PKI
- 功能: 公開金鑰加解密、數位簽章及驗證
- 目的: 使用者身分鑑別
- 研究: 得出公鑰 crt 憑證、RSA 私鑰 key,讀取 CRL 檢查是否已撤銷

RFID
- 功能: 讀取/寫入私鑰檔跟個人設定之類的資訊
- 目的: 當作使用者身分鑑別的媒介
- 研究: RFID Tag Block 寫入資料區塊的利用分配

MySQL
- 功能: 儲存使用者相關資料
- 目的: 跟 RFID 連動取得進一步的使用者資料
- 研究: 資料表要放什麼

Apache
- 功能: 網頁伺服器
- 目的: 上傳/下載公文、個人檔案的傳輸媒介,利用 SSL 加密
- 研究: 如何讓使用者上下傳檔案

C#
- 功能: 撰寫程式
- 目的: 利用 RFID 讀取私鑰檔,連接 MySQL 取得資料進行驗證。前端 GUI 顯示公文、個人檔案等等
- 研究: 如何結合以上功能寫出程式

DB裡面

Database裡面應該會這樣放
覺得可以再更改的地方再提出吧!


ID_Transformation
Tag_IDDatabase_IDChanged
1212343456567878980000010
1212343456567899980000021
1212343456567800980000032
1212343456567812980000043
//Tag_ID 代表 Tag的ID
//Database_ID 代表Database上的ID
//changed 代表 Tag_ID更改次數,也可以代表Tag遺失次數!XD



Key(Key的儲存)
Database_ID Public_Key
98000001 qwerasdfzxcvtgbh
98000002 qasdfghjklmnbvcx
98000003 qasdfghjklmnbaaa
98000004 qasdfghjklmasdfg
// Public_Key 儲存 public key,認證用!



Paper(記錄公文的資訊)
Database_ID Paper_Read
98000001 y
98000002 n
98000003 n
98000004 y
// Paper_Read 代表 公文已讀與否!


Identity(user身分)
Database_ID NameLevelUser_ID
98000001 Jimmy Chairman A123456789
98000002 RobertCamel Manager B123456789
98000003 Scribe Manager C123456789
98000004 Soultaker Chairman D123456789
// Level 代表 職位
// User_ID 代表 身分證字號



ENV_Set(用來儲存user的設定值)
Database_ID Set_1Set_2Set_3
98000001 0 1 1
98000002 1 1 1
98000003 0 1 0
98000004 1 0 1
// set_1 代表 user環境的設定 0:off 1:on (例如button要不要透明化!?)

2009/08/12

架設 OpenCA 1.0.2 on Ubuntu 9.04 Server

終於完成啦!完全照著嚕嚕咪的教學(見此)照抄就完成了,沒啥好說的。

倒是順便安裝了 phpMyAdmin 套件 (apt-get phpmyadmin),不是直接下載原始碼安裝,最後會裝到 /usr/share/phpmyadmin,但是網頁預設目錄是 /var/www,所以要建立一個 Symbolic link,在 /var/www 目錄下 ln -s /usr/share/phpmyadmin phpmyadmin 就可以建立一個連結,不必複製整個目錄就可以使用。

既然架好了,請其他人盡速開始研究 MySQL 跟 OpenCA 囉。

2009/08/02

PKI Research #4 - Digital Signature

這篇預定要來研究數位簽章。

數位簽章的方式跟加解密剛好是反過來的,加解密使用公鑰加密,使用私鑰解密;數位簽章卻是使用私鑰簽章,使用公鑰驗證。

加解密的對象是明文檔案內容,而數位簽章的對象則是明文檔案內容經過雜湊後的值。常用的有 MD5 跟 SHA-1,聽說這兩個都有被碰撞破解的個案,所以使用 SHA256 (SHA-2 家族)。

.NET Fraamework 的 System.Security.Cryptography 命名空間實作不少相關雜湊,我直接寫成以下方法:
public static byte[] useSHA256(byte[] data){
SHA256 sha = new SHA256Managed();
return sha.ComputeHash(data);
}

流程圖一張:
http://en.wikipedia.org/wiki/File:Digital_Signature_diagram.svg

先從簽章實作。第一件事就是將文章進行 Hash,我使用上面寫好的 SHA256 方法來製造。然後要將此 Hash 以私鑰加密,因為 X509Certificate2 不能直接支援 RSA Private Key PEM 檔 (範例中使用 DER 格式方便 RFID Tag 儲存),所以我使用 Bouncy Castle C# 來進行私鑰的讀取,並用來加密 Hash 產生數位簽章 (稱為 Hash')。

另一邊進行驗證。驗證也需要將文章進行 Hash,以便跟數位簽章比對。一樣使用 SHA256 產生 Hash。然後將數位簽章使用 .crt 憑證內存的公鑰解密 (得到 Hash'),結果拿來跟 Hash 比對。如果完全正確 (Hash' = Hash) 表示文章沒有經過竄改,而且也能確定簽章者正是本人。但是 X509Certificate2 不能直接拿公鑰進行解密 (Decrypt),而是提供另一個驗證 (VerifyHash) 方法供呼叫,我也搞不懂為什麼不行。

public static byte[] signature(byte[] data){
byte[] hash = useSHA256(data);
try{
var seq = (Asn1Sequence) Asn1Object.FromStream(File.OpenRead(@"B:\OpenSSL\1-der.key"));
var rsa = new RsaPrivateKeyStructure(seq);
var privSpec = new RsaPrivateCrtKeyParameters(
rsa.Modulus, rsa.PublicExponent, rsa.PrivateExponent,
rsa.Prime1, rsa.Prime2, rsa.Exponent1, rsa.Exponent2,
rsa.Coefficient
);

var encryptEngine = new Pkcs1Encoding(new RsaEngine());
encryptEngine.Init(true, privSpec);
return encryptEngine.ProcessBlock(hash, 0, hash.Length); // Digital Signature
}catch(Exception e){
throw new IOException("problem creating private key: " + e.ToString());
}
}


public static bool verify(byte[] message, byte[] sig){
byte[] hash = useSHA256(message);

var crt = new X509Certificate2(@"B:\OpenSSL\1.crt"); // Read X509 Certificate file
var rsa = (RSACryptoServiceProvider) crt.PublicKey.Key; // Get public key

return rsa.VerifyHash(hash, "SHA256", sig);
}

PKI Research #3 - RSA 加解密

CA 部分還沒搞定,但生成憑證簽署方面都可以利用 OpenSSL 來完成。OpenSSL 是開放原始碼專案,在 Unix-like 界非常廣泛,Windows 有別人 Port 好的版本:OpenSSL Binary Distributions,可以下載來玩看看。下指令的方式上一篇已經有許多篇參考資料,不再說明。

複習一下 .key 檔跟 .crt 檔的樣子:

user.key: (PEM-encoded)
-----BEGIN RSA PRIVATE KEY-----
MIIBOQIBAAJBAOWeK9TSYVnPEkmK45TeI7NrC8MzvwabSVg1aEuTmcNkLRg/Qibv
B6ST5F056HCk0xvzm5rfO5A+q+4ncBMGMhkCAwEAAQJABGaLoICHrRjy2MX4ppm7
RWz/xLXxK0c+mJotbYVepQf1KYB6kPc4df1t4KrFFgxMyXzdJ/eiegf4j3HFT4Hs
gQIhAPiVVZNSMqp1uUx7OXB3GGW+3uslvA3+NvuAKqVguIo9AiEA7Hf6ykGeuRBN
k2EN+J+Vof4ryzFOtJGH+eyycmKPsQ0CIFJYteZ9nkcVhHKvh1GYQj7CQfpHn8pK
4k/iHz51kexJAiBwKs1kiVHv+QLDSQNmjtRcngNKBB6QWoQEkjlnNsdwNQIgbJYa
Ft4zOc8TnbXnGxXRfGVG0RZfXmxcQNv2yX2G4Vc=
-----END RSA PRIVATE KEY-----


user.crt: (PEM-encoded)
-----BEGIN CERTIFICATE-----
MIIBtDCCAV4CCQChpB3R8j5vwDANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQGEwJU
VzEPMA0GA1UECBMGVGFpd2FuMQ8wDQYDVQQHEwZUYWl3YW4xHDAaBgNVBAoTE05D
SFUgTUlTIERlcGFydG1lbnQxEjAQBgNVBAMTCXNvdWx0YWtlcjAeFw0wOTA3MzEw
NDE1MDJaFw0xMDA3MzEwNDE1MDJaMGExCzAJBgNVBAYTAlRXMQ8wDQYDVQQIEwZU
YWl3YW4xDzANBgNVBAcTBlRhaXdhbjEcMBoGA1UEChMTTkNIVSBNSVMgRGVwYXJ0
bWVudDESMBAGA1UEAxMJc291bHRha2VyMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJB
AOWeK9TSYVnPEkmK45TeI7NrC8MzvwabSVg1aEuTmcNkLRg/QibvB6ST5F056HCk
0xvzm5rfO5A+q+4ncBMGMhkCAwEAATANBgkqhkiG9w0BAQUFAANBAGQhB8VKtY2N
hecmukSUmJkTzFWwT15fPmTGzf0KdrccNCSK2bE/u6bTW9dkUF12MkTaHNVytmKd
u7MoPuNvCnA=
-----END CERTIFICATE-----


所以,當我有了 .key 檔 (RSA 私鑰) 跟 .crt 憑證 (含公鑰的憑證) 後,該怎麼使用?這是本篇要研究的目標。

C# 利用 X.509 憑證加密,網路上有很棒的教學:使用X.509数字证书加密解密实务(二)-- 使用RSA证书加密敏感数据,圖文並茂很容易了解。基本上 .NET Framework 的 X509Certificate2 類別非常好用,它屬於 System.Security.Cryptography.X509Certificates 命名空間。只要建構元放入 .crt 憑證檔路徑,就可以抽出公鑰,加密資料。

解密的話,X509Certificate2 支援公私鑰綁一起的 PKCS#12,但不支援 OpenSSL 直接匯出的私鑰格式 (稱為 PEM 格式),當你直接指定 .key 檔位置會丟出例外,所以解密要找其他方案。

剛好無意間找到利用 Bouncy Castle for C# API 直接讀取 PEM 格式 RSA 私鑰進行解密的教學:用openSSL生成了一对RSA密钥,在C#里面怎么使用它啊? ,回答的第二項就是我們要的。你可能覺得奇怪它說第三項也可以用啊,但實際上那是讀取 .pfx 或 .p12 的 PKCS#12 個人憑證,直接讀取 OpenSSL RSA 私鑰 PEM 格式檔是絕對不行的。

DER (Distinguished Encoding Rules):
二進位內容,是屬於 ASN.1 制定的編碼之一。

PEM (Privacy-enhanced Electronic Mail):
就是 DER 格式經過 BASE64 編碼後所能見到的內容,通常會有 ----- BEGIN XXX ----- / ----- END XXX ----- 之類的東西包夾起來。由於 PEM 格式採用了 BASE64 編碼,文字都被編成常用的英文數字符號,方便於網路上傳送及複製 (如即時通訊、電子郵件等)。OpenSSL 預設產生的檔案都是 PEM 格式。當然也可以透過 -inform / -outform 來指定 DER 等格式。

using System;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Encodings;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;


namespace PKI_RSAEnDecryptDemo {
class Program {
static void Main(string[] args) {
byte[] plainTextByte = Encoding.UTF8.GetBytes("超爽的,撿到100塊咧~咧?");

byte[] encTextType = encrypt(plainTextByte);
string base64 = Convert.ToBase64String(encTextType);
Console.WriteLine(base64);

byte[] oristr = decrypt_DER(encTextType);
Console.WriteLine(Encoding.UTF8.GetString(oristr));

Console.ReadKey();
}

public static byte[] encrypt(byte[] plainTextByte) {
var crt = new X509Certificate2(@"B:\OpenSSL\1.crt"); // Read X509 Certificate file
var rsa = (RSACryptoServiceProvider) crt.PublicKey.Key; // Get public key

byte[] Cryptograph = rsa.Encrypt(plainTextByte, false); // Encrypt
return Cryptograph;
}

public static byte[] decrypt(byte[] encTextType) {
AsymmetricCipherKeyPair keyPair;
using(var reader = File.OpenText(@"B:\OpenSSL\1.key")) { // Direct read pem format RSA private key file
keyPair = (AsymmetricCipherKeyPair) new PemReader(reader).ReadObject();
}
var decryptEngine = new Pkcs1Encoding(new RsaEngine());
decryptEngine.Init(false, keyPair.Private);

return decryptEngine.ProcessBlock(encTextType, 0, encTextType.Length); // Decrypt
}

public static byte[] decrypt_DER(byte[] encText) {
try{
var seq = (Asn1Sequence) Asn1Object.FromStream(File.OpenRead(@"B:\OpenSSL\1-der.key"));
var rsa = new RsaPrivateKeyStructure(seq);
var privSpec = new RsaPrivateCrtKeyParameters(
rsa.Modulus, rsa.PublicExponent, rsa.PrivateExponent,
rsa.Prime1, rsa.Prime2, rsa.Exponent1, rsa.Exponent2,
rsa.Coefficient
);

var decryptEngine = new Pkcs1Encoding(new RsaEngine());
decryptEngine.Init(false, privSpec);
return decryptEngine.ProcessBlock(encText, 0, encText.Length); // Decrypt
}catch(Exception e){
throw new IOException("problem creating private key: " + e.ToString());
}
}
}
}


---
參考資料:
OpenSSL 常用語法整理