Skip to content
Snippets Groups Projects
signingUtilities.js 33.2 KiB
Newer Older
import {canTryPincode, failPincodeAttempt, getTimeLeftInLocalStorage, makeid} from './appUtility';
import {bufferToHexCodes, stringToArrayBuffer} from 'pvutils';
import {fromBER} from 'asn1js';
import {ContentInfo, SignedData} from 'pkijs';
import {algomap, rdnmap} from '../constants/certificates';

const libmime = require('libmime');
const pkijs = require('pkijs');
const asn1js = require('asn1js');
const pvutils = require('pvutils');

//*********************************************************************************

const CERTIFIATE_Version_1 = 0;
const CERTIFIATE_Version_3 = 2;

//these bit fields are reversed, WTF!
const KEY_USAGE_DigitalSignature	= 0x80;//01;
const KEY_USAGE_NonRepudiation		= 0x40;//02;
const KEY_USAGE_KeyEncipherment		= 0x20;//04;
const KEY_USAGE_DataEncipherment	= 0x10;//08;
const KEY_USAGE_KeyAgreement		= 0x08;//10;
const KEY_USAGE_KeyCertSign			= 0x04;//20;
const KEY_USAGE_CRLSign				= 0x02;//40;
//const KEY_USAGE_EncipherOnly		= 0x01;//80; // Not used for now. Must be used together with KEY_USAGE_KeyAgreement (maybe should be ORed as a constant directly?)
//const KEY_USAGE_DecipherOnly		= 0x80;//0100; // If used, modify "KeyUsage" extension array buffer size and appropriate bit operators to accomodate for extra byte

const KEY_USAGE_LeafCertificate		= KEY_USAGE_DigitalSignature | KEY_USAGE_NonRepudiation | KEY_USAGE_KeyEncipherment | KEY_USAGE_DataEncipherment;
const KEY_USAGE_CertificateAuthority= KEY_USAGE_DigitalSignature | KEY_USAGE_KeyCertSign | KEY_USAGE_CRLSign;


const OID_EXT_KEY_USAGE_Any     	= "2.5.29.37.0";
const OID_ID_PKIX_ServerAuth		= "1.3.6.1.5.5.7.3.1";
const OID_ID_PKIX_ClientAuth		= "1.3.6.1.5.5.7.3.2";
const OID_ID_PKIX_CodeSigning		= "1.3.6.1.5.5.7.3.3";
const OID_ID_PKIX_EmailProtection	= "1.3.6.1.5.5.7.3.4";
const OID_ID_PKIX_TimeStamping	    = "1.3.6.1.5.5.7.3.8";
const OID_ID_PKIX_OCSPSigning		= "1.3.6.1.5.5.7.3.9";
// const OID_EXT_KEY_USAGE_MS...	= "1.3.6.1.4.1.311.10.3.1"; // Microsoft Certificate Trust List signing
// const OID_EXT_KEY_USAGE_MS...	= "1.3.6.1.4.1.311.10.3.4";  // Microsoft Encrypted File System
const OID_PKCS7_Data                = "1.2.840.113549.1.7.1";
const OID_PKCS7_SignedData          = "1.2.840.113549.1.7.2";
const OID_PKCS7_EnvelopedData       = "1.2.840.113549.1.7.3";
const OID_PKCS9_EmailAddress        = "1.2.840.113549.1.9.1";
const OID_PKCS9_ContentType         = "1.2.840.113549.1.9.3";
const OID_PKCS9_MessageDigest       = "1.2.840.113549.1.9.4";
const OID_PKCS9_SigningTime         = "1.2.840.113549.1.9.5";

const defaultAlgorithms = {
  hashAlg: "SHA-256",
  signAlg: "RSASSA-PKCS1-v1_5",
  keyLength: 2048
};

const AES_encryptionVariant_Password  = 2;
const encryptionAlgorithm = {
  name: "AES-CBC",
  length: 128
};

