PeculiarVentures / PKI.js

PKI.js is a pure JavaScript library implementing the formats that are used in PKI applications (signing, encryption, certificate requests, OCSP and TSP requests/responses). It is built on WebCrypto (Web Cryptography API) and requires no plug-ins.
http://pkijs.org
Other
1.25k stars 204 forks source link

Slow PKCS #12 generation #403

Open pboguslawski opened 2 months ago

pboguslawski commented 2 months ago

PKCS #12 generating with 600k iterations with code below (similar to openSSLLike example) takes 49-60s (tested in Firefox 115.9.1esr 64bit and Chromium 90 64-bit). Tested that most of the execution time is eaten by last makeInternalValues call (integrity protection envelope).

PKCS #12 generating with 600k iterations on same machine with openssl 3 from Debian 12 takes less than 1s:

$ time openssl pkcs12 -export -in test.crt -inkey test.key -out test.p12 -name 'test' -password pass:1234 -iter 600000

real    0m0.917s
user    0m0.894s
sys 0m0.000s

Is it expected or PKI.js bug?

PKCS #12 generation function code using PKI.js (PBKDF2_ITERATION_COUNT is set to 600000 during test):

// downloadPKCS12 downloads given private key and certificate in encrypted PKCS #12 file.
export async function downloadPKCS12(
    keyPair: CryptoKeyPair,
    certificate: Certificate,
    ownerId: string,
    password: string,
    filename: string
) {
    if (!password) {
        throw new Error('password cannot be empty');
    }
    if (!ownerId) {
        throw new Error('ownerId cannot be empty');
    }
    if (!filename) {
        throw new Error('filename cannot be empty');
    }

    const crypto = getCrypto(true);
    const passwordConverted = Convert.FromUtf8String(password);
    const certFingerprint = await crypto.digest('SHA-1', certificate.toSchema().toBER(false));
    const privateKeyBinary = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey);
    const pkcs8Simpl = new PrivateKeyInfo({ schema: fromBER(privateKeyBinary).result });

    // Put initial values for PKCS#12 structures.
    const pkcs12 = new PFX({
        parsedValue: {
            integrityMode: 0, // Password-Based Integrity Mode.
            authenticatedSafe: new AuthenticatedSafe({
                parsedValue: {
                    safeContents: [
                        {
                            privacyMode: 1, // Password-Based Privacy Protection Mode.
                            value: new SafeContents({
                                safeBags: [
                                    new SafeBag({
                                        bagId: '1.2.840.113549.1.12.10.1.3',
                                        bagValue: new CertBag({
                                            parsedValue: certificate
                                        }),
                                        bagAttributes: [
                                            new Attribute({
                                                type: '1.2.840.113549.1.9.21', // localKeyID
                                                values: [new OctetString({ valueHex: certFingerprint })]
                                            }),
                                            new Attribute({
                                                type: '1.2.840.113549.1.9.20', // friendlyName
                                                values: [new BmpString({ value: ownerId })]
                                            })
                                        ]
                                    })
                                ]
                            })
                        },
                        {
                            privacyMode: 0, // No-privacy Protection Mode.
                            value: new SafeContents({
                                safeBags: [
                                    new SafeBag({
                                        bagId: '1.2.840.113549.1.12.10.1.2',
                                        bagValue: new PKCS8ShroudedKeyBag({
                                            parsedValue: pkcs8Simpl
                                        }),
                                        bagAttributes: [
                                            new Attribute({
                                                type: '1.2.840.113549.1.9.21', // localKeyID
                                                values: [new OctetString({ valueHex: certFingerprint })]
                                            }),
                                            new Attribute({
                                                type: '1.2.840.113549.1.9.20', // friendlyName
                                                values: [new BmpString({ value: ownerId })]
                                            })
                                        ]
                                    })
                                ]
                            })
                        }
                    ]
                }
            })
        }
    });

    // Encode internal values for PKCS8ShroudedKeyBag.
    if (!(pkcs12.parsedValue && pkcs12.parsedValue.authenticatedSafe)) {
        throw new Error('pkcs12.parsedValue.authenticatedSafe is empty');
    }
    await pkcs12.parsedValue.authenticatedSafe.parsedValue.safeContents[1].value.safeBags[0].bagValue.makeInternalValues(
        {
            password: passwordConverted,
            contentEncryptionAlgorithm: {
                name: 'AES-CBC', // OpenSSL can handle AES-CBC only.
                length: 256
            },
            hmacHashAlgorithm: 'SHA-256',
            iterationCount: PBKDF2_ITERATION_COUNT
        }
    );

    // Encode internal values for all SafeContents first (create all Privacy Protection envelopes).
    await pkcs12.parsedValue.authenticatedSafe.makeInternalValues({
        safeContents: [
            {
                password: passwordConverted,
                contentEncryptionAlgorithm: {
                    name: 'AES-CBC', // OpenSSL can handle AES-CBC only.
                    length: 256
                },
                hmacHashAlgorithm: 'SHA-256',
                iterationCount: PBKDF2_ITERATION_COUNT
            },
            {
                // Empty parameters for second SafeContent since No Privacy protection mode there.
            }
        ]
    });

    // Encode internal values for Integrity Protection envelope.
    await pkcs12.makeInternalValues({
        password: passwordConverted,
        iterations: PBKDF2_ITERATION_COUNT, // Big value here causes long generation time.
        pbkdf2HashAlgorithm: 'SHA-256', // Least two parameters are equal because at the moment it is not clear how to use PBMAC1 schema with PKCS#12 integrity protection.
        hmacHashAlgorithm: 'SHA-256'
    });

    // Download prepared PKCS #12 content to file.
    downloadFile(filename, 'application/pkcs12', pkcs12.toSchema().toBER(false));
}