如何用SubtleCrypto从种子生成ECDSA密钥?解决导入格式错误
问题:基于种子和盐用SubtleCrypto生成ECDSA密钥对的错误解决
问题描述
希望仅使用浏览器原生JavaScript的SubtleCrypto API(不依赖任何外部库),以种子和盐为输入生成ECDSA密钥对。但示例代码因椭圆曲线密钥类型与Raw私钥数据格式不匹配,抛出「SyntaxError: Cannot create a key using the specified key usages」错误。根据MDN文档,ECDSA私钥仅支持通过PKCS8或JWT格式导入,而生成PKCS8格式又需要已有密钥调用exportKey(),陷入循环困境,求解决方法。
原错误代码
// Step 1: PBKDF2 to derive seed from mnemonic and password async function mnemonicToSeed(mnemonic, password = '') { const mnemonicBuffer = new TextEncoder().encode(mnemonic.normalize('NFKD')); const saltBuffer = new TextEncoder().encode('mnemonic' + password.normalize('NFKD')); const keyMaterial = await window.crypto.subtle.importKey( 'raw', mnemonicBuffer, { name: 'PBKDF2' }, false, ['deriveBits'] ); const seed = await window.crypto.subtle.deriveBits( { name: 'PBKDF2', salt: saltBuffer, iterations: 2048, hash: 'SHA-512' }, keyMaterial, 512 // 512 bits = 64 bytes ); return new Uint8Array(seed); // Return the seed as a Uint8Array } // Helper function to convert Uint8Array to hex string function toHex(buffer) { return Array.from(buffer).map(b => b.toString(16).padStart(2, '0')).join(''); } // Step 2: Generate an ECDSA key pair using WebCrypto and the P-384 curve async function generateECDSAKeys(mnemonic, password) { // Step 2.1: Convert mnemonic to seed const seed = await mnemonicToSeed(mnemonic, password); // Step 2.2: Use part of the seed as the private key const privateKeyBytes = seed.slice(0, 48); // First 48 bytes as private key material (384 bits) // Step 2.3: Import the private key into WebCrypto (P-384 curve) const privateKey = await window.crypto.subtle.importKey( 'raw', privateKeyBytes, { name: 'ECDSA', namedCurve: 'P-384' }, true, ['sign'] ); // Step 2.4: Export the public key derived from the private key const publicKey = await window.crypto.subtle.exportKey('spki', privateKey); // Step 2.5: Export the private key in raw format (for display) const exportedPrivateKey = await window.crypto.subtle.exportKey('raw', privateKey); return { privateKey: toHex(new Uint8Array(exportedPrivateKey)), publicKey: toHex(new Uint8Array(publicKey)) }; } // Example usage const mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; const password = 'your_password'; generateECDSAKeys(mnemonic, password).then(keys => { console.log('Private Key:', keys.privateKey); console.log('Public Key:', keys.publicKey); }).catch(console.error);
解决方案
核心问题
SubtleCrypto的importKey方法不支持直接导入raw格式的ECDSA私钥字节,仅接受PKCS8、JWK等结构化格式。
解决步骤
手动构造PKCS8格式的DER编码数据,将raw私钥字节嵌入其中,再导入到SubtleCrypto。P-384曲线对应的OID是1.3.132.0.34,我们可以按PKCS8的ASN.1标准构造结构。
修正后的完整代码
// Step 1: PBKDF2 to derive seed from mnemonic and password async function mnemonicToSeed(mnemonic, password = '') { const mnemonicBuffer = new TextEncoder().encode(mnemonic.normalize('NFKD')); const saltBuffer = new TextEncoder().encode('mnemonic' + password.normalize('NFKD')); const keyMaterial = await window.crypto.subtle.importKey( 'raw', mnemonicBuffer, { name: 'PBKDF2' }, false, ['deriveBits'] ); const seed = await window.crypto.subtle.deriveBits( { name: 'PBKDF2', salt: saltBuffer, iterations: 2048, hash: 'SHA-512' }, keyMaterial, 512 // 512 bits = 64 bytes ); return new Uint8Array(seed); // Return the seed as a Uint8Array } // Helper function to convert Uint8Array to hex string function toHex(buffer) { return Array.from(buffer).map(b => b.toString(16).padStart(2, '0')).join(''); } // Helper: Convert raw P-384 private key bytes to PKCS8 DER format function rawPrivateKeyToPkcs8(rawKey) { // P-384 curve OID: 1.3.132.0.34, encoded as DER: 06 05 2B 81 04 00 22 const curveOid = new Uint8Array([0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x22]); // ECPrivateKey structure (ASN.1) const privateKeySeq = new Uint8Array([ 0x30, 0x74, // SEQUENCE length (116 bytes) 0x02, 0x01, 0x01, // Version: 1 0x04, 0x30, // OCTET STRING length (48 bytes) ...rawKey, 0xA0, 0x41, // [0] EXPLICIT SEQUENCE length (65 bytes) 0x30, 0x3F, // SEQUENCE length (63 bytes) ...curveOid, // Unused public key field (required for parsers, zeroed) 0x03, 0x38, 0x00, 0x04, ...new Uint8Array(96) ]); // PKCS8 structure const pkcs8 = new Uint8Array([ 0x30, 0x81, 0x9B, // SEQUENCE length (155 bytes) 0x02, 0x01, 0x00, // Version: 0 0x30, 0x13, // AlgorithmIdentifier SEQUENCE length (19 bytes) 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, // EC OID: 1.2.840.10045.2.1 ...curveOid, 0x04, 0x81, 0x82, // Private key OCTET STRING length (130 bytes) ...privateKeySeq ]); return pkcs8; } // Step 2: Generate an ECDSA key pair using WebCrypto and the P-384 curve async function generateECDSAKeys(mnemonic, password) { // Step 2.1: Convert mnemonic to seed const seed = await mnemonicToSeed(mnemonic, password); // Step 2.2: Use part of the seed as the private key const privateKeyBytes = seed.slice(0, 48); // First 48 bytes as private key material (384 bits) // Step 2.3: Convert raw private key to PKCS8 format const pkcs8PrivateKey = rawPrivateKeyToPkcs8(privateKeyBytes); // Step 2.4: Import the PKCS8 private key into WebCrypto const privateKey = await window.crypto.subtle.importKey( 'pkcs8', pkcs8PrivateKey, { name: 'ECDSA', namedCurve: 'P-384' }, true, ['sign'] ); // Step 2.5: Export the public key derived from the private key const publicKey = await window.crypto.subtle.exportKey('spki', privateKey); // Step 2.6: Export private key in JWK format (raw not supported for ECDSA) const exportedPrivateKey = await window.crypto.subtle.exportKey('jwk', privateKey); return { privateKey: exportedPrivateKey.d, // Raw private key (Base64URL encoded) publicKey: toHex(new Uint8Array(publicKey)) }; } // Example usage const mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; const password = 'your_password'; generateECDSAKeys(mnemonic, password).then(keys => { console.log('Private Key:', keys.privateKey); console.log('Public Key:', keys.publicKey); }).catch(console.error);
关键说明
rawPrivateKeyToPkcs8函数:按照PKCS8和ECPrivateKey的ASN.1标准,将raw私钥字节包装成合法的PKCS8 DER编码数据,包含曲线OID和必要结构字段。- 私钥导入:改用
pkcs8格式导入构造好的密钥数据,符合SubtleCrypto的要求。 - 私钥导出:ECDSA私钥不支持导出为raw格式,改用JWK格式导出后提取
d字段(raw私钥的Base64URL编码),如需十六进制格式可自行转换。
内容的提问来源于stack exchange,提问作者vladimir_1969_2
相关产品推荐
相关产品推荐

