Skip to content
Advertisement

PHP / Docusign – Verify HMAC signature on completed event

I’m trying to secure my callback url when completed event is triggered.

My Controller:

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

My DocusignService functions:

    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;
    }

I’ve created my hmac key in my Docusign Connect and i’m receiving the signature in the header and the payload but the verification always failed. I’ve followed the Docusign documentation here What’s wrong ?

PS: Sorry for my bad english

Advertisement

Answer

Your code looks good to me. Make sure that you are only sending one HMAC signature. That way your hmacKey is the correct one.

As a check, I’d print out the utf8_encode($payload) and check that it looks right (it should be the incoming XML, no headers). Also, I don’t think it should have a CR/NL in it at the beginning. That’s the separator between the HTTP header and body.

Update

I have verified that the PHP code from the DocuSign web site works correctly.

The payload value must not contain either a leading or trailing newline. It should start with <?xml and end with >

I suspect that your software is adding a leading or trailing newline.

The secret (from DocuSign) ends with an =. It is a Base64 encoded value. Do not decode it. Just use it as a string.

Another update

The payload (the body of the request) contains zero new lines.

If you’re printing the payload, you’ll need to wrap it in <pre> since it includes < characters. Or look at the page source.

It contains UTF-8 XML such as

<?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....

We’ve done some more testing, and the line

$payload = file_get_contents('php://input');

should be very early in your script. The problem is that a framework can munge the php://input stream so it won’t work properly thereafter.

Note this page from the Symfony site — it indicates that the right way to get the request body is:

use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationParameterBag;

$app->before(function (Request $request) {
        $payload = $request->getContent();
        hmac_verify($payload, $secret);
});

I would try to use the Symfony code instead of file_get_contents('php://input');

User contributions licensed under: CC BY-SA
2 People found this is helpful
Advertisement