WebhookSignature.php 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. <?php
  2. namespace Stripe;
  3. abstract class WebhookSignature
  4. {
  5. const EXPECTED_SCHEME = 'v1';
  6. /**
  7. * Verifies the signature header sent by Stripe. Throws an
  8. * Exception\SignatureVerificationException exception if the verification fails for
  9. * any reason.
  10. *
  11. * @param string $payload the payload sent by Stripe
  12. * @param string $header the contents of the signature header sent by
  13. * Stripe
  14. * @param string $secret secret used to generate the signature
  15. * @param int $tolerance maximum difference allowed between the header's
  16. * timestamp and the current time
  17. *
  18. * @throws Exception\SignatureVerificationException if the verification fails
  19. *
  20. * @return bool
  21. */
  22. public static function verifyHeader($payload, $header, $secret, $tolerance = null)
  23. {
  24. // Extract timestamp and signatures from header
  25. $timestamp = self::getTimestamp($header);
  26. $signatures = self::getSignatures($header, self::EXPECTED_SCHEME);
  27. if (-1 === $timestamp) {
  28. throw Exception\SignatureVerificationException::factory(
  29. 'Unable to extract timestamp and signatures from header',
  30. $payload,
  31. $header
  32. );
  33. }
  34. if (empty($signatures)) {
  35. throw Exception\SignatureVerificationException::factory(
  36. 'No signatures found with expected scheme',
  37. $payload,
  38. $header
  39. );
  40. }
  41. // Check if expected signature is found in list of signatures from
  42. // header
  43. $signedPayload = "{$timestamp}.{$payload}";
  44. $expectedSignature = self::computeSignature($signedPayload, $secret);
  45. $signatureFound = false;
  46. foreach ($signatures as $signature) {
  47. if (Util\Util::secureCompare($expectedSignature, $signature)) {
  48. $signatureFound = true;
  49. break;
  50. }
  51. }
  52. if (!$signatureFound) {
  53. throw Exception\SignatureVerificationException::factory(
  54. 'No signatures found matching the expected signature for payload',
  55. $payload,
  56. $header
  57. );
  58. }
  59. // Check if timestamp is within tolerance
  60. if (($tolerance > 0) && (\abs(\time() - $timestamp) > $tolerance)) {
  61. throw Exception\SignatureVerificationException::factory(
  62. 'Timestamp outside the tolerance zone',
  63. $payload,
  64. $header
  65. );
  66. }
  67. return true;
  68. }
  69. /**
  70. * Extracts the timestamp in a signature header.
  71. *
  72. * @param string $header the signature header
  73. *
  74. * @return int the timestamp contained in the header, or -1 if no valid
  75. * timestamp is found
  76. */
  77. private static function getTimestamp($header)
  78. {
  79. $items = \explode(',', $header);
  80. foreach ($items as $item) {
  81. $itemParts = \explode('=', $item, 2);
  82. if ('t' === $itemParts[0]) {
  83. if (!\is_numeric($itemParts[1])) {
  84. return -1;
  85. }
  86. return (int) ($itemParts[1]);
  87. }
  88. }
  89. return -1;
  90. }
  91. /**
  92. * Extracts the signatures matching a given scheme in a signature header.
  93. *
  94. * @param string $header the signature header
  95. * @param string $scheme the signature scheme to look for
  96. *
  97. * @return array the list of signatures matching the provided scheme
  98. */
  99. private static function getSignatures($header, $scheme)
  100. {
  101. $signatures = [];
  102. $items = \explode(',', $header);
  103. foreach ($items as $item) {
  104. $itemParts = \explode('=', $item, 2);
  105. if (\trim($itemParts[0]) === $scheme) {
  106. $signatures[] = $itemParts[1];
  107. }
  108. }
  109. return $signatures;
  110. }
  111. /**
  112. * Computes the signature for a given payload and secret.
  113. *
  114. * The current scheme used by Stripe ("v1") is HMAC/SHA-256.
  115. *
  116. * @param string $payload the payload to sign
  117. * @param string $secret the secret used to generate the signature
  118. *
  119. * @return string the signature as a string
  120. */
  121. private static function computeSignature($payload, $secret)
  122. {
  123. return \hash_hmac('sha256', $payload, $secret);
  124. }
  125. }