user4271704
user4271704

Reputation: 763

How to sign some data using public/private RSA key?

I have a RSA public/private key xml file. And I want to use it to sign some data, with the following classes and here is how I use the class:

$processor = new RSAProcessor("certificate.xml", RSAKeyType:: XMLFile);
$data =  $processor->sign($data); 
print(base64_encode($data));

but I get the error: WARNING: str_repeat(): Second argument has to be greater than or equal to 0 in RSA.php on line 81. How to fix it? I guess something is outdated for php 7.2? I appreciate your help what changes should I do in my class to avoid this error?

Rsa class:

define("BCCOMP_LARGER", 1);
class RSA {
 static function rsa_encrypt($message, $public_key, $modulus, $keylength) {
  $padded = RSA::add_PKCS1_padding($message, true, $keylength / 8);
  $number = RSA::binary_to_number($padded);
  $encrypted = RSA::pow_mod($number, $public_key, $modulus);
  $result = RSA::number_to_binary($encrypted, $keylength / 8);
  return $result;
 }
 static function rsa_decrypt($message, $private_key, $modulus, $keylength) {
  $number = RSA::binary_to_number($message);
  $decrypted = RSA::pow_mod($number, $private_key, $modulus);
  $result = RSA::number_to_binary($decrypted, $keylength / 8);
  return RSA::remove_PKCS1_padding($result, $keylength / 8);
 }
 static function rsa_sign($message, $private_key, $modulus, $keylength) {
  $padded = RSA::add_PKCS1_padding($message, false, $keylength / 8);
  $number = RSA::binary_to_number($padded);
  $signed = RSA::pow_mod($number, $private_key, $modulus);
  $result = RSA::number_to_binary($signed, $keylength / 8);
  return $result;
 }
 static function rsa_verify($message, $public_key, $modulus, $keylength) {
  return RSA::rsa_decrypt($message, $public_key, $modulus, $keylength);
 }
 static function rsa_kyp_verify($message, $public_key, $modulus, $keylength) {
  $number = RSA::binary_to_number($message);
  $decrypted = RSA::pow_mod($number, $public_key, $modulus);
  $result = RSA::number_to_binary($decrypted, $keylength / 8);
  return RSA::remove_KYP_padding($result, $keylength / 8);
 }
 static function pow_mod($p, $q, $r) {
  $factors = array();
  $div = $q;
  $power_of_two = 0;
while(bccomp($div, "0") == BCCOMP_LARGER)  {
   $rem = bcmod($div, 2);
   $div = bcdiv($div, 2);
   if($rem) array_push($factors, $power_of_two);
   $power_of_two++;
  }
  $partial_results = array();
  $part_res = $p;
  $idx = 0;
  foreach($factors as $factor)  {
   while($idx < $factor)
   {
    $part_res = bcpow($part_res, "2");
    $part_res = bcmod($part_res, $r);
    $idx++;
   }
   array_push($partial_results, $part_res);
  }
  $result = "1";
  foreach($partial_results as $part_res)
  {
   $result = bcmul($result, $part_res);
   $result = bcmod($result, $r);
  }
  return $result;
 }
 static  function add_PKCS1_padding($data, $isPublicKey, $blocksize)
 {
  $pad_length = $blocksize - 3 - strlen($data);
  if($isPublicKey)
  {
   $block_type = "\x02";
   $padding = "";
   for($i = 0; $i < $pad_length; $i++)
   {
    $rnd = mt_rand(1, 255);
    $padding .= chr($rnd);
   }
  }
  else
  {
   $block_type = "\x01";
   $padding = str_repeat("\xFF", $pad_length);
  }
  return "\x00" . $block_type . $padding . "\x00" . $data;
 }
  static function remove_PKCS1_padding($data, $blocksize)
 {
  assert(strlen($data) == $blocksize);
  $data = substr($data, 1);
  if($data{0} == '\0')
  die("Block type 0 not implemented.");
  assert(($data{0} == "\x01") || ($data{0} == "\x02"));
  $offset = strpos($data, "\0", 1);
  return substr($data, $offset + 1);
 }
  static function remove_KYP_padding($data, $blocksize)
 {
  assert(strlen($data) == $blocksize);
  $offset = strpos($data, "\0");
  return substr($data, 0, $offset);
 }
 static function binary_to_number($data)
 {
  $base = "256";
  $radix = "1";
  $result = "0";
  for($i = strlen($data) - 1; $i >= 0; $i--)
  {
   $digit = ord($data{$i});
   $part_res = bcmul($digit, $radix);
   $result = bcadd($result, $part_res);
   $radix = bcmul($radix, $base);
  }
  return $result;
  }
 static  function number_to_binary($number, $blocksize)
 {
  $base = "256";
  $result = "";
  $div = $number;
  while($div > 0)
  {
   $mod = bcmod($div, $base);
   $div = bcdiv($div, $base);
   $result = chr($mod) . $result;
  }
  return str_pad($result, $blocksize, "\x00", STR_PAD_LEFT);
 }
}

