Passed
Push — release_2_1 ( 8ffb06...6b1ac5 )
by Maja
10:32
created

RFC6614Tests::tlsClientSideCheck()   D

Complexity

Conditions 20
Paths 33

Size

Total Lines 55
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 40
dl 0
loc 55
rs 4.1666
c 2
b 1
f 0
cc 20
nc 33
nop 4

How to fix   Long Method    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
2
3
/*
4
 * *****************************************************************************
5
 * Contributions to this work were made on behalf of the GÉANT project, a 
6
 * project that has received funding from the European Union’s Framework 
7
 * Programme 7 under Grant Agreements No. 238875 (GN3) and No. 605243 (GN3plus),
8
 * Horizon 2020 research and innovation programme under Grant Agreements No. 
9
 * 691567 (GN4-1) and No. 731122 (GN4-2).
10
 * On behalf of the aforementioned projects, GEANT Association is the sole owner
11
 * of the copyright in all material which was developed by a member of the GÉANT
12
 * project. GÉANT Vereniging (Association) is registered with the Chamber of 
13
 * Commerce in Amsterdam with registration number 40535155 and operates in the 
14
 * UK as a branch of GÉANT Vereniging.
15
 * 
16
 * Registered office: Hoekenrode 3, 1102BR Amsterdam, The Netherlands. 
17
 * UK branch address: City House, 126-130 Hills Road, Cambridge CB2 1PQ, UK
18
 *
19
 * License: see the web/copyright.inc.php file in the file structure or
20
 *          <base_url>/copyright.php after deploying the software
21
 */
22
23
namespace core\diag;
24
25
use \Exception;
0 ignored issues
show
Bug introduced by
The type \Exception was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
26
27
/**
28
 * Test suite to verify that a given NAI realm has NAPTR records according to
29
 * consortium-agreed criteria
30
 * Can only be used if \config\Diagnostics::RADIUSTESTS is configured.
31
 *
32
 * @author Stefan Winter <[email protected]>
33
 * @author Tomasz Wolniewicz <[email protected]>
34
 * @author Maja Gorecka-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
    /**
44
     * dictionary of translatable texts around the certificates we check
45
     * 
46
     * @var array
47
     */
48
    private $TLS_certkeys = [];
49
50
    /**
51
     * list of IP addresses which are candidates for dynamic discovery targets
52
     * 
53
     * @var array
54
     */
55
    private $candidateIPs;
56
57
    /**
58
     * the hostname which should show up in the certificate when establishing
59
     * a connection to the RADIUS/TLS server (hostname is an intermediary result
60
     * of the RFC7585 DNS resolution algorithm, in SRV response)
61
     * 
62
     * @var string
63
     */
64
    private $expectedName;
65
66
    /**
67
     * associative array holding the server-side cert test results for a given IP (IP is the key)
68
     * 
69
     * @var array
70
     */
71
    public $TLS_CA_checks_result;
72
73
    /**
74
     * associative array holding the client-side cert test results for a given IP (IP is the key)
75
     * 
76
     * @var array
77
     */
78
    public $TLS_clients_checks_result;
79
80
    /**
81
     * which consortium are we testing against?
82
     * 
83
     * @var string
84
     */
85
    private $consortium;
86
    /**
87
     * Sets up the instance for testing of a number of candidate IPs
88
     * 
89
     * @param array  $listOfIPs    candidates to test
90
     * @param string $expectedName expected server name to test against
91
     * @param string $consortium   which consortium to test against
92
     */
93
    public function __construct($listOfIPs, $expectedName, $consortium = "eduroam")
94
    {
95
        parent::__construct();
96
        \core\common\Entity::intoThePotatoes();
97
        $this->TLS_certkeys = [
98
            'eduPKI' => _('eduPKI'),
99
            'NCU' => _('Nicolaus Copernicus University'),
100
            'ACCREDITED' => _('accredited'),
101
            'NONACCREDITED' => _('non-accredited'),
102
            'CORRECT' => _('correct certificate'),
103
            'WRONGPOLICY' => _('certificate with wrong policy OID'),
104
            'EXPIRED' => _('expired certificate'),
105
            'REVOKED' => _('revoked certificate'),
106
            'PASS' => _('pass'),
107
            'FAIL' => _('fail'),
108
            'non-eduPKI-accredited' => _("eduroam-accredited CA (now only for tests)"),
109
        ];
110
        $this->TLS_CA_checks_result = [];
111
        $this->TLS_clients_checks_result = [];
112
113
        $this->candidateIPs = $listOfIPs;
114
        $this->expectedName = $expectedName;
115
116
        switch ($consortium) {
117
            case "eduroam":
118
                // fall-through intended
119
            case "openroaming":
120
                $this->consortium = $consortium;
121
                break;
122
            default:
123
                throw new Exception("Certificate checks against unknown consortium identifier requested!");
124
        }
125
126
        \core\common\Entity::outOfThePotatoes();
127
    }
