PKI OpenSSL (pt 1)

OpenSSL-based PKI (part 1)

Sources:
https://openssl-ca.readthedocs.io/en/latest/index.html
https://www.golinuxcloud.com/tutorial-pki-certificates-authority-ocsp/
https://social.technet.microsoft.com/wiki/contents/articles/2900.offline-root-certification-authority-ca.aspx
https://pki-tutorial.readthedocs.io/en/latest/index.html

An airgapped Root CA

The system that contains the RootCA is supposed to be airgapped, to be under controlled access, and to be used only for the emission of new CAs and revocation of compromised CAs.
In this example, we can put everything on the same server, but one should try to separate the RootCA from the other CAs as much as possible.
The first step is to create a Root Certification Authority (RootCA), which will only create other CAs.
Switch to the root user:

1
2
3
4
5
6
7
mkdir /root/ca && cd $_
mkdir /root/ca/{certs,crl,newcerts,private}
chmod 700 /root/ca/private
touch /root/ca/index.txt
echo 1000 > /root/ca/serial
echo 1000 > /root/ca/crlnumber
cat > /root/ca/openssl.cnf

/root/ca/openssl.cnf

# OpenSSL root CA configuration file.
# Copy to '/root/ca/openssl.cnf.
[ ca ]
# 'man ca'
default_ca = CA_default

[ CA_default ]
# Directory and file locations.
dir               = /root/ca
certs             = $dir/certs
crl_dir           = $dir/crl
new_certs_dir     = $dir/newcerts
database          = $dir/index.txt
serial            = $dir/serial
RANDFILE          = $dir/private/.rand

# The root key and root certificate.
private_key       = $dir/private/ca.key.pem
certificate       = $dir/certs/ca.cert.pem

# For certificate revocation lists.
crlnumber         = $dir/crlnumber
crl               = $dir/crl/ca.crl.pem
crl_extensions    = crl_ext
default_crl_days  = 385

# SHA-1 is deprecated, so use SHA-2 instead.
default_md        = sha512

name_opt          = ca_default
cert_opt          = ca_default
default_days      = 375
preserve          = no
policy            = policy_strict

[ policy_strict ]
# The root CA should only sign intermediate certificates that match.
# See the POLICY FORMAT section of 'man ca'.
countryName             = match
stateOrProvinceName     = match
organizationName        = match
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ policy_loose ]
# Allow the intermediate CA to sign a more diverse range of certificates.
# See the POLICY FORMAT section of the 'ca' man page.
countryName             = optional
stateOrProvinceName     = optional
localityName            = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

[ req ]
# Options for the 'req' tool ('man req').
default_bits        = 4096
distinguished_name  = req_distinguished_name
string_mask         = utf8only

# SHA-1 is deprecated, so use SHA-2 instead.
default_md          = sha512

# Extension to add when the -x509 option is used.
x509_extensions     = v3_ca

[ req_distinguished_name ]
# See <https://en.wikipedia.org/wiki/Certificate_signing_request>.
countryName                     = Country Name (2 letter code)
stateOrProvinceName             = State or Province Name
localityName                    = Locality Name
0.organizationName              = Organization Name
organizationalUnitName          = Organizational Unit Name
commonName                      = Common Name
emailAddress                    = Email Address

# Optionally, specify some defaults.
countryName_default             = PT
stateOrProvinceName_default     = Lisboa
localityName_default            = Lisboa
0.organizationName_default      = TiagoJoaoSilva
organizationalUnitName_default  = TJS
emailAddress_default            = bofh@tjs.lan

[ v3_ca ]
# Extensions for a typical CA ('man x509v3_config').
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true
keyUsage = critical, digitalSignature, cRLSign, keyCertSign

[ v3_intermediate_ca ]
# Extensions for a typical intermediate CA ('man x509v3_config').
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints = critical, CA:true, pathlen:0
keyUsage = critical, digitalSignature, cRLSign, keyCertSign

[ usr_cert ]
# Extensions for client certificates ('man x509v3_config').
basicConstraints = CA:FALSE
nsCertType = client, email
nsComment = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = clientAuth, emailProtection

[ server_cert ]
# Extensions for server certificates ('man x509v3_config').
basicConstraints = CA:FALSE
nsCertType = server
nsComment = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer:always
keyUsage = critical, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth

[ crl_ext ]
# Extension for CRLs ('man x509v3_config').
authorityKeyIdentifier=keyid:always

[ ocsp ]
# Extension for OCSP signing certificates ('man ocsp').
basicConstraints = CA:FALSE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
keyUsage = critical, digitalSignature
extendedKeyUsage = critical, OCSPSigning


