Passed
Push — master ( 3e8aca...8d1237 )
by Stefan
05:20
created

RADIUSTests::extractIncomingCertsfromEAP()   D

Complexity

Conditions 18
Paths 218

Size

Total Lines 94
Code Lines 53

Duplication

Lines 6
Ratio 6.38 %

Importance

Changes 0
Metric Value
cc 18
eloc 53
nc 218
nop 2
dl 6
loc 94
rs 4.2063
c 0
b 0
f 0

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
 * Copyright 2011-2017 DANTE Ltd. and GÉANT on behalf of the GN3, GN3+, GN4-1 
6
 * and GN4-2 consortia
7
 *
8
 * License: see the web/copyright.php file in the file structure
9
 * ******************************************************************************
10
 */
11
12
/**
13
 * This file contains code for testing EAP servers
14
 *
15
 * @author Stefan Winter <[email protected]>
16
 * @author Tomasz Wolniewicz <[email protected]>
17
 * @author Maja Gorecka-Wolniewicz <[email protected]>
18
 *
19
 * @package Developer
20
 * 
21
 */
22
23
namespace core\diag;
24
25
use \Exception;
26
27
require_once(dirname(dirname(__DIR__)) . "/config/_config.php");
28
29
/**
30
 * Test suite to verify that an EAP setup is actually working as advertised in
31
 * the real world. 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 RADIUSTests extends AbstractTest {
41
42
    /**
43
     * The variables below maintain state of the result of previous checks.
44
     * 
45
     */
46
    private $UDP_reachability_executed;
47
    private $errorlist;
48
49
    /**
50
     * This private variable contains the realm to be checked. Is filled in the
51
     * class constructor.
52
     * 
53
     * @var string
54
     */
55
    private $realm;
56
    private $outerUsernameForChecks;
57
    private $expectedCABundle;
58
    private $expectedServerNames;
59
60
    /**
61
     * the list of EAP types which the IdP allegedly supports.
62
     * 
63
     * @var array
64
     */
65
    private $supportedEapTypes;
66
    private $opMode;
67
    public $UDP_reachability_result;
68
69
    const RADIUS_TEST_OPERATION_MODE_SHALLOW = 1;
70
    const RADIUS_TEST_OPERATION_MODE_THOROUGH = 2;
71
72
    /**
73
     * Constructor for the EAPTests class. The single mandatory parameter is the
74
     * realm for which the tests are to be carried out.
75
     * 
76
     * @param string $realm
77
     * @param string $outerUsernameForChecks
78
     * @param array $supportedEapTypes (array of integer representations of EAP types)
79
     * @param array $expectedServerNames (array of strings)
80
     * @param array $expectedCABundle (array of PEM blocks)
81
     */
82
    public function __construct($realm, $outerUsernameForChecks, $supportedEapTypes = [], $expectedServerNames = [], $expectedCABundle = []) {
83
        parent::__construct();
84
        $oldlocale = $this->languageInstance->setTextDomain('diagnostics');
85
86
        $this->realm = $realm;
87
        $this->outerUsernameForChecks = $outerUsernameForChecks;
88
        $this->expectedCABundle = $expectedCABundle;
89
        $this->expectedServerNames = $expectedServerNames;
90
        $this->supportedEapTypes = $supportedEapTypes;
91
92
        $this->opMode = self::RADIUS_TEST_OPERATION_MODE_SHALLOW;
93
94
        $caNeeded = FALSE;
95
        $serverNeeded = FALSE;
96
        foreach ($supportedEapTypes as $oneEapType) {
97
            if ($oneEapType->needsServerCACert()) {
98
                $caNeeded = TRUE;
99
            }
100
            if ($oneEapType->needsServerName()) {
101
                $serverNeeded = TRUE;
102
            }
103
        }
104
105
        if ($caNeeded) {
106
            // we need to have info about at least one CA cert and server names
107
            if (count($this->expectedCABundle) == 0) {
108
                Throw new Exception("Thorough checks for an EAP type needing CAs were requested, but the required parameters were not given.");
109
            } else {
110
                $this->opMode = self::RADIUS_TEST_OPERATION_MODE_THOROUGH;
111
            }
112
        }
113
114
        if ($serverNeeded) {
115
            if (count($this->expectedServerNames) == 0) {
116
                Throw new Exception("Thorough checks for an EAP type needing server names were requested, but the required parameter was not given.");
117
            } else {
118
                $this->opMode = self::RADIUS_TEST_OPERATION_MODE_THOROUGH;
119
            }
120
        }
121
122
        $this->loggerInstance->debug(4, "RADIUSTests is in opMode " . $this->opMode . ", parameters were: $realm, $outerUsernameForChecks, " . print_r($supportedEapTypes, true));
123
        $this->loggerInstance->debug(4, print_r($expectedServerNames, true));
124
        $this->loggerInstance->debug(4, print_r($expectedCABundle, true));
125
126
        $this->UDP_reachability_result = [];
127
        $this->errorlist = [];
128
        $this->languageInstance->setTextDomain($oldlocale);
129
    }
130
131
    private function printDN($distinguishedName) {
132
        $out = '';
133
        foreach (array_reverse($distinguishedName) as $nameType => $nameValue) { // to give an example: "CN" => "some.host.example" 
134
            if (!is_array($nameValue)) { // single-valued: just a string
135
                $nameValue = ["$nameValue"]; // convert it to a multi-value attrib with just one value :-) for unified processing later on
136
            }
137
            foreach ($nameValue as $oneValue) {
138
                if ($out) {
139
                    $out .= ',';
140
                }
141
                $out .= "$nameType=$oneValue";
142
            }
143
        }
144
        return($out);
145
    }
146
147
    private function printTm($time) {
148
        return(gmdate(\DateTime::COOKIE, $time));
149
    }
150
151
    /**
152
     * This function parses a X.509 server cert and checks if it finds client device incompatibilities
153
     * 
154
     * @param array $servercert the properties of the certificate as returned by processCertificate(), 
155
     *    $servercert is modified, if CRL is defied, it is downloaded and added to the array
156
     *    incoming_server_names, sAN_DNS and CN array values are also defined
157
     * @return array of oddities; the array is empty if everything is fine
158
     */