//*********************************************************************************
// Returns promise, resolved to keyPair object {publicKey, privateKey}
//*********************************************************************************
function generateKeys(algorithms) {
  //region Get a "crypto" extension
  const crypto = pkijs.getCrypto();
  if (typeof crypto === "undefined") {
    return Promise.reject("No WebCrypto extension found");
  }
  //endregion Get a "crypto" extension

  if (!algorithms) {
    algorithms = defaultAlgorithms;
  } else {
    if (!algorithms.hashAlg) {
      algorithms.hashAlg = defaultAlgorithms.hashAlg;
    }
    if (!algorithms.signAlg) {
      algorithms.signAlg = defaultAlgorithms.signAlg;
    }
    if (!algorithms.keyLength) {
      algorithms.keyLength = defaultAlgorithms.keyLength;
    }
  }

  //region Get default algorithm parameters for key generation
  const algorithm = pkijs.getAlgorithmParameters(algorithms.signAlg, "generatekey");
  if("hash" in algorithm.algorithm) {
    algorithm.algorithm.hash.name = algorithms.hashAlg;
  }
  algorithm.algorithm.modulusLength = algorithms.keyLength;
  //endregion

  return crypto.generateKey(algorithm.algorithm, true, algorithm.usages);
}

//*********************************************************************************
function createCertificate(certData, issuerData = null)
{

  if (typeof certData === "undefined") {
    return Promise.reject("No Certificate data provided");
  }

  if (typeof certData.subject === "undefined") {
    return Promise.reject("No Certificate subject data provided");
  }


  //region Get a "crypto" extension
  const crypto = pkijs.getCrypto();

  if (typeof crypto === "undefined") {
    return Promise.reject("No WebCrypto extension found");
  }
  //endregion Get a "crypto" extension

  //region Initial variables
  let sequence = Promise.resolve();

  const certificate = new pkijs.Certificate();
  let publicKey;
  let privateKey;

  let certificateBuffer;// = new ArrayBuffer(0); // ArrayBuffer with loaded or created CERT
  let privateKeyBuffer;// = new ArrayBuffer(0);
  let publicKeyBuffer;// = new ArrayBuffer(0);

  //endregion Initial variables

  if (certData.keyPair) {
    //region Create a new key pair
    sequence = sequence.then(() =>
    {
      return certData.keyPair;
    });
    //endregion Create a new key pair

  } else {
    //region Create a new key pair
    sequence = sequence.then(() =>
    {
      return generateKeys(certData.algorithms);
    });
    //endregion Create a new key pair
  }

  //region Store new key in an interim variables
  sequence = sequence.then(keyPair =>
  {
    publicKey = keyPair.publicKey;
    privateKey = keyPair.privateKey;
  }, error => Promise.reject(`Error during key generation: ${error}`));
  //endregion Store new key in an interim variables

  //region Exporting public key into "subjectPublicKeyInfo" value of certificate
  sequence = sequence.then(() =>
    certificate.subjectPublicKeyInfo.importKey(publicKey)
  );
  //endregion Exporting public key into "subjectPublicKeyInfo" value of certificate

  sequence = sequence.then(
    () => crypto.digest({ name: "SHA-1" }, certificate.subjectPublicKeyInfo.subjectPublicKey.valueBlock.valueHex),
    error => Promise.reject(`Error during importing public key: ${error}`)
  );

  //region Fill in cert data
  sequence = sequence.then(subjKeyIdBuffer =>
  {

    //region Put a static values
    certificate.version = CERTIFIATE_Version_3;

    const serialNumberBuffer = new ArrayBuffer(20);
    const serialNumberView = new Uint8Array(serialNumberBuffer);
    pkijs.getRandomValues(serialNumberView);
    // noinspection JSUnresolvedFunction
    certificate.serialNumber = new asn1js.Integer({ valueHex: serialNumberView });
    //endregion Put a static values

    //region Subject
    // For reference http://oidref.com/2.5.4.3
    if (certData.subject.commonName) {
      // noinspection JSUnresolvedFunction
      certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({
        type: "2.5.4.3", // Common name
        value: new asn1js.PrintableString({ value: certData.subject.commonName })
      }));
    }

    if (certData.subject.country) {
      // noinspection JSUnresolvedFunction
      certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({
        type: "2.5.4.6", // Country name
        value: new asn1js.PrintableString({ value: certData.subject.country })
      }));
    }

    if (certData.subject.locality) {
      // noinspection JSUnresolvedFunction
      certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({
        type: "2.5.4.7", // Locality Name
        value: new asn1js.PrintableString({ value: certData.subject.locality })
      }));
    }

    if (certData.subject.state) {
      // noinspection JSUnresolvedFunction
      certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({
        type: "2.5.4.8", // State or Province name
        value: new asn1js.PrintableString({ value: certData.subject.state })
      }));
    }

    if (certData.subject.organization) {
      // noinspection JSUnresolvedFunction
      certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({
        type: "2.5.4.10", // Organization name
        value: new asn1js.PrintableString({ value: certData.subject.organization })
      }));
    }

    if (certData.subject.organizationUnit) {
      // noinspection JSUnresolvedFunction
      certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({
        type: "2.5.4.11", // Organization unit name
        value: new asn1js.PrintableString({ value: certData.subject.organizationUnit })
      }));
    }

    if (certData.subject.email) {
      // noinspection JSUnresolvedFunction
      certificate.subject.typesAndValues.push(new pkijs.AttributeTypeAndValue({
        type: OID_PKCS9_EmailAddress, // Email, deprecated but still widely used
        value: new asn1js.IA5String({ value: certData.subject.email })
      }));
    }
    //endregion Subject


    //region Issuer
    if (issuerData && issuerData.certificate) {
      certificate.issuer = issuerData.certificate.subject;
    } else {
      certificate.issuer = certificate.subject;
    }
    //endregion Issuer

    //region Validity
    if (!certData.validity) {
      certData.validity = {}
    }

    if (certData.validity.notBefore) {
      certificate.notBefore.value = certData.validity.notBefore; //date
    } else {
      const tmp = new Date();
      certificate.notBefore.value = new Date(tmp.getFullYear(), tmp.getMonth(), tmp.getDate(), 0, 0, 0);
    }

    if (certData.validity.notAfter) {
      certificate.notAfter.value = certData.validity.notAfter; //date
    } else {
      const tmp = certificate.notBefore.value;
      const validYears = certData.validity.validYears || 1;
      certificate.notAfter.value = new Date(tmp.getFullYear() + validYears, tmp.getMonth(), tmp.getDate(), 23, 59, 59);
    }
    //endregion Validity

    //region Extensions
    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: !!certData.isCA,
      //pathLenConstraint: 0 //TODO add logic for leaf CA
    });

    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 "BasicConstraints" extension

    //region "KeyUsage" extension
    const keyUsageBuffer = new ArrayBuffer(1);
    const keyUsageBitView = new Uint8Array(keyUsageBuffer);

    keyUsageBitView[0] = !!certData.isCA ? KEY_USAGE_CertificateAuthority : KEY_USAGE_LeafCertificate;

    // noinspection JSUnresolvedFunction
    const keyUsage = new asn1js.BitString({ valueHex: keyUsageBuffer });

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

    //region "ExtKeyUsage" extension
    if (!certData.isCA && certData.subject.email) {
      const extKeyUsage = new pkijs.ExtKeyUsage({
        keyPurposes: [
          OID_ID_PKIX_EmailProtection
        ]
      });

      certificate.extensions.push(new pkijs.Extension({
        extnID: "2.5.29.37",
        critical: false,
        extnValue: extKeyUsage.toSchema().toBER(false),
        parsedValue: extKeyUsage // Parsed value for well-known extensions
      }));
    }
    //endregion "ExtKeyUsage" extension

    //region "SubjAltName" extension
    if (certData.subject.email || certData.subject.url) {

      const names = [];

      if (certData.subject.email) {
        names.push(new pkijs.GeneralName({
          type: 1, // rfc822Name
          value: certData.subject.email
        }));
      }

      if (certData.subject.url) {
        names.push(new pkijs.GeneralName({
          type: 2, // dNSName
          value: certData.subject.url
        }));
      }

      const subjAltNames = new pkijs.GeneralNames({
        names: names
      });

      certificate.extensions.push(new pkijs.Extension({
        extnID: "2.5.29.17",
        critical: false,
        extnValue: subjAltNames.toSchema().toBER(false),
        parsedValue: subjAltNames // Parsed value for well-known extensions
      }));
    }
    //endregion "SubjAltName" extension


    //region "SubjectKeyIdentifier" extension
    const subjKeyId = new asn1js.OctetString({ valueHex: subjKeyIdBuffer });

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

    /* COULD NOT GET IT WORKING
        //region "AuthorityKeyIdentifier" extension
        if (issuerData && issuerData.certificate) {

          let issuerSubjKeyExt = null;

          let extLength = issuerData.certificate.extensions.length;
          for (var i = 0; i < extLength; i++) {
            let ext = issuerData.certificate.extensions[i];
            if (ext.extnID == "2.5.29.14") {
              issuerSubjKeyExt = ext;
              break;
            }
          }

          if (issuerSubjKeyExt) {

            const asn1 = asn1js.fromBER(issuerSubjKeyExt.extnValue);

            const authKeyIdentifier = new AuthorityKeyIdentifier({
              keyIdentifier: new asn1js.OctetString({
                //isHexOnly: true,
                //valueHex: issuerSubjKeyExt.parsedValue.valueBlock.valueHex
                value: new asn1js.OctetString({ valueHex: subjKeyIdBuffer })
              })
            });
            // const authKeyIdentifier = new AuthorityKeyIdentifier({
            // 	//keyIdentifier: new asn1js.OctetString({ valueHex: subjKeyIdBuffer })

            // });

            certificate.extensions.push(new Extension({
              extnID: "2.5.29.35",
              critical: false,
              extnValue: authKeyIdentifier.toSchema().toBER(false),
              parsedValue: authKeyIdentifier // Parsed value for well-known extensions
            }));
          }
        }
        //endregion "AuthorityKeyIdentifier" extension
    */
    //endregion Extensions
  });
  //region Fill in cert data


  //region Signing final certificate
  sequence = sequence.then(() => {
      let signerKey = (issuerData && issuerData.privateKey) ? issuerData.privateKey : privateKey;
      return certificate.sign(signerKey, (certData.algorithms && certData.algorithms.hashAlg) ? certData.algorithms.hashAlg : defaultAlgorithms.hashAlg)
    },
    error => Promise.reject(`Error during exporting public key: ${error}`));
  //endregion

  //region Encode and store certificate
  sequence = sequence.then(() =>
  {
    certificateBuffer = certificate.toSchema(true).toBER(false);
  }, error => Promise.reject(`Error during signing: ${error}`));
  //endregion

  //region Exporting public key
  sequence = sequence.then(() =>
    crypto.exportKey("spki", publicKey)
  );
  //endregion

  //region Store exported public key on Web page
  sequence = sequence.then(result =>
  {
    publicKeyBuffer = result;
  }, error => Promise.reject(`Error during exporting of public key: ${error}`));
  //endregion

  //region Exporting private key
  sequence = sequence.then(() =>
    crypto.exportKey("pkcs8", privateKey)
  );
  //endregion

  //region Store exported key on Web page
  sequence = sequence.then(result =>
  {
    privateKeyBuffer = result;
  }, error => Promise.reject(`Error during exporting of private key: ${error}`));
  //endregion

  return sequence.then(() => {

    const result = {
      certificate: certificate,
      certificatePEM: encodePEM(certificateBuffer, "CERTIFICATE"),
      publicKey: publicKey,
      publicKeyPEM: encodePEM(publicKeyBuffer, "PUBLIC KEY"),
      privateKey: privateKey,
      privateKeyPEM: encodePEM(privateKeyBuffer, "PRIVATE KEY")
    };
    return result;
  });
}

