chore: sign document

This commit is contained in:
Mythie
2023-09-25 15:57:10 +10:00
parent e67e96333b
commit a52c19355a
12 changed files with 217 additions and 3 deletions

Binary file not shown.

BIN
apps/web/example/cert.p12 Normal file

Binary file not shown.

46
package-lock.json generated
View File

@ -1876,6 +1876,10 @@
"resolved": "packages/prisma",
"link": true
},
"node_modules/@documenso/signing": {
"resolved": "packages/signing",
"link": true
},
"node_modules/@documenso/tailwind-config": {
"resolved": "packages/tailwind-config",
"link": true
@ -6423,6 +6427,15 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.0.tgz",
"integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A=="
},
"node_modules/@types/node-forge": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.4.tgz",
"integrity": "sha512-08scBQriFsBbm/CuBWOXRMD1RG7ydFW01EDR6vGX27nxcj6E/jGSCOLdICNd8ETwQlLFXVBVA854RX6Y7vPSrQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/nodemailer": {
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.9.tgz",
@ -14370,11 +14383,30 @@
}
}
},
"node_modules/node-forge": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
"integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
"engines": {
"node": ">= 6.13.0"
}
},
"node_modules/node-releases": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
},
"node_modules/node-signpdf": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/node-signpdf/-/node-signpdf-2.0.0.tgz",
"integrity": "sha512-B6fDvD8z2v0pRntQjgPO2ARqxh0pfNwfSn6YEbP7cv6xmPgcphFwcrMA3N47LztmpVbAM3vUFUslX32L6NRYDg==",
"engines": {
"node": ">=12"
},
"peerDependencies": {
"node-forge": "^1.2.1"
}
},
"node_modules/node-sql-parser": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/node-sql-parser/-/node-sql-parser-4.9.0.tgz",
@ -19827,6 +19859,20 @@
"typescript": "^5.1.6"
}
},
"packages/signing": {
"name": "@documenso/signing",
"version": "1.0.0",
"license": "MIT",
"dependencies": {
"@documenso/tsconfig": "*",
"node-forge": "^1.3.1",
"node-signpdf": "^2.0.0",
"pdf-lib": "^1.17.1"
},
"devDependencies": {
"@types/node-forge": "^1.3.4"
}
},
"packages/tailwind-config": {
"name": "@documenso/tailwind-config",
"version": "0.0.0",

View File

@ -19,6 +19,7 @@
"@aws-sdk/signature-v4-crt": "^3.410.0",
"@documenso/email": "*",
"@documenso/prisma": "*",
"@documenso/signing": "*",
"@next-auth/prisma-adapter": "1.0.7",
"@pdf-lib/fontkit": "^1.1.1",
"@scure/base": "^1.1.3",
@ -38,4 +39,4 @@
"@types/bcrypt": "^5.0.0",
"@types/luxon": "^3.3.1"
}
}
}

View File

