Created
October 11, 2024 05:54
-
-
Save revant/39456fa8f9ce8c47f695fac9611586d1 to your computer and use it in GitHub Desktop.
Frappe Framework and PyHanko digital signatures
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# License: MIT | |
# Store signatures in "Digital Signature" | |
# it uploads signatures on S3 for secure storage. | |
# Code snippet to fetch signature and sign document | |
import io | |
import os | |
import ntpath | |
import boto3 | |
import botocore.exceptions | |
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter | |
from pyhanko.sign import signers | |
from pyhanko.sign.fields import SigFieldSpec, append_signature_field | |
import frappe | |
from frappe.utils.pdf import get_pdf | |
def validate_s3_settings(): | |
settings_doc = frappe.get_single("Digital Signature Settings") | |
if ( | |
not settings_doc.s3_endpoint_url | |
or not settings_doc.s3_region_name | |
or not settings_doc.s3_bucket | |
or not settings_doc.aws_access_key_id | |
or not settings_doc.get_password("aws_secret_access_key") | |
): | |
frappe.throw( | |
"Please enter all mandatory fields for S3 Details in Digital Signature Settings" # noqa | |
) | |
def get_s3_client(settings_doc): | |
aws_secret_access_key = settings_doc.get_password("aws_secret_access_key") | |
return boto3.client( | |
service_name="s3", | |
endpoint_url=settings_doc.s3_endpoint_url, | |
region_name=settings_doc.s3_region_name, | |
aws_access_key_id=settings_doc.aws_access_key_id, | |
aws_secret_access_key=aws_secret_access_key, | |
) | |
def get_pfx_from_s3(key): | |
validate_s3_settings() | |
settings_doc = frappe.get_single("Digital Signature Settings") | |
s3_client = get_s3_client(settings_doc) | |
try: | |
path = "/tmp/" + ntpath.basename(key) | |
s3_client.download_file(settings_doc.s3_bucket, key, path) | |
return path | |
except botocore.exceptions.ClientError: | |
frappe.throw("Failed to get signature file.", title="Error") | |
def get_signed_pdf(doctype, docname, print_format=None): | |
doc = frappe.get_doc(doctype, docname) | |
html = frappe.get_print(doctype, docname, print_format or "Offer Letter") | |
file = get_pdf(html) | |
file = io.BytesIO(file) | |
w = IncrementalPdfFileWriter(file) | |
p = None | |
if doc.name_of_primary_officer: | |
p = sign_officer(doc.name_of_primary_officer, w, 43, 297, 1) | |
if doc.name_of_authorized_signatory: | |
p = sign_officer(doc.name_of_authorized_signatory, w, 350, 297, 1) | |
return p.getbuffer() if p else get_pdf(html) | |
def get_officer_signer(officer, file_path): | |
signer = signers.SimpleSigner.load_pkcs12( | |
pfx_file=file_path, | |
passphrase=(officer.get_password("sign_secret")).encode(), | |
) | |
return signer | |
def sign_officer(officer_name, w, x1, y1, page): | |
""" | |
Manipulate following values to adjust signature: | |
x1: Horizontal Position (increase to move right) | |
y1: Vertical Position (increase to move upward) | |
x2: Width | |
y2: Height | |
""" | |
officer = frappe.get_doc("Digital Signature", officer_name) | |
if officer.s3_key: | |
x2 = x1 + 140 | |
y2 = y1 + 44 | |
file_path = get_pfx_from_s3(officer.s3_key) | |
officer_signer = get_officer_signer(officer, file_path) | |
append_signature_field( | |
w, | |
SigFieldSpec( | |
sig_field_name=officer.name, | |
box=(x1, y1, x2, y2), | |
on_page=page, | |
), | |
) | |
out = signers.sign_pdf( | |
w, | |
signers.PdfSignatureMetadata(field_name=officer.name), | |
signer=officer_signer, | |
) | |
os.remove(file_path) | |
return out |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment