Skip to content
ecdsa.com

Sign a message · PHP

How to sign a message with ECDSA in PHP

To sign a message with ECDSA in PHP, load the private key with openssl_pkey_get_private and call openssl_sign — the OpenSSL extension that ships with PHP does the hashing and signing. The result is an ASN.1 DER signature that OpenSSL, Java and Python verify as-is; the algorithm argument must be given explicitly, because PHP's default is still SHA-1.

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

sign.php — SHA-256 with ECDSA, DER output
<?php
// a path needs the file:// scheme; the second argument is the passphrase of an encrypted key
$privateKey = openssl_pkey_get_private('file://' . __DIR__ . '/key.pem', getenv('KEY_PASSPHRASE') ?: null);
if ($privateKey === false) {
throw new RuntimeException('cannot read the private key: ' . openssl_error_string());
}
$message = file_get_contents('message.txt');
if (!openssl_sign($message, $signature, $privateKey, OPENSSL_ALGO_SHA256)) { // the default would be SHA-1
throw new RuntimeException('signing failed: ' . openssl_error_string());
}
file_put_contents('sig.der', $signature); // ASN.1 DER, at most 72 bytes on P-256
echo 'DER signature: ', strlen($signature), " bytes\n";
echo 'base64: ', base64_encode($signature), "\n"; // for JSON or HTTP headers

How it works

  1. openssl_pkey_get_private accepts PEM text or a file:// path, plus an optional passphrase for an encrypted key; it reads both BEGIN PRIVATE KEY and BEGIN EC PRIVATE KEY.
  2. openssl_sign writes the signature into its second argument (by reference) and returns true or false, so the failure case is checked explicitly.
  3. OPENSSL_ALGO_SHA256 makes OpenSSL hash the message with SHA-256 and sign the digest with ECDSA — the key type selects ECDSA, the constant selects the hash.
  4. The signature is binary DER, varying in length from signature to signature (at most 72 bytes on P-256); base64_encode makes it safe for JSON or HTTP headers. The run verifies it with openssl dgst -verify.

Gotchas

  • Without the fourth argument openssl_sign uses SHA-1. The signature is valid, but a verifier expecting SHA-256 returns 0 — the most common reason PHP-made ECDSA signatures "never verify" elsewhere.
  • The output is DER. JWS (ES256) and WebCrypto expect the fixed 64-byte r‖s form; convert it as in the DER ⇄ raw recipe, or a JWT library will reject the token.
  • Every call returns different bytes: OpenSSL draws a random nonce for each signature. Compare signatures by verifying them, never byte for byte.
  • A bare path such as 'key.pem' is treated as PEM text and fails; prefix it with file://, or pass file_get_contents().

Related recipes