159
    private function propertyCheckServercert(&$servercert) {
160
        $this->loggerInstance->debug(5, "SERVER CERT IS: " . print_r($servercert, TRUE));
161
// we share the same checks as for CAs when it comes to signature algorithm and basicconstraints
162
// so call that function and memorise the outcome
163
        $returnarray = $this->propertyCheckIntermediate($servercert, TRUE);
164
        $sANdns = [];
165
        if (!isset($servercert['full_details']['extensions'])) {
166
            $returnarray[] = RADIUSTests::CERTPROB_NO_TLS_WEBSERVER_OID;
167
            $returnarray[] = RADIUSTests::CERTPROB_NO_CDP_HTTP;
168
        } else { // Extensions are present...
169
            if (!isset($servercert['full_details']['extensions']['extendedKeyUsage']) || !preg_match("/TLS Web Server Authentication/", $servercert['full_details']['extensions']['extendedKeyUsage'])) {
170
                $returnarray[] = RADIUSTests::CERTPROB_NO_TLS_WEBSERVER_OID;
171
            }
172
            if (isset($servercert['full_details']['extensions']['subjectAltName'])) {
173
                $sANlist = explode(", ", $servercert['full_details']['extensions']['subjectAltName']);
174
                foreach ($sANlist as $subjectAltName) {
175
                    if (preg_match("/^DNS:/", $subjectAltName)) {
176
                        $sANdns[] = substr($subjectAltName, 4);
177
                    }
178
                }
179
            }
180
        }
181
182
        // often, there is only one name, so we store it in an array of one member
183
        $commonName = [$servercert['full_details']['subject']['CN']];
184
        // if we got an array of names instead, then that is already an array, so override
185
        if (isset($servercert['full_details']['subject']['CN']) && is_array($servercert['full_details']['subject']['CN'])) {
186
            $commonName = $servercert['full_details']['subject']['CN'];
187
            $returnarray[] = RADIUSTests::CERTPROB_MULTIPLE_CN;
188
        }
189
190
        $allnames = array_unique(array_merge($commonName, $sANdns));
191
// check for wildcards
192
// check for real hostnames, and whether there is a wildcard in a name
193
        foreach ($allnames as $onename) {
194
            if (preg_match("/\*/", $onename)) {
195
                $returnarray[] = RADIUSTests::CERTPROB_WILDCARD_IN_NAME;
196
                continue; // otherwise we'd ALSO complain that it's not a real hostname
197
            }
198
            if ($onename != "" && filter_var("foo@" . idn_to_ascii($onename), FILTER_VALIDATE_EMAIL) === FALSE) {
199
                $returnarray[] = RADIUSTests::CERTPROB_NOT_A_HOSTNAME;
200
            }
201
        }
202
        $servercert['incoming_server_names'] = $allnames;
203
        $servercert['sAN_DNS'] = $sANdns;
204
        $servercert['CN'] = $commonName;
205
        return $returnarray;
206
    }
207
208
    /**
209
     * This function parses a X.509 intermediate CA cert and checks if it finds client device incompatibilities
210
     * 
211
     * @param array $intermediateCa the properties of the certificate as returned by processCertificate()
212
     * @param boolean complain_about_cdp_existence: for intermediates, not having a CDP is less of an issue than for servers. Set the REMARK (..._INTERMEDIATE) flag if not complaining; and _SERVER if so
213
     * @return array of oddities; the array is empty if everything is fine
214
     */
215
    private function propertyCheckIntermediate(&$intermediateCa, $serverCert = FALSE) {
216
        $returnarray = [];
217
        if (preg_match("/md5/i", $intermediateCa['full_details']['signatureTypeSN'])) {
218
            $returnarray[] = RADIUSTests::CERTPROB_MD5_SIGNATURE;
219
        }
220
        if (preg_match("/sha1/i", $intermediateCa['full_details']['signatureTypeSN'])) {
221
            $returnarray[] = RADIUSTests::CERTPROB_SHA1_SIGNATURE;
222
        }
223
        $this->loggerInstance->debug(4, "CERT IS: " . print_r($intermediateCa, TRUE));
224
        if ($intermediateCa['basicconstraints_set'] == 0) {
225
            $returnarray[] = RADIUSTests::CERTPROB_NO_BASICCONSTRAINTS;
226
        }
227
        if ($intermediateCa['full_details']['public_key_length'] < 1024) {
228
            $returnarray[] = RADIUSTests::CERTPROB_LOW_KEY_LENGTH;
229
        }
230
        $validFrom = $intermediateCa['full_details']['validFrom_time_t'];
231
        $now = time();
232
        $validTo = $intermediateCa['full_details']['validTo_time_t'];
233
        if ($validFrom > $now || $validTo < $now) {
234
            $returnarray[] = RADIUSTests::CERTPROB_OUTSIDE_VALIDITY_PERIOD;
235
        }
236
        $addCertCrlResult = $this->addCrltoCert($intermediateCa);
237
        if ($addCertCrlResult !== 0 && $serverCert) {
238
            $returnarray[] = $addCertCrlResult;
239
        }
240
241
        return $returnarray;
242
    }
243
244
    /**
245
     * This function returns an array of errors which were encountered in all the tests.
246
     * 
247
     * @return array all the errors
248
     */
249
    public function listerrors() {
250
        return $this->errorlist;
251
    }
252
253
    /**
254
     * This function performs actual authentication checks with MADE-UP credentials.
255
     * Its purpose is to check if a RADIUS server is reachable and speaks EAP.
256
     * The function fills array RADIUSTests::UDP_reachability_result[$probeindex] with all check detail
257
     * in case more than the return code is needed/wanted by the caller
258
     * 
259
     * @param int $probeindex refers to the specific UDP-host in the config that should be checked
260
     * @param boolean $opnameCheck should we check choking on Operator-Name?
261
     * @param boolean $frag should we cause UDP fragmentation? (Warning: makes use of Operator-Name!)
262
     * @return int returncode
263
     */
264
    public function udpReachability($probeindex, $opnameCheck = TRUE, $frag = TRUE) {
265
        // for EAP-TLS to be a viable option, we need to pass a random client cert to make eapol_test happy
266
        // the following PEM data is one of the SENSE EAPLab client certs (not secret at all)
267
        $clientcert = file_get_contents(dirname(__FILE__) . "/clientcert.p12");
268
        if ($clientcert === FALSE) {
269
            throw new Exception("A dummy client cert is part of the source distribution, but could not be loaded!");
270
        }
271
        // if we are in thorough opMode, use our knowledge for a more clever check
272
        // otherwise guess
273
        if ($this->opMode == self::RADIUS_TEST_OPERATION_MODE_THOROUGH) {
274
            return $this->udpLogin($probeindex, $this->supportedEapTypes[0]->getArrayRep(), $this->outerUsernameForChecks, 'eaplab', $opnameCheck, $frag, $clientcert);
275
        }
276
        return $this->udpLogin($probeindex, \core\common\EAP::EAPTYPE_ANY, "cat-connectivity-test@" . $this->realm, 'eaplab', $opnameCheck, $frag, $clientcert);
277
    }
