From ef32d5d91ce103c33431dc3a8df5709a9c64095f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 27 Jun 2024 09:51:32 +0200 Subject: [PATCH] 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 --- pythonProject/assets/.gitignore | 4 ++ pythonProject/assets/dummy.p12 | Bin 0 -> 2675 bytes pythonProject/assets/test.pdf | Bin 0 -> 6542 bytes pythonProject/requirements.txt | 20 +++++++++ pythonProject/sign.py | 74 ++++++++++++++++++++++++++++++ pythonProject/sign_individual.py | 19 ++++++++ pythonProject/worker.py | 75 +++++++++++++++++++++++++++++++ 7 files changed, 192 insertions(+) create mode 100644 pythonProject/assets/.gitignore create mode 100644 pythonProject/assets/dummy.p12 create mode 100644 pythonProject/assets/test.pdf create mode 100644 pythonProject/requirements.txt create mode 100644 pythonProject/sign.py create mode 100644 pythonProject/sign_individual.py create mode 100644 pythonProject/worker.py diff --git a/pythonProject/assets/.gitignore b/pythonProject/assets/.gitignore new file mode 100644 index 0000000..74e0e63 --- /dev/null +++ b/pythonProject/assets/.gitignore @@ -0,0 +1,4 @@ +* +!dummy.p12 +!test.pdf +!.gitignore \ No newline at end of file diff --git a/pythonProject/assets/dummy.p12 b/pythonProject/assets/dummy.p12 new file mode 100644 index 0000000000000000000000000000000000000000..fc577cfd68dc79547712b0ad062759be85dfa504 GIT binary patch literal 2675 zcmai$X*3iH8^_Hw#x`Tg7Na|2gkc!Q5@J$AG{o4q#+p4lH6z=Qa7ET3ib$nWX{^~I zOO}Z-TuYe}vWGFKZs~Q;`=0K{_rr6Z^Z)uM|W*TW~|s=g`XkN(fp8#LIii zNKW-)asd#?%Y}x3dH=f&$j8lvJ__dZNHqgoJOlzDLBdM8QZ3JEavJ;ER~+D12D1|> zATIGs<>#6V7WO0rI@23Y^PT_5l&P@D*EKQ=4<}J&7qtpGN_{tX?5nCKNWI*K##l{F z=In%zUYT&1m}ySVT1S*JM985mNO>G~HBkXuLtThVBOAwfhLYssVlDoPa;@(m=s)lg z#O-b^i(_yIO;$xkXCh*{>$UWJW@3tXe`gm7g*4M2ZEsOt4?Npf9b;7V^7A#!+2LjJ z1r~AKpk7C)lhKlxV#{kZkQuouQwkiwXrJH8hN+G}4|b>KWas%UXk?$B@eVR`{8?}m z42E0S@FRvE`%Xfc0l9Q{0 z1mKIMl~3}9&_;~~uCT~N4xVG2k95GS1~sjr_j0%tqaOc^8sL4Ii4oUa+HS#@1T^u1 zWN;;yS29=Pt-n8+zmYc2na>vSlCSV-KBVtInnlt&ZyxALyXhmQ{;3l*9HHCQBzDEH zuvnm>K=Gu(yu4!q`~DwSAKsQzdbT*db|0fQblZ)$-mxmoj%<U{ zuj!Qj#i=4V%2D~rfVC4nNU15ijTJ`F&0x+aVar&%qZJ$dFJ|(R(&HU+y8k%tidVO@ ziOuJe|8!zue=&XF@}!W}1y_z-%;LGjO1E{<{0*tjN!R0Y?K7iN_BV{Q-|B?6EoSqqo^AFdD!fe4J6jvR)jwVI1W!tsI+F+1MrabK9(O+DDQSJ!f*_tmuZ2q=*{hiuJ>BL_ zuVm`@kxHC)8)n@H%{6Ta z?J@_WC)GCu01+YsmfCKAFrYOa0JL&=}A=EArd~7)Am%Dq?j>BW z{IhsFT>t*`Syw2}Y4y7T-<~b!i!rKin?!nz8?8eVj}%OenNS%k0E%efiusXt5^4tD zB|tqn6WF$;z3Y)8_@TITYu+UjQ@VSk7aQZ6-mgV-MN_y_e)HrXKygQdDBR%(_N9Xg z01Nz&T@Zc%@F3JUFj4;xNZ-E=Q`pMHk^c*%o7e&3r%;X9!aJg)w}ialrQG^qqw#VLC>vya@Rr?@GtU%~mO1{X?+)#hQ-ql8nv!EUi?S zX`w}vSge7HK`r;TozxCo$t~i+F@sZs?WS^I#)&z8_SnzWX$_nT&zXT@FE_UR4A`=8(%}yzGT0ty6|{R}1jGZ2uSZ zHnV)_OVJB$q*uHax1XH{ySW$pO-Lr1Q0*CY#@-ofAXDy1yflcn>n&Tg-Dq16fy=^` zfVbfx`m>zuslUxXrn|l^vZ{LiCL*>TEidb4%fdNjDKK55x5<=*AU!*H4jAb)OdzC1 z4xD+h_o}9nzSQ=;2Ir0ICa3|bQ(?te5}Oa2F=7*k7Ma`#MUazYj^_5y2|L|+E;@J* zB2-h#>bj0$yj-Dc^?2#2E|tC^5| zI@HhYQq()e?b~+i)A>Vt#03adOKQ8x z4=8`@@C`_YxiPzmD=lV&ItmWAisB*I@=Zc}P!^BU>0KU}(+%)P4`f%~FY?=_zoJ+} zeJfd^6YAFTHhdZ8XdmgKf!mWuVj*nO%D#pITK+d+`NTCOKw`>koCMd;CBU-E5e?(V z-EKI(YoF1^Xbm*<_qzrJaC3oVEx+y#J_%^0k|_?VHxw3^L%1FVY(uMIyHJ;N#@S=C RAYM}C-)ubw`rJRL{SSxA&-(xX literal 0 HcmV?d00001 diff --git a/pythonProject/assets/test.pdf b/pythonProject/assets/test.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a9c7002b3283ef85a52d267d65e70e537bdb0440 GIT binary patch literal 6542 zcma)B2{@E%`|sGwkbMhfEMqJ&`(i%~BV=E)Gsf7$FvgmsRAdb$vXqF*mh9QGMD}bY z`xcU-L^{HMrmypz?fb6p|6bS3`#kr1@AvP0=DL0ZGSS8>z!cE{P+NXmLtAy*9RM5% z1(IF70II5x(PZ0C3uf4nLmNql@>Nwc?;pqA{3;pF{Oe9(*NDaUE6wWPxg+O} zMt;W>6eJlpeUJJQd?+^Wg;=~CWpOGk?&zWWFQPw~O06}9s)~IoikT5ia2K;hywIHx7Eh*wG0S-86K%}g`%X?B!zQffJ2;@Y*cozt`)g;lEJka$J)3}L zrP}ZH2R$;U#}@`>`9m0u+RpiDyN9$KhNWwEkH0^+fyjT-bNKMw#Pwa#in11o&!{>s z)i!wD`?pccgkSde(Gc~RwmhL8rOYBcbQgZJy$g@b8C2yz~8ylxuFg)P1i|x>PS5J4a9fy zK%gRweY;ic*@<+=yiWq5dMDfW6Iu+chu8gY2VBXR0jtY|>vE}|7f3E^<5SFV{@Cj2 zKd@}Eav}U$gpBx>ivr(dg1k5LX5S2}PmkEOubSdJx5&jW(+;OyknhIuUiELo6Qk#J zofak6-(SUgwS6Z0s)r$5CduK<^;>e0^ar>Z)q z>gtKF_f#f=!%4fO6avo{sP4pZid~8VirBtIU7!_}Ps+?#LL1i_(u$uIuuj z^~^YvrkXPOdQw|$bTDU0I%)CfLyH1Ab@o6rCo!fwD>kE%_6D2`0V;>&01dtj=;~8pRl5>nt&1Et)Lxc`j0}`#~nJTHU0%mrb56 zWb45eIKm+zL*6TOWWiJj$@g$3khHPotN#7*OFWI`1C@2gt-ZZN-WpZ^?eY-71TtU1aPo06@m z7P$T*GHhTYH&SjS%+mr7*bmmeb=>dRJXt=fK2Wb5G-IqJvn zU|6zZCC>M%%AzoOU4FIeN?0XVx`HQmL%KGtte1TM6|oSm^bunqvaWg8dpL~?=dg6;Phk$ zt3Gy)yIzk_gOSesQ8FHy3|kVwzz?nUx1G+ml3FeW$caprIv{^7ek_qdc(65+_hp5` z-rV;fIl(-5CbmlT>b>lb>P2cEuS=Jte|cjO94{2_+IH ze4S5LM(5$`HnX*Fv1&ti7%Zf3%Q0cR+y)>)0lrYh82B2FHIFh}B~)@6UU61Pn>3 z`^HdXnVODctr3{hJ5YGNA9eAH%lE+Q&nn8RE`o^$fqa<>8uBNE2~nR%osoioCw(lI zW5VCTa>+r&%={^#Q;y}fGO`X{$s^BKvg4;84sHsmiRs?3;I_EaHoa@nDm`oREYU8- zcPAI+Ue}?_-;M*he=|f_6}x@wUAV=nc94zAVTwfPAWq-8U8r#_v+wpjN-KEqH zxH9cEP&Q`5z;7~R;v)FEr05fK$@`No$&hr2RUhiQ1tTkp%ZV}f=%N z?QOfmC%#PtXukEEsOdl5xZ*vQc4B*d;$W@C=vsP?dd0)kD& z`cJ-l?>W<)+c$XrP~?RL*7pN}O${^3zSCQJ^46aS)RnH3C?wF z+ABDG!kS+qxPc=4d3=znVi8l=0`|QGGx`t}mv3|n{cEGI@gcUR05SaR?mXwlZifHa zQ!b-R7cF<0K%6U&4}IG?f44=#`jfnN)KQrLv*-kqOEdbyS7Pu81D#rg}&w)Da6ok$MeyDX;B z%rdA`$|qZca77#-^SGHXrd#R-Xbuc<3dXz%h8I-R|>23_AhNJ%tIlrcvP zQ^Jg;i=`y=Z=;7*Kp)#VW!|yl#zjjKeQMH!yF1`JfystF+#tn5v3p%z6AO1eNpfjj za-LEhJR|EufhKpdEO~6?z!Fq%OMedvT)+woE6%Tg1Hcug@n2dps3&=^UCF44C!#K* zMuqz1DgaYp_X>UKjFl9`3Y(-z94IA2I_#t{o9;W95>8TI8h!KErFQ?YdJdh^_&O<` zE*qp$5t1;O>Ivda02O##ytB=^CzZM(KB#r zGbKJzYJoC8!J)uc+$Z(wM#ua^DkZIREQ_^}A9S&QF-Da;Gm$iCYhlzwC>Ban5a{2C zbr$!~H$t9s8|u1oDOvrcN{9g$Ozn!cR!{9h zvIuwKGhb_~XPkC5Md`gg#l+j`1!_b1zXG2O2m)c1vWz4kCF?G0mt@N~8 zQJ!V+MN<6zC&zT9(H~9~6lUS3HCHYQou2ESSN;W{9XX%EeTy4_<1zCS9kQ2;wTRV_ zBdGGpR%)_U&fj|(XQsDkoTq-p_ww+A=O5uUnU|Tc8^cYz1|?PE$a}hjomH7!tGato zTOsX6*2U5)1Huh+mylj~pKN*y)-`|aBDy*7?3^8J#9=&JPQdQ9v z2Jc(wD=yET9mHD&j6CYgF9g(-AZk-L| z_qAv{y^-v>y_egm=)N}t7Dh{3hO1$+5ptw0>-q2t+TWN8oWV`j$}r{a<=KGRpx2kT z)D~V6gZ9g|tnFnTTdH;?8-)!-iJ~LH7hD2g_Z7csmd_S?j_|mCM^N^BL_^rymE!_q zrFEOV-f!KPKG=8+3*l6I7P>ZUl|wkpUx;aOMy(yUJvVWI0r#{A<(hh5pz$+DB0;{yu9`m_L~lD z4h9ap9aRllo0ewm2l(#&x7{2QWy*@X2XzaK8ulH%iZ%Az4mtJ3j%Y{edk2i=-EKDb z3=|9=-mklVN72FNX}Rm|`jDl28yK>Z&r*#ynYH>#?v+l#_LoKN-yeOjZ29W4qBGYU z>Y`3oSyzQL`SlZ2UfU7R?br2H%MVP27+Kpp+J=#K?1!h*+o8>=1N*Hs#uR~x4@G1CT)trymr*Fj|~+pkm_eaQ(^_2mPrWfaTb zdSc#?{WJC$TJZJH){&4*=8DkC@3l5a9Qa6pW&0_Cu8eQxgB=|gpMQUw{LAQ~N`6YJ z*9V#U`e@EapcL3iaXwKK$s@JLpwW?};c$K`Ga|!V zviTHIvK0{$-I0NpI??B0Vf_|Iyz06U)9nWudclOunr%$}bp6?vBVJ;Chw5Q&c?Ip2 zh^I=W#Zy?5^Iw{O;kef^@*jvAIt4>#XOKu3=HE;W<`1UEBGiur!IOQd+N1y?#nX>U zrZG;mtr5Y81kuu@U-G9tT}TuH)syUNPNI0a0qB&JdjJqgThKJMw8$ZL3MeE3sDOYX zfG{`)160Do93XmBg10A8)7Rab1cU-0ngK+TFO|Nchx)n15&TY(Jl#F0bQ1u?oJ#Vs z0%B=8bk8&bj06YLX(clNjV1b>XTtuG{ZGCOjzIknw#>n#-nSjdwe~Gm0rH&HdwJ0M zRAHPEB{_P2kr7&aJYR^b~pN)T&Tkz9z@gIqS(@wN#c0=C^9n_k>E&^P?Q#g~JVm?;zp$SB8E&*stxosLPP7 zK@itzeJFM)3(TK08sCtgm@WRM;KQk8jka9@uGQ9}#@KC^TX&K8x6-a9Ts-4FI&kXJ zomXqu2qSmoQYs`0xj1W5{2Vs+96I{;r*9lP6P%OQygSVEMOYGbQbl%@BKv+L*rz2r zOKw?Y`Xfc)KR`g~kAD4o+7RLhC4h?aAW-NdJWcqy zMgSmZJYA_C0d^SL-08DN(vX0_Dghn-HN-z~1*F5y3;?kpTl#v^=}aJfl7AU34Ejf| zco>kL`Cp&I{`8!dEbxZ`&8G>4Of)A^?I1LI6=FdOp*s8-F@Li9QL72Todhu^Of22n<_j!B-e9sjtyAu_#{6g$0AlP%^3^2L4+Fb@`GQ#6*aCe6y}keS1*Q2uP4IP>aib_` z>B#d(at7{;U^t&Ds_b(kZ zP4|aBO%F-4N27m!X!d9X?L(8(RG{>M!F6bN1kKlvkhHwDX?(hjwlW+FN5Eh(1PlX1 z!qM_j7#Io#%l@e!Arz7u01AX50W?oPFCZF)LZEfw3y#s!*4EU

TMlCWa2e04jk({V`HB6r}_Jfpm=Vfd2#Xj3~JP literal 0 HcmV?d00001 diff --git a/pythonProject/requirements.txt b/pythonProject/requirements.txt new file mode 100644 index 0000000..dafc337 --- /dev/null +++ b/pythonProject/requirements.txt @@ -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 diff --git a/pythonProject/sign.py b/pythonProject/sign.py new file mode 100644 index 0000000..4c1d293 --- /dev/null +++ b/pythonProject/sign.py @@ -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 + + + + + diff --git a/pythonProject/sign_individual.py b/pythonProject/sign_individual.py new file mode 100644 index 0000000..9e3c79c --- /dev/null +++ b/pythonProject/sign_individual.py @@ -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()) diff --git a/pythonProject/worker.py b/pythonProject/worker.py new file mode 100644 index 0000000..49f7d58 --- /dev/null +++ b/pythonProject/worker.py @@ -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()