<?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; <?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
openssl_pkey_get_privatereadsBEGIN PRIVATE KEYandBEGIN EC PRIVATE KEYalike (the run checks both give the same output), plus encrypted keys when you pass the passphrase.openssl_pkey_get_details($key)['key']is the public half as SPKI PEM — byte-identical toopenssl pkey -pubout, which the run compares.- For a JWK,
['ec']['x']and['ec']['y']are the raw coordinates; base64url without padding turns them into the JWK'sxandy. 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) toP-256,P-384,P-521and the padding width (32, 48, 66) when keys vary. - An encrypted key without its passphrase makes
openssl_pkey_get_privatereturnfalse; pass the passphrase as the second argument.