EthSign.php 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. <?php
  2. namespace app\common\library;
  3. use InvalidArgumentException;
  4. use Elliptic\EC;
  5. use Elliptic\EC\KeyPair;
  6. use Elliptic\EC\Signature;
  7. use kornrunner\Keccak;
  8. class EthSign
  9. {
  10. /**
  11. * SHA3_NULL_HASH
  12. *
  13. * @const string
  14. */
  15. const SHA3_NULL_HASH = 'c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470';
  16. /**
  17. * secp256k1
  18. *
  19. * @var \Elliptic\EC
  20. */
  21. protected $secp256k1;
  22. /**
  23. * construct
  24. *
  25. * @return void
  26. */
  27. public function __construct()
  28. {
  29. $this->secp256k1 = new EC('secp256k1');
  30. }
  31. /**
  32. * recoverPublicKey
  33. *
  34. * @param string $msg
  35. * @param string $sign
  36. * @param string $address
  37. * @return bool
  38. */
  39. public function verifySign(string $msg, string $sign, string $address): bool
  40. {
  41. if (strlen($sign) !== 132) {
  42. throw new InvalidArgumentException('Invalid signature length.');
  43. }
  44. $r = substr($sign, 2, 64);
  45. $s = substr($sign, 66, 64);
  46. if (strlen($r) !== 64 || strlen($s) !== 64) {
  47. throw new InvalidArgumentException('Invalid signature length.');
  48. }
  49. $hash = $this->hashPersonalMessage($msg);
  50. // $hash = Keccak::hash(sprintf("\x19Ethereum Signed Message:\n%s%s", strlen($msg), $msg), 256);
  51. $q = substr($sign, 130, 2);
  52. $w = hex2bin($q);
  53. $recid = ord($w) - 27;
  54. $publicKey = $this->secp256k1->recoverPubKey($hash, [
  55. 'r' => $r,
  56. 's' => $s
  57. ], $recid);
  58. $publicKey = $publicKey->encode('hex');
  59. $publicAddress = $this->publicKeyToAddress($publicKey);
  60. $address = strtolower($address);
  61. $publicAddress = strtolower($publicAddress);
  62. return $publicAddress == $address;
  63. }
  64. /**
  65. * recoverPublicKey
  66. *
  67. * @param string $hash
  68. * @param string $r
  69. * @param string $s
  70. * @param int $v
  71. * @return string
  72. */
  73. public function recoverPublicKey(string $hash, string $r, string $s, int $v)
  74. {
  75. if ($this->isHex($hash) === false) {
  76. throw new InvalidArgumentException('Invalid hash format.');
  77. }
  78. $hash = $this->stripZero($hash);
  79. if ($this->isHex($r) === false || $this->isHex($s) === false) {
  80. throw new InvalidArgumentException('Invalid signature format.');
  81. }
  82. $r = $this->stripZero($r);
  83. $s = $this->stripZero($s);
  84. if (strlen($r) !== 64 || strlen($s) !== 64) {
  85. throw new InvalidArgumentException('Invalid signature length.');
  86. }
  87. $publicKey = $this->secp256k1->recoverPubKey($hash, [
  88. 'r' => $r,
  89. 's' => $s
  90. ], $v);
  91. $publicKey = $publicKey->encode('hex');
  92. return '0x' . $publicKey;
  93. }
  94. /**
  95. * sha3
  96. * keccak256
  97. *
  98. * @param string $value
  99. * @return string
  100. */
  101. public function sha3(string $value)
  102. {
  103. $hash = Keccak::hash($value, 256);
  104. if ($hash === $this::SHA3_NULL_HASH) {
  105. return null;
  106. }
  107. return $hash;
  108. }
  109. /**
  110. * isZeroPrefixed
  111. *
  112. * @param string $value
  113. * @return bool
  114. */
  115. public function isZeroPrefixed(string $value)
  116. {
  117. return (strpos($value, '0x') === 0);
  118. }
  119. /**
  120. * stripZero
  121. *
  122. * @param string $value
  123. * @return string
  124. */
  125. public function stripZero(string $value)
  126. {
  127. if ($this->isZeroPrefixed($value)) {
  128. $count = 1;
  129. return str_replace('0x', '', $value, $count);
  130. }
  131. return $value;
  132. }
  133. /**
  134. * isHex
  135. *
  136. * @param string $value
  137. * @return bool
  138. */
  139. public function isHex(string $value)
  140. {
  141. return (is_string($value) && preg_match('/^(0x)?[a-fA-F0-9]+$/', $value) === 1);
  142. }
  143. /**
  144. * publicKeyToAddress
  145. *
  146. * @param string $publicKey
  147. * @return string
  148. */
  149. public function publicKeyToAddress(string $publicKey)
  150. {
  151. if ($this->isHex($publicKey) === false) {
  152. throw new InvalidArgumentException('Invalid public key format.');
  153. }
  154. $publicKey = $this->stripZero($publicKey);
  155. if (strlen($publicKey) !== 130) {
  156. throw new InvalidArgumentException('Invalid public key length.');
  157. }
  158. return '0x' . substr($this->sha3(substr(hex2bin($publicKey), 1)), 24);
  159. }
  160. /**
  161. * privateKeyToPublicKey
  162. *
  163. * @param string $privateKey
  164. * @return string
  165. */
  166. public function privateKeyToPublicKey(string $privateKey)
  167. {
  168. if ($this->isHex($privateKey) === false) {
  169. throw new InvalidArgumentException('Invalid private key format.');
  170. }
  171. $privateKey = $this->stripZero($privateKey);
  172. if (strlen($privateKey) !== 64) {
  173. throw new InvalidArgumentException('Invalid private key length.');
  174. }
  175. $privateKey = $this->secp256k1->keyFromPrivate($privateKey, 'hex');
  176. $publicKey = $privateKey->getPublic(false, 'hex');
  177. return '0x' . $publicKey;
  178. }
  179. /**
  180. * hasPersonalMessage
  181. *
  182. * @param string $message
  183. * @return string
  184. */
  185. public function hashPersonalMessage(string $message)
  186. {
  187. $prefix = sprintf("\x19Ethereum Signed Message:\n%d", strlen($message));
  188. return $this->sha3($prefix . $message);
  189. }
  190. }