php – How to digitally sign a PDF?

Question:

I'm developing a system in PHP with the Laravel 5.1 framework that will generate some technical reports, and the client wants these reports to be digitally signed, that is, to have the electronic signature of the responsible technician in the PDF generated.

I have no idea how this works… I know there are some tokens, cards, etc that are used for this, but how do I pull this signature through my system and put it there in the document? Can anyone give me a direction? Is there any API or something like that?

Answer:

Short answer:

  1. Obtain a digital certificate from a certifying entity (Ex.: SERPRO). Have to pay and renew periodically.
  2. Use the certificate's primary key together with the pdf to create a digital signature with the PHP openssl_sign function.
  3. Write this signature to a file so you or someone else can test later if the file was signed by you with the openssl_verify function.

Long answer:

public and private keys

There are several ways to identify the origin of a particular file. One of those ways is through the use of a "key pair". A private key and a public key. The private key is uniquely yours and must not be given to anyone. The public key must be available to the general public. The algorithms that work with this key pair encrypt with the private key and decrypt with the public key. This way, because the private key is not known, if any file can be decrypted with the known public key of a certain user, we are sure that this file came from that user. Likewise, if we encrypt a file with the user's public key, only he can decrypt it with the private key.

hashes

Another concept is document integrity. Ensure that the document arriving at the destination is exactly the same as the source. For this, the "hash" of the file is calculated, in other words, a set of characters that represent the file and that are mathematically guaranteed to be unique for a certain file. The can public file hash. If at the destination you compare the hash of your file with the initial hash, you ensure its integrity. There are several hashing algorithms. One of the most common is MD5.

Digital signature

Simply put, a digital signature is nothing more than a hash encrypted by a user's private key.

Digital Certificates

The problem is: who guarantees that the key belongs to that user? answer: someone known to everyone. In other words, a public certification authority. These entities charge for their services. They are like digital registry offices. There are several with different prices. They provide so-called "digital certificates" which contain the key pair you need to sign things so that you can digitally identify yourself among other functions.

certificate hierarchy

Thus, for a certificate to be valid, it has a "certificate hierarchy", that is, another certificate that validates its certificate, another that validates the entity that certified it, until it reaches the national root entity (in our case, ICP-Brasil) . This hierarchy usually takes a few steps. A maximum of 4.

server certificates

In addition to the user having a certificate for themselves, so that you can access certificates on a certain server, you need to enable HTTPS mode. For that, the server itself needs a certificate to guarantee that that machine is really the machine you want to access. You then enable HTTPS mode on the HTTP server of your choice, using the server certificate.

Physical storage medium

These certificates can be provided in several ways:

  • A1 which are basically files that you can save yourself to a pen-drive or something and import directly into the browser.
  • A3 which are special hardware devices to contain only certificates. They look like a pen-drive, but have the special function of containing certificates in a special memory. There are other versions that come in cards with the same function. The advantage is that because they are hardware specific, they are more secure than a certificate present in a single file, which can be easily deleted or replaced. In this case, in addition to the certificate cost, there is the hardware cost.

If you want, you can create your own certification authority and issue your own certificates. The problem with this is that you don't have the security of transactions that guarantee the entire process and you can fall victim to the infamous "Man-in-the-middle" attack if someone manages to get in the middle of the origin and destination of your data. In other words, its certifying entity is not known to the general public and therefore the origin of your data cannot be guaranteed. Typically certifiers of this type are only used for development and real certificates are purchased for production servers.


To sign and verify a file

So let's go to the steps in a more elaborate way:

  1. you need to obtain a valid digital certificate from a certifying entity
  2. if it is an A3 certificate, load the device driver in the "security devices in your browser" function.
  3. import the certificates of your certification body in the "authorities" tab of the certificates function of your browser
  4. import your certificate in the "people" tab
  5. open your certificate and export the public and private key to files
  6. then in your PHP file, load the PDF file into a variable and the private key to another
  7. then use the openssl_sign function with these 2 variables to create the signature (check the documentation)
  8. to verify the signature use openssl_verify with the public key.

