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.3k stars 204 forks source link

Certificate created are invalid #346

Closed Ottunger closed 2 years ago

Ottunger commented 2 years ago

Hi,

I've recently started to use your lib rather than node-forge to create certificates, so as to register the CRL distribution endpoint inside (and by the way, the CRL creation itself works fine). However, the certificates I get are invalid and I can't understand why. Could this be because I use node-webcrypto-ossl as provider? This is indeed heavily based on your "Certificate complex example", hence the question. Again, the CRL emitting part works fine.

The following code gives you a reproducible example:

import {atob, btoa} from 'anote-server-libs/services/utils.js';
import {arrayBufferToString, toBase64} from 'pvutils';
import * as asn1js from 'asn1js';
import {Crypto} from 'node-webcrypto-ossl';
import {AttributeTypeAndValue, GeneralName, BasicConstraints, Certificate, CertificateRevocationList, CryptoEngine, ExtKeyUsage, CRLDistributionPoints,
    DistributionPoint, Time, RevokedCertificate, Extension, Extensions, CertificateTemplate, CAVersion, getAlgorithmParameters, setEngine} from 'pkijs';

function base64ToArrayBuffer(b64) {
    const byteString = atob(b64);
    const byteArray = new Uint8Array(byteString.length);
    for(var i=0; i < byteString.length; i++) {
        byteArray[i] = byteString.charCodeAt(i);
    }
    return byteArray;
}
function pemToArrayBuffer(pem) {
    const b64Lines = pem.replace(/[\r\n]/g, '');
    return base64ToArrayBuffer(b64Lines.replace(/-----[A-Z ]+KEY-----/g, ''));
}
function formatPEM(pemString) {
    const PEM_STRING_LENGTH = pemString.length, LINE_LENGTH = 64;
    const wrapNeeded = PEM_STRING_LENGTH > LINE_LENGTH;
    if(wrapNeeded) {
        let formattedString = '', wrapIndex = 0;
        for(let i = LINE_LENGTH; i < PEM_STRING_LENGTH; i += LINE_LENGTH) {
            formattedString += pemString.substring(wrapIndex, i) + '\r\n';
            wrapIndex = i;
        }
        formattedString += pemString.substring(wrapIndex, PEM_STRING_LENGTH);
        return formattedString;
    } else {
        return pemString;
    }
}

