diff --git a/javascript/src/helpers/mailparser.js b/javascript/src/helpers/mailparser.js index 9bd873ff9ba1d20c06bd890a4bd525e640c378ec..a6da68a5fb223932afe77acc67fc3d7149711e4e 100644 --- a/javascript/src/helpers/mailparser.js +++ b/javascript/src/helpers/mailparser.js @@ -61,7 +61,7 @@ function calculateParts(body, from, to, previousBondary) { let boundary = findFirstBoundary(body, from, to); if (boundary == null) { - return [{ indices: { from: from, to: to }, boundary: previousBondary }]; + return [{ indices: { from: from, to: to }, boundary: previousBondary, leaf: true }]; } const realBoundary = boundary; @@ -78,6 +78,9 @@ function calculateParts(body, from, to, previousBondary) { } let bodies = []; + if (previousBondary !== null) { + bodies.push({indices: {from: from, to: to}, boundary: previousBondary, leaf: false}); + } for (let i = 0; i < boundaryIndicesLength - 1; i++) { const firstPair = boundaryPairs[i]; @@ -155,13 +158,14 @@ export function parseMIME(mime) { parts.push({ indices: { from: 0, to: mime.length, headersEnd: headersEnd }, headers, - boundary: "mimemessage" + boundary: "mimemessage", + leaf: false }); return parts; } -function getHeaderValue(header, part) { +export function getHeaderValue(header, part) { if (part.headers && part.headers[header] && part.headers[header].length) { return part.headers[header]; } diff --git a/javascript/src/iframe/viamapi-iframe.js b/javascript/src/iframe/viamapi-iframe.js index efd6fe262831e165c52e6723ba09232701b48b6a..428d5ea7b0312c7ef7c918c3e3908e6cfebf01d3 100644 --- a/javascript/src/iframe/viamapi-iframe.js +++ b/javascript/src/iframe/viamapi-iframe.js @@ -24,7 +24,8 @@ import { createPassportCertificate, decryptMessage, encryptMessage, - signEmail + signEmail, + verifySMIME } from "../utilities/signingUtilities"; import { signPdf } from "../utilities/pdfUtilities"; import CryptoData from "../CryptoData"; @@ -1051,6 +1052,37 @@ const connection = Penpal.connectToParent({ ); }); }, + verifySMIME: async (smimeString) => { + const authenticationPublicKey = localStorage.getItem( + "authenticatedIdentity" + ); + + if ( + !authenticationPublicKey || + !window.loadedIdentities[authenticationPublicKey] || + !extendPinCodeTtl(authenticationPublicKey) + ) { + return encodeResponse("400", "", "Identity not authenticated"); + } + + //TODO cache (for some time) the root certificate + // either as PEM or as certificate object (preferred) + const rootCaResponse = await executeRestfulFunction( + "private", + window.viamApi, + window.viamApi.signRetrieveRootCertificate, + null + ); + + if (rootCaResponse.code !== "200") { + return encodeResponse("400", "", rootCaResponse.status); + } + + const rootCaPem = rootCaResponse.data; + const verificationResult = await verifySMIME(smimeString, rootCaPem); + + return encodeResponse("200", verificationResult.verified, verificationResult.message); + }, signEmail: async (passportUUID, emailArg, emailMessage) => { const authenticationPublicKey = localStorage.getItem( "authenticatedIdentity" diff --git a/javascript/src/utilities/emailUtilities.js b/javascript/src/utilities/emailUtilities.js index df59737b20636ffca4b5b5431c024be65ff6668f..70ebc48aa9aab0afb438de858f4d017d6fa5d9d7 100644 --- a/javascript/src/utilities/emailUtilities.js +++ b/javascript/src/utilities/emailUtilities.js @@ -11,9 +11,12 @@ import { getAttachment, getGlobalHeaderValue } from "../helpers/mailparser"; -import { getCertificateChain } from "./signingUtilities"; +import { + getCertificateChain, + parseSignedData +} from "./signingUtilities"; -const SIGNATURE_CONTENT_TYPE = "application/pkcs7-signature"; +export const SIGNATURE_CONTENT_TYPE = "application/pkcs7-signature"; export const DEFAULT_ATTACHMENT_NAME = "attachment"; const splitParticipants = participantsList => { @@ -44,7 +47,7 @@ export const parseSMIME = smimeString => { rawAttachment ); - if (contentType.indexOf(SIGNATURE_CONTENT_TYPE) !== -1) { + if (contentType.startsWith(SIGNATURE_CONTENT_TYPE)) { signatureBase64 = base64; } @@ -58,7 +61,14 @@ export const parseSMIME = smimeString => { }); } - const certificateChain = getCertificateChain(signatureBase64); + let certificateChain; + if (signatureBase64) { + const signedData = parseSignedData(signatureBase64); + if (signedData) { + //TODO revise and use signedData's generation of cert chain (per signer) + certificateChain = getCertificateChain(signedData); + } + } const from = splitParticipants(getGlobalHeaderValue("from", parts)); const to = splitParticipants(getGlobalHeaderValue("to", parts)); diff --git a/javascript/src/utilities/signingUtilities.js b/javascript/src/utilities/signingUtilities.js index 27d23e4c4484f13701f2058cc1d4c974d0c507b2..db47ed62ed7c1c3fd455405cd95092a84a7129ff 100644 --- a/javascript/src/utilities/signingUtilities.js +++ b/javascript/src/utilities/signingUtilities.js @@ -4,10 +4,28 @@ import { getTimeLeftInLocalStorage, makeid } from "./appUtility"; -import { bufferToHexCodes, stringToArrayBuffer } from "pvutils"; +import { + bufferToHexCodes, + stringToArrayBuffer, + isEqualBuffer +} from "pvutils"; import { fromBER } from "asn1js"; import { ContentInfo, SignedData } from "pkijs"; import { algomap, rdnmap } from "../constants/certificates"; +import { + fixNewLines, + getAttachment, + getAttachments, + getGlobalHeaderValue, + getHeaderValue, + parseMIME +} from "../helpers/mailparser"; +import dataUriToBlob from "data-uri-to-blob"; +import { + extractHtmlBodyFromString, + getFilenameFromHeaders, + SIGNATURE_CONTENT_TYPE +} from "./emailUtilities"; const libmime = require("libmime"); const pkijs = require("pkijs"); @@ -1006,7 +1024,7 @@ function makeBoundary() { return "W0RyLiBEYW15YW4gTWl0ZXZd--" + makeid(len); } -export const parseCertificates = signatureBase64 => { +export const parseSignedData = signatureBase64 => { try { const certificateDecoded = atob(signatureBase64); const buffer = stringToArrayBuffer(certificateDecoded); @@ -1014,7 +1032,15 @@ export const parseCertificates = signatureBase64 => { const contentInfo = new ContentInfo({ schema: asn1.result }); const signedData = new SignedData({ schema: contentInfo.content }); + return signedData; + } catch (e) { + console.error("Error parsing signed data:", e); + return null; + } +}; +export const parseCertificates = signedData => { + try { return signedData.certificates.map((certificate, index) => { const certificateData = { issuer: {}, subject: {}, validity: {} }; const serialNumber = bufferToHexCodes( @@ -1064,11 +1090,11 @@ export const parseCertificates = signatureBase64 => { } }; -export const getCertificateChain = signatureBase64 => { +export const getCertificateChain = signedData => { const certificateChain = []; try { - const certificates = parseCertificates(signatureBase64); + const certificates = parseCertificates(signedData); // Add first certificate in the chain certificateChain.push(certificates[0]); @@ -1087,3 +1113,142 @@ export const getCertificateChain = signatureBase64 => { 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; +}; + +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) { + continue; + } + contentType = contentType[0]; + + if (contentType && contentType.startsWith(SIGNATURE_CONTENT_TYPE)) { + 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({ + 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`; + 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; + resolve(verificationResult); + }, 50); + }); +};