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

Faulty Certificate generation #307

Open kacper43 opened 3 years ago

kacper43 commented 3 years ago

Hi, I ran into a strange problem when generating certificates. Sometimes I couldn't use keys properly. I was encountering decryption error while using private key. I've made some tests to find out what is wrong and the conclusions are quite surprising. I wrote a simple service that encrypts given string with public key and decrypts it with private key. Function returns true value if given string is exactly the same as decrypted string. If something will go wrong it will return false. I am using that in a loop to find out how many iterations will it take to generate faulty certificate. Here are some test results: image

You can see that library sometimes creates incorrect Keys Pair. To eliminate that problem in app, I created simple if statement that checks if keys pair is correct, if not, app generates another pair.

Keys Testing service uses code from https://github.com/PeculiarVentures/PKI.js/tree/master/examples/HowToEncryptCMSviaCertificate to encrypt and decrypt string value. It can be triggered by simple function: startTest(publicKey: string, privateKey: string, testMessage: string): Observable<boolean> { let correctness = new Subject<boolean>(); let correctnessObs = correctness.asObservable(); this.publicKey = publicKey; this.privateKey = privateKey; this.testMessage = testMessage; this.envelopedEncrypt().then(isEncrypted => { if(isEncrypted) { this.envelopedDecrypt().then(decryptionStatus => { correctness.next(decryptionStatus); }) } else { correctness.next(false); } });

That function is triggered when new Certificate is generated: `createCertificate() { return this.createCertificateInternal().then(() => { var certificateString = String.fromCharCode.apply(null, new Uint8Array(this.certificateBuffer)); var resultString = "".concat(this.formatPEM(toBase64(certificateString))); this.publicKey = resultString;

  var privateKeyString = String.fromCharCode.apply(null, new Uint8Array(this.privateKeyBuffer));
  resultString = "".concat(this.formatPEM(toBase64(privateKeyString)));
  this.privateKey = resultString;

  this.testKeysSubscription = this.testKeys.startTest(this.publicKey, this.privateKey, "Test message").subscribe(isOK => {
    if(isOK) {
      console.log("Keys are OK");
      this.areKeysOk = true;
      if(this.publicKey && this.privateKey && this.areKeysOk){ 
        // here u can put code to trigger when everything is ok
      } 
      this.testKeysSub.unsubscribe();
    } else {
      console.log("Keys are corrupted");
      this.areKeysOk = false;
      this.testKeysSub.unsubscribe();
      this.createCertificate(); //create another pair of keys and test it
    }
  })`

I hope that it will be helpful for someone :) Good luck!

microshine commented 3 years ago

@kacper43 Do you use NodeJS or Browser runtime?

kacper43 commented 3 years ago

@microshine browser

microshine commented 3 years ago

@kacper43 Could you say which algorithms throw that exception?

Here is a simplified JS file that allows run tests in NodeJS

/* eslint-disable */
import assert from "assert";
import * as asn1js from "asn1js";
import { Crypto } from "@peculiar/webcrypto";
import * as utils from "pvutils";
import * as pkijs from "./src/index.js";

const name = "@peculiar/webcrypto";
const crypto = new Crypto();
pkijs.setEngine(name, new pkijs.CryptoEngine({ name, crypto, subtle: crypto.subtle }), new pkijs.CryptoEngine({ name, crypto, subtle: crypto.subtle }));

let hashAlg = "SHA-1";
let signAlg = "RSASSA-PKCS1-v1_5";
let oaepHashAlg = "SHA-1";

const encAlg = {
  name: "AES-CBC",
  length: 128
};

//#region Create CERT  

