[{"data":1,"prerenderedAt":1973},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fsign-a-pdf-report-digitally-with-python":3,"surround:\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fsign-a-pdf-report-digitally-with-python":1940,"ancestors:\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fsign-a-pdf-report-digitally-with-python":1949,"children:\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fsign-a-pdf-report-digitally-with-python":1954,"related:\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fsign-a-pdf-report-digitally-with-python":1955},{"id":4,"title":5,"body":6,"dateModified":1906,"datePublished":1906,"description":1907,"extension":1908,"faq":1909,"keyword":1918,"kind":1919,"meta":1920,"navigation":214,"path":1929,"seo":1930,"slug":1933,"stem":1934,"topics":1935,"type":1938,"__hash__":1939},"docs\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fsign-a-pdf-report-digitally-with-python\u002Findex.md","Sign a PDF Report Digitally with Python",{"type":7,"value":8,"toc":1894},"minimark",[9,19,84,89,108,119,123,546,552,556,739,747,907,911,1107,1110,1114,1423,1461,1487,1678,1743,1747,1761,1765,1803,1807,1813,1817,1835,1841,1847,1857,1861,1890],[10,11,12,13,18],"p",{},"A PDF of a monthly report is easy to change. Anyone who receives it can edit a figure with a PDF editor and pass it on, and the copy looks exactly like the original. For most internal reports that does not matter, but for reports that leave the organisation — statements to clients, figures for a regulator, board packs that are archived as the record of a decision — readers sometimes need to know two things: that the document really came from the finance team, and that nobody has altered it since. A digital signature answers both. It is a cryptographic seal over the bytes of the file, made with a private key that only the signer holds, which PDF readers check automatically and which breaks if a single byte of the signed content changes. This page signs a PDF exported from an Excel report with the pyHanko library, adds a visible signature box, and validates the result, including a tampered copy. It belongs to ",[14,15,17],"a",{"href":16},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002F","Exporting Excel Reports to PDF",".",[20,21,26],"pre",{"className":22,"code":23,"language":24,"meta":25,"style":25},"language-bash shiki shiki-themes github-light github-dark-high-contrast","soffice --headless --convert-to pdf monthly_report.xlsx > \u002Fdev\u002Fnull 2>&1 && ls -la monthly_report.pdf | awk '{print $5, $9}'\n","bash","",[27,28,29],"code",{"__ignoreMap":25},[30,31,34,38,42,45,49,52,56,59,62,66,69,72,75,78,81],"span",{"class":32,"line":33},"line",1,[30,35,37],{"class":36},"sMTad","soffice",[30,39,41],{"class":40},"sP0c6"," --headless",[30,43,44],{"class":40}," --convert-to",[30,46,48],{"class":47},"srMev"," pdf",[30,50,51],{"class":47}," monthly_report.xlsx",[30,53,55],{"class":54},"s-kum"," >",[30,57,58],{"class":47}," \u002Fdev\u002Fnull",[30,60,61],{"class":54}," 2>&1",[30,63,65],{"class":64},"skGVy"," && ",[30,67,68],{"class":36},"ls",[30,70,71],{"class":40}," -la",[30,73,74],{"class":47}," monthly_report.pdf",[30,76,77],{"class":54}," |",[30,79,80],{"class":36}," awk",[30,82,83],{"class":47}," '{print $5, $9}'\n",[85,86,88],"h2",{"id":87},"prerequisites","Prerequisites",[20,90,92],{"className":22,"code":91,"language":24,"meta":25,"style":25},"pip install pyhanko cryptography\n",[27,93,94],{"__ignoreMap":25},[30,95,96,99,102,105],{"class":32,"line":33},[30,97,98],{"class":36},"pip",[30,100,101],{"class":47}," install",[30,103,104],{"class":47}," pyhanko",[30,106,107],{"class":47}," cryptography\n",[10,109,110,111,114,115,118],{},"The PDF to sign can come from any export route — LibreOffice, Excel itself or a PDF library. The signature needs a certificate and its private key, normally delivered as a password-protected PKCS#12 file (",[27,112,113],{},".p12"," or ",[27,116,117],{},".pfx","). For production that certificate comes from a certificate authority or your organisation's internal one; for this example a self-signed certificate is created in step 1 so everything can run locally.",[85,120,122],{"id":121},"step-1-a-certificate-to-sign-with-test-only","Step 1 — A certificate to sign with (test only)",[20,124,128],{"className":125,"code":126,"language":127,"meta":25,"style":25},"language-python shiki shiki-themes github-light github-dark-high-contrast","from datetime import datetime, timedelta, timezone\nfrom cryptography import x509\nfrom cryptography.x509.oid import NameOID\nfrom cryptography.hazmat.primitives import hashes, serialization\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom cryptography.hazmat.primitives.serialization import pkcs12\n\nkey = rsa.generate_private_key(public_exponent=65537, key_size=3072)\nname = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, \"Finance Reporting\"),\n                  x509.NameAttribute(NameOID.ORGANIZATION_NAME, \"Example Ltd\")])\nnow = datetime.now(timezone.utc)\ncert = (x509.CertificateBuilder().subject_name(name).issuer_name(name).public_key(key.public_key())\n        .serial_number(x509.random_serial_number())\n        .not_valid_before(now - timedelta(minutes=5)).not_valid_after(now + timedelta(days=365))\n        .add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)\n        .sign(key, hashes.SHA256()))\nwith open(\"finance_signer.p12\", \"wb\") as f:\n    f.write(pkcs12.serialize_key_and_certificates(b\"finance\", key, cert, None,\n                                                  serialization.BestAvailableEncryption(b\"change-me\")))\nwith open(\"finance_signer.pem\", \"wb\") as f:\n    f.write(cert.public_bytes(serialization.Encoding.PEM))\nprint(cert.subject.rfc4514_string(), \"valid until\", f\"{cert.not_valid_after_utc:%Y-%m-%d}\")\n","python",[27,129,130,144,157,170,183,196,209,216,251,273,290,301,312,318,357,393,399,428,448,462,484,495],{"__ignoreMap":25},[30,131,132,135,138,141],{"class":32,"line":33},[30,133,134],{"class":54},"from",[30,136,137],{"class":64}," datetime ",[30,139,140],{"class":54},"import",[30,142,143],{"class":64}," datetime, timedelta, timezone\n",[30,145,147,149,152,154],{"class":32,"line":146},2,[30,148,134],{"class":54},[30,150,151],{"class":64}," cryptography ",[30,153,140],{"class":54},[30,155,156],{"class":64}," x509\n",[30,158,160,162,165,167],{"class":32,"line":159},3,[30,161,134],{"class":54},[30,163,164],{"class":64}," cryptography.x509.oid ",[30,166,140],{"class":54},[30,168,169],{"class":64}," NameOID\n",[30,171,173,175,178,180],{"class":32,"line":172},4,[30,174,134],{"class":54},[30,176,177],{"class":64}," cryptography.hazmat.primitives ",[30,179,140],{"class":54},[30,181,182],{"class":64}," hashes, serialization\n",[30,184,186,188,191,193],{"class":32,"line":185},5,[30,187,134],{"class":54},[30,189,190],{"class":64}," cryptography.hazmat.primitives.asymmetric ",[30,192,140],{"class":54},[30,194,195],{"class":64}," rsa\n",[30,197,199,201,204,206],{"class":32,"line":198},6,[30,200,134],{"class":54},[30,202,203],{"class":64}," cryptography.hazmat.primitives.serialization ",[30,205,140],{"class":54},[30,207,208],{"class":64}," pkcs12\n",[30,210,212],{"class":32,"line":211},7,[30,213,215],{"emptyLinePlaceholder":214},true,"\n",[30,217,219,222,225,228,232,234,237,240,243,245,248],{"class":32,"line":218},8,[30,220,221],{"class":64},"key ",[30,223,224],{"class":54},"=",[30,226,227],{"class":64}," rsa.generate_private_key(",[30,229,231],{"class":230},"sa561","public_exponent",[30,233,224],{"class":54},[30,235,236],{"class":40},"65537",[30,238,239],{"class":64},", ",[30,241,242],{"class":230},"key_size",[30,244,224],{"class":54},[30,246,247],{"class":40},"3072",[30,249,250],{"class":64},")\n",[30,252,254,257,259,262,265,267,270],{"class":32,"line":253},9,[30,255,256],{"class":64},"name ",[30,258,224],{"class":54},[30,260,261],{"class":64}," x509.Name([x509.NameAttribute(NameOID.",[30,263,264],{"class":40},"COMMON_NAME",[30,266,239],{"class":64},[30,268,269],{"class":47},"\"Finance Reporting\"",[30,271,272],{"class":64},"),\n",[30,274,276,279,282,284,287],{"class":32,"line":275},10,[30,277,278],{"class":64},"                  x509.NameAttribute(NameOID.",[30,280,281],{"class":40},"ORGANIZATION_NAME",[30,283,239],{"class":64},[30,285,286],{"class":47},"\"Example Ltd\"",[30,288,289],{"class":64},")])\n",[30,291,293,296,298],{"class":32,"line":292},11,[30,294,295],{"class":64},"now ",[30,297,224],{"class":54},[30,299,300],{"class":64}," datetime.now(timezone.utc)\n",[30,302,304,307,309],{"class":32,"line":303},12,[30,305,306],{"class":64},"cert ",[30,308,224],{"class":54},[30,310,311],{"class":64}," (x509.CertificateBuilder().subject_name(name).issuer_name(name).public_key(key.public_key())\n",[30,313,315],{"class":32,"line":314},13,[30,316,317],{"class":64},"        .serial_number(x509.random_serial_number())\n",[30,319,321,324,327,330,333,335,338,341,344,346,349,351,354],{"class":32,"line":320},14,[30,322,323],{"class":64},"        .not_valid_before(now ",[30,325,326],{"class":54},"-",[30,328,329],{"class":64}," timedelta(",[30,331,332],{"class":230},"minutes",[30,334,224],{"class":54},[30,336,337],{"class":40},"5",[30,339,340],{"class":64},")).not_valid_after(now ",[30,342,343],{"class":54},"+",[30,345,329],{"class":64},[30,347,348],{"class":230},"days",[30,350,224],{"class":54},[30,352,353],{"class":40},"365",[30,355,356],{"class":64},"))\n",[30,358,360,363,366,368,371,373,376,378,381,384,387,389,391],{"class":32,"line":359},15,[30,361,362],{"class":64},"        .add_extension(x509.BasicConstraints(",[30,364,365],{"class":230},"ca",[30,367,224],{"class":54},[30,369,370],{"class":40},"True",[30,372,239],{"class":64},[30,374,375],{"class":230},"path_length",[30,377,224],{"class":54},[30,379,380],{"class":40},"None",[30,382,383],{"class":64},"), ",[30,385,386],{"class":230},"critical",[30,388,224],{"class":54},[30,390,370],{"class":40},[30,392,250],{"class":64},[30,394,396],{"class":32,"line":395},16,[30,397,398],{"class":64},"        .sign(key, hashes.SHA256()))\n",[30,400,402,405,408,411,414,416,419,422,425],{"class":32,"line":401},17,[30,403,404],{"class":54},"with",[30,406,407],{"class":40}," open",[30,409,410],{"class":64},"(",[30,412,413],{"class":47},"\"finance_signer.p12\"",[30,415,239],{"class":64},[30,417,418],{"class":47},"\"wb\"",[30,420,421],{"class":64},") ",[30,423,424],{"class":54},"as",[30,426,427],{"class":64}," f:\n",[30,429,431,434,437,440,443,445],{"class":32,"line":430},18,[30,432,433],{"class":64},"    f.write(pkcs12.serialize_key_and_certificates(",[30,435,436],{"class":54},"b",[30,438,439],{"class":47},"\"finance\"",[30,441,442],{"class":64},", key, cert, ",[30,444,380],{"class":40},[30,446,447],{"class":64},",\n",[30,449,451,454,456,459],{"class":32,"line":450},19,[30,452,453],{"class":64},"                                                  serialization.BestAvailableEncryption(",[30,455,436],{"class":54},[30,457,458],{"class":47},"\"change-me\"",[30,460,461],{"class":64},")))\n",[30,463,465,467,469,471,474,476,478,480,482],{"class":32,"line":464},20,[30,466,404],{"class":54},[30,468,407],{"class":40},[30,470,410],{"class":64},[30,472,473],{"class":47},"\"finance_signer.pem\"",[30,475,239],{"class":64},[30,477,418],{"class":47},[30,479,421],{"class":64},[30,481,424],{"class":54},[30,483,427],{"class":64},[30,485,487,490,493],{"class":32,"line":486},21,[30,488,489],{"class":64},"    f.write(cert.public_bytes(serialization.Encoding.",[30,491,492],{"class":40},"PEM",[30,494,356],{"class":64},[30,496,498,501,504,507,509,512,515,519,522,525,528,531,534,536,539,542,544],{"class":32,"line":497},22,[30,499,500],{"class":40},"print",[30,502,503],{"class":64},"(cert.subject.rfc4514_string(), ",[30,505,506],{"class":47},"\"valid until\"",[30,508,239],{"class":64},[30,510,511],{"class":54},"f",[30,513,514],{"class":47},"\"",[30,516,518],{"class":517},"sSjpA","{",[30,520,521],{"class":64},"cert.not_valid_after_utc:",[30,523,524],{"class":54},"%",[30,526,527],{"class":64},"Y",[30,529,530],{"class":54},"-%",[30,532,533],{"class":64},"m",[30,535,530],{"class":54},[30,537,538],{"class":64},"d",[30,540,541],{"class":517},"}",[30,543,514],{"class":47},[30,545,250],{"class":64},[10,547,548,549,551],{},"A self-signed certificate is enough to see how signing works, but readers' software will not trust it: Acrobat will show the signature as made by an unknown identity. For reports that go to outside readers, use a certificate from a provider on the Adobe Approved Trust List, which Acrobat trusts out of the box; inside an organisation, a certificate from the company's own certificate authority works when that authority is installed on readers' machines. Either way, the ",[27,550,113],{}," file holds the private key — treat it like a password, store it in the same secret store as other credentials, and never commit it to version control.",[85,553,555],{"id":554},"step-2-sign-the-pdf","Step 2 — Sign the PDF",[20,557,559],{"className":125,"code":558,"language":127,"meta":25,"style":25},"from pyhanko.sign import signers\nfrom pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter\n\nsigner = signers.SimpleSigner.load_pkcs12(\"finance_signer.p12\", passphrase=b\"change-me\")\nmeta = signers.PdfSignatureMetadata(field_name=\"FinanceSignature\", reason=\"Issued by Finance — September 2026 figures\",\n                                    location=\"London\", name=\"Finance Reporting\")\nwith open(\"monthly_report.pdf\", \"rb\") as src, open(\"monthly_report_signed.pdf\", \"wb\") as out:\n    signers.sign_pdf(IncrementalPdfFileWriter(src), meta, signer=signer, output=out)\nprint(\"signed\")\n",[27,560,561,573,585,589,614,644,665,707,728],{"__ignoreMap":25},[30,562,563,565,568,570],{"class":32,"line":33},[30,564,134],{"class":54},[30,566,567],{"class":64}," pyhanko.sign ",[30,569,140],{"class":54},[30,571,572],{"class":64}," signers\n",[30,574,575,577,580,582],{"class":32,"line":146},[30,576,134],{"class":54},[30,578,579],{"class":64}," pyhanko.pdf_utils.incremental_writer ",[30,581,140],{"class":54},[30,583,584],{"class":64}," IncrementalPdfFileWriter\n",[30,586,587],{"class":32,"line":159},[30,588,215],{"emptyLinePlaceholder":214},[30,590,591,594,596,599,601,603,606,608,610,612],{"class":32,"line":172},[30,592,593],{"class":64},"signer ",[30,595,224],{"class":54},[30,597,598],{"class":64}," signers.SimpleSigner.load_pkcs12(",[30,600,413],{"class":47},[30,602,239],{"class":64},[30,604,605],{"class":230},"passphrase",[30,607,224],{"class":54},[30,609,436],{"class":54},[30,611,458],{"class":47},[30,613,250],{"class":64},[30,615,616,619,621,624,627,629,632,634,637,639,642],{"class":32,"line":185},[30,617,618],{"class":64},"meta ",[30,620,224],{"class":54},[30,622,623],{"class":64}," signers.PdfSignatureMetadata(",[30,625,626],{"class":230},"field_name",[30,628,224],{"class":54},[30,630,631],{"class":47},"\"FinanceSignature\"",[30,633,239],{"class":64},[30,635,636],{"class":230},"reason",[30,638,224],{"class":54},[30,640,641],{"class":47},"\"Issued by Finance — September 2026 figures\"",[30,643,447],{"class":64},[30,645,646,649,651,654,656,659,661,663],{"class":32,"line":198},[30,647,648],{"class":230},"                                    location",[30,650,224],{"class":54},[30,652,653],{"class":47},"\"London\"",[30,655,239],{"class":64},[30,657,658],{"class":230},"name",[30,660,224],{"class":54},[30,662,269],{"class":47},[30,664,250],{"class":64},[30,666,667,669,671,673,676,678,681,683,685,688,691,693,696,698,700,702,704],{"class":32,"line":211},[30,668,404],{"class":54},[30,670,407],{"class":40},[30,672,410],{"class":64},[30,674,675],{"class":47},"\"monthly_report.pdf\"",[30,677,239],{"class":64},[30,679,680],{"class":47},"\"rb\"",[30,682,421],{"class":64},[30,684,424],{"class":54},[30,686,687],{"class":64}," src, ",[30,689,690],{"class":40},"open",[30,692,410],{"class":64},[30,694,695],{"class":47},"\"monthly_report_signed.pdf\"",[30,697,239],{"class":64},[30,699,418],{"class":47},[30,701,421],{"class":64},[30,703,424],{"class":54},[30,705,706],{"class":64}," out:\n",[30,708,709,712,715,717,720,723,725],{"class":32,"line":218},[30,710,711],{"class":64},"    signers.sign_pdf(IncrementalPdfFileWriter(src), meta, ",[30,713,714],{"class":230},"signer",[30,716,224],{"class":54},[30,718,719],{"class":64},"signer, ",[30,721,722],{"class":230},"output",[30,724,224],{"class":54},[30,726,727],{"class":64},"out)\n",[30,729,730,732,734,737],{"class":32,"line":253},[30,731,500],{"class":40},[30,733,410],{"class":64},[30,735,736],{"class":47},"\"signed\"",[30,738,250],{"class":64},[10,740,741,742,746],{},"pyHanko signs by appending to the PDF rather than rewriting it: the original bytes stay exactly as they were, and an ",[743,744,745],"em",{},"incremental update"," adds a signature field and the signature value. The signature covers every byte of the file except the space reserved for itself, so any later change to the content is detectable. The reason and location are shown by PDF readers in the signature panel and are a good place to say what the signature means — \"issued by\", \"approved by\", \"figures as at\" — because a signature on its own proves who signed and that the file is unchanged, not what the signer intended.",[748,749,757,758,757,762,757,766,757,774,757,784,757,791,757,796,757,800,757,806,757,811,757,815,757,821,757,825,757,829,757,833,757,837,757,840,757,845,757,850,757,852,757,855,757,858,757,862,757,865,757,870,757,875,757,878,757,881,757,884,757,888,757,891,757,894,757,898,757,901,757,904],"svg",{"viewBox":750,"role":751,"ariaLabelledBy":752,"xmlns":755,"style":756},"4 0 752 126","img",[753,754],"sig-how-t","sig-how-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[759,760,761],"title",{"id":753},"What signing adds to the file",[763,764,765],"desc",{"id":754},"The original PDF bytes are left untouched. An incremental update appends a signature field, and a hash of all the file's bytes except the signature itself is signed with the private key. Readers recompute the hash and check it with the certificate's public key.",[767,768],"rect",{"x":769,"y":770,"width":771,"height":772,"fill":773},"4","0","752","126","#ffffff",[767,775],{"x":776,"y":777,"width":778,"height":779,"rx":780,"fill":781,"stroke":782,"style":783},"20.0","16.0","115.2","94.0","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[785,786,790],"text",{"x":787,"y":788,"style":789},"77.6","50.2","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Original PDF",[785,792,795],{"x":787,"y":793,"style":794},"69.8","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","bytes",[785,797,799],{"x":787,"y":798,"style":794},"87.2","unchanged",[32,801],{"x1":802,"y1":803,"x2":804,"y2":803,"stroke":805,"style":783},"140.2","63.0","162.2","var(--brand,#5b5cf0)",[807,808],"polygon",{"points":809,"fill":810},"166.2,63.0 157.2,68.0 157.2,58.0","#5b5cf0",[767,812],{"x":813,"y":777,"width":778,"height":779,"rx":780,"fill":814,"stroke":805,"style":783},"171.2","#f0f4ff",[785,816,820],{"x":817,"y":818,"style":819},"228.8","41.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Incremental",[785,822,824],{"x":817,"y":823,"style":819},"59.0","update",[785,826,828],{"x":817,"y":827,"style":794},"78.5","signature field",[785,830,832],{"x":817,"y":831,"style":794},"96.0","added",[32,834],{"x1":835,"y1":803,"x2":836,"y2":803,"stroke":805,"style":783},"291.4","313.4",[807,838],{"points":839,"fill":810},"317.4,63.0 308.4,68.0 308.4,58.0",[767,841],{"x":842,"y":777,"width":778,"height":779,"rx":780,"fill":843,"stroke":844,"style":783},"322.4","#fdefd8","var(--gold,#b4740a)",[785,846,849],{"x":847,"y":818,"style":848},"380.0","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Hash of the",[785,851,795],{"x":847,"y":823,"style":848},[785,853,854],{"x":847,"y":827,"style":794},"all but the",[785,856,857],{"x":847,"y":831,"style":794},"signature",[32,859],{"x1":860,"y1":803,"x2":861,"y2":803,"stroke":805,"style":783},"442.6","464.6",[807,863],{"points":864,"fill":810},"468.6,63.0 459.6,68.0 459.6,58.0",[767,866],{"x":867,"y":777,"width":778,"height":779,"rx":780,"fill":868,"stroke":869,"style":783},"473.6","#d9f4f1","var(--teal,#0f9488)",[785,871,874],{"x":872,"y":818,"style":873},"531.2","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Signed with",[785,876,877],{"x":872,"y":823,"style":873},"private key",[785,879,880],{"x":872,"y":827,"style":794},"from the .p12",[785,882,883],{"x":872,"y":831,"style":794},"file",[32,885],{"x1":886,"y1":803,"x2":887,"y2":803,"stroke":805,"style":783},"593.8","615.8",[807,889],{"points":890,"fill":810},"619.8,63.0 610.8,68.0 610.8,58.0",[767,892],{"x":893,"y":777,"width":778,"height":779,"rx":780,"fill":781,"stroke":782,"style":783},"624.8",[785,895,897],{"x":896,"y":818,"style":789},"682.4","Reader",[785,899,900],{"x":896,"y":823,"style":789},"checks",[785,902,903],{"x":896,"y":827,"style":794},"public key +",[785,905,906],{"x":896,"y":831,"style":794},"hash",[85,908,910],{"id":909},"step-3-add-a-visible-signature-box","Step 3 — Add a visible signature box",[20,912,914],{"className":125,"code":913,"language":127,"meta":25,"style":25},"from pyhanko import stamp\nfrom pyhanko.sign import fields\n\nwith open(\"monthly_report.pdf\", \"rb\") as src, open(\"monthly_report_signed_visible.pdf\", \"wb\") as out:\n    w = IncrementalPdfFileWriter(src)\n    fields.append_signature_field(w, sig_field_spec=fields.SigFieldSpec(\"FinanceSignature\", on_page=0, box=(360, 40, 560, 95)))\n    pdf_signer = signers.PdfSigner(meta, signer=signer,\n                                   stamp_style=stamp.TextStampStyle(stamp_text=\"Signed by %(signer)s\\n%(ts)s\"))\n    pdf_signer.sign_pdf(w, output=out)\nprint(\"signed with a visible box\")\n",[27,915,916,928,939,943,980,990,1043,1060,1085,1096],{"__ignoreMap":25},[30,917,918,920,923,925],{"class":32,"line":33},[30,919,134],{"class":54},[30,921,922],{"class":64}," pyhanko ",[30,924,140],{"class":54},[30,926,927],{"class":64}," stamp\n",[30,929,930,932,934,936],{"class":32,"line":146},[30,931,134],{"class":54},[30,933,567],{"class":64},[30,935,140],{"class":54},[30,937,938],{"class":64}," fields\n",[30,940,941],{"class":32,"line":159},[30,942,215],{"emptyLinePlaceholder":214},[30,944,945,947,949,951,953,955,957,959,961,963,965,967,970,972,974,976,978],{"class":32,"line":172},[30,946,404],{"class":54},[30,948,407],{"class":40},[30,950,410],{"class":64},[30,952,675],{"class":47},[30,954,239],{"class":64},[30,956,680],{"class":47},[30,958,421],{"class":64},[30,960,424],{"class":54},[30,962,687],{"class":64},[30,964,690],{"class":40},[30,966,410],{"class":64},[30,968,969],{"class":47},"\"monthly_report_signed_visible.pdf\"",[30,971,239],{"class":64},[30,973,418],{"class":47},[30,975,421],{"class":64},[30,977,424],{"class":54},[30,979,706],{"class":64},[30,981,982,985,987],{"class":32,"line":185},[30,983,984],{"class":64},"    w ",[30,986,224],{"class":54},[30,988,989],{"class":64}," IncrementalPdfFileWriter(src)\n",[30,991,992,995,998,1000,1003,1005,1007,1010,1012,1014,1016,1019,1021,1023,1026,1028,1031,1033,1036,1038,1041],{"class":32,"line":198},[30,993,994],{"class":64},"    fields.append_signature_field(w, ",[30,996,997],{"class":230},"sig_field_spec",[30,999,224],{"class":54},[30,1001,1002],{"class":64},"fields.SigFieldSpec(",[30,1004,631],{"class":47},[30,1006,239],{"class":64},[30,1008,1009],{"class":230},"on_page",[30,1011,224],{"class":54},[30,1013,770],{"class":40},[30,1015,239],{"class":64},[30,1017,1018],{"class":230},"box",[30,1020,224],{"class":54},[30,1022,410],{"class":64},[30,1024,1025],{"class":40},"360",[30,1027,239],{"class":64},[30,1029,1030],{"class":40},"40",[30,1032,239],{"class":64},[30,1034,1035],{"class":40},"560",[30,1037,239],{"class":64},[30,1039,1040],{"class":40},"95",[30,1042,461],{"class":64},[30,1044,1045,1048,1050,1053,1055,1057],{"class":32,"line":211},[30,1046,1047],{"class":64},"    pdf_signer ",[30,1049,224],{"class":54},[30,1051,1052],{"class":64}," signers.PdfSigner(meta, ",[30,1054,714],{"class":230},[30,1056,224],{"class":54},[30,1058,1059],{"class":64},"signer,\n",[30,1061,1062,1065,1067,1070,1073,1075,1078,1081,1083],{"class":32,"line":218},[30,1063,1064],{"class":230},"                                   stamp_style",[30,1066,224],{"class":54},[30,1068,1069],{"class":64},"stamp.TextStampStyle(",[30,1071,1072],{"class":230},"stamp_text",[30,1074,224],{"class":54},[30,1076,1077],{"class":47},"\"Signed by ",[30,1079,1080],{"class":517},"%(signer)s\\n%(ts)s",[30,1082,514],{"class":47},[30,1084,356],{"class":64},[30,1086,1087,1090,1092,1094],{"class":32,"line":253},[30,1088,1089],{"class":64},"    pdf_signer.sign_pdf(w, ",[30,1091,722],{"class":230},[30,1093,224],{"class":54},[30,1095,727],{"class":64},[30,1097,1098,1100,1102,1105],{"class":32,"line":275},[30,1099,500],{"class":40},[30,1101,410],{"class":64},[30,1103,1104],{"class":47},"\"signed with a visible box\"",[30,1106,250],{"class":64},[10,1108,1109],{},"An invisible signature is checked by the reader and listed in its signature panel, but nothing appears on the page. A visible one also draws a box — here at the bottom right of the first page, with the coordinates in points from the lower-left corner — containing the signer's name and the signing time. For printed copies this is the only sign that the document was signed, though of course a printed signature box proves nothing on its own; the cryptographic check only works on the file. Place the box where the report has empty space, typically the foot of the cover page or the last page.",[85,1111,1113],{"id":1112},"step-4-validate-the-signature-and-break-it","Step 4 — Validate the signature, and break it",[20,1115,1117],{"className":125,"code":1116,"language":127,"meta":25,"style":25},"from pyhanko.keys import load_cert_from_pemder\nfrom pyhanko.pdf_utils.reader import PdfFileReader\nfrom pyhanko.sign.validation import validate_pdf_signature\nfrom pyhanko_certvalidator import ValidationContext\n\ntrust = ValidationContext(trust_roots=[load_cert_from_pemder(\"finance_signer.pem\")])\n\ndef check(path):\n    with open(path, \"rb\") as f:\n        sig = PdfFileReader(f).embedded_signatures[0]\n        status = validate_pdf_signature(sig, trust)\n        return f\"{path}: intact={status.intact} valid={status.valid} -> {status.summary()}\"\n\nprint(check(\"monthly_report_signed.pdf\"))\n\nfrom pathlib import Path\n\ndata = bytearray(Path(\"monthly_report_signed.pdf\").read_bytes())\ni = data.find(b\"\u002FProducer\")\ndata[i + 1] = ord(\"p\")                             # change one byte inside the signed range\nPath(\"monthly_report_tampered.pdf\").write_bytes(data)\nprint(check(\"monthly_report_tampered.pdf\"))\n",[27,1118,1119,1131,1143,1155,1167,1171,1193,1197,1209,1227,1242,1252,1302,1306,1317,1321,1333,1337,1355,1372,1402,1413],{"__ignoreMap":25},[30,1120,1121,1123,1126,1128],{"class":32,"line":33},[30,1122,134],{"class":54},[30,1124,1125],{"class":64}," pyhanko.keys ",[30,1127,140],{"class":54},[30,1129,1130],{"class":64}," load_cert_from_pemder\n",[30,1132,1133,1135,1138,1140],{"class":32,"line":146},[30,1134,134],{"class":54},[30,1136,1137],{"class":64}," pyhanko.pdf_utils.reader ",[30,1139,140],{"class":54},[30,1141,1142],{"class":64}," PdfFileReader\n",[30,1144,1145,1147,1150,1152],{"class":32,"line":159},[30,1146,134],{"class":54},[30,1148,1149],{"class":64}," pyhanko.sign.validation ",[30,1151,140],{"class":54},[30,1153,1154],{"class":64}," validate_pdf_signature\n",[30,1156,1157,1159,1162,1164],{"class":32,"line":172},[30,1158,134],{"class":54},[30,1160,1161],{"class":64}," pyhanko_certvalidator ",[30,1163,140],{"class":54},[30,1165,1166],{"class":64}," ValidationContext\n",[30,1168,1169],{"class":32,"line":185},[30,1170,215],{"emptyLinePlaceholder":214},[30,1172,1173,1176,1178,1181,1184,1186,1189,1191],{"class":32,"line":198},[30,1174,1175],{"class":64},"trust ",[30,1177,224],{"class":54},[30,1179,1180],{"class":64}," ValidationContext(",[30,1182,1183],{"class":230},"trust_roots",[30,1185,224],{"class":54},[30,1187,1188],{"class":64},"[load_cert_from_pemder(",[30,1190,473],{"class":47},[30,1192,289],{"class":64},[30,1194,1195],{"class":32,"line":211},[30,1196,215],{"emptyLinePlaceholder":214},[30,1198,1199,1202,1206],{"class":32,"line":218},[30,1200,1201],{"class":54},"def",[30,1203,1205],{"class":1204},"s_Opv"," check",[30,1207,1208],{"class":64},"(path):\n",[30,1210,1211,1214,1216,1219,1221,1223,1225],{"class":32,"line":253},[30,1212,1213],{"class":54},"    with",[30,1215,407],{"class":40},[30,1217,1218],{"class":64},"(path, ",[30,1220,680],{"class":47},[30,1222,421],{"class":64},[30,1224,424],{"class":54},[30,1226,427],{"class":64},[30,1228,1229,1232,1234,1237,1239],{"class":32,"line":275},[30,1230,1231],{"class":64},"        sig ",[30,1233,224],{"class":54},[30,1235,1236],{"class":64}," PdfFileReader(f).embedded_signatures[",[30,1238,770],{"class":40},[30,1240,1241],{"class":64},"]\n",[30,1243,1244,1247,1249],{"class":32,"line":292},[30,1245,1246],{"class":64},"        status ",[30,1248,224],{"class":54},[30,1250,1251],{"class":64}," validate_pdf_signature(sig, trust)\n",[30,1253,1254,1257,1260,1262,1264,1267,1269,1272,1274,1277,1279,1282,1284,1287,1289,1292,1294,1297,1299],{"class":32,"line":303},[30,1255,1256],{"class":54},"        return",[30,1258,1259],{"class":54}," f",[30,1261,514],{"class":47},[30,1263,518],{"class":517},[30,1265,1266],{"class":64},"path",[30,1268,541],{"class":517},[30,1270,1271],{"class":47},": intact=",[30,1273,518],{"class":517},[30,1275,1276],{"class":64},"status.intact",[30,1278,541],{"class":517},[30,1280,1281],{"class":47}," valid=",[30,1283,518],{"class":517},[30,1285,1286],{"class":64},"status.valid",[30,1288,541],{"class":517},[30,1290,1291],{"class":47}," -> ",[30,1293,518],{"class":517},[30,1295,1296],{"class":64},"status.summary()",[30,1298,541],{"class":517},[30,1300,1301],{"class":47},"\"\n",[30,1303,1304],{"class":32,"line":314},[30,1305,215],{"emptyLinePlaceholder":214},[30,1307,1308,1310,1313,1315],{"class":32,"line":320},[30,1309,500],{"class":40},[30,1311,1312],{"class":64},"(check(",[30,1314,695],{"class":47},[30,1316,356],{"class":64},[30,1318,1319],{"class":32,"line":359},[30,1320,215],{"emptyLinePlaceholder":214},[30,1322,1323,1325,1328,1330],{"class":32,"line":395},[30,1324,134],{"class":54},[30,1326,1327],{"class":64}," pathlib ",[30,1329,140],{"class":54},[30,1331,1332],{"class":64}," Path\n",[30,1334,1335],{"class":32,"line":401},[30,1336,215],{"emptyLinePlaceholder":214},[30,1338,1339,1342,1344,1347,1350,1352],{"class":32,"line":430},[30,1340,1341],{"class":64},"data ",[30,1343,224],{"class":54},[30,1345,1346],{"class":40}," bytearray",[30,1348,1349],{"class":64},"(Path(",[30,1351,695],{"class":47},[30,1353,1354],{"class":64},").read_bytes())\n",[30,1356,1357,1360,1362,1365,1367,1370],{"class":32,"line":450},[30,1358,1359],{"class":64},"i ",[30,1361,224],{"class":54},[30,1363,1364],{"class":64}," data.find(",[30,1366,436],{"class":54},[30,1368,1369],{"class":47},"\"\u002FProducer\"",[30,1371,250],{"class":64},[30,1373,1374,1377,1379,1382,1385,1387,1390,1392,1395,1398],{"class":32,"line":464},[30,1375,1376],{"class":64},"data[i ",[30,1378,343],{"class":54},[30,1380,1381],{"class":40}," 1",[30,1383,1384],{"class":64},"] ",[30,1386,224],{"class":54},[30,1388,1389],{"class":40}," ord",[30,1391,410],{"class":64},[30,1393,1394],{"class":47},"\"p\"",[30,1396,1397],{"class":64},")                             ",[30,1399,1401],{"class":1400},"s-wDw","# change one byte inside the signed range\n",[30,1403,1404,1407,1410],{"class":32,"line":486},[30,1405,1406],{"class":64},"Path(",[30,1408,1409],{"class":47},"\"monthly_report_tampered.pdf\"",[30,1411,1412],{"class":64},").write_bytes(data)\n",[30,1414,1415,1417,1419,1421],{"class":32,"line":497},[30,1416,500],{"class":40},[30,1418,1312],{"class":64},[30,1420,1409],{"class":47},[30,1422,356],{"class":64},[20,1424,1426],{"className":22,"code":1425,"language":24,"meta":25,"style":25},"pdfsig monthly_report_signed.pdf | grep -E \"Common Name|Signature Validation\"\npdfsig monthly_report_tampered.pdf | grep \"Signature Validation\"\n",[27,1427,1428,1447],{"__ignoreMap":25},[30,1429,1430,1433,1436,1438,1441,1444],{"class":32,"line":33},[30,1431,1432],{"class":36},"pdfsig",[30,1434,1435],{"class":47}," monthly_report_signed.pdf",[30,1437,77],{"class":54},[30,1439,1440],{"class":36}," grep",[30,1442,1443],{"class":40}," -E",[30,1445,1446],{"class":47}," \"Common Name|Signature Validation\"\n",[30,1448,1449,1451,1454,1456,1458],{"class":32,"line":146},[30,1450,1432],{"class":36},[30,1452,1453],{"class":47}," monthly_report_tampered.pdf",[30,1455,77],{"class":54},[30,1457,1440],{"class":36},[30,1459,1460],{"class":47}," \"Signature Validation\"\n",[10,1462,1463,1464,1467,1468,1471,1472,1475,1476,1479,1480,1482,1483,1486],{},"Validation answers three separate questions. ",[743,1465,1466],{},"Intact",": do the signed bytes still match the hash in the signature? ",[743,1469,1470],{},"Valid",": was that hash signed by the key belonging to the certificate? ",[743,1473,1474],{},"Trusted",": does the certificate lead back to a root the checker trusts? The signed file passes all three here, because the test certificate was given to the validator as a trust root. Changing a single byte — one letter in the document's metadata — makes the hash no longer match: pyHanko reports the signature as not intact and the overall result as ",[27,1477,1478],{},"INVALID",", and Poppler's ",[27,1481,1432],{}," tool reports a digest mismatch. Note that ",[27,1484,1485],{},"valid"," stays true for the tampered copy. That is not a contradiction: the signature is still a genuine signature over the hash that was recorded at signing time, it is just that the file no longer matches that hash. This is why the summary, not any single flag, is what a job should check. Running the same check without the trust root reports the signature as intact but untrusted, which is exactly what a reader who does not know the self-signed certificate will see.",[748,1488,757,1494,757,1497,757,1500,757,1505,757,1511,757,1515,757,1521,757,1525,757,1529,757,1533,757,1537,757,1541,757,1545,757,1549,757,1553,757,1557,757,1561,757,1564,757,1568,757,1570,757,1572,757,1574,757,1576,757,1578,757,1580,757,1582,757,1585,757,1588,757,1592,757,1594,757,1598,757,1600,757,1603,757,1605,757,1607,757,1609,757,1611,757,1613,757,1617,757,1619,757,1623,757,1625,757,1628,757,1630,757,1633,757,1635,757,1637,757,1639,757,1642,757,1646,757,1649,757,1652,757,1655,757,1657,757,1660,757,1662,757,1664,757,1666,757,1668,757,1670,757,1672,757,1674],{"viewBox":1489,"role":751,"ariaLabelledBy":1490,"xmlns":755,"style":1493},"58 0 643 168",[1491,1492],"sig-results-t","sig-results-d","width:100%;max-width:643px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[759,1495,1496],{"id":1491},"Validation results",[763,1498,1499],{"id":1492},"The signed file is intact, valid and trusted. After a one-byte change, the hash no longer matches, so the signature is reported as not intact and the overall result is invalid. Checked without the trust root, the unchanged file is intact but untrusted.",[767,1501],{"x":1502,"y":770,"width":1503,"height":1504,"fill":773},"58","643","168",[767,1506],{"x":1507,"y":777,"width":1508,"height":1509,"rx":770,"fill":781,"stroke":782,"style":1510},"74.8","32.0","24.0","stroke-width:1px",[767,1512],{"x":1513,"y":777,"width":1514,"height":1509,"rx":770,"fill":781,"stroke":782,"style":1510},"106.8","150.1",[785,1516,1520],{"x":1517,"y":1518,"style":1519},"181.9","33.0","font-size:11px;font-weight:600;fill:var(--muted,#5b6780);text-anchor:middle","A",[767,1522],{"x":1523,"y":777,"width":1524,"height":1509,"rx":770,"fill":781,"stroke":782,"style":1510},"256.9","59.3",[785,1526,1528],{"x":1527,"y":1518,"style":1519},"286.6","B",[767,1530],{"x":1531,"y":777,"width":1532,"height":1509,"rx":770,"fill":781,"stroke":782,"style":1510},"316.3","55.5",[785,1534,1536],{"x":1535,"y":1518,"style":1519},"344.0","C",[767,1538],{"x":1539,"y":777,"width":1540,"height":1509,"rx":770,"fill":781,"stroke":782,"style":1510},"371.8","72.5",[785,1542,1544],{"x":1543,"y":1518,"style":1519},"408.0","D",[767,1546],{"x":1547,"y":777,"width":1548,"height":1509,"rx":770,"fill":781,"stroke":782,"style":1510},"444.3","240.9",[785,1550,1552],{"x":1551,"y":1518,"style":1519},"564.7","E",[767,1554],{"x":1507,"y":1555,"width":1508,"height":1556,"rx":770,"fill":781,"stroke":782,"style":1510},"40.0","28.0",[785,1558,1560],{"x":1559,"y":823,"style":1519},"90.8","1",[767,1562],{"x":1513,"y":1555,"width":1514,"height":1556,"rx":770,"fill":814,"stroke":805,"style":1563},"stroke-width:1.5px",[785,1565,1567],{"x":1517,"y":823,"style":1566},"font-size:12px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","File",[767,1569],{"x":1523,"y":1555,"width":1524,"height":1556,"rx":770,"fill":814,"stroke":805,"style":1563},[785,1571,1466],{"x":1527,"y":823,"style":1566},[767,1573],{"x":1531,"y":1555,"width":1532,"height":1556,"rx":770,"fill":814,"stroke":805,"style":1563},[785,1575,1470],{"x":1535,"y":823,"style":1566},[767,1577],{"x":1539,"y":1555,"width":1540,"height":1556,"rx":770,"fill":814,"stroke":805,"style":1563},[785,1579,1474],{"x":1543,"y":823,"style":1566},[767,1581],{"x":1547,"y":1555,"width":1548,"height":1556,"rx":770,"fill":814,"stroke":805,"style":1563},[785,1583,1584],{"x":1551,"y":823,"style":1566},"Summary",[767,1586],{"x":1507,"y":1587,"width":1508,"height":1556,"rx":770,"fill":781,"stroke":782,"style":1510},"68.0",[785,1589,1591],{"x":1559,"y":1590,"style":1519},"87.0","2",[767,1593],{"x":1513,"y":1587,"width":1514,"height":1556,"rx":770,"fill":773,"stroke":782,"style":1510},[785,1595,1597],{"x":1517,"y":1590,"style":1596},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Signed",[767,1599],{"x":1523,"y":1587,"width":1524,"height":1556,"rx":770,"fill":773,"stroke":782,"style":1510},[785,1601,1602],{"x":1527,"y":1590,"style":1596},"yes",[767,1604],{"x":1531,"y":1587,"width":1532,"height":1556,"rx":770,"fill":773,"stroke":782,"style":1510},[785,1606,1602],{"x":1535,"y":1590,"style":1596},[767,1608],{"x":1539,"y":1587,"width":1540,"height":1556,"rx":770,"fill":773,"stroke":782,"style":1510},[785,1610,1602],{"x":1543,"y":1590,"style":1596},[767,1612],{"x":1547,"y":1587,"width":1548,"height":1556,"rx":770,"fill":868,"stroke":869,"style":1563},[785,1614,1616],{"x":1551,"y":1590,"style":1615},"font-size:12px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","INTACT:TRUSTED,UNTOUCHED",[767,1618],{"x":1507,"y":831,"width":1508,"height":1556,"rx":770,"fill":781,"stroke":782,"style":1510},[785,1620,1622],{"x":1559,"y":1621,"style":1519},"115.0","3",[767,1624],{"x":1513,"y":831,"width":1514,"height":1556,"rx":770,"fill":773,"stroke":782,"style":1510},[785,1626,1627],{"x":1517,"y":1621,"style":1596},"One byte changed",[767,1629],{"x":1523,"y":831,"width":1524,"height":1556,"rx":770,"fill":773,"stroke":782,"style":1510},[785,1631,1632],{"x":1527,"y":1621,"style":1596},"no",[767,1634],{"x":1531,"y":831,"width":1532,"height":1556,"rx":770,"fill":773,"stroke":782,"style":1510},[785,1636,1602],{"x":1535,"y":1621,"style":1596},[767,1638],{"x":1539,"y":831,"width":1540,"height":1556,"rx":770,"fill":773,"stroke":782,"style":1510},[785,1640,1641],{"x":1543,"y":1621,"style":1596},"—",[767,1643],{"x":1547,"y":831,"width":1548,"height":1556,"rx":770,"fill":1644,"stroke":1645,"style":1563},"#fce9e9","var(--danger,#dc2626)",[785,1647,1478],{"x":1551,"y":1621,"style":1648},"font-size:12px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle",[767,1650],{"x":1507,"y":1651,"width":1508,"height":1556,"rx":770,"fill":781,"stroke":782,"style":1510},"124.0",[785,1653,769],{"x":1559,"y":1654,"style":1519},"143.0",[767,1656],{"x":1513,"y":1651,"width":1514,"height":1556,"rx":770,"fill":773,"stroke":782,"style":1510},[785,1658,1659],{"x":1517,"y":1654,"style":1596},"Signed, no trust root",[767,1661],{"x":1523,"y":1651,"width":1524,"height":1556,"rx":770,"fill":773,"stroke":782,"style":1510},[785,1663,1602],{"x":1527,"y":1654,"style":1596},[767,1665],{"x":1531,"y":1651,"width":1532,"height":1556,"rx":770,"fill":773,"stroke":782,"style":1510},[785,1667,1602],{"x":1535,"y":1654,"style":1596},[767,1669],{"x":1539,"y":1651,"width":1540,"height":1556,"rx":770,"fill":773,"stroke":782,"style":1510},[785,1671,1632],{"x":1543,"y":1654,"style":1596},[767,1673],{"x":1547,"y":1651,"width":1548,"height":1556,"rx":770,"fill":843,"stroke":844,"style":1563},[785,1675,1677],{"x":1551,"y":1654,"style":1676},"font-size:12px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","INTACT:UNTRUSTED,UNTOUCHED",[748,1679,757,1685,757,1688,757,1691,757,1696,757,1700,757,1703,757,1707,757,1711,757,1714,757,1717,757,1721,757,1725,757,1729,757,1732,757,1735,757,1739],{"viewBox":1680,"role":751,"ariaLabelledBy":1681,"xmlns":755,"style":1684},"84 0 592 281",[1682,1683],"sig-questions-t","sig-questions-d","width:100%;max-width:592px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[759,1686,1687],{"id":1682},"Three questions a validator asks",[763,1689,1690],{"id":1683},"First whether the signed bytes still match the recorded hash, then whether the hash was signed by the key of the certificate, then whether the certificate chains to a trusted root. Only a yes to all three gives a fully valid signature.",[767,1692],{"x":1693,"y":770,"width":1694,"height":1695,"fill":773},"84","592","281",[767,1697],{"x":1698,"y":777,"width":1699,"height":823,"rx":780,"fill":814,"stroke":805,"style":783},"100.0","560.0",[785,1701,1702],{"x":847,"y":818,"style":819},"Intact?",[785,1704,1706],{"x":847,"y":1705,"style":794},"61.0","bytes still match the hash",[32,1708],{"x1":847,"y1":1709,"x2":847,"y2":1710,"stroke":805,"style":783},"80.0","102.0",[807,1712],{"points":1713,"fill":810},"380.0,106.0 375.0,97.0 385.0,97.0",[767,1715],{"x":1698,"y":1716,"width":1699,"height":823,"rx":780,"fill":843,"stroke":844,"style":783},"111.0",[785,1718,1720],{"x":847,"y":1719,"style":848},"136.5","Valid?",[785,1722,1724],{"x":847,"y":1723,"style":794},"156.0","hash signed by the certificate's key",[32,1726],{"x1":847,"y1":1727,"x2":847,"y2":1728,"stroke":805,"style":783},"175.0","197.0",[807,1730],{"points":1731,"fill":810},"380.0,201.0 375.0,192.0 385.0,192.0",[767,1733],{"x":1698,"y":1734,"width":1699,"height":823,"rx":780,"fill":868,"stroke":869,"style":783},"206.0",[785,1736,1738],{"x":847,"y":1737,"style":873},"231.5","Trusted?",[785,1740,1742],{"x":847,"y":1741,"style":794},"251.0","certificate chains to a known root",[85,1744,1746],{"id":1745},"signing-in-a-scheduled-job","Signing in a scheduled job",[10,1748,1749,1750,1752,1753,1756,1757,18],{},"In an automated report job, signing is the last step after the PDF is exported and checked, and before it is sent or archived. Load the ",[27,1751,113],{}," passphrase from the environment or a secret store rather than the script, and validate each signed file straight after signing, with the real trust roots, so a broken certificate chain or an expired certificate fails the job instead of producing files that readers' software flags. Certificates expire — commonly after one to three years — so add a check that warns a month before ",[27,1754,1755],{},"not_valid_after",". For files that must remain verifiable for many years, such as archived statements, pyHanko can also add a trusted timestamp from a timestamping authority and long-term validation data, which keep the signature checkable after the certificate has expired; that is worth combining with PDF\u002FA, described in ",[14,1758,1760],{"href":1759},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fexport-an-excel-report-to-pdf-a-for-archiving\u002F","Export an Excel Report to PDF\u002FA for Archiving",[85,1762,1764],{"id":1763},"common-pitfalls","Common pitfalls",[1766,1767,1768,1776,1782,1791,1797],"ul",{},[1769,1770,1771,1775],"li",{},[1772,1773,1774],"strong",{},"Editing the PDF after signing."," Add page numbers, stamps and metadata first, then sign.",[1769,1777,1778,1781],{},[1772,1779,1780],{},"A self-signed certificate for outside readers."," It shows as untrusted; use a CA certificate.",[1769,1783,1784,1787,1788,1790],{},[1772,1785,1786],{},"The private key in the repository."," Keep the ",[27,1789,113],{}," and its passphrase in a secret store.",[1769,1792,1793,1796],{},[1772,1794,1795],{},"Assuming a printed box proves anything."," Only the file can be verified.",[1769,1798,1799,1802],{},[1772,1800,1801],{},"Expired certificates."," Monitor expiry and add timestamps for long-term records.",[85,1804,1806],{"id":1805},"conclusion","Conclusion",[10,1808,1809,1810,1812],{},"To sign a PDF report in Python, load the signer's certificate and key from a PKCS#12 file with pyHanko, sign with a reason and location that say what the signature means, optionally add a visible box, and validate the result straight away. Any later change to the file breaks the signature, which pyHanko, ",[27,1811,1432],{}," and Acrobat all report.",[85,1814,1816],{"id":1815},"frequently-asked-questions","Frequently asked questions",[10,1818,1819,1822,1823,1826,1827,1830,1831,1834],{},[1772,1820,1821],{},"How do I digitally sign a PDF in Python?","\nUse pyHanko: load a signer with ",[27,1824,1825],{},"SimpleSigner.load_pkcs12",", then call ",[27,1828,1829],{},"signers.sign_pdf"," with an ",[27,1832,1833],{},"IncrementalPdfFileWriter"," over the original file.",[10,1836,1837,1840],{},[1772,1838,1839],{},"Why does Acrobat say the signer's identity is unknown?","\nThe certificate is not trusted on that machine — usually because it is self-signed or from an internal CA that is not installed; use a certificate from a trusted provider.",[10,1842,1843,1846],{},[1772,1844,1845],{},"Can a signed PDF be changed?","\nAny change to the signed bytes makes the signature invalid; later incremental updates such as further signatures are allowed and are reported separately.",[10,1848,1849,1852,1853,1856],{},[1772,1850,1851],{},"How do I verify a PDF signature from the command line?","\nRun ",[27,1854,1855],{},"pdfsig file.pdf"," from Poppler, which shows the signer and whether the signature and certificate are valid.",[85,1858,1860],{"id":1859},"related","Related",[1766,1862,1863,1869,1876,1883],{},[1769,1864,1865,1866,1868],{},"Up one level: ",[14,1867,17],{"href":16}," — PDF output from Excel.",[1769,1870,1871,1875],{},[14,1872,1874],{"href":1873},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-to-pdf-on-linux-with-libreoffice\u002F","Convert Excel to PDF on Linux with LibreOffice"," — exporting before signing.",[1769,1877,1878,1882],{},[14,1879,1881],{"href":1880},"\u002Fformatting-and-charting-excel-reports-with-python\u002Fprotecting-and-sharing-excel-workbooks\u002Fpassword-protect-an-excel-file-with-python\u002F","Password Protect an Excel File with Python"," — protecting the workbook itself.",[1769,1884,1885,1889],{},[14,1886,1888],{"href":1887},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fadd-headers-footers-and-page-numbers-to-an-excel-pdf\u002F","Add Headers, Footers and Page Numbers to an Excel PDF"," — finish the PDF before signing.",[1891,1892,1893],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}",{"title":25,"searchDepth":146,"depth":146,"links":1895},[1896,1897,1898,1899,1900,1901,1902,1903,1904,1905],{"id":87,"depth":146,"text":88},{"id":121,"depth":146,"text":122},{"id":554,"depth":146,"text":555},{"id":909,"depth":146,"text":910},{"id":1112,"depth":146,"text":1113},{"id":1745,"depth":146,"text":1746},{"id":1763,"depth":146,"text":1764},{"id":1805,"depth":146,"text":1806},{"id":1815,"depth":146,"text":1816},{"id":1859,"depth":146,"text":1860},"2026-10-02","Add a cryptographic signature to a PDF exported from an Excel report with pyHanko, so readers can prove who issued it and that nobody changed it afterwards: load a certificate from a PKCS#12 file, sign with a reason and location, add a visible signature box, validate the result in Python and with pdfsig, and see exactly what a one-byte change does to it.","md",[1910,1912,1914,1916],{"q":1821,"a":1911},"Use pyHanko: load a signer with SimpleSigner.load_pkcs12, then call signers.sign_pdf with an IncrementalPdfFileWriter over the original file.",{"q":1839,"a":1913},"The certificate is not trusted on that machine — usually because it is self-signed or from an internal CA that is not installed; use a certificate from a trusted provider.",{"q":1845,"a":1915},"Any change to the signed bytes makes the signature invalid; later incremental updates such as further signatures are allowed and are reported separately.",{"q":1851,"a":1917},"Run pdfsig file.pdf from Poppler, which shows the signer and whether the signature and certificate are valid.","digitally sign pdf python",null,{"breadcrumb":1921},[1922,1925,1928],{"name":1923,"item":1924},"Home","\u002F",{"name":1926,"item":1927},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":17,"item":16},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fsign-a-pdf-report-digitally-with-python",{"title":1931,"description":1932},"Sign a PDF Report Digitally with Python and pyHanko","Digitally sign a PDF report from Excel with pyHanko: PKCS#12 certificate, reason and location, visible signature box, validation in Python and pdfsig, and tamper detection.","sign-a-pdf-report-digitally-with-python","automating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fsign-a-pdf-report-digitally-with-python\u002Findex",[1936,1937],"pdf-export","protection-and-sharing","how-to","xAebF5UBqWLXkx3PNp4tgK7EyM_McebnhbqdC9yOrCE",[1941,1945],{"title":1942,"path":1943,"stem":1944,"children":-1},"Password-Protect a PDF Exported from Excel","\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fpassword-protect-a-pdf-exported-from-excel","automating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fpassword-protect-a-pdf-exported-from-excel\u002Findex",{"title":1946,"path":1947,"stem":1948,"children":-1},"Turn PDF Report Pages into Images","\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fturn-pdf-report-pages-into-images","automating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fturn-pdf-report-pages-into-images\u002Findex",[1950,1952],{"path":1951,"title":17},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf",{"path":1953,"title":1926},"\u002Fautomating-reporting-workflows",[],[1956,1959,1962,1965,1967,1970],{"path":1957,"title":1958,"kind":1938},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Ffill-pdf-forms-from-excel-rows-with-python","Fill PDF Forms from Excel Rows with Python",{"path":1960,"title":1961,"kind":1938},"\u002Fautomating-reporting-workflows\u002Fgenerating-excel-reports-from-templates\u002Fgenerate-one-pdf-per-excel-row","Generate One PDF per Excel Row",{"path":1963,"title":1964,"kind":1938},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fadd-bookmarks-and-a-cover-page-to-an-excel-pdf-export","Add Bookmarks and a Cover Page to an Excel PDF Export",{"path":1966,"title":1888,"kind":1938},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fadd-headers-footers-and-page-numbers-to-an-excel-pdf",{"path":1968,"title":1969,"kind":1938},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fcheck-a-pdf-export-for-cut-off-columns","Check a PDF Export for Cut-Off Columns",{"path":1971,"title":1972,"kind":1938},"\u002Fautomating-reporting-workflows\u002Fexporting-excel-reports-to-pdf\u002Fconvert-excel-file-to-pdf-with-python","Convert an Excel File to PDF with Python",1790972137859]