First commit: create mvp project
- create an orchestrator to orchestrate signature of pdf; - create a worker to get request from amqp and resend it to amqp
This commit is contained in:
commit
ef32d5d91c
4
pythonProject/assets/.gitignore
vendored
Normal file
4
pythonProject/assets/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
*
|
||||||
|
!dummy.p12
|
||||||
|
!test.pdf
|
||||||
|
!.gitignore
|
BIN
pythonProject/assets/dummy.p12
Normal file
BIN
pythonProject/assets/dummy.p12
Normal file
Binary file not shown.
BIN
pythonProject/assets/test.pdf
Normal file
BIN
pythonProject/assets/test.pdf
Normal file
Binary file not shown.
20
pythonProject/requirements.txt
Normal file
20
pythonProject/requirements.txt
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
asn1crypto==1.5.1
|
||||||
|
certifi==2024.6.2
|
||||||
|
cffi==1.16.0
|
||||||
|
charset-normalizer==3.3.2
|
||||||
|
click==8.1.7
|
||||||
|
cryptography==42.0.8
|
||||||
|
idna==3.7
|
||||||
|
oscrypto==1.3.0
|
||||||
|
pika==1.3.2
|
||||||
|
pycparser==2.22
|
||||||
|
pyHanko==0.25.0
|
||||||
|
pyhanko-certvalidator==0.26.3
|
||||||
|
pypng==0.20220715.0
|
||||||
|
PyYAML==6.0.1
|
||||||
|
qrcode==7.4.2
|
||||||
|
requests==2.32.3
|
||||||
|
typing_extensions==4.12.2
|
||||||
|
tzlocal==5.2
|
||||||
|
uritools==4.0.3
|
||||||
|
urllib3==2.2.2
|
74
pythonProject/sign.py
Normal file
74
pythonProject/sign.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import io
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pyhanko import stamp
|
||||||
|
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
|
||||||
|
from pyhanko.sign import signers, timestamps, fields
|
||||||
|
from pyhanko_certvalidator import ValidationContext
|
||||||
|
from typing_extensions import Buffer
|
||||||
|
|
||||||
|
|
||||||
|
class SignOrchestrator:
|
||||||
|
"""Orchestrate the signature on document"""
|
||||||
|
|
||||||
|
def __init__(self, pkcs12_path: str, timestamp_url: str, pkcs12_password: Optional[bytes] = None):
|
||||||
|
# Load signer key material from PKCS#12 file
|
||||||
|
# This assumes that any relevant intermediate certs are also included
|
||||||
|
# in the PKCS#12 file.
|
||||||
|
self.signer = signers.SimpleSigner.load_pkcs12(
|
||||||
|
pfx_file=pkcs12_path, passphrase=pkcs12_password
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set up a timestamping client to fetch timestamps tokens
|
||||||
|
self.timestamper = timestamps.HTTPTimeStamper(
|
||||||
|
url=timestamp_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stamp_style = stamp.TextStampStyle(
|
||||||
|
stamp_text="Signé par:\n%(signer_text)s\nLe %(ts)s",
|
||||||
|
border_width=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_signature_metadata(self, reason: str, field_name: str):
|
||||||
|
# Settings for PAdES-LTA
|
||||||
|
return signers.PdfSignatureMetadata(
|
||||||
|
field_name=field_name, md_algorithm='sha256',
|
||||||
|
# Mark the signature as a PAdES signature
|
||||||
|
subfilter=fields.SigSeedSubFilter.PADES,
|
||||||
|
# We'll also need a validation context
|
||||||
|
# to fetch & embed revocation info.
|
||||||
|
# validation_context=ValidationContext(allow_fetching=True),
|
||||||
|
# Embed relevant OCSP responses / CRLs (PAdES-LT)
|
||||||
|
# embed_validation_info=True,
|
||||||
|
# Tell pyHanko to put in an extra DocumentTimeStamp
|
||||||
|
# to kick off the PAdES-LTA timestamp chain.
|
||||||
|
use_pades_lta=True,
|
||||||
|
reason=reason,
|
||||||
|
)
|
||||||
|
|
||||||
|
def sign(self, reason: str, signature_index: int, input_content: Buffer, on_page: int, box_place: (int, int, int, int), signer_text: str) -> io.BytesIO:
|
||||||
|
field_name = 'Signature' + str(signature_index)
|
||||||
|
signature_meta = self._make_signature_metadata(reason, field_name)
|
||||||
|
|
||||||
|
pdf_signer = signers.PdfSigner(
|
||||||
|
signature_meta, signer=self.signer, timestamper=self.timestamper,
|
||||||
|
stamp_style=self.stamp_style
|
||||||
|
)
|
||||||
|
|
||||||
|
inf = io.BytesIO(input_content)
|
||||||
|
w = IncrementalPdfFileWriter(inf)
|
||||||
|
fields.append_signature_field(w, sig_field_spec=fields.SigFieldSpec(
|
||||||
|
field_name, on_page=on_page, box=box_place
|
||||||
|
))
|
||||||
|
outf = io.BytesIO()
|
||||||
|
pdf_signer.sign_pdf(
|
||||||
|
w, output=outf, appearance_text_params={'signer_text': signer_text}
|
||||||
|
)
|
||||||
|
|
||||||
|
return outf
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
19
pythonProject/sign_individual.py
Normal file
19
pythonProject/sign_individual.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from sign import SignOrchestrator
|
||||||
|
|
||||||
|
orchestrator = SignOrchestrator('./assets/dummy.p12','http://freetsa.org/tsr', pkcs12_password=None)
|
||||||
|
|
||||||
|
with open('./assets/test.pdf', 'rb') as input:
|
||||||
|
signed_content = orchestrator.sign(reason="first signer", signature_index=0,
|
||||||
|
input_content=input.read(), box_place=(300, 600, 500, 660), on_page=0,
|
||||||
|
signer_text="Mme Caroline Diallo")
|
||||||
|
|
||||||
|
with open('./assets/test_signed_0.pdf', 'wb') as output:
|
||||||
|
output.write(signed_content.read())
|
||||||
|
|
||||||
|
with open('./assets/test_signed_0.pdf', 'rb') as input:
|
||||||
|
signed_content = orchestrator.sign(reason="second signer", signature_index=1,
|
||||||
|
input_content=input.read(), box_place=(100, 600, 300, 660), on_page=0,
|
||||||
|
signer_text="M. Bah Mamadou")
|
||||||
|
|
||||||
|
with open('./assets/test_signed_1.pdf', 'wb') as output:
|
||||||
|
output.write(signed_content.read())
|
75
pythonProject/worker.py
Normal file
75
pythonProject/worker.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import base64
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import pika
|
||||||
|
import sign
|
||||||
|
|
||||||
|
dsn = 'amqp://guest:guest@localhost:32773/%2f/to_python_sign'
|
||||||
|
|
||||||
|
LOG_FORMAT = ('%(levelname) -10s %(asctime)s %(name) -30s %(funcName) '
|
||||||
|
'-35s %(lineno) -5d: %(message)s')
|
||||||
|
logging.basicConfig(level=logging.INFO, format=LOG_FORMAT)
|
||||||
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
LOGGER.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
orchestrator = sign.SignOrchestrator('./assets/dummy.p12',
|
||||||
|
'http://freetsa.org/tsr', pkcs12_password=None)
|
||||||
|
|
||||||
|
parameters = pika.URLParameters(dsn)
|
||||||
|
connection = pika.BlockingConnection(parameters)
|
||||||
|
channel = connection.channel()
|
||||||
|
channel.confirm_delivery()
|
||||||
|
|
||||||
|
|
||||||
|
def on_message(channel, method_frame, header_frame, body):
|
||||||
|
LOGGER.debug("receiving a message")
|
||||||
|
body_content = json.loads(body)
|
||||||
|
LOGGER.info(f"request to add a signature, signatureId: {body_content['signatureId']}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
box_place = (body_content['signatureZone']['x'], body_content['signatureZone']['y'],
|
||||||
|
body_content['signatureZone']['x'] + body_content['signatureZone']['width'],
|
||||||
|
body_content['signatureZone']['y'] + body_content['signatureZone']['height'])
|
||||||
|
LOGGER.debug("will try signature")
|
||||||
|
signed = orchestrator.sign(reason=body_content['reason'], signature_index=body_content['signatureZoneIndex'],
|
||||||
|
box_place=box_place, on_page=body_content['signatureZone']['PDFPage']['index'],
|
||||||
|
signer_text=body_content['signerText'],
|
||||||
|
input_content=base64.b64decode(body_content['content']))
|
||||||
|
LOGGER.info(f"signature obtained, signatureId: {body_content['signatureId']}")
|
||||||
|
|
||||||
|
with open(f"./assets/new.{method_frame.consumer_tag}.{method_frame.delivery_tag}.pdf", 'wb') as f:
|
||||||
|
f.write(signed.read())
|
||||||
|
LOGGER.debug("signed file saved")
|
||||||
|
# because we consumed the buffer to write a file, we have to rewind it
|
||||||
|
signed.seek(0)
|
||||||
|
channel.basic_publish(exchange='signed_docs',
|
||||||
|
body=json.dumps({'signatureId': body_content['signatureId'],
|
||||||
|
'content': base64.b64encode(signed.read()).decode('utf-8')}),
|
||||||
|
properties=pika.BasicProperties(content_type='application/json',
|
||||||
|
delivery_mode=pika.DeliveryMode.Transient),
|
||||||
|
routing_key='signed_doc')
|
||||||
|
LOGGER.debug("signed file resend to amqp")
|
||||||
|
channel.basic_ack(delivery_tag=method_frame.delivery_tag)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
LOGGER.warning(f"error encountered while signing: {e}")
|
||||||
|
if method_frame.redelivered:
|
||||||
|
LOGGER.warning(
|
||||||
|
f"stopping handling this message, because the message is already redelivered, signatureId: {body_content['signatureId']}")
|
||||||
|
channel.basic_reject(delivery_tag=method_frame.delivery_tag, requeue=True)
|
||||||
|
else:
|
||||||
|
LOGGER.warning(f"first try failed, signatureId: {body_content['signatureId']}")
|
||||||
|
channel.basic_ack(delivery_tag=method_frame.delivery_tag)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
LOGGER.info('starting worker')
|
||||||
|
channel.basic_consume('to_python_sign', on_message)
|
||||||
|
try:
|
||||||
|
LOGGER.info("start consuming")
|
||||||
|
channel.start_consuming()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
LOGGER.info("keyboard interrupt")
|
||||||
|
channel.stop_consuming()
|
||||||
|
connection.close()
|
Loading…
Reference in New Issue
Block a user