Passed
Push — release_2_0 ( b0c573...4fb011 )
by Stefan
11:13
created

RFC6614Tests::tlsClientSideCheck()   C

Complexity

Conditions 15
Paths 18

Size

Total Lines 45
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 33
nc 18
nop 1
dl 0
loc 45
rs 5.9166
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 40 and the first side effect is on line 26.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
/*
3
 * *****************************************************************************
4
 * Contributions to this work were made on behalf of the GÉANT project, a 
5
 * project that has received funding from the European Union’s Framework 
6
 * Programme 7 under Grant Agreements No. 238875 (GN3) and No. 605243 (GN3plus),
7
 * Horizon 2020 research and innovation programme under Grant Agreements No. 
8
 * 691567 (GN4-1) and No. 731122 (GN4-2).
9
 * On behalf of the aforementioned projects, GEANT Association is the sole owner
10
 * of the copyright in all material which was developed by a member of the GÉANT
11
 * project. GÉANT Vereniging (Association) is registered with the Chamber of 
12
 * Commerce in Amsterdam with registration number 40535155 and operates in the 
13
 * UK as a branch of GÉANT Vereniging.
14
 * 
15
 * Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands. 
16
 * UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK
17
 *
18
 * License: see the web/copyright.inc.php file in the file structure or
19
 *          <base_url>/copyright.php after deploying the software
20
 */
21
22
namespace core\diag;
23
24
use \Exception;
25
26
require_once dirname(dirname(__DIR__)) . "/config/_config.php";
27
28
/**
29
 * Test suite to verify that a given NAI realm has NAPTR records according to
30
 * consortium-agreed criteria
31
 * Can only be used if CONFIG_DIAGNOSTICS['RADIUSTESTS'] is configured.
32
 *
33
 * @author Stefan Winter <[email protected]>
34
 * @author Tomasz Wolniewicz <[email protected]>
35
 *
36
 * @license see LICENSE file in root directory
37
 *
38
 * @package Developer
39
 */