278
279
    /**
280
     * There is a CRL Distribution Point URL in the certificate. So download the
281
     * CRL and attach it to the cert structure so that we can later find out if
282
     * the cert was revoked
283
     * @param array $cert by-reference: the cert data we are writing into
284
     * @return int result code whether we were successful in retrieving the CRL
285
     */
286
    private function addCrltoCert(&$cert) {
287
        $crlUrl = [];
288
        $returnresult = 0;
289
        if (!isset($cert['full_details']['extensions']['crlDistributionPoints'])) {
290
            $returnresult = RADIUSTests::CERTPROB_NO_CDP;
291
        } else if (!preg_match("/^.*URI\:(http)(.*)$/", str_replace(["\r", "\n"], ' ', $cert['full_details']['extensions']['crlDistributionPoints']), $crlUrl)) {
292
            $returnresult = RADIUSTests::CERTPROB_NO_CDP_HTTP;
293
        } else { // first and second sub-match is the full URL... check it
294
            $crlcontent = \core\common\OutsideComm::downloadFile(trim($crlUrl[1] . $crlUrl[2]));
295
            if ($crlcontent === FALSE) {
296
                $returnresult = RADIUSTests::CERTPROB_NO_CRL_AT_CDP_URL;
297
            }
298
            $crlBegin = strpos($crlcontent, "-----BEGIN X509 CRL-----");
299
            if ($crlBegin === FALSE) {
300
                $pem = chunk_split(base64_encode($crlcontent), 64, "\n");
301
                $crlcontent = "-----BEGIN X509 CRL-----\n" . $pem . "-----END X509 CRL-----\n";
302
            }
303
            $cert['CRL'] = [];
304
            $cert['CRL'][] = $crlcontent;
305
        }
306
        return $returnresult;
307
    }
308
309
    /**
310
     * We don't want to write passwords of the live login test to our logs. Filter them out
311
     * @param string $stringToRedact what should be redacted
312
     * @param array $inputarray array of strings (outputs of eapol_test command)
313
     * @return string[] the output of eapol_test with the password redacted
314
     */
315
    private function redact($stringToRedact, $inputarray) {
316
        $temparray = preg_replace("/^.*$stringToRedact.*$/", "LINE CONTAINING PASSWORD REDACTED", $inputarray);
317
        $hex = bin2hex($stringToRedact);
318
        $spaced = "";
319
        $origLength = strlen($hex);
320
        for ($i = 1; $i < $origLength; $i++) {
321
            if ($i % 2 == 1 && $i != strlen($hex)) {
322
                $spaced .= $hex[$i] . " ";
323
            } else {
324
                $spaced .= $hex[$i];
325
            }
326
        }
327
        return preg_replace("/$spaced/", " HEX ENCODED PASSWORD REDACTED ", $temparray);
328
    }
329
330
    /**
331
     * Filters eapol_test output and finds out the packet codes out of which the conversation was comprised of
332
     * 
333
     * @param array $inputarray array of strings (outputs of eapol_test command)
334
     * @return array the packet codes which were exchanged, in sequence
335
     */
336
    private function filterPackettype($inputarray) {
337
        $retarray = [];
338
        foreach ($inputarray as $line) {
339
            if (preg_match("/RADIUS message:/", $line)) {
340
                $linecomponents = explode(" ", $line);
341
                $packettypeExploded = explode("=", $linecomponents[2]);
342
                $packettype = $packettypeExploded[1];
343
                $retarray[] = $packettype;
344
            }
345
        }
346
        return $retarray;
347
    }
348
349
    const LINEPARSE_CHECK_REJECTIGNORE = 1;
350
    const LINEPARSE_CHECK_691 = 2;
351
    const LINEPARSE_EAPACK = 3;
352
353
    /**
354
     * this function checks for various special conditions which can be found 
355
     * only by parsing eapol_test output line by line. Checks currently 
356
     * implemented are:
357
     * * if the ETLRs sent back an Access-Reject because there appeared to
358
     *   be a timeout further downstream
359
     * * did the server send an MSCHAP Error 691 - Retry Allowed in a Challenge
360
     *   instead of an outright reject?
361
     * * was an EAP method ever acknowledged by both sides during the EAP
362
     *   conversation
363
     * 
364
     * @param array $inputarray array of strings (outputs of eapol_test command)
365
     * @param int $desiredCheck which test should be run (see constants above)
366
     * @return boolean returns TRUE if ETLR Reject logic was detected; FALSE if not
367
     */
368
    private function checkLineparse($inputarray, $desiredCheck) {
369
        foreach ($inputarray as $lineid => $line) {
370
            switch ($desiredCheck) {
371 View Code Duplication
                case self::LINEPARSE_CHECK_REJECTIGNORE:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
372
                    if (preg_match("/Attribute 18 (Reply-Message)/", $line) && preg_match("/Reject instead of Ignore at eduroam.org/", $inputarray[$lineid + 1])) {
373
                        return TRUE;
374
                    }
375
                    break;
376 View Code Duplication
                case self::LINEPARSE_CHECK_691:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
377
                    if (preg_match("/MSCHAPV2: error 691/", $line) && preg_match("/MSCHAPV2: retry is allowed/", $inputarray[$lineid + 1])) {
378
                        return TRUE;
379
                    }
380
                    break;
381
                case self::LINEPARSE_EAPACK:
382
                    if (preg_match("/CTRL-EVENT-EAP-PROPOSED-METHOD/", $line) && !preg_match("/NAK$/", $line)) {
383
                        return TRUE;
384
                    }
385
                    break;
386
                default:
387
                    throw new Exception("This lineparse test does not exist.");
388
            }
389
        }
390
        return FALSE;
391
    }
392
393
    /**
394
     * 
395
     * @param array $eaptype array representation of the EAP type
396
     * @param string $inner inner username
397
     * @param string $outer outer username
398
     * @param string $password the password
399
     * @return string[] [0] is the actual config for wpa_supplicant, [1] is a redacted version for logs
400
     */