128
129
    /**
130
     * run all checks on all candidates
131
     * 
132
     * @return void
133
     */
134
    public function allChecks()
135
    {
136
        foreach ($this->candidateIPs as $oneIP) {
137
            $this->cApathCheck($oneIP);
138
            $this->tlsClientSideCheck($oneIP, '', '');
139
        }
140
    }
141
142
    /**
143
     * This function executes openssl s_clientends command to check if a server accepts a CA
144
     * 
145
     * @param string $host IP:port
146
     * @return int returncode
147
     */
148
    public function cApathCheck(string $host)
149
    {
150
        if (!isset($this->TLS_CA_checks_result[$host])) {
151
            $this->TLS_CA_checks_result[$host] = [];
152
        }
153
        $opensslbabble = $this->execOpensslClient($host, '', $this->TLS_CA_checks_result[$host]);
154
        $overallRetval = $this->opensslCAResult($host, $opensslbabble);
155
        if ($overallRetval == AbstractTest::RETVAL_OK) {
156
            $this->checkServerName($host);
157
        } 
158
        return $overallRetval;
159
    }
160
161
    /**
162
     * checks whether the received servername matches the expected server name
163
     * 
164
     * @param string $host IP:port
165
     * @return bool yes or no
166
     */
167
    private function checkServerName($host)
168
    {
169
        // it could match CN or sAN:DNS, we don't care which
170
        if (isset($this->TLS_CA_checks_result[$host]['certdata']['subject'])) {
171
            $this->loggerInstance->debug(4, "Checking expected server name " . $this->expectedName . 
172
                    " against Subject: " . $this->TLS_CA_checks_result[$host]['certdata']['subject']);
173
            // we are checking against accidental misconfig, not attacks, so loosely checking against end of string is appropriate
174
            if (preg_match("/CN=" . $this->expectedName . "/", $this->TLS_CA_checks_result[$host]['certdata']['subject']) === 1) {
175
                return TRUE;
176
            }
177
        }
178
        if (isset($this->TLS_CA_checks_result[$host]['certdata']['extensions']['subjectaltname'])) {
179
            $this->loggerInstance->debug(4, "Checking expected server name " . $this->expectedName . " against sANs: ");
180
            $this->loggerInstance->debug(4, $this->TLS_CA_checks_result[$host]['certdata']['extensions']['subjectaltname']);
181
            $testNames = $this->TLS_CA_checks_result[$host]['certdata']['extensions']['subjectaltname'];
182
            if (!is_array($testNames)) {
183
                $testNames = [$testNames];
184
            }
185
            foreach ($testNames as $oneName) {
186
                if (preg_match("/" . $this->expectedName . "/", $oneName) === 1) {
187
                    return TRUE;
188
                }
189
            }
190
        }
191
        $this->loggerInstance->debug(3, "Tried to check expected server name " . $this->expectedName . " but neither CN nor sANs matched.");
192
193
        $this->TLS_CA_checks_result[$host]['cert_oddity'] = RADIUSTests::CERTPROB_DYN_SERVER_NAME_MISMATCH;
194
        return FALSE;
195
    }
196
197
    /**
198
     * This function executes openssl s_client command to check if a server accepts a client certificate
199
     * 
200
     * @param string $host       IP:port
201
     * @return int returncode
202
     */
203
    public function tlsClientSideCheck(string $host, string $ename, string $realm, array $protocols = [])
0 ignored issues
show
Unused Code introduced by
The parameter $ename is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

203
    public function tlsClientSideCheck(string $host, /** @scrutinizer ignore-unused */ string $ename, string $realm, array $protocols = [])

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $realm is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

