* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace AcmePhp\Ssl\Signer; use AcmePhp\Ssl\Exception\DataSigningException; use AcmePhp\Ssl\PrivateKey; use Webmozart\Assert\Assert; /** * Provide tools to sign data using a private key. * * @author Titouan Galopin */ class DataSigner { public const FORMAT_DER = 'DER'; public const FORMAT_ECDSA = 'ECDSA'; /** * Generate a signature of the given data using a private key and an algorithm. * * @param string $data Data to sign * @param PrivateKey $privateKey Key used to sign * @param int $algorithm Signature algorithm defined by constants OPENSSL_ALGO_* * @param string $format Format of the output */ public function signData(string $data, PrivateKey $privateKey, int $algorithm = OPENSSL_ALGO_SHA256, string $format = self::FORMAT_DER): string { Assert::oneOf($format, [self::FORMAT_ECDSA, self::FORMAT_DER], 'The format %s to sign request does not exists. Available format: %s'); $resource = $privateKey->getResource(); if (!openssl_sign($data, $signature, $resource, $algorithm)) { throw new DataSigningException(sprintf('OpenSSL data signing failed with error: %s', openssl_error_string())); } // PHP 8 automatically frees the key instance and deprecates the function if (\PHP_VERSION_ID < 80000) { openssl_free_key($resource); } switch ($format) { case self::FORMAT_DER: return $signature; case self::FORMAT_ECDSA: switch ($algorithm) { case OPENSSL_ALGO_SHA256: return $this->DERtoECDSA($signature, 64); case OPENSSL_ALGO_SHA384: return $this->DERtoECDSA($signature, 96); case OPENSSL_ALGO_SHA512: return $this->DERtoECDSA($signature, 132); } throw new DataSigningException('Unable to generate a ECDSA signature with the given algorithm'); default: throw new DataSigningException('The given format does exists'); } } /** * Convert a DER signature into ECDSA. * * The code is a copy/paste from another lib (web-token/jwt-core) which is not compatible with php <= 7.0 * * @see https://github.com/web-token/jwt-core/blob/master/Util/ECSignature.php */ private function DERtoECDSA($der, $partLength): string { $hex = unpack('H*', $der)[1]; if ('30' !== mb_substr($hex, 0, 2, '8bit')) { // SEQUENCE throw new DataSigningException('Invalid signature provided'); } if ('81' === mb_substr($hex, 2, 2, '8bit')) { // LENGTH > 128 $hex = mb_substr($hex, 6, null, '8bit'); } else { $hex = mb_substr($hex, 4, null, '8bit'); } if ('02' !== mb_substr($hex, 0, 2, '8bit')) { // INTEGER throw new DataSigningException('Invalid signature provided'); } $Rl = hexdec(mb_substr($hex, 2, 2, '8bit')); $R = $this->retrievePositiveInteger(mb_substr($hex, 4, $Rl * 2, '8bit')); $R = str_pad($R, $partLength, '0', STR_PAD_LEFT); $hex = mb_substr($hex, 4 + $Rl * 2, null, '8bit'); if ('02' !== mb_substr($hex, 0, 2, '8bit')) { // INTEGER throw new DataSigningException('Invalid signature provided'); } $Sl = hexdec(mb_substr($hex, 2, 2, '8bit')); $S = $this->retrievePositiveInteger(mb_substr($hex, 4, $Sl * 2, '8bit')); $S = str_pad($S, $partLength, '0', STR_PAD_LEFT); return pack('H*', $R.$S); } /** * The code is a copy/paste from another lib (web-token/jwt-core) which is not compatible with php <= 7.0. * * @see https://github.com/web-token/jwt-core/blob/master/Util/ECSignature.php */ private function retrievePositiveInteger($data) { while ('00' === mb_substr($data, 0, 2, '8bit') && mb_substr($data, 2, 2, '8bit') > '7f') { $data = mb_substr($data, 2, null, '8bit'); } return $data; } }