RSAProcessor class:

class RSAProcessor
{
 private $public_key = null;
 private $private_key = null;
 private $modulus = null;
 private $key_length = "1024";
 public function __construct($xmlRsakey=null,$type=null)
 {
         $xmlObj = null;
         if ($xmlRsakey==null) {
             $xmlObj = simplexml_load_file("xmlfile/RSAKey.xml");
          } elseif ($type==RSAKeyType::XMLFile) {
             $xmlObj = simplexml_load_file($xmlRsakey);
          } else {
             $xmlObj = simplexml_load_string($xmlRsakey);
          }
         $this->modulus = RSA::binary_to_number(base64_decode($xmlObj->Modulus));
         $this->public_key = RSA::binary_to_number(base64_decode($xmlObj->Exponent));
         $this->private_key = RSA::binary_to_number(base64_decode($xmlObj->D));
         $this->key_length = strlen(base64_decode($xmlObj->Modulus))*8;
 }
 public function getPublicKey() {
  return $this->public_key;
 }
 public function getPrivateKey() {
  return $this->private_key;
 }
 public function getKeyLength() {
  return $this->key_length;
 }
 public function getModulus() {
  return $this->modulus;
 }
 public function encrypt($data) {
  return base64_encode(RSA::rsa_encrypt($data,$this->public_key,$this->modulus,$this->key_length));
 }
  public function dencrypt($data) {
  return RSA::rsa_decrypt($data,$this->private_key,$this->modulus,$this->key_length);
 }
  public function sign($data) {
  return RSA::rsa_sign($data,$this->private_key,$this->modulus,$this->key_length);
 }
  public function verify($data) {
  return RSA::rsa_verify($data,$this->public_key,$this->modulus,$this->key_length);
 }
}


class RSAKeyType{
 const XMLFile = 0;
 const XMLString = 1;
}

certificate.xml:

<RSAKeyValue>
<Modulus>tCZiqDS5BVQQZDBUYbyeoP4rENN4mX5FZJjjMNfGbyzfzH45RY2/YsMaY0yI1jMCOpukvkUyl153tcn0LXhMCDdsEhhZPoKbPUGMniKtFGjs18rv/b5FFUUW1utgwoL8+WJqjOqhQGgvbja63X9+WMFP0nM3d8yudn9C/X55KyM=</Modulus>
    <Exponent>AQAB</Exponent>
    <P>5HXvmU4IfqUG2jFLSqi/BMEQ3x1NsUDvx3zN5O1p9yLLspJ4sqAt4RUkxzcGodYgBSdXsR9IGcPwjQfbx3a7nQ==</P>
    <Q>yd2hDCF/5Zqtz9DXjY1NRYGvBjTS4AQn83ERR46Y5eBSnLjpVjv6gPfARuhsUP44nikrQPvwPnjxQcOhJaOlvw==</Q>
    <DP>ztuqUplBP8qU5cN0dOlN7DQT3rFdw30Unv/2Pa5qIAc1gT72YmZ+pCrM3kSIkMicvY3d7NZyJkIv8MKI0ZZEUQ==</DP>
    <DQ>QFLJ5YarLWubZPQEK4vSCornTY/5ff51CIKH4ghTOjS/vkbBu4PDL+NCNpYLJcfMHMG7kap2BEIfhjgjGk5KGw==</DQ>
    <InverseQ>WE6TqpcexQJwt9Mnp1FbeLtarBcFkXVdBauouFKHcbHCfQjA3IjUrGTxgSO74O/4QSKqaF2gnlL6GI7gKuGbzQ==</InverseQ>  <D>czYtWDfHsFGv3fNOs+cGaB3E+xDTiw7HYGuquJz2qjk/s69x/zqFEKuIH8Ndq+eZYFQUCx+EGGxxENDkmYPa0z8wbfFI6JEHpxaLmQfpkkbSL1BJIp9Z5BNM2gy6jJqgbWwQPcN/4jpiMefHZWAqhMKqenUu1KIq1ZX6Bz5xKYk=</D>
