1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
/* |
6
|
|
|
* This file is part of the box project. |
7
|
|
|
* |
8
|
|
|
* (c) Kevin Herrera <[email protected]> |
9
|
|
|
* Théo Fidry <[email protected]> |
10
|
|
|
* |
11
|
|
|
* This source file is subject to the MIT license that is bundled |
12
|
|
|
* with this source code in the file LICENSE. |
13
|
|
|
*/ |
14
|
|
|
|
15
|
|
|
namespace KevinGH\Box; |
16
|
|
|
|
17
|
|
|
use KevinGH\Box\Exception\Exception; |
18
|
|
|
use KevinGH\Box\Exception\FileException; |
19
|
|
|
use KevinGH\Box\Exception\OpenSslException; |
20
|
|
|
use KevinGH\Box\Signature\Hash; |
21
|
|
|
use KevinGH\Box\Signature\PublicKeyDelegate; |
22
|
|
|
use KevinGH\Box\Signature\VerifyInterface; |
23
|
|
|
use PharException; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Retrieves and verifies a phar's signature without using the extension. |
27
|
|
|
* |
28
|
|
|
* While the phar extension is not used to retrieve or verify a phar's |
29
|
|
|
* signature, other extensions may still be needed to properly process |
30
|
|
|
* the signature. |
31
|
|
|
* |
32
|
|
|
* @author Kevin Herrera <[email protected]> |
33
|
|
|
*/ |
34
|
|
|
class Signature |
35
|
|
|
{ |
36
|
|
|
/** |
37
|
|
|
* The phar file path. |
38
|
|
|
* |
39
|
|
|
* @var string |
40
|
|
|
*/ |
41
|
|
|
private $file; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* The file handle. |
45
|
|
|
* |
46
|
|
|
* @var resource |
47
|
|
|
*/ |
48
|
|
|
private $handle; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* The size of the file. |
52
|
|
|
* |
53
|
|
|
* @var int |
54
|
|
|
*/ |
55
|
|
|
private $size; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* The recognized signature types. |
59
|
|
|
* |
60
|
|
|
* @var array |
61
|
|
|
*/ |
62
|
|
|
private static $types = [ |
63
|
|
|
[ |
64
|
|
|
'name' => 'MD5', |
65
|
|
|
'flag' => 0x01, |
66
|
|
|
'size' => 16, |
67
|
|
|
'class' => Hash::class, |
68
|
|
|
], |
69
|
|
|
[ |
70
|
|
|
'name' => 'SHA-1', |
71
|
|
|
'flag' => 0x02, |
72
|
|
|
'size' => 20, |
73
|
|
|
'class' => Hash::class, |
74
|
|
|
], |
75
|
|
|
[ |
76
|
|
|
'name' => 'SHA-256', |
77
|
|
|
'flag' => 0x03, |
78
|
|
|
'size' => 32, |
79
|
|
|
'class' => Hash::class, |
80
|
|
|
], |
81
|
|
|
[ |
82
|
|
|
'name' => 'SHA-512', |
83
|
|
|
'flag' => 0x04, |
84
|
|
|
'size' => 64, |
85
|
|
|
'class' => Hash::class, |
86
|
|
|
], |
87
|
|
|
[ |
88
|
|
|
'name' => 'OpenSSL', |
89
|
|
|
'flag' => 0x10, |
90
|
|
|
'size' => null, |
91
|
|
|
'class' => PublicKeyDelegate::class, |
92
|
|
|
], |
93
|
|
|
]; |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* Sets the phar file path. |
97
|
|
|
* |
98
|
|
|
* @param string $path the phar file path |
99
|
|
|
* |
100
|
|
|
* @throws Exception |
101
|
|
|
* @throws FileException if the file does not exist |
102
|
|
|
*/ |
103
|
|
|
public function __construct($path) |
104
|
|
|
{ |
105
|
|
|
if (!is_file($path)) { |
106
|
|
|
throw FileException::create( |
107
|
|
|
'The path "%s" does not exist or is not a file.', |
108
|
|
|
$path |
109
|
|
|
); |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
$this->file = realpath($path); |
|
|
|
|
113
|
|
|
|
114
|
|
|
if (false === ($this->size = @filesize($path))) { |
115
|
|
|
throw FileException::lastError(); |
116
|
|
|
} |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* Closes the open file handle. |
121
|
|
|
*/ |
122
|
|
|
public function __destruct() |
123
|
|
|
{ |
124
|
|
|
$this->close(); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* Creates a new instance of Signature. |
129
|
|
|
* |
130
|
|
|
* @param string $path the phar file path |
131
|
|
|
* |
132
|
|
|
* @return Signature the new instance |
133
|
|
|
*/ |
134
|
|
|
public static function create($path) |
135
|
|
|
{ |
136
|
|
|
return new self($path); |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* Returns the signature for the phar. |
141
|
|
|
* |
142
|
|
|
* The value returned is identical to that of `Phar->getSignature()`. If |
143
|
|
|
* $required is not given, it will default to the `phar.require_hash` |
144
|
|
|
* current value. |
145
|
|
|
* |
146
|
|
|
* @param bool $required Is the signature required? |
147
|
|
|
* |
148
|
|
|
* @throws PharException if the phar is not valid |
149
|
|
|
* |
150
|
|
|
* @return array the signature |
151
|
|
|
*/ |
152
|
|
|
public function get($required = null) |
153
|
|
|
{ |
154
|
|
|
if (null === $required) { |
155
|
|
|
$required = (bool) ini_get('phar.require_hash'); |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
$this->seek(-4, SEEK_END); |
159
|
|
|
|
160
|
|
|
if ('GBMB' !== $this->read(4)) { |
161
|
|
|
if ($required) { |
162
|
|
|
throw new PharException( |
163
|
|
|
sprintf( |
164
|
|
|
'The phar "%s" is not signed.', |
165
|
|
|
$this->file |
166
|
|
|
) |
167
|
|
|
); |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
return null; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
$this->seek(-8, SEEK_END); |
174
|
|
|
|
175
|
|
|
$flag = unpack('V', $this->read(4)); |
|
|
|
|
176
|
|
|
$flag = $flag[1]; |
177
|
|
|
|
178
|
|
|
foreach (self::$types as $type) { |
179
|
|
|
if ($flag === $type['flag']) { |
180
|
|
|
break; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
unset($type); |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
if (!isset($type)) { |
187
|
|
|
throw new PharException( |
188
|
|
|
sprintf( |
189
|
|
|
'The signature type (%x) is not recognized for the phar "%s".', |
190
|
|
|
$flag, |
191
|
|
|
$this->file |
192
|
|
|
) |
193
|
|
|
); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
$offset = -8; |
197
|
|
|
|
198
|
|
|
if (0x10 === $type['flag']) { |
|
|
|
|
199
|
|
|
$offset = -12; |
200
|
|
|
|
201
|
|
|
$this->seek(-12, SEEK_END); |
202
|
|
|
|
203
|
|
|
$type['size'] = unpack('V', $this->read(4)); |
204
|
|
|
$type['size'] = $type['size'][1]; |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
$this->seek($offset - $type['size'], SEEK_END); |
208
|
|
|
|
209
|
|
|
$hash = $this->read($type['size']); |
210
|
|
|
$hash = unpack('H*', $hash); |
211
|
|
|
|
212
|
|
|
return [ |
213
|
|
|
'hash_type' => $type['name'], |
214
|
|
|
'hash' => strtoupper($hash[1]), |
215
|
|
|
]; |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* Verifies the signature of the phar. |
220
|
|
|
* |
221
|
|
|
* @throws Exception |
222
|
|
|
* @throws FileException if the private key could not be read |
223
|
|
|
* @throws OpenSslException if there is an OpenSSL error |
224
|
|
|
* |
225
|
|
|
* @return bool TRUE if verified, FALSE if not |
226
|
|
|
*/ |
227
|
|
|
public function verify() |
228
|
|
|
{ |
229
|
|
|
$signature = $this->get(); |
230
|
|
|
|
231
|
|
|
$size = $this->size; |
232
|
|
|
$type = null; |
233
|
|
|
|
234
|
|
|
foreach (self::$types as $type) { |
235
|
|
|
if ($type['name'] === $signature['hash_type']) { |
236
|
|
|
if (0x10 === $type['flag']) { |
237
|
|
|
$this->seek(-12, SEEK_END); |
238
|
|
|
|
239
|
|
|
$less = $this->read(4); |
240
|
|
|
$less = unpack('V', $less); |
|
|
|
|
241
|
|
|
$less = $less[1]; |
242
|
|
|
|
243
|
|
|
$size -= 12 + $less; |
244
|
|
|
} else { |
245
|
|
|
$size -= 8 + $type['size']; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
break; |
249
|
|
|
} |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
$this->seek(0); |
253
|
|
|
|
254
|
|
|
/** @var $verify VerifyInterface */ |
255
|
|
|
$verify = new $type['class'](); |
256
|
|
|
$verify->init($type['name'], $this->file); |
257
|
|
|
|
258
|
|
|
$buffer = 64; |
259
|
|
|
|
260
|
|
|
while (0 < $size) { |
261
|
|
|
if ($size < $buffer) { |
262
|
|
|
$buffer = $size; |
263
|
|
|
$size = 0; |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
$verify->update($this->read($buffer)); |
267
|
|
|
|
268
|
|
|
$size -= $buffer; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
return $verify->verify($signature['hash']); |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
/** |
275
|
|
|
* Closes the open file handle. |
276
|
|
|
*/ |
277
|
|
|
private function close(): void |
278
|
|
|
{ |
279
|
|
|
if (is_resource($this->handle)) { |
280
|
|
|
@fclose($this->handle); |
|
|
|
|
281
|
|
|
|
282
|
|
|
$this->handle = null; |
283
|
|
|
} |
284
|
|
|
} |
285
|
|
|
|
286
|
|
|
/** |
287
|
|
|
* Returns the file handle. |
288
|
|
|
* |
289
|
|
|
* If the file handle is not opened, it will be automatically opened. |
290
|
|
|
* |
291
|
|
|
* @throws Exception |
292
|
|
|
* @throws FileException if the file could not be opened |
293
|
|
|
* |
294
|
|
|
* @return resource the file handle |
295
|
|
|
*/ |
296
|
|
|
private function handle() |
297
|
|
|
{ |
298
|
|
|
if (!$this->handle) { |
299
|
|
|
if (!($this->handle = @fopen($this->file, 'rb'))) { |
|
|
|
|
300
|
|
|
throw FileException::lastError(); |
301
|
|
|
} |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
return $this->handle; |
|
|
|
|
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
/** |
308
|
|
|
* Reads a number of bytes from the file. |
309
|
|
|
* |
310
|
|
|
* @param int $bytes the number of bytes |
311
|
|
|
* |
312
|
|
|
* @throws Exception |
313
|
|
|
* @throws FileException if the file could not be read |
314
|
|
|
* |
315
|
|
|
* @return string the read bytes |
316
|
|
|
*/ |
317
|
|
|
private function read($bytes) |
318
|
|
|
{ |
319
|
|
|
if (false === ($read = @fread($this->handle(), $bytes))) { |
320
|
|
|
throw FileException::lastError(); |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
if (($actual = strlen($read)) !== $bytes) { |
324
|
|
|
throw FileException::create( |
325
|
|
|
'Only read %d of %d bytes from "%s".', |
326
|
|
|
$actual, |
327
|
|
|
$bytes, |
328
|
|
|
$this->file |
329
|
|
|
); |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
return $read; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
/** |
336
|
|
|
* Seeks to a specific point in the file. |
337
|
|
|
* |
338
|
|
|
* @param int $offset the offset to seek |
339
|
|
|
* @param int $whence the direction |
340
|
|
|
* |
341
|
|
|
* @throws Exception |
342
|
|
|
* @throws FileException if the file could not be seeked |
343
|
|
|
*/ |
344
|
|
|
private function seek($offset, $whence = SEEK_SET): void |
345
|
|
|
{ |
346
|
|
|
if (-1 === @fseek($this->handle(), $offset, $whence)) { |
347
|
|
|
throw FileException::lastError(); |
348
|
|
|
} |
349
|
|
|
} |
350
|
|
|
} |
351
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.