Completed
Pull Request — master (#301)
by thomas
72:42 queued 01:01
created

RequestSigner::supportsSha256()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 1
b 0
f 0
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 === RequestSigner::NONE) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
183
            return true;
184
        }
185
186
        if ($type === RequestSigner::SHA256) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
187
            $algorithm = OPENSSL_ALGO_SHA256;
188
        } else if ($type === RequestSigner::SHA1) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $cert of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
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);
0 ignored issues
show
Security Bug introduced by
It seems like $certData defined by $this->fetchCertificateParent($cert) on line 315 can also be of type false; however, BitWasp\Bitcoin\PaymentP...ner::parseCertificate() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
317
        }
318
319
        return $result;
320
    }
321
}
322