To use digital certificates in a web application

  1. Follow steps 1 to 4 above.
  2. Get a digital certificate for the server
  3. Configure your HTTP server to use that certificate and enable HTTPS mode
  4. With your certificate installed on your machine and the server configured for HTTPS, you should be asked for your certificate password when accessing the website. This password request process happens before any page on your site appears, because the HTTPS protocol is negotiated before the transmission of HTTP pages.
  5. Once the above steps are done, in the system you can access the client's certificate through the global variable $_SERVER['SSL_CLIENT_CERT'] . You can convert the contents of this variable into an array containing the data present in the certificate (personal data) with the command openssl_x509_parse($_SERVER['SSL_CLIENT_CERT']) .
  6. The public and private keys can be obtained with the openssl_get_publickey() and openssl_get_privatekey() respectively. Remember that you can only get the private key from your own certificates. The private key is reserved for the owner of the certificate.
  7. Then use steps 6 through 8 of the above procedure to sign and verify a document.

Configure HTTPS on Apache 2

Put in one file (say 'certificatechain.crt') the entire certificate hierarchy of the server certificate. Just copy and paste one under the other in a text file. Leave the server certificate in a separate file ('server.crt') and the server certificate's private key in another ('server.key').

Below is an HTTPS configuration template on Apache 2

<VirtualHost *:443>
    ServerAdmin webmaster@localhost

    DocumentRoot /var/www
    Alias /sistema /opt/sistema
    <Directory /opt/sistema>
        Options Indexes FollowSymLinks MultiViews 
        AllowOverride All
        AcceptPathInfo On
        Order allow,deny
        allow from all
    </Directory>

    SSLEngine on
    SSLOptions +StdEnvVars +ExportCertData
    SSLCertificateFile      /opt/sistema/certs/servidor.crt
    SSLCertificateKeyFile   /opt/sistema/certs/servidor.key
    SSLCACertificateFile    /opt/sistema/certs/cadeiadecertificados.crt
    SSLVerifyClient require
    SSLVerifyDepth  10

    ErrorLog ${APACHE_LOG_DIR}/error.log
    LogLevel warn
    CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

Creating a Local Certificate Authority and a Locally Signed Server Certificate

The Bash script (Linux) below creates a certificate authority for issuing certificates locally.

  • make sure the openssl package is installed. If not, install with sudo apt-get install openssl (debian distributions), or corresponding command in your distribution.
  • put the code below in a file named "openssl.cnf" and keep this file in the same folder as the scripts below.

openssl.cnf file:

#
# OpenSSL example configuration file.
# This is mostly being used for generation of certificate requests.
#

# This definition stops the following lines choking if HOME isn't
# defined.
HOME            = .
RANDFILE        = $ENV::HOME/.rnd

# Extra OBJECT IDENTIFIER info:
#oid_file       = $ENV::HOME/.oid
oid_section     = new_oids

# To use this configuration file with the "-extfile" option of the
# "openssl x509" utility, name here the section containing the
# X.509v3 extensions to use:
# extensions        = 
# (Alternatively, use a configuration file that has only
# X.509v3 extensions in its main [= default] section.)

[ new_oids ]

# We can add new OIDs in here for use by 'ca', 'req' and 'ts'.
# Add a simple OID like this:
# testoid1=1.2.3.4
# Or use config file substitution like this:
# testoid2=${testoid1}.5.6

# Policies used by the TSA examples.
tsa_policy1 = 1.2.3.4.1
tsa_policy2 = 1.2.3.4.5.6
tsa_policy3 = 1.2.3.4.5.7

####################################################################
[ ca ]
default_ca  = CA_default        # The default ca section

####################################################################
[ CA_default ]

dir     = .         # Where everything is kept
certs       = $dir/certs        # Where the issued certs are kept
crl_dir     = $dir/crl      # Where the issued crl are kept
database    = $dir/index.txt    # database index file.
#unique_subject = no            # Set to 'no' to allow creation of
                    # several ctificates with same subject.
new_certs_dir   = $dir          # default place for new certs.

certificate = $dir/cacert.pem   # The CA certificate
serial      = $dir/serial       # The current serial number
crlnumber   = $dir/crlnumber    # the current crl number
                    # must be commented out to leave a V1 CRL
crl     = $dir/crl.pem      # The current CRL
private_key = $dir/private/cakey.pem# The private key
RANDFILE    = $dir/private/.rand    # private random number file

x509_extensions = usr_cert      # The extentions to add to the cert

