HarmonyOS 鸿蒙Next网络编程系列58-仓颉版TLS数字证书查看及验签示例

HarmonyOS 鸿蒙Next网络编程系列58-仓颉版TLS数字证书查看及验签示例

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_entry

internal 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_entry

import 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 {
    @Publish
    public var certFileSelected: Bool = false
    @Publish
    public var certFileUri: String = ""
}

@Entry
@Component
class EntryView {
    @State
    var title: String = '数字证书验签示例';
    //连接、通讯历史记录
    @State
    var msgHistory: String = ''

    //根证书选择状态
    @State
    var rootCertStatus: CertFileSelectStatus = CertFileSelectStatus()

    //中间证书选择状态
    @State
    var middleCertStatus: CertFileSelectStatus = CertFileSelectStatus()

    //用户证书选择状态
    @State
    var 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 = true
                    case _ => ()
                }
            }
        }
        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) => context
            case _ => 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


更多关于HarmonyOS 鸿蒙Next网络编程系列58-仓颉版TLS数字证书查看及验签示例的实战教程也可以访问 https://www.itying.com/category-93-b0.html

2 回复

鸿蒙Next仓颉版TLS数字证书查看及验签示例:

  1. 证书查看: 使用security.certManager模块的getX509Certificates方法获取证书链,通过getEncoded()获取DER格式数据,getPublicKey()提取公钥信息。

  2. 验签操作: 通过security.cryptoFramework创建验签器:

let verifier = cryptoFramework.createVerify("RSA|SHA256");
verifier.init(publicKey);
verifier.update(data);
verifier.verify(data, signature, (err, result) => {
  // 处理验签结果
});
  1. 关键参数:
  • 支持算法:RSA/PSS/ECDSA with SHA256/SHA384
  • 证书格式:X.509标准
  • 验签数据需为Uint8Array类型,

更多关于HarmonyOS 鸿蒙Next网络编程系列58-仓颉版TLS数字证书查看及验签示例的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


这是一个非常完整的HarmonyOS仓颉版TLS数字证书查看及验签示例实现。主要技术点包括:

  1. 证书信任链验证:通过根证书、中间证书和用户证书三级结构实现完整的证书链验证。

  2. 证书信息查看:展示了如何获取并显示证书的颁发者、主题、有效期、签名算法等关键信息。

  3. 文件操作:处理证书文件的读取和权限问题,通过沙箱机制安全访问证书文件。

  4. UI交互:实现了证书选择、查看和验签的完整用户交互流程。

代码结构清晰,主要功能封装在以下几个方法中:

  • selectCertFile():处理证书文件选择
  • verifyCert():执行证书验证逻辑
  • getCert():读取并解析证书文件
  • showCertInfo():显示证书详细信息

这个示例很好地展示了如何在HarmonyOS中使用仓颉语言实现TLS证书验证功能,可以作为开发安全网络应用的参考实现。

回到顶部