40
class RFC6614Tests extends AbstractTest {
41
42
    /**
43
     * dictionary of translatable texts around the certificates we check
44
     * 
45
     * @var array
46
     */
47
    private $TLS_certkeys = [];
48
    
49
    /**
50
     * list of IP addresses which are candidates for dynamic discovery targets
51
     * 
52
     * @var array
53
     */
54
    private $candidateIPs;
55
    
56
    /**
57
     * the hostname which should show up in the certificate when establishing
58
     * a connection to the RADIUS/TLS server (hostname is an intermediary result
59
     * of the RFC7585 DNS resolution algorithm, in SRV response)
60
     * 
61
     * @var string
62
     */
63
    private $expectedName;
64
    
65
    /**
66
     * associative array holding the server-side cert test results for a given IP (IP is the key)
67
     * 
68
     * @var array
69
     */
70
    public $TLS_CA_checks_result;
71
    
72
    /**
73
     * associative array holding the client-side cert test results for a given IP (IP is the key)
74
     * 
75
     * @var array
76
     */
77
    public $TLS_clients_checks_result;
78
79
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $expectedName should have a doc-comment as per coding-style.
Loading history...
80
     * Sets up the instance for testing of a number of candidate IPs
81
     * 
82
     * @param array $listOfIPs candidates to test
83
     */
84
    public function __construct($listOfIPs, $expectedName) {
85
        parent::__construct();
86
        \core\common\Entity::intoThePotatoes();
87
        $this->TLS_certkeys = [
88
            'eduPKI' => _('eduPKI'),
89
            'NCU' => _('Nicolaus Copernicus University'),
90
            'ACCREDITED' => _('accredited'),
91
            'NONACCREDITED' => _('non-accredited'),
92
            'CORRECT' => _('correct certificate'),
93
            'WRONGPOLICY' => _('certificate with wrong policy OID'),
94
            'EXPIRED' => _('expired certificate'),
95
            'REVOKED' => _('revoked certificate'),
96
            'PASS' => _('pass'),
97
            'FAIL' => _('fail'),
98
            'non-eduPKI-accredited' => _("eduroam-accredited CA (now only for tests)"),
99
        ];
100
        $this->TLS_CA_checks_result = [];
101
        $this->TLS_clients_checks_result = [];
102
        
103
        $this->candidateIPs = $listOfIPs;
104
        $this->expectedName = $expectedName;
105
        \core\common\Entity::outOfThePotatoes();
106
    }
107
108
    /**
109
     * run all checks on all candidates
110
     * 
111
     * @return void
112
     */
113
    public function allChecks() {
114
        foreach ($this->candidateIPs as $oneIP) {
115
            $this->cApathCheck($oneIP);
116
            $this->tlsClientSideCheck($oneIP);
117
        }
118
    }
119
    
120
    /**
121
     * This function executes openssl s_clientends command to check if a server accepts a CA
122
     * 
123
     * @param string $host IP:port
124
     * @return int returncode
125
     */
126
    public function cApathCheck(string $host) {
127
        if (!isset($this->TLS_CA_checks_result[$host])) {
128
            $this->TLS_CA_checks_result[$host] = [];
129
        }
130
        $opensslbabble = $this->execOpensslClient($host, '', $this->TLS_CA_checks_result[$host]);
131
        $overallRetval = $this->opensslCAResult($host, $opensslbabble);
132
        if ($overallRetval == AbstractTest::RETVAL_OK) {
133
            $this->checkServerName($host);
134
        }
135
        return $overallRetval;
136
    }
137
138
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $host should have a doc-comment as per coding-style.
Loading history...
139
     * checks whether the received servername matches the expected server name
140
     * 
141
     * @return bool yes or no
142
     */
143
    private function checkServerName($host) {
144
        // it could match CN or sAN:DNS, we don't care which
145
        if (isset($this->TLS_CA_checks_result[$host]['certdata']['subject']['CN'])) {
146
            $testNames = $this->TLS_CA_checks_result[$host]['certdata']['subject']['CN'];
147
            if (!is_array($testNames)) {
148
                $testNames = [$testNames];
149
            }
150
            if (array_search($this->expectedName, $testNames) !== FALSE) {
151
                return TRUE;
152
            }
153
        }
154
        if (isset($this->TLS_CA_checks_result[$host]['certdata']['extensions']['subjectaltname'])) {
155
            $testNames = $this->TLS_CA_checks_result[$host]['certdata']['extensions']['subjectaltname'];
156
            if (!is_array($testNames)) {
157
                $testNames = [$testNames];
158
            }
159
            foreach ($testNames as $oneName) {
160
                if (preg_match("/".$this->expectedName."/", $oneName) === 1) {
161
                    return TRUE;
162
                }
163
            }
164
        }
165
        $this->TLS_CA_checks_result[$host]['cert_oddity'] = RADIUSTests::CERTPROB_DYN_SERVER_NAME_MISMATCH;
166
        return FALSE;
167
        
168
    }
169
    
170
    /**
171
     * This function executes openssl s_client command to check if a server accepts a client certificate
172
     * 
173
     * @param string $host IP:port
174
     * @return int returncode
175
     */
176
    public function tlsClientSideCheck(string $host) {
177
        $res = RADIUSTests::RETVAL_OK;
178
        if (!is_array(CONFIG_DIAGNOSTICS['RADIUSTESTS']['TLS-clientcerts']) || count(CONFIG_DIAGNOSTICS['RADIUSTESTS']['TLS-clientcerts']) == 0) {
179
            return RADIUSTests::RETVAL_SKIPPED;
180
        }
181
        if (preg_match("/\[/", $host)) {
182
            return RADIUSTests::RETVAL_INVALID;
183
        }
184
        foreach (CONFIG_DIAGNOSTICS['RADIUSTESTS']['TLS-clientcerts'] as $type => $tlsclient) {
185
            $this->TLS_clients_checks_result[$host]['ca'][$type]['clientcertinfo']['from'] = $type;
186
            $this->TLS_clients_checks_result[$host]['ca'][$type]['clientcertinfo']['status'] = $tlsclient['status'];
187
            $this->TLS_clients_checks_result[$host]['ca'][$type]['clientcertinfo']['message'] = $this->TLS_certkeys[$tlsclient['status']];
188
            $this->TLS_clients_checks_result[$host]['ca'][$type]['clientcertinfo']['issuer'] = $tlsclient['issuerCA'];
189
            foreach ($tlsclient['certificates'] as $k => $cert) {
190
                $this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['status'] = $cert['status'];
191
                $this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['message'] = $this->TLS_certkeys[$cert['status']];
192
                $this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['expected'] = $cert['expected'];
193
                $add = ' -cert ' . ROOT . '/config/cli-certs/' . $cert['public'] . ' -key ' . ROOT . '/config/cli-certs/' . $cert['private'];
194
                if (!isset($this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k])) {
195
                    $this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k] = [];
196
                }
197
                $opensslbabble = $this->execOpensslClient($host, $add, $this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]);
198
                $res = $this->opensslClientsResult($host, $opensslbabble, $this->TLS_clients_checks_result, $type, $k);
199
                if ($cert['expected'] == 'PASS') {
200
                    if (!$this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['connected']) {
201
                        if (($tlsclient['status'] == 'ACCREDITED') && ($cert['status'] == 'CORRECT')) {
202
                            $this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['returncode'] = RADIUSTests::CERTPROB_NOT_ACCEPTED;
203
                            $this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['finalerror'] = 1;
204
                            break;
205
                        }
206
                    }
207
                } else {
208
                    if ($this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['connected']) {
209
                        $this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['returncode'] = RADIUSTests::CERTPROB_WRONGLY_ACCEPTED;
210
                    }
211
212
                    if (($this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['reason'] == RADIUSTests::CERTPROB_UNKNOWN_CA) && ($tlsclient['status'] == 'ACCREDITED') && ($cert['status'] == 'CORRECT')) {
213
                        $this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['finalerror'] = 1;
214
                        echo "koniec zabawy2<br>";
215
                        break;
216
                    }
217
                }
218
            }
