Skip to content
ecdsa.com

Decode a certificate · PHP

How to decode an X.509 certificate in PHP

To decode an X.509 certificate in PHP, read it with openssl_x509_read and turn it into an array with openssl_x509_parse — subject, issuer, serial, validity as Unix timestamps and every extension as OpenSSL's text. The public key, its curve and the certificate fingerprint come from neighbouring functions of the same bundled OpenSSL extension.

Tested with PHP 8.5.11 (ext/openssl, OpenSSL 3.5.7) and OpenSSL 3.5.7 (DER copy of the certificate).

decode.php — subject, validity, SANs, key and fingerprint
<?php
$cert = openssl_x509_read(file_get_contents('cert.pem')); // PEM text, or 'file://' . $path
if ($cert === false) {
throw new RuntimeException('not a certificate: ' . openssl_error_string());
}
$info = openssl_x509_parse($cert);
echo 'subject: ', $info['name'], "\n"; // OpenSSL's one-line form
echo 'common name: ', $info['subject']['CN'], "\n";
echo 'issuer organization: ', $info['issuer']['O'], "\n";
echo 'serial: ', $info['serialNumberHex'], "\n";
echo 'valid: ', gmdate('c', $info['validFrom_time_t']), ' -> ', gmdate('c', $info['validTo_time_t']), "\n";
echo 'signed with: ', $info['signatureTypeSN'], "\n"; // the issuer's algorithm
echo 'SANs: ', $info['extensions']['subjectAltName'] ?? '(none)', "\n";
echo 'key usage: ', $info['extensions']['keyUsage'] ?? '(none)', "\n";
$key = openssl_pkey_get_details(openssl_pkey_get_public($cert));
if ($key['type'] === OPENSSL_KEYTYPE_EC) {
echo 'key: EC ', $key['bits'], '-bit on ', $key['ec']['curve_name'], "\n";
}
echo 'SHA-256 fingerprint: ', openssl_x509_fingerprint($cert, 'sha256'), "\n"; // default would be SHA-1
echo 'days until expiry: ', intdiv($info['validTo_time_t'] - time(), 86400), "\n";
der.php — binary DER certificates and a signature check
<?php
// openssl_x509_read wants PEM: wrap binary DER (e.g. from a .cer file) first
function derToPem(string $der): string
{
return "-----BEGIN CERTIFICATE-----\n" . chunk_split(base64_encode($der), 64, "\n") . "-----END CERTIFICATE-----\n";
}
$cert = openssl_x509_read(derToPem(file_get_contents('cert.der')));
echo 'DER input: ', openssl_x509_parse($cert)['subject']['CN'], "\n";
echo 'signed by its own key: ', openssl_x509_verify($cert, openssl_pkey_get_public($cert)) === 1 ? 'yes' : 'no', "\n";

How it works

  1. openssl_x509_read returns an OpenSSLCertificate for PEM text (or a file:// path) and false otherwise; the other openssl_x509_* functions accept that object.
  2. openssl_x509_parse returns the subject and issuer both as arrays (['subject']['CN']) and as OpenSSL's one-line name, the serial as serialNumberHex, and the validity as validFrom_time_t / validTo_time_t Unix timestamps.
  3. Extensions come back as the strings openssl x509 -text prints: subjectAltName is DNS:a, DNS:b, keyUsage is Digital Signature.
  4. openssl_pkey_get_public($cert) extracts the key; its details give the type, size and curve_name. openssl_x509_fingerprint($cert, 'sha256') hashes the whole DER certificate.
  5. openssl_x509_verify($cert, $key) returns 1 when that key signed the certificate — for a self-signed certificate, its own key.

Gotchas

  • openssl_x509_read and openssl_x509_parse do not accept binary DER: the run's raw .cer bytes fail with PEM routines::no start line. Wrap DER in PEM armor first, as derToPem does.
  • openssl_x509_fingerprint($cert) without the algorithm returns SHA-1. Browsers and current tools show SHA-256, so the default output matches nothing they display.
  • Parsed extension values are display text, not data: split subjectAltName on , and strip the DNS: prefix to get host names, and do not rely on the exact wording of other extensions across OpenSSL versions.
  • Decoding is not validation. For trust decisions use openssl_x509_checkpurpose with a CA file, or let the TLS layer (cURL, stream contexts with verify_peer) validate the chain.

Related recipes