203
    public function tlsClientSideCheck(string $host, string $ename, /** @scrutinizer ignore-unused */ string $realm, array $protocols = [])

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
204
    {
205
        $res = RADIUSTests::RETVAL_OK;
206
        if (!is_array(\config\Diagnostics::RADIUSTESTS['TLS-clientcerts']) || count(\config\Diagnostics::RADIUSTESTS['TLS-clientcerts']) == 0) {
207
            return RADIUSTests::RETVAL_SKIPPED;
208
        }
209
        if (preg_match("/\[/", $host)) {
210
            return RADIUSTests::RETVAL_INVALID;
211
        }
212
        foreach (\config\Diagnostics::RADIUSTESTS['TLS-clientcerts'] as $type => $tlsclient) {
213
            $this->TLS_clients_checks_result[$host]['ca'][$type]['clientcertinfo']['from'] = $type;
214
            $this->TLS_clients_checks_result[$host]['ca'][$type]['clientcertinfo']['status'] = $tlsclient['status'];
215
            $this->TLS_clients_checks_result[$host]['ca'][$type]['clientcertinfo']['message'] = $this->TLS_certkeys[$tlsclient['status']];
216
            $this->TLS_clients_checks_result[$host]['ca'][$type]['clientcertinfo']['issuer'] = $tlsclient['issuerCA'];
217
            foreach ($tlsclient['certificates'] as $k => $cert) {
218
                $this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['status'] = $cert['status'];
219
                $this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['message'] = $this->TLS_certkeys[$cert['status']];
220
                $this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['expected'] = $cert['expected'];
221
                $add = ' -cert ' . ROOT . '/config/cli-certs/' . $cert['public'] . ' -key ' . ROOT . '/config/cli-certs/' . $cert['private'];
222
                if (!file_exists(ROOT . '/config/cli-certs/' . $cert['public']) ||!file_exists(ROOT . 
223
                        '/config/cli-certs/' . $cert['private'])) {
224
                    $this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['finalerror'] = 2;
225
                    continue;
226
                }
227
                if (!isset($this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k])) {
228
                    $this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k] = [];
229
                }
230
                $prot = "-no_ssl3";
231
                if (in_array("TLS1.3", $protocols) && count($protocols) > 1) {
232
                    $prot .= ' -no_tls1_3';
233
                }
234
                $add .= ' ' . $prot;
235
                $opensslbabble = $this->execOpensslClient($host, $add, $this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]);
236
                $res = $this->opensslClientsResult($host, $opensslbabble, $this->TLS_clients_checks_result, $type, $k);
237
                if ($cert['expected'] == 'PASS') {
238
                    if (!$this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['connected']) {
239
                        if (($tlsclient['status'] == 'ACCREDITED') && ($cert['status'] == 'CORRECT')) {
240
                            $this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['returncode'] = RADIUSTests::CERTPROB_NOT_ACCEPTED;
241
                            $this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['finalerror'] = 1;
242
                            break;
243
                        }
244
                    }
245
                } else {
246
                    if ($this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['connected']) {
247
                        $this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['returncode'] = RADIUSTests::CERTPROB_WRONGLY_ACCEPTED;
248
                    }
249
250
                    if (isset($this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['reason']) && ($this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['reason'] == RADIUSTests::CERTPROB_UNKNOWN_CA) && ($tlsclient['status'] == 'ACCREDITED') && ($cert['status'] == 'CORRECT')) {
251
                        $this->TLS_clients_checks_result[$host]['ca'][$type]['certificate'][$k]['finalerror'] = 1;
252
                        break;
253
                    }
254
                }
255
            }
256
        }
257
        return $res;
258
    }
259
260
    /**
261
     * This function executes openssl s_client command
262
     * 
263
     * @param string $host        IP address
264
     * @param string $consortium  which consortium to check against
265
     * @param string $arg         arguments to add to the openssl command 
266
     * @param array  $testresults by-reference: the testresults array we are writing into
267
     * @return array result of openssl s_client ...
268
     */
269
    private function execOpensslClient($host, $arg, &$testresults)
270
    {
271
// we got the IP address either from DNS (guaranteeing well-formedness)
272
// or from filter_var'ed user input. So it is always safe as an argument
273
// but code analysers want this more explicit, so here is this extra
274
// call to escapeshellarg()
275
        $escapedHost = escapeshellarg($host);
276
        $this->loggerInstance->debug(4, \config\Master::PATHS['openssl'] . " s_client -connect " . $escapedHost . " -CApath " . ROOT . "/config/ca-certs/$this->consortium/ $arg 2>&1\n");
277
        $time_start = microtime(true);
278
        $opensslbabble = [];
279
        $result = 999; // likely to become zero by openssl; don't want to initialise to zero, could cover up exec failures
280
        exec(\config\Master::PATHS['openssl'] . " s_client -connect " . $escapedHost . " -CApath " . ROOT . "/config/ca-certs/$this->consortium/ $arg 2>&1", $opensslbabble, $result);
281
        $time_stop = microtime(true);
282
        $testresults['time_millisec'] = floor(($time_stop - $time_start) * 1000);
283
        $testresults['returncode'] = $result;
284
        return $opensslbabble;
285
    }
