<?php/** DER SEQUENCE { INTEGER r, INTEGER s } to fixed-width r‖s; $size: 32 P-256, 48 P-384, 66 P-521. */function derToRaw(string $der, int $size): string{ $offset = 0; $read = function (int $length) use ($der, &$offset): string { if ($offset + $length > strlen($der)) { throw new InvalidArgumentException('truncated DER signature'); } $bytes = substr($der, $offset, $length); $offset += $length; return $bytes; }; if ($read(1) !== "\x30") throw new InvalidArgumentException('not a DER SEQUENCE'); $length = ord($read(1)); if ($length === 0x81) $length = ord($read(1)); // long form: P-521 only if ($length !== strlen($der) - $offset) throw new InvalidArgumentException('bad SEQUENCE length'); $raw = ''; for ($i = 0; $i < 2; $i++) { if ($read(1) !== "\x02") throw new InvalidArgumentException('expected INTEGER'); $value = ltrim($read(ord($read(1))), "\0"); // drop the sign byte if (strlen($value) > $size) throw new InvalidArgumentException('integer does not fit the curve'); $raw .= str_pad($value, $size, "\0", STR_PAD_LEFT); // left-pad to the fixed width } return $raw;} /** Fixed-width r‖s to minimal DER. */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;} <?phprequire 'signature-formats.php'; $der = hex2bin('30450221009f4118b059a34a2f344c39a1d085d93bab560402938281dc1cb2f3dfb8eee58602200bd54b34a72042551c3316e10f461a1d69036ba4f0d72d95d42465340b56def8');$raw = derToRaw($der, 32);echo 'raw: ', bin2hex($raw), "\n";echo 'round trip: ', rawToDer($raw) === $der ? 'true' : 'false', "\n";echo 'JWS form: ', rtrim(strtr(base64_encode($raw), '+/', '-_'), '='), "\n"; // base64url, no padding How it works
- A DER signature is
30 len 02 len r 02 len s.derToRawreads that layout with bounds checks, so truncated input, a wrong tag, a mismatched length or trailing bytes raiseInvalidArgumentExceptioninstead of producing garbage. - Each INTEGER may carry a leading
0x00sign byte and may be shorter than the curve size.ltrimremoves the zeros andstr_pad(..., STR_PAD_LEFT)restores the fixed width. rawToDerreverses it: strip leading zeros, add a0x00when the top bit is set (DER INTEGERs are signed), wrap both in a SEQUENCE; P-521 signatures need the long-form length0x81.- The JWS form is the raw bytes in base64url without padding:
strtrswaps the alphabet andrtrimdrops the=.
Gotchas
$x ?: "\0"is a tempting shortcut for "empty string to zero byte", but the one-character string"0"(byte 0x30) is falsy in PHP too — an integer whose remaining byte is 0x30 would be silently corrupted. Compare with=== '', as the code does.- Slicing DER at fixed offsets (
substr($der, 4, 32)) works on most signatures and fails on the ones with a 31-byte r or s, or a 33-byte one with a sign byte. The run converts two real 31-byte cases and 1,000 fresh signatures on P-256 and P-521. $sizemust match the curve: 32 for P-256, 48 for P-384, 66 for P-521. A wrong size raises "integer does not fit the curve" or produces a signature of the wrong length.openssl_verifygiven raw bytes returns-1, not0— and-1is truthy. Convert before verifying, and compare the result with=== 1.