Skip to content
Snippets Groups Projects
Commit 5ae2a8e2 authored by Zdravko Iliev's avatar Zdravko Iliev
Browse files

handling not signed pdfs, error handling

parent e9e461f7
No related branches found
No related tags found
1 merge request!1Draft: Resolve "[Document Sealing] Implement PDF parser"
Pipeline #49367 failed
Showing
with 446 additions and 44 deletions
......@@ -2,9 +2,10 @@ import fs from "fs";
import path from "path";
import { describe, it, expect } from "@jest/globals";
import PDFparser from "../src/pdfParser";
import { AppError } from "../src/lib/errors";
describe("PDF parser", () => {
it("should return pdf document metadata", async () => {
it("should return pdf document metadata including signatures", async () => {
const file = fs.readFileSync(
path.resolve(__dirname, "./abacus-two-signatures.pdf")
);
......@@ -12,8 +13,26 @@ describe("PDF parser", () => {
const parser = new PDFparser(file);
const actual = await parser.getPDFMeta();
console.log(actual);
expect(actual.pages).toEqual(2);
});
it("should return pdf document metadata without signatures", async () => {
const file = fs.readFileSync(path.resolve(__dirname, "./example.pdf"));
const parser = new PDFparser(file);
const actual = await parser.getPDFMeta();
expect(actual.pages).toEqual(1);
expect(actual.title).toEqual("PDF Digital Signatures");
expect(actual.author).toEqual("Tecxoft");
});
it("should throw error without file", async () => {
try {
const parser = new PDFparser(null);
const actual = await parser.getPDFMeta();
} catch (error) {
expect(error).toBeInstanceOf(AppError);
}
});
});
......@@ -12,6 +12,6 @@ exports.config = {
outline: true,
metadata: true,
info: true,
permissions: true,
permissions: true, // get permissions
},
};
export declare const extractCertificatesDetails: (certs: any) => any;
export declare const sortCertificateChain: (certs: any) => unknown[];
export declare const getClientCertificate: (certs: any) => unknown;
export declare const isCertsExpired: (certs: any) => boolean;
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isCertsExpired = exports.getClientCertificate = exports.sortCertificateChain = exports.extractCertificatesDetails = void 0;
const forge = __importStar(require("@vereign/node-forge"));
const issued = (cert) => (anotherCert) => cert !== anotherCert && anotherCert.issued(cert);
const getIssuer = (certsArray) => (cert) => certsArray.find(issued(cert));
const inverse = (f) => (x) => !f(x);
const hasNoIssuer = (certsArray) => inverse(getIssuer(certsArray));
const getChainRootCertificateIdx = (certsArray) => certsArray.findIndex(hasNoIssuer(certsArray));
const isIssuedBy = (cert) => (anotherCert) => cert !== anotherCert && cert.issued(anotherCert);
const getChildIdx = (certsArray) => (parent) => certsArray.findIndex(isIssuedBy(parent));
const extractCertificatesDetails = (certs) => certs.map(extractSingleCertificateDetails).map((cert, i) => {
if (i)
return cert;
return Object.assign({ clientCertificate: true }, cert);
});
exports.extractCertificatesDetails = extractCertificatesDetails;
const mapEntityAtrributes = (attrs) => attrs.reduce((agg, { name, value }) => {
if (!name)
return agg;
agg[name] = value;
return agg;
}, {});
const extractSingleCertificateDetails = (cert) => {
const { issuer, subject, validity } = cert;
return {
issuedBy: mapEntityAtrributes(issuer.attributes),
issuedTo: mapEntityAtrributes(subject.attributes),
validityPeriod: validity,
pemCertificate: forge.pki.certificateToPem(cert),
};
};
const sortCertificateChain = (certs) => {
const certsArray = Array.from(certs);
const rootCertIndex = getChainRootCertificateIdx(certsArray);
const certificateChain = certsArray.splice(rootCertIndex, 1);
while (certsArray.length) {
const lastCert = certificateChain[0];
const childCertIdx = getChildIdx(certsArray)(lastCert);
if (childCertIdx === -1)
certsArray.splice(childCertIdx, 1);
else {
const [childCert] = certsArray.splice(childCertIdx, 1);
certificateChain.unshift(childCert);
}
}
return certificateChain;
};
exports.sortCertificateChain = sortCertificateChain;
const getClientCertificate = (certs) => (0, exports.sortCertificateChain)(certs)[0];
exports.getClientCertificate = getClientCertificate;
const isCertsExpired = (certs) => !!certs.find(({ validity: { notAfter, notBefore } }) => notAfter.getTime() < Date.now() || notBefore.getTime() > Date.now());
exports.isCertsExpired = isCertsExpired;
declare class GeneralError extends Error {
constructor(message: string);
}
declare class AppError extends GeneralError {
}
export { GeneralError, AppError };
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppError = exports.GeneralError = void 0;
class GeneralError extends Error {
constructor(message) {
super();
this.message = message;
}
}
exports.GeneralError = GeneralError;
class AppError extends GeneralError {
}
exports.AppError = AppError;
/// <reference types="node" />
export declare const checkForSubFilter: (pdfBuffer: Buffer) => void;
export declare const getByteRange: (pdfBuffer: Buffer) => {
byteRangePlaceholder: string;
byteRanges: number[][];
};
export declare const preparePDF: (pdf: any) => Buffer;
export declare const getMetaRegexMatch: (keyName: string) => (str: any) => any;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getMetaRegexMatch = exports.preparePDF = exports.getByteRange = exports.checkForSubFilter = void 0;
const errors_1 = require("./errors");
const DEFAULT_BYTE_RANGE_PLACEHOLDER = "**********";
const checkForSubFilter = (pdfBuffer) => {
const matches = pdfBuffer.toString().match(/\/SubFilter\s*\/([\w.]*)/);
const subFilter = Array.isArray(matches) && matches[1];
if (!subFilter) {
throw new errors_1.AppError("Can not find subfilter");
}
const supportedTypes = [
"adbe.pkcs7.detached",
"etsi.cades.detached",
"etsi.rfc3161",
];
if (!supportedTypes.includes(subFilter.trim().toLowerCase())) {
throw new errors_1.AppError("Subfilter not supported");
}
};
exports.checkForSubFilter = checkForSubFilter;
const getByteRange = (pdfBuffer) => {
const byteRangeStrings = pdfBuffer
.toString()
.match(/\/ByteRange\s*\[{1}\s*(?:(?:\d*|\/\*{10})\s+){3}(?:\d+|\/\*{10}){1}\s*\]{1}/g);
const byteRangePlaceholder = byteRangeStrings.find((s) => s.includes(`/${DEFAULT_BYTE_RANGE_PLACEHOLDER}`));
const strByteRanges = byteRangeStrings.map((brs) => brs.match(/[^[\s]*(?:\d|\/\*{10})/g));
const byteRanges = strByteRanges.map((n) => n.map(Number));
return {
byteRangePlaceholder,
byteRanges,
};
};
exports.getByteRange = getByteRange;
const preparePDF = (pdf) => {
try {
if (Buffer.isBuffer(pdf))
return pdf;
return Buffer.from(pdf);
}
catch (error) {
throw new errors_1.AppError(error);
}
};
exports.preparePDF = preparePDF;
const getMetaRegexMatch = (keyName) => (str) => {
const regex = new RegExp(`/${keyName}\\s*\\(([\\w.\\s@+~_\\-://,]*)`, "g");
const matches = [...str.matchAll(regex)];
const meta = matches.length ? matches[matches.length - 1][1] : null;
return meta;
};
exports.getMetaRegexMatch = getMetaRegexMatch;
/// <reference types="node" />
export declare const verifyPDF: (pdf: Buffer) => {
expired: any;
signatures: any;
verified?: undefined;
message?: undefined;
error?: undefined;
} | {
verified: boolean;
message: any;
error: any;
expired?: undefined;
signatures?: undefined;
};
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.verifyPDF = void 0;
const generalUtils_1 = require("./generalUtils");
const signatureUtils_1 = require("./signatureUtils");
const verify_1 = require("./verify");
const verifyPDF = (pdf) => {
const pdfBuffer = (0, generalUtils_1.preparePDF)(pdf);
(0, generalUtils_1.checkForSubFilter)(pdfBuffer);
try {
const { signatureStr, signedData, signatureMeta } = (0, signatureUtils_1.extractSignature)(pdfBuffer);
const signatures = signedData.map((_signed, index) => {
return (0, verify_1.verify)(signatureStr[index], signatureMeta[index]);
});
return {
// authenticity: signatures.every((o) => o.authenticity === true),
expired: signatures.some((o) => o.expired === true),
signatures,
};
}
catch (error) {
return { verified: false, message: error.message, error };
}
};
exports.verifyPDF = verifyPDF;
export declare const extractSignature: (pdf: any) => {
byteRanges: number[][];
signatureStr: any[];
signedData: any[];
signatureMeta: {
reason: any;
contactInfo: any;
location: any;
signDate: string;
}[];
};
export declare const getMessageFromSignature: (signature: any) => any;
export declare const authenticateSignature: (certs: any) => boolean;
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.authenticateSignature = exports.getMessageFromSignature = exports.extractSignature = void 0;
const forge = __importStar(require("@vereign/node-forge"));
const errors_1 = require("./errors");
const generalUtils_1 = require("./generalUtils");
const timeUtils_1 = require("./timeUtils");
const rootCAs = require("./rootCAs");
const getSignatureMeta = (signedData) => {
const str = Buffer.isBuffer(signedData) ? signedData.toString() : signedData;
const formattedSignDate = (0, timeUtils_1.formatPdfTime)((0, generalUtils_1.getMetaRegexMatch)("M")(str));
return {
reason: (0, generalUtils_1.getMetaRegexMatch)("Reason")(str),
contactInfo: (0, generalUtils_1.getMetaRegexMatch)("ContactInfo")(str),
location: (0, generalUtils_1.getMetaRegexMatch)("Location")(str),
signDate: formattedSignDate,
};
};
const verifyCaBundle = (certs) => !!certs.find((cert, i) => certs[i + 1] && certs[i + 1].issued(cert));
const getRootCAs = () => rootCAs;
const verifyRootCert = (chainRootInForgeFormat) => !!getRootCAs().find((rootCAInPem) => {
try {
const rootCAInForgeCert = forge.pki.certificateFromPem(rootCAInPem);
return (forge.pki.certificateToPem(chainRootInForgeFormat) === rootCAInPem ||
rootCAInForgeCert.issued(chainRootInForgeFormat));
}
catch (e) {
return false;
}
});
const extractSignature = (pdf) => {
const pdfBuffer = (0, generalUtils_1.preparePDF)(pdf);
const { byteRanges } = (0, generalUtils_1.getByteRange)(pdfBuffer);
const signatureStr = [];
const signedData = [];
byteRanges.forEach((byteRange) => {
signedData.push(Buffer.concat([
pdfBuffer.slice(byteRange[0], byteRange[0] + byteRange[1]),
pdfBuffer.slice(byteRange[2], byteRange[2] + byteRange[3]),
]));
const signatureHex = pdfBuffer
.slice(byteRange[0] + byteRange[1] + 1, byteRange[2])
.toString("latin1");
signatureStr.push(Buffer.from(signatureHex, "hex").toString("latin1"));
});
const signatureMeta = signedData.map((sd) => getSignatureMeta(sd));
return {
byteRanges,
signatureStr,
signedData,
signatureMeta,
};
};
exports.extractSignature = extractSignature;
const getMessageFromSignature = (signature) => {
try {
const p7Asn1 = forge.asn1.fromDer(signature);
return forge.pkcs7.messageFromAsn1(p7Asn1);
}
catch (error) {
//no signature is found return empty object
if (error.message === "Too few bytes to parse DER.") {
return {};
}
throw new errors_1.AppError(error);
}
};
exports.getMessageFromSignature = getMessageFromSignature;
const authenticateSignature = (certs) => verifyCaBundle(certs) && verifyRootCert(certs[certs.length - 1]);
exports.authenticateSignature = authenticateSignature;
export declare const formatPdfTime: (datetimeString: string) => string;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.formatPdfTime = void 0;
const formatPdfTime = (datetimeString) => {
if (!datetimeString)
return;
const result = datetimeString.split("D:");
const timestamp = result[1].split("Z");
const year = timestamp[0].substring(0, 4);
const month = timestamp[0].substring(4, 6);
const day = timestamp[0].substring(6, 8);
const hours = timestamp[0].substring(8, 10);
const min = timestamp[0].substring(10, 12);
const seconds = timestamp[0].substring(12, 14);
return `${year}.${month}.${day} ${hours}:${min}:${seconds}`;
};
exports.formatPdfTime = formatPdfTime;
export declare const verify: (signature: any, signatureMeta: any) => {
isExpired: boolean;
meta: any;
};
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.verify = void 0;
const forge = __importStar(require("@vereign/node-forge"));
const certUtils_1 = require("./certUtils");
const errors_1 = require("./errors");
const signatureUtils_1 = require("./signatureUtils");
const verify = (signature, signatureMeta) => {
const message = (0, signatureUtils_1.getMessageFromSignature)(signature);
const { certificates, rawCapture: { signature: sig, authenticatedAttributes: attrs, digestAlgorithm, }, } = message;
const hashAlgorithmOid = forge.asn1.derToOid(digestAlgorithm);
const hashAlgorithm = forge.pki.oids[hashAlgorithmOid].toLowerCase();
const set = forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SET, true, attrs);
const clientCertificate = (0, certUtils_1.getClientCertificate)(certificates);
const digest = forge.md[hashAlgorithm]
.create()
.update(forge.asn1.toDer(set).data)
.digest()
.getBytes();
const validAuthenticatedAttributes = clientCertificate["publicKey"].verify(digest, sig);
if (!validAuthenticatedAttributes) {
throw new errors_1.AppError("Wrong authenticated attributes");
}
// WIP: fix integrity check
// const messageDigestAttr = forge.pki.oids.messageDigest;
// const fullAttrDigest = attrs.find(
// (attr) => forge.asn1.derToOid(attr.value[0].value) === messageDigestAttr
// );
// const attrDigest = fullAttrDigest.value[1].value[0].value;
// const dataDigest = forge.md[hashAlgorithm]
// .create()
// .update(signedData.toString("latin1"))
// .digest()
// .getBytes();
// const integrity = dataDigest === attrDigest;
const sortedCerts = (0, certUtils_1.sortCertificateChain)(certificates);
const parsedCerts = (0, certUtils_1.extractCertificatesDetails)(sortedCerts);
//WIP: fix authenticity check after you have the root cert
// const authenticity = authenticateSignature(sortedCerts);
const isExpired = (0, certUtils_1.isCertsExpired)(sortedCerts);
return {
// verified: integrity && authenticity && !expired,
// authenticity,
// integrity,
isExpired,
meta: Object.assign({ certs: parsedCerts }, signatureMeta),
};
};
exports.verify = verify;
......@@ -8,46 +8,41 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const verify_pdf_1 = __importDefault(require("@ninja-labs/verify-pdf"));
const pdf_lib_1 = require("pdf-lib");
const pdfdataextract_1 = require("pdfdataextract");
const config_1 = require("./config");
const { PDFDocument } = require("pdf-lib");
const lib_1 = require("./lib");
const timeUtils_1 = require("./lib/timeUtils");
const errors_1 = require("./lib/errors");
class PDFparser {
constructor(document) {
this.getPDFMeta = () => __awaiter(this, void 0, void 0, function* () {
if (!(this.document instanceof Buffer)) {
throw new errors_1.AppError("Document is not Buffer");
}
try {
const signaturesMeta = yield (0, lib_1.verifyPDF)(this.document);
const pdfMeta = yield pdfdataextract_1.PdfData.extract(this.document, config_1.config);
const signaturesMeta = yield verify_pdf_1.default(this.document);
return {
verified: signaturesMeta.verified,
authenticity: signaturesMeta.authenticity,
integrity: signaturesMeta.integrity,
expired: signaturesMeta.expired,
meta: {
certs: signaturesMeta.certs,
},
const result = {
pages: pdfMeta.pages,
fingerpring: pdfMeta.fingerprint,
creation_data: pdfMeta.info.CreationDate,
creator: pdfMeta.info.Creator,
author: pdfMeta.info.Author,
title: pdfMeta.info.Title,
description: pdfMeta.info.Keywords,
mod_date: pdfMeta.info.ModDate,
title: pdfMeta.info.Title || "Unknown",
author: pdfMeta.info.Author || "Unknown",
creation_date: (0, timeUtils_1.formatPdfTime)(pdfMeta.info.CreationDate),
mod_date: (0, timeUtils_1.formatPdfTime)(pdfMeta.info.ModDate),
};
if (signaturesMeta) {
result["signatures"] = signaturesMeta.signatures;
result["expired"] = signaturesMeta.expired;
}
return result;
}
catch (error) {
console.error(error);
throw new Error("Could not get pdf metadata");
throw new errors_1.GeneralError(error);
}
});
this.insertQrCode = (imgBytes, url, scaleFactor) => __awaiter(this, void 0, void 0, function* () {
const pdfDoc = yield PDFDocument.load(this.document);
const pdfDoc = yield pdf_lib_1.PDFDocument.load(this.document);
const img = yield pdfDoc.embedPng(imgBytes);
const scaled = img.scale(scaleFactor);
const pages = pdfDoc.getPages();
......
......@@ -18,21 +18,11 @@ export interface Icert {
pemCertificate: string;
}
export interface IgetMetaResponse {
verified: boolean;
authenticity: boolean;
integrity: boolean;
expired: boolean;
meta: {
certs: Array<{
Icert: any;
}>;
};
expired?: boolean;
signatures?: Array<any>;
pages: number;
fingerpring: string;
creation_data: string;
creator: string;
author: string;
title: string;
description: string;
author: string;
creation_date: string;
mod_date: string;
}
import * as forge from "node-forge";
import * as forge from "@vereign/node-forge";
const issued = (cert) => (anotherCert) =>
cert !== anotherCert && anotherCert.issued(cert);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment