You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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);

关键说明

  1. rawPrivateKeyToPkcs8函数:按照PKCS8和ECPrivateKey的ASN.1标准,将raw私钥字节包装成合法的PKCS8 DER编码数据,包含曲线OID和必要结构字段。
  2. 私钥导入:改用pkcs8格式导入构造好的密钥数据,符合SubtleCrypto的要求。
  3. 私钥导出:ECDSA私钥不支持导出为raw格式,改用JWK格式导出后提取d字段(raw私钥的Base64URL编码),如需十六进制格式可自行转换。

内容的提问来源于stack exchange,提问作者vladimir_1969_2

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 07:02:07