function createCertificateInternal(config) {
    let sequence = Promise.resolve();

    const certificate = new Certificate();
    certificate.version = 2;
    certificate.serialNumber = new asn1js.Integer({value: config.serial});
    certificate.issuer.typesAndValues.push(new AttributeTypeAndValue({
        type: '2.5.4.6', // Country name
        value: new asn1js.PrintableString({value: config.country})
    }));
    certificate.issuer.typesAndValues.push(new AttributeTypeAndValue({
        type: '2.5.4.3', // Common name
        value: new asn1js.BmpString({value: config.issuerName})
    }));
    certificate.issuer.typesAndValues.push(new AttributeTypeAndValue({
        type: '2.5.4.10', // Organization name
        value: new asn1js.BmpString({value: config.organization})
    }));
    certificate.subject.typesAndValues.push(new AttributeTypeAndValue({
        type: '2.5.4.6', // Country name
        value: new asn1js.PrintableString({value: config.country})
    }));
    certificate.subject.typesAndValues.push(new AttributeTypeAndValue({
        type: '2.5.4.3', // Common name
        value: new asn1js.BmpString({value: config.name})
    }));
    certificate.subject.typesAndValues.push(new AttributeTypeAndValue({
        type: '2.5.4.10', // Organization name
        value: new asn1js.BmpString({value: config.organization})
    }));

    certificate.notBefore.value = new Date();
    certificate.notAfter.value = new Date();
    certificate.notAfter.value.setFullYear(certificate.notAfter.value.getFullYear() + 100);

    certificate.extensions = [];
    const basicConstr = new BasicConstraints({
        cA: true,
        pathLenConstraint: 3
    });
    certificate.extensions.push(new Extension({
        extnID: '2.5.29.19',
        critical: true,
        extnValue: basicConstr.toSchema().toBER(false),
        parsedValue: basicConstr
    }));

    const crlDistributionPoints = new CRLDistributionPoints({
        distributionPoints: [new DistributionPoint({
            distributionPoint: new GeneralName({type: 1, value: config.crlDistributionPoint}),
            cRLIssuer: new GeneralName({type: 1, value: config.issuerName})
        })]
    });
    certificate.extensions.push(new Extension({
        extnID: '2.5.29.31',
        critical: false,
        extnValue: crlDistributionPoints.toSchema().toBER(false),
        parsedValue: crlDistributionPoints
    }));

    const bitArray = new ArrayBuffer(1);
    const bitView = new Uint8Array(bitArray);
    bitView[0] |= 0x01; // digital signature and non-repudiation
    const keyUsage = new asn1js.BitString({valueHex: bitArray});
    certificate.extensions.push(new Extension({
        extnID: '2.5.29.15',
        critical: false,
        extnValue: keyUsage.toBER(false),
        parsedValue: keyUsage
    }));

    const extKeyUsage = new ExtKeyUsage({
        keyPurposes: [
            '1.3.6.1.5.5.7.3.2', // id-kp-clientAuth
            '1.3.6.1.5.5.7.3.4', // id-kp-emailProtection
            '1.3.6.1.5.5.7.3.8', // id-kp-timeStamping
        ]
    });
    certificate.extensions.push(new Extension({
        extnID: '2.5.29.37',
        critical: false,
        extnValue: extKeyUsage.toSchema().toBER(false),
        parsedValue: extKeyUsage
    }));

    const certType = new asn1js.Utf8String({value: 'certType'});
    certificate.extensions.push(new Extension({
        extnID: '1.3.6.1.4.1.311.20.2',
        critical: false,
        extnValue: certType.toBER(false),
        parsedValue: certType
    }));

    const prevHash = new asn1js.OctetString({valueHex: (new Uint8Array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])).buffer});
    certificate.extensions.push(new Extension({
        extnID: '1.3.6.1.4.1.311.21.2',
        critical: false,
        extnValue: prevHash.toBER(false),
        parsedValue: prevHash
    }));

    const certificateTemplate = new CertificateTemplate({
        templateID: '1.1.1.1.1.1',
        templateMajorVersion: 10,
        templateMinorVersion: 20
    });
    certificate.extensions.push(new Extension({
        extnID: '1.3.6.1.4.1.311.21.7',
        critical: false,
        extnValue: certificateTemplate.toSchema().toBER(false),
        parsedValue: certificateTemplate
    }));

    const caVersion = new CAVersion({
        certificateIndex: 10,
        keyIndex: 20
    });
    certificate.extensions.push(new Extension({
        extnID: '1.3.6.1.4.1.311.21.1',
        critical: false,
        extnValue: caVersion.toSchema().toBER(false),
        parsedValue: caVersion
    }));

    const crypto = new Crypto({
        directory: './key_storage'
    });
    setEngine('openssl', crypto, new CryptoEngine({name: 'openssl', crypto: crypto, subtle: crypto.subtle}));
    sequence = sequence.then(() =>
    {
        const algorithm = getAlgorithmParameters('RSASSA-PKCS1-v1_5', 'generatekey');
        if('hash' in algorithm.algorithm)
            algorithm.algorithm.hash.name = 'SHA-256';
        algorithm.algorithm.modulusLength = config.modulusLength;
        return Promise.all([
            crypto.subtle.generateKey(algorithm.algorithm, true, algorithm.usages),
            crypto.subtle.importKey('pkcs8', pemToArrayBuffer(config.signingPem), {name:'RSASSA-PKCS1-v1_5', hash:{name:'SHA-256'}}, true, ['sign', 'verify'])
        ]);
    });
    sequence = sequence.then(([keyPair, signingKey]) =>
        [keyPair, signingKey, certificate.subjectPublicKeyInfo.importKey(keyPair.publicKey)]
    );
    sequence = sequence.then(([keyPair, signingKey]) => certificate.sign(keyPair.privateKey, 'SHA-256').then(() => keyPair));
    sequence = sequence.then((keyPair) =>
    {
        return [keyPair, certificate.toSchema(true).toBER(false)];
    });
    sequence = sequence.then(([keyPair, certificateBuffer]) =>
        crypto.subtle.exportKey('pkcs8', keyPair.privateKey).then(privateKeyBuffer => [privateKeyBuffer, certificateBuffer]));

    return sequence;
}

