Skip to content
ecdsa.com

Generate a CSR · PHP

How to generate a CSR for an EC P-256 key with SANs in PHP

PHP generates a CSR with ext-openssl: openssl_pkey_new creates the P-256 key and openssl_csr_new builds and signs the request. Extensions such as subjectAltName can only come from an OpenSSL config section, so the recipe ships a three-line csr.cnf. The run decodes the CSR with OpenSSL.

Tested with PHP 8.5.11 (ext-openssl (OpenSSL 3.5.7)).

csr.cnf — the extension section openssl_csr_new reads
# csr.cnf — openssl_csr_new takes extensions only from a config section
[ req ]
distinguished_name = req_dn
req_extensions = v3_req
[ req_dn ]
[ v3_req ]
subjectAltName = DNS:api.example.com, DNS:www.example.com
csr.php — P-256 key and CSR with two SANs
<?php
$key = openssl_pkey_new(['private_key_type' => OPENSSL_KEYTYPE_EC, 'curve_name' => 'prime256v1']); // P-256
$csr = openssl_csr_new(
['countryName' => 'US', 'organizationName' => 'Example Corp', 'commonName' => 'api.example.com'],
$key,
['config' => __DIR__ . '/csr.cnf', 'req_extensions' => 'v3_req', 'digest_alg' => 'sha256'],
);
if ($csr === false) {
throw new RuntimeException(openssl_error_string());
}
openssl_csr_export($csr, $csrPem);
openssl_pkey_export($key, $keyPem); // PKCS#8; pass a passphrase to encrypt it
file_put_contents('csr.pem', $csrPem);
file_put_contents('key.pem', $keyPem);

How it works

  1. openssl_pkey_new with OPENSSL_KEYTYPE_EC and curve_name => 'prime256v1' creates the key (prime256v1 is OpenSSL's name for P-256).
  2. The first argument of openssl_csr_new is the subject; keys are OpenSSL's long names (countryName, organizationName, commonName).
  3. config points at csr.cnf and req_extensions names its v3_req section, which holds subjectAltName.
  4. openssl_csr_export and openssl_pkey_export return the PEM strings; openssl_pkey_export writes PKCS#8.

Gotchas

  • Without digest_alg, the request is signed with SHA-1 (ecdsa-with-SHA1 in OpenSSL's output). CAs reject such CSRs; always pass 'digest_alg' => 'sha256'.
  • There is no option for SANs in the PHP API: without a config section, the CSR has only a CN, which modern clients do not accept for host names.
  • openssl_csr_new returns false on failure without throwing; check the result and read openssl_error_string() for the reason.
  • For dynamic SAN lists, either write the config file at runtime or put subjectAltName = $ENV::SAN in it and set SAN=DNS:a.example.com,DNS:b.example.com in the environment — OpenSSL expands it when it reads the section.

Related recipes