401
    private function wpaSupplicantConfig(array $eaptype, string $inner, string $outer, string $password) {
402
        $eapText = \core\common\EAP::eapDisplayName($eaptype);
403
        $config = '
404
network={
405
  ssid="' . CONFIG['APPEARANCE']['productname'] . ' testing"
406
  key_mgmt=WPA-EAP
407
  proto=WPA2
408
  pairwise=CCMP
409
  group=CCMP
410
  ';
411
// phase 1
412
        $config .= 'eap=' . $eapText['OUTER'] . "\n";
413
        $logConfig = $config;
414
// phase 2 if applicable; all inner methods have passwords
415
        if (isset($eapText['INNER']) && $eapText['INNER'] != "") {
416
            $config .= '  phase2="auth=' . $eapText['INNER'] . "\"\n";
417
            $logConfig .= '  phase2="auth=' . $eapText['INNER'] . "\"\n";
418
        }
419
// all methods set a password, except EAP-TLS
420
        if ($eaptype != \core\common\EAP::EAPTYPE_TLS) {
421
            $config .= "  password=\"$password\"\n";
422
            $logConfig .= "  password=\"not logged for security reasons\"\n";
423
        }
424
// for methods with client certs, add a client cert config block
425
        if ($eaptype == \core\common\EAP::EAPTYPE_TLS || $eaptype == \core\common\EAP::EAPTYPE_ANY) {
426
            $config .= "  private_key=\"./client.p12\"\n";
427
            $logConfig .= "  private_key=\"./client.p12\"\n";
428
            $config .= "  private_key_passwd=\"$password\"\n";
429
            $logConfig .= "  private_key_passwd=\"not logged for security reasons\"\n";
430
        }
431
432
// inner identity
433
        $config .= '  identity="' . $inner . "\"\n";
434
        $logConfig .= '  identity="' . $inner . "\"\n";
435
// outer identity, may be equal
436
        $config .= '  anonymous_identity="' . $outer . "\"\n";
437
        $logConfig .= '  anonymous_identity="' . $outer . "\"\n";
438
// done
439
        $config .= "}";
440
        $logConfig .= "}";
441
442
        return [$config, $logConfig];
443
    }
444
445
    private function packetCountEvaluation(&$testresults, $packetcount) {
446
        $reqs = $packetcount[1] ?? 0;
447
        $accepts = $packetcount[2] ?? 0;
448
        $rejects = $packetcount[3] ?? 0;
449
        $challenges = $packetcount[11] ?? 0;
450
        $testresults['packetflow_sane'] = TRUE;
451
        if ($reqs - $accepts - $rejects - $challenges != 0 || $accepts > 1 || $rejects > 1) {
452
            $testresults['packetflow_sane'] = FALSE;
453
        }
454
455
        $this->loggerInstance->debug(5, "XYZ: Counting req, acc, rej, chal: $reqs, $accepts, $rejects, $challenges");
456
457
// calculate the main return values that this test yielded
458
459
        $finalretval = RADIUSTests::RETVAL_INVALID;
460
        if ($accepts + $rejects == 0) { // no final response. hm.
461
            if ($challenges > 0) { // but there was an Access-Challenge
462
                $finalretval = RADIUSTests::RETVAL_SERVER_UNFINISHED_COMM;
463
            } else {
464
                $finalretval = RADIUSTests::RETVAL_NO_RESPONSE;
465
            }
466
        } else // either an accept or a reject
467
// rejection without EAP is fishy
468
        if ($rejects > 0) {
469
            if ($challenges == 0) {
470
                $finalretval = RADIUSTests::RETVAL_IMMEDIATE_REJECT;
471
            } else { // i.e. if rejected with challenges
472
                $finalretval = RADIUSTests::RETVAL_CONVERSATION_REJECT;
473
            }
474
        } else if ($accepts > 0) {
475
            $finalretval = RADIUSTests::RETVAL_OK;
476
        }
477
478
        return $finalretval;
479
    }
480
481
    /**
482
     * generate an eapol_test command-line config for the fixed config filename 
483
     * ./udp_login_test.conf
484
     * @param int $probeindex number of the probe to check against
485
     * @param boolean $opName include Operator-Name in request?
486
     * @param boolean $frag make request so large that fragmentation is needed?
487
     * @return string the command-line for eapol_test
488
     */
489
    private function eapolTestConfig($probeindex, $opName, $frag) {
490
        $cmdline = CONFIG_DIAGNOSTICS['PATHS']['eapol_test'] .
491
                " -a " . CONFIG_DIAGNOSTICS['RADIUSTESTS']['UDP-hosts'][$probeindex]['ip'] .
492
                " -s " . CONFIG_DIAGNOSTICS['RADIUSTESTS']['UDP-hosts'][$probeindex]['secret'] .
493
                " -o serverchain.pem" .
494
                " -c ./udp_login_test.conf" .
495
                " -M 22:44:66:CA:20:" . sprintf("%02d", $probeindex) . " " .
496
                " -t " . CONFIG_DIAGNOSTICS['RADIUSTESTS']['UDP-hosts'][$probeindex]['timeout'] . " ";
497
        if ($opName) {
498
            $cmdline .= '-N126:s:"1cat.eduroam.org" ';
499
        }
500
        if ($frag) {
501
            for ($i = 0; $i < 6; $i++) { // 6 x 250 bytes means UDP fragmentation will occur - good!
502
                $cmdline .= '-N26:x:0000625A0BF961616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161 ';
503
            }
504
        }
505
        return $cmdline;
506
    }
507
508
    private function createCArepository($tmpDir, &$intermOdditiesCAT, $servercert, $eapIntermediates, $eapIntermediateCRLs) {
509
        // collect CA certificates, both the incoming EAP chain and from CAT config
510
        // Write the root CAs into a trusted root CA dir
511
        // and intermediate and first server cert into a PEM file
512
        // for later chain validation
513
514
        if (!mkdir($tmpDir . "/root-ca-allcerts/", 0700, true)) {
515
            throw new Exception("unable to create root CA directory (RADIUS Tests): $tmpDir/root-ca-allcerts/\n");
516
        }
517
        if (!mkdir($tmpDir . "/root-ca-eaponly/", 0700, true)) {
518
            throw new Exception("unable to create root CA directory (RADIUS Tests): $tmpDir/root-ca-eaponly/\n");
519
        }
520
// make a copy of the EAP-received chain and add the configured intermediates, if any
521
        $catIntermediates = [];
522
        $catRoots = [];
523
        foreach ($this->expectedCABundle as $oneCA) {
524
            $x509 = new \core\common\X509();
525
            $decoded = $x509->processCertificate($oneCA);
526
            if ($decoded === FALSE) {
527
                throw new Exception("Unable to parse an expected CA certificate.");
528
            }
529
            if ($decoded['ca'] == 1) {
530
                if ($decoded['root'] == 1) { // save CAT roots to the root directory
531
                    file_put_contents($tmpDir . "/root-ca-eaponly/configuredroot" . count($catRoots) . ".pem", $decoded['pem']);
532
                    file_put_contents($tmpDir . "/root-ca-allcerts/configuredroot" . count($catRoots) . ".pem", $decoded['pem']);
533
                    $catRoots[] = $decoded['pem'];
534
                } else { // save the intermediates to allcerts directory
535
                    file_put_contents($tmpDir . "/root-ca-allcerts/cat-intermediate" . count($catIntermediates) . ".pem", $decoded['pem']);
536
                    $intermOdditiesCAT = array_merge($intermOdditiesCAT, $this->propertyCheckIntermediate($decoded));
537
                    if (isset($decoded['CRL']) && isset($decoded['CRL'][0])) {
538
                        $this->loggerInstance->debug(4, "got an intermediate CRL; adding them to the chain checks. (Remember: checking end-entity cert only, not the whole chain");
539
                        file_put_contents($tmpDir . "/root-ca-allcerts/crl_cat" . count($catIntermediates) . ".pem", $decoded['CRL'][0]);
540
                    }
541
                    $catIntermediates[] = $decoded['pem'];
542
                }
543
            }
544
        }
545
        // save all intermediate certificates and CRLs to separate files in 
546
        // both root-ca directories
547 View Code Duplication
        foreach ($eapIntermediates as $index => $onePem) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
548
            file_put_contents($tmpDir . "/root-ca-eaponly/intermediate$index.pem", $onePem);
549
            file_put_contents($tmpDir . "/root-ca-allcerts/intermediate$index.pem", $onePem);
550
        }
551 View Code Duplication
        foreach ($eapIntermediateCRLs as $index => $onePem) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
552
            file_put_contents($tmpDir . "/root-ca-eaponly/intermediateCRL$index.pem", $onePem);
553
            file_put_contents($tmpDir . "/root-ca-allcerts/intermediateCRL$index.pem", $onePem);
554
        }