# Comment out the following two lines for the "traditional"
# (and highly broken) format.
name_opt    = ca_default        # Subject Name options
cert_opt    = ca_default        # Certificate field options

# Extension copying option: use with caution.
# copy_extensions = copy

# Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs
# so this is commented out by default to leave a V1 CRL.
# crlnumber must also be commented out to leave a V1 CRL.
# crl_extensions    = crl_ext

default_days    = 365           # how long to certify for
default_crl_days= 30            # how long before next CRL
default_md  = default       # use public key default MD
preserve    = no            # keep passed DN ordering

# A few difference way of specifying how similar the request should look
# For type CA, the listed attributes must be the same, and the optional
# and supplied fields are just that :-)
policy      = policy_match

# For the CA policy
[ policy_match ]
countryName     = match
stateOrProvinceName = match
organizationName    = match
organizationalUnitName  = optional
commonName      = supplied
emailAddress        = optional

# For the 'anything' policy
# At this point in time, you must list all acceptable 'object'
# types.
[ policy_anything ]
countryName     = optional
stateOrProvinceName = optional
localityName        = optional
organizationName    = optional
organizationalUnitName  = optional
commonName      = supplied
emailAddress        = optional

####################################################################
[ req ]
default_bits        = 1024
default_keyfile     = privkey.pem
distinguished_name  = req_distinguished_name
attributes      = req_attributes
x509_extensions = v3_ca # The extentions to add to the self signed cert

# Passwords for private keys if not present they will be prompted for
# input_password = secret
# output_password = secret

# This sets a mask for permitted string types. There are several options. 
# default: PrintableString, T61String, BMPString.
# pkix   : PrintableString, BMPString (PKIX recommendation before 2004)
# utf8only: only UTF8Strings (PKIX recommendation after 2004).
# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings).
# MASK:XXXX a literal mask value.
# WARNING: ancient versions of Netscape crash on BMPStrings or UTF8Strings.
string_mask = utf8only

# req_extensions = v3_req # The extensions to add to a certificate request

[ req_distinguished_name ]
countryName         = Country Name (2 letter code)
countryName_default     = AU
countryName_min         = 2
countryName_max         = 2

stateOrProvinceName     = State or Province Name (full name)
stateOrProvinceName_default = Some-State

localityName            = Locality Name (eg, city)

0.organizationName      = Organization Name (eg, company)
0.organizationName_default  = Internet Widgits Pty Ltd

# we can do this but it is not needed normally :-)
#1.organizationName     = Second Organization Name (eg, company)
#1.organizationName_default = World Wide Web Pty Ltd

organizationalUnitName      = Organizational Unit Name (eg, section)
#organizationalUnitName_default =

commonName          = Common Name (e.g. server FQDN or YOUR name)
commonName_max          = 64

emailAddress            = Email Address
emailAddress_max        = 64

# SET-ex3           = SET extension number 3

[ req_attributes ]
challengePassword       = A challenge password
challengePassword_min       = 4
challengePassword_max       = 20

unstructuredName        = An optional company name

[ usr_cert ]

# These extensions are added when 'ca' signs a request.

# This goes against PKIX guidelines but some CAs do it and some software
# requires this to avoid interpreting an end user certificate as a CA.

basicConstraints=CA:FALSE

# Here are some examples of the usage of nsCertType. If it is omitted
# the certificate can be used for anything *except* object signing.

# This is OK for an SSL server.
# nsCertType            = server

# For an object signing certificate this would be used.
# nsCertType = objsign

# For normal client use this is typical
# nsCertType = client, email

# and for everything including object signing:
# nsCertType = client, email, objsign

# This is typical in keyUsage for a client certificate.
# keyUsage = nonRepudiation, digitalSignature, keyEncipherment

# This will be displayed in Netscape's comment listbox.
nsComment           = "OpenSSL Generated Certificate"

# PKIX recommendations harmless if included in all certificates.
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer

# This stuff is for subjectAltName and issuerAltname.
# Import the email address.
# subjectAltName=email:copy
# An alternative to produce certificates that aren't
# deprecated according to PKIX.
# subjectAltName=email:move

# Copy subject details
# issuerAltName=issuer:copy

#nsCaRevocationUrl      = http://www.domain.dom/ca-crl.pem
#nsBaseUrl
#nsRevocationUrl
#nsRenewalUrl
#nsCaPolicyUrl
#nsSslServerName

