PHP / Docusign - 在完成的事件上验证 HMAC 签名
PHP / Docusign - Verify HMAC signature on completed event
我正在尝试在触发完成事件时保护我的回调 url。
我的控制器:
public function callbackSubscriptionCompleted(
int $subscriptionId,
DocusignService $docusignService,
Request $request
) {
$signature = $request->headers->get("X-DocuSign-Signature-1");
$payload = file_get_contents('php://input');
$isValid = $docusignService->isValidHash($signature, $payload);
if (!$isValid) {
throw new ApiException(
Response::HTTP_BAD_REQUEST,
'invalid_subscription',
'Signature not OK'
);
}
return new Response("Signature OK", Response::HTTP_OK);
}
我的 DocusignService 功能:
private function createEnvelope(Company $company, Subscription $subscription, LegalRepresentative $legalRepresentative, Correspondent $correspondent, $correspondents) : array
{
// ...
$data = [
'disableResponsiveDocument' => 'false',
'emailSubject' => 'Your Subscription',
'emailBlurb' => 'Subscription pending',
'status' => 'sent',
'notification' => [
'useAccountDefaults' => 'false',
'reminders' => [
'reminderEnabled' => 'true',
'reminderDelay' => '1',
'reminderFrequency' => '1'
],
'expirations' => [
'expireEnabled' => 'True',
'expireAfter' => '250',
'expireWarn' => '2'
]
],
'compositeTemplates' => [
[
'serverTemplates' => [
[
'sequence' => '1',
'templateId' => $this->templateId
]
],
'inlineTemplates' => [
[
'sequence' => '2',
'recipients' => [
'signers' => [
[
'email' => $legalRepresentative->getEmail(),
'name' => $legalRepresentative->getLastname(),
'recipientId' => '1',
'recipientSignatureProviders' => [
[
'signatureProviderName' => 'universalsignaturepen_opentrust_hash_tsp',
'signatureProviderOptions' => [
'sms' => substr($legalRepresentative->getCellphone(), 0, 3) == '+33' ? $legalRepresentative->getCellphone() : '+33' . substr($legalRepresentative->getCellphone(), 1),
]
]
],
'roleName' => 'Client',
'clientUserId' => $legalRepresentative->getId(),
'tabs' => [
'textTabs' => $textTabs,
'radioGroupTabs' => $radioTabs,
'checkboxTabs' => $checkboxTabs
]
]
]
]
]
]
]
],
'eventNotification' => [
"url" => $this->router->generate("api_post_subscription_completed_callback", [
"subscriptionId" => $subscription->getId()
], UrlGeneratorInterface::ABSOLUTE_URL),
"includeCertificateOfCompletion" => "false",
"includeDocuments" => "true",
"includeDocumentFields" => "true",
"includeHMAC" => "true",
"requireAcknowledgment" => "true",
"envelopeEvents" => [
[
"envelopeEventStatusCode" => "completed"
]
]
]
];
$response = $this->sendRequest(
'POST',
$this->getBaseUri() . '/envelopes',
[
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $this->getCacheToken()
],
json_encode($data)
);
}
public function isValidHash(string $signature, string $payload): bool
{
$hexHash = hash_hmac('sha256',utf8_encode($payload),utf8_encode($this->hmacKey));
$base64Hash = base64_encode(hex2bin($hexHash));
return $signature === $base64Hash;
}
我已经在我的 Docusign Connect 中创建了我的 hmac 密钥,并且我在 header 和有效负载中收到了签名,但验证总是失败。
我遵循了 Docusign 文档 here
怎么了?
PS:抱歉我的英语不好
我觉得你的代码不错。确保您只发送一个 HMAC 签名。这样你的 hmacKey 就是正确的。
作为检查,我会打印出 utf8_encode($payload)
并检查它看起来是否正确(它应该是传入的 XML,而不是 headers)。另外,我不认为它的开头应该有一个 CR/NL 。这是 HTTP header 和 body.
之间的分隔符
更新
我已验证来自 DocuSign 网站的 PHP 代码工作正常。
有效负载值不得包含前导换行符或尾随换行符。它应该以 <?xml
开头并以 >
结尾
我怀疑您的软件正在添加前导或尾随换行符。
秘密(来自 DocuSign)以 =
结尾。它是一个 Base64 编码值。 不要解码。将其用作字符串即可。
另一个更新
负载(请求的 body)包含零个新行。
如果要打印负载,则需要将其包装在 <pre>
中,因为它包含 <
个字符。或者查看页面源码。
包含UTF-8XML如
<?xml version="1.0" encoding="utf-8"?><DocuSignEnvelopeInformation xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.docusign.net/API/3.0"><EnvelopeStatus><RecipientStatuses><RecipientStatus><Type>Signer</Type><Email>larry@worldwidecorp.us</Email><UserName>Larry Kluger</UserName><RoutingOrder>1</RoutingOrder><Sent>2020-08-05T03:11:13.057</Sent><Delivered>2020-08-05T03:11:27.657</Delivered><DeclineReason xsi:nil="true" /><Status>Delivered</Status><RecipientIPAddress>5.102.239.40</RecipientIPAddress><CustomFields /><TabStatuses><TabStatus><TabType>Custom</TabType><Status>Active</Status><XPosition>223</XPosition><YPosition>744....
我们做了更多的测试,
$payload = file_get_contents('php://input');
应该非常在脚本的早期。问题是框架可以修改 php://input
流,因此此后它将无法正常工作。
注意this page from the Symfony site -- 说明获取请求的正确方式body是:
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\ParameterBag;
$app->before(function (Request $request) {
$payload = $request->getContent();
hmac_verify($payload, $secret);
});
我会尝试使用 Symfony 代码而不是 file_get_contents('php://input');
我正在尝试在触发完成事件时保护我的回调 url。
我的控制器:
public function callbackSubscriptionCompleted(
int $subscriptionId,
DocusignService $docusignService,
Request $request
) {
$signature = $request->headers->get("X-DocuSign-Signature-1");
$payload = file_get_contents('php://input');
$isValid = $docusignService->isValidHash($signature, $payload);
if (!$isValid) {
throw new ApiException(
Response::HTTP_BAD_REQUEST,
'invalid_subscription',
'Signature not OK'
);
}
return new Response("Signature OK", Response::HTTP_OK);
}
我的 DocusignService 功能:
private function createEnvelope(Company $company, Subscription $subscription, LegalRepresentative $legalRepresentative, Correspondent $correspondent, $correspondents) : array
{
// ...
$data = [
'disableResponsiveDocument' => 'false',
'emailSubject' => 'Your Subscription',
'emailBlurb' => 'Subscription pending',
'status' => 'sent',
'notification' => [
'useAccountDefaults' => 'false',
'reminders' => [
'reminderEnabled' => 'true',
'reminderDelay' => '1',
'reminderFrequency' => '1'
],
'expirations' => [
'expireEnabled' => 'True',
'expireAfter' => '250',
'expireWarn' => '2'
]
],
'compositeTemplates' => [
[
'serverTemplates' => [
[
'sequence' => '1',
'templateId' => $this->templateId
]
],
'inlineTemplates' => [
[
'sequence' => '2',
'recipients' => [
'signers' => [
[
'email' => $legalRepresentative->getEmail(),
'name' => $legalRepresentative->getLastname(),
'recipientId' => '1',
'recipientSignatureProviders' => [
[
'signatureProviderName' => 'universalsignaturepen_opentrust_hash_tsp',
'signatureProviderOptions' => [
'sms' => substr($legalRepresentative->getCellphone(), 0, 3) == '+33' ? $legalRepresentative->getCellphone() : '+33' . substr($legalRepresentative->getCellphone(), 1),
]
]
],
'roleName' => 'Client',
'clientUserId' => $legalRepresentative->getId(),
'tabs' => [
'textTabs' => $textTabs,
'radioGroupTabs' => $radioTabs,
'checkboxTabs' => $checkboxTabs
]
]
]
]
]
]
]
],
'eventNotification' => [
"url" => $this->router->generate("api_post_subscription_completed_callback", [
"subscriptionId" => $subscription->getId()
], UrlGeneratorInterface::ABSOLUTE_URL),
"includeCertificateOfCompletion" => "false",
"includeDocuments" => "true",
"includeDocumentFields" => "true",
"includeHMAC" => "true",
"requireAcknowledgment" => "true",
"envelopeEvents" => [
[
"envelopeEventStatusCode" => "completed"
]
]
]
];
$response = $this->sendRequest(
'POST',
$this->getBaseUri() . '/envelopes',
[
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $this->getCacheToken()
],
json_encode($data)
);
}
public function isValidHash(string $signature, string $payload): bool
{
$hexHash = hash_hmac('sha256',utf8_encode($payload),utf8_encode($this->hmacKey));
$base64Hash = base64_encode(hex2bin($hexHash));
return $signature === $base64Hash;
}
我已经在我的 Docusign Connect 中创建了我的 hmac 密钥,并且我在 header 和有效负载中收到了签名,但验证总是失败。 我遵循了 Docusign 文档 here 怎么了?
PS:抱歉我的英语不好
我觉得你的代码不错。确保您只发送一个 HMAC 签名。这样你的 hmacKey 就是正确的。
作为检查,我会打印出 utf8_encode($payload)
并检查它看起来是否正确(它应该是传入的 XML,而不是 headers)。另外,我不认为它的开头应该有一个 CR/NL 。这是 HTTP header 和 body.
更新
我已验证来自 DocuSign 网站的 PHP 代码工作正常。
有效负载值不得包含前导换行符或尾随换行符。它应该以 <?xml
开头并以 >
我怀疑您的软件正在添加前导或尾随换行符。
秘密(来自 DocuSign)以 =
结尾。它是一个 Base64 编码值。 不要解码。将其用作字符串即可。
另一个更新
负载(请求的 body)包含零个新行。
如果要打印负载,则需要将其包装在 <pre>
中,因为它包含 <
个字符。或者查看页面源码。
包含UTF-8XML如
<?xml version="1.0" encoding="utf-8"?><DocuSignEnvelopeInformation xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.docusign.net/API/3.0"><EnvelopeStatus><RecipientStatuses><RecipientStatus><Type>Signer</Type><Email>larry@worldwidecorp.us</Email><UserName>Larry Kluger</UserName><RoutingOrder>1</RoutingOrder><Sent>2020-08-05T03:11:13.057</Sent><Delivered>2020-08-05T03:11:27.657</Delivered><DeclineReason xsi:nil="true" /><Status>Delivered</Status><RecipientIPAddress>5.102.239.40</RecipientIPAddress><CustomFields /><TabStatuses><TabStatus><TabType>Custom</TabType><Status>Active</Status><XPosition>223</XPosition><YPosition>744....
我们做了更多的测试,
$payload = file_get_contents('php://input');
应该非常在脚本的早期。问题是框架可以修改 php://input
流,因此此后它将无法正常工作。
注意this page from the Symfony site -- 说明获取请求的正确方式body是:
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\ParameterBag;
$app->before(function (Request $request) {
$payload = $request->getContent();
hmac_verify($payload, $secret);
});
我会尝试使用 Symfony 代码而不是 file_get_contents('php://input');