How to generate a CSR with SANs in PHP

A simple tutorial on how to generate a CSR with SANs in PHP. Code samples are supplied.

How to generate a CSR with SANs in PHP

I recent project required me to generate a CSR programmatically in PHP. There are lots of tutorials on the generation of CSR with just a Common Name (with no SANs). To my surprise, as soon as you add in a list of SANs, the process is much more complex, and the tutorials are quite thin.

In this post I hope to make this process as simple as possible for everyone, in the hopes it helps at least one other person.

Step 1 - Create a Distinguished Name

An SSL certificate contains your Distinguished Name information to help your users trust your certificate. You should replace the dummy data with your own

// The Distinguished Name to be used in the certificate.
$dn = [
  'commonName' => example.com,
  'organizationName' => 'ACME Inc',
  'organizationalUnitName' => 'IT',
  'localityName' => 'Seattle',
  'stateOrProvinceName' => 'Washington',
  'countryName' => 'US',
  'emailAddress' => 'foo@example.com',
];

Step 2 - Generate a new private key

Here is where you generate your private key. 4096 bits is used as this is stronger than the default of 2048 bits.

// Generates a new private key
$privateKey = openssl_pkey_new([
  'private_key_type' => OPENSSL_KEYTYPE_RSA,
  'private_key_bits' => 4096
]);

Step 3 - Generate OpenSSL config file

For some reason, OpenSSL in PHP requires you to create a file to supply arguments to add in SANs, they cannot be supplied in PHP.

[ req ]
distinguished_name = req_distinguished_name
req_extensions = v3_req

[ req_distinguished_name ]

[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = @san

[ san ]
{% for san in sans %}
DNS.{{ loop.index }} = {{ san }}
{% endfor %}

I render the above template using twig, and then create a file with the contents of the compiled template.

file_put_contents('/tmp/openssl.cnf', $contents);

If you did not want to use twig, the format of the [ san ] section down the bottom is very simple to replicate in raw PHP, or another templating language, after being compiled, it should look something like this (replace the SANs with your own):

[ san ]
DNS.1 = www.example.com
DNS.2 = shop.example.com
DNS.3 = foo.example.com
DNS.4 = *.foo.com

Essentially, a 1 indexed, newline separated, list of SANs you want to list on your certificate.

N.B. You should not include your Common Name in the SAN list.

Step 4 - Generate the CSR

Here is the magic that pulls in all the above code, and exports your CSR and private key into files on your filesystem.

$csrResource = openssl_csr_new($dn, $privateKey, [
  'digest_alg' => 'sha256',
  'config' => '/tmp/openssl.cnf',
]);

openssl_csr_export($csrResource, $csrString);
openssl_pkey_export($privateKey, $privateKeyString);

file_put_contents('/tmp/private.key', $privateKeyString);
file_put_contents('/tmp/public.csr', $csrString);

Final thoughts

This process seemed fairly complex for what I thought to be a simple process. I found no PHP libraries thought would help make this process a bit more Object Oriented. If you happen to know of a CSR generation PHP library let me know in the comments.