I am trying to digitally sign a PDF using a hardware USB token (Hypersecure USB with eMudhra DSC) in Python. The goal is to create a visible digital signature on the PDF that can be verified by standard PDF viewers like Adobe Acrobat. I am using the following libraries:
PyKCS11 for interacting with the hardware token. pikepdf for manipulating the PDF and adding signature data.
from csv import reader, writer
from pdfrw import PdfReader, PdfWriter
from pdfrw.objects import PdfDict, PdfName, PdfString, PdfArray
import hashlib
import PyKCS11
class TokenSigner:
def __init__(self, token_library_path, pin):
self.pkcs11 = PyKCS11.PyKCS11Lib()
try:
self.pkcs11.load(token_library_path)
print("PKCS11 library loaded successfully.")
except Exception as e:
print(f"Error loading PKCS11 library: {e}")
self.pkcs11 = None
self.session = None
self.token_slot = None
if self.pkcs11:
self.login(pin)
def login(self, pin):
try:
slots = self.pkcs11.getSlotList(tokenPresent=True)
if not slots:
print("No tokens found.")
return
self.token_slot = slots[0]
self.session = self.pkcs11.openSession(self.token_slot, PyKCS11.CKF_SERIAL_SESSION)
self.session.login(pin)
print("Logged in to token.")
except Exception as e:
print(f"Error during login: {e}")
def get_private_key(self):
try:
private_keys = self.session.findObjects([(PyKCS11.CKA_CLASS, PyKCS11.CKO_PRIVATE_KEY)])
if not private_keys:
print("No private keys found.")
return None
for private_key in private_keys:
key_type = self.session.getAttributeValue(private_key, [PyKCS11.CKA_KEY_TYPE])[0]
if key_type == PyKCS11.CKK_RSA:
print("Found valid RSA private key.")
return private_key
print("No valid RSA private key found.")
except Exception as e:
print(f"Error retrieving private key: {e}")
return None
def sign(self, private_key, data):
try:
print(f"Signing data with private key: {private_key}")
if private_key is None:
print("Private key is None!")
return None
hash_data = hashlib.sha256(data).digest()
print(f"Hash data: {hash_data.hex()}")
mechanism = PyKCS11.Mechanism(PyKCS11.CKM_SHA256_RSA_PKCS, None)
signed_data = self.session.sign(private_key, hash_data, mechanism)
print("Data signed successfully.")
return bytes(signed_data)
except PyKCS11.PyKCS11Error as e:
print(f"Signing error: {e}")
return None
def logout(self):
try:
if self.session:
self.session.logout()
self.session.closeSession()
print("Logged out from token.")
except Exception as e:
print(f"Error logging out: {e}")
from pikepdf import Pdf, Name, Array, String, Dictionary
def sign_pdf(input_pdf_path, output_pdf_path, signature_text, signature_position, token_library_path, pin):
try:
# Initialize the signer
signer = TokenSigner(token_library_path, pin)
if signer is None:
print("Signer initialization failed.")
return
private_key = signer.get_private_key()
if private_key is None:
print("Private key not found. Exiting.")
return
# Read the PDF and data for signing
with open(input_pdf_path, "rb") as f:
data_to_sign = f.read()
# Sign the data
signed_data = signer.sign(private_key, data_to_sign)
if signed_data is None:
print("Error signing data.")
return
# Open the input PDF
with Pdf.open(input_pdf_path) as pdf:
first_page = pdf.pages[0]
# Define the signature field dictionary
signature_dict = Dictionary(
Type=Name("/Annot"),
Subtype=Name("/Widget"),
FT=Name("/Sig"),
Rect=Array([
signature_position[0], signature_position[1],
signature_position[0] + signature_position[2], signature_position[1] + signature_position[3]
]),
T=String(signature_text),
DA=String("/Helv 0 Tf 0 g"),
V=Dictionary(
Contents=String(signed_data.hex())
)
)
print("\n signature data = ", signature_dict)
# Add the signature field to the annotations
if "/Annots" not in first_page:
first_page.Annots = Array()
first_page.Annots.append(signature_dict)
# Save the updated PDF
pdf.save(output_pdf_path)
print(f"PDF signed successfully and saved to: {output_pdf_path}")
except Exception as e:
print(f"An unexpected error occurred: {e}")
finally:
if signer:
signer.logout()
token_library_path = "HYP2003-Linux-x86_64/redist/libcastle_v2.so.1.0.0"
pin = "12345678"
pdf_path = "sample.pdf"
output_path = "signed_sample_visible.pdf"
signature_text = "JAYESH MAHATO"
signature_position = (100, 50, 200, 50)
sign_pdf(pdf_path, output_path, signature_text, signature_position, token_library_path, pin)
GETTING OUTPUT AS :
PKCS11 library loaded successfully. Logged in to token. Found valid
RSA private key. Signing data with private key:
CKA_ALWAYS_AUTHENTICATE: False CKA_ALWAYS_SENSITIVE: True CKA_CLASS:
CKO_PRIVATE_KEY CKA_DECRYPT: True CKA_DERIVE: False CKA_END_DATE: ()
CKA_EXTRACTABLE: False CKA_ID: (122, 171, 175, 175, 25, 237, 96, 58,
207, 130, 173, 148, 193, 195, 74, 94, 121, 110, 10, 31)
CKA_KEY_TYPE: CKK_RSA CKA_LABEL:
48055270-5655-4074-92f5-5f043e5b7908 CKA_LOCAL: True
CKA_MODIFIABLE: True CKA_MODULUS: (157, 159, 241, 25, 154, 190,
52, 253, 254, 145, 218, 252, 211, 2, 17, 85, 223, 229, 71, 192, 8,
62, 249, 36, 2, 41, 176, 194, 172, 106, 202, 61, 94, 96, 77, 48, 72,
93, 167, 128, 128, 175, 108, 104, 225, 68, 26, 216, 171, 110, 230,
91, 243, 102, 158, 92, 93, 20, 42, 182, 199, 212, 53, 3, 248, 60,
42, 242, 185, 150, 13, 97, 238, 226, 93, 109, 215, 61, 228, 56,
16, 204, 101, 9, 126, 237, 88, 12, 158, 154, 234, 250, 23, 201,
148, 183, 167, 93, 222, 247, 219, 232, 247, 196, 175, 34, 197,
136, 181, 50, 235, 106, 139, 92, 88, 0, 176, 30, 137, 113, 174,
66, 124, 15, 120, 21, 77, 248, 194, 49, 47, 76, 76, 174, 204,
141, 94, 213, 161, 96, 240, 123, 70, 86, 109, 231, 242, 183, 10,
108, 10, 148, 40, 125, 197, 187, 130, 57, 82, 65, 16, 100, 158,
114, 102, 0, 232, 247, 8, 47, 58, 175, 155, 86, 255, 35, 96, 213,
34, 37, 255, 189, 162, 143, 54, 92, 147, 57, 43, 167, 44, 157,
128, 231, 196, 72, 13, 218, 71, 18, 153, 122, 39, 226, 86, 173,
201, 142, 102, 63, 142, 238, 234, 27, 77, 186, 98, 143, 105, 218,
57, 118, 37, 227, 72, 150, 192, 134, 139, 91, 72, 37, 26, 164,
205, 78, 182, 130, 142, 177, 49, 189, 213, 130, 9, 219, 0, 87,
23, 223, 80, 100, 233, 176, 69, 155) CKA_NEVER_EXTRACTABLE: True
CKA_PRIVATE: True CKA_PUBLIC_EXPONENT: (1, 0, 1) CKA_SENSITIVE: True
CKA_SIGN: True CKA_SIGN_RECOVER: True CKA_START_DATE: ()
CKA_SUBJECT: () CKA_TOKEN: True CKA_UNWRAP: True
CKA_WRAP_WITH_TRUSTED: False Hash data:
d5ddf867002b9541fcc6ea8169a85ca2a741b6afa36448d2b389cdfc0e80bd5b
Data signed successfully.
signature data = pikepdf.Dictionary(Type="/Annot")({ "/DA": "/Helv 0 Tf 0 g", "/FT": "/Sig", "/Rect": [ 100, 50, 300, 100 ],
"/Subtype": "/Widget", "/T": "JAYESH MAHATO", "/Type": "/Annot",
"/V": {
"/Contents": "7d5653390f44bf06377b2ae5264538a3d0d4e9c524d33374ce8b6ad03033ea0354de6fde1d07fbded86679b4f2ebe29a0ffb139eb47ad5c7cd04b7486a671a783936c77fde54a871a00afb818e7acf7140c1570be07597c906d7d138645d3e011fcb9da1c788ba3cc79b31029fc7de1a53cadd50983ddcbb160c7fd78f5e968a53074217f56b85f2f23292f133a9086346c33fe68f13c8c41ab8c2fbbc45bb51648553ba68e9c76bd11e3e7616a67c5bde5f09f55639c8a5f38dcf3b96e5b02eb53c1af4bd3e4ff88db36d204e00cf78867967a7c577a910576e5bf68aec8966a1f18546016fc09c56c388a50ed9ad954608bf07c0c23ff549e10477138e413e"
} }) PDF signed successfully and saved to: signed_sample_visible.pdf
Logged out from token.
The output of my script shows that the signing process completes successfully, and the signed PDF is saved. However:
The resulting PDF does not contain any visible signature. The PDF viewers cannot verify the digital signature. The dictionary data for the signature is being added to the PDF annotations, but the signature is not working as expected. It seems like the digital signature data is either incomplete or not being embedded correctly.
Key points:
The USB token is successfully recognized, and I can retrieve the private key using PyKCS11. Data is being hashed and signed using CKM_SHA256_RSA_PKCS. The visible annotation is added to the PDF, but the digital signature is not functional.
Environment:
Python version: (specify version, e.g., Python 3.8) Libraries: PyKCS11, pikepdf, hashlib Hardware: Hypersecure USB token with eMudhra DSC.
Any help, guidance, or working examples would be greatly appreciated.