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);


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

沒有留言:

張貼留言