In this example we are going to authenticate client request with X.509 Public Key Infrastructure (PKI) standard. The main advantage of using this authentication standard over other types such as Basic, Digest, JWT, OAuth etc. is that the client neither sends a username nor password to the server. This makes the man-in-the-middle (MITM) attack (the attacker secretly relays and possibly alters the communication) harder. The client sends a personalised certificate and a key to the server. In general stealing the login credentials is easier than stealing a certificate.


System setup


The system is based on Debian 8.10 (Jessie) and uses Nginx Server 1.6.2 along with PHP 7.2.6.


Steps


  1. Create PHP API and configure Nginx server: Setup a dummy PHP API so that the client can consume it without going through any authentication.

  2. Create server and client certificates: Create necessary certificate and key files for both the server and the client.

  3. Nginx server re-configuration: Update Nginx configuration so that X.509 authentication is enforced at server level before hitting the API.

  4. Client certificate and key transfer: Hand client's certificate and key over so that both can be used to consume the API.

  5. Test: Test authentication process.

1. Create PHP API and configure Nginx server


Setup a dummy PHP API so that the client can consume it without going through any authentication.


$ mkdir /srv/www/football

$ nano /srv/www/football/index.php

# This is the file content
echo PHP_EOL;
print_r($_SERVER);
echo PHP_EOL;

$ sudo nano /etc/nginx/sites-available/football

# This is the file content
server {
listen 83;

root /srv/www/football;

server_name football.dev;

location / {
index index.php index.html index.htm;
try_files $uri $uri/ =404;
}

location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php7.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}

error_log /var/log/nginx/football_error.log;
access_log /var/log/nginx/football_access.log;
}

$ sudo ln -s /etc/nginx/sites-available/football /etc/nginx/sites-enabled/football

$ sudo service nginx restart

$ curl http://192.168.99.20:83

