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

Signed certificates sometimes does not verify against their CA public key #385

Open or390 opened 7 months ago

or390 commented 7 months ago

I've encountered a weird issue where signed certificates sometimes does not verify against their CA public key. It rarely happens, only once in couple of thousands signatures, and it only happens with crypto.X509Certificate.verify function (pkijs.Certificate verify does return true). I'm not sure if the issue is somewhere in your code or webcrypto's, I was hoping you could confirm that.

Replication code (includes both the check that does work and the one that doesn't):

const pkijs = require("pkijs");
const asn1js = require("asn1js");
const crypto = require("crypto");
const assert = require('node:assert/strict');

const CA_CERT = "MIIBVjCB/aADAgECAgkAvRYDXayXsWswCgYIKoZIzj0EAwIwDzENMAsGA1UEAwwEVGVzdDAeFw0yMzExMDUxNjIwMDBaFw00ODExMDUxNjIwMDBaMA8xDTALBgNVBAMMBFRlc3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATgh9XNXTfcEdQ1tfZwqWiTshUsIv+j2bTLUYLSzI110WD9Pte83iN4OkXL/bK060wwXKmkEXG9I47NH8lwVX1So0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBROogLnS8YM4FqWlz0IVS+AJv3yWjAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwIDSAAwRQIhAJ91UC+6I70mMOwbkDWZR8lVpzZjCs0meyt3InmBe2lVAiAIJ8fXSSgzc9Fl9LOQ4a6aDKOBS49lSbXhUr+fSh2S7Q==";
const CA_PRIVATE_KEY = "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQglGJ0AxhOPZlH1recNlmzqEMATJqvZInZUBob2Ij3gcOhRANCAATgh9XNXTfcEdQ1tfZwqWiTshUsIv+j2bTLUYLSzI110WD9Pte83iN4OkXL/bK060wwXKmkEXG9I47NH8lwVX1S";

it('demonstrate webcrypto verify error', async function () {
    this.timeout(600000);

    const cryptoEngine = new pkijs.CryptoEngine({
        name: 'OpenSSL',
        crypto: crypto.webcrypto
    });
    const caCert = pkijs.Certificate.fromBER(new Uint8Array(Buffer.from(CA_CERT, 'base64')).buffer);
    const caPrivateKey = await cryptoEngine.importKey('pkcs8', new Uint8Array(Buffer.from(CA_PRIVATE_KEY, 'base64')).buffer, {
        name: 'ECDSA',
        namedCurve: 'P-256'
    }, false, ['sign']);
    const caPublicKey = crypto.KeyObject.from(await cryptoEngine.importKey('spki', caCert.subjectPublicKeyInfo.toSchema().toBER(false), {
        name: 'ECDSA',
        namedCurve: 'P-256'
    }, true, ['verify']));

    const {publicKey, privateKey} = await cryptoEngine.generateKey(
        {
            name: 'ECDSA',
            namedCurve: 'P-256'
        },
        true,
        ['sign', 'verify']
    );

    const certificateSigningRequest = await getCertificateSigningRequest(cryptoEngine, publicKey, privateKey);

    const certificate = new pkijs.Certificate();

    certificate.version = 2;
    certificate.serialNumber = new asn1js.Integer({value: 1});

    certificate.notBefore.value = new Date();
    certificate.notBefore.value.setDate(certificate.notBefore.value.getDate() - 1);

    certificate.notAfter.value = new Date();
    certificate.notAfter.value.setDate(certificate.notAfter.value.getDate() + 7);

    certificate.subject = certificateSigningRequest.subject;

    certificate.subjectPublicKeyInfo = certificateSigningRequest.subjectPublicKeyInfo;

    certificate.issuer = caCert.subject;

    for (let i = 1; i <= 200000; i++) {
        console.log(`verifying signature [${i}]`)
        await certificate.sign(caPrivateKey, 'SHA-256', cryptoEngine);
        const certDER = certificate.toSchema(true).toBER(false);
        const x509Certificate = new crypto.X509Certificate(Buffer.from(certDER));

        const verified_pkijs = await certificate.verify(caCert, cryptoEngine); // this always works

        assert(verified_pkijs === true)

        const verified_crypto = x509Certificate.verify(caPublicKey); // this sometimes fails

        assert(verified_crypto === true)
    }
});

async function getCertificateSigningRequest(cryptoEngine, publicKey, privateKey) {
    const csr = new pkijs.CertificationRequest();

    csr.version = 0;

    csr.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({
        type: '2.5.4.3', // commonName
        value: new asn1js.Utf8String({value: 'Test'})
    }));

    csr.attributes = [];

    await csr.subjectPublicKeyInfo.importKey(publicKey, cryptoEngine);

    await csr.sign(privateKey, 'SHA-256', cryptoEngine);

    return csr;
}