返回

GMP反转运算陷阱与避坑:模逆元计算详解

php

GMP 反转运算的陷阱与避坑指南

使用 GMP 扩展进行大数运算时,gmp_invert 函数常用于计算模逆元,这在密码学等领域十分常见。但初学者经常会遇到诸如 "Code is not working with gmp_invert("2",$p), why?" 的问题,表面上看逻辑似乎正确,实则隐藏着一些微妙的坑。本篇文章深入剖析这类问题的原因并提供实用的解决方案。

问题分析

上述代码的核心问题在于 gmp_invert 函数返回的 类型 和期望的类型不匹配,造成了后续操作错误。

具体来说,gmp_invert 返回的是一个 GMP resource (或 GMP 对象) ,它表示一个任意精度的整数,而非字符串或者 PHP 标量类型的数字。 在 PHP 中,直接输出一个资源类型的结果会显示资源的 id,因此,echo 打印时会得到 "resource id #...", 此时,$k 并不会成为数字,更不会是我们期望的数字 57896044618658097711785492504343953926634992332820282019728792003954417335832 。后续代码尝试用字符串类型的私钥创建密钥时出现错误,导致了问题。

错误信息 Warning: gmp_init(): Unable to convert variable to GMP - wrong type 非常清晰地表明,gmp_init 函数期待一个能够转换为 GMP 类型的参数(例如字符串,或者一个整数),而我们传递给它的却是 GMP resource 类型的变量。

当直接赋予字符串 $k 一个数值时,它正确地被理解成了一个代表大数的字符串,从而被 gmp_init 正确处理,后续逻辑顺利进行。 而使用gmp_invert时,返回了 gmp 资源对象,这就与原本的代码产生了不兼容的情况。

解决方案

为了正确地使用 gmp_invert 返回的结果,并将其作为 gmp_init 的输入,我们需要将 GMP resource 对象 显式转换为字符串形式 。 可以利用 gmp_strval 或者 gmp_export 函数完成此操作。

以下提供三种解决方案。

方案一:使用 gmp_strval() 显式转换

gmp_strval() 将 GMP resource 对象转换为对应的十进制字符串, 我们可以安全地使用字符串传递给 gmp_init() 或者其他需要字符串表示的地方。

<?php

class BitcoinECDSA {
    public $k, $p, $G;

    public function __construct() {
        $this->p = gmp_init('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F', 16);
        $this->G = ['x' => gmp_init('55066263022277343669578718895168534326250603453777594175500187360389116729240'),
                     'y' => gmp_init('32670510020758816978083085130507043184471273380659243275938904335757337482424')];

        
    }

    public function doublePoint($pt) {
        $p = $this->p;
        $slope = gmp_mod(gmp_mul(gmp_invert(gmp_mod(gmp_mul(gmp_init(2), $pt['y']), $p), $p), gmp_add(gmp_mul(gmp_init(3), gmp_pow($pt['x'], 2)), gmp_init(0))), $p);
        return ['x' => gmp_mod(gmp_sub(gmp_sub(gmp_pow($slope, 2), $pt['x']), $pt['x']), $p),
                'y' => gmp_mod(gmp_sub(gmp_mul($slope, gmp_sub($pt['x'], gmp_mod(gmp_sub(gmp_sub(gmp_pow($slope, 2), $pt['x']), $pt['x']), $p))), $pt['y']), $p)];
    }

    public function addPoints($pt1, $pt2) {
        if (gmp_cmp($pt1['x'], $pt2['x']) === 0 && gmp_cmp($pt1['y'], $pt2['y']) === 0) return $this->doublePoint($pt1);
        $slope = gmp_mod(gmp_mul(gmp_sub($pt1['y'], $pt2['y']), gmp_invert(gmp_sub($pt1['x'], $pt2['x']), $this->p)), $this->p);
        return ['x' => gmp_mod(gmp_sub(gmp_sub(gmp_pow($slope, 2), $pt1['x']), $pt2['x']), $this->p),
                'y' => gmp_mod(gmp_sub(gmp_mul($slope, gmp_sub($pt1['x'], gmp_mod(gmp_sub(gmp_sub(gmp_pow($slope, 2), $pt1['x']), $pt2['x']), $this->p))), $pt1['y']), $this->p)];
    }

    public function mulPoint($k, $pG) {
        $k = gmp_init($k, 10);
        $kBin = gmp_strval($k, 2);
        $lastPoint = $pG;
        for ($i = 1; $i < strlen($kBin); $i++) {
            $lastPoint = $kBin[$i] === '1' ? $this->addPoints($this->doublePoint($lastPoint), $pG) : $this->doublePoint($lastPoint);
        }
        return $lastPoint;
    }

    public function getPubKeyPoints() {
        if (!isset($this->k)) throw new \Exception('No Private Key defined');
        $pubKey = $this->mulPoint($this->k, $this->G);
        return ['x' => $pubKey['x'],
                'y' => $pubKey['y']];
    }

