Skip to content
Snippets Groups Projects
signingUtilities.js 46.2 KiB
Newer Older
  • Learn to ignore specific revisions
  •   //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
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      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
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    `.replace(newline, "\r\n");
    
    
      const detachedSignature = true;
      const addExt = true;
      const hashAlg = "SHA-256";
      let cmsSignedSimpl;
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      let mimeHeadersTitles = [
    
        "Content-Type",
        "Content-Transfer-Encoding",
        "Content-ID",
        "Content-Description",
        "Content-Disposition",
        "Content-Language",
        "Content-Location"
      ];
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      mime = mime.replace(newline, "\r\n");
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      let headersEnd = mime.indexOf("\r\n\r\n"); //the first empty line
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      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);
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        for (let i = 0; i < mimeHeadersTitles.length; i++) {
    
          let key = mimeHeadersTitles[i].toLowerCase();
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          if (key in headers) {
    
            mimeHeaders[key] = headers[key];
    
    Alexey Lunin's avatar
    Alexey Lunin committed
            delete headers[key];
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        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";
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        for (let key in mimeHeaders) {
          if (!(key === "")) {
            newMimeHeaderLines +=
              capitalizeHeader(key) + ": " + mimeHeaders[key] + "\r\n";
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          newMimeHeaderLines = "Content-Type: text/plain\r\n"; //should not happen
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        mime = newMimeHeaderLines + "\r\n" + mimeBody;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      let dataBuffer = Buffer.from(mime, "utf-8");
    
    
      let sequence = Promise.resolve();
    
      //region Check if user wants us to include signed extensions
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      if (addExt) {
    
        //region Create a message digest
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        sequence = sequence.then(() =>
          crypto.digest({ name: hashAlg }, dataBuffer)
    
        );
        //endregion
    
        //region Combine all signed extensions
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        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
            */
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          signedAttr.push(
            new pkijs.Attribute({
    
              type: OID_PKCS9_ContentType, //contentType
              values: [
    
    Alexey Lunin's avatar
    Alexey Lunin committed
                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
              */
    
    Alexey Lunin's avatar
    Alexey Lunin committed
            })
          ); // contentType
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          signedAttr.push(
            new pkijs.Attribute({
    
              type: OID_PKCS9_SigningTime, //Signing Time
    
    Alexey Lunin's avatar
    Alexey Lunin committed
              values: [new asn1js.UTCTime({ valueDate: new Date() })]
            })
          ); // signingTime
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          signedAttr.push(
            new pkijs.Attribute({
    
              type: OID_PKCS9_MessageDigest, //messageDigest
    
    Alexey Lunin's avatar
    Alexey Lunin committed
              values: [new asn1js.OctetString({ valueHex: messageHash })]
            })
          ); // messageDigest
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          return signedAttr;
        });
    
        //endregion
      }
      //endregion
    
      //region Initialize CMS Signed Data structures and sign it
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      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
    
    Alexey Lunin's avatar
    Alexey Lunin committed
            })
          ],
          certificates: certificateChain //array
        });
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        if (addExt) {
          cmsSignedSimpl.signerInfos[0].signedAttrs = new pkijs.SignedAndUnsignedAttributes(
            {
    
              type: 0,
              attributes: signedAttr
    
    Alexey Lunin's avatar
    Alexey Lunin committed
            }
          );
        }
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        if (detachedSignature === false) {
          const contentInfo = new pkijs.EncapsulatedContentInfo({
            eContent: new asn1js.OctetString({ valueHex: dataBuffer })
          });
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          cmsSignedSimpl.encapContentInfo.eContent = contentInfo.eContent;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          return cmsSignedSimpl.sign(privateKey, 0, hashAlg);
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    
        return cmsSignedSimpl.sign(privateKey, 0, hashAlg, dataBuffer);
      });
    
      //endregion
    
      //region Create final result
      sequence = sequence.then(
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        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;
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          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
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      sequence = sequence.then(cmsSignedBuffer => {
        let signature = arrayBufferToBase64Formatted(cmsSignedBuffer);
        let boundary = makeBoundary();
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        template = template.replace(/{{boundary}}/g, boundary);
        template = template.replace("{{signature}}", signature);
        template = template.replace("{{headers}}", newHeaderLines);
        template = template.replace("{{mime}}", mime);
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        //template = template.replace(newline, '\r\n')
        return template;
      });
    
    
      return sequence;
    }
    
    const newline = /\r\n|\r|\n/g;
    
    function capitalizeFirstLetter(string) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      if (string === "id") {
        return "ID";
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      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;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      return "W0RyLiBEYW15YW4gTWl0ZXZd--" + makeid(len);
    
    Damyan Mitev's avatar
    Damyan Mitev committed
    export const parseSignedData = 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 });
    
    Damyan Mitev's avatar
    Damyan Mitev committed
        return signedData;
      } catch (e) {
    
        console.error("Error parsing signed data:", e);
        return null;
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      }
    };
    
    export const parseCertificates = signedData => {
      try {
    
        return signedData.certificates.map((certificate) => {
          const certificateData = new CertificateData(certificate);
    
          return certificateData;
        });
      } catch (e) {
        console.error("Error parsing certificate", e);
      }
    };
    
    
    Damyan Mitev's avatar
    Damyan Mitev committed
    export const getCertificateChain = signedData => {
    
      const certificateChain = [];
    
      try {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
        const certificates = parseCertificates(signedData);
    
    
        // 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 => {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          if (
            certificateChain[0].issuer.commonName === certificate.subject.commonName
          ) {
    
            certificateChain.unshift(certificate);
          }
        });
      } catch (e) {
        console.warn("Error getting certificate data", e);
      }
    
      return certificateChain;
    };
    
    const isVereignSignature = (signerInfo, signerVerificationResult) => {
      const signerCert = signerVerificationResult.signerCertificate;
    
      for (const typeAndValue of signerCert.subject.typesAndValues) {
        try {
          if (typeAndValue.type === "2.5.4.10" &&
              typeAndValue.value.valueBlock.value === "Vereign AG"
          ) {
            return true;
          }
        } catch (ignore) {}
      }
    
      return false;
    };
    
    
    Damyan Mitev's avatar
    Damyan Mitev committed
    export const verifySMIME = (smimeString, rootCaPem) => {
      return new Promise(resolve => {
        setTimeout(async () => {
          const emailString = fixNewLines(smimeString);
          const parts = parseMIME(emailString);
    
          let signatureBase64;
          let signatureBoundary;
    
          for (const part of parts) {
            let contentType = getHeaderValue("content-type", part);
    
            if (!contentType) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
              continue;
            }
            contentType = contentType[0];
    
    
            if (contentType && contentType.startsWith(SIGNATURE_CONTENT_TYPE)) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
              signatureBase64 = getAttachment(emailString, part).base64;
              signatureBoundary = part.boundary;
              break;
            }
          }
    
    
          const verificationResult = {
            verified: false,
            message: "",
            vereignSignatures: 0,
            nonVereignSignatures: 0
          };
    
          if (!signatureBase64) {
            verificationResult.message = "Not a signed MIME";
            resolve(verificationResult);
            return;
          }
    
          const dataPart = parts[0];
          if (dataPart.boundary !== signatureBoundary) {
            verificationResult.message = "Invalid SMIME format: wrong boundary on first MIME part";
            resolve(verificationResult);
            return;
          }
    
          const data = emailString.slice(
            dataPart.indices.from,
            dataPart.indices.to
          );
          const dataBuffer = stringToArrayBuffer(data);
    
          const rootCa = parseCertificate(rootCaPem);
          if (rootCa.tbs.byteLength === 0) {
            rootCa.tbs = rootCa.encodeTBS();
          }
    
          const signedData = parseSignedData(signatureBase64);
          if (!signedData) {
            verificationResult.message = "Corrupt SMIME signature";
            resolve(verificationResult);
            return;
          }
    
          for (let i = 0; i < signedData.signerInfos.length; i++) {
            let signerResult;
            try {
              signerResult = await signedData.verify({
    
    Damyan Mitev's avatar
    Damyan Mitev committed
                signer: i,
                data: dataBuffer,
                trustedCerts: [rootCa],
                checkDate: new Date(),
                checkChain: true,
                extendedMode: true,
                passedWhenNotRevValues: false
              });
    
            } catch (e) {
              verificationResult.message = e.message;
              resolve(verificationResult);
              return;
            }
    
            const signerVerified = !!signerResult.signatureVerified && !!signerResult.signerCertificateVerified;
    
            if (!signerVerified) {
              if (signerResult.message) {
                verificationResult.message = signerResult.message;
              } else {
                verificationResult.message = "Message integrity is compromised";
              }
              resolve(verificationResult);
              return;
            }
    
            if (isVereignSignature(signedData.signerInfos[i], signerResult)) {
              const signerPath = signerResult.certificatePath;
              const signerRoot = signerPath[signerPath.length - 1];
              if (signerRoot.tbs.byteLength === 0) {
                signerRoot.tbs = signerRoot.encodeTBS();
              }
              if (!isEqualBuffer(signerRoot.tbs, rootCa.tbs)) {
                verificationResult.message =
                  `Vereign signature ${i} has root certificate, different from Vereign root CA`;
    
    Damyan Mitev's avatar
    Damyan Mitev committed
                resolve(verificationResult);
                return;
              }
    
              verificationResult.vereignSignatures++;
            } else {
              verificationResult.nonVereignSignatures++;
    
          if (signedData.signerInfos.length === 0) {
            verificationResult.message = "No signatures found";
          } else
          if (verificationResult.vereignSignatures === 0) {
            verificationResult.message = "Verified succesfully, but no Vereign signatures found";
          } else {
            verificationResult.message = "Verified succesfully";
          }
          verificationResult.verified = true;
    
    Damyan Mitev's avatar
    Damyan Mitev committed
          resolve(verificationResult);
        }, 50);
      });
    };
    
    Damyan Mitev's avatar
    Damyan Mitev committed
    export class ImageData {
    
      /**
       * Constructor for ImageData class
       * @param {Object} [parameters] Object in format
       * {
       *   contentType: String,
       *   content: String -- base64 encoded
       * }
       */
      constructor(parameters = {}) {
        this.contentType = null; //string: the content type
        this.content = null; // Uint8Array: decoded content
        this.contentBase64 = null; // string: base64 encoded content
    
        if (typeof parameters === "object") {
          this.fromParameters(parameters);
        }
      }
    
      fromParameters(parameters) {
        if ("contentType" in parameters) {
          this.contentType = parameters.contentType;
        }
    
        if ("content" in parameters) {
          this.content = parameters.content;
        }
    
        if ("contentBase64" in parameters) {
          this.contentBase64 = parameters.contentBase64;
        }
    
        this.getContent();
        this.getContentBase64();
      }
    
      //fromDataURL()
      //fromContentTypeAndContentAsByteArray()
    
      toDataURL() {
        return "data:" + this.contentType + ";base64," + this.getContentBase64();
      }
    
      getContent() {
        if (!this.content) {
          if (this.contentBase64) {
            this.content = base64ToByteArray(this.contentBase64);
          }
        }
        return this.content;
      }
    
      getContentBase64() {
        if (!this.contentBase64) {
          if (this.content) {
            this.contentBase64 = byteArrayToBase64(this.content);
          }
        }
        return this.contentBase64;
      }
    
      toJSON() {
        return {
          "contentType": this.contentType,
          "contentBase64": this.getContentBase64()
        };
      }
    }
    
    
    //Initialization block
    fixPkijsRDN();