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