Skip to content

Commit ac00be3

Browse files
committed
[feat][pdf_signing] Add PKCS#12 digital signature with original formatting retention
1 parent 1340dd9 commit ac00be3

11 files changed

+167
-1
lines changed

.idea/.gitignore

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/inspectionProfiles/profiles_settings.xml

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/misc.xml

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/modules.xml

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/pdfly.iml

+15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/vcs.xml

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

certificate.pem

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDkTCCAnmgAwIBAgIUO1Cg7xxTUTSBR83IYBVJNU14E/owDQYJKoZIhvcNAQEL
3+
BQAwWDELMAkGA1UEBhMCQ04xEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
4+
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDERMA8GA1UEAwwIY3l5LTIwMjQwHhcN
5+
MjQxMTA5MTExMDQ1WhcNMjUxMTA5MTExMDQ1WjBYMQswCQYDVQQGEwJDTjETMBEG
6+
A1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkg
7+
THRkMREwDwYDVQQDDAhjeXktMjAyNDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
8+
AQoCggEBAMY14YDUyJe+Jpm+uuW/652NFubm0kkk516bn46u8Jkp//RDFLY3uB7D
9+
8FaQqJMP0DZ0q2uqw8/oaplhKR/hURSj/emp8iq5xweU9IfHzqKPpYH0Xqn6Hx2l
10+
wqCXtwTkkhL71ZFSs6aleJR+P5zvo0HasHy8A95XQrE1gA8SWSd36WBUUl9l0nA4
11+
EOToC5pYOm4IzxLNXdsL/AuSjXRkfLb+DqeqPEYEdYdyWl1DpzCbM8wvf610z1bS
12+
TwDLHURjpp5r73dnGh9AQPCwVf2UxUjWrB91YRdy4EH48wIv8O0lNqt7GGHBgJNu
13+
IwXYyPMjem6NZ9+JjwOFbW0Li9qgUqsCAwEAAaNTMFEwHQYDVR0OBBYEFO1BOvKs
14+
vRFCS3x4W3vbJ6JnwQViMB8GA1UdIwQYMBaAFO1BOvKsvRFCS3x4W3vbJ6JnwQVi
15+
MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAA+zcd5VS0vnJD1I
16+
I8t/ZxqNT+p0lx8OIIYvbbSYuqMgS5kV5cufKJbrg8wNdOj0f27D2CLRIjS6Xb8y
17+
+huYH1A3jvjisNF2yRs7JOVW1/wMNflHXxvqN2PzAFprzpTVVjQDgO4/mN8mQVoG
18+
2d1O1FHHPPMnN+BwWiMr5DEAx8olmRAZYevmfWDiCWuFBolVWBDNtiODqeNcGPg6
19+
qa+5V/HXm99dzalzHED8JX5rG3tJOY0+KqnpT+odc9Fv+C5i1OkLt31j55dDRpH5
20+
J+z8cXlvaWkbsqSGuqzCmU15DoErWaonBR7JIlqomY17M9AUM8bf+dYC3mpLyFM7
21+
fINsxOc=
22+
-----END CERTIFICATE-----

certs.p12

2.61 KB
Binary file not shown.

pdfly/cli.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"""
66

77
from pathlib import Path
8-
from typing import List
8+
from typing import List, Optional
99

1010
import typer
1111
from typing_extensions import Annotated
@@ -19,6 +19,7 @@
1919
import pdfly.up2
2020
import pdfly.update_offsets
2121
import pdfly.x2pdf
22+
import pdfly.sign # 导入 sign 模块
2223

2324

2425
def version_callback(value: bool) -> None:
@@ -37,6 +38,29 @@ def version_callback(value: bool) -> None:
3738
),
3839
rich_markup_mode="rich", # Allows to pretty-print commands documentation
3940
)
41+
@entry_point.command(name="sign", help="Sign a PDF file using a PKCS12 (.p12) certificate.")
42+
def sign(
43+
input_pdf: Path = typer.Argument(..., help="Input PDF file."),
44+
output_pdf: Path = typer.Option(..., "--output", "-o", help="Output signed PDF."),
45+
p12: Optional[Path] = typer.Option(None, "--p12", help="PKCS12 certificate file."),
46+
password: Optional[str] = typer.Option(None, "--password", help="Password for the certificate."),
47+
) -> None:
48+
"""
49+
Sign a PDF file using a PKCS12 (.p12) certificate.
50+
"""
51+
print(f"input_pdf: {input_pdf}")
52+
print(f"output_pdf: {output_pdf}")
53+
print(f"p12: {p12}")
54+
print(f"password: {password}")
55+
56+
# Ensure that the PKCS12 certificate file exists
57+
if not p12 or not p12.exists():
58+
typer.echo("Error: Please provide a valid PKCS12 (.p12) certificate file using --p12.")
59+
raise typer.Exit(1)
60+
61+
# Call the signing function in the sign module, using the .p12 certificate for signing
62+
pdfly.sign.sign_pdf_with_p12(input_pdf, output_pdf, p12, password)
63+
typer.echo(f"Successfully signed PDF saved at {output_pdf}")
4064

4165

4266
@entry_point.callback() # type: ignore[misc]

pdfly/sign.py

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""Sign a PDF file with a PKCS#12 certificate, retaining original formatting and adding a signature page."""
2+
3+
from pathlib import Path
4+
from fpdf import FPDF
5+
import fitz # PyMuPDF for precise page content extraction
6+
7+
def sign_pdf_with_p12(input_pdf: Path, output_pdf: Path, p12_path: Path, password: str) -> None:
8+
"""
9+
Load the original PDF content, retain its layout, add a signature page, and digitally sign the PDF.
10+
11+
Parameters:
12+
- input_pdf: Path to the PDF file to be signed.
13+
- output_pdf: Path where the signed PDF file will be saved.
14+
- p12_path: Path to the PKCS#12 certificate file (.p12).
15+
- password: Password for the PKCS#12 certificate.
16+
"""
17+
18+
# Step 1: Create a new FPDF object
19+
pdf = FPDF()
20+
21+
# Step 2: Copy each page's content from the original PDF while retaining layout
22+
doc = fitz.open(input_pdf)
23+
for page_num in range(doc.page_count):
24+
page = doc[page_num]
25+
rect = page.rect
26+
pdf.add_page(orientation="P" if rect.width < rect.height else "L")
27+
pdf.set_auto_page_break(False)
28+
text = page.get_text("text")
29+
pdf.set_xy(0, 0)
30+
pdf.set_font("Arial", size=12)
31+
pdf.multi_cell(0, 10, txt=text)
32+
33+
# Step 3: Add a signature page
34+
pdf.add_page()
35+
pdf.set_xy(10, 10)
36+
pdf.set_font("Arial", size=12)
37+
pdf.cell(200, 10, txt="This document is signed with PKCS#12.", ln=True)
38+
39+
# Step 4: Digitally sign the PDF
40+
pdf.sign_pkcs12(str(p12_path), password=password.encode("utf-8"))
41+
pdf.output(str(output_pdf))
42+