function formatPEM(pemString) {
  const lineWidth = 64;
  let resultString = "";
  let start = 0;
  let piece;
  while ((piece = pemString.substring(start, start + lineWidth)).length > 0) {
    start += lineWidth;
    resultString += piece + '\r\n';
  }
  return resultString;
}

function encodePEM(buffer, label) {
  const bufferString = String.fromCharCode.apply(null, new Uint8Array(buffer));

  const header = `-----BEGIN ${label}-----\r\n`;
  const base64formatted = formatPEM(window.btoa(bufferString));
  const footer = `-----END ${label}-----\r\n`;
  const resultString = header + base64formatted + footer;

  return resultString;
}

function decodePEM(pemString) {
  const pemStripped = pemString.replace(/(-----(BEGIN|END) [a-zA-Z ]*-----|\r|\n)/g, '');
  const pemDecoded = window.atob(pemStripped);
  const buffer = pvutils.stringToArrayBuffer(pemDecoded);
  return buffer;
}

//*********************************************************************************
export function parseCertificate(certificatePEM) {
  const certificateBuffer = decodePEM(certificatePEM);
  const asn1 = asn1js.fromBER(certificateBuffer);
  const certificate = new pkijs.Certificate({ schema: asn1.result });
  return certificate;
}

//*********************************************************************************
export function encryptMessage(message, password, label) {
  const buffer = pvutils.stringToArrayBuffer(message);
  const secret = pvutils.stringToArrayBuffer(password);

  const enveloped = new pkijs.EnvelopedData();
  enveloped.addRecipientByPreDefinedData(secret, {}, AES_encryptionVariant_Password);
  return enveloped.encrypt(encryptionAlgorithm, buffer).
  then(
    () => {
      const content = new pkijs.ContentInfo();
      content.contentType = OID_PKCS7_EnvelopedData;
      content.content = enveloped.toSchema();
      const ber = content.toSchema().toBER(false);
      return encodePEM(ber, label)
    },
    error => Promise.reject(`encryption error: ${error}`)
  )
}