# This is required for TSA certificates.
# extendedKeyUsage = critical,timeStamping

[ v3_req ]

# Extensions to add to a certificate request

basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment

[ v3_ca ]


# Extensions for a typical CA


# PKIX recommendation.

subjectKeyIdentifier=hash

authorityKeyIdentifier=keyid:always,issuer

# This is what PKIX recommends but some broken software chokes on critical
# extensions.
#basicConstraints = critical,CA:true
# So we do this instead.
basicConstraints = CA:true

# Key usage: this is typical for a CA certificate. However since it will
# prevent it being used as an test self-signed certificate it is best
# left out by default.
# keyUsage = cRLSign, keyCertSign

# Some might want this also
# nsCertType = sslCA, emailCA

# Include email address in subject alt name: another PKIX recommendation
# subjectAltName=email:copy
# Copy issuer details
# issuerAltName=issuer:copy

# DER hex encoding of an extension: beware experts only!
# obj=DER:02:03
# Where 'obj' is a standard or added object
# You can even override a supported extension:
# basicConstraints= critical, DER:30:03:01:01:FF

[ crl_ext ]

# CRL extensions.
# Only issuerAltName and authorityKeyIdentifier make any sense in a CRL.

# issuerAltName=issuer:copy
authorityKeyIdentifier=keyid:always

[ proxy_cert_ext ]
# These extensions should be added when creating a proxy certificate

# This goes against PKIX guidelines but some CAs do it and some software
# requires this to avoid interpreting an end user certificate as a CA.

basicConstraints=CA:FALSE

# Here are some examples of the usage of nsCertType. If it is omitted
# the certificate can be used for anything *except* object signing.

# This is OK for an SSL server.
# nsCertType            = server

# For an object signing certificate this would be used.
# nsCertType = objsign

# For normal client use this is typical
# nsCertType = client, email

# and for everything including object signing:
# nsCertType = client, email, objsign

# This is typical in keyUsage for a client certificate.
# keyUsage = nonRepudiation, digitalSignature, keyEncipherment

# This will be displayed in Netscape's comment listbox.
nsComment           = "OpenSSL Generated Certificate"

# PKIX recommendations harmless if included in all certificates.
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer

# This stuff is for subjectAltName and issuerAltname.
# Import the email address.
# subjectAltName=email:copy
# An alternative to produce certificates that aren't
# deprecated according to PKIX.
# subjectAltName=email:move

# Copy subject details
# issuerAltName=issuer:copy

#nsCaRevocationUrl      = http://www.domain.dom/ca-crl.pem
#nsBaseUrl
#nsRevocationUrl
#nsRenewalUrl
#nsCaPolicyUrl
#nsSslServerName

# This really needs to be in place for it to be a proxy certificate.
proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo

####################################################################
[ tsa ]

default_tsa = tsa_config1   # the default TSA section

[ tsa_config1 ]

# These are used by the TSA reply generation only.
dir     = ./demoCA      # TSA root directory
serial      = $dir/tsaserial    # The current serial number (mandatory)
crypto_device   = builtin       # OpenSSL engine to use for signing
signer_cert = $dir/tsacert.pem  # The TSA signing certificate
                    # (optional)
certs       = $dir/cacert.pem   # Certificate chain to include in reply
                    # (optional)
signer_key  = $dir/private/tsakey.pem # The TSA private key (optional)

default_policy  = tsa_policy1       # Policy if request did not specify it
                    # (optional)
other_policies  = tsa_policy2, tsa_policy3  # acceptable policies (optional)
digests     = md5, sha1     # Acceptable message digests (mandatory)
accuracy    = secs:1, millisecs:500, microsecs:100  # (optional)
clock_precision_digits  = 0 # number of digits after dot. (optional)
ordering        = yes   # Is ordering defined for timestamps?
                # (optional, default: no)
tsa_name        = yes   # Must the TSA name be included in the reply?
                # (optional, default: no)
ess_cert_id_chain   = no    # Must the ESS cert id chain be included?
                # (optional, default: no)

Just place the openssl.cnf file and the script below in a folder and run the script.

#/bin/bash