555
556
        $checkstring = "";
557
        if (isset($servercert['CRL']) && isset($servercert['CRL'][0])) {
558
            $this->loggerInstance->debug(4, "got a server CRL; adding them to the chain checks. (Remember: checking end-entity cert only, not the whole chain");
559
            $checkstring = "-crl_check_all";
560
            file_put_contents($tmpDir . "/root-ca-eaponly/crl-server.pem", $servercert['CRL'][0]);
561
            file_put_contents($tmpDir . "/root-ca-allcerts/crl-server.pem", $servercert['CRL'][0]);
562
        }
563
564
565
// now c_rehash the root CA directory ...
566
        system(CONFIG_DIAGNOSTICS['PATHS']['c_rehash'] . " $tmpDir/root-ca-eaponly/ > /dev/null");
567
        system(CONFIG_DIAGNOSTICS['PATHS']['c_rehash'] . " $tmpDir/root-ca-allcerts/ > /dev/null");
568
        return $checkstring;
569
    }
570
571
    private function thoroughChainChecks(&$testresults, &$intermOdditiesCAT, $tmpDir, $servercert, $eapIntermediates, $eapIntermediateCRLs) {
572
573
        $crlCheckString = $this->createCArepository($tmpDir, $intermOdditiesCAT, $servercert, $eapIntermediates, $eapIntermediateCRLs);
574
575
// ... and run the verification test
576
        $verifyResultEaponly = [];
577
        $verifyResultAllcerts = [];
578
// the error log will complain if we run this test against an empty file of certs
579
// so test if there's something PEMy in the file at all
580
        if (filesize("$tmpDir/incomingserver.pem") > 10) {
581
            exec(CONFIG['PATHS']['openssl'] . " verify $crlCheckString -CApath $tmpDir/root-ca-eaponly/ -purpose any $tmpDir/incomingserver.pem", $verifyResultEaponly);
582
            $this->loggerInstance->debug(4, CONFIG['PATHS']['openssl'] . " verify $crlCheckString -CApath $tmpDir/root-ca-eaponly/ -purpose any $tmpDir/incomingserver.pem\n");
583
            $this->loggerInstance->debug(4, "Chain verify pass 1: " . print_r($verifyResultEaponly, TRUE) . "\n");
584
            exec(CONFIG['PATHS']['openssl'] . " verify $crlCheckString -CApath $tmpDir/root-ca-allcerts/ -purpose any $tmpDir/incomingserver.pem", $verifyResultAllcerts);
585
            $this->loggerInstance->debug(4, CONFIG['PATHS']['openssl'] . " verify $crlCheckString -CApath $tmpDir/root-ca-allcerts/ -purpose any $tmpDir/incomingserver.pem\n");
586
            $this->loggerInstance->debug(4, "Chain verify pass 2: " . print_r($verifyResultAllcerts, TRUE) . "\n");
587
        }
588
589
590
// now we do certificate verification against the collected parents
591
// this is done first for the server and then for each of the intermediate CAs
592
// any oddities observed will 
593
// openssl should havd returned exactly one line of output,
594
// and it should have ended with the string "OK", anything else is fishy
595
// The result can also be an empty array - this means there were no
596
// certificates to check. Don't complain about chain validation errors
597
// in that case.
598
// we have the following test result possibilities:
599
// 1. test against allcerts failed
600
// 2. test against allcerts succeded, but against eaponly failed - warn admin
601
// 3. test against eaponly succeded, in this case critical errors about expired certs
602
//    need to be changed to notices, since these certs obviously do tot participate
603
//    in server certificate validation.
604
        if (count($verifyResultAllcerts) == 0 || count($verifyResultEaponly) == 0) {
605
            throw new Exception("No output at all from openssl?");
606
        }
607 View Code Duplication
        if (!preg_match("/OK$/", $verifyResultAllcerts[0])) { // case 1
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
608
            if (preg_match("/certificate revoked$/", $verifyResultAllcerts[1])) {
609
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_SERVER_CERT_REVOKED;
610
            } elseif (preg_match("/unable to get certificate CRL/", $verifyResultAllcerts[1])) {
611
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_UNABLE_TO_GET_CRL;
612
            } else {
613
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_TRUST_ROOT_NOT_REACHED;
614
            }
615
            return 1;
616
        }
617 View Code Duplication
        if (!preg_match("/OK$/", $verifyResultEaponly[0])) { // case 2
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
618
            if (preg_match("/certificate revoked$/", $verifyResultEaponly[1])) {
619
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_SERVER_CERT_REVOKED;
620
            } elseif (preg_match("/unable to get certificate CRL/", $verifyResultEaponly[1])) {
621
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_UNABLE_TO_GET_CRL;
622
            } else {
623
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_TRUST_ROOT_REACHED_ONLY_WITH_OOB_INTERMEDIATES;
624
            }
625
            return 2;
626
        }
627
        return 3;
628
    }
