Skip to content

Instantly share code, notes, and snippets.

@marco74
Last active August 22, 2025 11:32
Show Gist options
  • Select an option

  • Save marco74/1426ef75f0f7b90c5cd984ff32ffc881 to your computer and use it in GitHub Desktop.

Select an option

Save marco74/1426ef75f0f7b90c5cd984ff32ffc881 to your computer and use it in GitHub Desktop.
This gist shows how to self-sign a pdf

Sign PDF with self signed certs

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

Certificates

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.

Root CA

First generate the key:

openssl genrsa -aes256 -out ca.key 4096

This 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.

Signer

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
EOF

Now 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.ext

Because 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.

Conversion to other format

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.pfx

Here 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.

PDF signing with Python

Create a dummy pdf with the size of A4

## 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
EOF

Just to know, where this is coming from. You can create the pdf using

mutool create -o dummy.pdf /dev/null

This creates an empty pdf file. To not be dependent on mutool, the file's content could be cated into the file directly.

Initialize the python environment

uv init
uv add 'pyHanko[pkcs11,image-support,opentype,qr]' pyhanko-cli

Source code

Use 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment