為什麼我對已簽名消息簽名的驗證在 PHP 中不起作用?
我正在嘗試驗證 PHP 中的簽名消息。
需要明確的是,我不希望與 JSON-RPC 或任何外部服務進行互動,我既 a) 知道這些服務有效,並且 b) 成功驗證了我提供的範例簽名消息。
此外,我很清楚“消息前綴”和消息長度問題。那不是我遇到問題的地方。
問題似乎源於我正在使用的 Signature 類(包括在下面的內聯,為了便於閱讀,刪除了所有未使用的位,但從這裡提取:https ://github.com/tuaris/CryptoCurrencyPHP/blob/ master/Signature.class.php)或從簽名本身提取的 R 和 S 值的 GMP 轉換。
我的程式碼中使用的唯一外部庫是https://github.com/0xbb/php-sha3/blob/master/src/Sha3.php,它需要修改第 334 行,更改
0x06
為0x01
,以實現 keccak 兼容性,這就是乙太坊使用的。(我知道這對我來說是一個正確的更改,因為0x01
在使用 web3 庫對同一消息進行散列時,將其保持為對原始消息進行散列時會產生不同的結果。下面是我的程式碼。如果有人能夠告訴我哪裡出錯了,我將不勝感激。
<?php use bb\Sha3\Sha3; require_once('./Sha3.php'); $message = 'This is an example of a signed message.'; $signerAddress = '0xd4e01f608982ff53022e8c3ff43e145a192a9c4a'; $signedMessage = '0x6a65ed07a44715169177223ce508a2257f8167db452df0b2e37966b39350a61940e370616b3a0ea0f20adfa4661a7db10eeb583ca5a58ec8468e726eff4131a11c'; $signedMessageStrip = '6a65ed07a44715169177223ce508a2257f8167db452df0b2e37966b39350a61940e370616b3a0ea0f20adfa4661a7db10eeb583ca5a58ec8468e726eff4131a11c'; $prefix = "\x19Ethereum Signed Message:\n".strlen($message); $stringToSign = $prefix.$message; //\x19Ethereum Signed Message:\n39This is an example of a signed message. $messageHex = Sha3::hash($stringToSign, 256); //this matches web3.sha() output for the given message and prefix. $messageGmp = gmp_init("0x".$messageHex); $r = substr($signedMessageStrip, 0,64); $s = substr($signedMessageStrip, 64,64); $v = substr($signedMessageStrip, 128,2); $vChecksum = hexdec($v) - 27; if($vChecksum !== 0 && $vChecksum !== 1) { echo "Invalid checksum.\n"; exit; } $rGmp = gmp_init("0x".$r); $sGmp = gmp_init("0x".$s); $publicKey = Signature::recoverPublicKey($rGmp, $sGmp, $messageGmp, $vChecksum); //the below line is where things are going wrong. The output hash of Sha3::hash($publicKey['x'].$publicKey['y'], 256) is not correct, according to stepping through similar processes using the web3 library, which generates different results, despite an earlier check that publicKey *is* correct. I cannot figure out what's going wrong. $recovered = "0x".substr(Sha3::hash($publicKey['x'].$publicKey['y'], 256),24)."\n"; //convert to public address format //$recovered = 0xf2517bd73c56d6d5a5409c4a1ee29c8f2d5438ff if (strtolower($recovered) == strtolower($signerAddress)) { echo "Address recovered successfully.\n"; } else { echo "Address NOT recovered successfully.\n"; } ?> <?php class SECp256k1 { public $a; public $b; public $p; public $n; public $G; public function __construct(){ $this->a = gmp_init('0', 10); $this->b = gmp_init('7', 10); $this->p = gmp_init('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F', 16); $this->n = gmp_init('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', 16); $this->G = array('x' => gmp_init('55066263022277343669578718895168534326250603453777594175500187360389116729240'), 'y' => gmp_init('32670510020758816978083085130507043184471273380659243275938904335757337482424')); } } class Signature { public static function recoverPublicKey($R, $S, $hash, $recoveryFlags){ $secp256k1 = new SECp256k1(); $a = $secp256k1->a; $b = $secp256k1->b; $G = $secp256k1->G; $n = $secp256k1->n; $p = $secp256k1->p; $isYEven = ($recoveryFlags & 1) != 0; $isSecondKey = ($recoveryFlags & 2) != 0; // PointMathGMP::mulPoint wants HEX String $e = gmp_strval($hash, 16); $s = gmp_strval($S, 16); // Precalculate (p + 1) / 4 where p is the field order // $p_over_four is GMP static $p_over_four; // XXX just assuming only one curve/prime will be used if (!$p_over_four) { $p_over_four = gmp_div(gmp_add($p, 1), 4); } // 1.1 Compute x // $x is GMP if (!$isSecondKey) { $x = $R; } else { $x = gmp_add($R, $n); } // 1.3 Convert x to point // $alpha is GMP $alpha = gmp_mod(gmp_add(gmp_add(gmp_pow($x, 3), gmp_mul($a, $x)), $b), $p); // $beta is DEC String (INT) $beta = gmp_strval(gmp_powm($alpha, $p_over_four, $p)); // If beta is even, but y isn't or vice versa, then convert it, // otherwise we're done and y == beta. if (PointMathGMP::isEvenNumber($beta) == $isYEven) { // gmp_sub function will convert the DEC String "$beta" into a GMP // $y is a GMP $y = gmp_sub($p, $beta); } else { // $y is a GMP $y = gmp_init($beta); } // 1.4 Check that nR is at infinity (implicitly done in construtor) -- Not reallly // $Rpt is Array(GMP, GMP) $Rpt = array('x' => $x, 'y' => $y); // 1.6.1 Compute a candidate public key Q = r^-1 (sR - eG) // $rInv is a HEX String $rInv = gmp_strval(gmp_invert($R, $n), 16); // $eGNeg is Array (GMP, GMP) $eGNeg = PointMathGMP::negatePoint(PointMathGMP::mulPoint($e, $G, $a, $b, $p)); $sR = PointMathGMP::mulPoint($s, $Rpt, $a, $b, $p); $sR_plus_eGNeg = PointMathGMP::addPoints($sR, $eGNeg, $a, $p); // $Q is Array (GMP, GMP) $Q = PointMathGMP::mulPoint($rInv, $sR_plus_eGNeg, $a, $b, $p); // Q is the derrived public key // $pubkey is Array (HEX String, HEX String) // Ensure it's always 64 HEX Charaters $pubKey['x'] = str_pad(gmp_strval($Q['x'], 16), 64, 0, STR_PAD_LEFT); $pubKey['y'] = str_pad(gmp_strval($Q['y'], 16), 64, 0, STR_PAD_LEFT); return $pubKey; } } class PointMathGMP { /*** * Computes the result of a point addition and returns the resulting point as an Array. * * @param Array $pt * @return Array Point * @throws \Exception */ public static function doublePoint(Array $pt, $a, $p) { $gcd = gmp_strval(gmp_gcd(gmp_mod(gmp_mul(gmp_init(2, 10), $pt['y']), $p),$p)); if($gcd != '1') { throw new \Exception('This library doesn\'t yet supports point at infinity. See https://github.com/BitcoinPHP/BitcoinECDSA.php/issues/9'); } // SLOPE = (3 * ptX^2 + a )/( 2*ptY ) // Equals (3 * ptX^2 + a ) * ( 2*ptY )^-1 $slope = gmp_mod( gmp_mul( gmp_invert( gmp_mod( gmp_mul( gmp_init(2, 10), $pt['y'] ), $p ), $p ), gmp_add( gmp_mul( gmp_init(3, 10), gmp_pow($pt['x'], 2) ), $a ) ), $p ); // nPtX = slope^2 - 2 * ptX // Equals slope^2 - ptX - ptX $nPt = array(); $nPt['x'] = gmp_mod( gmp_sub( gmp_sub( gmp_pow($slope, 2), $pt['x'] ), $pt['x'] ), $p ); // nPtY = slope * (ptX - nPtx) - ptY $nPt['y'] = gmp_mod( gmp_sub( gmp_mul( $slope, gmp_sub( $pt['x'], $nPt['x'] ) ), $pt['y'] ), $p ); return $nPt; } /*** * Computes the result of a point addition and returns the resulting point as an Array. * * @param Array $pt1 * @param Array $pt2 * @return Array Point * @throws \Exception */ public static function addPoints(Array $pt1, Array $pt2, $a, $p) { if(gmp_cmp($pt1['x'], $pt2['x']) == 0 && gmp_cmp($pt1['y'], $pt2['y']) == 0) //if identical { return self::doublePoint($pt1, $a, $p); } $gcd = gmp_strval(gmp_gcd(gmp_sub($pt1['x'], $pt2['x']), $p)); if($gcd != '1') { throw new \Exception('This library doesn\'t yet support points at infinity. See https://github.com/BitcoinPHP/BitcoinECDSA.php/issues/9'); } // SLOPE = (pt1Y - pt2Y)/( pt1X - pt2X ) // Equals (pt1Y - pt2Y) * ( pt1X - pt2X )^-1 $slope = gmp_mod( gmp_mul( gmp_sub( $pt1['y'], $pt2['y'] ), gmp_invert( gmp_sub( $pt1['x'], $pt2['x'] ), $p ) ), $p ); // nPtX = slope^2 - ptX1 - ptX2 $nPt = array(); $nPt['x'] = gmp_mod( gmp_sub( gmp_sub( gmp_pow($slope, 2), $pt1['x'] ), $pt2['x'] ), $p ); // nPtX = slope * (ptX1 - nPtX) - ptY1 $nPt['y'] = gmp_mod( gmp_sub( gmp_mul( $slope, gmp_sub( $pt1['x'], $nPt['x'] ) ), $pt1['y'] ), $p ); return $nPt; } /*** * Computes the result of a point multiplication and returns the resulting point as an Array. * * @param String Hex $k * @param Array $pG (GMP, GMP) * @param $base (INT) * @throws \Exception * @return Array Point (GMP, GMP) */ public static function mulPoint($k, Array $pG, $a, $b, $p, $base = null) { //in order to calculate k*G if($base == 16 || $base == null || is_resource($base)) $k = gmp_init($k, 16); if($base == 10) $k = gmp_init($k, 10); $kBin = gmp_strval($k, 2); $lastPoint = $pG; for($i = 1; $i < strlen($kBin); $i++) { if(substr($kBin, $i, 1) == 1 ) { $dPt = self::doublePoint($lastPoint, $a, $p); $lastPoint = self::addPoints($dPt, $pG, $a, $p); } else { $lastPoint = self::doublePoint($lastPoint, $a, $p); } } if(!self::validatePoint(gmp_strval($lastPoint['x'], 16), gmp_strval($lastPoint['y'], 16), $a, $b, $p)){ throw new \Exception('The resulting point is not on the curve.'); } return $lastPoint; } /*** * Returns true if the point is on the curve and false if it isn't. * * @param $x * @param $y * @return bool */ public static function validatePoint($x, $y, $a, $b, $p) { $x = gmp_init($x, 16); $y2 = gmp_mod( gmp_add( gmp_add( gmp_powm($x, gmp_init(3, 10), $p), gmp_mul($a, $x) ), $b ), $p ); $y = gmp_mod(gmp_pow(gmp_init($y, 16), 2), $p); if(gmp_cmp($y2, $y) == 0) return true; else return false; } /*** * Returns Negated Point (Y). * * @param $point Array(GMP, GMP) * @return Array(GMP, GMP) */ public static function negatePoint($point) { return array('x' => $point['x'], 'y' => gmp_neg($point['y'])); } // Checks is the given number (DEC String) is even public static function isEvenNumber($number) { return (((int)$number[strlen($number)-1]) & 1) == 0; } } ?>
經過多次頭部撞擊(撞牆)並將問題隔離到生成的公鑰的散列之後,(很好地總結在我的另一個問題中,在這裡:為什麼我的私鑰的公鑰沒有生成正確的公共地址?),事實證明,必須將公鑰的字節而不是十六進制散列本身傳遞給 keccak 散列算法。
$recovered = "0x".substr(Sha3::hash(hex2bin($publicKey['x'].$publicKey['y']), 256),24)
經過同樣長時間的敲打,我在 PHP https://github.com/digitaldonkey/ecverify中為 web3.ecverify 編寫了一個 PHP 等效項