如何在Web Crypto API中将签名与公钥(ArrayBuffer)转为字符串存储
问题
我正在使用Web Crypto API进行数据签名,目前已获得ArrayBuffer类型的公钥和签名,需要将它们分别存储到对应文件中:
- 公钥以字符串格式存入"public_key"文件
- 签名以字符串格式存入"signature"文件
我是该领域新手,不清楚便于Java后端等其他语言/工具处理的合适文件格式。公钥采用SPKI格式的PEM字符串似乎可行,但不知道签名的处理方法,已编写如下代码:
const generateKeyPair = async function() { return crypto.subtle.generateKey( { name: "ECDSA", namedCurve: "P-384", }, true, ["sign", "verify"] ); }; const sign = async function(privateKey: CryptoKey, buffer: BufferSource) { return crypto.subtle.sign({ name: privateKey.algorithm.name, hash: 'SHA-512' }, privateKey, buffer); }; const exportKeyAsSPKI = async function(cryptoKey: CryptoKey) { return crypto.subtle.exportKey("spki", cryptoKey) }; const spkiToPEM = function(buffer: ArrayBuffer) { const asString = String.fromCharCode.apply(null, new Uint8Array(buffer)); const asBase64 = btoa(asString); return `-----BEGIN PUBLIC KEY-----\n${asBase64}\n-----END PUBLIC KEY-----`; }; (async () => { // Generate keys const { privateKey, publicKey } = await generateKeyPair(); // Sign data const data = { foo: 'bar' }; const stringifiedData = JSON.stringify(data); const buffer = new TextEncoder().encode(stringifiedData); const signature = await sign(privateKey, buffer); // Convert public key to string to store into file const publicKeyAsSPKIFormat = await exportKeyAsSPKI(publicKey); const pemEncodedKey = spkiToPEM(publicKeyAsSPKIFormat); // Convert signature to string to store into file // ... TODO ... // DONE console.log("Public key:"); console.log(pemEncodedKey); console.log("Signature:"); console.log("todo..."); })();
请问签名应该如何转换为可存储的字符串?
解决方案
1. 基础转换:二进制签名转Base64字符串
Web Crypto返回的签名是ArrayBuffer二进制数据,要转成可存储的字符串,最通用的方式是Base64编码,几乎所有语言(包括Java)都能直接解码处理。
你可以用和公钥转换类似的逻辑实现:
const arrayBufferToBase64 = (buffer: ArrayBuffer) => { const uint8Array = new Uint8Array(buffer); const asString = String.fromCharCode.apply(null, uint8Array); return btoa(asString); };
在代码中替换TODO部分:
// Convert signature to string to store into file const signatureBase64 = arrayBufferToBase64(signature);
2. 兼容Java后端的关键:签名格式转换
注意:Web Crypto的ECDSA签名返回的是IEEE P1363格式(直接拼接r和s的二进制,P-384曲线下每个部分48字节,总长度96字节),而Java默认的Signature类验证的是ASN.1 DER格式的签名(带有ASN.1结构的编码)。如果直接传递Base64的P1363格式签名给Java,会验证失败。
需要把P1363格式转成DER格式,再转Base64,实现函数如下:
const ecdsaP1363ToDer = (signature: ArrayBuffer) => { const uint8 = new Uint8Array(signature); const curveByteLength = uint8.length / 2; // P-384是48字节,总长度96 let r = uint8.slice(0, curveByteLength); let s = uint8.slice(curveByteLength); // 移除r和s开头的0字节(ASN.1要求整数是最小长度) while (r[0] === 0) r = r.slice(1); while (s[0] === 0) s = s.slice(1); // 如果最高位是1,加一个0字节避免被解析为负数 if (r[0] >= 0x80) r = new Uint8Array([0, ...r]); if (s[0] >= 0x80) s = new Uint8Array([0, ...s]); // 构建ASN.1 DER结构:SEQUENCE(INTEGER(r), INTEGER(s)) const derHeader = new Uint8Array([ 0x30, // SEQUENCE标签 2 + r.length + 2 + s.length, // 总长度 0x02, // INTEGER标签 r.length, // r的长度 ...r, 0x02, // INTEGER标签 s.length, // s的长度 ...s ]); return derHeader.buffer; };
然后在代码中使用:
// Convert signature to DER format first, then Base64 const derSignature = ecdsaP1363ToDer(signature); const signatureBase64 = arrayBufferToBase64(derSignature);
这样得到的Base64字符串,Java后端可以直接用Base64.getDecoder().decode()解码,再通过Signature.verify()方法验证。
3. 可选:给签名加PEM包装(非必须)
如果想和公钥的PEM格式统一,也可以给Base64的签名加上PEM头部尾部:
const signatureToPEM = (signatureBase64: string) => { return `-----BEGIN ECDSA SIGNATURE-----\n${signatureBase64}\n-----END ECDSA SIGNATURE-----`; };
不过多数场景下,直接存储Base64字符串就足够了,Java处理起来也更简便。
内容的提问来源于stack exchange,提问作者baitendbidz
相关产品推荐
相关产品推荐

