Skip to content
ecdsa.com

Self-signed certificate · PHP

How to create a self-signed EC P-256 certificate with SANs in PHP

PHP's ext-openssl turns a CSR into a certificate with openssl_csr_sign; passing null as the CA certificate makes the result self-signed. The extensions come from the config section named by x509_extensions, so the recipe ships a small cert.cnf. The run decodes cert.pem with OpenSSL and verifies it for localhost and 127.0.0.1.

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

cert.cnf — the extension section openssl_csr_sign reads
# cert.cnf — openssl_csr_sign takes certificate extensions only from a config section
[ req ]
distinguished_name = req_dn
[ req_dn ]
[ server_cert ]
subjectAltName = DNS:localhost, IP:127.0.0.1
basicConstraints = critical, CA:FALSE
keyUsage = critical, digitalSignature
extendedKeyUsage = serverAuth
cert.php — P-256 key, CSR and self-signed certificate
<?php
$options = ['config' => __DIR__ . '/cert.cnf', 'x509_extensions' => 'server_cert', 'digest_alg' => 'sha256'];
$key = openssl_pkey_new(['private_key_type' => OPENSSL_KEYTYPE_EC, 'curve_name' => 'prime256v1']); // P-256
$csr = openssl_csr_new(['commonName' => 'localhost'], $key, $options);
// CA certificate null = self-signed; validity in days, counted from now
$cert = openssl_csr_sign($csr, null, $key, 365, $options, random_int(1, PHP_INT_MAX));
if ($cert === false) {
throw new RuntimeException(openssl_error_string());
}
openssl_x509_export($cert, $certPem);
openssl_pkey_export($key, $keyPem);
file_put_contents('cert.pem', $certPem);
file_put_contents('key.pem', $keyPem);

How it works

  1. openssl_pkey_new with OPENSSL_KEYTYPE_EC and curve_name => 'prime256v1' creates a P-256 key; openssl_csr_new builds a CSR for commonName => 'localhost'.
  2. openssl_csr_sign($csr, null, $key, 365, $options, $serial) signs the request with its own key — null as the CA certificate means self-signed — for 365 days from now.
  3. x509_extensions => 'server_cert' names the cert.cnf section with subjectAltName, basicConstraints, keyUsage and extendedKeyUsage; digest_alg => 'sha256' sets the signature hash.
  4. openssl_x509_export and openssl_pkey_export return the PEM strings for cert.pem and key.pem.

Gotchas

  • The serial number defaults to 0 (serial=00 in OpenSSL's output), which RFC 5280 does not allow — serials must be positive. Pass a random one, such as random_int(1, PHP_INT_MAX).
  • Without digest_alg, the certificate is signed with SHA-1 (ecdsa-with-SHA1); pass 'digest_alg' => 'sha256'.
  • Without x509_extensions, and with a config whose [ req ] section names no default, the certificate has no extensions at all — no SANs and no basicConstraints.
  • A misspelt section name does not throw: openssl_csr_sign returns false with only a warning (Error loading extensions_section section missing_section of c.cnf), so check the result before exporting.

Related recipes