如何将SubtleCrypto生成的ECDSA密钥转为更便携短格式?
实现ECDSA(P-521)密钥的便携格式转换(压缩/十六进制)
问题描述
我在JavaScript中使用浏览器内置的SubtleCrypto库生成ECDSA密钥对,代码如下:
let keyPair = await crypto.subtle.generateKey( { name: "ECDSA", namedCurve: "P-521", }, true, ['sign', 'verify'] ) console.log(keyPair) let exportedPublicKey = await crypto.subtle.exportKey("jwk", keyPair.publicKey) let exportedPrivateKey = await crypto.subtle.exportKey("jwk", keyPair.privateKey) console.log(exportedPublicKey) console.log(exportedPrivateKey)
生成的密钥为JWK格式,我希望将其转换为更「便携」的格式:非JSON结构、长度更短,比如十六进制或压缩格式。尝试使用"raw"格式调用exportKey函数时,返回"Operation is not supported"错误。参考Eth-Crypto的publicKey.compress()方法可将公钥压缩为短字符串(如'03a34d6aef3eb42335fb3cacb59478c0b44c0bbeb8bb4ca427dbc7044157a5d24b'),请问如何实现类似功能?
解决方案
SubtleCrypto本身不支持直接导出压缩格式的ECDSA公钥,也不支持raw格式导出P-521曲线的密钥,因此需要手动从JWK格式转换。
1. 压缩P-521公钥
ECDSA压缩公钥的规则是:用一个前缀标识y坐标的奇偶性,仅保留x坐标数据,从而大幅缩短长度:
- 前缀
02:表示y坐标为偶数 - 前缀
03:表示y坐标为奇数 - 前缀后直接拼接x坐标的十六进制字符串
实现代码:
function compressP521PublicKey(jwkPublicKey) { // 工具函数:解码base64url为Uint8Array const decodeBase64Url = (str) => { str = str.replace(/-/g, '+').replace(/_/g, '/'); const padLength = (4 - str.length % 4) % 4; str += '='.repeat(padLength); return Uint8Array.from(atob(str), c => c.charCodeAt(0)); }; // 解码JWK中的x、y字段 const xBytes = decodeBase64Url(jwkPublicKey.x); const yBytes = decodeBase64Url(jwkPublicKey.y); // 判断y坐标是否为奇数 const isYOdd = yBytes[yBytes.length - 1] % 2 !== 0; const prefix = isYOdd ? '03' : '02'; // 将x字节转为十六进制字符串 const xHex = Array.from(xBytes) .map(byte => byte.toString(16).padStart(2, '0')) .join(''); return prefix + xHex; } // 使用示例 const compressedPubKey = compressP521PublicKey(exportedPublicKey); console.log(compressedPubKey); // 输出类似02/03开头的压缩公钥字符串
2. 私钥转十六进制格式
JWK格式的私钥中,d字段是私钥的base64url编码,直接解码后转为十六进制即可得到便携的字符串格式:
function privateKeyToHex(jwkPrivateKey) { const decodeBase64Url = (str) => { str = str.replace(/-/g, '+').replace(/_/g, '/'); const padLength = (4 - str.length % 4) % 4; str += '='.repeat(padLength); return Uint8Array.from(atob(str), c => c.charCodeAt(0)); }; const dBytes = decodeBase64Url(jwkPrivateKey.d); return Array.from(dBytes) .map(byte => byte.toString(16).padStart(2, '0')) .join(''); } // 使用示例 const privateKeyHex = privateKeyToHex(exportedPrivateKey); console.log(privateKeyHex); // 输出私钥的十六进制字符串
3. 从压缩公钥恢复完整密钥(可选)
如果需要将压缩公钥重新导入SubtleCrypto使用,需要通过椭圆曲线运算计算出y坐标。可以借助第三方库(如@noble/curves)实现:
// 先安装依赖:npm install @noble/curves import { p521 } from '@noble/curves/p521'; async function importCompressedPublicKey(compressedHex) { // 提取前缀和x坐标十六进制 const prefix = compressedHex.slice(0, 2); const xHex = compressedHex.slice(2); const x = BigInt('0x' + xHex); // 根据前缀计算对应的y坐标 const y = p521.getYfromX(x, prefix === '03'); // 工具函数:将Uint8Array编码为base64url const encodeBase64Url = (bytes) => { return btoa(String.fromCharCode(...bytes)) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); }; // 构造JWK格式 const jwk = { kty: 'EC', crv: 'P-521', x: encodeBase64Url(p521.utils.intToBytes(x, 66)), y: encodeBase64Url(p521.utils.intToBytes(y, 66)), ext: true }; // 导入密钥到SubtleCrypto return crypto.subtle.importKey( 'jwk', jwk, { name: 'ECDSA', namedCurve: 'P-521' }, true, ['verify'] ); }
内容的提问来源于stack exchange,提问作者sudoExclamationExclamation
相关产品推荐
相关产品推荐

