我們說的資訊安全是相對於系統安全而言的,它更側重於加密,解密,數位簽章,驗證,認證等等.而系統安全主要側重於系統本身是否有安全性漏洞,如常見的由於軟體設計的不完善而導致的滿天飛的緩衝區溢位等等.
Java語言中負責加密功能的組件是JCE(Java Crypto Extenstion),它使用開放式的思想,可以允許使用者自己編寫密碼編譯演算法的具體實現的模組等.這些東西被稱為JCE Provider,JCE的提供者.SUN公司本身提供了一些Provider.不過我推薦使用Bouncy Castle的Provider.原因是它實現的密碼編譯演算法多,連比較新的橢圓曲線(ECC)演算法都有了.去http://www.bouncycastle.org/可以找到你所希望的.Bouncy Castle除了提供Provider本身以外,還包括了一個S/MIME和一個open pgp 的jar包只有Provider本身是必要的,後兩個包是方便你編程而提供的.例如有了S/MIME包後,你就不再需要為諸如"加密一個字串或者一片文章然後簽名"之類的很現實的應用程式寫上一大堆代碼了,只要引用S/MIME包中的某幾個類,很快就可以搞定.而open pgp的存在,使你在用Java編寫和PGP/GPG互動的程式時方便多了.
用Java寫資訊安全方面的程式需要做的準備工作已經說清楚了.現在假設你已經是一個對Java語言本身比較熟悉,能夠用Java寫一些程式的人,並且這些該下載的jar包已經下載好了,可以正式開工了.
在所有的此類程式的開頭(無論是在類的建構函式中也好,在初始化函數中也好),都要先來上這麼一句:Security.addProvider(new BouncyCastleProvider());將BouncyCaslte的Provider添加到系統中,這樣以後系統在運行相關程式的時候調用的就是這個Provider中的密碼編譯演算法.
然後我們就可以開始開CA了.首先,作為一個CA要有自己的一對公開金鑰和私密金鑰,我們先要產生這麼一對.使用KeyPairGenerator對象就可以了,調用KeyPairGenerator.getInstance方法可以根據要產生的密鑰類型來產生一個合適的執行個體,例如常用的RSA,DSA等.然後調用該對象的initialize方法和generateKeyPair方法就可以產生一個KeyPair對象了.然後調用KeyPair對象中的相應方法就可以擷取產生的金鑰組中的公開金鑰和私密金鑰了.
有了公開金鑰和私密金鑰對以後,下面的一個很現實問題就是如何把它們儲存起來.通常我們要對這些安全性實體,如公開金鑰,私密金鑰,認證等先進行編碼.編碼的目的是為了把結構複雜的安全性實體變成位元組流以便儲存和讀取,如DER編碼.另外,通常還把DER編碼後的位元組流再次進行base64編碼,以便使位元組流中所有的位元組都變成可列印的位元組.
在Java語言中,這些安全性實體基本上都有getEncoded()方法.例如:
byte[] keyBytes = privateKey.getEncoded();
這樣就把一個私密金鑰進行了DER編碼後的結果儲存到一個byte數組中了.然後就可以把這個byte數組儲存到任何介質中.如果有必要的話,可以使用BC Provider中的Base64編碼解碼器類進行編碼,就像這樣:
byte data[] = Base64.encode(keyBytes);
要從檔案中讀取私密金鑰則應該這樣:
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyData);
KeyFactory kfac = KeyFactory.getInstance("RSA");
privateKey = kfac.generatePrivate(spec);
這裡說明一下,對RSA私密金鑰進行編碼就會自動地按照PKCS8進行.因此讀取的時候將包含編碼的位元組數組作為PKCS8EncodedKeySpec對象的建構函式的參數就可以產生一個該類型的對象.然後建立一個密鑰工廠對象就可以按照要求產生一個RSA私密金鑰了.很顯然這裡的keyData應該是和上面的keyBytes內容相同.
為了提高系統的安全性,通常私密金鑰在經過DER編碼後,還會使用一個口令進行加密,然後再儲存在檔案系統中.在使用私密金鑰的時候,如果沒有正確的口令,是無法把私密金鑰還原出來的.
儲存認證和儲存私密金鑰類似.Certificate對象中也有一個getEncoded的方法.
這次就講這些.大家應該可以把這些安全性實體很熟練地從檔案系統和記憶體之間來回地折騰了吧.這對以後實現CA是很重要的.下面我會講一下認證中表示主體的方法:DN.
上面我們講到如何產生金鑰組,以及如何將諸如公開金鑰,私密金鑰,認證等這一類安全性實體在檔案系統和記憶體之間來迴轉換.這些是準備開CA的基本功,現在我們講一下CA的基本原理以及如何使用主體名稱結構DN(Distinguish Name)來表示每一個認證中的主體.
一份認證就是一個權威機構對一個主體的身份的確認的證明.即一份認證表示一個權威機構確認了一個主體就是它自己,而不是其它的冒名頂替者.主體可以是一個個人,也可以不是,例如,在需要安全服務的時候,需要為一台網站的伺服器頒發認證,這種認證的主體就是一台伺服器.簽署認證的權威機構就叫做CA,該權威機構本身也是一個主體.權威機構通過對包含有待認證的主體的一系列資訊的待簽署憑證"(TBS,to be signed)進行數位簽章來表示該機構對它的認可.一份包含了待認證的主體的相關資訊的TBS再加上CA使用自己的私密金鑰進行簽名產生的位元組流放在一起,就構成了一份標準的X509認證.
一個TBS中包含了以下這些主要資訊:
認證的版本,通常是3(X509v3)
認證的序號,RFC3280中規定,每個CA必須確保它頒發的每一份認證的序號都是唯一的,並且序號只能使用非負整數.
簽發者(CA)的主體名稱,一個DN對象.
認證的有效期間,表示方法是兩個時間值,表示了該認證從何時開始生效,何時開始作廢.
待認證的主體的主體名稱,也是一個DN對象.
待認證的主體的公開金鑰,任何安全應用在確認完認證的有效性後,就可以開始使用該主體的公開金鑰與之進行安全通訊.
如果是X509v3認證,即版本號碼是3的話,後面還有一個認證擴充資訊欄位,可以在認證裡面添加一些其它的資訊.
下面我們來看一下表示主體的主體名稱結構:DN.這個結就構是一個屬性的集合.每個屬性有屬性名稱和屬性值.它的作用就是用來表示"我是誰",也就是說,這個認證到底是誰頒發給誰的,這個認證對應的公開金鑰是誰擁有的.
通常使用一個字串來表示DN結構,這種字串說明了這種結構中的各個屬性的類型和值:
C=CN;S=BeiJing;L=BeiJing;O=PKU;OU=ICST;CN=wolfenstein
這裡C是國家和地區代碼,S和L都是地區代碼,S相當於省或者州這樣的層級,L相當於城市層級,O是組織機構名稱,OU是次級組織機構名稱,CN是主體的通用名(common name).在這裡,C,S,L等等屬性的類型都是相對固定的,例如C一般就是用來表示國家和地區代碼,在DN結構中還可以添加一些其它類型的資訊,一般也都是以"xxx=xxx"這樣來表示的.
下面我們來說明如何在Java語言中構造出一個主體名稱對象.
BC Provider中使用X509Name對象來表示DN,構造一個X509Name的步驟大體如下:
先構造兩個vector對象,用來表示屬性名稱和屬性值:
Vector oids = new Vector();
Vector attributes = new Vector();
然後在oids這個用來表示屬性名稱的vector對象中將屬性名稱一個一個添加進去:
oids.addElement(X509Name.C);
......
oids.addElement(X509Name.CN);
X509Name對象裡面有若干常量,例如上面的X509Name.C.還有X509Name.ST等等,都可以和上面舉的例子對應起來.
然後將屬性值添加到attributes對象中:
attributes.addElement("CN");
......
attributes.addElement("Wolfenstein");
最後就可以構造出一個X509Name對象:
X509Name SubjectDN = new X509Name(oids, attributes);
這樣,我們的主體名稱結構就確立起來了.
下面我們就可以講關鍵的部分了,那就是如何用Java程式完成CA最重要的功能,簽署認證.
要做CA,第一步要準備好自己的認證和私密金鑰.私密金鑰如何從檔案裡面讀取出來前面已經講過了.從檔案系統中讀出認證的代碼如下:
CertificateFactory certCF = CertificateFactory.getInstance("X.509");
X509Certificate caCert = certCF.generateCertificate(certBIS);
這裡cerBIS是一個InputStream類型的對象.例如一個標準的X509v3格式的認證檔案所形成的輸入資料流.
第二步就是從使用者那裡擷取輸入,然後構造主體名稱結構DN,如何構造DN上次已經說過了,如何從使用者那裡擷取輸入,這個不在本文討論範圍之內.
下一步就是擷取使用者的公開金鑰,好和他所需要的認證對應起來.也有不少CA的做法就是在這裡替使用者現場產生一對金鑰組,然後把公開金鑰放到認證中籤發給使用者.這個應該看實際需要選擇合適的方式.
現在一切資訊都已經準備好了,可以簽發認證了,下面的代碼說明了這個過程:
//構造一個認證產生器對象
X509V3CertificateGenerator certGen = new X509V3CertificateGenerator();
// 從CA的認證中擷取簽發者的主體名稱(DN)
// 這裡有一點小技巧,我們要把JCE中定義的
// 用來表示DN的對象X500Principal轉化成在
// BC Provider中的相應的對象X509Name
// 先從CA的認證中讀取出CA的DN進行DER編碼
DERInputStream dnStream =
new DERInputStream(
new ByteArrayInputStream(
caCert.getSubjectX500Principal().
getEncoded()));
// 馬上又從編碼後的位元組流中讀取DER編碼對象
DERConstructedSequence dnSequence =
(DERConstructedSequence)dnStream.readObject();
// 利用讀取出來的DER編碼對象建立X509Name
// 對象,並設定為認證產生器中的"簽發者DN"
certGen.setIssuerDN(new X509Name(dnSequence));
// 設定好認證產生器中的"接收方DN"
certGen.setSubjectDN(subjectDN);
// 設定好一些擴充欄位,包括簽發者和
// 接收者的公開金鑰標識
certGen.addExtension(X509Extensions.SubjectKeyIdentifier, false,
createSubjectKeyId(keyToCertify));
certGen.addExtension(X509Extensions.AuthorityKeyIdentifier, false,
createAuthorityKeyId(caCert.getPublicKey()));
// 設定認證的有效期間和序號
certGen.setNotBefore(startDate);
certGen.setNotAfter(endDate);
certGen.setSerialNumber(serialNumber);
// 設定簽名演算法,本例中使用MD5hash後RSA
// 簽名,並且設定好主體的公開金鑰
certGen.setSignatureAlgorithm("MD5withRSA");
certGen.setPublicKey(keyToCertify);
// 如果以上一切都正常地話,就可以產生認證了
X509Certificate cert = null;
cert = certGen.generateX509Certificate(caPrivateKey);
這裡是上面用到的產生簽發者公開金鑰標識的函數:
protected AuthorityKeyIdentifier createAuthorityKeyId(PublicKey pubKey)
{
AuthorityKeyIdentifier authKeyId = null;
try
{
ByteArrayInputStream bIn = new ByteArrayInputStream(pubKey.getEncoded());
SubjectPublicKeyInfo info = new SubjectPublicKeyInfo(
(DERConstructedSequence)new DERInputStream(bIn).readObject());
authKeyId = new AuthorityKeyIdentifier(info);
}
catch (IOException e)
{
System.err.println("Error generating SubjectKeyIdentifier: " +
e.toString());
System.exit(1);
}
return authKeyId;
}
產生主體公開金鑰標識的函數和上面的類似,把AuthorityKeyIdentifier替換成SubjectKeyIdentifier就可以了.
這裡要注意的是,CA的公開金鑰也是在一份認證裡,這種認證的特點是簽發者DN和接收者DN一樣,也就是說,這種認證是CA自己給自己頒發的認證,也就是"自我簽署憑證",它上面的公開金鑰是CA自身的公開金鑰,用來簽名的私密金鑰就是該公開金鑰對應的私密金鑰.一般每個CA都要有這麼一份認證,除非該CA不是根CA,即它的權威性不是由它自己證明,而是由它的上級CA證明.但是,最後總歸要有一個根CA,它為各個安全應用程式的使用者所信賴.
到這裡我們已經把CA最基本的功能如何用Java實現講完了,下面講如何從PKCS#10格式認證請求檔案中讀取出使用者資訊,然後直接簽發公開金鑰.
前面已經把如何用Java實現一個CA基本上講完了.但是他們都有一個特點,就是使用者的資訊都是在現場擷取的,不能做申請和簽發相分離.現在我們要講述的是PKCS#10認證請求檔案.它的作用就是可以使申請和簽發相分離.
PKCS#10認證請求結構中的主要資訊包含了被簽發者(認證申請者)的主體名稱(DN)和他的公開金鑰.因此一個CA在擷取到一個PKCS#10認證請求後,就可以從中擷取到任何和簽發認證有關的資訊,然後用它自己的私密金鑰簽發認證.
使用BC Provider在Java中構造一個認證請求格式的對象調用其建構函式即可,這個函數如下:
PKCS10CertificationRequest(java.lang.String signatureAlgorithm, X509Name subject, java.security.PublicKey key, ASN1Set attributes, java.security.PrivateKey signingKey)
它的參數是自簽名演算法,認證申請者的DN,認證申請者的公開金鑰,額外的屬性集(就是要申請的認證的擴充資訊),申請認證者的私密金鑰.申請認證者的私密金鑰僅僅是用來進行一下自簽名,並不出現在認證請求中,需要自簽名的目的是保證該公開金鑰確實為申請者所有.
調用該對象的getEncoded()方法可以將其進行DER編碼,然後儲存起來,該對象還有另一個建構函式:
PKCS10CertificationRequest(byte[] bytes)
這個建構函式的作用就是直接從儲存的DER編碼中把這個對象還原出來.
利用認證請求結構進行認證簽發的代碼如下,這裡假設CSR是一個已經擷取出來的PKCS10CertificationRequest結構:
PublicKey SubjectPublicKey = CSR.getPublicKey();
CertificationRequestInfo CSRInfo = CSR.getCertificationRequestInfo();
X509Name SubjectDN = CSRInfo.getSubject();
ASN1Set Attributes = CSRInfo.getAttributes();
這樣,申請者的主體DN,申請者的公開金鑰,申請者希望在認證擴充資訊中填寫的屬性都得到了,剩下的事情就和使用者在現場輸入時一樣了,其它的資訊一般是申請者不能決定的.另外認證請求格式中有一樣資訊沒有明確給出來,那就是認證的有效期間,這個應該單獨詢問使用者,或者用其它的方法儲存起來.