We create a random complex password:

1
2
3
openssl rand -base64 12 > /root/ca/private/ca.key.pass
chmod 400 /root/ca/private/ca.key.pass
cat /root/ca/private/ca.key.pass


ayUiXnIjZFRh


We create the key and provide the complex password:

1
2
openssl genrsa -aes256 -passout file:/root/ca/private/ca.key.pass \
-out /root/ca/private/ca.key.pem 8192


Generating RSA private key, 8192 bit long modulus (2 primes)
......................................................+++
..+++
e is 65537 (0x010001)


Then we create (and self-sign) the RootCA certificate:

1
2
3
4
5
6
chmod 400 /root/ca/private/ca.key.pem
openssl req -config /root/ca/openssl.cnf \
    -passin file:/root/ca/private/ca.key.pass \
    -key /root/ca/private/ca.key.pem \
    -new -x509 -days 7300 -sha512 -extensions v3_ca \
    -out /root/ca/certs/ca.cert.pem

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [PT]:
State or Province Name [Lisboa]:
Locality Name [Lisboa]:
Organization Name [TiagoJoaoSilva]:
Organizational Unit Name [TJS]:
Common Name []:ca-tjs-1
Email Address [bofh@tjs.lan]:

1
chmod 444 /root/ca/certs/ca.cert.pem

We can check the certificate’s contents:

1
openssl x509 -noout -text -in /root/ca/certs/ca.cert.pem

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            25:92:4d:14:da:6f:9f:8b:4b:49:55:fc:68:be:9e:c7:57:24:cd:70
        Signature Algorithm: sha512WithRSAEncryption
        Issuer: C = PT, ST = Lisboa, L = Lisboa, O = TiagoJoaoSilva, OU = TJS, CN = ca-tjs-1, emailAddress = bofh@tjs.lan
        Validity
            Not Before: Aug 12 19:38:30 2021 GMT
            Not After : Aug  7 19:38:30 2041 GMT
        Subject: C = PT, ST = Lisboa, L = Lisboa, O = TiagoJoaoSilva, OU = TJS, CN = ca-tjs-1, emailAddress = bofh@tjs.lan
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (8192 bit)
                Modulus: 
[...]
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Subject Key Identifier:
                01:F2:CC:54:E0:F2:58:AC:E2:14:8E:2B:DB:6D:B6:FF:5C:25:41:A0
            X509v3 Authority Key Identifier:
                keyid:01:F2:CC:54:E0:F2:58:AC:E2:14:8E:2B:DB:6D:B6:FF:5C:25:41:A0

            X509v3 Basic Constraints: critical
                CA:TRUE
            X509v3 Key Usage: critical
                Digital Signature, Certificate Sign, CRL Sign
    Signature Algorithm: sha512WithRSAEncryption
 [...]

Certificate Revocation

Before we go any further, I think we have to talk right now about the revocation of certificates, unlike all other Internet tutorials which leave it until the end, if at all; if we do not configure Certificate Revocation now, it will be too late to be included in any certificate created by our RootCA.

How to revoke a certificate

1
2
3
4
5
6
stamp=$(date +%F_%T)
mv /root/ca/inter-ca/certs/example-ca.cert.pem{,-revoked_$stamp}
openssl ca -config /root/ca/openssl.cnf \
    -passin file:/root/ca/private/ca.key.pass \
    -revoke /root/ca/inter-ca/certs/example-ca.cert.pem-revoked_$stamp \
    -crl_reason unspecified

-crl_reason can also have the values keyCompromise, CACompromise, affiliationChanged, superseded, cessationOfOperation, certificateHold, or (for Delta-CRLs only) removeFromCRL
-crl_hold automatically places the reason as certificateHold but that’s outside the scope of this document
-crl_compromise AAAAMMDDHHMMSSZ sets the reason as keyCompromise on the suppied date.
-crl_CA_compromise AAAAMMDDHHMMSSZ does the same, but the reason is CACompromise

We need to set up a mechanism for the RootCA to disseminate the information of the revoked certificates, and this needs to be done before creating any subsequent certificate, by adding information to the RootCA’s openssl.cnf before de creating any certificate (up to and including other CAs).

CRL distribution

The classic method is the direct distribution of the CRL lists, which can be done by various means, such as an HTTP server, an LDAP or AD directory, etc.; there may still be an additional complication because to avoid the constant transmission of large files you can use Delta-CRLs containing only the changes to the CRL, but while these can be implemented in Windows or other PKI packages, they are not available on OpenSSL.
These lists are usually updated (because they expire and all CA certificates are no longer accepted) at a maximum of every 30 days, but the RootCA’s CRL list should not expire (or expire only annually when it is time to update the system that hosts the RootCA).