219
        }
220
        return $res;
221
    }
222
223
    /**
224
     * This function executes openssl s_client command
225
     * 
226
     * @param string $host        IP address
227
     * @param string $arg         arguments to add to the openssl command 
228
     * @param array  $testresults by-reference: the testresults array we are writing into
229
     * @return array result of openssl s_client ...
230
     */
231
    private function execOpensslClient($host, $arg, &$testresults) {
232
// we got the IP address either from DNS (guaranteeing well-formedness)
233
// or from filter_var'ed user input. So it is always safe as an argument
234
// but code analysers want this more explicit, so here is this extra
235
// call to escapeshellarg()
236
        $escapedHost = escapeshellarg($host);
237
        $this->loggerInstance->debug(4, CONFIG['PATHS']['openssl'] . " s_client -connect " . $escapedHost . " -tls1 -CApath " . ROOT . "/config/ca-certs/ $arg 2>&1\n");
238
        $time_start = microtime(true);
239
        $opensslbabble = [];
240
        $result = 999; // likely to become zero by openssl; don't want to initialise to zero, could cover up exec failures
241
        exec(CONFIG['PATHS']['openssl'] . " s_client -connect " . $escapedHost . " -no_ssl2 -no_ssl3 -CApath " . ROOT . "/config/ca-certs/ $arg 2>&1", $opensslbabble, $result);
242
        $time_stop = microtime(true);
243
        $testresults['time_millisec'] = floor(($time_stop - $time_start) * 1000);
244
        $testresults['returncode'] = $result;
245
        return $opensslbabble;
246
    }
247
248
    /**
249
     * This function parses openssl s_client result
250
     * 
251
     * @param string $host          IP:port
252
     * @param array  $opensslbabble openssl command output
253
     * @return int return code
254
     */
255
    private function opensslCAResult($host, $opensslbabble) {
256
        $res = RADIUSTests::RETVAL_OK;
257
        if (preg_match('/connect: Connection refused/', implode($opensslbabble))) {
258
            $this->TLS_CA_checks_result[$host]['status'] = RADIUSTests::RETVAL_CONNECTION_REFUSED;
259
            $res = RADIUSTests::RETVAL_INVALID;
260
        }
261
        if (preg_match('/verify error:num=19/', implode($opensslbabble))) {
262
            $this->TLS_CA_checks_result[$host]['cert_oddity'] = RADIUSTests::CERTPROB_UNKNOWN_CA;
263
            $this->TLS_CA_checks_result[$host]['status'] = RADIUSTests::RETVAL_INVALID;
264
            $res = RADIUSTests::RETVAL_INVALID;
265
        }
266
        if (preg_match('/verify return:1/', implode($opensslbabble))) {
267
            $this->TLS_CA_checks_result[$host]['status'] = RADIUSTests::RETVAL_OK;
268
            $servercertStage1 = implode("\n", $opensslbabble);
269
            $servercert = preg_replace("/.*(-----BEGIN CERTIFICATE-----.*-----END CERTIFICATE-----\n).*/s", "$1", $servercertStage1);
270
            $data = openssl_x509_parse($servercert);
271
            $this->TLS_CA_checks_result[$host]['certdata']['subject'] = $data['name'];
272
            $this->TLS_CA_checks_result[$host]['certdata']['issuer'] = $this->getCertificateIssuer($data);
273
            if (($altname = $this->getCertificatePropertyField($data, 'subjectAltName'))) {
274
                $this->TLS_CA_checks_result[$host]['certdata']['extensions']['subjectaltname'] = $altname;
275
            }
276
            
277
            $oids = $this->propertyCheckPolicy($data);
278
            if (!empty($oids)) {
279
                foreach ($oids as $resultArrayKey => $o) {
280
                    $this->TLS_CA_checks_result[$host]['certdata']['extensions']['policyoid'][] = " $o ($resultArrayKey)";
281
                }
282
            }
283
            if (($crl = $this->getCertificatePropertyField($data, 'crlDistributionPoints'))) {
284
                $this->TLS_CA_checks_result[$host]['certdata']['extensions']['crlDistributionPoint'] = $crl;
285
            }
286
            if (($ocsp = $this->getCertificatePropertyField($data, 'authorityInfoAccess'))) {
287
                $this->TLS_CA_checks_result[$host]['certdata']['extensions']['authorityInfoAccess'] = $ocsp;
288
            }
289
        }
290
        return $res;
291
    }