private_key.pem

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDGNeGA1MiXviaZ
3+
vrrlv+udjRbm5tJJJOdem5+OrvCZKf/0QxS2N7gew/BWkKiTD9A2dKtrqsPP6GqZ
4+
YSkf4VEUo/3pqfIquccHlPSHx86ij6WB9F6p+h8dpcKgl7cE5JIS+9WRUrOmpXiU
5+
fj+c76NB2rB8vAPeV0KxNYAPElknd+lgVFJfZdJwOBDk6AuaWDpuCM8SzV3bC/wL
6+
ko10ZHy2/g6nqjxGBHWHclpdQ6cwmzPML3+tdM9W0k8Ayx1EY6aea+93ZxofQEDw
7+
sFX9lMVI1qwfdWEXcuBB+PMCL/DtJTarexhhwYCTbiMF2MjzI3pujWffiY8DhW1t
8+
C4vaoFKrAgMBAAECggEAVvQDOf/fAga2q2LrXegMhqEDJ4eiP9lTMQNng3JGdYLh
9+
2PfmqauW99QwZuFuOpnI12LmYsTWwyosPZ5MqsIvTjVFKlFWPh0i7bYQclKa2WDY
10+
FEMuljX2mYyC7e2wqhJV7MMS5X5Y9qYH2GjsIj5Uqgq0uvvGXK9+P/x+8d20Q+YG
11+
QqH3ERshgTBlG2G8mNGjxamvQ6V9g7l3mbP8DJ8Ty5WVc6GOfyxgIAnE5Q/IVekI
12+
bzIsUYLyLkYeNGUGK+IoXwy2Q/QY8sTwwoV+1DtURZG/26a0CRCPjJStX/g0kFG7
13+
K9ViDz44PhXvcyVfjVQXHaso4rQzE0HyoSC2T7hjzQKBgQDzDPTXPy7FnvTwPyfN
14+
txyc0qbaF4uDMHWfA4lRsRh6yAZjuWDcPUKzpsQr5nDlNCpOvSgp8dLyNUtyQPdu
15+
puynVwXn0tFu2yzlcezDNrOQpO0MHvDQytd1EZ7iNjm0xwBd1wb/HRL3Gr9aSRU/
16+
wqouQImCRT5jZHk5gjfvSQ2JZQKBgQDQxVXyfqxkmwA+2RgKrv6LoZNt5uQeSGs1
17+
9nVb+QzmWxHj5d4dNMjnnUkWnaO3wyp7VlGxlR6J4RbFpbRTTIlAwixb6kiPQAQr
18+
yW6nx6O9bWoPSr4dQA9r6KqmIdC8EiR6QOEfYmSA0p4zIyVTDfSYLLsGRNpKGchp
19+
Z3N7gy6yzwKBgGF7+uAymWHuRbPuwNpD7ZgA7adf9jciQqsK0hMQAw+MFvP8sJrl
20+
f1FrPBeXkAR+jdGTEP7x3XgEZERpRlT9YsIjp1y6NAJQqotEzH/n+tGzNNi9uD0m
21+
fpCYBrAYq8CUaNM6obXFRYwTEFj4Iyu1umhevkif2UwoSm8EicbR+Dn5AoGAQwiJ
22+
z0ITMn5+dq+YQ53qx4TK5Mf1SS/xlLMc/bobBUAKn4VoazJOq+fZ1vQo5FE7K70M
23+
oBuEYbsvZ6kMHI7/pxZxzdWNFMn2TOTxrdexYJpoKp7SKmwuR3S+jndfIXQl2EdK
24+
wZwDL0XxW/QWAPQDLHV4W8vx10cuDYIVF3yImwsCgYBP9mm5wJjH7kSbJ6I7B4/H
25+
WzSPjsQiVrXIIO7Sgw3dprtR64CrLIsxBp7b4PrJPTi984jSjml/rQaCTIicBKPh
26+
UsQ0kvwJRAb70c74rGcSKZ7VVcPwS/bE43XpKg5jasCVrOqXeCjb/w+V7EulCCKe
27+
p2vcvcLI2/0NBBq9q4vd/A==
28+
-----END PRIVATE KEY-----

0 commit comments

Comments
 (0)