export function createCertificate(config) {
    return createCertificateInternal(config).then(([privateKeyBuffer, certificateBuffer]) => {
        const certificateString = String.fromCharCode.apply(null, new Uint8Array(certificateBuffer));
        let certResultString = '-----BEGIN CERTIFICATE-----\r\n';
        certResultString = `${certResultString}${formatPEM(btoa(certificateString))}`;
        certResultString = `${certResultString}\r\n-----END CERTIFICATE-----\r\n`;

        const privateKeyString = String.fromCharCode.apply(null, new Uint8Array(privateKeyBuffer));
        let keyResultString = `-----BEGIN PRIVATE KEY-----\r\n`;
        keyResultString = `${keyResultString}${formatPEM(btoa(privateKeyString))}`;
        keyResultString = `${keyResultString}\r\n-----END PRIVATE KEY-----\r\n`;

        return [keyResultString, certResultString];
    });
}

createCertificate({
            signingPem: `-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQCmL1BWiJEUXOrOPAnMM6VM7Iy3mAV5hOsP1lIj/6lDzpQ3Q+7f
PkG8jBHHoSJM3wLWNtKQMBpu0VsxFnoMIuwkVc/+vZj7nlYMBLrSqOZfY8FBSrOt
7Xv+IvgiYgShBAG4L9bVp5ABJGcsoZnEDa1TfW2HlwoPk7sd5wmY7J6f9wIDAQAB
AoGAHLJs6BR7IQ4OigB6HKYKdGcgwY9h2qMmSDzVQFwkqY3hsE1t0WUZyupRI6zi
lG2qOr2KzNVRqzNB0Q81kiTxq+lIqDNVzpWVfQU7qDQ+MDDp3AmJKdsdUVLUtm1o
TdP6M6MtjASZwezsnbx5V7Lcn+osoVEHAbVrjpFRd8LEVOECQQDYTwRmGQg1zFkc
xzD/Lw+KeVACpQ7p9XYB4NDeswFiU8FZFY4X4knEzVXU9T2Cbj0Cn385VWcsAvv+
1LSJ6keRAkEAxK3Blhdr9/PMrXBaV3r+tJyLqORyccaJ74FhtO6yg3rlJAvVWGX5
Rm8vEFhVraU51zLXt2PW7TLlYGPrR5R7BwJAOdh3vq33ChwJwK5sJfH53/gtM2fc
oyhnVH1Ani2Usyzeyen/w9daDu0yhO7Icjb0zdzFcxmpq5Voum87kJ48YQJAOeSX
ljGgw2TNO8RVo2h97vYhmf5cvabeVVS1SQf2HgOfzWN6UkH6BUSXCu2lkq6O/wxl
OQM3cazInf3rdK99IwJAVKYi2RLe5rflCLslT8tpZ5gQZrysTjl6h50UA7QjlHS5
yS5Q3QkH1/Ltfp3q+CFRFylfP/2BEnDTVKShi2RbAw==
-----END RSA PRIVATE KEY-----`,
            serial: parseInt(1 + '50653853', 10),
            country: 'XX',
            organization: 'Citizens',
            name: '50653853',
            issuerName: 'DigitalID',
            crlDistributionPoint: 'https://xxx/api/v1/users/crl',
            modulusLength: 2048
        }).then(console.log, console.error);

/*
function createCRLInternal(config: CreateCRLConfig): Promise<ArrayBuffer> {
    let sequence: Promise<any> = Promise.resolve();

    const crlSimpl = new CertificateRevocationList();
    crlSimpl.version = 1;
    crlSimpl.issuer.typesAndValues.push(new AttributeTypeAndValue({
        type: '2.5.4.6', // Country name
        value: new asn1js.PrintableString({
            value: config.country
        })
    }));
    crlSimpl.issuer.typesAndValues.push(new AttributeTypeAndValue({
        type: '2.5.4.3', // Common name
        value: new asn1js.BmpString({
            value: config.name
        })
    }));

    crlSimpl.thisUpdate = new Time({
        type: 0,
        value: new Date()
    });

    crlSimpl.revokedCertificates = config.revokedCerts.map(r => new RevokedCertificate({
        userCertificate: new asn1js.Integer({
            value: r.serial
        }),
        revocationDate: new Time({
            value: r.revokedDate
        })
    }));
    crlSimpl.crlExtensions = new Extensions({
        extensions: [new Extension({
            extnID: '2.5.29.20', // cRLNumber
            extnValue: (new asn1js.Integer({
                value: config.version
            })).toBER(false)
        })]
    });

    sequence = sequence.then(() =>
    {
        const crypto = new Crypto({
            directory: __dirname + '/key_storage'
        });
        setEngine('openssl', crypto, new CryptoEngine({name: 'openssl', crypto: crypto, subtle: crypto.subtle}));
        return crypto.subtle.importKey('pkcs8', pemToArrayBuffer(config.signingPem), {name:'RSASSA-PKCS1-v1_5', hash:{name:'SHA-256'}}, true, ['sign', 'verify']);
    });
    sequence = sequence.then(privateKey =>
    {
        return crlSimpl.sign(privateKey, 'SHA-256');
    });
    sequence = sequence.then(() =>
    {
        return crlSimpl.toSchema(true).toBER(false);
    });

    return sequence;
}

export function createCRL(config: CreateCRLConfig) {
    return createCRLInternal(config).then(crlBuffer => {
        let resultString = '-----BEGIN X509 CRL-----\r\n';
        resultString += formatPEM(toBase64(arrayBufferToString(crlBuffer)));
        resultString = `${resultString}\r\n-----END X509 CRL-----\r\n`;
        return resultString;
    });
}
*/
Ottunger commented 2 years ago

