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');