# Este script cria uma autoridade certificadora (CA) local para ser usada 
# em ambientes desenvolvimento e servir como base para emissão de 
# certificados digitais e um certificado de servidor emitido por esta CA. 
#
# São criados os seguintes arquivos:
# rootCA.key => Chave privada da autoridade certificadora local
# rootCA.crt => Certificado da autoridade certificadora local auto-assinado
# server.csr => Solicitação de emissão de certificado de servidor 
# server.key => Chave privada do certificado de servidor solicitado
# server.crt => Certificado de servidor emitido pela autoridade certificadora local
# index.txt e serial são arquivos necessários apenas ao processo de 
# emissão de certificados pelo openssl e não são relevantes para o 
# usuário. Não devem ser apagados mas podem ser ignorados.

echo 00 > serial
touch index.txt

echo "Criar chave privada da Autoridade Ceritficadora (CA)"
openssl genrsa -des3 -passout pass:123 -out  ./rootCA.key 2048

echo "Remover a senha da chave privada"
openssl rsa -passin pass:123 -in ./rootCA.key -out ./rootCA.key

echo "Criar certificado auto-assinado da CA"
openssl req -config openssl.cnf -new -x509 -subj '/C=BR/L=Dev/O=COMPANHIA/CN=CA' -days 99999 -key ./rootCA.key -out ./rootCA.crt

echo "Criar chave privada do servidor"
openssl genrsa -des3 -passout pass:123 -out ./server.key 2048

echo "Remover senha da chave privada do servidor"
openssl rsa -passin pass:123 -in ./server.key -out ./server.key

echo "Criar requisição de certificado para o servidor"
openssl req -config ./openssl.cnf -new -subj '/C=BR/L=Dev/O=COMPANHIA/CN=server' -key ./server.key -out ./server.csr

echo "Criar certificado para o servidor a partir da requisição de certificado"
openssl ca -batch -config ./openssl.cnf -days 999 -in ./server.csr -out ./server.crt -keyfile ./rootCA.key -cert ./rootCA.crt -policy policy_anything

echo "Apagando arquivos temporários"
rm -f index.txt*
touch index.txt
rm serial.old
rm *.pem

echo "Finalizado."

Issuing certificates from the local certificate authority

The script below issues certificates from the certificate authority created with the script above.

#!/bin/bash

# Este script emite certificados com base na autoridade certificadora 
# local. É criada uma pasta cujo nome é o nome do dono do certificado, 
# contendo os seguintes arquivos:
# <NOME>.key => Chave privada do usuário
# <NOME>.csr => Requisição de certificado à CA local
# <NOME>.crt => Certificado emitido pela CA local
# <NOME>.p12 => Certificado em formato PKCS12

if [[ -z "$1" || -z "$2" ]]; then
    echo "Uso: emitircertificado <NOME> <CPF>"
    exit
fi

NOME=$1
CPF=$2
DN="/C=BR/L=Dev/O=COMPANHIA/CN=$1:$2"
ARQ="${NOME//[[:space:]]/}"

echo -e "\nCriando pasta... " 
mkdir $ARQ
echo "feito."

echo -e "\nCriando chave privada... "
openssl genrsa -des3 -passout pass:123 -out ./$ARQ/$ARQ.key 2048
echo "feito."

echo -e "\nRemovendo senha..."
openssl rsa -passin pass:123 -in ./$ARQ/$ARQ.key -out ./$ARQ/$ARQ.key
echo "feito."

echo -e "\nCriando CSR... "
openssl req -config ./openssl.cnf -new -subj "$DN" -key ./$ARQ/$ARQ.key -out ./$ARQ/$ARQ.csr
echo "feito."

echo -e "\nCriando certificado do cliente"
openssl ca -batch -config ./openssl.cnf -days 999 -in ./$ARQ/$ARQ.csr -out ./$ARQ/$ARQ.crt -keyfile ./rootCA.key -cert ./rootCA.crt -policy policy_anything
echo "feito."

echo -e "\nExportar o cliente para pkcs12 para importar no navegador"
openssl pkcs12 -export -passout pass:123 -in ./$ARQ/$ARQ.crt -inkey ./$ARQ/$ARQ.key -certfile ./rootCA.crt -out ./$ARQ/$ARQ.p12

echo -e "\nApagar arquivos temporários"
rm -f index.txt*
touch index.txt
rm serial.old
rm *.pem

echo "Finalizado."

Hope it's useful. Please feel free to ask more.

Scroll to Top