PyHanko
PyHanko is a Python library designed for stamping and digitally signing PDF files, offering extensive functionality for handling digital signatures, including support for various PAdES profiles and cryptographic operations. It is actively maintained with frequent minor releases, currently at version 0.34.1, and aims to cover the digital signing features of the PDF standard comprehensively.
Warnings
- breaking The `pyhanko-cli` package was separated from the main `pyhanko` library. Direct imports or reliance on the CLI being bundled with `pyhanko` will break.
- breaking The `certvalidator` dependency, originally an internal fork, was renamed to `pyhanko_certvalidator` to prevent namespace conflicts.
- gotcha PyHanko considers 'hybrid reference files' less secure and disables strict parsing for them by default to avoid accidental corruption. Attempting to process them in strict mode will result in errors.
- deprecated The old LTV (Long-Term Validation) functionality provided by `async_validate_pdf_ltv_signature()` has been deprecated.
- gotcha PyHanko does not provide explicit support for signing or stamping PDF/A and PDF/UA files, meaning the output may not comply with these standards.
- gotcha Comments and annotations added to a signed PDF are considered 'unsafe' changes by `pyhanko`, regardless of the signer's policy.
Install
-
pip install 'pyHanko[pkcs11,image-support,opentype,qr]' pyhanko-cli -
pip install pyhanko
Imports
- IncrementalPdfFileWriter
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
- SimpleSigner
from pyhanko.sign.signers import SimpleSigner
- PdfSignatureMetadata
from pyhanko.sign.signers import PdfSignatureMetadata
- PdfSigner
from pyhanko.sign.signers import PdfSigner
- sign_pdf
from pyhanko.sign.signers import sign_pdf
- HTTPTimeStamper
from pyhanko.sign.timestamps import HTTPTimeStamper
- pyhanko_certvalidator
import pyhanko_certvalidator
Quickstart
import io
import os
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
from pyhanko.sign import signers
def sign_document_example(input_path, output_path, key_path, cert_path, ca_chain_path=None, key_passphrase=None):
# Create dummy key and cert files for runnable example
with open('dummy_key.pem', 'w') as f: f.write('-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----') # Placeholder
with open('dummy_cert.pem', 'w') as f: f.write('-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----') # Placeholder
if ca_chain_path: # Create dummy CA chain if path provided
with open('dummy_ca_chain.pem', 'w') as f: f.write('-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----') # Placeholder
# In a real scenario, replace 'dummy_key.pem' and 'dummy_cert.pem'
# with paths to your actual signer key and certificate.
# key_passphrase should be bytes, e.g., b'your_password'
# Load the signer key and certificate
cms_signer = signers.SimpleSigner.load(
key_path or 'dummy_key.pem',
cert_path or 'dummy_cert.pem',
ca_chain_files=(ca_chain_path or 'dummy_ca_chain.pem',) if ca_chain_path else None,
key_passphrase=key_passphrase
)
with open(input_path, 'rb') as doc_input:
w = IncrementalPdfFileWriter(doc_input)
out = signers.sign_pdf(
w,
signers.PdfSignatureMetadata(field_name='Signature1'), # Use an existing field or 'Signature1' will be created
signer=cms_signer,
)
with open(output_path, 'wb') as doc_output:
doc_output.write(out.read())
print(f"Document signed: {output_path}")
# Example usage (requires a dummy PDF and actual key/cert files in a real scenario)
# You can create a dummy PDF file like 'input.pdf' for testing.
# Replace 'your_key.pem', 'your_cert.pem', 'your_ca_chain.pem' with actual paths.
# To run this example, ensure you have a 'input.pdf' file.
# And replace the '...'(s) with actual PEM contents from your test certificates if you want to run it end to end.
# try:
# # Create a minimal dummy PDF for testing if it doesn't exist
# if not os.path.exists('input.pdf'):
# from PyPDF2 import PdfWriter
# writer = PdfWriter()
# writer.add_blank_page(width=72, height=72)
# with open('input.pdf', 'wb') as f: writer.write(f)
#
# sign_document_example(
# input_path='input.pdf',
# output_path='signed_output.pdf',
# key_path=os.environ.get('PYHANKO_SIGNER_KEY_PATH', 'dummy_key.pem'),
# cert_path=os.environ.get('PYHANKO_SIGNER_CERT_PATH', 'dummy_cert.pem'),
# ca_chain_path=os.environ.get('PYHANKO_CA_CHAIN_PATH', 'dummy_ca_chain.pem'),
# key_passphrase=os.environ.get('PYHANKO_KEY_PASSPHRASE', '').encode('utf-8')
# )
# finally:
# # Clean up dummy files
# for f in ['dummy_key.pem', 'dummy_cert.pem', 'dummy_ca_chain.pem']:
# if os.path.exists(f): os.remove(f)