Skip to content
ecdsa.com

Verify a signature · PHP

How to verify an ECDSA signature in PHP

To verify an ECDSA signature in PHP, the bundled OpenSSL extension is enough: openssl_pkey_get_public loads the key and openssl_verify checks an ASN.1 DER signature. Two details decide whether the check is real — openssl_verify returns -1 on errors, which PHP treats as true, and it reads DER only, so raw r‖s signatures from JWS or WebCrypto must be re-encoded first.

Tested with PHP 8.5.11 (ext/openssl, OpenSSL 3.5.7).

verify.php — verify a DER signature with ext-openssl
<?php
$publicKey = openssl_pkey_get_public(file_get_contents('pub.pem')); // "BEGIN PUBLIC KEY"
if ($publicKey === false) {
throw new RuntimeException('cannot read the public key: ' . openssl_error_string());
}
$message = file_get_contents('message.txt');
$signature = file_get_contents($argv[1] ?? 'sig.der'); // ASN.1 DER, as OpenSSL produces it
$result = openssl_verify($message, $signature, $publicKey, OPENSSL_ALGO_SHA256);
// 1 = valid, 0 = invalid, -1 = error (for example, bytes that are not DER) — and -1 is truthy
echo match ($result) {
1 => "valid\n",
0 => "INVALID\n",
default => "error: openssl_verify returned $result\n",
};
verify-raw.php — raw r‖s signatures: rebuild the DER, then verify
<?php
// r‖s from a JWS or WebCrypto (64 bytes on P-256): rebuild DER, which is all ext-openssl reads
function rawToDer(string $raw): string
{
$integer = function (string $bytes): string {
$bytes = ltrim($bytes, "\0");
if ($bytes === '' || ord($bytes[0]) & 0x80) {
$bytes = "\0" . $bytes; // DER INTEGERs are signed: keep the value positive
}
return "\x02" . chr(strlen($bytes)) . $bytes;
};
$half = intdiv(strlen($raw), 2);
$body = $integer(substr($raw, 0, $half)) . $integer(substr($raw, $half));
return "\x30" . (strlen($body) > 127 ? "\x81" : '') . chr(strlen($body)) . $body;
}
$publicKey = openssl_pkey_get_public(file_get_contents('pub.pem'));
$der = rawToDer(file_get_contents('sig.raw'));
$result = openssl_verify(file_get_contents('message.txt'), $der, $publicKey, OPENSSL_ALGO_SHA256);
echo $result === 1 ? "valid\n" : "INVALID\n";

How it works

  1. openssl_pkey_get_public accepts the PEM text of a BEGIN PUBLIC KEY block (or a certificate) and returns an OpenSSLAsymmetricKey, or false with the reason in openssl_error_string().
  2. openssl_verify($message, $signature, $key, OPENSSL_ALGO_SHA256) hashes the message itself; pass the original bytes, never a digest.
  3. The result is an integer: 1 for a valid signature, 0 for an invalid one, -1 for an error. The match compares it exactly.
  4. rawToDer wraps each half of r‖s as a DER INTEGER — leading zeros stripped, a 0x00 added when the top bit is set — inside a SEQUENCE, the only signature encoding OpenSSL's verifier reads.

Gotchas

  • if (openssl_verify(...)) accepts errors: -1 is truthy in PHP. Passing the raw 64-byte signature straight to openssl_verify returns exactly that -1 (the run shows it), so a format mistake would pass a truthiness check. Always compare with === 1.
  • For that raw-signature case the OpenSSL error queue stays empty — openssl_error_string() returns false, so there is no message to log beyond the return value.
  • The fourth argument defaults to OPENSSL_ALGO_SHA1. A SHA-256 signature checked without it returns 0, indistinguishable from a forgery.
  • Key parameters given as a path need the file:// prefix; a bare path is treated as PEM text and fails with Supplied key param cannot be coerced into a public key.

Related recipes