</RSAKeyValue>

I appreciate your help what changes should I do in my class to avoid this error?

Upvotes: 0

Views: 830

Answers (1)

Topaco
Topaco

Reputation: 49121

In my local environment, the code runs smoothly, and also on http://phpfiddle.org/ (PHP 7.0.33)! For the latter, however, the key must be passed as string, because online no filesystem is available:

$privateKey = "<RSAKeyValue>...</RSAKeyValue>";
$processor = new RSAProcessor($privateKey, RSAKeyType::XMLString);

On the other hand, the same code fails on http://sandbox.onlinephpfunctions.com/ (PHP 7.01 - PHP 7.3.5) with the same error message as yours. The reason is that the method simplexml_load_string() is disabled. Thus the key cannot be loaded and the member variables $modulus, $key_length, $private_key and $public_key of the RSAProcessor-class get the value 0. As a result the method add_PKCS1_padding, called with $blocksize=$keylength/8, determines a negative value for $pad_length=$blocksize-3-strlen($data), which transparently leads to the error message str_repeat(): Second argument has to be greater than or equal to 0 when str_repeat("\xFF", $pad_length) is called.

If I disable the method simplexml_load_file in my local environment, I can reproduce exactly this behavior. Since the code works in principle, it can be assumed that methods are disabled on your system as well, especially simplexml_load_file.

Disabled methods can be enabled by removing them from the disable_functions-list in the php.ini.

Update:

As already mentioned in the comments, RSA is supported by the vast majority of libraries, e.g. phpseclib, here, which also supports the key format used in your code, here.

In general for PKCS#1, not the data itself, but their hashes with preceding digest info (DER encoding of the DigestInfo value) are signed according to RFC 8017, see also here, which is why a digest must also be specified. Assuming that your data (i.e. $data which is passed to your RSAProcessor#sign-method) follow this convention, i.e.:

$dataToSign = 'The quick brown fox jumps over the lazy dog'; 
$data = hex2bin('3031300d060960864801650304020105000420' . hash('SHA256', $dataToSign, false));

the following code using the phpseclib-library (with SHA256 as digest) would generate the same signature:

include('RSA.php');
include('BigInteger.php');

$rsa = new Crypt_RSA();

$privateKey = "<RSAKeyValue>...</RSAKeyValue>";
$dataToSign = 'The quick brown fox jumps over the lazy dog';

$rsa->setPrivateKeyFormat(CRYPT_RSA_PRIVATE_FORMAT_XML);  // private key format
$rsa->loadKey($privateKey);

$rsa->setHash('sha256');                                  // digest for signing
$rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);        // RSASSA-PKCS1-v1_5-padding
$signature = $rsa->sign($dataToSign);

echo 'Signature (hex):    ' . bin2hex($signature) . "\n";
echo 'Signature (base64): ' . base64_encode($signature) . "\n";

Both codes then provide the following signature (as hex string):

6f00eb1126470e097991aaa1e3e94b49a7b8de6c9b2bf683382f96563479b87067be20635ccc1d36fedff9d60f682fdefcb108f5351dc672ebb442e49231d0306b302b258f6b21fb724b59ad765151f6c724de214d41afa0e1b08228b070c537020df8acf9cad5f2fdde63bc698875527a52e49155bd5e2fd761f541844e92bb

Update 2:

For SHA1, the ID plus hash for your code can be determined as follows:

$data = hex2bin('3021300906052b0e03021a05000414' . hash('SHA1', $dataToSign, false));

or

$data = hex2bin('3021300906052b0e03021a05000414' . SHA1($dataToSign, false));

Either the hash-function or the sha1-function can be used. In both cases, the last parameter $raw_output controls whether the return is a binary (TRUE) or a hex string (FALSE). Because of the concatenation with the ID I return the hash as hex string, do the concatenation and finally convert explicitly with hex2bin into the binary representation. The digest ID can also be found here, page 47.

In the phpseclib-code

$rsa->setHash('sha1');

must be used.

Upvotes: 1

Related Questions