Skip to content
ecdsa.com

Export public key PEM · PHP

How to export a public key as PEM in PHP

To export a public key as PEM in PHP from an ECDSA private key, load the private key with openssl_pkey_get_private and take the 'key' entry of openssl_pkey_get_details — it is already the SPKI BEGIN PUBLIC KEY block other stacks read. The same details array holds the curve coordinates for a JWK, with one PHP-specific catch shown in the second snippet.

Tested with PHP 8.5.11 (ext/openssl, OpenSSL 3.5.7) and OpenSSL 3.5.7 (openssl pkey cross-check).

export.php — private key PEM (PKCS#8 or SEC 1) in, SPKI PEM out
<?php
// PKCS#8 ("BEGIN PRIVATE KEY") or SEC 1 ("BEGIN EC PRIVATE KEY"); add the passphrase if it is encrypted
$privateKey = openssl_pkey_get_private(file_get_contents($argv[1] ?? 'key.pem'));
if ($privateKey === false) {
throw new RuntimeException('cannot read the private key: ' . openssl_error_string());
}
$publicPem = openssl_pkey_get_details($privateKey)['key']; // SPKI, "BEGIN PUBLIC KEY"
file_put_contents('pub.pem', $publicPem);
echo $publicPem;
jwk.php — the same public key as a JWK, with fixed-width coordinates
<?php
// The same public key as a JWK: the curve point's coordinates in base64url, no padding
$ec = openssl_pkey_get_details(openssl_pkey_get_public(file_get_contents($argv[1] ?? 'pub.pem')))['ec'];
// PHP drops leading zero bytes; a JWK coordinate must keep the full 32 bytes of P-256
$coordinate = fn (string $bytes): string =>
rtrim(strtr(base64_encode(str_pad($bytes, 32, "\0", STR_PAD_LEFT)), '+/', '-_'), '=');
echo json_encode(['kty' => 'EC', 'crv' => 'P-256', 'x' => $coordinate($ec['x']), 'y' => $coordinate($ec['y'])]), "\n";

How it works

  1. openssl_pkey_get_private reads BEGIN PRIVATE KEY and BEGIN EC PRIVATE KEY alike (the run checks both give the same output), plus encrypted keys when you pass the passphrase.
  2. openssl_pkey_get_details($key)['key'] is the public half as SPKI PEM — byte-identical to openssl pkey -pubout, which the run compares.
  3. For a JWK, ['ec']['x'] and ['ec']['y'] are the raw coordinates; base64url without padding turns them into the JWK's x and y.
  4. str_pad(..., 32, "\0", STR_PAD_LEFT) restores the fixed width P-256 coordinates must have; the run includes a real key whose x starts with a zero byte.

Gotchas

  • PHP returns EC coordinates without leading zero bytes. Generating 3,000 P-256 keys in PHP, 25 had a 31-byte x, y or d; a JWK built from those bytes as-is is rejected by strict parsers (WebCrypto among them) or describes a different point. Always pad to the curve size.
  • The details array of a private key also contains the secret scalar ['ec']['d']. Take only what you need from it and never log or serialize the whole array.
  • Hard-coding 'crv' => 'P-256' is only right for P-256 keys; map ['ec']['curve_name'] (prime256v1, secp384r1, secp521r1) to P-256, P-384, P-521 and the padding width (32, 48, 66) when keys vary.
  • An encrypted key without its passphrase makes openssl_pkey_get_private return false; pass the passphrase as the second argument.

Related recipes