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; };