Completed
Pull Request — master (#9)
by ARCANEDEV
02:34
created

SslChecker::checkBlackList()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2.0116

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 11
ccs 6
cts 7
cp 0.8571
rs 9.4285
cc 2
eloc 7
nc 2
nop 1
crap 2.0116
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\Utilities\Request
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
     * @throws ApiConnectionException
66
     *
67
     * @return bool
68
     */
69
    public function checkCert($url)
70
    {
71
        if ( ! $this->hasStreamExtensions()) {
72
            return $this->showStreamExtensionWarning();
73
        }
74
75
        $this->setUrl($url);
76
77
        list($result, $errorNo, $errorStr) = $this->streamSocketClient();
78
79
        $this->checkResult($result, $errorNo, $errorStr);
80
81
        $params = stream_context_get_params($result);
82
        $cert   = $params['options']['ssl']['peer_certificate'];
83
84
        openssl_x509_export($cert, $pemCert);
85
86
        $this->checkBlackList($pemCert);
87
88
        return true;
89
    }
90
91
    /* ------------------------------------------------------------------------------------------------
92
     |  Check Functions
93
     | ------------------------------------------------------------------------------------------------
94
     */
95
    /**
96
     * Check black list.
97
     *
98
     * @param  string  $pemCert
99
     *
100
     * @throws ApiConnectionException
101
     */
102 5
    public function checkBlackList($pemCert)
103
    {
104 5
        if ($this->isBlackListed($pemCert)) {
105 5
            throw new ApiConnectionException(
106
                'Invalid server certificate. You tried to connect to a server that '.
107 5
                'has a revoked SSL certificate, which means we cannot securely send '.
108 5
                'data to that server.  Please email [email protected] if you need '.
109
                'help connecting to the correct API server.'
110 5
            );
111
        }
112
    }
113
114
    /**
115
     * Checks if a valid PEM encoded certificate is blacklisted.
116
     *
117
     * @param  string  $cert
118
     *
119
     * @return bool
120
     */
121 10
    public function isBlackListed($cert)
122
    {
123 10
        $lines = explode("\n", trim($cert));
124
125
        // Kludgily remove the PEM padding
126 10
        array_shift($lines);
127 10
        array_pop($lines);
128
129 10
        $derCert     = base64_decode(implode('', $lines));
130 10
        $fingerprint = sha1($derCert);
131
132 10
        return in_array($fingerprint, [
133 10
            '05c0b3643694470a888c6e7feb5c9e24e823dc53',
134 10
            '5b7dc7fbc98d78bf76d4d4fa6f597a0c901fad5c',
135 10
        ]);
136
    }
137
138
139
    /**
140
     * Stream Extension exists - Return true if one of the extensions not found.
141
     *
142
     * @return bool
143
     */
144
    private function hasStreamExtensions()
145
    {
146
        return function_exists('stream_context_get_params') &&
147
               function_exists('stream_socket_enable_crypto');
148
    }
149
150
    /**
151
     * Check if has errors or empty result.
152
     *
153
     * @param  mixed     $result
154
     * @param  int|null  $errorNo
155
     * @param  string    $errorStr
156
     *
157
     * @throws ApiConnectionException
158
     */
159 10
    private function checkResult($result, $errorNo, $errorStr)
160
    {
161
        if (
162 10
            ($errorNo !== 0 && $errorNo !== null) ||
163
            $result === false
164 10
        ) {
165 10
            $stripeStatus = 'https://twitter.com/stripestatus';
166
167 10
            throw new ApiConnectionException(
168 10
                'Could not connect to Stripe (' . $this->getUrl() . ').  Please check your internet connection and try again.  '.
169 10
                'If this problem persists, you should check Stripe\'s service status at ' . $stripeStatus . '.  '.
170 10
                'Reason was: ' . $errorStr
171 10
            );
172
        }
173
    }
174
175
    /**
176
     * Check if has SSL Errors
177
     *
178
     * @param  int  $errorNum
179
     *
180
     * @return bool
181
     */
182 638
    public static function hasCertErrors($errorNum)
183
    {
184 638
        return in_array($errorNum, [
185 638
            CURLE_SSL_CACERT,
186 638
            CURLE_SSL_PEER_CERTIFICATE,
187
            CURLE_SSL_CACERT_BADFILE
188 638
        ]);
189
    }
190
191
    /* ------------------------------------------------------------------------------------------------
192
     |  Other Functions
193
     | ------------------------------------------------------------------------------------------------
194
     */
195
    /**
196
     * Prepare SSL URL.
197
     *
198
     * @param  string  $url
199
     *
200
     * @return string
201
     */
202 5
    private function prepareUrl($url)
203
    {
204 5
        $url  = parse_url($url);
205 5
        $port = isset($url['port']) ? $url['port'] : 443;
206
207 5
        return "ssl://{$url['host']}:{$port}";
208
    }
209
210
    /**
211
     * Open a socket connection.
212
     *
213
     * @return array
214
     */
215
    private function streamSocketClient()
216
    {
217
        $result = stream_socket_client(
218
            $this->getUrl(),
219
            $errorNo,
220
            $errorStr,
221
            30,
222
            STREAM_CLIENT_CONNECT,
223
            stream_context_create([
224
                'ssl' => [
225
                    'capture_peer_cert' => true,
226
                    'verify_peer'       => true,
227
                    'cafile'            => self::caBundle(),
228
                ]
229
            ])
230
        );
231
232
        return [$result, $errorNo, $errorStr];
233
    }
234
235
    /**
236
     * Get the certificates file path.
237
     *
238
     * @return string
239
     */
240 5
    public static function caBundle()
241
    {
242 5
        $path = realpath(__DIR__ . '/../../../data/ca-certificates.crt');
243
244 5
        return $path;
245
    }
246
247
    /**
248
     * Show Stream Extension Warning (stream_socket_enable_crypto is not supported in HHVM).
249
     *
250
     * @return true
251
     */
252 5
    private function showStreamExtensionWarning()
253
    {
254 5
        $message = 'Warning: ' . (is_hhvm() ? 'The HHVM (HipHop VM)' : 'This version of PHP') .
255 5
            ' does not support checking SSL certificates Stripe cannot guarantee that the server has a ' .
256 5
            'certificate which is not blacklisted.';
257
258 5
        if ( ! is_testing()) {
259
            error_log($message);
260
        }
261
262 5
        return true;
263
    }
264
}
265