創建X509證書,并獲取證書密鑰的一點研究
作者:肖波
個人博客:http://blog.csdn.net/eaglet ; http://www.cnblogs.com/eaglet
2007/7 南京
?
背景
服務器SSL數字證書和客戶端單位數字證書的格式遵循 X.509 標準。 X.509 是由國際電信聯盟(ITU-T)制定的數字證書標準。為了提供公用網絡用戶目錄信息服務, ITU 于 1988 年制定了 X.500 系列標準。其中 X.500 和 X.509 是安全認證系統的核心, X.500 定義了一種區別命名規則,以命名樹來確保用戶名稱的唯一性; X.509 則為 X.500 用戶名稱提供了通信實體鑒別機制,并規定了實體鑒別過程中廣泛適用的證書語法和數據接口, X.509 稱之為證書。
?? X.509 給出的鑒別框架是一種基于公開密鑰體制的鑒別業務密鑰管理。一個用戶有兩把密鑰:一把是用戶的專用密鑰(簡稱為:私鑰),另一把是其他用戶都可得到和利用的公共密鑰(簡稱為:公鑰)。用戶可用常規加密算法(如 DES)為信息加密,然后再用接收者的公共密鑰對 DES 進行加密并將之附于信息之上,這樣接收者可用對應的專用密鑰打開 DES 密鎖,并對信息解密。該鑒別框架允許用戶將其公開密鑰存放在CA的目錄項中。一個用戶如果想與另一個用戶交換秘密信息,就可以直接從對方的目錄項中獲得相應的公開密鑰,用于各種安全服務。
?? 最初的 X.509 版本公布于 1988 年,版本 3 的建議稿 1994 年公布,在 1995 年獲得批準。本質上, X.509 證書由用戶公共密鑰與用戶標識符組成,此外還包括版本號、證書序列號、CA 標識符、簽名算法標識、簽發者名稱、證書有效期等。用戶可通過安全可靠的方式向 CA 提供其公共密鑰以獲得證書,這樣用戶就可公開其證書,而任何需要此用戶的公共密鑰者都能得到此證書,并通過 CA 檢驗密鑰是否正確。這一標準的最新版本 -- X.509 版本 3 是針對包含擴展信息的數字證書,提供一個擴展字段,以提供更多的靈活性及特殊環境下所需的信息傳送。
?? 為了進行身份認證, X.509 標準及公共密鑰加密系統提供了一個稱作數字簽名的方案。用戶可生成一段信息及其摘要(亦稱作信息“指紋”)。用戶用專用密鑰對摘要加密以形成簽名,接收者用發送者的公共密鑰對簽名解密,并將之與收到的信息“指紋”進行比較,以確定其真實性。
?? 目前, X.509 標準已在編排公共密鑰格式方面被廣泛接受,已用于許多網絡安全應用程序,其中包括 IP 安全( Ipsec )、安全套接層( SSL )、安全電子交易( SET )、安全多媒體 INTERNET 郵件擴展( S/MIME )等。
?
創建X509 證書
創建X509證書方法較多,在Windows 環境下大致總結了幾中辦法,
1)????? 通過CA獲取證書,
2)????? 通過微軟提供的makecert 工具得到測試證書
3)????? 編程的方法創建,.Net提供了 X509Certificate2 類,該類可以用于創建證書,但只能從RawData中創建,創建后無法修改除FriendlyName以外的任何屬性。
?
我在互聯網上找了很久,始終沒有找到完全通過程序創建自定義的證書的方法。后來想了一個折中辦法,就是用程序調用 makecert.exe 先生成一個證書,證書的一些參數如Subject,有效期,序列號等可以通過參數傳入,然后把生成的證書文件讀到Rawdata中,得到X509Certificate2 類型的證書對象。當然這種方法確實比較笨,必須要依賴外部進程。等后面有時間的話,我還是想按照X509 V3 標準,自己創建RawData,然后生成證書,這樣應該是比較靈活的做法。不知道網友們有沒有什么更好的方法來創建一個自定義的證書。
?
通過 makecert.exe 創建X509證書的代碼如下,供大家參考
?
static object semObj = new object();
?
/// <summary>
/// 自定義的證書信息
/// </summary>
public class T_CertInfo
{
??? public String FriendlyName;
??? public String Subject;
??? public DateTime BeginDate;
??? public DateTime EndDate;
??? public int SerialNumber;
}
?
/// <summary>
/// 生成X509證書
/// </summary>
/// <param name="makecrtPath">makecert進程的目錄</param>
/// <param name="crtPath">證書文件臨時目錄</param>
/// <param name="certInfo">證書信息</param>
/// <returns></returns>
public static X509Certificate2 CreateCertificate(String makecrtPath, String crtPath,
??? T_CertInfo certInfo)
{
??? Debug.Assert(certInfo != null);
??? Debug.Assert(certInfo.Subject != null);
?
??? string MakeCert = makecrtPath + "makecert.exe";
??? string fileName = crtPath + "cer";
?
??? string userName = Guid.NewGuid().ToString();
?
??? StringBuilder arguments = new StringBuilder();
?
??? arguments.AppendFormat("-r -n \"{0}\" -ss my -sr currentuser -sky exchange ",
??????? certInfo.Subject);
?
??? if (certInfo.SerialNumber > 0)
??? {
??????? arguments.AppendFormat("-# {0} ", certInfo.SerialNumber);
??? }
?
??? arguments.AppendFormat("-b {0} ", certInfo.BeginDate.ToString(@"MM\/dd\/yyyy"));
??? arguments.AppendFormat("-e {0} ", certInfo.EndDate.ToString(@"MM\/dd\/yyyy"));
??? arguments.AppendFormat("\"{0}\"", fileName);
?
??? lock (semObj)
??? {
??????? Process p = Process.Start(MakeCert, arguments.ToString());
??????? p.WaitForExit();
?
??????? byte[] certBytes = ReadFile(fileName);
??????? X509Certificate2 cert = new X509Certificate2(certBytes);
??????? cert = new X509Certificate2(certBytes);
?
??????? if (certInfo.FriendlyName != null)
??????? {
??????????? cert.FriendlyName = certInfo.FriendlyName;
??????? }
?
??????? return cert;
??? }
}
?
?
internal static byte[] ReadFile(string fileName)
{
??? using (FileStream f = new FileStream(fileName, FileMode.Open, FileAccess.Read))
??? {
??????? int size = (int)f.Length;
??????? byte[] data = new byte[size];
??????? size = f.Read(data, 0, size);
??????? return data;
??? }
}
?
獲取證書私鑰
通過上述方法得到的X509證書,只能獲取其公鑰信息,由于公鑰私鑰是成對出現的,如果我們要在程序中使用該證書來加解密,就必須要獲取公鑰對應的那個私鑰。一樣是在互聯網上沒有找到很好的解決辦法,只能自己研究。目前總結出兩種方法,給大家分享:
第一種方法:
從密鑰容器中獲取私鑰。具體方法如下:
首先在 makecert 的參數中要加入一條 -sk keyname ?指定主題的密鑰容器位置,該位置包含私鑰。如果密鑰容器不存在,系統將創建一個。
然后 在執行完 p.WaitForExit(); 這一句后執行下面語句獲取私鑰和私鑰參數
RSAParameters privateKey;
RSACryptoServiceProvider rsa = GetKeyFromContainer("keyname");
privateKey = rsa.ExportParameters(true);
?
public static RSACryptoServiceProvider GetKeyFromContainer(string ContainerName)
{
??? // Create the CspParameters object and set the key container
??? // name used to store the RSA key pair.
??? CspParameters cp = new CspParameters();
??? cp.KeyContainerName = ContainerName;
?
??? // Create a new instance of RSACryptoServiceProvider that accesses
??? // the key container MyKeyContainerName.
??? return new RSACryptoServiceProvider(cp);
}
?
這種方法有一個缺點就是程序的調用者必須要具備讀取密鑰容器的權限才行,如果是Web應用,由于IIS來賓帳戶沒有這個權限,將無法讀取密鑰容器中的密鑰。嘗試采用模擬超級用戶登錄的方法(NetworkSecurity.ImpersonateUser),也無法解決這個問題,而且這樣做我個人覺得對網站的安全性方面也不是很好。后來想出了第二種方法,就是干脆重置密鑰對,用自己生成的密鑰對替換證書中的密鑰對,試了一下,還是行之有效的。
第二種方法:
重置密鑰對,方法如下:
首先要生成一個加密算法和加密位數與makecert生成的證書密鑰相同的密鑰。通過實測發現makecert采用交換密鑰時,默認產生一個1024位RSA密鑰,Exponent 為1,0,1,這和
RSACryptoServiceProvider 默認的密鑰是相同的。所以只要用 RSACryptoServiceProvider RSA = new RSACryptoServiceProvider() 生成一個密鑰就可以了。
?
第二步就是替換,也就是將密鑰文件中公鑰參數替換為要置換的公鑰參數。
?
RSAParameters publicKey;
RSAParameters privateKey;
?
RSACryptoServiceProvider RSA = (RSACryptoServiceProvider)cert.PublicKey.Key;
publicKey = RSA.ExportParameters(false);
?
//查找公鑰參數在RawData中的位置
if (publicKey.Modulus.Length != 128 || publicKey.Exponent.Length != 3)
{
??? throw new Exception("public key module lenght != 128!");
}
?
if (publicKey.Exponent[0] != 1 ||
??? publicKey.Exponent[1] != 0 ||
??? publicKey.Exponent[2] != 1)
{
??? throw new Exception("public key Exponent != 101!");
}
?
byte[] module = publicKey.Modulus;
?
int i = 0;
int matchCnt = 0;
int modulePos = 0;
int j = 0;
?
while (i < certBytes.Length)
{
?
??? if (certBytes[i] == module[j]) //cerBytes 為 RawData,什么可以參加創建證書的代碼
??? {
??????? i++;
??????? j++;
?????? ?matchCnt++;
?
??????? if (matchCnt == 128)
??????? {
??????????? modulePos = i - 128;
??????????? break;
??????? }
??? }
??? else
??? {
??????? if (matchCnt == 128)
??????? {
??????????? modulePos = i - 128;
??????????? break;
??????? }
??????? else
????? ??{
??????????? matchCnt = 0;
??????????? j = 0;
??????????? i++;
??????? }
??? }
}
?
//創建密鑰對
RSA = new RSACryptoServiceProvider();
publicKey = RSA.ExportParameters(false);
privateKey = RSA.ExportParameters(true);
?
//將要重置的密鑰對中的公鑰參數覆蓋原參數
?
j = 0;
for (i = modulePos; i < modulePos + 128; i++)
{
??? certBytes[i] = publicKey.Modulus[j];
??? j++;
}
?
//用新參數重新創建證書
cert = new X509Certificate2(certBytes);
?
這樣一來privateKey就成了新證書的私鑰了。
?
這種方法的問題:
這種方法的問題是查找證書中公鑰信息,是通過匹配的方式來做的,這是一個偷懶的方法,正確的做法應該是按照標準的定義來查找,由于暫時沒有太多時間去仔細研究標準,所以就偷了一個懶,但感覺這種方法目前來說還是行之有效的,待以后改進吧。
?
?
參考資料
http://www.ietf.org/rfc/rfc2459.txt?IETF 的X509 V3 版本標準全文
http://blog.csdn.net/chinaipcnet/archive/2007/05/23/1621989.aspx?makecert.exe使用說明
?from :http://www.cnblogs.com/eaglet/archive/2007/07/11/814600.html