//*********************************************************************************
export function decryptMessage(message, password) {
  if (canTryPincode()) {
    const secret = pvutils.stringToArrayBuffer(password);
    const buffer = decodePEM(message);

    const asn1 = asn1js.fromBER(buffer);
    const content = new pkijs.ContentInfo({schema: asn1.result});
    const enveloped = new pkijs.EnvelopedData({schema: content.content});
    return enveloped.decrypt(0, {preDefinedData: secret}).then(result => {
      return pvutils.arrayBufferToString(result);
    }).catch(() => {
      return Promise.reject(failPincodeAttempt(password));
    });
  } else {
    return Promise.reject(getTimeLeftInLocalStorage());
  }
}

//*********************************************************************************
export function parsePrivateKey(privateKeyPEM) {
  const privateKeyBuffer = decodePEM(privateKeyPEM);
  const crypto = pkijs.getCrypto();
  const privateKeyPromise = crypto.importKey(
    "pkcs8",
    privateKeyBuffer,
    {   //these are the algorithm options
      name: "RSASSA-PKCS1-v1_5",
      hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
    },
    true,
    ["sign"]
  );
  return privateKeyPromise;
}


export function createPassportCertificate(commonNameArg) {
  const certData = {
    algorithms: {
      hashAlg: "SHA-256",
      signAlg: "RSASSA-PKCS1-v1_5",
      keyLength: 2048
    },
    //keyPair: generateKeys(), //optional , if provided must be object or promise that resolves to object {publicKey, prvateKey}. If it is not provided, new ones are generated automatically
    subject: {
      commonName: commonNameArg + "-userdevice", //optional for leaf, recommended for CA
      country: "CH", //optional for leaf, recommended for CA
      locality: "Zug", //optional for leaf, recommended for CA
      state: "Zug", //optional for leaf, recommended for CA
      organization: "Vereign AG", //optional for leaf, recommended for CA
      organizationUnit:"Business Dep", //optional for leaf, recommended for CA
      //email: "damyan.mitev@vereign.com", // added to DN and Subject Alternative Name extension. Optional for CA. Mandatory for leaf certificate, used for email protection
      //url: "www.vereign.com" // optional url, recommended for CA, added to Subject Alternative Name extension
    },
    validity: {
      //notBefore: new Date() // optional, defaults to today at 00:00:00
      //notAfter: new Date()  // optional, defaults to notBefore + validYears at 23:59:59
      validYears: 5 //optional, defaults to 1
    },
    isCA: true // optional flag denoting if this is CA certificate or leaf certificate, defaults to false
  };

  return createCertificate(certData, null)
}

