CertificateDownloader.php 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. #!/usr/bin/env php
  2. <?php
  3. // load autoload.php
  4. $possibleFiles = [__DIR__.'/../vendor/autoload.php', __DIR__.'/../../../autoload.php', __DIR__.'/../../autoload.php'];
  5. $file = null;
  6. foreach ($possibleFiles as $possibleFile) {
  7. if (file_exists($possibleFile)) {
  8. $file = $possibleFile;
  9. break;
  10. }
  11. }
  12. if (null === $file) {
  13. throw new RuntimeException('Unable to locate autoload.php file.');
  14. }
  15. require_once $file;
  16. unset($possibleFiles, $possibleFile, $file);
  17. use GuzzleHttp\HandlerStack;
  18. use GuzzleHttp\Handler\CurlHandler;
  19. use GuzzleHttp\Client;
  20. use GuzzleHttp\Exception\RequestException;
  21. use WechatPay\GuzzleMiddleware\WechatPayMiddleware;
  22. use WechatPay\GuzzleMiddleware\Validator;
  23. use WechatPay\GuzzleMiddleware\Util\PemUtil;
  24. use WechatPay\GuzzleMiddleware\Util\AesUtil;
  25. use WechatPay\GuzzleMiddleware\Auth\CertificateVerifier;
  26. use WechatPay\GuzzleMiddleware\Auth\WechatPay2Validator;
  27. class CertificateDownloader
  28. {
  29. const VERSION = '0.1.0';
  30. public function run()
  31. {
  32. $opts = $this->parseOpts();
  33. if (!$opts) {
  34. $this->printHelp();
  35. exit(1);
  36. }
  37. if (isset($opts['help'])) {
  38. $this->printHelp();
  39. exit(0);
  40. }
  41. if (isset($opts['version'])) {
  42. echo self::VERSION . "\n";
  43. exit(0);
  44. }
  45. $this->downloadCert($opts);
  46. }
  47. private function downloadCert($opts)
  48. {
  49. try {
  50. // 构造一个WechatPayMiddleware
  51. $builder = WechatPayMiddleware::builder()
  52. ->withMerchant($opts['mchid'], $opts['serialno'], PemUtil::loadPrivateKey($opts['privatekey'])); // 传入商户相关配置
  53. if (isset($opts['wechatpay-cert'])) {
  54. $builder->withWechatPay([ PemUtil::loadCertificate($opts['wechatpay-cert']) ]); // 使用平台证书验证
  55. }
  56. else {
  57. $builder->withValidator(new NoopValidator); // 临时"跳过”应答签名的验证
  58. }
  59. $wechatpayMiddleware = $builder->build();
  60. // 将WechatPayMiddleware添加到Guzzle的HandlerStack中
  61. $stack = HandlerStack::create();
  62. $stack->push($wechatpayMiddleware, 'wechatpay');
  63. // 创建Guzzle HTTP Client时,将HandlerStack传入
  64. $client = new GuzzleHttp\Client(['handler' => $stack]);
  65. // 接下来,正常使用Guzzle发起API请求,WechatPayMiddleware会自动地处理签名和验签
  66. $resp = $client->request('GET', 'https://api.mch.weixin.qq.com/v3/certificates', [
  67. 'headers' => [ 'Accept' => 'application/json' ]
  68. ]);
  69. if ($resp->getStatusCode() < 200 || $resp->getStatusCode() > 299) {
  70. echo "download failed, code={$resp->getStatusCode()}, body=[{$resp->getBody()}]\n";
  71. return;
  72. }
  73. $list = json_decode($resp->getBody(), true);
  74. $plainCerts = [];
  75. $x509Certs = [];
  76. $decrypter = new AesUtil($opts['key']);
  77. foreach ($list['data'] as $item) {
  78. $encCert = $item['encrypt_certificate'];
  79. $plain = $decrypter->decryptToString($encCert['associated_data'],
  80. $encCert['nonce'], $encCert['ciphertext']);
  81. if (!$plain) {
  82. echo "encrypted certificate decrypt fail!\n";
  83. exit(1);
  84. }
  85. // 通过加载对证书进行简单合法性检验
  86. $cert = \openssl_x509_read($plain); // 从字符串中加载证书
  87. if (!$cert) {
  88. echo "downloaded certificate check fail!\n";
  89. exit(1);
  90. }
  91. $plainCerts[] = $plain;
  92. $x509Certs[] = $cert;
  93. }
  94. // 使用下载的证书再来验证一次应答的签名
  95. $validator = new WechatPay2Validator(new CertificateVerifier($x509Certs));
  96. if (!$validator->validate($resp)) {
  97. echo "validate response fail using downloaded certificates!";
  98. exit(1);
  99. }
  100. // 输出证书信息,并保存到文件
  101. foreach ($list['data'] as $index => $item) {
  102. echo "Certificate {\n";
  103. echo " Serial Number: ".$item['serial_no']."\n";
  104. echo " Not Before: ".(new DateTime($item['effective_time']))->format('Y-m-d H:i:s')."\n";
  105. echo " Not After: ".(new DateTime($item['expire_time']))->format('Y-m-d H:i:s')."\n";
  106. echo " Text: \n ".str_replace("\n", "\n ", $plainCerts[$index])."\n";
  107. echo "}\n";
  108. $outpath = $opts['output'].DIRECTORY_SEPARATOR.'wechatpay_'.$item['serial_no'].'.pem';
  109. file_put_contents($outpath, $plainCerts[$index]);
  110. }
  111. }
  112. catch (RequestException $e) {
  113. echo "download failed, message=[{$e->getMessage()}] ";
  114. if ($e->hasResponse()) {
  115. echo "code={$e->getResponse()->getStatusCode()}, body=[{$e->getResponse()->getBody()}]\n";
  116. }
  117. exit(1);
  118. }
  119. catch (Exception $e) {
  120. echo "download failed, message=[{$e->getMessage()}]\n";
  121. echo $e;
  122. exit(1);
  123. }
  124. }
  125. private function parseOpts()
  126. {
  127. $opts = [
  128. [ 'key', 'k', true ],
  129. [ 'mchid', 'm', true ],
  130. [ 'privatekey', 'f', true ],
  131. [ 'serialno', 's', true ],
  132. [ 'output', 'o', true ],
  133. [ 'wechatpay-cert', 'c', false ],
  134. ];
  135. $shortopts = 'hV';
  136. $longopts = [ 'help', 'version' ];
  137. foreach ($opts as $opt) {
  138. $shortopts .= $opt[1].':';
  139. $longopts[] = $opt[0].':';
  140. }
  141. $parsed = getopt($shortopts, $longopts);
  142. if (!$parsed) {
  143. return false;
  144. }
  145. $args = [];
  146. foreach ($opts as $opt) {
  147. if (isset($parsed[$opt[0]])) {
  148. $args[$opt[0]] = $parsed[$opt[0]];
  149. }
  150. else if (isset($parsed[$opt[1]])) {
  151. $args[$opt[0]] = $parsed[$opt[1]];
  152. }
  153. else if ($opt[2]) {
  154. return false;
  155. }
  156. }
  157. if (isset($parsed['h']) || isset($parsed['help'])) {
  158. $args['help'] = true;
  159. }
  160. if (isset($parsed['V']) || isset($parsed['version'])) {
  161. $args['version'] = true;
  162. }
  163. return $args;
  164. }
  165. private function printHelp()
  166. {
  167. echo <<<EOD
  168. Usage: 微信支付平台证书下载工具 [-hV] [-c=<wechatpayCertificatePath>]
  169. -f=<privateKeyFilePath> -k=<apiV3key> -m=<merchantId>
  170. -o=<outputFilePath> -s=<serialNo>
  171. -m, --mchid=<merchantId> 商户号
  172. -s, --serialno=<serialNo> 商户证书的序列号
  173. -f, --privatekey=<privateKeyFilePath>
  174. 商户的私钥文件
  175. -k, --key=<apiV3key> ApiV3Key
  176. -c, --wechatpay-cert=<wechatpayCertificatePath>
  177. 微信支付平台证书,验证签名
  178. -o, --output=<outputFilePath>
  179. 下载成功后保存证书的路径
  180. -V, --version Print version information and exit.
  181. -h, --help Show this help message and exit.
  182. EOD;
  183. }
  184. }
  185. class NoopValidator implements Validator
  186. {
  187. public function validate(\Psr\Http\Message\ResponseInterface $response)
  188. {
  189. return true;
  190. }
  191. }
  192. // main
  193. (new CertificateDownloader())->run();