1: <?php
2: namespace Worldline\Connect\Sdk\Webhooks;
3:
4: /**
5: * Class SignatureValidator
6: *
7: * @package Worldline\Connect\Sdk\Webhooks
8: */
9: class SignatureValidator
10: {
11: /** @var SecretKeyStore */
12: private $secretKeyStore;
13:
14: /**
15: * @param SecretKeyStore $secretKeyStore
16: */
17: public function __construct(SecretKeyStore $secretKeyStore)
18: {
19: $this->secretKeyStore = $secretKeyStore;
20: }
21:
22: /**
23: * Validates the given body using the given request headers.
24: * @param string $body
25: * @param array $requestHeaders
26: * @throws SignatureValidationException
27: */
28: public function validate($body, $requestHeaders)
29: {
30: $this->validateBody($body, $requestHeaders);
31: }
32:
33: // utility methods
34:
35: private function validateBody($body, $requestHeaders)
36: {
37: $signature = $this->getHeaderValue($requestHeaders, 'X-GCS-Signature');
38: $keyId = $this->getHeaderValue($requestHeaders, 'X-GCS-KeyId');
39: $secretKey = $this->secretKeyStore->getSecretKey($keyId);
40:
41: $expectedSignature = base64_encode(hash_hmac("sha256", $body, $secretKey, true));
42:
43: $isValid = $this->areEqualSignatures($signature, $expectedSignature);
44: if (!$isValid) {
45: throw new SignatureValidationException("failed to validate signature '$signature'");
46: }
47: }
48:
49: private function areEqualSignatures($signature, $expectedSignature) {
50: if (function_exists('hash_equals')) {
51: return hash_equals($expectedSignature, $signature);
52: } else {
53: // copied from http://php.net/manual/en/function.hash-equals.php#115635
54: if (strlen($expectedSignature) != strlen($signature)) {
55: return false;
56: } else {
57: $res = $expectedSignature ^ $signature;
58: $ret = 0;
59: // @phpstan-ignore-next-line
60: for ($i = strlen($res) - 1; $i >= 0; $i--) $ret |= ord($res[$i]);
61: return !$ret;
62: }
63: }
64: }
65:
66: // general utility methods
67:
68: private function getHeaderValue($requestHeaders, $headerName) {
69: $lowerCaseHeaderName = strtolower($headerName);
70: foreach ($requestHeaders as $name => $value) {
71: if ($lowerCaseHeaderName === strtolower($name)) {
72: return $value;
73: }
74: }
75: throw new SignatureValidationException("could not find header '$headerName'");
76: }
77: }
78: