Completed
Pull Request — master (#15)
by ARCANEDEV
11:13
created

SslChecker::checkCert()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 21
ccs 0
cts 11
cp 0
rs 9.3142
cc 2
eloc 11
nc 2
nop 1
crap 6
1
<?php namespace Arcanedev\Stripe\Http\Curl;
2
3
use Arcanedev\Stripe\Contracts\Http\Curl\SslCheckerInterface;
4
use Arcanedev\Stripe\Exceptions\ApiConnectionException;
5
6
/**
7
 * Class     SslChecker
8
 *
9
 * @package  Arcanedev\Stripe\Http\Curl
10
 * @author   ARCANEDEV <[email protected]>
11
 */
12
class SslChecker implements SslCheckerInterface
13
{
14
    /* ------------------------------------------------------------------------------------------------
15
     |  Properties
16
     | ------------------------------------------------------------------------------------------------
17
     */
18
    /** @var string */
19
    protected $url = '';
20
21
    /* ------------------------------------------------------------------------------------------------
22
     |  Getters & Setters
23
     | ------------------------------------------------------------------------------------------------
24
     */
25
    /**
26
     * Get URL.
27
     *
28
     * @return string
29
     */
30 15
    public function getUrl()
31
    {
32 15
        return $this->url;
33
    }
34
35
    /**
36
     * Set URL.
37
     *
38
     * @param  string  $url
39
     *
40
     * @return self
41
     */
42 5
    public function setUrl($url)
43
    {
44 5
        $this->url = $this->prepareUrl($url);
45
46 5
        return $this;
47
    }
48
49
    /* ------------------------------------------------------------------------------------------------
50
     |  Main Functions
51
     | ------------------------------------------------------------------------------------------------
52
     */
53
    /**
54
     * Preflight the SSL certificate presented by the backend. This isn't 100%
55
     * bulletproof, in that we're not actually validating the transport used to
56
     * communicate with Stripe, merely that the first attempt to does not use a
57
     * revoked certificate.
58
     *
59
     * Unfortunately the interface to OpenSSL doesn't make it easy to check the
60
     * certificate before sending potentially sensitive data on the wire. This
61
     * approach raises the bar for an attacker significantly.
62
     *
63
     * @param  string  $url
64
     *
65
     * @return bool
66
     */
67
    public function checkCert($url)
68
    {
69
        if ( ! $this->hasStreamExtensions()) {
70
            return $this->showStreamExtensionWarning();
71
        }
72
73
        $this->setUrl($url);
74
75
        list($result, $errorNo, $errorStr) = $this->streamSocketClient();
76
77
        $this->checkResult($result, $errorNo, $errorStr);
78
79
        $params = stream_context_get_params($result);
80
        $cert   = $params['options']['ssl']['peer_certificate'];
81
82
        openssl_x509_export($cert, $pemCert);
83
84
        $this->checkBlackList($pemCert);
85
86
        return true;
87
    }
88
89
    /* ------------------------------------------------------------------------------------------------
90
     |  Check Functions
91
     | ------------------------------------------------------------------------------------------------
92
     */
93
    /**
94
     * Check black list.
95
     *
96
     * @param  string  $pemCert
97
     *
98
     * @throws \Arcanedev\Stripe\Exceptions\ApiConnectionException
99
     */
100 5
    public function checkBlackList($pemCert)
101
    {
102 5
        if ($this->isBlackListed($pemCert)) {
103 5
            throw new ApiConnectionException(
104
                'Invalid server certificate. You tried to connect to a server that '.
105 4
                'has a revoked SSL certificate, which means we cannot securely send '.
106 4
                'data to that server.  Please email [email protected] if you need '.
107 1
                'help connecting to the correct API server.'
108 4
            );
109
        }
110
    }
111
112
    /**
113
     * Checks if a valid PEM encoded certificate is blacklisted.
114
     *
115
     * @param  string  $cert
116
     *
117
     * @return bool
118
     */
119 10
    public function isBlackListed($cert)
120
    {
121 10
        $lines = explode("\n", trim($cert));
122
123
        // Kludgily remove the PEM padding
124 10
        array_shift($lines);
125 10
        array_pop($lines);
126
127 10
        $derCert     = base64_decode(implode('', $lines));
128 10
        $fingerprint = sha1($derCert);
129
130 10
        return in_array($fingerprint, [
131 10
            '05c0b3643694470a888c6e7feb5c9e24e823dc53',
132 8
            '5b7dc7fbc98d78bf76d4d4fa6f597a0c901fad5c',
133 8
        ]);
134
    }
135
136
137
    /**
138
     * Stream Extension exists - Return true if one of the extensions not found.
139
     *
140
     * @return bool
141
     */
142
    private function hasStreamExtensions()
143
    {
144
        return function_exists('stream_context_get_params') &&
145
               function_exists('stream_socket_enable_crypto');
146
    }
147
148
    /**
149
     * Check if has errors or empty result.
150
     *
151
     * @param  mixed     $result
152
     * @param  int|null  $errorNo
153
     * @param  string    $errorStr
154
     *
155
     * @throws \Arcanedev\Stripe\Exceptions\ApiConnectionException
156
     */
157 10
    private function checkResult($result, $errorNo, $errorStr)
158
    {
159
        if (
160 10
            ($errorNo !== 0 && $errorNo !== null) ||
161 2
            $result === false
162 8
        ) {
163 10
            $stripeStatus = 'https://twitter.com/stripestatus';
164
165 10
            throw new ApiConnectionException(
166 10
                'Could not connect to Stripe (' . $this->getUrl() . ').  Please check your internet connection and try again.  '.
167 10
                'If this problem persists, you should check Stripe\'s service status at ' . $stripeStatus . '.  '.
168 10
                'Reason was: ' . $errorStr
169 8
            );
170
        }
171
    }
172
173
    /**
174
     * Check if has SSL Errors
175
     *
176
     * @param  int  $errorNum
177
     *
178
     * @return bool
179
     */
180 654
    public static function hasCertErrors($errorNum)
181
    {
182 654
        return in_array($errorNum, [
183 654
            CURLE_SSL_CACERT,
184 654
            CURLE_SSL_PEER_CERTIFICATE,
185 131
            CURLE_SSL_CACERT_BADFILE
186 523
        ]);
187
    }
188
189
    /* ------------------------------------------------------------------------------------------------
190
     |  Other Functions
191
     | ------------------------------------------------------------------------------------------------
192
     */
193
    /**
194
     * Prepare SSL URL.
195
     *
196
     * @param  string  $url
197
     *
198
     * @return string
199
     */
200 5
    private function prepareUrl($url)
201
    {
202 5
        $url  = parse_url($url);
203 5
        $port = isset($url['port']) ? $url['port'] : 443;
204
205 5
        return "ssl://{$url['host']}:{$port}";
206
    }
207
208
    /**
209
     * Open a socket connection.
210
     *
211
     * @return array
212
     */
213
    private function streamSocketClient()
214
    {
215
        $result = stream_socket_client(
216
            $this->getUrl(),
217
            $errorNo,
218
            $errorStr,
219
            30,
220
            STREAM_CLIENT_CONNECT,
221
            stream_context_create([
222
                'ssl' => [
223
                    'capture_peer_cert' => true,
224
                    'verify_peer'       => true,
225
                    'cafile'            => self::caBundle(),
226
                ]
227
            ])
228
        );
229
230
        return [$result, $errorNo, $errorStr];
231
    }
232
233
    /**
234
     * Get the certificates file path.
235
     *
236
     * @return string
237
     */
238 5
    public static function caBundle()
239
    {
240 5
        $basePath = __DIR__ . '/../../..';
241
242 5
        return realpath($basePath . '/data/ca-certificates.crt');
243
    }
244
245
    /**
246
     * Show Stream Extension Warning (stream_socket_enable_crypto is not supported in HHVM).
247
     *
248
     * @return true
249
     */
250 5
    private function showStreamExtensionWarning()
251
    {
252 5
        $message = 'Warning: ' . (is_hhvm() ? 'The HHVM (HipHop VM)' : 'This version of PHP') .
253 5
            ' does not support checking SSL certificates Stripe cannot guarantee that the server has a ' .
254 5
            'certificate which is not blacklisted.';
255
256 5
        if ( ! is_testing()) {
257
            error_log($message);
258
        }
259
260 5
        return true;
261
    }
262
}
263