629
630
    private function thoroughNameChecks($servercert, &$testresults) {
631
        // check the incoming hostname (both Subject:CN and subjectAltName:DNS
632
        // against what is configured in the profile; it's a significant error
633
        // if there is no match!
634
        // FAIL if none of the configured names show up in the server cert
635
        // WARN if the configured name is only in either CN or sAN:DNS
636
        // Strategy for checks: we are TOTALLY happy if any one of the
637
        // configured names shows up in both the CN and a sAN
638
        // This is the primary check.
639
        // If that was not the case, we are PARTIALLY happy if any one of
640
        // the configured names was in either of the CN or sAN lists.
641
        // we are UNHAPPY if no names match!
642
643
        $happiness = "UNHAPPY";
644
        foreach ($this->expectedServerNames as $expectedName) {
645
            $this->loggerInstance->debug(4, "Managing expectations for $expectedName: " . print_r($servercert['CN'], TRUE) . print_r($servercert['sAN_DNS'], TRUE));
646
            if (array_search($expectedName, $servercert['CN']) !== FALSE && array_search($expectedName, $servercert['sAN_DNS']) !== FALSE) {
647
                $this->loggerInstance->debug(4, "Totally happy!");
648
                $happiness = "TOTALLY";
649
                break;
650
            } else {
651
                if (array_search($expectedName, $servercert['CN']) !== FALSE || array_search($expectedName, $servercert['sAN_DNS']) !== FALSE) {
652
                    $happiness = "PARTIALLY";
653
// keep trying with other expected names! We could be happier!
654
                }
655
            }
656
        }
657
        switch ($happiness) {
658
            case "UNHAPPY":
659
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_SERVER_NAME_MISMATCH;
660
                return;
661
            case "PARTIALLY":
662
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_SERVER_NAME_PARTIAL_MATCH;
663
                return;
664
            default: // nothing to complain about!
665
                return;
666
        }
667
    }
668
669
    private function executeEapolTest($tmpDir, $probeindex, $eaptype, $innerUser, $password, $opnameCheck, $frag) {
670
        $finalInner = $innerUser;
671
        $finalOuter = $this->outerUsernameForChecks;
672
673
        $theconfigs = $this->wpaSupplicantConfig($eaptype, $finalInner, $finalOuter, $password);
674
        // the config intentionally does not include CA checking. We do this
675
        // ourselves after getting the chain with -o.
676
        file_put_contents($tmpDir . "/udp_login_test.conf", $theconfigs[0]);
677
678
        $cmdline = $this->eapolTestConfig($probeindex, $opnameCheck, $frag);
679
        $this->loggerInstance->debug(4, "Shallow reachability check cmdline: $cmdline\n");
680
        $this->loggerInstance->debug(4, "Shallow reachability check config: $tmpDir\n" . $theconfigs[1] . "\n");
681
        $time_start = microtime(true);
682
        $pflow = [];
683
        exec($cmdline, $pflow);
684
        if ($pflow === NULL) {
685
            throw new Exception("The output of an exec() call really can't be NULL!");
686
        }
687
        $time_stop = microtime(true);
688
        $this->loggerInstance->debug(5, print_r($this->redact($password, $pflow), TRUE));
689
        return [
690
            "time" => ($time_stop - $time_start) * 1000,
691
            "output" => $pflow,
692
        ];
693
    }
694
695
    private function checkRadiusPacketFlow(&$testresults, $packetflow_orig) {
696
697
        $packetflow = $this->filterPackettype($packetflow_orig);
698
699
700
// when MS-CHAPv2 allows retry, we never formally get a reject (just a 
701
// Challenge that PW was wrong but and we should try a different one; 
702
// but that effectively is a reject
703
// so change the flow results to take that into account
704 View Code Duplication
        if ($packetflow[count($packetflow) - 1] == 11 && $this->checkLineparse($packetflow_orig, self::LINEPARSE_CHECK_691)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
705
            $packetflow[count($packetflow) - 1] = 3;
706
        }
707
// also, the ETLRs sometimes send a reject when the server is not 
708
// responding. This should not be considered a real reject; it's a middle
709
// box unduly altering the end-to-end result. Do not consider this final
710
// Reject if it comes from ETLR
711 View Code Duplication
        if ($packetflow[count($packetflow) - 1] == 3 && $this->checkLineparse($packetflow_orig, self::LINEPARSE_CHECK_REJECTIGNORE)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
712
            array_pop($packetflow);
713
        }
714
        $this->loggerInstance->debug(5, "Packetflow: " . print_r($packetflow, TRUE));
715
        $packetcount = array_count_values($packetflow);
716
        $testresults['packetcount'] = $packetcount;
717
        $testresults['packetflow'] = $packetflow;
718
719
// calculate packet counts and see what the overall flow was
720
        return $this->packetCountEvaluation($testresults, $packetcount);
721
    }
722
723
    /**
724
     * parses the eapol_test output to determine whether we got to a point where
725
     * an EAP type was mutually agreed
726
     * 
727
     * @param array $testresults by-reference, we add our findings if something is noteworthy
728
     * @param array $packetflow_orig the array of text output from eapol_test
729
     * @return bool
730
     */
731
    private function wasEapTypeNegotiated(&$testresults, $packetflow_orig) {
732
        $testresults['cert_oddities'] = [];
733
734
        $negotiatedEapType = $this->checkLineparse($packetflow_orig, self::LINEPARSE_EAPACK);
735
        if (!$negotiatedEapType) {
736
            $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_NO_COMMON_EAP_METHOD;
737
        }
738
739
        return $negotiatedEapType;
740
    }
741
742
    const SERVER_NO_CA_EXTENSION = 1;
743
    const SERVER_CA_SELFSIGNED = 2;
744
    const CA_INTERMEDIATE = 3;
745
    const CA_ROOT = 4;
746
747
    private function determineCertificateType(&$cert, $totalCertCount) {
748
        if ($cert['ca'] == 0 && $cert['root'] == 0) {
749
            return RADIUSTests::SERVER_NO_CA_EXTENSION;
750
        }
751
        if ($cert['ca'] == 1 && $cert['root'] == 1) {
752
            if ($totalCertCount == 1) {
753
                $cert['full_details']['type'] = 'totally_selfsigned';
754
                return RADIUSTests::SERVER_CA_SELFSIGNED;
755
            } else {
756
                return RADIUSTests::CA_ROOT;
757
            }
758
        }
759
        return RADIUSTests::CA_INTERMEDIATE;
760
    }