async function createCertificateInternal() {
  const certificate = new pkijs.Certificate();

  //#region Put a static values
  certificate.version = 2;
  certificate.serialNumber = new asn1js.Integer({ value: 1 });
  certificate.issuer.typesAndValues.push(new pkijs.AttributeTypeAndValue({
    type: "2.5.4.6", // Country name
    value: new asn1js.PrintableString({ value: "RU" })
  }));
  certificate.issuer.typesAndValues.push(new pkijs.AttributeTypeAndValue({
    type: "2.5.4.3", // Common name
    value: new asn1js.BmpString({ value: "Test" })
  }));
  certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({
    type: "2.5.4.6", // Country name
    value: new asn1js.PrintableString({ value: "RU" })
  }));
  certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({
    type: "2.5.4.3", // Common name
    value: new asn1js.BmpString({ value: "Test" })
  }));

  certificate.notBefore.value = new Date(2016, 1, 1);
  certificate.notAfter.value = new Date(2019, 1, 1);

  certificate.extensions = []; // Extensions are not a part of certificate by default, it's an optional array

  //#region "BasicConstraints" extension
  const basicConstr = new pkijs.BasicConstraints({
    cA: true,
    pathLenConstraint: 3
  });

  certificate.extensions.push(new pkijs.Extension({
    extnID: "2.5.29.19",
    critical: true,
    extnValue: basicConstr.toSchema().toBER(false),
    parsedValue: basicConstr // Parsed value for well-known extensions
  }));
  //#endregion

  //#region "KeyUsage" extension
  const bitArray = new ArrayBuffer(1);
  const bitView = new Uint8Array(bitArray);

  bitView[0] |= 0x02; // Key usage "cRLSign" flag
  bitView[0] |= 0x04; // Key usage "keyCertSign" flag

  const keyUsage = new asn1js.BitString({ valueHex: bitArray });

  certificate.extensions.push(new pkijs.Extension({
    extnID: "2.5.29.15",
    critical: false,
    extnValue: keyUsage.toBER(false),
    parsedValue: keyUsage // Parsed value for well-known extensions
  }));
  //#endregion
  //#endregion

  //#region Create a new key pair
  //#region Get default algorithm parameters for key generation
  const algorithm = pkijs.getAlgorithmParameters(signAlg, "generatekey");
  if ("hash" in algorithm.algorithm)
    algorithm.algorithm.hash.name = hashAlg;
  //#endregion

  const { publicKey, privateKey } = await crypto.subtle.generateKey(algorithm.algorithm, true, algorithm.usages);
  //#endregion

  //#region Exporting public key into "subjectPublicKeyInfo" value of certificate
  await certificate.subjectPublicKeyInfo.importKey(publicKey)
  //#endregion

  //#region Signing final certificate
  await certificate.sign(privateKey, hashAlg);
  //#endregion

  return {
    certificate,
    privateKey,
    publicKey
  }
}
//#endregion 

//#region Encrypt input data 

async function envelopedEncryptInternal(cert, valueBuffer) {
  const cmsEnveloped = new pkijs.EnvelopedData({
    originatorInfo: new pkijs.OriginatorInfo({
      certs: new pkijs.CertificateSet({
        certificates: [cert.certificate]
      })
    })
  });

  cmsEnveloped.addRecipientByCertificate(cert.certificate, { oaepHashAlgorithm: oaepHashAlg });

  await cmsEnveloped.encrypt(encAlg, valueBuffer);
  const cmsContentSimpl = new pkijs.ContentInfo();
  cmsContentSimpl.contentType = "1.2.840.113549.1.7.3";
  cmsContentSimpl.content = cmsEnveloped.toSchema();

  return cmsContentSimpl;
}
//#endregion 

//#region Decrypt input data 

async function envelopedDecryptInternal(cert, cms) {
  //#region Decode CMS Enveloped content
  const cmsEnvelopedSimp = new pkijs.EnvelopedData({ schema: cms.content });
  //#endregion

  const result = await cmsEnvelopedSimp.decrypt(0,
    {
      recipientCertificate: cert.certificate,
      recipientPrivateKey: await crypto.subtle.exportKey("pkcs8", cert.privateKey)
    });
  return result;
}
//#endregion 

context("How To Encrypt CMS via Certificate", () => {
  //#region Initial variables
  const hashAlgs = ["SHA-1", "SHA-256", "SHA-384", "SHA-512"];
  const oaepHashAlgs = ["SHA-1", "SHA-256", "SHA-384", "SHA-512"];
  const signAlgs = ["RSASSA-PKCS1-V1_5", "ECDSA", "RSA-PSS"];
  const encAlgs = ["AES-CBC", "AES-GCM"];
  const encLens = [128, 192, 256];
  // const hashAlgs = ["SHA-512"];
  // const oaepHashAlgs = ["SHA-1"];
  // const signAlgs = ["ECDSA"];
  // const encAlgs = ["AES-GCM"];
  // const encLens = [256];
  const iterations = 1;

  const valueBuffer = (new Uint8Array([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09])).buffer;
  //#endregion

  encAlgs.forEach(_encAlg => {
    encLens.forEach(_encLen => {
      signAlgs.forEach(_signAlg => {
        hashAlgs.forEach(_hashAlg => {
          oaepHashAlgs.forEach(_oaepHashAlg => {
            const testName = `${_encAlg} with ${_encLen}, ${_hashAlg} + ${_signAlg}, OAEP hash: ${_oaepHashAlg}`;

            let count = iterations;

            while (count--) {
              it(testName, async () => {
                hashAlg = _hashAlg;
                signAlg = _signAlg;
                oaepHashAlg = _oaepHashAlg;

                encAlg.name = _encAlg;
                encAlg.length = _encLen;

                const cert = await createCertificateInternal();
                const cms = await envelopedEncryptInternal(cert, valueBuffer);
                const message = await envelopedDecryptInternal(cert, cms);
                assert.strictEqual(utils.isEqualBuffer(message, valueBuffer), true, "Decrypted value must be equal with initially encrypted value");
              });
            }
          });
        });
      });
    });
  });
});
microshine commented 3 years ago

I can't reproduce that error. I ran 2880 tests (each mechanism 10 iterations) image

microshine commented 3 years ago

Here is the same test in the browser. I don't have any errors https://codesandbox.io/s/faulty-certificate-generation-issue-307-xgu83?file=/main.js