GMP反转运算陷阱与避坑:模逆元计算详解
2025-01-15 19:03:56
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);
?>
步骤:
- 使用
gmp_invert("2",$p)
计算 2 关于模 p 的逆元,得到 GMP resource 对象。 - 使用
gmp_strval()
将该资源对象转换为一个十进制的字符串。 - 现在可以正常运行。
方案二:直接使用 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);
?>
步骤:
- 使用
gmp_invert("2",$p)
计算 2 关于模 p 的逆元,得到 GMP resource 对象。 - 将
$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);
?>
步骤:
- 使用
gmp_invert("2",$p)
计算 2 关于模 p 的逆元,得到 GMP resource 对象。 - 使用
gmp_export
将其导出为字节字符串。 - 利用
bin2hex()
转为16进制字符串 - 使用
gmp_init()
函数 将16进制字符初始化为 gmp 对象
安全建议
- 在使用
gmp_invert
前,必须确保所要进行反转操作的数与模数互质,否则逆元不存在,gmp_invert
会返回null
。 - GMP 对象属于资源类型,在使用完成且不再需要时,应手动释放。但是PHP7后版本 会自动回收资源,可选择是否手动释放。
总结
本篇文章探讨了使用 gmp_invert
计算模逆元时容易忽略的一个关键细节:gmp_invert
函数返回 GMP resource 对象。 为了正确使用这一结果,需要进行类型转换。 本文提供了使用 gmp_strval()
或者 gmp_export()
的解决方案,以及直接传递资源对象并在使用的时候转换的方法,以及相应的代码示例和详细步骤。 掌握这些知识可以帮助避免由不正确的类型使用导致的程序错误,确保代码能够正常运转。