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