    public function getPubKey(array $pubKeyPts = []) {
        if (empty($pubKeyPts)) $pubKeyPts = $this->getPubKeyPoints();
          return  $pubKeyPts;
    }

    public function setPrivateKey($k) {
      $this->k = $k;
  }
}

// Ensure $p is a GMP number
$p = gmp_init('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F', 16);

// Initialize the private key as a GMP object, explicitly converted
$k = gmp_strval(gmp_invert("2",$p));

// $k = "57896044618658097711785492504343953926634992332820282019728792003954417335832";
$bitcoinECDSA = new BitcoinECDSA();
$bitcoinECDSA->setPrivateKey($k);
$s = $bitcoinECDSA->getPubKey();
print_r($s);


?>

步骤:

  1. 使用 gmp_invert("2",$p) 计算 2 关于模 p 的逆元,得到 GMP resource 对象。
  2. 使用 gmp_strval() 将该资源对象转换为一个十进制的字符串。
  3. 现在可以正常运行。

方案二:直接使用 gmp 资源对象进行操作

在设置私钥时候直接设置资源, 在mulpoint等方法中先将其转换为字符创然后 gmp_init 使用。

<?php

class BitcoinECDSA {
    public $k, $p, $G;

    public function __construct() {
        $this->p = gmp_init('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F', 16);
        $this->G = ['x' => gmp_init('55066263022277343669578718895168534326250603453777594175500187360389116729240'),
                     'y' => gmp_init('32670510020758816978083085130507043184471273380659243275938904335757337482424')];

        
    }

    public function doublePoint($pt) {
        $p = $this->p;
        $slope = gmp_mod(gmp_mul(gmp_invert(gmp_mod(gmp_mul(gmp_init(2), $pt['y']), $p), $p), gmp_add(gmp_mul(gmp_init(3), gmp_pow($pt['x'], 2)), gmp_init(0))), $p);
        return ['x' => gmp_mod(gmp_sub(gmp_sub(gmp_pow($slope, 2), $pt['x']), $pt['x']), $p),
                'y' => gmp_mod(gmp_sub(gmp_mul($slope, gmp_sub($pt['x'], gmp_mod(gmp_sub(gmp_sub(gmp_pow($slope, 2), $pt['x']), $pt['x']), $p))), $pt['y']), $p)];
    }

    public function addPoints($pt1, $pt2) {
        if (gmp_cmp($pt1['x'], $pt2['x']) === 0 && gmp_cmp($pt1['y'], $pt2['y']) === 0) return $this->doublePoint($pt1);
        $slope = gmp_mod(gmp_mul(gmp_sub($pt1['y'], $pt2['y']), gmp_invert(gmp_sub($pt1['x'], $pt2['x']), $this->p)), $this->p);
        return ['x' => gmp_mod(gmp_sub(gmp_sub(gmp_pow($slope, 2), $pt1['x']), $pt2['x']), $this->p),
                'y' => gmp_mod(gmp_sub(gmp_mul($slope, gmp_sub($pt1['x'], gmp_mod(gmp_sub(gmp_sub(gmp_pow($slope, 2), $pt1['x']), $pt2['x']), $this->p))), $pt1['y']), $this->p)];
    }

      public function mulPoint($k, $pG) {
      $k = gmp_init(gmp_strval($k), 10);
      $kBin = gmp_strval($k, 2);
      $lastPoint = $pG;
        for ($i = 1; $i < strlen($kBin); $i++) {
          $lastPoint = $kBin[$i] === '1' ? $this->addPoints($this->doublePoint($lastPoint), $pG) : $this->doublePoint($lastPoint);
        }
      return $lastPoint;
  }

    public function getPubKeyPoints() {
      if (!isset($this->k)) throw new \Exception('No Private Key defined');
      $pubKey = $this->mulPoint($this->k, $this->G);
      return ['x' => $pubKey['x'],
              'y' => $pubKey['y']];
    }

    public function getPubKey(array $pubKeyPts = []) {
        if (empty($pubKeyPts)) $pubKeyPts = $this->getPubKeyPoints();
          return  $pubKeyPts;
    }

    public function setPrivateKey($k) {
      $this->k = $k;
  }
}


// Ensure $p is a GMP number
$p = gmp_init('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F', 16);

// Initialize the private key as a GMP object
$k = gmp_invert("2",$p);

// $k = "57896044618658097711785492504343953926634992332820282019728792003954417335832";
$bitcoinECDSA = new BitcoinECDSA();
$bitcoinECDSA->setPrivateKey($k);
$s = $bitcoinECDSA->getPubKey();
print_r($s);


?>

步骤:

  1. 使用 gmp_invert("2",$p) 计算 2 关于模 p 的逆元,得到 GMP resource 对象。
  2. $k 设置为资源对象,在 mulpoint 时候转换成字符串再使用 gmp_init()

方案三: 使用 gmp_export()

