哈工大密码学实验(CA证书认证系统)
前言
本文是哈工大17級密碼學原理與實踐課程的實踐部分(CA證書認證系統)實驗報告,由于本實驗代碼中包含了數據庫部分,每個人的電腦配置環境也不一樣,所以,提供的參考代碼不會直接運行成功,但給大家提供了寫實驗的一些思路。本實驗報告是最終版,非常詳盡。
本文僅供參考。希望各位學弟學妹認真對待實驗,在學習時間充足的情況下,借此大大提高自己的編程能力。
代碼下載地址:哈工大密碼學實驗(CA證書認證系統)
以下正文。
1. 背景與意義
CA認證系統,即CA證書頒發系統,是公鑰基礎設施(PKI)中的核心環節,是公鑰加密過程中的第三方權威認證方,負責密鑰和證書的產生、發布、管理、存儲和撤銷等功能,廣泛用于電子商務等需要非對稱加密的信息傳輸場景中。所有通過CA的信息傳輸方,都要無條件的信任CA的公正性,在消息傳輸的過程中,CA為信息傳輸的雙方提供公私鑰加密環境,提供身份認證、安全傳輸、不可否認性和數據完整性等功能。
公鑰基礎設施PKI( Publie Key Infrastrueture, 簡稱PKI)是利用公鑰理論和技術建立的提供安全服務的基礎設施, 是信息安全 的核心,也是電子商務安全的關鍵所在。PKI技術采用證書管理公鑰, 通過第三方的可信任機構—認證中心CA(Certificate Authority),把用戶的公鑰和用戶的其他標識信息(如名稱、E-mail、身份證號等)捆綁在一起,在Internet上驗證用戶的身份(其中認證機構CA是PKI系統的核心部分)。目前, 通用的辦法是采用建立在PKI基礎之上的數字證書,通過把要傳輸的數字信息進行加密和簽名,保證信息傳輸的機密性、真實性、完整性和不可否認性,從而保證信息的安全傳輸。
2. 國內外應用現狀
美國是最早提出PKI概念的國家,并于1996年成立了美國聯邦PKI籌委會。與PKI相關的絕大部分標準都有美國制定,其PKI技術在世界上處于領先地位。2000年6月30日,美國總統克林頓正式簽署美國《全球及全國商業電子簽名法》,給與電子簽名、數字證書以法律上的保護,這一決定使電子認證問題迅速成為各國政府關注的熱點。加拿大在1993年就已經開始了政府PKI體系雛形的研究工作,到2000年已經在PKI體系方面獲得重要進展。加拿大與美國代表了發達國家PKI發展的主流。
歐洲在PKI基礎建設方面也成績顯著。已頒布了93/1999EC法規,強調技術中立、隱私權保護、國內與國外相互認證以及無歧視等原則。為了解決各國PKI之間的協同工作問題,他采取了一系列策略:如積極自主相關研究所、大學和企業研究PKI相關技術;自主PKI互操作性相關技術研究,并建立CA網絡及其頂級CA。并于2000年10成立了歐洲橋CA指導委員會,于2001年3月23日成立歐洲橋CA。
我國的PKI技術從1998年開始起步,由于政府和各有關部門近年來對PKI產業的發展給予了高度重視,2001年PKI技術被列為”十五“863計劃信息安全主題重大項目,并于同年10月成立了國家863計劃信息安全基礎設施研究中心。國家計委也在制定新的計劃來支持PKI產業的發展,在國家電子政務工程中明確提出了要構建PKI體系。目前,我國已全面推動PKI技術研究于應用。
1998年國內第一家以實體形式運營的上海CA中心(SHECA)成立。目前,國內的CA機構分為區域型、行業型、商業性和企業型四類;截至2002年底,前三種CA機構已有60余家,58%的省市建立了區域CA,部分部委建立了行業CA。其中全國型的行業CA中心有中國金融認證中心CFCA、CTCA中國電信認證中心等。區域型CA有一定地區性,也稱地區CA,如上海CA中心、廣東電子商務認證中心。
3. 需求分析
3.1 總體需求
(圖片略)
3.2 功能需求
本次實驗中,CA的功能需求主要有如下幾點:
接收驗證用戶數字證書的申請
為了實現接收驗證用戶數字證書的申請,需要用戶向CA提供自己的身份信息,并提供申請機構的相關有效信息。
生成證書
要生成一個證書,需要設計好證書的內容以及格式。
存儲證書
要存儲一個證書,需要設計好證書的存儲形式,以及是否需要加密。
向申請者頒發(或拒絕頒發)數字證書
向申請者頒發數字證書,需要設計好如何想申請者頒發證書,頒發過程中是否需要加密傳輸。在這為了模擬CA的功能,將不實現拒絕頒發功能。
接收用戶的數字證書的查詢、撤銷
需要設計好數字證書的查詢和撤銷需要哪些權限,用戶需要提供哪些信息來進行查詢和撤銷。
產生和發布證書的有效期
需要設計好如何為用戶產生證書的有效期,有效期的時長選擇以及如何發布有效期。
數字證書的歸檔
需要設計好如何將數字證書歸檔,是以數據庫的形式還是以文件的形式。
密鑰歸檔
同數字證書的歸檔。
3.3 性能需求
本CA系統未考慮網站性能需求。
3.4 技術選型
- html+css+javascript
- jdbc+druid+mysql+beanutils
- jquery+jstl+standard
- dom4j
- jsencrypt.min.js+sha256-min.js
4. 概要設計
圍繞著需求分析,將展開如下設計:
接收驗證用戶數字證書的申請
生成證書
生成證書完全是后端的內容,用戶在提交證書申請的相關信息后,由服務器在后臺處理相關信息,并生成固定格式的.mycer證書文件。
存儲證書
證書一律以文件的形式存儲在D:\DriveY\IntelliJ\cryptotw2\out\artifacts\cryptotw2_Web_exploded\download路徑下。在數據庫中,證書以如下形式字段存儲相關信息。
向申請者頒發(或拒絕頒發)數字證書
接收用戶的數字證書的查詢、撤銷
產生和發布證書的有效期
證書的有效期為從證書制作時間起,1年/2年/3年止,時限由用戶在申請證書時選擇。
數字證書的歸檔
數字證書的歸檔一律以文件的形式在D:\DriveY\IntelliJ\cryptotw2\out\artifacts\cryptotw2_Web_exploded\download路徑下。詳細信息以字段形式保存在數據庫中。
密鑰歸檔
密鑰(申請人的公鑰)一律以文件的形式保存在D:\DriveY\IntelliJ\cryptotw2\out\artifacts\cryptotw2_Web_exploded\upload路徑下。密鑰內容不會在數據庫中保存。
5. 細節設計
本部分,細節設計報告說明將以逐個功能、技術要點的形式呈現,邏輯實現請參見Java doc文檔。
本實驗使用的前端模板取自 大氣黑色注冊表單html5模板。包括首頁在內,有相當部分的css代碼經本人親手改造后呈現。
由于某些界面不能夠全部展示,將以一定比例的縮放給出界面貼圖,可能存在貼圖比例失衡的情況。
由于撰寫報告時,仍有些功能正在調試,同一功能的前后貼圖可能存在不一樣的地方。
加密部分和數據結構部分將在小節中集中體現。
5.1 登陸/注冊界面
登陸界面中有一個標題和一個container面板,面板中左半部分是登錄部分,右半部分是注冊部分,上圖中,“記住我“,”忘記密碼?“和”其他方式登錄“功能都是無效的。
5.1.1 注冊
用戶注冊需要輸入用戶名、密碼、身份證等信息,需要滿足以下幾個條件:
- 用戶名必須為字母、數字、下劃線、減號的組合,長度為6~16位
- 密碼至少包括數字和字母,長度至少為6位,至多20位。
- 身份證信息必須滿足中華人民共和國居民18位身份證號要求
在用戶輸入合法信息并點擊注冊后,相關信息會經過加密存入到數據庫的ra_user表中:
注意,此處為了演示,保留了一個用戶名為shen,密碼為123的簡單用戶,實際上這個用戶的用戶名和密碼是非法的,不能被注冊。
5.1.2 登錄
用戶登錄需要輸入用戶名、密碼信息,用戶名和密碼必須屬于同一用戶。后臺將從數據庫中提取注冊密碼密文和鹽值,對用戶輸入的密碼加密后與密文比對,如果匹配成功,則驗證登陸成功。
5.2 主頁面
主頁面涉及兩個部分,一個是置于頁面頂端中間的用戶登錄狀態,以及頁面中間的container面板。
5.2.1 用戶登錄狀態(Filter)
其實最開始的時候沒有做這個功能,后來才考慮到加進去的。記錄用戶登錄狀態的意義在于,一個是要保證所有訪問本網站的用戶,都必須登錄后才能訪問網站內的資源(主要指除了登錄/注冊頁面的其它頁面),另一個是記錄此時用戶的登錄狀態,將證書和申請時的登錄用戶關聯,以便在撤銷證書時,必須輸入證書申請時的用戶的登錄密碼,起到了驗證身份的作用,限制了撤銷權限。
后面的頁面在這個位置都會有用戶登錄狀態,不再贅述。
5.2.2 主操作面板
主操作面板中一共包括6個功能:
- 申請證書
- (下載)密鑰生成器
- 下載證書
- 撤銷證書
- 下載 CRL(證書撤銷列表)
- 查看證書(包含下載功能)
5.3 申請證書
申請證書頁面包括一個container,其中有用戶申請證書需要提交的表單。該表單包括:
- 組織機構
- 工商注冊號
- 法人姓名
- 經辦人姓名
- 經辦人電話
- 有效期(1年/2年/3年)
- 密鑰文件
其中,組織機構、工商注冊號、法人姓名、經辦人姓名、經辦人電話,為了簡單起見,都沒有做拼寫檢查或合法性檢查,有效期只能三選一,密鑰文件從本地上傳(該文件應該是本網站提供的密鑰生成器genkey.exe生成的公鑰文件)。
5.4 密鑰生成器
點擊密鑰生成器后,馬上從瀏覽器獲取一個genkey.exe文件,運行該可執行程序將在與該文件所在路徑同級的目錄下生成兩個文件pk.key和sk.key。前者為公鑰文件,后者為私鑰文件,兩者共同組成一對 1024bits 的 RSA 公私密鑰對。
5.5 下載證書
下載證書頁面非常簡單,只需要輸入要下載的證書序列號,就可以從瀏覽器獲取相應的證書文件。
5.6 撤銷證書
撤銷證書頁面也非常簡單,只需要輸入要下載的證書序列號,并且輸入相應的撤銷身份驗證密碼,就可以成功撤銷該證書。證書被撤銷后不會從證書庫中刪除,但會加入到 CRL 庫中。
撤銷身份驗證密碼指的是證書在申請時,正在操作的用戶的登錄密碼。這樣設計雖然比較簡單,但是限制了撤銷權限,將撤銷功能與用戶登錄狀態相關聯,由于證書序列號是(對所有人)可見的,所以限制請求者只有知道證書申請時的登錄密碼,才可以撤銷成功。
其實大型公證CA系統的證書的申請和撤銷都是比較嚴謹的工作,通常需要多方核實,并且在一到三個工作日內給出證書的申請和撤銷反饋。本次實驗中,申請和撤銷都是自動化的,對于證書的申請沒有加權限,證書的撤銷也只加了登錄密碼的權限限定。
5.7 下載 URL
點擊下載 CRL會直接從瀏覽器獲取一個 CRL 文件,CRL文件的具體結構將在數據結構小節中介紹。
5.8 查看證書
查看證書功能將數據庫certificate表中的信息抽取出來,分頁展示在前端網頁。前端展示提供了證書的序列號、組織機構、工商注冊號、證書有效期起、證書有效期止信息,并提供了一個下載鏈接,單機該鏈接可以直接獲取到該證書文件。
此時數據庫中只有兩條記錄,為了方便展示,所以在這里設計成每頁只有一條記錄。
5.9 數據結構設計
5.9.1 證書
證書的設計是本次實驗CA系統的重頭戲,現在互聯網上比較流行的證書為X.509結構。
X.509是密碼學里公鑰證書的格式標準。X.509證書已應用在包括TLS/SSL在內的眾多網絡協議里,同時它也用在很多非在線應用場景里,比如電子簽名服務。X.509證書里含有公鑰、身份信息(比如網絡主機名,組織的名稱或個體名稱等)和簽名信息(可以是證書簽發機構CA的簽名,也可以是自簽名)。對于一份經由可信的證書簽發機構簽名或者可以通過其它方式驗證的證書,證書的擁有者就可以用證書及相應的私鑰來創建安全的通信,對文檔進行數字簽名。
另外除了證書本身功能,X.509還附帶了證書吊銷列表和用于從最終對證書進行簽名的證書簽發機構直到最終可信點為止的證書合法性驗證算法。
X.509是ITU-T標準化部門基于他們之前的ASN.1定義的一套證書標準。
證書組成結構標準用ASN.1(一種標準的語言)來進行描述. X.509 v3 數字證書結構如下:
- 證書
- 版本號
- 序列號
- 簽名算法
- 頒發者
- 證書有效期
- 此日期前無效
- 此日期后無效
- 主題
- 主題公鑰信息
- 公鑰算法
- 主題公鑰
- 頒發者唯一身份信息(可選項)
- 主題唯一身份信息(可選項)
- 擴展信息(可選項)
- …
- 證書簽名算法
- 數字簽名
——引用自維基百科
像csdn網站的證書文件如下:
參考X.509證書結構,設計自己的證書結構如下:
- 序列號(Serial Number: )——以當前時間為前綴的組織機構名稱(如,20191222091200HIT)的16進制md5散列值。
- 簽名算法(Sign Algorithm: )——sha1RSA
- 簽名哈希算法(Encrypt Algorithm: )——sha256
- 頒發者——GothamCityTrust RSA CA 2019, www.tofushen.com, Gotham City Trust, CN
- 有效期從(Valid Time From: )——當前日期,如2019年12月21日 21:32:00
- 到(Valid Time To: )——當前日期向后(1年/2年/3年),如2022年12月21日 21:32:00
- 使用者(User: )——組織機構名
- 公鑰(Public Key: )——公鑰文件字串
- 簽名(Sign: )——公鑰文件字串的簽名值,使用sha1RSA算法哈希,RSA算法加密,CA私鑰加密。
樣例如下:
證書文件內容為文本形式,文件格式為.mycer。
因為證書的申請要和登錄者信息相關聯,所以在數據庫中存儲的證書申請信息如下:
- id
- 序列號
- 組織機構
- 工商注冊號
- 文件路徑(用于下載證書)
- 有效期起
- 有效期止
- 法人姓名
- 經辦人姓名
- 經辦人電話
- 申請人登錄用戶名
5.9.2 證書吊銷列表(CRL)
在一些密碼系統的運作中(一般情況下是公開密鑰基礎建設[注 1]的系統),證書吊銷列表(英文:Certificate revocation list,縮寫:CRL,或譯作證書廢止清冊[參 1])是尚未到期就被證書頒發機構吊銷的數字證書的名單。這些在證書吊銷列表中的證書不再會受到信任。
當前,在線證書狀態協議可以代替本列表實現證書狀態檢查。
——引用自維基百科
要從 CA 獲得 CRL,請執行以下步驟:
——引用自Oracle文檔
從上述資料中可知,CRL首先是存儲在CA中的一個列表形式的文件,該文件可以下載到用戶主機上,并且可以通過Windows驅動安裝到系統中,和瀏覽器建立聯系,瀏覽器在瀏覽網站時,內置了許多互聯網上網站的證書和CRL,瀏覽器檢測根據證書內容和CRL驗證證書的有效性,給用戶提供可信提示。
CRL的發布存在一種博弈過程。如果CA頻繁地更新CRL,會加大CA的維護負擔,但是反而如果CA更新CRL過慢,又會導致用戶不能及時獲取CRL,可能存在一些證書已經被撤銷了,而用戶沒有來得及得知,產生信賴風險。
因此,本次實驗為了簡化模型,模擬CRL的發布,給出如下設計:
- CRL為一個.xml文件,格式為
.xml文件根標簽為<crls>,一級標簽為<crl>,每個一級標簽內存放了撤銷證書的信息,包括四個二級標簽,分別表示撤銷證書的序列號、組織機構、有效期起、有效期止。
- CRL以文件下載的形式提供給用戶,用戶需要自行到CA系統網站上下載(先登錄)。
因此,本系統的CRL是實時更新的,但把信賴程度的實時保障責任推給了用戶,如果用戶沒有及時的下載CRL,那么他將面臨信任風險。
5.10 加密部分
本次實驗涉及到的加密有非對稱加密(1024bits RSA)和哈希函數(sha256),加密場景如下:
5.10.1 前端加密和簽名(HTTP的傳輸安全)
前端加密的確是一個存在爭議的問題。要保證HTTP的傳輸安全,現在流行的方式是采用HTTPS協議和建立SSL通道。因為HTTP是明文傳輸,所以不可避免地存在報文交換的過程中,被竊聽的問題。攻擊者可以竊聽到傳輸報文,截獲報文,修改報文內容轉發,還可能實施重放攻擊。信息傳輸的雙方可以用加密的方式,保證報文的保密性,還可以用簽名的方式,保證報文的完整性,還可以用時間戳的方式,抵御重放攻擊。但是,沒有一種有效的方式,可以保證HTTP傳輸的萬無一失,因此,HTTPS協議和SSL證書通道被提出,改善HTTP傳輸的加密環境。
然而,既然沒有一種方式可以保證HTTP報文不被攻擊,那可不可以說對HTTP報文的傳輸進行加密,是無用功呢?不能說是無用功,只要是增大攻擊者的攻擊成本的加密方案都是好方案。
前端加密就存在這么一種討論。因為前端加密是在前端頁面中用JavaScript函數加密,而前端頁面很容易就可以暴露在攻擊者面前,甚至加密的密鑰都是公開的,前端隱藏技術也不能完美的保證前端的安全性,所以,是否值得在前端做加密確實存在爭議。
本系統中的前端加密采用RSA加密方案,前端公鑰加密,后端私鑰解密,公鑰以明文形式定義在JavaScript函數中,私鑰以明文的形式定義在后端代碼中。后端可以成功獲取前端傳遞的加密密文,并用對應的私鑰解密獲得明文。
本系統中用到前端加密的場景:登錄/注冊的用戶名、密碼、身份證信息;證書申請時的組織機構、工商注冊號、法人姓名、經辦人姓名、經辦人電話信息;證書撤銷時的序列號和身份驗證密碼信息。
為了提高HTTP傳輸的消息完整性,前端除了加密還用到了簽名。對消息在前端用加密的同時,給消息做一個哈希處理(sha256),將消息的哈希摘要與密文同時發送到后端,在后端對解密得到的明文做同樣的哈希處理(sha256),然后與前端傳過來的哈希摘要比對,如果比對成功,則認為消息是完整的,否則向前端回復一個“消息可能被損壞”的信息,并作廢這次提交。
之所以沒有在前端簽名的過程中,對摘要用RSA加密處理的原因是,在前端用RSA私鑰對摘要加密是不安全的,因為私鑰不應該被公開,而前端代碼是不可避免被暴露的。而用公鑰對摘要加密是沒有意義的,任何人都可以用公鑰偽造一個摘要。我認為,這里的簽名驗證的有效性,是建立在保密性之上的,如果保密性被破壞(明文被破解),那么簽名驗證也會很容易的被偽造。
5.10.2 簽名
簽名是信息傳輸中保證信息完整性的環節,大致流程為,發送方對明文做哈希處理得到一個摘要,對摘要用私鑰加密,和消息一同傳輸給接收端,接收端用發送端的公鑰解密得到摘要,再用同樣的哈希算法對消息哈希處理得到摘要,比對兩個摘要,如果兩個摘要匹配成功,則認為該消息傳輸過程中完整性得到了保障。
這是因為發送方和接收方在處理摘要的加解密時,用的是發送方的公私鑰,發送方用私鑰加密,接收方用公鑰解密,可以肯定的是,發送方的私鑰是保密的,因此,簽名是不可偽造的,如果消息被篡改,那么接收方可以通過比對兩個摘要內容不匹配,斷定消息的不完整性。
在CA給客戶簽發證書時,需要對證書中的(客戶的公鑰)做數字簽名,用的是CA的私鑰,在驗證證書有效性時,要用CA的公鑰,這樣就可以來驗證證書確實是CA頒發的。
5.10.3 RSA消息傳輸加密
由于在本系統中,包括電商和網銀在內的消息傳輸還沒有用到比較大的報文傳輸,所以為了簡便,在消息傳輸的過程中沒有使用DES加密方案,全部使用RSA加密方案(包括簽名)。實際上消息傳輸應該遵照下列模型:
5.10.4 存儲安全
本CA系統的存儲安全只作用在用戶數據庫中ra_user,因為考慮到CRL和證書庫的信息都是公開的,所以就沒有加密。
登錄用戶的登錄密碼和身份證都是隱私信息,應該加密存儲。事實上,密碼的安全性建立在數據庫安全上。如果數據庫被攻破,則密碼的保密性將不復存在。
設計存儲安全保密方案如下:
5.10.5 驗證碼
驗證碼的作用是為了給惡意破解密碼、反復自動提交表單攻擊造成麻煩。本CA系統中需要用到驗證碼的部分有:登錄/注冊、申請證書、撤銷證書。
5.10.6 前端密碼隱藏顯示
前端密碼隱藏顯示可以給偷窺攻擊帶來麻煩。
5.11 證書驗證
嚴格意義上的證書驗證,應該是對一個合法的X.509證書文件,利用Windows的證書相關程序和瀏覽器內置的驅動程序,將證書安裝到瀏覽器上,每次訪問網頁時,由瀏覽器驗證證書有效性,并給出相應的提示:
在本實驗中,為了模擬上述功能,把證書驗證封裝好了一個工具類VerifyCerUtil,該類中有一個方法verify以證書文件(File)作為參數或者以證書文件內容(String)作為參數,返回一個boolean值,驗證有效則返回true,無效則返回false。
起初的證書驗證是將該函數分配給電商和網銀的兩個服務器上,各自做本地驗證。但是考慮到任何用戶登錄電商和網銀都應該給出證書有效性的驗證,所以,這個驗證應該改到前端上驗證。
為此,在驗收前一天晚上將證書驗證倉促改為一個模擬的小驅動程序,該驅動程序使用IE瀏覽器的ActiveX技術,相當于給IE瀏覽器安裝一個小插件,在JavaScript代碼中啟動驅動程序。
將對電商和網銀的證書驗證封裝成兩個.exe驅動程序,放到指定目錄下,還要求用戶主機上該目錄下應該有電商和網銀的證書,以及CRL列表和CA認證機構的公鑰文件。這個目錄相當于在用戶主機上,模擬了一個瀏覽器環境,每當用戶使用瀏覽器(IE)訪問電商和網銀兩個網站的時候,都會給出證書有效性提示如下:
6. 實現與測試
本節將對一部分代碼和所有功能的實現測試給出說明。
6.1 登錄/注冊
6.1.1 注冊
由于CSDN上傳圖片時出了點小問題,這部分的圖都略了,參考價值不大。
用戶名不合法
密碼不合法
密碼強度
身份證不合法
當用戶iamjack注冊成功時
6.1.2 登錄
登錄驗證碼錯誤
輸入用戶jd,或者用戶iamjack的密碼錯誤,將顯示用戶名或密碼錯誤。當用戶的用戶名或密碼輸入錯誤時,不應該顯式地指出到底是用戶名錯誤還是密碼錯誤,防止攻擊者對用戶名或密碼針對性的攻擊。
用戶iamjack成功登錄
6.2 申請證書
首先正確申請一個證書
6.3 密鑰生成器
運行下載的genkey.exe文件,將在同級目錄下生成兩個密鑰文件pk.key(公鑰)和sk.key(私鑰)。
6.4 下載證書
驗證碼功能不再演示。
如果序列號不對的話,提示此證書不存在。
正確下載證書的結果:
6.5 撤銷證書
驗證碼功能不再演示。
驗證驗證碼正確后,會驗證撤銷身份驗證密碼:
驗證撤銷身份驗證密碼正確后,會驗證證書是否存在,以及證書有效性:
成功撤銷證書演示:
再次撤銷該證書,提示該證書已失效:
6.6 下載CRL
點擊下載CRL后開始下載。
CRL文件如下所示:
6.7 查看證書
證書查看列表如下(每頁1條目):
下載鏈接有效:
7. 核心代碼
7.1 后端
7.1.1 后端對前端的傳輸解密
String username = req.getParameter("username");String password = req.getParameter("password");String sign_username = req.getParameter("sign_username");String sign_password = req.getParameter("sign_password");PrivateKey privateKey = null;try {privateKey = // 私鑰內容省略不寫username = new String(Ende.decrypt((RSAPrivateKey) privateKey, Base64.getDecoder().decode(username)), "utf-8");password = new String(Ende.decrypt((RSAPrivateKey) privateKey, Base64.getDecoder().decode(password)), "utf-8");} catch (Exception e) {e.printStackTrace();}7.1.2 后端對前端簽名的驗證
if (!SHADigest.getDigest(username).equalsIgnoreCase(sign_username)|| !SHADigest.getDigest(password).equalsIgnoreCase(sign_password)) {req.setAttribute("login_error", "登錄失敗!報文可能被損壞!請報警!");req.getRequestDispatcher("/index.jsp").forward(req, resp);return;}7.1.3 用戶注冊加密
public boolean register(User registerUser) {String salt = UUID.randomUUID().toString(); // 生成鹽值String username = registerUser.getUsername();String rawPwd = registerUser.getPassword();String idcard = registerUser.getIdcard();PasswordEncryptor passwordEncryptor = new PasswordEncryptor(salt, "sha-256"); // 生成加密器// 加鹽加密String encPwd = passwordEncryptor.encode(rawPwd);String encId = passwordEncryptor.encode(idcard);Object[] a = {username};int u =template.queryForObject("SELECT COUNT(*) FROM ra_user WHERE username = ?", a, Integer.class);if (u >= 1) {return false;}Object[] insertArgs = {username, encPwd, encId, salt};String sql = "INSERT INTO ra_user(username, password, idcard, salt) VALUES (?,?,?,?)";template.update(sql, insertArgs);return true;}7.1.4 用戶登錄的驗證
public boolean verify(User user) {String username = user.getUsername();String rawPwd = user.getPassword();Object[] a = {username};String encPwd = null;String salt = null;try {encPwd =template.queryForObject("SELECT password FROM ra_user WHERE username = ?", a,String.class); // 獲取數據庫中用戶的密碼salt =template.queryForObject("SELECT salt FROM ra_user WHERE username = ?", a,String.class); // 獲取鹽值} catch (DataAccessException e) {e.printStackTrace();return false;}PasswordEncryptor passwordEncryptor = new PasswordEncryptor(salt, "sha-256"); // 生成加密器boolean isValid = passwordEncryptor.isPasswordValid(encPwd, rawPwd); // 判斷密碼正確性if (isValid) {return true;} else {return false;}}7.1.5 Filter
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {HttpServletRequest request = (HttpServletRequest) req;String uri = request.getRequestURI();// 用戶訪問除了資源文件、主頁、驗證碼servlet、登錄servlet、注冊servlet之外,都要進行登錄狀態檢查if (uri.contains("/index.jsp") || uri.contains("/loginServlet") || uri.contains("/css/")|| uri.contains("/js/") || uri.contains("/images/") || uri.contains("/checkCodeServlet")|| uri.contains("/loginServlet") || uri.contains("/registerServlet")) {chain.doFilter(req, resp);} else {Object username = request.getSession().getAttribute("username");if (username != null) {chain.doFilter(req, resp);} else {request.setAttribute("login_msg", "您尚未登錄,請登錄");request.getRequestDispatcher("/index.jsp").forward(request, resp);}// chain.doFilter(req, resp);}}7.1.6 Ende(加解密代碼)
Ende類中的代碼取自參考文獻[14].
7.1.7 RSASignature(簽名算法)
RSASignature類中的代碼取自參考文獻[14].
7.1.8 驗證證書有效性(該方法被封裝在驅動程序中)
package com.caiji.util;import com.caiji.domain.Mycrl; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException;import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.NoSuchPaddingException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import java.io.*; import java.security.InvalidKeyException; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.interfaces.RSAPublicKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Base64; import java.util.Date; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern;public class VerifyCerUtil {private static final String timePattern = "(\\d*年\\d*月\\d*日 \\d{2}:\\d{2}:\\d{2})";/*** 驗證證書有效性* @param cer 證書內容字串* @return 如果有效,返回true;否則,返回false* @throws ParseException* @throws IOException* @throws NoSuchPaddingException* @throws NoSuchAlgorithmException* @throws IllegalBlockSizeException* @throws BadPaddingException* @throws InvalidKeyException* @throws InvalidKeySpecException*/public static boolean verify(String cer) throws ParseException, IOException, NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidKeySpecException {String serial_number = parseSerialNumber(cer);String validTimeFrom = parseValidTimeFrom(cer);String validTimeTo = parseValidTimeTo(cer);String sign = parseSign(cer);String publicKey = parsePublicKey(cer);// 驗證證書有效期if (!judgeTime(validTimeFrom, validTimeTo)) {return false;}// 驗證證書簽名if (!RSASignature.doCheck(publicKey, sign, KeyUtil.loadPublicKeyByFile("D:\\DriveY"+ "\\IntelliJ\\cryptotw2\\out\\artifacts\\cryptotw2_Web_exploded\\download\\pk.key"), "utf-8")) {return false;}// 解析CRL,驗證證書是否已被撤銷List<String> mycrlList = getNode("D:\\DriveY\\IntelliJ\\cryptotw2\\out\\artifacts"+ "\\cryptotw2_Web_exploded\\download\\crl.xml");for (String deprecated : mycrlList) {if (deprecated.equals(serial_number)) {return false;}}return true;}/*** 驗證證書有效性* @param certificate 證書文件* @return 如果有效,返回true;否則,返回false* @throws IOException* @throws ParseException* @throws NoSuchPaddingException* @throws NoSuchAlgorithmException* @throws IllegalBlockSizeException* @throws BadPaddingException* @throws InvalidKeyException* @throws InvalidKeySpecException*/public static boolean verify(File certificate) throws IOException, ParseException, NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidKeySpecException {BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(new FileInputStream(certificate), "GBK"));String cerLine = "";String cer = "";while ((cerLine = bufferedReader.readLine()) != null) {cer += cerLine + "\n";}bufferedReader.close();return verify(cer);}/*** 內部工具方法,解析crl.xml文件內容* @param url crl.xml文件url路徑* @return 將xml文件解析為一個String列表,其中包含xml的每一條目信息*/private static List<String> getNode(String url) {List<String> mycrlList = new ArrayList<>();DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();try {DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder();Document document = builder.parse(url);NodeList nodeList = document.getElementsByTagName("crl");for (int i = 0; i < nodeList.getLength(); i++) {Node node = nodeList.item(i);NodeList childNodes = node.getChildNodes();for (int j = 0; j < childNodes.getLength(); j++) {if (childNodes.item(j).getNodeType() == Node.ELEMENT_NODE&& childNodes.item(j).getNodeName().equals("serial_number")) {mycrlList.add(childNodes.item(j).getFirstChild().getNodeValue());}}}} catch (ParserConfigurationException e) {e.printStackTrace();} catch (SAXException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}return mycrlList;}/*** 驗證證書有效期* @param validTimeFrom 證書的有效期起字段* @param validTimeTo 證書的有效期至字段* @return 如果證書時間有效,返回true;否則,返回false* @throws ParseException*/private static boolean judgeTime(String validTimeFrom, String validTimeTo) throws ParseException {SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");Date from = simpleDateFormat.parse(validTimeFrom);Date to = simpleDateFormat.parse(validTimeTo);Date now = new Date();if (now.before(from) || now.after(to)) {return false;} else {return true;}}/*** 解析證書的序列號* @param cer 證書文件內容字串* @return 證書序列號字串*/private static String parseSerialNumber(String cer) {String serial_number = null;Pattern serialNumberPattern = Pattern.compile("Serial Number: " + "(\\w*)\\n");Matcher serialNumberMatch = serialNumberPattern.matcher(cer);while (serialNumberMatch.find()) {serial_number = serialNumberMatch.group(1);}return serial_number;}/*** 解析證書有效期起字串* @param cer 證書內容字串* @return 證書有效期起字串*/private static String parseValidTimeFrom(String cer) {String validTimeFrom = null;Pattern validTimeFromPattern = Pattern.compile("Valid Time From: " + timePattern);Matcher validTimeFromMatch = validTimeFromPattern.matcher(cer);while (validTimeFromMatch.find()) {validTimeFrom = validTimeFromMatch.group(1);}return validTimeFrom;}/*** 解析證書有效期至字串* @param cer 證書內容字串* @return 證書有效期至字串*/private static String parseValidTimeTo(String cer) {String validTimeTo = null;Pattern validTimeToPattern = Pattern.compile("Valid Time To: " + timePattern);Matcher validTimeToMatch = validTimeToPattern.matcher(cer);while (validTimeToMatch.find()) {validTimeTo = validTimeToMatch.group(1);}return validTimeTo;}/*** 解析證書公鑰字串* @param cer 證書內容字串* @return 證書公鑰字串*/private static String parsePublicKey(String cer) {String publicKey = null;Pattern publicKeyPattern = Pattern.compile("Public Key: " + "(.*)\\n");Matcher publicKeyMatch = publicKeyPattern.matcher(cer);while (publicKeyMatch.find()) {publicKey = publicKeyMatch.group(1);}return publicKey;}/*** 解析證書簽名字串* @param cer 證書內容字串* @return 證書簽名字串*/private static String parseSign(String cer) {String sign = null;Pattern signPattern = Pattern.compile("Sign: " + "(.*)\\n");Matcher signMatch = signPattern.matcher(cer);while (signMatch.find()) {sign = signMatch.group(1);}return sign;} }7.1.9 證書制作
private String makeCertificate(List<String> cerLines) {String cer_path = this.getServletContext().getRealPath("/download/");String cer_name =cerLines.get(0).substring(cerLines.get(0).indexOf(":") + 2, cerLines.get(0).length());try {BufferedWriter bufferedWriter =new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(cer_path + cer_name))));for (int i = 0; i < cerLines.size(); i++) {System.out.println(cerLines.get(i));bufferedWriter.write(cerLines.get(i));bufferedWriter.write("\n");bufferedWriter.flush();}bufferedWriter.close();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}return cer_path + cer_name;}7.1.10 證書下載
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {String checkCode = request.getParameter("check_code");HttpSession session = request.getSession();String checkCode_session = (String) session.getAttribute("checkCode_session");session.removeAttribute("checkCode_session");if (request.getParameter("no_check_code") == null && (checkCode_session == null|| !checkCode_session.equalsIgnoreCase(checkCode))) {request.setAttribute("msg", "驗證碼錯誤!");request.getRequestDispatcher("download_cer.jsp").forward(request, response);return;}request.removeAttribute("no_check_code");String serial_number = request.getParameter("serial_number");System.out.println(serial_number);CertificateDao certificateDao = new CertificateDao();String filePath = certificateDao.getFilePath(serial_number);if (filePath == null) {request.setAttribute("msg", "此證書不存在!");request.getRequestDispatcher("download_cer.jsp").forward(request, response);return;}String filename = serial_number + ".mycer";ServletContext servletContext = this.getServletContext();// String mimeType = servletContext.getMimeType(file_path);response.setHeader("content-type", "application/octet-stream");response.setHeader("content-disposition", "attachment;filename=" + filename);FileInputStream fileInputStream = new FileInputStream(filePath);ServletOutputStream servletOutputStream = response.getOutputStream();byte[] buff = new byte[1024 * 8];int len = 0;while ((len = fileInputStream.read(buff)) != -1) {servletOutputStream.write(buff, 0, len);}fileInputStream.close();}7.2 前端JavaScript
7.2.1 驗證碼加載
<script>window.onload = function () {document.getElementById("apply_check_img").onclick = function () {this.src = "/tw/checkCodeServlet?time=" + new Date().getTime();};}</script>7.2.2 前端傳輸加密和簽名
function apply_encrypt() {var publicKey ="MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCLFxU/IBjwf0UjGsXy/dx6QiRH6pvvZoGvtUtDhGT1Wq0Vga2h7CUKwNazN8/l8YksKgz01JqIEh8NUAGOFY3tVMk/sNlMgYqCWyRw6QvMtspLDe3dzJn/83qAsMq3wr3Ooxgk143AXV6YlXhDvipYqwasP7luwRrbgdTANItl3wIDAQAB";var encrypt = new JSEncrypt();encrypt.setPublicKey(publicKey);var enc_organization = encrypt.encrypt(organization.value);var enc_registration_number = encrypt.encrypt(registration_number.value);var enc_juridical_person = encrypt.encrypt(juridical_person.value);var enc_charge_person = encrypt.encrypt(charge_person.value);var enc_charge_phone = encrypt.encrypt(charge_phone.value);var sign_organization = hex_sha256(organization.value);var sign_registration_number = hex_sha256(registration_number.value);var sign_juridical_person = hex_sha256(juridical_person.value);var sign_charge_person = hex_sha256(charge_person.value);var sign_charge_phone = hex_sha256(charge_phone.value);organization.value = enc_organization;registration_number.value = enc_registration_number;juridical_person.value = enc_juridical_person;charge_person.value = enc_charge_person;charge_phone.value = enc_charge_phone;this.sign_organization.value = sign_organization;this.sign_registration_number.value = sign_registration_number;this.sign_juridical_person.value = sign_juridical_person;this.sign_charge_person.value = sign_charge_person;this.sign_charge_phone.value = sign_charge_phone;}7.2.3 證書內容(數據庫前端)顯示
<body> <span style="text-align: right; font-family: 楷體; font-weight: bold; font-size: 20px; margin-top: 20px; margin-right: 20px; color: #FFFFFF"><%=request.getSession().getAttribute("username")%>,歡迎您</span> <br> <a href="/tw/logoutServlet"style="text-align: right; font-family: 楷體; font-weight: normal; font-size:20px; margin-top:20px; margin-right: 20px; color: #FFFFFF">退出登錄</a> <h1>哥譚市數字證書認證中心</h1><div class="container" style="width: 75%;"><h3>證 書 列 表</h3><table><tr class="tr-header"><th>序列號</th><th>組織機構</th><th>工商注冊號</th><th>證書有效期起</th><th>證書有效期止</th><th>下載鏈接</th></tr><c:forEach items="${requestScope.certItems}" var="certItem" varStatus="s"><tr class="tr-body"><th>${certItem.serial_number}</th><th>${certItem.organization}</th><th>${certItem.registration_number}</th><th>${certItem.start_time}</th><th>${certItem.end_time}</th><th><ahref="/tw/downloadCerServlet?serial_number=${certItem.serial_number}&no_check_code=123">下 載</a></th></tr></c:forEach><tr class="tr-footer"><td colspan="3"style="text-align: right; padding-right: 20px; padding-top: 5px; padding-bottom:5px;">當前為第${page.currentPage}頁,共${page.totalPage} 頁</td><td colspan="3" style="text-align: left; padding-left: 20px; padding-top: 5px; padding-bottom:5px;"><c:choose><c:when test="${page.hasPrePage}"><a href="/tw/certificateInfoServlet?currentPage=1">首頁</a> |<a href="/tw/certificateInfoServlet?currentPage=${page.currentPage-1}">上一頁</a></c:when><c:otherwise>首頁 | 上一頁</c:otherwise></c:choose><c:choose><c:when test="${page.hasNextPage}"><a href="/tw/certificateInfoServlet?currentPage=${page.currentPage+1}">下一頁</a> |</c:when><c:otherwise>下一頁 | 尾頁</c:otherwise></c:choose></td></tr></table><div class="clear"></div></div> </body>7.2.4 前端注冊合法性檢查
register_btn.addEventListener("click", function () {var uPattern = /^[a-zA-Z0-9_-]{6,16}$/;if (!uPattern.test(register_username.value)) {alert("用戶名必須為字母、數字、下劃線、減號的組合,長度為6-16!");return;}var pPattern = /^(?=.*[a-zA-Z])(?=.*\d)[^]{6,20}$/;if (!pPattern.test(register_password.value)) {alert("密碼長度至少為6位,至多20位!并且至少包括數字和字母!");return;}var cP = /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;if (!cP.test(register_idcard.value)) {alert("請輸入合法的身份證!");return;}reg_encrypt();document.getElementById("register_form").submit();register_username.value = "";register_password.value = "";register_idcard.value = "";});7.2.5 密碼強度顯示
function CheckIntensity(pwd) {var Mcolor, Wcolor, Scolor, Color_Html;var m = 0;//匹配數字if (/\d+/.test(pwd)) {debugger;m++;};//匹配字母if (/[A-Za-z]+/.test(pwd)) {m++;};//匹配除數字字母外的特殊符號if (/[^0-9a-zA-Z]+/.test(pwd)) {m++;};if (pwd.length <= 6) {m = 1;}if (pwd.length <= 0) {m = 0;}switch (m) {case 1:Wcolor = "pwd pwd_Weak_c";Mcolor = "pwd pwd_c";Scolor = "pwd pwd_c pwd_c_r";Color_Html = "弱";break;case 2:Wcolor = "pwd pwd_Medium_c";Mcolor = "pwd pwd_Medium_c";Scolor = "pwd pwd_c pwd_c_r";Color_Html = "中";break;case 3:Wcolor = "pwd pwd_Strong_c";Mcolor = "pwd pwd_Strong_c";Scolor = "pwd pwd_Strong_c pwd_Strong_c_r";Color_Html = "強";break;default:Wcolor = "pwd pwd_c";Mcolor = "pwd pwd_c pwd_f";Scolor = "pwd pwd_c pwd_c_r";Color_Html = "無";break;}document.getElementById('pwd_Weak').className = Wcolor;document.getElementById('pwd_Medium').className = Mcolor;document.getElementById('pwd_Strong').className = Scolor;document.getElementById('pwd_Medium').innerHTML = Color_Html;}8. 調試過程中出現的幾個問題
在做前端消息傳輸的簽名,后端驗證消息完整性的過程中,會遇到中文消息簽名不匹配問題,究其原因是編碼問題,將后端的字節數組轉換改為utf-8編碼解決。
byte[] plaintext = message.getBytes("utf-8");在證書查看列表中點擊下載鏈接,將因為驗證碼不正確而導致下載失敗。原始是下載的servlet中加了對驗證碼的判斷,從列表中點擊下載鏈接時,可以加上一個no_check_code=123的參數,在下載的servlet中,判斷如果no_check_code不為空,則不進行驗證碼的判斷。
9. 結束語
經過了八周的密碼學實驗實踐,自己動手,從設計構思,到一個個功能的不斷實現,完成了一個具有申請、撤銷、下載、查詢等基本功能的CA認證系統。
在實踐過程中感受到最大的困難,就是前期的框架構思。我們組花了兩周的時間,去仔細調查了CA證書(認證機構)、網上銀行和電子商務之間的聯系,其中包括消息傳輸,加密規則,每個個體的作用等。在了解這些之后,畫出了結構草圖,并按照結構草圖,開始逐步實現功能。
我自己開發的CA認證系統的功能實現模式是菜單模式,在前端創造一個菜單,并在上面逐步添加功能并實現。
邊學習邊開發的過程讓我學到了很多。特別感謝嗶哩嗶哩上的視頻教程:JavaWeb(放慢-腳步),逐步學到了HTML、CSS、JavaScript、xml、servlet、response、session、filter、jsp+el+stl等一套技術棧,并成功運用到了本次實驗的項目中。
此外,特別感謝我的兩位隊友,王久金同學和韓嘯同學,在開發過程中團隊積極相互溝通,解決了很多困難,兩位隊友在各自的開發中也解決了很多難題,總體開發效率很高。
10. 參考文獻
[1] https://blog.csdn.net/wangliang369/article/details/83792116 “jsp 實現分頁操作”
[2] https://blog.csdn.net/yeyuwanfeng/article/details/81907856 “html a標簽樣式設置”
[3] http://www.cssmoban.com/cssthemes/6824.shtml “大氣黑色登錄注冊表單html5模板”
[4] https://www.cnblogs.com/daizhongxing/p/11593137.html “常見密碼正則表達式”
[5] https://www.cnblogs.com/raphael1982/p/8012634.html “用戶名、密碼等15個常用的js正則表達式”
[6] https://blog.csdn.net/weixin_36293343/article/details/85090852 “X509證書結構及解析”
[7] https://zh.wikipedia.org/wiki/X.509 “X.509,維基百科”
[8] https://docs.oracle.com/cd/E19146-01/820-0872/gdagx/index.html “管理證書撤銷列表 (Certificate Revocation List, CRL)”
[9] https://baike.baidu.com/item/%E8%AF%81%E4%B9%A6%E6%92%A4%E9%94%80/747891?fr=aladdin(https://baike.baidu.com/item/證書撤銷/747891?fr=aladdin) “證書撤銷”
[10] https://www.jianshu.com/p/c65fa3af1c01 “PKI/CA工作原理及架構”
[11] https://blog.csdn.net/ayang1986/article/details/80810072 “CA認證簡單介紹與工作流程”
[12] https://blog.csdn.net/chu_jian86a/article/details/83246960 “ActiveXObject對象使用整理(JS調用本地exe程序)”
[13] https://blog.csdn.net/xiao_cs/article/details/6262144 “WshShell.Run方法說明”
[14] https://www.cnblogs.com/demodashi/p/8458113.html “Java使用RSA加密解密簽名及校驗”
[15] https://space.bilibili.com/250181517?spm_id_from=333.788.b_765f7570696e666f.2 “Java Web教程”
[16] 江為強, 陳波. PKI/CA技術的起源、現狀和前景綜述[J]. 西南科技大學學報, 2003, 18(4):75-78.
總結
以上是生活随笔為你收集整理的哈工大密码学实验(CA证书认证系统)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: editplus注释快捷键
- 下一篇: 4 WPF依赖属性