286
287
    /**
288
     * This function parses openssl s_client result
289
     * 
290
     * @param string $host          IP:port
291
     * @param array  $opensslbabble openssl command output
292
     * @return int return code
293
     */
294
    private function opensslCAResult($host, $opensslbabble)
295
    {
296
        if (preg_match('/connect: Connection refused/', implode($opensslbabble))) {
297
            $this->TLS_CA_checks_result[$host]['status'] = RADIUSTests::RETVAL_CONNECTION_REFUSED;
298
            return RADIUSTests::RETVAL_INVALID;
299
        }
300
        if (preg_match('/no peer certificate available/', implode($opensslbabble))) {
301
            $this->TLS_CA_checks_result[$host]['status'] = RADIUSTests::RETVAL_SERVER_UNFINISHED_COMM;
302
            return RADIUSTests::RETVAL_INVALID;
303
        }
304
        /*if (preg_match('/verify error:num=19/', implode($opensslbabble))) {
305
            $this->TLS_CA_checks_result[$host]['cert_oddity'] = RADIUSTests::CERTPROB_UNKNOWN_CA;
306
            $this->TLS_CA_checks_result[$host]['status'] = RADIUSTests::RETVAL_INVALID;
307
            return RADIUSTests::RETVAL_INVALID;
308
        }*/
309
        if (preg_match('/Verification error: self-signed certificate in certificate chain/', implode($opensslbabble))) {
310
            $this->TLS_CA_checks_result[$host]['cert_oddity'] = RADIUSTests::CERTPROB_UNKNOWN_CA;
311
            $this->TLS_CA_checks_result[$host]['status'] = RADIUSTests::RETVAL_INVALID;
312
            return RADIUSTests::RETVAL_INVALID;
313
        }
314
        if (preg_match('/Cipher is (NONE)/', implode($opensslbabble))) {
315
            $this->TLS_CA_checks_result[$host]['status'] = RADIUSTests::RETVAL_SERVER_UNFINISHED_COMM;
316
            return RADIUSTests::RETVAL_INVALID;
317
        }
318
        /*if (preg_match('/verify return:1/', implode($opensslbabble))) {*/
319
        if (preg_match('/Verification: OK/', implode($opensslbabble))) {
320
            $this->TLS_CA_checks_result[$host]['status'] = RADIUSTests::RETVAL_OK;
321
            $servercertStage1 = implode("\n", $opensslbabble);
322
            $servercert = preg_replace("/.*(-----BEGIN CERTIFICATE-----.*-----END CERTIFICATE-----\n).*/s", "$1", $servercertStage1);
323
            $data = openssl_x509_parse($servercert);
324
            $this->TLS_CA_checks_result[$host]['certdata']['subject'] = $data['name'];
325
            $this->TLS_CA_checks_result[$host]['certdata']['validTo'] = $data['validTo'];
326
            $this->TLS_CA_checks_result[$host]['certdata']['issuer'] = $this->getCertificateIssuer($data);
327
            if (($altname = $this->getCertificatePropertyField($data, 'subjectAltName'))) {
328
                $this->TLS_CA_checks_result[$host]['certdata']['extensions']['subjectaltname'] = $altname;
329
            }
330
331
            $oids = $this->propertyCheckPolicy($data);
332
            if (!empty($oids)) {
333
                foreach ($oids as $resultArrayKey => $o) {
334
                    $this->TLS_CA_checks_result[$host]['certdata']['extensions']['policyoid'][] = " $o ($resultArrayKey)";
335
                }
336
            }
337
            if (($crl = $this->getCertificatePropertyField($data, 'crlDistributionPoints'))) {
338
                $this->TLS_CA_checks_result[$host]['certdata']['extensions']['crlDistributionPoint'] = $crl;
339
            }
340
            if (($ocsp = $this->getCertificatePropertyField($data, 'authorityInfoAccess'))) {
341
                $this->TLS_CA_checks_result[$host]['certdata']['extensions']['authorityInfoAccess'] = $ocsp;
342
            }
343
            return RADIUSTests::RETVAL_OK;
344
        }
345
        // we should have been caught somewhere along the way. If we got here,
346
        // something seriously unexpected happened. Let's talk about it.
347
        return RADIUSTests::RETVAL_INVALID;
348
    }