# This is the result (Access to http://192.168.99.20:83/ via browser produces the similar output)
Array
(
[USER] => www-data
[HOME] => /var/www
[HTTP_ACCEPT] => */*
[HTTP_HOST] => 192.168.99.20:83
[HTTP_USER_AGENT] => curl/7.38.0
[REDIRECT_STATUS] => 200
[SERVER_NAME] => football.dev
[SERVER_PORT] => 83
[SERVER_ADDR] => 192.168.99.20
[REMOTE_PORT] => 40541
[REMOTE_ADDR] => 192.168.99.20
[SERVER_SOFTWARE] => nginx/1.6.2
[GATEWAY_INTERFACE] => CGI/1.1
[SERVER_PROTOCOL] => HTTP/1.1
[DOCUMENT_ROOT] => /srv/www/football
[DOCUMENT_URI] => /index.php
[REQUEST_URI] => /
[SCRIPT_NAME] => /index.php
[CONTENT_LENGTH] =>
[CONTENT_TYPE] =>
[REQUEST_METHOD] => GET
[QUERY_STRING] =>
[SCRIPT_FILENAME] => /srv/www/football/index.php
[PATH_INFO] =>
[FCGI_ROLE] => RESPONDER
[PHP_SELF] => /index.php
[REQUEST_TIME_FLOAT] => 1539377667.6224
[REQUEST_TIME] => 1539377667
)

2. Create server and client certificates


Run all commands below on the server. Send the client key and certificate to whom (in a trusted manner) will be using to consume our API. Note: Optionally you can combine client key and certificate to generate a pem file and let clients use it instead of asking them to use two different files. This is actually important if you want to implement Certificate revocation list (CRL) but we will not do it for now. Sometimes we want to revoke certificates before their expiry date for any given reason so that's why this is an important feature to have.


Create Certificate Authority (CA) private key and certificate


These will be used for Self Signing Client Certificates. CA, in real World examples, can be VeriSign, Thawte, Symantec, Comodo so on but at this case CA is yourself. They provide X.509 certificates.


$ sudo mkdir -p /etc/nginx/ssl/key
$ sudo mkdir -p /etc/nginx/ssl/certificate

$ sudo openssl genrsa -out /etc/nginx/ssl/key/football_ca.key 4096

Generating RSA private key, 4096 bit long modulus
............++
.....++
65537 (0x10001)

$ sudo openssl req -new -x509 -days 365 -key /etc/nginx/ssl/key/football_ca.key -out /etc/nginx/ssl/certificate/football_ca.crt

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) [AU]:UK
State or Province Name (full name) [Some-State]:Greater London
Locality Name (eg, city) []:City of London
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Football Stats Ltd.
Organizational Unit Name (eg, section) []:Tech
Common Name (e.g. server FQDN or YOUR name) []:football.dev
Email Address []:info@football.dev

$ ls -l /etc/nginx/ssl/key
-rw-r--r-- 1 root root 3247 Oct 16 22:13 football_ca.key

$ ls -l /etc/nginx/ssl/certificate
-rw-r--r-- 1 root root 2183 Oct 16 22:18 football_ca.crt

Create server private key and Certificate Signing Request (CSR)


A certificate signing request (CSR) is a message sent from an applicant to a Certificate Authority (CA) in order to apply for a digital identity certificate.


$ sudo openssl genrsa -out /etc/nginx/ssl/key/football.key 1024

Generating RSA private key, 1024 bit long modulus
...++++++
.........................++++++
65537 (0x10001)

$ sudo openssl req -new -key /etc/nginx/ssl/key/football.key -out /etc/nginx/ssl/certificate/football.csr

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) [AU]:UK
State or Province Name (full name) [Some-State]:Greater London
Locality Name (eg, city) []:City of London
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Football Stats Ltd.
Organizational Unit Name (eg, section) []:Tech
Common Name (e.g. server FQDN or YOUR name) []:football.dev
Email Address []:info@football.dev

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:123123
An optional company name []:Footy

$ ls -l /etc/nginx/ssl/key
-rw-r--r-- 1 root root 3247 Oct 16 22:13 football_ca.key
-rw-r--r-- 1 root root 887 Oct 16 22:24 football.key

$ ls -l /etc/nginx/ssl/certificate
-rw-r--r-- 1 root root 2183 Oct 16 22:18 football_ca.crt
-rw-r--r-- 1 root root 798 Oct 16 22:27 football.csr

Self-signing our own server certificate


Avoid this in production. Instead, obtain one from a Certificate Authority (such as VeriSign, Thawte, Symantec, Comodo so on) since it is considered "risky". Although self-signed SSL Certificates encrypt customers' login credentials and other sensitive data, they prompt web servers to display a security alert. Similar alerts occurs when using cURL and Guzzle as well. The reason is because the certificate was not actually verified by a trusted Certificate Authority (CA). Hence reason such alert would cause your users to leave your site.


$ sudo openssl x509 -req -days 365 -in /etc/nginx/ssl/certificate/football.csr -CA /etc/nginx/ssl/certificate/football_ca.crt -CAkey /etc/nginx/ssl/key/football_ca.key -set_serial 01 -out /etc/nginx/ssl/certificate/football.crt

Signature ok
subject=/C=UK/ST=Greater London/L=City of London/O=Football Stats Ltd./OU=Tech/CN=football.dev/emailAddress=info@football.dev
Getting CA Private Key

$ ls -l /etc/nginx/ssl/certificate
-rw-r--r-- 1 root root 2183 Oct 16 22:18 football_ca.crt
-rw-r--r-- 1 root root 1529 Oct 16 22:33 football.crt
-rw-r--r-- 1 root root 798 Oct 16 22:27 football.csr

Create client key and Certificate Signing Request (CSR)


The client key will be sent to actual client in a trusted manner so that they can use it while consuming our API.


$ sudo openssl genrsa -out /etc/nginx/ssl/key/client_a.key 1024

Generating RSA private key, 1024 bit long modulus
.......++++++
.++++++
65537 (0x10001)

$ sudo openssl req -new -key /etc/nginx/ssl/key/client_a.key -out /etc/nginx/ssl/certificate/client_a.csr

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) [AU]:UK
State or Province Name (full name) [Some-State]:Greater Manchester
Locality Name (eg, city) []:City of Manchester
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Client A Ltd.
Organizational Unit Name (eg, section) []:Sales
Common Name (e.g. server FQDN or YOUR name) []:client_a.com
Email Address []:info@client_a.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:aaaaaa
An optional company name []:CA Ltd.

$ ls -l /etc/nginx/ssl/key
-rw-r--r-- 1 root root 887 Oct 16 22:36 client_a.key
-rw-r--r-- 1 root root 3247 Oct 16 22:13 football_ca.key
-rw-r--r-- 1 root root 887 Oct 16 22:24 football.key

$ ls -l /etc/nginx/ssl/certificate
-rw-r--r-- 1 root root 802 Oct 16 22:40 client_a.csr
-rw-r--r-- 1 root root 2183 Oct 16 22:18 football_ca.crt
-rw-r--r-- 1 root root 1529 Oct 16 22:33 football.crt
-rw-r--r-- 1 root root 798 Oct 16 22:27 football.csr

Signing the client certificate


The client certificate will be sent to actual client in a trusted manner so that they can use it while consuming our API.


$ sudo openssl x509 -req -days 365 -in /etc/nginx/ssl/certificate/client_a.csr -CA /etc/nginx/ssl/certificate/football_ca.crt -CAkey /etc/nginx/ssl/key/football_ca.key -set_serial 01 -out /etc/nginx/ssl/certificate/client_a.crt

Signature ok
subject=/C=UK/ST=Greater Manchester/L=City of Manchester/O=Client A Ltd./OU=Sales/CN=client_a.com/emailAddress=info@client_a.com
Getting CA Private Key

$ ls -l /etc/nginx/ssl/certificate

-rw-r--r-- 1 root root 1533 Oct 16 22:45 client_a.crt
-rw-r--r-- 1 root root 802 Oct 16 22:40 client_a.csr
-rw-r--r-- 1 root root 2183 Oct 16 22:18 football_ca.crt
-rw-r--r-- 1 root root 1529 Oct 16 22:33 football.crt
-rw-r--r-- 1 root root 798 Oct 16 22:27 football.csr

Generate pem file (optional)


You can combine client key and certificate to generate pem file and let clients use it instead of asking them to use two different files. As described above, this is important only if you are going to use Certificate revocation list (CRL). Privacy Enhanced Mail (PEM) is a Base64 encoded certificate. PEM certificates are frequently used for web servers. It is used as --cert, not --key.


$ sudo bash -c 'cat /etc/nginx/ssl/certificate/client_a.crt /etc/nginx/ssl/key/client_a.key > /etc/nginx/ssl/certificate/client_a.pem'

3. Nginx server re-configuration


Update Nginx configuration so that X.509 authentication is enforced at server level.


server {
listen 83;

root /srv/www/football;

server_name football.dev;

listen 443;
ssl on;
ssl_certificate /etc/nginx/ssl/certificate/football.crt;
ssl_certificate_key /etc/nginx/ssl/key/football.key;
ssl_client_certificate /etc/nginx/ssl/certificate/football_ca.crt;
ssl_verify_client optional; # Set to "on" if you only allow authenticated requests

location / {
index index.php index.html index.htm;
try_files $uri $uri/ =404;
}

location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php7.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SSL_CLIENT_VERIFY $ssl_client_verify;
fastcgi_param SSL_CLIENT_S_DN $ssl_client_s_dn;
include fastcgi_params;
}

error_log /var/log/nginx/football_error.log;
access_log /var/log/nginx/football_access.log;
}

If you want to prevent API is being hit when the certificate is invalid you can add code below to location ~ \.php$ block of configuration above so this is optional.


if ($ssl_client_verify != SUCCESS) {
return 403;
}

$ sudo nginx -c /etc/nginx/nginx.conf -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

$ sudo service nginx restart

4. Client certificate and key transfer


Hand over client's certificate client_a.crt and key client_a.key so that both can be used by the client to consume the API.


5. Test


We are now on client's computer and consuming the API which is based on server computer. Pay attention to SSL_CLIENT_VERIFY and SSL_CLIENT_S_DN keys. We have to use --insecure flag because Self Signed SSL Certificates are being used. If we don't, we would get curl: (60) SSL certificate problem: self signed certificate error message.


# VALID
$ curl --insecure --key /srv/www/client_a.key --cert /srv/www/client_a.crt https://192.168.99.20:83

# OR pem version
# $ curl --insecure --cert /srv/www/client_a.pem https://192.168.99.20:83

Array
(
[USER] => www-data
[HOME] => /var/www
[HTTP_ACCEPT] => */*
[HTTP_HOST] => 192.168.99.20:83
[HTTP_USER_AGENT] => curl/7.38.0
[SSL_CLIENT_VERIFY] => SUCCESS
[SSL_CLIENT_S_DN] => /C=UK/ST=Greater Manchester/L=City of Manchester/O=Client A Ltd./OU=Sales/CN=client_a.com/emailAddress=info@client_a.com
[REDIRECT_STATUS] => 200
[SERVER_NAME] => football.dev
[SERVER_PORT] => 83
[SERVER_ADDR] => 192.168.99.20
[REMOTE_PORT] => 42553
[REMOTE_ADDR] => 192.168.99.30
[SERVER_SOFTWARE] => nginx/1.6.2
[GATEWAY_INTERFACE] => CGI/1.1
[HTTPS] => on
[SERVER_PROTOCOL] => HTTP/1.1
[DOCUMENT_ROOT] => /srv/www/football
[DOCUMENT_URI] => /index.php
[REQUEST_URI] => /
[SCRIPT_NAME] => /index.php
[CONTENT_LENGTH] =>
[CONTENT_TYPE] =>
[REQUEST_METHOD] => GET
[QUERY_STRING] =>
[SCRIPT_FILENAME] => /srv/www/football/index.php
[PATH_INFO] =>
[FCGI_ROLE] => RESPONDER
[PHP_SELF] => /index.php
[REQUEST_TIME_FLOAT] => 1539727722.2362
[REQUEST_TIME] => 1539727722
)

# MALFORMED certificate
$ curl --insecure --key /srv/www/client_a.key --cert /srv/www/client_a.crt https://192.168.99.20:83
curl: (58) unable to use client certificate (no key found or wrong pass phrase?)

# WRONG certificate
$ curl --insecure --key /srv/www/client_a.key --cert /srv/www/client_x.crt https://192.168.99.20:83
curl: (58) unable to set private key file: '/srv/www/client_a.key' type PEM

# WRONG key
$ curl --insecure --key /srv/www/client_x.key --cert /srv/www/client_a.crt https://192.168.99.20:83
curl: (58) unable to set private key file: '/srv/www/client_x.key' type PEM

References