[CA_default]
default_crl_days  = 385

At that time, the CRL is refreshed and (in the case of an airgapped RootCA system) extracted on paper, QR Code, CD-R or other safe medium, then taken to the system from where it will be distributed.
The distribution format of these lists is DER (binary coding), but the file must have a .CRL extension (RFC 2585).
The other method, more recommended these days, is using an OCSP Responder to answer queries about the validity of a certificate interactively, which is a quick operation that does not require much bandwidth, certifies the revocation by signing it with a key from the originator CA, and that answer can be cached on the clients (through OCSP Stapling).

As the RootCA’s CRL will contain very few certificates (the RootCA will only issue other CAs, called IntermediateCAs, SubordinateCAs ou
SigningCAs, which won’t be many), let’s set up a CRLDistribution package for the RootCA.

We begin by putting the information from where the CRL will be served on the [ v3_intermediate_ca ] section of /root/ca/openssl.cnf, to be
Included in any certificates issued by the RootCA.
We will use an HTTP server on port 7788:

[ v3_intermediate_ca ]
crlDistributionPoints = URI:http://server.tjs.lan:7788/root-ca.crl

We create the CRL (https://blog.didierstevens.com/2013/05/08/howto-make-your-own-cert-and-revocation-list-with-openssl/)

1
2
3
4
openssl ca -config /root/ca/openssl.cnf -gencrl \
    -passin file:/root/ca/private/ca.key.pass \
    -keyfile /root/ca/private/ca.key.pem -cert /root/ca/certs/ca.cert.pem \
    -out /root/ca/crl/ca.crl.pem

We verify the contents of the CRL:

1
openssl crl -in /root/ca/crl/ca.crl.pem -noout -text


Certificate Revocation List (CRL):
        Version 2 (0x1)
        Signature Algorithm: sha512WithRSAEncryption
        Issuer: C = PT, ST = Lisboa, L = Lisboa, O = TiagoJoaoSilva, OU = TJS, CN = ca-tjs-1, emailAddress = bofh@tjs.lan
        Last Update: Aug 13 23:46:07 2021 GMT
        Next Update: Sep  2 23:46:07 2022 GMT
        CRL extensions:
            X509v3 Authority Key Identifier:
                keyid:01:F2:CC:54:E0:F2:58:AC:E2:14:8E:2B:DB:6D:B6:FF:5C:25:41:A0

            X509v3 CRL Number:
                4097
Revoked Certificates:
    Serial Number: 1001
        Revocation Date: Aug 13 22:11:25 2021 GMT
        CRL entry extensions:
            X509v3 CRL Reason Code:
                Unspecified
    Serial Number: 1002
        Revocation Date: Aug 13 23:41:51 2021 GMT
        CRL entry extensions:
            X509v3 CRL Reason Code:
                Superseded
    Signature Algorithm: sha512WithRSAEncryption 
    [...]


And we need to convert the PEM (Base64) into DER format:

1
2
# openssl crl -inform PEM -in /root/ca/crl/ca.crl.pem \
-outform DER -out /root/ca/crl/root-ca.crl

Network-connected system

Since the RootCA should be airgapped and offline, it is not on the RootCA’s system that we should configure the CRL’s distribution because that system must be accessible on the network.
We set up a new Debian 11 system, and recreate the folder that contains the RootCA’s CRL:

1
mkdir -p /root/ca/crl

We bring the root-ca.crl file to it using a safe mechanism (burn a CD-R, printing and scanning with OCR, etc.) e place it in /root/ca/crl

Install and configure the CRLDistribution webserver

Let’s use a very basic webserver, lighttpd

1
apt install lighttpd -y

According to Debian’s wiki about Lighttpd, the default installation configures the service, grabs the 80/TCP port, runs with the wser www-data, set the webroot to be /var/www/html, and the default file to be index.php or index.html, although it comes pre-configured to not run .php files.
Let’s set up a different port (7788), another webroot to not interfere with the defaults from other webservers, and we will disable the automatic redirection to index.html
In addition, and this can be important, the MIME type settings have been pre-configured with a Perl script, which we may have to edit and re-do because the CRL files have to be served by HTTP with a specific MIME type, application/pkix-crl
(https://pki-tutorial.readthedocs.io/en/latest/mime.html)

1
cp /etc/lighttpd/lighttpd.conf{,.orig}

Change the lines:

 1 server.modules = (
 2 #   "mod_indexfile",
 3 #   "mod_access",
 4 #   "mod_alias",
 5 #   "mod_redirect",
 6 )
 8 server.document-root        = "/var/www/crl"
10 server.errorlog             = "/var/log/lighttpd/crl-error.log"
14 server.port                 = 7788
41 #index-file.names            = ( "index.php", "index.html" )

Create a new webroot:

1
mkdir /var/www/crl

Move the original index.html installed by the lighttpd package to that place and deactivate it:

1
2
mv /var/www/html/index.lighttpd.html
/var/www/crl/index.lighttpd.html.disabled

Place the DER-format CRL inside webroot:

1
2
3
cp /root/ca/crl/root-ca.crl /var/www/crl/root-ca.crl
chown www-data:www-data /var/www/crl/root-ca.crl
chmod 400 /var/www/crl/root-ca.crl

To configure the MIME types, the lighttpd.conf package called a script that creates a MIME configuration from the types defined in /etc/mime.types; did the latter already have a definition for the CRL’s MIME type?

1
grep pkix-crl /etc/mime.types


application/pkix-crl crl


Therefore, line 47 of lighttpd.conf will create a list of MIME types that includes our CRL type each time the lighttpd service is started.

47 include_shell "/usr/share/lighttpd/create-mime.conf.pl"

We check lighttpd.conf’s syntax for errors:

1
lighttpd -tt -f /etc/lighttpd/lighttpd.conf


2021-08-14 02:26:38: configfile.c.1142) WARNING: unknown config-key:
url.access-deny (ignored)


This error isn’t important, so:

1
2
systemctl restart lighttpd
systemctl status lighttpd

● lighttpd.service - Lighttpd Daemon
Loaded: loaded (/lib/systemd/system/lighttpd.service; enabled; vendor preset: enabled)
Active: active (running)

1
systemctl enable lighttpd


Synchronizing state of lighttpd.service with SysV service script with /lib/systemd/systemd-sysv-install.
Executing: /lib/systemd/systemd-sysv-install enable lighttpd


We open port 7788 in the firewall:

1
ufw allow 7788

And test access to the file:

1
wget localhost:7788/root-ca.crl


Resolving localhost (localhost)... ::1, 127.0.0.1
Connecting to localhost (localhost)|::1|:7788... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1359 (1.3K) [application/pkix-crl]
Saving to: ‘root-ca.crl’

root-ca.crl                   100%[=================================================>]   1.33K  --.-KB/s    in 0s

2021-08-14 02:33:18 (168 MB/s) - ‘root-ca.crl’ saved [1359/1359] 


Connection was established and the file is served with the correct MIME type.

But unfortunately, you should only use CRLs when we are sure that our clients support this format (and in this regard, none is better than Microsoft). For example, it is impossible to revoke webserver certificates by CRLDistribution because only Internet Explorer has the code to download and analyze a CRL (Firefox and Chrome don’t even care) https://news.netcraft.com/archives/2013/05/13/how-certificate-revocation-doesnt-work-in-practice.html .
That’s why it’s safer to set up the alternative mechanism, which is an OCSP Responder, also called AIA due to the directive that configures it ( AuthorityInfoAccess).

Setting up the OCSP Responder on the RootCA’s CNF

We start by placing on the RootCA’s openssl.cnf the following, on the [ intermediate_ca ] section:

authorityInfoAccess = OCSP;URI:http://ocsp.tjs.lan:7899

We can use any TCP port for the OCSP Responder, and it’s usually listening on a seldom-used HTTP port (like 8008). I chose port 7899 because after analyzing IANA’s port list, TCP ports 7888-7899 are not assigned, and may be used for this purpose. If we weren’t using CRLD, Lighttpd could also be switched off and port 7788 reused.
The protocol is HTTP and not HTTPS because how would any OCSP’s SSL Certificate be checked? (https://social.technet.microsoft.com/Forums/office/en-US/c65e1784-39be-4732-a135-bfba7446ad05/should-the-ocsp-responder-service-be-running-http-80-or-https-443-)
The configuration of an OCSP Responder will be documented in the next part, which will about configuring an IntermediateCA. But we left the information in the RootCA’s .CNF in case we want to implement an OCSP Responder later.

Note: if this information is not configured now, it can only be introduced on the PKI (and passed to any downstream certificates issued by the RootCA) by revoking and re-issuing all certificates already issued; therefore the structure of a PKI should be very well-planned before being implemented.

Now we can start the IntermediateCA’s configuration.