761
762
    private function extractIncomingCertsfromEAP(&$testresults, $tmpDir) {
763
764
        /*
765
         *  EAP's house rules:
766
         * 1) it is unnecessary to include the root CA itself (adding it has
767
         *    detrimental effects on performance)
768
         * 2) TLS Web Server OID presence (Windows OSes need that)
769
         * 3) MD5 signature algorithm disallowed (iOS barks if so)
770
         * 4) CDP URL (Windows Phone 8 barks if not present)
771
         * 5) there should be exactly one server cert in the chain
772
         */
773
774
        $x509 = new \core\common\X509();
775
        $eapCertArray = [];
776
// $eap_certarray holds all certs received in EAP conversation
777
        $incomingData = file_get_contents($tmpDir . "/serverchain.pem");
778
        if ($incomingData !== FALSE) {
779
            $eapCertArray = $x509->splitCertificate($incomingData);
780
        }
781
        $numberServer = 0;
782
        $eapIntermediates = [];
783
        $eapIntermediateCRLs = [];
784
        $servercert = FALSE;
785
        $intermOdditiesEAP = [];
786
787
        $testresults['certdata'] = [];
788
789
790
        foreach ($eapCertArray as $certPem) {
791
            $cert = $x509->processCertificate($certPem);
792
            if ($cert == FALSE) {
793
                continue;
794
            }
795
// consider the certificate a server cert 
796
// a) if it is not a CA and is not a self-signed root
797
// b) if it is a CA, and self-signed, and it is the only cert in
798
//    the incoming cert chain
799
//    (meaning the self-signed is itself the server cert)
800
            switch ($this->determineCertificateType($cert, count($eapCertArray))) {
801
                case RADIUSTests::SERVER_NO_CA_EXTENSION: // both are handled same, fall-through
802
                case RADIUSTests::SERVER_CA_SELFSIGNED:
803
                    $numberServer = $numberServer + 1;
804
805
                    $servercert = $cert;
806
                    if ($numberServer == 1) {
807
                        if (file_put_contents($tmpDir . "/incomingserver.pem", $certPem . "\n") === FALSE) {
808
                            $this->loggerInstance->debug(4, "The (first) server certificate could not be written to $tmpDir/incomingserver.pem!\n");
809
                        }
810
                        $this->loggerInstance->debug(4, "This is the (first) server certificate, with CRL content if applicable: " . print_r($servercert, true));
811
                    }
812 View Code Duplication
                    if ($numberServer > 1 && !in_array(RADIUSTests::CERTPROB_TOO_MANY_SERVER_CERTS, $testresults['cert_oddities'])) {
813
                        $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_TOO_MANY_SERVER_CERTS;
814
                    }
815
                    break;
816
                case RADIUSTests::CA_ROOT:
817 View Code Duplication
                    if (!in_array(RADIUSTests::CERTPROB_ROOT_INCLUDED, $testresults['cert_oddities'])) {
818
                        $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_ROOT_INCLUDED;
819
                    }
820
// do not save the root CA, it serves no purpose
821
// chain checks need to be against the UPLOADED CA of the
822
// IdP/profile, not against an EAP-discovered CA
823
                    break;
824
                case RADIUSTests::CA_INTERMEDIATE:
825
                    $intermOdditiesEAP = array_merge($intermOdditiesEAP, $this->propertyCheckIntermediate($cert));
826
                    $eapIntermediates[] = $certPem;
827
828
                    if (isset($cert['CRL']) && isset($cert['CRL'][0])) {
829
                        $eapIntermediateCRLs[] = $cert['CRL'][0];
830
                    }
831
                    break;
832
                default:
833
                    throw new Exception("Status of certificate could not be determined!");
834
            }
835
            $testresults['certdata'][] = $cert['full_details'];
836
        }
837
838
        if ($numberServer == 0) {
839
            $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_NO_SERVER_CERT;
840
        }
841
// check server cert properties
842
        if ($numberServer > 0) {
843
            if ($servercert === FALSE) {
844
                throw new Exception("We incremented the numberServer counter and added a certificate. Now it's gone?!");
845
            }
846
            $testresults['cert_oddities'] = array_merge($testresults['cert_oddities'], $this->propertyCheckServercert($servercert));
847
            $testresults['incoming_server_names'] = $servercert['incoming_server_names'];
848
        }
849
        return [
850
            "SERVERCERT" => $servercert,
851
            "INTERMEDIATE_CA" => $eapIntermediates,
852
            "INTERMEDIATE_CRL" => $eapIntermediateCRLs,
853
            "INTERMEDIATE_OBSERVED_ODDITIES" => $intermOdditiesEAP,
854
        ];
855
    }
856
857
    /**
858
     * The big Guy. This performs an actual login with EAP and records how far 
859
     * it got and what oddities were observed along the way
860
     * @param int $probeindex the probe we are connecting to (as set in product config)
861
     * @param array $eaptype EAP type to use for connection
862
     * @param string $innerUser inner username to try
863
     * @param string $password password to try
864
     * @param boolean $opnameCheck whether or not we check with Operator-Name set
865
     * @param boolean $frag whether or not we check with an oversized packet forcing fragmentation
866
     * @param string $clientcertdata client certificate credential to try
867
     * @return int overall return code of the login test
868
     * @throws Exception
869
     */
