201 lines
6.5 KiB
PHP
201 lines
6.5 KiB
PHP
<?php
|
|
|
|
namespace Yeepay\Yop\Sdk\Auth\Signer;
|
|
|
|
use DateTime;
|
|
use Yeepay\Yop\Sdk\Auth\Signer;
|
|
use Yeepay\Yop\Sdk\Auth\SignOptions;
|
|
use Yeepay\Yop\Sdk\Auth\YopRsaCredentials;
|
|
use Yeepay\Yop\Sdk\Exception\VerifySignFailedException;
|
|
use Yeepay\Yop\Sdk\Exception\YopClientException;
|
|
use Yeepay\Yop\Sdk\Http\Headers;
|
|
use Yeepay\Yop\Sdk\Http\YopHttpResponse;
|
|
use Yeepay\Yop\Sdk\Internal\Request;
|
|
use Yeepay\Yop\Sdk\Security\Encodes;
|
|
use Yeepay\Yop\Sdk\Utils\DateUtils;
|
|
use Yeepay\Yop\Sdk\Utils\Http\HttpUtils;
|
|
|
|
class RsaSigner implements Signer
|
|
{
|
|
|
|
private static $yopAuthVersion = 'yop-auth-v3';
|
|
|
|
private static $defaultHeadersToSign;
|
|
|
|
private static $headerJoiner = "\n";
|
|
|
|
private static $signedHeaderStringJoiner = ';';
|
|
|
|
private $logger;
|
|
|
|
public static function __init()
|
|
{
|
|
self::$defaultHeadersToSign = [
|
|
strtolower(Headers::CONTENT_LENGTH),
|
|
strtolower(Headers::CONTENT_TYPE),
|
|
strtolower(Headers::CONTENT_DISPOSITION),
|
|
strtolower(Headers::CONTENT_MD5),
|
|
strtolower(Headers::YOP_REQUEST_ID),
|
|
strtolower(Headers::YOP_DATE),
|
|
strtolower(Headers::YOP_APPKEY),
|
|
strtolower(Headers::YOP_CONTENT_SHA256),
|
|
strtolower(Headers::YOP_HASH_CRC64ECMA),
|
|
];
|
|
}
|
|
|
|
public function sign(Request $request, YopRsaCredentials $credentials = null, SignOptions $options = null)
|
|
{
|
|
if ($credentials == null) {
|
|
return;
|
|
}
|
|
$accessKeyId = $credentials->getAppKey();
|
|
$request->addHeader(Headers::HOST, HttpUtils:: generateHostHeader($request->getEndpoint()));
|
|
$timestamp = new DateTime();
|
|
$timestamp->setTimezone(DateUtils::$UTC_TIMEZONE);
|
|
|
|
$contentSha256 = $this->calculateContentHash($request);
|
|
if (isset($contentSha256)) {
|
|
$request->addHeader(Headers::YOP_CONTENT_SHA256, $contentSha256);
|
|
}
|
|
$canonicalQueryString = $this->getCanonicalQueryString($request);
|
|
$headersToSign = $this->getHeadersToSign($request->getHeaders(), self::$defaultHeadersToSign);
|
|
$canonicalHeader = $this->getCanonicalHeaders($headersToSign);
|
|
$signedHeaders = strtolower(trim(implode(self::$signedHeaderStringJoiner, array_keys($headersToSign))));
|
|
|
|
$authString = self::$yopAuthVersion.'/'.$accessKeyId.'/'.DateUtils::formatAlternateIso8601Date($timestamp).'/'
|
|
.$options->getExpirationInSeconds();
|
|
|
|
$canonicalURI = $this->getCanonicalURIPath($request->getResourcePath());
|
|
$canonicalRequest = $authString.self::$headerJoiner.$request->getHttpMethod().self::$headerJoiner.$canonicalURI
|
|
.self::$headerJoiner.$canonicalQueryString.self::$headerJoiner.$canonicalHeader;
|
|
|
|
$signature = $this->computeSignature($canonicalRequest, $credentials->getPrivateKey(),
|
|
$options->getDigestAlg());
|
|
$authorizationHeader = $options->getProtocolPrefix().' '.$authString.'/'.$signedHeaders.'/'.$signature;
|
|
$request->addHeader(Headers::AUTHORIZATION, $authorizationHeader);
|
|
}
|
|
|
|
private function calculateContentHash(Request $request)
|
|
{
|
|
$content = null;
|
|
if (HttpUtils::usePayloadForQueryParameters($request)) {
|
|
$content = HttpUtils::getCanonicalQueryString($request->getParameters(), true);
|
|
} else {
|
|
if (is_string($request->getContent())) {
|
|
$content = $request->getContent();
|
|
}
|
|
}
|
|
if (isset($content)) {
|
|
return hash('sha256', $content);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private function getCanonicalQueryString(Request $request)
|
|
{
|
|
if (HttpUtils::usePayloadForQueryParameters($request)) {
|
|
return '';
|
|
}
|
|
|
|
return HttpUtils::getCanonicalQueryString($request->getParameters(), true);
|
|
}
|
|
|
|
/**
|
|
* @param $path string
|
|
* @return string
|
|
*/
|
|
private function getCanonicalURIPath($path)
|
|
{
|
|
if (empty($path)) {
|
|
return '/';
|
|
} else {
|
|
if ($path[0] == '/') {
|
|
return HttpUtils::urlEncodeExceptSlash($path);
|
|
} else {
|
|
return '/'.HttpUtils::urlEncodeExceptSlash($path);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param $headers array
|
|
* @return string
|
|
*/
|
|
private function getCanonicalHeaders($headers)
|
|
{
|
|
if (count($headers) == 0) {
|
|
return '';
|
|
}
|
|
|
|
$headerStrings = [];
|
|
foreach ($headers as $k => $v) {
|
|
if ($k === null) {
|
|
continue;
|
|
}
|
|
if ($v === null) {
|
|
$v = '';
|
|
}
|
|
$headerStrings[] = rawurlencode(
|
|
strtolower(trim($k))
|
|
).':'.rawurlencode(trim($v));
|
|
}
|
|
sort($headerStrings);
|
|
|
|
return implode(self::$headerJoiner, $headerStrings);
|
|
}
|
|
|
|
/**
|
|
* @param $headers array
|
|
* @param $headersToSign array
|
|
* @return array
|
|
*/
|
|
private function getHeadersToSign($headers, $headersToSign)
|
|
{
|
|
$ret = [];
|
|
if ($headersToSign !== null) {
|
|
$tmp = [];
|
|
foreach ($headersToSign as $header) {
|
|
$tmp[] = strtolower(trim($header));
|
|
}
|
|
$headersToSign = $tmp;
|
|
}
|
|
foreach ($headers as $k => $v) {
|
|
if (trim((string) $v) !== '') {
|
|
if ($headersToSign !== null) {
|
|
$k = strtolower(trim($k));
|
|
if (in_array($k, $headersToSign) && $k != trim(Headers::AUTHORIZATION)) {
|
|
$ret[$k] = $v;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
private function computeSignature($canonicalRequest, $privateKey, $digestAlg)
|
|
{
|
|
$signature = null;
|
|
if (openssl_sign($canonicalRequest, $signature, $privateKey, $digestAlg)) {
|
|
return Encodes::base64url_encode($signature).'$'.$digestAlg;
|
|
}
|
|
throw new YopClientException('compute signature failed.');
|
|
}
|
|
|
|
public function checkSignature(YopHttpResponse $httpResponse, $signature, $publicKey, SignOptions $options)
|
|
{
|
|
$content = $httpResponse->readContent();
|
|
\Log::channel('yeepay')->info($content);
|
|
|
|
$content = str_replace([" ", "\n", "\t"], "", $content);
|
|
if (openssl_verify($content, base64_decode($signature), $publicKey, $options->getDigestAlg()) == 1) {
|
|
return;
|
|
}
|
|
throw new VerifySignFailedException("response sign verify failure");
|
|
}
|
|
|
|
}
|
|
|
|
RsaSigner::__init();
|