349
350
    /**
351
     * This function parses openssl s_client result
352
     * 
353
     * @param string $host           IP:port
354
     * @param array  $opensslbabble  openssl command output
355
     * @param array  $testresults    by-reference: pointer to results array we write into
356
     * @param string $type           type of certificate
357
     * @param int    $resultArrayKey results array key
358
     * @return int return code
359
     */
360
    private function opensslClientsResult($host, $opensslbabble, &$testresults, $type = '', $resultArrayKey = 0)
361
    {
362
        \core\common\Entity::intoThePotatoes();
363
        $res = RADIUSTests::RETVAL_OK;
364
        $ret = $testresults[$host]['ca'][$type]['certificate'][$resultArrayKey]['returncode'];
365
        $jsondir = dirname(dirname(dirname(__FILE__)))."/var/json_cache/";
0 ignored issues
show
Unused Code introduced by
The assignment to $jsondir is dead and can be removed.
Loading history...
366
        $output = implode($opensslbabble);
367
        if ($ret == 0) {
368
            $testresults[$host]['ca'][$type]['certificate'][$resultArrayKey]['connected'] = 1;
369
        } else {
370
            $testresults[$host]['ca'][$type]['certificate'][$resultArrayKey]['connected'] = 0;
371
            if (preg_match('/connect: Connection refused/', implode($opensslbabble))) {
372
                $testresults[$host]['ca'][$type]['certificate'][$resultArrayKey]['returncode'] = RADIUSTests::RETVAL_CONNECTION_REFUSED;
373
                $resComment = _("No TLS connection established: Connection refused");
374
            } elseif (preg_match('/sslv3 alert certificate expired/', $output)) {
375
                $resComment = _("certificate expired");
376
            } elseif (preg_match('/sslv3 alert certificate revoked/', $output)) {
377
                $resComment = _("certificate was revoked");
378
            } elseif (preg_match('/SSL alert number 46/', $output) && 
379
                      preg_match('/sslv3 alert certificate unknown/', $output)) {
380
                $resComment = _("bad policy");
381
            } elseif (preg_match('/tlsv1 alert unknown ca/', $output)) {
382
                $resComment = _("unknown authority");
383
                $testresults[$host]['ca'][$type]['certificate'][$resultArrayKey]['reason'] = RADIUSTests::CERTPROB_UNKNOWN_CA;
384
            } else {
385
                $resComment = _("unknown authority or no certificate policy or another problem");
386
            }
387
            $testresults[$host]['ca'][$type]['certificate'][$resultArrayKey]['resultcomment'] = $resComment;
388
        }
389
        \core\common\Entity::outOfThePotatoes();
390
        return $res;
391
    }
392
393
    /**
394
     * This function parses a X.509 cert and returns all certificatePolicies OIDs
395
     * 
396
     * @param array $cert (returned from openssl_x509_parse) 
397
     * @return array of OIDs
398
     */
399
    private function propertyCheckPolicy($cert)
400
    {
401
        $oids = [];
402
        if ($cert['extensions']['certificatePolicies']) {
403
            foreach (\config\Diagnostics::RADIUSTESTS['TLS-acceptableOIDs'] as $key => $oid) {
404
                if (preg_match("/Policy: $oid/", $cert['extensions']['certificatePolicies'])) {
405
                    $oids[$key] = $oid;
406
                }
407
            }
408
        }
409
        return $oids;
410
    }
411
412
    /**
413
     * This function parses a X.509 cert and returns the value of $field
414
     * 
415
     * @param array $cert (returned from openssl_x509_parse) 
416
     * @return string value of the issuer field or ''
417
     */
418
    private function getCertificateIssuer($cert)
419
    {
420
        $issuer = '';
421
        foreach ($cert['issuer'] as $key => $val) {
422
            if (is_array($val)) {
423
                foreach ($val as $v) {
424
                    $issuer .= "/$key=$v";
425
                }
426
            } else {
427
                $issuer .= "/$key=$val";
428
            }
429
        }
430
        return $issuer;
431
    }
432
433
    /**
434
     * This function parses a X.509 cert and returns the value of $field
435
     * 
436
     * @param array  $cert  (returned from openssl_x509_parse) 
437
     * @param string $field the field to search for
438
     * @return string value of the extension named $field or ''
439
     */
440
    private function getCertificatePropertyField($cert, $field)
441
    {
442
        if ($cert['extensions'][$field]) {
443
            return $cert['extensions'][$field];
444
        }
445
        return '';
446
    }
447
}