@ -5,6 +5,7 @@ import { PDFDocument } from 'pdf-lib';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
import { signPdf } from '@documenso/signing';
import { getFile } from '../../universal/upload/get-file';
import { putFile } from '../../universal/upload/put-file';
@ -71,12 +72,14 @@ export const sealDocument = async ({ documentId }: SealDocumentOptions) => {
const pdfBytes = await doc.save();
const pdfBuffer = await signPdf({ pdf: Buffer.from(pdfBytes) });
const { name, ext } = path.parse(document.title);
const { data: newData } = await putFile({
name: `${name}_signed${ext}`,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(Buffer.from(pdfBytes)),
arrayBuffer: async () => Promise.resolve(pdfBuffer),
});
await prisma.documentData.update({

View File

@ -0,0 +1,73 @@
import signer from 'node-signpdf';
import { PDFArray, PDFDocument, PDFHexString, PDFName, PDFNumber, PDFString } from 'pdf-lib';
export type AddSigningPlaceholderOptions = {
pdf: Buffer;
};
export const addSigningPlaceholder = async ({ pdf }: AddSigningPlaceholderOptions) => {
const doc = await PDFDocument.load(pdf);
const pages = doc.getPages();
const byteRange = PDFArray.withContext(doc.context);
byteRange.push(PDFNumber.of(0));
byteRange.push(PDFName.of(signer.byteRangePlaceholder));
byteRange.push(PDFName.of(signer.byteRangePlaceholder));
byteRange.push(PDFName.of(signer.byteRangePlaceholder));
const signature = doc.context.obj({
Type: 'Sig',
Filter: 'Adobe.PPKLite',
SubFilter: 'adbe.pkcs7.detached',
ByteRange: byteRange,
Contents: PDFHexString.fromText(' '.repeat(8192)),
Reason: PDFString.of('Signed by Documenso'),
M: PDFString.fromDate(new Date()),
});
const signatureRef = doc.context.register(signature);
const widget = doc.context.obj({
Type: 'Annot',
Subtype: 'Widget',
FT: 'Sig',
Rect: [0, 0, 0, 0],
V: signatureRef,
T: PDFString.of('Signature1'),
F: 4,
P: pages[0].ref,
});
const widgetRef = doc.context.register(widget);
let widgets = pages[0].node.get(PDFName.of('Annots'));
if (widgets instanceof PDFArray) {
widgets.push(widgetRef);
} else {
const newWidgets = PDFArray.withContext(doc.context);
newWidgets.push(widgetRef);
pages[0].node.set(PDFName.of('Annots'), newWidgets);
widgets = pages[0].node.get(PDFName.of('Annots'));
}
if (!widgets) {
throw new Error('No widgets');
}
pages[0].node.set(PDFName.of('Annots'), widgets);
doc.catalog.set(
PDFName.of('AcroForm'),
doc.context.obj({
SigFlags: 3,
Fields: [widgetRef],
}),
);
return Buffer.from(await doc.save({ useObjectStreams: false }));
};

17
packages/signing/index.ts Normal file
View File

@ -0,0 +1,17 @@
import { match } from 'ts-pattern';
import { signWithLocalCert } from './transports/local-cert';
export type SignOptions = {
pdf: Buffer;
};
export const signPdf = async ({ pdf }: SignOptions) => {
const transport = process.env.NEXT_PRIVATE_SIGNING_TRANSPORT || 'local';
return await match(transport)
.with('local', async () => signWithLocalCert({ pdf }))
.otherwise(() => {
throw new Error(`Unsupported signing transport: ${transport}`);
});
};

View File

@ -0,0 +1,23 @@
{
"name": "@documenso/signing",
"version": "1.0.0",
"main": "./index.ts",
"types": "./index.ts",
"license": "AGPLv3",
"files": [
"transports/",
"index.ts"
],
"scripts": {
},
"dependencies": {
"@documenso/tsconfig": "*",
"node-forge": "^1.3.1",
"node-signpdf": "^2.0.0",
"pdf-lib": "^1.17.1",
"ts-pattern": "^5.0.5"
},
"devDependencies": {
"@types/node-forge": "^1.3.4"
}
}

View File

@ -0,0 +1,32 @@
import signer from 'node-signpdf';
import fs from 'node:fs';
import { addSigningPlaceholder } from '../helpers/addSigningPlaceholder';
export type SignWithLocalCertOptions = {
pdf: Buffer;
};
export const signWithLocalCert = async ({ pdf }: SignWithLocalCertOptions) => {
const pdfWithPlaceholder = await addSigningPlaceholder({ pdf });
let p12Cert: Buffer | null = null;
if (process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS) {
p12Cert = Buffer.from(process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS, 'base64');
}
if (!p12Cert) {
p12Cert = Buffer.from(
fs.readFileSync(process.env.NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH || './example/cert.p12'),
);
}
if (process.env.NEXT_PRIVATE_SIGNING_PASSPHRASE) {
return signer.sign(pdfWithPlaceholder, p12Cert, {
passphrase: process.env.NEXT_PRIVATE_SIGNING_PASSPHRASE,
});
}
return signer.sign(pdfWithPlaceholder, p12Cert);
};

View File

@ -0,0 +1,8 @@
{
"extends": "@documenso/tsconfig/react-library.json",
"compilerOptions": {
"types": ["@documenso/tsconfig/process-env.d.ts", "@types/node"]
},
"include": ["**/*.ts", "**/*.tsx", "**/*.d.ts", "**/*.json"],
"exclude": ["dist", "build", "node_modules"]
}

View File

@ -21,6 +21,12 @@ declare namespace NodeJS {
NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID?: string;
NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY?: string;
NEXT_PRIVATE_SIGNING_TRANSPORT?: 'local' | 'http' | 'gcloud-hsm';
NEXT_PRIVATE_SIGNING_PASSPHRASE?: string;
NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH?: string;
NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS?: string;
NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING?: string;
NEXT_PRIVATE_SMTP_TRANSPORT?: 'mailchannels' | 'smtp-auth' | 'smtp-api';
NEXT_PRIVATE_MAILCHANNELS_API_KEY?: string;

View File

@ -44,6 +44,11 @@
"NEXT_PRIVATE_UPLOAD_BUCKET",
"NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID",
"NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY",
"NEXT_PRIVATE_SIGNING_TRANSPORT",
"NEXT_PRIVATE_SIGNING_PASSPHRASE",
"NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH",
"NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS",
"NEXT_PRIVATE_SIGNING_LOCAL_FILE_ENCODING",
"NEXT_PRIVATE_SMTP_TRANSPORT",
"NEXT_PRIVATE_MAILCHANNELS_API_KEY",
"NEXT_PRIVATE_MAILCHANNELS_ENDPOINT",
@ -69,4 +74,4 @@
"POSTGRES_PRISMA_URL",
"POSTGRES_URL_NON_POOLING"
]
}
}