870
    public function udpLogin($probeindex, $eaptype, $innerUser, $password, $opnameCheck = TRUE, $frag = TRUE, $clientcertdata = NULL) {
871
872
        /** preliminaries */
873
        $eapText = \core\common\EAP::eapDisplayName($eaptype);
874
        // no host to send probes to? Nothing to do then
875
        if (!isset(CONFIG_DIAGNOSTICS['RADIUSTESTS']['UDP-hosts'][$probeindex])) {
876
            $this->UDP_reachability_executed = RADIUSTests::RETVAL_NOTCONFIGURED;
877
            return RADIUSTests::RETVAL_NOTCONFIGURED;
878
        }
879
        // if we need client certs but don't have one, return
880
        if (($eaptype == \core\common\EAP::EAPTYPE_ANY || $eaptype == \core\common\EAP::EAPTYPE_TLS) && $clientcertdata === NULL) {
881
            $this->UDP_reachability_executed = RADIUSTests::RETVAL_NOTCONFIGURED;
882
            return RADIUSTests::RETVAL_NOTCONFIGURED;
883
        }
884
        // if we don't have a string for outer EAP method name, give up
885
        if (!isset($eapText['OUTER'])) {
886
            $this->UDP_reachability_executed = RADIUSTests::RETVAL_NOTCONFIGURED;
887
            return RADIUSTests::RETVAL_NOTCONFIGURED;
888
        }
889
        // we will need a config blob for wpa_supplicant, in a temporary directory
890
        $temporary = $this->createTemporaryDirectory('test');
891
        $tmpDir = $temporary['dir'];
892
        chdir($tmpDir);
893
        $this->loggerInstance->debug(4, "temp dir: $tmpDir\n");
894
        if ($clientcertdata !== NULL) {
895
            file_put_contents($tmpDir . "/client.p12", $clientcertdata);
896
        }
897
        $testresults = [];
898
        // execute RADIUS/EAP converation
899
        $runtime_results = $this->executeEapolTest($tmpDir, $probeindex, $eaptype, $innerUser, $password, $opnameCheck, $frag);
900
        $testresults['time_millisec'] = $runtime_results['time'];
901
        $packetflow_orig = $runtime_results['output'];
902
        $radiusResult = $this->checkRadiusPacketFlow($testresults, $packetflow_orig);
903
        $negotiatedEapType = $this->wasEapTypeNegotiated($testresults, $packetflow_orig);
904
        // now let's look at the server cert+chain, if we got a cert at all
905
        // that's not the case if we do EAP-pwd or could not negotiate an EAP method at
906
        // all
907
        if (
908
                $eaptype != \core\common\EAP::EAPTYPE_PWD &&
909
                (($radiusResult == RADIUSTests::RETVAL_CONVERSATION_REJECT && $negotiatedEapType) || $radiusResult == RADIUSTests::RETVAL_OK)
910
        ) {
911
912
            $bundle = $this->extractIncomingCertsfromEAP($testresults, $tmpDir);
913
914
// FOR OWN REALMS check:
915
// 1) does the incoming chain have a root in one of the configured roots
916
//    if not, this is a signficant configuration error
917
// return this with one or more of the CERTPROB_ constants (see defs)
918
// TRUST_ROOT_NOT_REACHED
919
// TRUST_ROOT_REACHED_ONLY_WITH_OOB_INTERMEDIATES
920
// then check the presented names
921
// check intermediate ca cert properties
922
// check trust chain for completeness
923
// works only for thorough checks, not shallow, so:
924
            $intermOdditiesCAT = [];
925
            $verifyResult = 0;
926
927
            if ($this->opMode == self::RADIUS_TEST_OPERATION_MODE_THOROUGH) {
928
                $verifyResult = $this->thoroughChainChecks($testresults, $intermOdditiesCAT, $tmpDir, $bundle["SERVERCERT"], $bundle["INTERMEDIATE_CA"], $bundle["INTERMEDIATE_CRL"]);
929
                $this->thoroughNameChecks($bundle["SERVERCERT"], $testresults);
930
            }
931
932
            $testresults['cert_oddities'] = array_merge($testresults['cert_oddities'], $bundle["INTERMEDIATE_OBSERVED_ODDITIES"]);
933
            if (in_array(RADIUSTests::CERTPROB_OUTSIDE_VALIDITY_PERIOD, $intermOdditiesCAT) && $verifyResult == 3) {
934
                $key = array_search(RADIUSTests::CERTPROB_OUTSIDE_VALIDITY_PERIOD, $intermOdditiesCAT);
935
                $intermOdditiesCAT[$key] = RADIUSTests::CERTPROB_OUTSIDE_VALIDITY_PERIOD_WARN;
936
            }
937
938
            $testresults['cert_oddities'] = array_merge($testresults['cert_oddities'], $intermOdditiesCAT);
939
940
// mention trust chain failure only if no expired cert was in the chain; otherwise path validation will trivially fail
941
            if (in_array(RADIUSTests::CERTPROB_OUTSIDE_VALIDITY_PERIOD, $testresults['cert_oddities'])) {
942
                $this->loggerInstance->debug(4, "Deleting trust chain problem report, if present.");
943 View Code Duplication
                if (($key = array_search(RADIUSTests::CERTPROB_TRUST_ROOT_NOT_REACHED, $testresults['cert_oddities'])) !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
944
                    unset($testresults['cert_oddities'][$key]);
945
                }
946 View Code Duplication
                if (($key = array_search(RADIUSTests::CERTPROB_TRUST_ROOT_REACHED_ONLY_WITH_OOB_INTERMEDIATES, $testresults['cert_oddities'])) !== false) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
947
                    unset($testresults['cert_oddities'][$key]);
948
                }
949
            }
950
        }
951
        $this->loggerInstance->debug(4, "UDP_LOGIN\n");
952
        $this->loggerInstance->debug(4, $testresults);
953
        $this->loggerInstance->debug(4, "\nEND\n");
954
        $this->UDP_reachability_result[$probeindex] = $testresults;
955
        $this->UDP_reachability_executed = $radiusResult;
956
        return $radiusResult;
957
    }
958
959
    public function consolidateUdpResult($host) {
960
        $ret = [];
961
        $serverCert = [];
962
        $udpResult = $this->UDP_reachability_result[$host];
963
        if (isset($udpResult['certdata']) && count($udpResult['certdata'])) {
964
            foreach ($udpResult['certdata'] as $certdata) {
965
                if ($certdata['type'] != 'server' && $certdata['type'] != 'totally_selfsigned') {
966
                    continue;
967
                }
968
                if (isset($certdata['extensions'])) {
969
                    foreach ($certdata['extensions'] as $k => $v) {
970
                        $certdata['extensions'][$k] = iconv('UTF-8', 'UTF-8//IGNORE', $certdata['extensions'][$k]);
971
                    }
972
                }
973
                $serverCert = [
974
                    'subject' => $this->printDN($certdata['subject']),
975
                    'issuer' => $this->printDN($certdata['issuer']),
976
                    'validFrom' => $this->printTm($certdata['validFrom_time_t']),
977
                    'validTo' => $this->printTm($certdata['validTo_time_t']),
978
                    'serialNumber' => $certdata['serialNumber'] . sprintf(" (0x%X)", $certdata['serialNumber']),
979
                    'sha1' => $certdata['sha1'],
980
                    'extensions' => $certdata['extensions']
981
                ];
982
            }
983
        }
984
        $ret['server_cert'] = $serverCert;
985
        $ret['server'] = 0;
986
        if (isset($udpResult['incoming_server_names'][0])) {
987
            $ret['server'] = sprintf(_("Connected to %s."), $udpResult['incoming_server_names'][0]);
988
        }
989
        $ret['level'] = \core\common\Entity::L_OK;
990
        $ret['time_millisec'] = sprintf("%d", $udpResult['time_millisec']);
991
        if (empty($udpResult['cert_oddities'])) {
992
            $ret['message'] = _("<strong>Test successful</strong>: a bidirectional RADIUS conversation with multiple round-trips was carried out, and ended in an Access-Reject as planned.");
993
            return $ret;
994
        }
995
996
        $ret['message'] = _("<strong>Test partially successful</strong>: a bidirectional RADIUS conversation with multiple round-trips was carried out, and ended in an Access-Reject as planned. Some properties of the connection attempt were sub-optimal; the list is below.");
997
        $ret['cert_oddities'] = [];
998
        foreach ($udpResult['cert_oddities'] as $oddity) {
999
            $o = [];
1000
            $o['code'] = $oddity;
1001
            $o['message'] = isset($this->returnCodes[$oddity]["message"]) && $this->returnCodes[$oddity]["message"] ? $this->returnCodes[$oddity]["message"] : $oddity;
1002
            $o['level'] = $this->returnCodes[$oddity]["severity"];
1003
            $ret['level'] = max($ret['level'], $this->returnCodes[$oddity]["severity"]);
1004
            $ret['cert_oddities'][] = $o;
1005
        }
1006
1007
        return $ret;
1008
    }
1009
1010
}
1011