1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace BitWasp\Bitcoin\PaymentProtocol; |
4
|
|
|
|
5
|
|
|
use BitWasp\Bitcoin\PaymentProtocol\Protobufs\PaymentRequest as PaymentRequestBuf; |
6
|
|
|
use BitWasp\Bitcoin\PaymentProtocol\Protobufs\X509Certificates as X509CertificatesBuf; |
7
|
|
|
|
8
|
|
|
class RequestSigner |
9
|
|
|
{ |
10
|
|
|
const SHA256 = 'x509+sha256'; |
11
|
|
|
const SHA1 = 'x509+sha1'; |
12
|
|
|
const NONE = 'none'; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* @var string |
16
|
|
|
*/ |
17
|
|
|
private $type; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* @var int |
21
|
|
|
*/ |
22
|
|
|
private $algoConst; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* @var X509CertificatesBuf |
26
|
|
|
*/ |
27
|
|
|
private $certificates; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* @var resource |
31
|
|
|
*/ |
32
|
|
|
private $privateKey; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* @param string $type |
36
|
|
|
* @param string $keyFile |
37
|
|
|
* @param string $certFile |
38
|
|
|
* @throws \Exception |
39
|
|
|
*/ |
40
|
|
|
public function __construct($type = null, $keyFile = '', $certFile = '') |
41
|
|
|
{ |
42
|
|
|
if ($type === null) { |
43
|
|
|
$type = self::NONE; |
44
|
|
|
} |
45
|
|
|
|
46
|
|
|
if (false === in_array($type, [self::NONE, self::SHA1, self::SHA256], true)) { |
47
|
|
|
throw new \InvalidArgumentException('Invalid BIP70 signature type'); |
48
|
|
|
} |
49
|
|
|
|
50
|
|
|
$this->type = $type; |
51
|
|
|
$this->certificates = new X509CertificatesBuf(); |
52
|
|
|
|
53
|
|
|
if ($type !== self::NONE) { |
54
|
|
|
$this->initialize($keyFile, $certFile); |
55
|
|
|
} |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @return RequestSigner |
60
|
|
|
*/ |
61
|
|
|
public static function none() |
62
|
|
|
{ |
63
|
|
|
return new self(self::NONE); |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* @param string $keyFile |
68
|
|
|
* @param string $certFile |
69
|
|
|
* @return RequestSigner |
70
|
|
|
*/ |
71
|
|
|
public static function sha1($keyFile, $certFile) |
72
|
|
|
{ |
73
|
|
|
return new self(self::SHA1, $keyFile, $certFile); |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* @param string $keyFile |
78
|
|
|
* @param string $certFile |
79
|
|
|
* @return RequestSigner |
80
|
|
|
*/ |
81
|
|
|
public static function sha256($keyFile, $certFile) |
82
|
|
|
{ |
83
|
|
|
return new self(self::SHA256, $keyFile, $certFile); |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* @return bool |
88
|
|
|
*/ |
89
|
|
|
public function supportsSha256() |
90
|
|
|
{ |
91
|
|
|
return defined('OPENSSL_ALGO_SHA256'); |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* @param string $keyFile - path to key file |
96
|
|
|
* @param string $certFile - path to certificate chain file |
97
|
|
|
* @throws \Exception |
98
|
|
|
*/ |
99
|
|
|
private function initialize($keyFile, $certFile) |
100
|
|
|
{ |
101
|
|
|
if (false === file_exists($keyFile)) { |
102
|
|
|
throw new \InvalidArgumentException('Private key file does not exist'); |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
if (false === file_exists($certFile)) { |
106
|
|
|
throw new \InvalidArgumentException('Certificate file does not exist'); |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
if (self::SHA256 === $this->type && !$this->supportsSha256()) { |
110
|
|
|
throw new \Exception('Server does not support x.509+SHA256'); |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
$chain = $this->fetchChain($certFile); |
114
|
|
|
if (!is_array($chain) || count($chain) === 0) { |
115
|
|
|
throw new \RuntimeException('Certificate file contains no certificates'); |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
foreach ($chain as $cert) { |
119
|
|
|
$this->certificates->addCertificate($cert); |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
$pkeyid = openssl_get_privatekey(file_get_contents($keyFile)); |
123
|
|
|
if (false === $pkeyid) { |
124
|
|
|
throw new \InvalidArgumentException('Private key is invalid'); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
$this->privateKey = $pkeyid; |
128
|
|
|
$this->algoConst = $this->type === self::SHA256 |
129
|
|
|
? OPENSSL_ALGO_SHA256 |
130
|
|
|
: OPENSSL_ALGO_SHA1; |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* @param string $data |
135
|
|
|
* @return string |
136
|
|
|
* @throws \Exception |
137
|
|
|
*/ |
138
|
|
|
private function signData($data) |
139
|
|
|
{ |
140
|
|
|
if ($this->type === self::NONE) { |
141
|
|
|
throw new \RuntimeException('signData called when Signer is not configured for signatures'); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
$signature = ''; |
145
|
|
|
if (!openssl_sign($data, $signature, $this->privateKey, $this->algoConst)) { |
146
|
|
|
throw new \Exception('PaymentRequestSigner: Unable to create signature'); |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
return $signature; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* Applies the configured signature algorithm, adding values to |
154
|
|
|
* the protobuf: 'pkiType', 'signature', 'pkiData' |
155
|
|
|
* |
156
|
|
|
* @param PaymentRequestBuf $request |
157
|
|
|
* @return PaymentRequestBuf |
158
|
|
|
* @throws \Exception |
159
|
|
|
*/ |
160
|
|
|
public function sign(PaymentRequestBuf $request) |
161
|
|
|
{ |
162
|
|
|
$request->setPkiType($this->type); |
163
|
|
|
$request->setSignature(''); |
164
|
|
|
|
165
|
|
|
if ($this->type !== self::NONE) { |
166
|
|
|
// PkiData must be captured in signature, and signature must be empty! |
167
|
|
|
$request->setPkiData($this->certificates->serialize()); |
168
|
|
|
$signature = $this->signData($request->serialize()); |
169
|
|
|
$request->setSignature($signature); |
170
|
|
|
} |
171
|
|
|
|
172
|
|
|
return $request; |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* @param PaymentRequestBuf $request |
177
|
|
|
* @return bool |
178
|
|
|
*/ |
179
|
|
|
public function verify(PaymentRequestBuf $request) |
180
|
|
|
{ |
181
|
|
|
$type = $request->getPkiType(); |
182
|
|
|
if ($type === self::NONE) { |
183
|
|
|
return true; |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
if ($type === self::SHA256) { |
187
|
|
|
$algorithm = OPENSSL_ALGO_SHA256; |
188
|
|
|
} else if ($type === self::SHA1) { |
189
|
|
|
$algorithm = OPENSSL_ALGO_SHA1; |
190
|
|
|
} else { |
191
|
|
|
throw new \RuntimeException('Unsupported signature algorithm'); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
$clone = clone $request; |
195
|
|
|
$clone->setSignature(''); |
196
|
|
|
$data = $clone->serialize(); |
197
|
|
|
|
198
|
|
|
// Parse the public key |
199
|
|
|
$certificates = new X509CertificatesBuf(); |
200
|
|
|
$certificates->parse($clone->getPkiData()); |
201
|
|
|
$certificate = $this->der2pem($certificates->getCertificate(0)); |
202
|
|
|
$pubkeyid = openssl_pkey_get_public($certificate); |
203
|
|
|
|
204
|
|
|
return 1 === openssl_verify($data, $request->getSignature(), $pubkeyid, $algorithm); |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* Checks whether the decoded certificate is a root / self-signed certificate |
209
|
|
|
* @param array $certificate |
210
|
|
|
* @return bool |
211
|
|
|
*/ |
212
|
|
|
private function isRoot($certificate) |
213
|
|
|
{ |
214
|
|
|
return $certificate['issuer'] === $certificate['subject']; |
215
|
|
|
} |
216
|
|
|
|
217
|
|
|
/** |
218
|
|
|
* Fetches parent certificates using network requests |
219
|
|
|
* Todo: review use of file_get_contents |
220
|
|
|
* @param $leafCertificate |
221
|
|
|
* @return false|string |
222
|
|
|
*/ |
223
|
|
|
private function fetchCertificateParent($leafCertificate) |
224
|
|
|
{ |
225
|
|
|
$pattern = '/CA Issuers - URI:(\\S*)/'; |
226
|
|
|
$matches = array(); |
227
|
|
|
|
228
|
|
|
$nMatches = preg_match_all($pattern, $leafCertificate['extensions']['authorityInfoAccess'], $matches); |
229
|
|
|
if ($nMatches === 0) { |
230
|
|
|
return false; |
231
|
|
|
} |
232
|
|
|
foreach ($matches[1] as $url) { |
233
|
|
|
$parentCert = file_get_contents($url); |
234
|
|
|
if ($parentCert && $this->parseCertificate($parentCert)) { |
235
|
|
|
return $parentCert; |
236
|
|
|
} |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
return false; |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
/** |
243
|
|
|
* Parses a PEM or DER certificate |
244
|
|
|
* @param string $certData |
245
|
|
|
* @return array |
246
|
|
|
*/ |
247
|
|
|
private function parseCertificate($certData) |
248
|
|
|
{ |
249
|
|
|
$begin = '-----BEGIN CERTIFICATE-----'; |
250
|
|
|
$end = '-----END CERTIFICATE-----'; |
251
|
|
|
|
252
|
|
|
if (strpos($certData, $begin) !== false) { |
253
|
|
|
return openssl_x509_parse($certData); |
254
|
|
|
} |
255
|
|
|
$d = $begin . "\n"; |
256
|
|
|
$d .= chunk_split(base64_encode($certData)); |
257
|
|
|
$d .= $end . "\n"; |
258
|
|
|
return openssl_x509_parse($d); |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
/** |
262
|
|
|
* @param $certData |
263
|
|
|
* @return array |
264
|
|
|
*/ |
265
|
|
|
private function der2pem($certData) |
266
|
|
|
{ |
267
|
|
|
$begin = '-----BEGIN CERTIFICATE-----'; |
268
|
|
|
$end = '-----END CERTIFICATE-----'; |
269
|
|
|
|
270
|
|
|
$d = $begin . "\n"; |
271
|
|
|
$d .= chunk_split(base64_encode($certData)); |
272
|
|
|
$d .= $end . "\n"; |
273
|
|
|
return $d; |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
/** |
277
|
|
|
* Decode PEM data, return the internal DER data |
278
|
|
|
* @param string $pem_data - pem certificate data |
279
|
|
|
* @return string |
280
|
|
|
*/ |
281
|
|
|
private function pem2der($pem_data) |
282
|
|
|
{ |
283
|
|
|
$begin = 'CERTIFICATE-----'; |
284
|
|
|
$end = '-----END'; |
285
|
|
|
if (strpos($pem_data, $begin) === false) { |
286
|
|
|
return $pem_data; |
287
|
|
|
} |
288
|
|
|
$pem_data = substr($pem_data, strpos($pem_data, $begin) + strlen($begin)); |
289
|
|
|
$pem_data = substr($pem_data, 0, strpos($pem_data, $end)); |
290
|
|
|
$der = base64_decode($pem_data); |
291
|
|
|
return $der; |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
/** |
295
|
|
|
* @param string $leaf - path to a file with certificates |
296
|
|
|
* @return array|bool |
297
|
|
|
*/ |
298
|
|
|
private function fetchChain($leaf) |
299
|
|
|
{ |
300
|
|
|
$result = array(); |
301
|
|
|
$leaf = file_get_contents($leaf); |
302
|
|
|
$cert = $this->parseCertificate($leaf); |
303
|
|
|
if ($cert === false) { |
304
|
|
|
return false; |
305
|
|
|
} |
306
|
|
|
|
307
|
|
|
$certData = self::pem2der($leaf); |
308
|
|
|
$result[] = $certData; |
309
|
|
|
while ($cert) { |
|
|
|
|
310
|
|
|
$result[] = $certData; |
311
|
|
|
// Only break after adding Cert Data - allows for self-signed certificates |
312
|
|
|
if ($this->isRoot($cert)) { |
313
|
|
|
break; |
314
|
|
|
} |
315
|
|
|
$certData = $this->fetchCertificateParent($cert); |
316
|
|
|
$cert = $this->parseCertificate($certData); |
|
|
|
|
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
return $result; |
320
|
|
|
} |
321
|
|
|
} |
322
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.