292
293
    /**
294
     * This function parses openssl s_client result
295
     * 
296
     * @param string $host           IP:port
297
     * @param array  $opensslbabble  openssl command output
298
     * @param array  $testresults    by-reference: pointer to results array we write into
299
     * @param string $type           type of certificate
300
     * @param int    $resultArrayKey results array key
301
     * @return int return code
302
     */
303
    private function opensslClientsResult($host, $opensslbabble, &$testresults, $type = '', $resultArrayKey = 0) {
304
        \core\common\Entity::intoThePotatoes();
305
        $res = RADIUSTests::RETVAL_OK;
306
        $ret = $testresults[$host]['ca'][$type]['certificate'][$resultArrayKey]['returncode'];
307
        $output = implode($opensslbabble);
308
        if ($ret == 0) {
309
            $testresults[$host]['ca'][$type]['certificate'][$resultArrayKey]['connected'] = 1;
310
        } else {
311
            $testresults[$host]['ca'][$type]['certificate'][$resultArrayKey]['connected'] = 0;
312
            if (preg_match('/connect: Connection refused/', implode($opensslbabble))) {
313
                $testresults[$host]['ca'][$type]['certificate'][$resultArrayKey]['returncode'] = RADIUSTests::RETVAL_CONNECTION_REFUSED;
314
                $resComment = _("No TLS connection established: Connection refused");
315
            } elseif (preg_match('/sslv3 alert certificate expired/', $output)) {
316
                $resComment = _("certificate expired");
317
            } elseif (preg_match('/sslv3 alert certificate revoked/', $output)) {
318
                $resComment = _("certificate was revoked");
319
            } elseif (preg_match('/SSL alert number 46/', $output)) {
320
                $resComment = _("bad policy");
321
            } elseif (preg_match('/tlsv1 alert unknown ca/', $output)) {
322
                $resComment = _("unknown authority");
323
                $testresults[$host]['ca'][$type]['certificate'][$resultArrayKey]['reason'] = RADIUSTests::CERTPROB_UNKNOWN_CA;
324
            } else {
325
                $resComment = _("unknown authority or no certificate policy or another problem");
326
            }
327
            $testresults[$host]['ca'][$type]['certificate'][$resultArrayKey]['resultcomment'] = $resComment;
328
        }
329
        \core\common\Entity::outOfThePotatoes();
330
        return $res;
331
    }
332
333
        /**
334
     * This function parses a X.509 cert and returns all certificatePolicies OIDs
335
     * 
336
     * @param array $cert (returned from openssl_x509_parse) 
337
     * @return array of OIDs
338
     */
339
    private function propertyCheckPolicy($cert) {
340
        $oids = [];
341
        if ($cert['extensions']['certificatePolicies']) {
342
            foreach (CONFIG_DIAGNOSTICS['RADIUSTESTS']['TLS-acceptableOIDs'] as $key => $oid) {
343
                if (preg_match("/Policy: $oid/", $cert['extensions']['certificatePolicies'])) {
344
                    $oids[$key] = $oid;
345
                }
346
            }
347
        }
348
        return $oids;
349
    }
350
        /**
351
     * This function parses a X.509 cert and returns the value of $field
352
     * 
353
     * @param array $cert (returned from openssl_x509_parse) 
354
     * @return string value of the issuer field or ''
355
     */
356
    private function getCertificateIssuer($cert) {
357
        $issuer = '';
358
        foreach ($cert['issuer'] as $key => $val) {
359
            if (is_array($val)) {
360
                foreach ($val as $v) {
361
                    $issuer .= "/$key=$v";
362
                }
363
            } else {
364
                $issuer .= "/$key=$val";
365
            }
366
        }
367
        return $issuer;
368
    }
369
    /**
370
     * This function parses a X.509 cert and returns the value of $field
371
     * 
372
     * @param array  $cert  (returned from openssl_x509_parse) 
373
     * @param string $field the field to search for
374
     * @return string value of the extention named $field or ''
375
     */
376
    private function getCertificatePropertyField($cert, $field) {
377
        if ($cert['extensions'][$field]) {
378
            return $cert['extensions'][$field];
379
        }
380
        return '';
381
    }
382
383
}
384