1. TLS數字證書驗簽簡介
數字證書的簽名驗證是網絡編程中一個重要的功能,它保證了數字證書是由可信任的簽發方簽署的,在此基礎上,我們才可以信任該證書,進而信任基于該證書建立的安全通道,所以說,數字證書的真實性是通訊安全的基石,了解數字證書驗簽的原理和方法,有助于我們建立安全的通訊。
一般來說,用戶數字證書的來源是這樣的:
- 首先,由受信任的根證書頒發機構生成根證書,這是數字證書信任鏈的起源;
- 其次,根證書頒發機構使用根證書簽發中間證書,因為根證書的安全級別非常高,使用程序非常繁瑣,輕易不使用,所以一般使用中間證書做為簽發證書;
- 最后,使用中間證書簽發用戶數字證書。
本文將通過一個示例演示數字證書內容的查看方法以及如何對一個數字證書進行驗簽,本示例將使用倉頡語言在API17的環境下編寫,下面是詳細的演示過程。
2. TLS數字證書查看及驗簽演示
要進行數字證書的驗簽,需要提前準備根證書、中間證書和用戶證書,為方便起見,這里使用百度的數字證書及其簽發證書,獲取證書的步驟如下:
- 首先,打開百度網站,單擊地址欄前的圖標,會彈出下拉菜單,如圖所示:
- 然后,單擊“連接安全”菜單項,彈出安全菜單,如圖所示:
- 接著,單擊“證書有效”菜單項,彈出證書信息,進入詳細信息頁面,如圖所示:
- 在證書層次結構那里選擇 baidu.com,然后單擊下面的“導出”按鈕,即可導出百度的用戶證書。然后在證書層次結構那里選擇“GlobalSign RSA OV SSL CA 2018”,單擊下面的“導出”按鈕,即可導出中間證書,如圖所示。依次也可以導出根證書。這些證書需要預先上傳到手機上。
本應用打開的初始界面如圖所示:
單擊根證書后的“選擇”按鈕,彈出文件選擇窗口,如圖所示:
從中選擇對應的根證書,返回界面后單擊“查看”按鈕,效果如圖所示:
然后可以選擇中間證書和用戶證書,如圖所示
此時單擊“驗簽”按鈕,可以查看驗簽結果,如圖所示:
如果把用戶證書更換成其他的證書,然后再進行驗簽,會發現驗簽不通過,如圖所示:
3. TLS數字證書查看及驗簽示例編寫
下面詳細介紹創建該示例的步驟(確保DevEco Studio已安裝倉頡插件)。
步驟1:創建[Cangjie]Empty Ability項目。
步驟2:在module.json5配置文件加上對權限的聲明:
"requestPermissions": [{"name": "ohos.permission.INTERNET"}]
這里添加了訪問互聯網的權限。
步驟3:在build-profile.json5配置文件加上倉頡編譯架構:
"cangjieOptions": {"path": "./src/main/cangjie/cjpm.toml","abiFilters": ["arm64-v8a", "x86_64"]}
步驟4:在main_ability.cj文件里添加如下的代碼:
package ohos_app_cangjie_entryinternal import ohos.base.AppLog
internal import ohos.ability.AbilityStage
internal import ohos.ability.LaunchReason
internal import cj_res_entry.app
import ohos.ability.*//Ability全局上下文
var globalAbilityContext: Option<AbilityContext> = Option<AbilityContext>.None
class MainAbility <: Ability {public init() {super()registerSelf()}public override func onCreate(want: Want, launchParam: LaunchParam): Unit {AppLog.info("MainAbility OnCreated.${want.abilityName}")globalAbilityContext = Option<AbilityContext>.Some(this.context)match (launchParam.launchReason) {case LaunchReason.START_ABILITY => AppLog.info("START_ABILITY")case _ => ()}}public override func onWindowStageCreate(windowStage: WindowStage): Unit {AppLog.info("MainAbility onWindowStageCreate.")windowStage.loadContent("EntryView")}
}
步驟5:在index.cj文件里添加如下的代碼:
package ohos_app_cangjie_entryimport ohos.base.*
import ohos.component.*
import ohos.state_manage.*
import ohos.state_macro_manage.*
import ohos.file_picker.*
import ohos.ability.*
import ohos.file_fs.*
import crypto.x509.*
import ohos.crypto.*
import encoding.base64.toBase64String@Observed
//證書選擇狀態
class CertFileSelectStatus {@Publishpublic var certFileSelected: Bool = false@Publishpublic var certFileUri: String = ""
}@Entry
@Component
class EntryView {@Statevar title: String = '數字證書驗簽示例';//連接、通訊歷史記錄@Statevar msgHistory: String = ''//根證書選擇狀態@Statevar rootCertStatus: CertFileSelectStatus = CertFileSelectStatus()//中間證書選擇狀態@Statevar middleCertStatus: CertFileSelectStatus = CertFileSelectStatus()//用戶證書選擇狀態@Statevar userCertStatus: CertFileSelectStatus = CertFileSelectStatus()let scroller: Scroller = Scroller()func build() {Row {Column {Text(title).fontSize(14).fontWeight(FontWeight.Bold).width(100.percent).textAlign(TextAlign.Center).padding(10)Flex(FlexParams(justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center)) {Text("根證書:").fontSize(14).width(90).flexGrow(1)Button("選擇").onClick {evt => selectCertFile(this.rootCertStatus)}.width(60).fontSize(14)Button("查看").onClick {evt =>let cert = getCert(rootCertStatus.certFileUri)showCertInfo(cert)}.width(60).fontSize(14).enabled(rootCertStatus.certFileSelected)}.width(100.percent).padding(5)Text(rootCertStatus.certFileUri).fontSize(14).width(100.percent).padding(10)Flex(FlexParams(justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center)) {Text("中間證書:").fontSize(14).width(90).flexGrow(1)Button("選擇").onClick {evt => selectCertFile(this.middleCertStatus)}.width(60).fontSize(14)Button("查看").onClick {evt =>let cert = getCert(middleCertStatus.certFileUri)showCertInfo(cert)}.width(60).fontSize(14).enabled(middleCertStatus.certFileSelected)}.width(100.percent).padding(5)Text(middleCertStatus.certFileUri).fontSize(14).width(100.percent).padding(10)Flex(FlexParams(justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center)) {Text("用戶證書:").fontSize(14).width(90).flexGrow(1)Button("選擇").onClick {evt => selectCertFile(this.userCertStatus)}.width(60).fontSize(14)Button("查看").onClick {let cert = getCert(userCertStatus.certFileUri)showCertInfo(cert)}.width(60).fontSize(14).enabled(userCertStatus.certFileSelected)Button("驗簽").onClick {evt => verifyCert()}.width(60).fontSize(14).enabled(rootCertStatus.certFileSelected && userCertStatus.certFileSelected && middleCertStatus.certFileSelected)}.width(100.percent).padding(5)Text(userCertStatus.certFileUri).fontSize(14).width(100.percent).padding(10)Scroll(scroller) {Text(msgHistory).textAlign(TextAlign.Start).padding(10).width(100.percent).backgroundColor(0xeeeeee)}.align(Alignment.Top).backgroundColor(0xeeeeee).height(300).flexGrow(1).scrollable(ScrollDirection.Vertical).scrollBar(BarState.On).scrollBarWidth(20)}.width(100.percent).height(100.percent)}.height(100.percent)}//選擇證書文件func selectCertFile(certFileStatus: CertFileSelectStatus) {let picker = DocumentViewPicker(getContext())let documentSelectCallback = {errorCode: Option<AsyncError>, data: Option<Array<String>> => match (errorCode) {case Some(e) => msgHistory += "選擇失敗,錯誤碼:${e.code}\r\n"case _ => match (data) {case Some(value) =>certFileStatus.certFileUri = value[0]certFileStatus.certFileSelected = truecase _ => ()}}}picker.select(documentSelectCallback, option: DocumentSelectOptions(selectMode: DocumentSelectMode.MIXED))}//使用簽發證書驗證用戶證書func verifyCert() {try {let caCert: X509Certificate = getCert(rootCertStatus.certFileUri)let middleCert: X509Certificate = getCert(middleCertStatus.certFileUri)let userCert: X509Certificate = getCert(userCertStatus.certFileUri)var verifyOpt: VerifyOption = VerifyOption()verifyOpt.roots = [caCert]verifyOpt.intermediates = [middleCert]let result = userCert.verify(verifyOpt)if (result) {msgHistory += "證書驗簽通過\r\n"} else {msgHistory += "證書驗簽未通過\r\n"}} catch (err: Exception) {msgHistory += "驗簽異常:${err.message}\r\n"}}//獲取數字證書func getCert(certPath: String) {let fileName = getFileNameFromPath(certPath)let file = FileFs.open(certPath)//構造證書在沙箱cache文件夾的路徑let realUrl = getContext().filesDir.replace("files", "cache") + "/" + fileName//復制證書到沙箱給定路徑FileFs.copyFile(file.fd, realUrl)//關閉文件FileFs.close(file)//從沙箱讀取證書文件信息let certContent = FileFs.readText(realUrl)return X509Certificate.decodeFromPem(certContent)[0]}//輸出證書信息func showCertInfo(cert: X509Certificate) {try {this.msgHistory += "頒發者可分辨名稱:${ cert.issuer}\r\n"this.msgHistory += "證書主題可分辨名稱:${ cert.subject}\r\n"this.msgHistory += "證書主題CN名稱:${ cert.subject.commonName.getOrThrow()}\r\n"this.msgHistory += "證書有效期:${ cert.notBefore} 至${ cert.notAfter}\r\n"this.msgHistory += "證書簽名算法:${ cert.signatureAlgorithm}\r\n"let keyHash = getPubKeyHash(cert)this.msgHistory += "公鑰摘要:${ keyHash}\r\n"} catch (err: Exception) {msgHistory += "出現異常:${err.message}\r\n"}}//獲取證書的公鑰摘要func getPubKeyHash(cert: X509Certificate) {let mdSHA256 = createMd("SHA256")mdSHA256.update(DataBlob(cert.publicKey.encodeToDer().body));//公鑰摘要計算結果return toBase64String(mdSHA256.digest().data)}//從文件路徑獲取文件名稱public func getFileNameFromPath(filePath: String) {let segments = filePath.split('/')//文件名稱return segments[segments.size - 1]}//獲取Ability上下文func getContext(): AbilityContext {match (globalAbilityContext) {case Some(context) => contextcase _ => throw Exception("獲取全局Ability上下文異常")}}
}
步驟6:編譯運行,可以使用模擬器或者真機。
步驟7:按照本文第2部分“TLS數字證書查看及驗簽演示”操作即可。
4. 代碼分析
本示例中,讀取數字證書內容的時候也存在權限的問題,所以也要把選擇的數字證書復制到沙箱中,然后從沙箱中讀取文件內容,該部分代碼在getCert函數中。另外,獲取Ability上下文的方式也要注意,首先在main_ability.cj中定義了全局上下文對象globalAbilityContext,然后在onCreate事件中對其賦值,這樣在index.cj中就可以通過函數getContext獲取該對象了。
(本文作者原創,除非明確授權禁止轉載)
本文源碼地址:
https://gitee.com/zl3624/harmonyos_network_samples/tree/master/code/tls/CertVerify4Cj
本系列源碼地址:
https://gitee.com/zl3624/harmonyos_network_samples