This tutorial shows how to sign PDFs with a certificate. The best thing would be to sign it with certificates we can get from ID Austria. This would ensure that everybody could trust.
Unfortunately getting official certificates to sign PDFs officially is not that easy, rather you have to use a webinterface to upload the file there. Because that uses a user interface, automization is difficult to implement. Until that point that this might be easier, we can use self-signed certificates. This shows what is possible.
If you already have trusted certificates, you might also skip the part describing how to generate certificates and proceed to the python code. Probably you still need the part where the PEM-formatted certificate is converted to pkcs12
In this example we use a root certificate and one for a service called signer. The root certificate is like the authority granting the signer to sign in the name of the authority.
Therefore we first create the CA's (certification authority) key and certificate. Then we create a key and a signing request for the signer. The latter one gets signed by the first one, outputting a certificate.
First generate the key:
openssl genrsa -aes256 -out ca.key 4096This will ask for a password and to verify this.
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
In this example we used rootcapassword. Now we have a root key. Let's generate
the root certificate.
openssl req \
-x509 \
-new \
-nodes \
-key ca.key \
-sha256 \
-days 1826 \
-out ca.crt \
-subj '/CN=r1/C=AT/ST=Tirol/L=Rattenberg/O=201created.eu'You will get asked for the pass phrase, we used for the root key previously:
Enter pass phrase for ca.key:
Enter rootcapassword in this example.
The signer itself needs a key, but this should be sigened by the root ca. Therefore we need a certificate signing request (CSR):
openssl req \
-new \
-nodes \
-out signer.csr \
-newkey rsa:4096 \
-keyout signer.key \
-subj '/CN=signer/C=AT/ST=Tirol/L=Rattenberg/O=201created.eu'In order to have some more information concerting the certificate to check it's
validity, we need to use extension. Therefore we prepare a text file, called
signer.v3.ext in this case. You could also use a text editor and copy the part
between the two lines with EOF into it:
# Prepare an extension file
cat > signer.v3.ext <<EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = signer
EOFNow we have the CA's key/certificate, the signer's signing request and the extension information. So let's create the signers certificate.
# Sign the certificate
openssl x509 \
-req \
-in signer.csr \
-CA ca.crt \
-CAkey ca.key \
-CAcreateserial \
-out signer.crt \
-days 90 \
-sha256 \
-extfile signer.v3.extBecause we use the ca's key, we will get asked for it's password again:
Certificate request self-signature ok
subject=CN = signer, C = AT, ST = Tirol, L = Rattenberg, O = 201created.eu
Enter pass phrase for ca.key:
Therefore we type in rootcapassword in this example again.
Some software may require key/certificate in other formats than pem. Here we convert it to the pkcs12 format:
openssl pkcs12 -inkey signer.key -in signer.crt -export -out signer.pfxHere we get asked for a password again:
Enter Export Password:
Verifying - Enter Export Password:
Since we are not useing the ca's key for this step, it is recommended to use
another password here. Let's choose singerpassword here.
## according to: https://unix.stackexchange.com/questions/277892/how-do-i-create-a-blank-pdf-from-the-command-line
cat > dummy.pdf <<EOF
%PDF-1.7
%µ¶
1 0 obj
<</Type/Catalog/Pages 2 0 R>>
endobj
2 0 obj
<</Type/Pages/Count 1/Kids[4 0 R]>>
endobj
3 0 obj
<<>>
endobj
4 0 obj
<</Type/Page/MediaBox[0 0 595 842]/Rotate 0/Resources 3 0 R/Parent 2 0 R>>
endobj
xref
0 5
0000000000 00001 f
0000000016 00000 n
0000000062 00000 n
0000000114 00000 n
0000000135 00000 n
trailer
<</Size 5/Root 1 0 R>>
startxref
226
%%EOF
EOFJust to know, where this is coming from. You can create the pdf using
mutool create -o dummy.pdf /dev/nullThis creates an empty pdf file. To not be dependent on mutool, the file's
content could be cated into the file directly.
uv init
uv add 'pyHanko[pkcs11,image-support,opentype,qr]' pyhanko-cliUse this simple code here and write it to sign.py:
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
from pyhanko.sign import signers
if __name__ == '__main__':
# parepare the signer
signer = signers.SimpleSigner.load_pkcs12(
pfx_file='signer.pfx',
passphrase=b'singerpassword'
)
# open the unsigned pdf
with open('dummy.pdf', 'rb') as doc:
w = IncrementalPdfFileWriter(doc)
# open a new binary file
with open('document-signed.pdf', 'wb') as outf:
# sign the PDF and write it to the new binary file
signers.sign_pdf(
w,
signers.PdfSignatureMetadata(field_name='Signature'),
signer=signer,
output=outf
)Run the code with uv run sign.py