使用 gmp_export() 也可将 GMP resource 对象导出成字符串, 可以直接传递给gmp_init() 或者其他地方使用。 gmp_export()gmp_strval() 不同之处是其可以设置二进制字节顺序等选项。 如果仅仅想要输出十进制表示的字符串, 则使用 gmp_strval() 即可。

<?php

class BitcoinECDSA {
    public $k, $p, $G;

    public function __construct() {
        $this->p = gmp_init('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F', 16);
        $this->G = ['x' => gmp_init('55066263022277343669578718895168534326250603453777594175500187360389116729240'),
                     'y' => gmp_init('32670510020758816978083085130507043184471273380659243275938904335757337482424')];

        
    }

    public function doublePoint($pt) {
        $p = $this->p;
        $slope = gmp_mod(gmp_mul(gmp_invert(gmp_mod(gmp_mul(gmp_init(2), $pt['y']), $p), $p), gmp_add(gmp_mul(gmp_init(3), gmp_pow($pt['x'], 2)), gmp_init(0))), $p);
        return ['x' => gmp_mod(gmp_sub(gmp_sub(gmp_pow($slope, 2), $pt['x']), $pt['x']), $p),
                'y' => gmp_mod(gmp_sub(gmp_mul($slope, gmp_sub($pt['x'], gmp_mod(gmp_sub(gmp_sub(gmp_pow($slope, 2), $pt['x']), $pt['x']), $p))), $pt['y']), $p)];
    }

    public function addPoints($pt1, $pt2) {
        if (gmp_cmp($pt1['x'], $pt2['x']) === 0 && gmp_cmp($pt1['y'], $pt2['y']) === 0) return $this->doublePoint($pt1);
        $slope = gmp_mod(gmp_mul(gmp_sub($pt1['y'], $pt2['y']), gmp_invert(gmp_sub($pt1['x'], $pt2['x']), $this->p)), $this->p);
        return ['x' => gmp_mod(gmp_sub(gmp_sub(gmp_pow($slope, 2), $pt1['x']), $pt2['x']), $this->p),
                'y' => gmp_mod(gmp_sub(gmp_mul($slope, gmp_sub($pt1['x'], gmp_mod(gmp_sub(gmp_sub(gmp_pow($slope, 2), $pt1['x']), $pt2['x']), $this->p))), $pt1['y']), $this->p)];
    }

    public function mulPoint($k, $pG) {
        $k = gmp_init($k, 10);
        $kBin = gmp_strval($k, 2);
        $lastPoint = $pG;
        for ($i = 1; $i < strlen($kBin); $i++) {
            $lastPoint = $kBin[$i] === '1' ? $this->addPoints($this->doublePoint($lastPoint), $pG) : $this->doublePoint($lastPoint);
        }
        return $lastPoint;
    }

    public function getPubKeyPoints() {
        if (!isset($this->k)) throw new \Exception('No Private Key defined');
        $pubKey = $this->mulPoint($this->k, $this->G);
        return ['x' => $pubKey['x'],
                'y' => $pubKey['y']];
    }

    public function getPubKey(array $pubKeyPts = []) {
        if (empty($pubKeyPts)) $pubKeyPts = $this->getPubKeyPoints();
          return  $pubKeyPts;
    }

    public function setPrivateKey($k) {

        $this->k = $k;
    }
}


// Ensure $p is a GMP number
$p = gmp_init('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F', 16);

// Initialize the private key as a GMP object
$k = bin2hex(gmp_export(gmp_invert("2",$p), 1, GMP_MSW_FIRST|GMP_NATIVE_ENDIAN));
$k = gmp_init($k, 16);

// $k = "57896044618658097711785492504343953926634992332820282019728792003954417335832";

$bitcoinECDSA = new BitcoinECDSA();
$bitcoinECDSA->setPrivateKey($k);
$s = $bitcoinECDSA->getPubKey();
print_r($s);

?>

步骤:

  1. 使用gmp_invert("2",$p) 计算 2 关于模 p 的逆元,得到 GMP resource 对象。
  2. 使用 gmp_export 将其导出为字节字符串。
  3. 利用bin2hex() 转为16进制字符串
  4. 使用gmp_init() 函数 将16进制字符初始化为 gmp 对象

安全建议

  • 在使用 gmp_invert 前,必须确保所要进行反转操作的数与模数互质,否则逆元不存在, gmp_invert 会返回 null
  • GMP 对象属于资源类型,在使用完成且不再需要时,应手动释放。但是PHP7后版本 会自动回收资源,可选择是否手动释放。

总结

本篇文章探讨了使用 gmp_invert 计算模逆元时容易忽略的一个关键细节:gmp_invert 函数返回 GMP resource 对象。 为了正确使用这一结果,需要进行类型转换。 本文提供了使用 gmp_strval() 或者 gmp_export() 的解决方案,以及直接传递资源对象并在使用的时候转换的方法,以及相应的代码示例和详细步骤。 掌握这些知识可以帮助避免由不正确的类型使用导致的程序错误,确保代码能够正常运转。