export function createOneTimePassportCertificate(commonNameArg, emailArg, privateKeyIssuerArg, certicateIssuerArg) {
  var certData = null;
  if(emailArg != null && emailArg == "") {
      emailArg = null;
  certData = {
    algorithms: {
      hashAlg: "SHA-256",
      signAlg: "RSASSA-PKCS1-v1_5",
      keyLength: 2048
    },
    //keyPair: generateKeys(), //optional , if provided must be object or promise that resolves to object {publicKey, prvateKey}. If it is not provided, new ones are generated automatically
    subject: {
      commonName: commonNameArg + "-onetime", //optional for leaf, recommended for CA
      country: "CH", //optional for leaf, recommended for CA
      locality: "Zug", //optional for leaf, recommended for CA
      state: "Zug", //optional for leaf, recommended for CA
      organization: "Vereign AG", //optional for leaf, recommended for CA
      organizationUnit:"Business Dep", //optional for leaf, recommended for CA
      email: emailArg, // added to DN and Subject Alternative Name extension. Optional for CA. Mandatory for leaf certificate, used for email protection
      //url: "www.vereign.com" // optional url, recommended for CA, added to Subject Alternative Name extension
    },
    validity: {
      //notBefore: new Date() // optional, defaults to today at 00:00:00
      //notAfter: new Date()  // optional, defaults to notBefore + validYears at 23:59:59
      validYears: 5 //optional, defaults to 1
    },
    isCA: false // optional flag denoting if this is CA certificate or leaf certificate, defaults to false
  }


  return parsePrivateKey(privateKeyIssuerArg).then(privateKeyDecoded => {
    const issuerData = {
      certificate: parseCertificate(certicateIssuerArg),// vereignCACertPEM),
      privateKey: privateKeyDecoded
    };
    return createCertificate(certData, issuerData);
  });
}

function arrayBufferToBase64Formatted(buffer) {
  const bufferString = String.fromCharCode.apply(null, new Uint8Array(buffer));
  const base64formatted = formatPEM(window.btoa(bufferString));
  return base64formatted;
}

export function signEmail(mime, signingCert, certificateChain, privateKey) {
  const signingCertObj = parseCertificate(signingCert);
  const certificateChainObj = [];
  certificateChainObj[0] = parseCertificate(signingCert);
  for (let i = 0; i < certificateChain.length; i++) {
    certificateChainObj[i + 1] = parseCertificate(certificateChain[i])
  }

  return parsePrivateKey(privateKey).then(privateKeyDecoded => {
    return signEmailObjects(mime, signingCertObj, certificateChainObj, privateKeyDecoded);
  });
}

function signEmailObjects(mime, signingCert, certificateChain, privateKey) {

  //region Get a "crypto" extension
  const crypto = pkijs.getCrypto();
  if (typeof crypto === "undefined") {
    return Promise.reject("No WebCrypto extension found");
  }
  //endregion Get a "crypto" extension

  let template =
    `{{headers}}Content-Type: multipart/signed; protocol="application/pkcs7-signature"; micalg=sha-256; boundary="{{boundary}}"
MIME-Version: 1.0

This is a cryptographically signed message in MIME format.

--{{boundary}}
{{mime}}
--{{boundary}}
Content-Type: application/pkcs7-signature; name="smime.p7s"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="smime.p7s"
Content-Description: S/MIME Cryptographic Signature

{{signature}}
--{{boundary}}--

Vereign - Authentic Communication
`.replace(newline, '\r\n');

  const detachedSignature = true;
  const addExt = true;
  const hashAlg = "SHA-256";
  let cmsSignedSimpl;

  var mimeHeadersTitles = [
    "Content-Type",
    "Content-Transfer-Encoding",
    "Content-ID",
    "Content-Description",
    "Content-Disposition",
    "Content-Language",
    "Content-Location"
  ];

  mime = mime.replace(newline, '\r\n');

  let newHeaderLines = "";
  let headersEnd = mime.indexOf('\r\n\r\n'); //the first empty line

  if (headersEnd < 0 && mime.startsWith('\r\n')) {

    mime = mime.substring(2) //should not happen

  } else
  if (headersEnd >= 0) {

    let mimeHeaders = {};
    let mimeBody = mime.substring(headersEnd + 4);

    let mimeHeadersStr = mime.substring(0, headersEnd);

    let headers = libmime.decodeHeaders(mimeHeadersStr);
    for (var i = 0; i < mimeHeadersTitles.length; i++) {
      let key = mimeHeadersTitles[i].toLowerCase();
      if(key in headers) {
        mimeHeaders[key] = headers[key];
        delete headers[key]
      }
    }

    for(let key in headers) {
      if(!(key === "" || key === "MIME-Version".toLowerCase())) { //we have MIME-Version in the template
        newHeaderLines += capitalizeHeader(key) + ": " + headers[key] + '\r\n';
      }
    }

    let newMimeHeaderLines = "";
    for(let key in mimeHeaders) {
      if(!(key === "")) {
        newMimeHeaderLines += capitalizeHeader(key) + ": " + mimeHeaders[key] + '\r\n';
      }
    }

    if (newMimeHeaderLines === "") {
      newMimeHeaderLines = 'Content-Type: text/plain\r\n' //should not happen
    }

    mime = newMimeHeaderLines + '\r\n' + mimeBody
  }

  let dataBuffer = Buffer.from(mime,'utf-8');

  let sequence = Promise.resolve();

  //region Check if user wants us to include signed extensions
  if(addExt)
  {
    //region Create a message digest
    sequence = sequence.then(
      () => crypto.digest({ name: hashAlg }, dataBuffer)
    );
    //endregion

    //region Combine all signed extensions
    sequence = sequence.then(
      messageHash =>
      {
        const signedAttr = [];
        /*
                1.2.840.113549.1.9.1 - e-mailAddress
                1.2.840.113549.1.9.2 - PKCS-9 unstructuredName
                1.2.840.113549.1.9.3 - contentType
                1.2.840.113549.1.9.4 - messageDigest
                1.2.840.113549.1.9.5 - Signing Time
                1.2.840.113549.1.9.6 - counterSignature
        */
        signedAttr.push(new pkijs.Attribute({
          type: OID_PKCS9_ContentType, //contentType
          values: [
            new asn1js.ObjectIdentifier({ value: OID_PKCS7_Data}) //data
          ]
          /*
                      1.2.840.113549.1.7.1 - data
                      1.2.840.113549.1.7.2 - signedData
                      1.2.840.113549.1.7.3 - envelopedData
                      1.2.840.113549.1.7.4 - signedAndEnvelopedData
                      1.2.840.113549.1.7.5 - digestedData
                      1.2.840.113549.1.7.6 - encryptedData
          */
        })); // contentType

        signedAttr.push(new pkijs.Attribute({
          type: OID_PKCS9_SigningTime, //Signing Time
          values: [
            new asn1js.UTCTime({ valueDate: new Date() })
          ]
        })); // signingTime

        signedAttr.push(new pkijs.Attribute({
          type: OID_PKCS9_MessageDigest, //messageDigest
          values: [
            new asn1js.OctetString({ valueHex: messageHash })
          ]
        })); // messageDigest

        return signedAttr;
      }
    );
    //endregion
  }
  //endregion

  //region Initialize CMS Signed Data structures and sign it
  sequence = sequence.then(
    signedAttr =>
    {
      cmsSignedSimpl = new pkijs.SignedData({
        version: 1,
        encapContentInfo: new pkijs.EncapsulatedContentInfo({
          eContentType: OID_PKCS7_Data // "data" content type
        }),
        signerInfos: [
          new pkijs.SignerInfo({
            version: 1,
            sid: new pkijs.IssuerAndSerialNumber({
              issuer: signingCert.issuer,
              serialNumber: signingCert.serialNumber
            })
          })
        ],
        certificates: certificateChain //array
      });

      if(addExt)
      {
        cmsSignedSimpl.signerInfos[0].signedAttrs = new pkijs.SignedAndUnsignedAttributes({
          type: 0,
          attributes: signedAttr
        });
      }

      if(detachedSignature === false)
      {
        const contentInfo = new pkijs.EncapsulatedContentInfo({
          eContent: new asn1js.OctetString({ valueHex: dataBuffer })
        });

        cmsSignedSimpl.encapContentInfo.eContent = contentInfo.eContent;

        return cmsSignedSimpl.sign(privateKey, 0, hashAlg);
      }

      return cmsSignedSimpl.sign(privateKey, 0, hashAlg, dataBuffer);
    }
  );
  //endregion

  //region Create final result
  sequence = sequence.then(
    (result) =>
    {
      const cmsSignedSchema = cmsSignedSimpl.toSchema(true);

      const cmsContentSimp = new pkijs.ContentInfo({
        contentType: OID_PKCS7_SignedData, //signedData
        content: cmsSignedSchema
      });

      const _cmsSignedSchema = cmsContentSimp.toSchema();

      //region Make length of some elements in "indefinite form"
      _cmsSignedSchema.lenBlock.isIndefiniteForm = true;

      const block1 = _cmsSignedSchema.valueBlock.value[1];
      block1.lenBlock.isIndefiniteForm = true;

      const block2 = block1.valueBlock.value[0];
      block2.lenBlock.isIndefiniteForm = true;

      if(detachedSignature === false)
      {
        const block3 = block2.valueBlock.value[2];
        block3.lenBlock.isIndefiniteForm = true;
        block3.valueBlock.value[1].lenBlock.isIndefiniteForm = true;
        block3.valueBlock.value[1].valueBlock.value[0].lenBlock.isIndefiniteForm = true;
      }
      //endregion

      const cmsSignedBuffer = _cmsSignedSchema.toBER(false);
      return cmsSignedBuffer;
    },
    error => Promise.reject(`Erorr during signing of CMS Signed Data: ${error}`)
  );
  //endregion

  sequence = sequence.then(
    (cmsSignedBuffer) =>
    {
      let signature = arrayBufferToBase64Formatted(cmsSignedBuffer);
      let boundary = makeBoundary();

      template = template.replace(/{{boundary}}/g, boundary);
      template = template.replace("{{signature}}", signature);
      template = template.replace("{{headers}}", newHeaderLines);
      template = template.replace("{{mime}}", mime);

      //template = template.replace(newline, '\r\n')
      return template
    }
  );

  return sequence;
}


const newline = /\r\n|\r|\n/g;

function capitalizeFirstLetter(string) {
  if(string === "id") {
    return "ID"
  }

  if(string === "mime") {
    return "MIME";
  }

  return string.charAt(0).toUpperCase() + string.slice(1);
}

function capitalizeHeader(string) {
  let result = "";
  const tokens = string.split("-");
  for (let i = 0; i < tokens.length; i++) {
    result += capitalizeFirstLetter(tokens[i]);
    if (i !== tokens.length - 1) {
      result += "-";
    }
  }

  return result;
}



function makeBoundary() {
  let len = 20 + Math.random() * 20;
  return 'W0RyLiBEYW15YW4gTWl0ZXZd--' + makeid(len)
}

export const parseCertificates = signatureBase64 => {
  try {
    const certificateDecoded = atob(signatureBase64);
    const buffer = stringToArrayBuffer(certificateDecoded);
    const asn1 = fromBER(buffer);

    const contentInfo = new ContentInfo({ schema: asn1.result });
    const signedData = new SignedData({ schema: contentInfo.content });

    return signedData.certificates.map((certificate, index) => {
      const certificateData = { issuer: {}, subject: {}, validity: {} };
      const serialNumber = bufferToHexCodes(
        certificate.serialNumber.valueBlock.valueHex
      );
      const issuer = certificate.issuer.typesAndValues;
      const subject = certificate.subject.typesAndValues;
      const notAfter = certificate.notAfter.value;
      const notBefore = certificate.notBefore.value;
      let signatureAlgorithm =
        algomap[certificate.signatureAlgorithm.algorithmId];
      if (typeof signatureAlgorithm === "undefined") {
        signatureAlgorithm = certificate.signatureAlgorithm.algorithmId;
      } else {
        signatureAlgorithm = `${signatureAlgorithm}`;
      }

      for (const typeAndValue of issuer) {
        let typeVal = rdnmap[typeAndValue.type];
        if (typeof typeVal === "undefined") {
          typeVal = typeAndValue.type;
        }
        const subjVal = typeAndValue.value.valueBlock.value;
        certificateData.issuer[typeVal] = subjVal;
      }

      for (const typeAndValue of subject) {
        let typeVal = rdnmap[typeAndValue.type];
        if (typeof typeVal === "undefined") {
          typeVal = typeAndValue.type;
        }
        const subjVal = typeAndValue.value.valueBlock.value;
        certificateData.subject[typeVal] = subjVal;
      }

      certificateData.signatureAlgorithm = signatureAlgorithm;
      certificateData.serialNumber = serialNumber;
      certificateData.validity = {
        notAfter,
        notBefore
      };

      return certificateData;
    });
  } catch (e) {
    console.error("Error parsing certificate", e);
  }
};

export const getCertificateChain = signatureBase64 => {
  const certificateChain = [];

  try {
    const certificates = parseCertificates(signatureBase64);

    // Add first certificate in the chain
    certificateChain.push(certificates[0]);

    // Go through all certificates to build a chain from first certificate to the root
    certificates.forEach(certificate => {
      if (certificateChain[0].issuer.commonName === certificate.subject.commonName) {
        certificateChain.unshift(certificate);
      }
    });
  } catch (e) {
    console.warn("Error getting certificate data", e);
  }

  return certificateChain;
};