I've just tested on node v16 to have a native implementation of crypto, and it seems to be the same, so problems should lie somewhere else than the provider.

import {webcrypto as crypto} from 'crypto';
setEngine('node', crypto, new CryptoEngine({name: 'node', crypto: crypto, subtle: crypto.subtle}));
Ottunger commented 2 years ago

Problem was with btoa, closing!

microshine commented 2 years ago

@Ottunger Have you seen @peculiar/x509? It allows generating X509 certificate easier https://github.com/PeculiarVentures/x509#create-a-self-signed-certificate

Ottunger commented 2 years ago

@microshine I don't think https://github.com/PeculiarVentures/x509 can encode CRL distribution points can it?

Anyways, I'm still stuck with two problems using directly PKI.js;

Untitled

Can you tell me if my code above is correct for these two?

Ottunger commented 2 years ago

Found a fix for both. Far reaching dates still seem to be a problem.

microshine commented 2 years ago

Get CRL extension

import * as asn1Schema from "@peculiar/asn1-schema";
import * as asn1X509 from "@peculiar/asn1-x509";
import { AsnConvert } from "@peculiar/asn1-schema";
import * as x509 from "@peculiar/x509";

const crlDistPtrExt = cert.getExtension(id_ce_cRLDistributionPoints);
if (crlDistPtrExt) {
    const crlDistPtr = AsnConvert.parse(crlDistPtrExt.value, CRLDistributionPoints);
    console.log(crlDistPtr);
}

Cert generation with CRL extension

const alg = {
  name: "RSASSA-PKCS1-v1_5",
  hash: "SHA-256",
  publicExponent: new Uint8Array([1, 0, 1]),
  modulusLength: 2048,
};
const keys = await crypto.subtle.generateKey(alg, false, ["sign", "verify"]);

const crlDistPtr = new asn1X509.CRLDistributionPoints([
  new asn1X509.DistributionPoint({
    distributionPoint: new asn1X509.DistributionPointName({
      fullName: [
        new asn1X509.GeneralName({
          uniformResourceIdentifier: "https://some.com/crl",
        })
      ],
    })
  })
]);

const cert = await x509.X509CertificateGenerator.createSelfSigned({
  serialNumber: "01",
  name: "CN=Test",
  notBefore: new Date("2020/01/01"),
  notAfter: new Date("2020/01/02"),
  signingAlgorithm: alg,
  keys,
  extensions: [
    new x509.BasicConstraintsExtension(true, 2, true),
    new x509.ExtendedKeyUsageExtension(["1.2.3.4.5.6.7", "2.3.4.5.6.7.8"], true),
    new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true),
    await x509.SubjectKeyIdentifierExtension.create(keys.publicKey),
    new x509.Extension(asn1X509.id_ce_cRLDistributionPoints, false, AsnConvert.serialize(crlDistPtr)),
  ]
});

console.log(cert.toString("pem"));
microshine commented 2 years ago
image
Ottunger commented 2 years ago

Thanks for the heads up :)

Got it the same with

        const crlDistributionPoints = new CRLDistributionPoints({
        distributionPoints: [new DistributionPoint({
            distributionPoint: [new GeneralName({type: 6, value: config.crlDistributionPoint})], // URI type
            cRLIssuer: [new GeneralName({type: 1, value: config.issuerName})] // Name type
        })]
    });
    certificate.extensions.push(new Extension({
        extnID: '2.5.29.31',
        critical: false,
        extnValue: crlDistributionPoints.toSchema().toBER(false),
        parsedValue: crlDistributionPoints
    }));

Although I'll admit x509 lib reduces code length!