鸿蒙ArkTS SM2签名验签实战:从密钥生成到完整流程实现
1. 项目概述:为什么要在ArkTS里折腾SM2?
如果你正在用ArkTS开发鸿蒙应用,并且应用场景涉及金融支付、电子合同、身份认证或者任何需要确保数据完整性与来源可信的环节,那么“签名”和“验签”就是你绕不开的技术坎。SM2,作为国家密码管理局发布的椭圆曲线公钥密码算法标准,在国密合规场景下,其地位就如同国际上的RSA或ECDSA。而ArkTS,作为鸿蒙生态的主力开发语言,其安全能力库 @ohos.security.cryptoFramework 虽然强大,但官方文档在SM2这种具体算法的实战细节上,往往语焉不详,特别是从密钥对生成到签名验签的全链路,新手很容易踩坑。
这个实战指南,就是来解决这个痛点的。它不是一篇泛泛而谈的原理介绍,而是一份从零开始、手把手、带避坑指南的实操手册。我将基于最新的ArkTS API(API 11及以上),拆解SM2密钥对的生成、存储、签名与验签的每一个步骤,解释背后“为什么这么做”,并分享我在实际项目中趟过的雷。无论你是刚接触国密算法,还是已经在ArkTS中集成加密功能但遇到了问题,这篇文章都能给你提供可直接“抄作业”的代码和清晰的思路。
2. 核心概念与ArkTS加密框架扫盲
在直接敲代码之前,我们必须统一几个关键概念,并理解ArkTS提供的加密工具箱是如何组织的。这能避免后续出现“代码能跑,但不知道为啥”的尴尬。
2.1 SM2签名验签到底在做什么?
你可以把SM2签名验签想象成一个精密的数字“封条”和“验钞机”组合。
- 签名(Sign) : 数据发送方(比如你的App客户端)持有自己的 私钥 。当需要发送一份重要数据(如交易请求)时,他用私钥对这份数据的“数字指纹”(由SM3杂凑算法生成)进行一系列复杂的数学运算,生成一个独一无二的“签名串”。这个签名串就像是用只有发送方才有的特殊印章盖下的封条,封条本身不包含数据内容,但和数据牢牢绑定。
- 验签(Verify) : 数据接收方(比如你的服务器)持有发送方的 公钥 。收到数据和签名串后,接收方用同样的方法计算收到数据的“数字指纹”,然后用发送方的公钥去“解锁”那个签名串。如果解锁后得到的“指纹”和自己计算出的“指纹”完全一致,就证明了两件事:第一,数据在传输过程中没有被篡改(完整性);第二,这份数据确实来自声称的发送方(身份认证)。
这里的关键是 私钥签名,公钥验签 。私钥必须绝对保密,通常存储在设备的安全芯片(如TEE)或由用户妥善保管;公钥则可以公开发布,任何人都可以用来验证签名的真伪。
2.2 ArkTS CryptoFramework 架构解析
ArkTS通过 @ohos.security.cryptoFramework 提供了统一的密码学操作接口。它的设计是模块化和异步的,理解其核心类的关系至关重要:
- CryptoFramework : 工厂类,一切的开端。用于创建
SymKeyGenerator(对称密钥生成器)、AsyKeyGenerator(非对称密钥生成器,我们用它生成SM2密钥对)、Cipher(加密解密)、Sign(签名验签)等对象。 - AsyKeyGenerator : 非对称密钥生成器。你需要指定算法(如
SM2_256)和参数,通过它来生成密钥对。 - KeyPair : 生成的密钥对容器,包含
pubKey(公钥)和priKey(私钥)两个属性。 - Sign : 签名验签操作的核心类。你需要先
init初始化(设置密钥和模式:签名或验签),然后update传入要处理的数据,最后sign生成签名或verify进行验签。 - DataBlob : ArkTS中用于表示二进制数据的通用对象,简单理解为一个
{ data: Uint8Array }的结构。密钥、签名、待处理数据通常都封装在DataBlob里进行传递。
整个流程是异步Promise驱动的,这意味着你需要熟悉 async/await 的写法。框架的这种设计保证了耗时操作不会阻塞UI主线程。
注意 : 在查阅资料时,你可能会看到一些基于旧版API或Java的SM2示例。请务必以当前鸿蒙官方文档为准,因为API在迭代中可能有较大变动。本文的代码基于主流的稳定版本。
3. 实战第一步:生成SM2密钥对
生成密钥对是后续所有操作的基础。在ArkTS中,我们不仅关心如何生成,更关心如何以安全的格式获取和保存它们。
3.1 密钥生成代码实现
首先,在项目的 entry/src/main/ets 目录下,创建一个用于处理加密的工具类,比如 CryptoUtil.ets 。
// CryptoUtil.ets
import cryptoFramework from '@ohos.security.cryptoFramework';
export class CryptoUtil {
/**
* 生成SM2密钥对
* @returns 返回生成的KeyPair(密钥对)
*/
static async generateSM2KeyPair(): Promise<cryptoFramework.KeyPair> {
try {
// 1. 创建非对称密钥生成器,指定算法为SM2,密钥长度为256位(固定)
let keyGenAlg = 'SM2_256';
let asyKeyGenerator = cryptoFramework.createAsyKeyGenerator(keyGenAlg);
if (asyKeyGenerator == null) {
throw new Error(`创建密钥生成器失败,请检查算法名称'${keyGenAlg}'是否正确或系统是否支持`);
}
// 2. 生成密钥对
let keyPair: cryptoFramework.KeyPair = await asyKeyGenerator.generateKeyPair();
console.info('SM2密钥对生成成功!');
// 3. 这里可以打印或转换密钥,用于调试(生产环境切勿日志输出私钥!)
// await this.logKeyPair(keyPair);
return keyPair;
} catch (error) {
console.error(`生成SM2密钥对失败: ${error.message}`);
throw error;
}
}
// 一个用于调试的辅助方法,将密钥转换为十六进制字符串查看
private static async logKeyPair(keyPair: cryptoFramework.KeyPair): Promise<void> {
let pubKeyBlob = await keyPair.pubKey.getEncoded();
let priKeyBlob = await keyPair.priKey.getEncoded();
console.info('公钥Hex:', this.uint8ArrayToHex(pubKeyBlob.data));
console.info('私钥Hex:', this.uint8ArrayToHex(priKeyBlob.data));
}
private static uint8ArrayToHex(uint8Array: Uint8Array): string {
return Array.from(uint8Array).map(b => b.toString(16).padStart(2, '0')).join('');
}
}
关键点解析:
SM2_256: 这是ArkTS框架中标识SM2算法的参数。256指曲线参数的长度,对于SM2是固定的。generateKeyPair(): 这是一个异步方法,返回一个Promise<KeyPair>。在实际设备上,生成过程可能会利用硬件安全能力,需要一定时间。getEncoded(): 这是Key对象的方法,用于获取密钥的二进制编码格式(DataBlob)。这个格式通常是DER或PKCS#8格式的,不是简单的裸密钥字节。
3.2 密钥的格式、保存与安全考量
生成的 KeyPair 对象在内存中,应用重启后就会丢失。因此,持久化保存是必须的,但这里的安全风险极高。
1. 公钥的保存: 公钥可以公开,所以保存方式很灵活。通常我们会将其 getEncoded() 后的二进制数据( DataBlob.data )进行Base64编码或转换为十六进制字符串,然后:
- 存储在应用的
Preferences(轻量级存储)中。 - 上传到服务器。
- 硬编码在代码里(不推荐,不利于更换)。
// 保存公钥示例
import util from '@ohos.util';
let pubKeyBlob = await keyPair.pubKey.getEncoded();
let base64Encoder = new util.Base64Helper();
let pubKeyBase64 = base64Encoder.encodeToStringSync(pubKeyBlob.data);
// 将pubKeyBase64存入Preferences
2. 私钥的保存(重中之重!): 私钥的泄露意味着身份被伪造。绝对禁止以明文形式存储在 Preferences 、文件或任何不安全的介质中。
- 推荐方案:使用系统密钥库(HUKS) 。鸿蒙的
@ohos.security.huks模块提供了硬件级的安全密钥存储。你可以将生成的私钥导入到HUKS中,由系统安全芯片保护,应用只持有一个密钥的别名(Alias)句柄。后续签名时,使用这个别名从HUKS中调用私钥进行操作,私钥本身不会暴露给应用内存。这是最安全的方式。 - 折中方案(仅用于测试或低安全要求) : 如果必须由应用自己管理,可以考虑:
- 加密后存储 : 使用一个由用户口令派生的密钥(通过PBKDF2)对称加密私钥二进制数据,再将密文存储起来。但这只是增加了攻击难度,并非绝对安全。
- 使用
cryptoFramework的SymKeyGenerator生成一个临时密钥进行加密 ,但这个临时密钥又面临同样存储问题。
实操心得: 在真实的商业应用中,尤其是涉及金融、政务的App, 必须使用HUKS来管理私钥 。虽然集成HUKS会增加一些代码复杂度(需要处理密钥属性、访问控制等),但这是通过安全审计和合规性检查的必经之路。开发初期为了快速验证流程,可以暂时将密钥对保存在内存或临时文件中,但务必在发布前替换为HUKS方案。
4. 核心环节:使用SM2进行数据签名
有了密钥对,我们就可以开始签名了。签名过程需要用到私钥和待签名的原始数据。
4.1 签名流程代码拆解
我们在 CryptoUtil 类中增加签名方法。
/**
* 使用SM2私钥对数据进行签名
* @param priKey 私钥对象
* @param data 待签名的原始数据 (Uint8Array 或 string)
* @returns 签名结果 (DataBlob)
*/
static async signData(priKey: cryptoFramework.PriKey, data: Uint8Array | string): Promise<cryptoFramework.DataBlob> {
try {
// 1. 统一输入数据为Uint8Array
let inputData: Uint8Array;
if (typeof data === 'string') {
// 将字符串转换为UTF-8编码的字节数组
let textEncoder = new util.TextEncoder();
inputData = textEncoder.encodeInto(data);
} else {
inputData = data;
}
// 2. 创建Sign实例,指定算法为SM2(带SM3杂凑)
let signAlg = 'SM2|SM3';
let signer = cryptoFramework.createSign(signAlg);
if (signer == null) {
throw new Error(`创建Sign实例失败,算法'${signAlg}'可能不支持`);
}
// 3. 初始化Signer,设置为签名模式,并传入私钥
await signer.init(priKey);
// 4. 更新(传入)待签名的数据
await signer.update({ data: inputData });
// 5. 执行签名操作
let signDataBlob = await signer.sign(null); // 参数为预留,通常传null
console.info('数据签名成功!签名长度:', signDataBlob.data.length);
return signDataBlob;
} catch (error) {
console.error(`SM2签名失败: ${error.message}`);
throw error;
}
}
关键点解析:
SM2|SM3: 这是ArkTS中指定SM2签名算法并携带SM3作为摘要算法的标准写法。SM2签名标准规定使用SM3生成数据的杂凑值。init(priKey): 明确使用私钥进行初始化,意味着接下来是签名操作。update(): 可以多次调用,用于处理大数据流。这里我们一次性传入所有数据。sign(null): 执行签名计算。参数预留,通常为null或空对象。返回的signDataBlob.data就是DER编码的签名值。
4.2 签名结果的编码与传输
签名结果 signDataBlob.data 是一个二进制的 Uint8Array 。为了在网络传输或存储中方便使用,我们需要将其编码为文本格式。
- Base64编码 : 最常用,长度适中,适合放在JSON、URL参数(需URL Safe)或文本文件中。
let base64Encoder = new util.Base64Helper(); let signatureBase64 = base64Encoder.encodeToStringSync(signDataBlob.data); // 现在可以将 signatureBase64 和原始数据一起发送给验签方 - 十六进制(Hex)编码 : 人类可读,但长度会增加一倍。
let signatureHex = Array.from(signDataBlob.data).map(b => b.toString(16).padStart(2, '0')).join('');
一个完整的签名数据包 通常包含:
- 原始数据(或数据的标识)。
- 签名值(Base64或Hex格式)。
- 签名使用的公钥(或公钥标识),以便验签方找到对应的公钥。
5. 核心环节:使用SM2进行签名验证
验签是签名的逆过程,发生在接收方。接收方持有原始数据、签名串和发送方的公钥。
5.1 验签流程代码实现
在 CryptoUtil 类中增加验签方法。
/**
* 使用SM2公钥验证签名
* @param pubKey 公钥对象
* @param data 原始数据 (Uint8Array 或 string)
* @param signatureToVerify 待验证的签名 (DataBlob 或 Uint8Array)
* @returns 验签是否通过 (boolean)
*/
static async verifySignature(pubKey: cryptoFramework.PubKey,
data: Uint8Array | string,
signatureToVerify: cryptoFramework.DataBlob | Uint8Array): Promise<boolean> {
try {
// 1. 统一输入数据
let inputData: Uint8Array;
if (typeof data === 'string') {
let textEncoder = new util.TextEncoder();
inputData = textEncoder.encodeInto(data);
} else {
inputData = data;
}
// 2. 统一签名数据
let signatureBlob: cryptoFramework.DataBlob;
if ((signatureToVerify as cryptoFramework.DataBlob).data !== undefined) {
signatureBlob = signatureToVerify as cryptoFramework.DataBlob;
} else {
signatureBlob = { data: signatureToVerify as Uint8Array };
}
// 3. 创建Sign实例,同样指定算法为 SM2|SM3
let verifyAlg = 'SM2|SM3';
let verifier = cryptoFramework.createVerify(verifyAlg);
if (verifier == null) {
throw new Error(`创建Verify实例失败,算法'${verifyAlg}'可能不支持`);
}
// 4. 初始化Verifier,设置为验签模式,并传入公钥
await verifier.init(pubKey);
// 5. 更新(传入)原始数据
await verifier.update({ data: inputData });
// 6. 执行验签操作
let verifyResult = await verifier.verify(signatureBlob);
console.info(`签名验证${verifyResult ? '通过' : '失败'}!`);
return verifyResult;
} catch (error) {
// 注意:验签失败可能抛出异常(如格式错误),也可能返回false。
// 这里捕获的是初始化、更新等过程的异常,verify()本身的失败会返回false。
console.error(`SM2验签过程发生错误: ${error.message}`);
return false;
}
}
关键点解析:
init(pubKey): 使用公钥初始化,表明接下来进行验签。verify(signatureBlob): 传入待验证的签名。该方法返回一个Promise<boolean>,true表示验签成功,false表示失败。- 异常处理 :
verify()方法本身在签名不匹配时返回false,而不是抛出异常。但如果签名数据格式错误、公钥不匹配算法等,之前的步骤(如init)可能会抛出异常。因此,我们需要在catch块中也返回false,表示验签未通过。
5.2 从编码格式还原密钥与签名
在实际场景中,你收到的公钥和签名往往是Base64或Hex字符串,而不是直接的 KeyPair 或 DataBlob 对象。因此,我们需要还原操作。
1. 从Base64字符串还原公钥进行验签: 这通常需要用到 cryptoFramework.createAsyKeyGenerator().convertKey 方法。但请注意, convertKey 通常需要密钥的格式参数(如PKCS#1, PKCS#8)。公钥的常见格式是X.509 SubjectPublicKeyInfo (SPKI)。ArkTS的 getEncoded() 默认输出的可能就是这种格式。假设你存储的是这个原始二进制数据的Base64。
static async getPubKeyFromBase64(base64Str: string): Promise<cryptoFramework.PubKey> {
let base64Decoder = new util.Base64Helper();
let pubKeyData = base64Decoder.decodeSync(base64Str); // 得到 Uint8Array
let keyGenAlg = 'SM2_256';
let asyKeyGenerator = cryptoFramework.createAsyKeyGenerator(keyGenAlg);
// 关键:将二进制数据转换回公钥对象。
// 这里假设数据是框架默认getEncoded()输出的格式。
let pubKey = await asyKeyGenerator.convertKey(pubKeyData, null);
return pubKey;
}
注意 :
convertKey的第二个参数是私钥转换时的密码,公钥转换时传null。如果转换失败,很可能是因为二进制数据的格式不对。你需要确认存储的公钥格式是否与getEncoded()输出的格式一致。
2. 从Base64字符串还原签名数据: 这个比较简单,解码后包装成 DataBlob 即可。
let base64Decoder = new util.Base64Helper();
let signatureData = base64Decoder.decodeSync(signatureBase64Str);
let signatureBlob: cryptoFramework.DataBlob = { data: signatureData };
// 然后将 signatureBlob 传入 verifySignature 方法
6. 完整流程串联与示例
让我们把上面的所有步骤串联起来,看一个从生成密钥对到签名再到验签的完整示例。假设在一个简单的用户登录场景,客户端对登录信息进行签名,服务端(这里模拟)进行验签。
// Example.ets
import { CryptoUtil } from './CryptoUtil';
import util from '@ohos.util';
async function demoSM2FullProcess() {
console.info('=== SM2 完整签名验签流程演示 ===');
// 步骤1:客户端生成密钥对(实际应用中,私钥应安全存储,公钥上传服务器)
console.info('\n1. 生成SM2密钥对...');
let keyPair;
try {
keyPair = await CryptoUtil.generateSM2KeyPair();
} catch (error) {
console.error('密钥对生成失败,流程终止。');
return;
}
// 步骤2:客户端准备待签名的数据(例如:`用户名:时间戳`)
let userId = 'user123';
let timestamp = Date.now().toString();
let rawData = `${userId}:${timestamp}`;
console.info(`\n2. 原始数据: "${rawData}"`);
// 步骤3:客户端使用私钥对数据进行签名
console.info('\n3. 客户端使用私钥进行签名...');
let signatureBlob;
try {
signatureBlob = await CryptoUtil.signData(keyPair.priKey, rawData);
let signatureBase64 = new util.Base64Helper().encodeToStringSync(signatureBlob.data);
console.info(` 生成签名(Base64): ${signatureBase64.substring(0, 50)}...`);
} catch (error) {
console.error('签名失败!');
return;
}
// 步骤4:模拟网络传输。客户端将 rawData, signatureBase64 和公钥(Base64格式)发送给服务器
let pubKeyBlob = await keyPair.pubKey.getEncoded();
let pubKeyBase64 = new util.Base64Helper().encodeToStringSync(pubKeyBlob.data);
console.info(`\n4. 客户端发送给服务器:`);
console.info(` 数据: ${rawData}`);
console.info(` 签名: ${signatureBase64.substring(0, 50)}...`);
console.info(` 公钥: ${pubKeyBase64.substring(0, 50)}...`);
// 步骤5:服务器端验签
console.info('\n5. 服务器端进行验签...');
// 5.1 服务器还原公钥对象(模拟)
let serverPubKey;
try {
// 注意:这里需要CryptoUtil中实现的getPubKeyFromBase64方法
serverPubKey = await CryptoUtil.getPubKeyFromBase64(pubKeyBase64);
} catch (error) {
console.error('服务器:公钥还原失败!');
return;
}
// 5.2 服务器还原签名数据
let serverSignatureData = new util.Base64Helper().decodeSync(signatureBase64);
let serverSignatureBlob: cryptoFramework.DataBlob = { data: serverSignatureData };
// 5.3 服务器进行验签
let isVerified;
try {
isVerified = await CryptoUtil.verifySignature(serverPubKey, rawData, serverSignatureBlob);
} catch (error) {
console.error('服务器:验签过程异常!');
return;
}
// 步骤6:验签结果
if (isVerified) {
console.info('\n✅ 验签成功!数据完整且来源可信。');
// 服务器可以放心处理 rawData 了
} else {
console.error('\n❌ 验签失败!数据可能被篡改或来源不可信。');
// 服务器应拒绝此请求
}
}
// 调用演示函数
demoSM2FullProcess();
7. 常见问题、踩坑实录与排查技巧
在实际开发中,你几乎一定会遇到下面这些问题。我把它们和解决方案整理出来,希望能帮你节省大量调试时间。
7.1 错误:“创建Sign/Verify实例失败”或“算法不支持”
- 问题现象 :
cryptoFramework.createSign('SM2|SM3')或createAsyKeyGenerator('SM2_256')返回null。 - 排查步骤 :
- 检查API版本 : 确认你的
compileSdkVersion和targetSdkVersion是否支持该算法。某些较老的SDK版本可能对国密算法支持不全。建议使用API 9及以上版本。 - 检查算法字符串 : 确保字符串完全正确,没有拼写错误或多余空格。
'SM2_256'和'SM2|SM3'是大小写敏感的。 - 检查导入模块 : 确认文件头部正确导入了
import cryptoFramework from '@ohos.security.cryptoFramework';。 - 查阅官方文档 : 前往 ArkTS API文档 ,确认当前版本文档中列出的支持算法。
- 检查API版本 : 确认你的
7.2 错误:签名或验签时抛出异常,提示“初始化错误”或“操作错误”
- 问题现象 : 在
signer.init(priKey)或verifier.init(pubKey)时报错。 - 可能原因与解决 :
- 密钥与算法不匹配 : 你用来初始化的密钥不是由
SM2_256算法生成的。例如,尝试用RSA的密钥进行SM2操作。确保密钥对是由正确的AsyKeyGenerator生成的。 - 密钥对象已损坏或无效 : 如果你尝试从存储的字符串还原密钥,但还原过程出错,得到的密钥对象是无效的。检查你的编解码过程,确保二进制数据没有丢失或损坏。 一个实用的调试方法 :在生成密钥对后,立即将其
getEncoded()并转换成Base64打印出来。在还原时,对比这个Base64字符串是否完全一致。 - 私钥用于验签或公钥用于签名 : 这是逻辑错误。
init时传入的密钥类型必须与操作匹配:签名用PriKey,验签用PubKey。
- 密钥与算法不匹配 : 你用来初始化的密钥不是由
7.3 错误:验签始终返回false,但步骤看似都正确
这是最令人头疼的问题。请按照以下清单逐一核对:
-
数据一致性(99%的问题出在这里) :
- 绝对保证 : 验签时使用的
原始数据,必须与签名时使用的原始数据逐字节完全相同 。一个额外的空格、不同的编码(如UTF-8 vs GBK)、甚至不可见的换行符(\nvs\r\n)都会导致杂凑值不同,从而使验签失败。 - 检查点 :
- 如果数据是字符串,签名和验签两端的字符串转换
Uint8Array的方式是否一致?(推荐都使用TextEncoder)。 - 如果数据包含时间戳,确保两端的时间戳字符串格式一致。
- 在网络传输中,是否对数据进行了不必要的URL解码或HTML实体解码?
- 如果数据是字符串,签名和验签两端的字符串转换
- 调试建议 : 在签名后和验签前,分别将数据的十六进制表示打印出来进行比对。
- 绝对保证 : 验签时使用的
-
签名数据一致性 :
- 确保传输的签名字符串(Base64/Hex)在接收端被正确解码回原始的二进制格式。Base64解码时注意URL Safe和Padding问题。
-
公钥一致性 :
- 验签使用的公钥,必须是对应签名私钥的配对公钥。确保服务器端使用的公钥确实是客户端生成的那个,没有弄混。
-
算法标识一致性 :
- 极少数情况下,不同系统或库对SM2签名的编码格式(如ASN.1 DER编码的序列结构)有细微差异。ArkTS使用的是标准格式。如果你需要与其他系统(如OpenSSL、GmSSL、Java BouncyCastle)交互,需要确认双方的签名输出/输入格式是否兼容。有时需要手动处理DER序列的编解码。
7.4 性能与内存优化提示
- 大文件签名 : 对于非常大的数据,不要一次性读取到内存再调用
update()。可以利用update()支持多次调用的特性,流式地读取和更新数据块。await signer.init(priKey); for (let chunk of largeDataChunks) { await signer.update({ data: chunk }); } let signature = await signer.sign(null); - 密钥对象复用 : 生成或转换密钥对象(
KeyPair,PubKey,PriKey)是相对耗时的操作。在应用生命周期内,如果密钥不变,应将其缓存起来,避免重复生成或转换。 - 异步操作 : 所有
cryptoFramework的主要操作都是异步的,避免在UI线程进行大量同步加密运算。
7.5 关于HUKS集成的一点补充
虽然本文示例为了清晰使用了内存中的密钥,但我必须再次强调生产环境使用HUKS的重要性。集成HUKS的基本思路是:
- 生成或导入密钥 : 使用
huks.generateKeyItem()或huks.importKeyItem()将密钥材料存入安全区,得到一个keyAlias。 - 签名时 : 不再直接使用
priKey对象,而是通过huks.init()、huks.update()、huks.finish()这一套接口,指定keyAlias来完成签名操作。私钥始终不出安全芯片。 - 验签时 : 公钥可以导出,因此验签流程和本文描述基本一致。
这部分的代码会更复杂,涉及大量的异步回调和参数配置,建议直接参考鸿蒙官方关于HUKS的专项文档和示例代码。
最后,SM2在ArkTS中的集成,核心在于理解框架的异步API设计、密钥的生命周期管理以及数据在各个环节的格式一致性。多写测试用例,对每个环节的输入输出进行十六进制打印比对,是快速定位问题的法宝。希望这份实战指南能让你在鸿蒙应用开发中,稳稳地跨过国密签名验签这道技术门槛。
更多推荐

所有评论(0)