How to solve the problem of signature verification failure using SM2 key pair?
Problem Description
When using SM2 signature in ArkTS, the signature verification fails on other platforms.
- Original text: This is Sign test plan
- Public key: 3059301306072a8648ce3d020106082a811ccf5501822d03420004974399fbf088c7c90bd2df21153d1e0d0a96b26026c3c8ad2374f63b9f04c1a3c5a19813d269653b4231a0d162d57ad35dbe0d9337fb6d89d1b404005f162b0e
- Signature: 3059301306072a8648ce3d020106082a811ccf5501822d03420004974399fbf088c7c90bd2df21153d1e0d0a96b26026c3c8ad2374f63b9f04c1a3c5a19813d269653b4231a0d162d57ad35dbe0d9337fb6d89d1b404005f162b0e
Background Knowledge
SM2 Digital Signature Algorithm: An asymmetric signature verification algorithm based on elliptic curves.
DER Format: In SM2 digital signatures, DER format is the standard encoding method for signature values (r, s), following the ASN.1 standard. This is a structured binary encoding format used to ensure the integrity and parsability of signature data. SM2 signature data in ArkTS is typically in DER format, while other platforms usually use raw signatures, which directly concatenate r||s (64 bytes).
Compressed vs Uncompressed Format: An SM2 public key typically refers to a point on an elliptic curve. In uncompressed format, the public key consists of a prefix 0x04 plus the X coordinate and Y coordinate, so the total length is 1+32+32=65 bytes (represented as 130 hexadecimal characters, including the 04 prefix). The compressed format public key only includes the X coordinate and a prefix (0x02 or 0x03), where the prefix is determined by the parity of the Y coordinate, so the total length is 1+32=33 bytes (represented as 66 hexadecimal characters). The public key formats on both ends are inconsistent.
Problem Analysis
Confirm encryption/decryption process: For issues with inconsistent encryption results between server and client, first verify that the signing and verification process on the same end is correct to determine if it's a cross-platform issue.
Confirm encoding format: During the signing and verification process, there are two encoding formats that need to be checked for consistency between server and client: public key format and signature format.
Analysis Conclusion
Signing and verification operations performed independently on the ArkTS side and server side both succeeded, indicating that the verification failure is a cross-platform issue.
Analyzing the public key format: After converting the public key generated by getEncoded on the ArkTS side to hexadecimal, it is an X.509 format public key in hexadecimal encoding, while the public key generated by the server is a raw elliptic curve public key in the format of 64 bytes of X||Y coordinate concatenation (without the 0x04 prefix). The public key formats on both ends are inconsistent.
Analyzing the signature format: The signature generated by signSync on the ArkTS side is in DER format, while the signature format generated by the server is a raw signature. The signature formats on both ends are inconsistent.
Solution
Convert ArkTS public key to raw format: Use the getAsyKeySpec method, specifying the parameters AsyKeySpecItem as ECC_PK_X_BN and ECC_PK_Y_BN to obtain the X coordinate and Y coordinate of the public key respectively. Then convert the coordinates to hexadecimal data, pad with leading zeros if the data is less than 64 bits, concatenate the processed X and Y, and finally add the prefix 04. Code implementation:
// Convert compressed public key to uncompressed public key
function convertPublicKeyFormat(keyPair: cryptoFramework.KeyPair): string {
let x = keyPair.pubKey.getAsyKeySpec(cryptoFramework.AsyKeySpecItem.ECC_PK_X_BN);
let y = keyPair.pubKey.getAsyKeySpec(cryptoFramework.AsyKeySpecItem.ECC_PK_Y_BN);
let pk = '04' + x.toString(16).padStart(64, '0') + y.toString(16).padStart(64, '0');
return pk;
}
Convert DER signature to raw signature: ArkTS has a genEccSignatureSpec method that can obtain r and s from ASN1 DER format SM2 signature data. After obtaining r and s, concatenate them. The final converted data is 64 bytes of binary data. For easier debugging and log configuration, it usually needs to be converted to hexadecimal data. Code for converting DER format signature to raw signature:
function bigIntTo32Bytes(bn: bigint): Uint8Array {
let hex = bn.toString(16).padStart(64, '0');
let bytes = new Uint8Array(32);
for (let i = 0; i < 64; i += 2) {
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
}
return bytes;
}
function convertSignFormat(signData: cryptoFramework.DataBlob): string {
let spec: cryptoFramework.EccSignatureSpec = cryptoFramework.SignatureUtils.genEccSignatureSpec(signData.data);
// Concatenate r||s raw signature
let rBytes = bigIntTo32Bytes(spec.r);
let sBytes = bigIntTo32Bytes(spec.s);
let rawSignature = new Uint8Array(64);
rawSignature.set(rBytes, 0); // Write r to positions 0-31
rawSignature.set(sBytes, 32); // Write s to positions 32-63
return uint8ArrayToHexStr(rawSignature);
}
Complete code:
import { cryptoFramework } from '@kit.CryptoArchitectureKit';
import { buffer } from '@kit.ArkTS';
import { hilog } from '@kit.PerformanceAnalysisKit';
const DOMAIN = 0x0000;
const FLAG = 'SM2_CRYPTO';
const rawMsg = 'This is Sign test plan';
let input: cryptoFramework.DataBlob = { data: new Uint8Array(buffer.from(rawMsg, 'utf-8').buffer) };
@Entry
@Component
struct Index {
build() {
Column() {
Button('Sign & Verify').onClick(() => {
main();
});
};
}
}
// uint8Array to hexadecimal utility function
function uint8ArrayToHexStr(array: Uint8Array): string {
let result = '';
for (let i = 0; i < array.length; i++) {
// Convert byte to hexadecimal and pad to two digits to ensure uniform format
const hexByte = array[i].toString(16).padStart(2, '0');
result += hexByte;
}
return result;
}
function signMessagePromise(priKey: cryptoFramework.PriKey) {
let signAlg = 'SM2_256|SM3';
let signer = cryptoFramework.createSign(signAlg);
signer.initSync(priKey);
let signData = signer.signSync(input);
return signData;
}
function verifyMessagePromise(signMessageBlob: cryptoFramework.DataBlob, pubKey: cryptoFramework.PubKey) {
let verifyAlg = 'SM2_256|SM3';
let verifier = cryptoFramework.createVerify(verifyAlg);
verifier.initSync(pubKey);
let res = verifier.verifySync(input, signMessageBlob);
return res;
}
function bigIntTo32Bytes(bn: bigint): Uint8Array {
let hex = bn.toString(16).padStart(64, '0');
let bytes = new Uint8Array(32);
for (let i = 0; i < 64; i += 2) {
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
}
return bytes;
}
function convertSignFormat(signData: cryptoFramework.DataBlob): string {
let spec: cryptoFramework.EccSignatureSpec = cryptoFramework.SignatureUtils.genEccSignatureSpec(signData.data);
// Concatenate r||s raw signature
let rBytes = bigIntTo32Bytes(spec.r);
let sBytes = bigIntTo32Bytes(spec.s);
let rawSignature = new Uint8Array(64);
rawSignature.set(rBytes, 0); // Write r to positions 0-31
rawSignature.set(sBytes, 32); // Write s to positions 32-63
return uint8ArrayToHexStr(rawSignature);
}
// Convert compressed public key to uncompressed public key
function convertPublicKeyFormat(keyPair: cryptoFramework.KeyPair): string {
let x = keyPair.pubKey.getAsyKeySpec(cryptoFramework.AsyKeySpecItem.ECC_PK_X_BN);
let y = keyPair.pubKey.getAsyKeySpec(cryptoFramework.AsyKeySpecItem.ECC_PK_Y_BN);
let pk = '04' + x.toString(16).padStart(64, '0') + y.toString(16).padStart(64, '0');
return pk;
}
async function main() {
let keyGenAlg = 'SM2_256';
try {
let generator = cryptoFramework.createAsyKeyGenerator(keyGenAlg);
let keyPair = generator.generateKeyPairSync();
let signData = signMessagePromise(keyPair.priKey);
let verifyResult = verifyMessagePromise(signData, keyPair.pubKey);
let signedData = convertSignFormat(signData);
hilog.info(DOMAIN, FLAG, `Original text: ${rawMsg}`);
hilog.info(DOMAIN, FLAG, `Compressed public key: ${uint8ArrayToHexStr(keyPair.pubKey.getEncoded().data)}`);
hilog.info(DOMAIN, FLAG, `Uncompressed public key: ${convertPublicKeyFormat(keyPair)}`);
hilog.info(DOMAIN, FLAG, `Private key: ${uint8ArrayToHexStr(keyPair.priKey.getEncoded().data)}`);
hilog.info(DOMAIN, FLAG, `DER signature: ${uint8ArrayToHexStr(signData.data)}`);
hilog.info(DOMAIN, FLAG, `Raw signature: ${signedData}`);
if (verifyResult === true) {
hilog.info(DOMAIN, FLAG, 'verify success');
} else {
hilog.error(DOMAIN, FLAG, 'verify failed');
}
} catch (e) {
hilog.info(DOMAIN, FLAG, `error: ${e}`);
}
}
Summary
SM2 cross-platform signature verification failures are typically caused by inconsistent data formats between platforms. The key issues are:
- Public key format mismatch: ArkTS generates X.509 format public keys, while other platforms use raw elliptic curve public keys (64-byte X||Y coordinate concatenation)
- Signature format mismatch: ArkTS generates DER format signatures, while other platforms use raw signatures (64-byte r||s concatenation)
The solution involves converting ArkTS data to match the format expected by other platforms by extracting the X and Y coordinates from the public key and the r and s values from the signature, then formatting them according to the raw format specification.
Top comments (0)