<?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 truthyecho match ($result) { 1 => "valid\n", 0 => "INVALID\n", default => "error: openssl_verify returned $result\n",}; <?php// r‖s from a JWS or WebCrypto (64 bytes on P-256): rebuild DER, which is all ext-openssl readsfunction 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
openssl_pkey_get_publicaccepts the PEM text of aBEGIN PUBLIC KEYblock (or a certificate) and returns anOpenSSLAsymmetricKey, orfalsewith the reason inopenssl_error_string().openssl_verify($message, $signature, $key, OPENSSL_ALGO_SHA256)hashes the message itself; pass the original bytes, never a digest.- The result is an integer:
1for a valid signature,0for an invalid one,-1for an error. Thematchcompares it exactly. rawToDerwraps each half ofr‖sas a DER INTEGER — leading zeros stripped, a0x00added when the top bit is set — inside a SEQUENCE, the only signature encoding OpenSSL's verifier reads.
Gotchas
if (openssl_verify(...))accepts errors:-1is truthy in PHP. Passing the raw 64-byte signature straight toopenssl_verifyreturns 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()returnsfalse, 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 returns0, indistinguishable from a forgery. - Key parameters given as a path need the
file://prefix; a bare path is treated as PEM text and fails withSupplied key param cannot be coerced into a public key.