Passed
Push — master ( 54cc30...cc7767 )
by Maja
08:24
created

RADIUSTests::extractIncomingCertsfromEAP()   C

Complexity

Conditions 15
Paths 58

Size

Total Lines 85

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
nc 58
nop 2
dl 0
loc 85
rs 5.0606
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
// we share the same checks as for CAs when it comes to signature algorithm and basicconstraints
161
// so call that function and memorise the outcome
162
        $returnarray = $this->propertyCheckIntermediate($servercert, TRUE);
163
        $sANdns = [];
164
        if (!isset($servercert['full_details']['extensions'])) {
165
            $returnarray[] = RADIUSTests::CERTPROB_NO_TLS_WEBSERVER_OID;
166
            $returnarray[] = RADIUSTests::CERTPROB_NO_CDP_HTTP;
167
        } else { // Extensions are present...
168
            if (!isset($servercert['full_details']['extensions']['extendedKeyUsage']) || !preg_match("/TLS Web Server Authentication/", $servercert['full_details']['extensions']['extendedKeyUsage'])) {
169
                $returnarray[] = RADIUSTests::CERTPROB_NO_TLS_WEBSERVER_OID;
170
            }
171
            if (isset($servercert['full_details']['extensions']['subjectAltName'])) {
172
                $sANlist = explode(", ", $servercert['full_details']['extensions']['subjectAltName']);
173
                foreach ($sANlist as $subjectAltName) {
174
                    if (preg_match("/^DNS:/", $subjectAltName)) {
175
                        $sANdns[] = substr($subjectAltName, 4);
176
                    }
177
                }
178
            }
179
        }
180
181
        // often, there is only one name, so we store it in an array of one member
182
        $commonName = [$servercert['full_details']['subject']['CN']];
183
        // if we got an array of names instead, then that is already an array, so override
184
        if (isset($servercert['full_details']['subject']['CN']) && is_array($servercert['full_details']['subject']['CN'])) {
185
            $commonName = $servercert['full_details']['subject']['CN'];
186
            $returnarray[] = RADIUSTests::CERTPROB_MULTIPLE_CN;
187
        }
188
        $allnames = array_values(array_unique(array_merge($commonName, $sANdns)));
189
// check for wildcards
190
// check for real hostnames, and whether there is a wildcard in a name
191
        foreach ($allnames as $onename) {
192
            if (preg_match("/\*/", $onename)) {
193
                $returnarray[] = RADIUSTests::CERTPROB_WILDCARD_IN_NAME;
194
                continue; // otherwise we'd ALSO complain that it's not a real hostname
195
            }
196
            if ($onename != "" && filter_var("foo@" . idn_to_ascii($onename), FILTER_VALIDATE_EMAIL) === FALSE) {
197
                $returnarray[] = RADIUSTests::CERTPROB_NOT_A_HOSTNAME;
198
            }
199
        }
200
        $servercert['incoming_server_names'] = $allnames;
201
        $servercert['sAN_DNS'] = $sANdns;
202
        $servercert['CN'] = $commonName;
203
        return $returnarray;
204
    }
205
206
    /**
207
     * This function parses a X.509 intermediate CA cert and checks if it finds client device incompatibilities
208
     * 
209
     * @param array $intermediateCa the properties of the certificate as returned by processCertificate()
210
     * @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
211
     * @return array of oddities; the array is empty if everything is fine
212
     */
213
    private function propertyCheckIntermediate(&$intermediateCa, $serverCert = FALSE) {
214
        $returnarray = [];
215
        if (preg_match("/md5/i", $intermediateCa['full_details']['signatureTypeSN'])) {
216
            $returnarray[] = RADIUSTests::CERTPROB_MD5_SIGNATURE;
217
        }
218
        if (preg_match("/sha1/i", $intermediateCa['full_details']['signatureTypeSN'])) {
219
            $returnarray[] = RADIUSTests::CERTPROB_SHA1_SIGNATURE;
220
        }
221
        $this->loggerInstance->debug(4, "CERT IS: " . print_r($intermediateCa, TRUE));
222
        if ($intermediateCa['basicconstraints_set'] == 0) {
223
            $returnarray[] = RADIUSTests::CERTPROB_NO_BASICCONSTRAINTS;
224
        }
225
        if ($intermediateCa['full_details']['public_key_algorithm'] == \core\common\X509::KNOWN_PUBLIC_KEY_ALGORITHMS[0] && $intermediateCa['full_details']['public_key_length'] < 2048) {
226
            $returnarray[] = RADIUSTests::CERTPROB_LOW_KEY_LENGTH;
227
        }
228
        if (!in_array($intermediateCa['full_details']['public_key_algorithm'], \core\common\X509::KNOWN_PUBLIC_KEY_ALGORITHMS)) {
229
            $returnarray[] = RADIUSTests::CERTPROB_UNKNOWN_PUBLIC_KEY_ALGORITHM;
230
        }
231
        $validFrom = $intermediateCa['full_details']['validFrom_time_t'];
232
        $now = time();
233
        $validTo = $intermediateCa['full_details']['validTo_time_t'];
234
        if ($validFrom > $now || $validTo < $now) {
235
            $returnarray[] = RADIUSTests::CERTPROB_OUTSIDE_VALIDITY_PERIOD;
236
        }
237
        $addCertCrlResult = $this->addCrltoCert($intermediateCa);
238
        if ($addCertCrlResult !== 0 && $serverCert) {
239
            $returnarray[] = $addCertCrlResult;
240
        }
241
242
        return $returnarray;
243
    }
244
245
    /**
246
     * This function returns an array of errors which were encountered in all the tests.
247
     * 
248
     * @return array all the errors
249
     */
250
    public function listerrors() {
251
        return $this->errorlist;
252
    }
253
254
    /**
255
     * This function performs actual authentication checks with MADE-UP credentials.
256
     * Its purpose is to check if a RADIUS server is reachable and speaks EAP.
257
     * The function fills array RADIUSTests::UDP_reachability_result[$probeindex] with all check detail
258
     * in case more than the return code is needed/wanted by the caller
259
     * 
260
     * @param int $probeindex refers to the specific UDP-host in the config that should be checked
261
     * @param boolean $opnameCheck should we check choking on Operator-Name?
262
     * @param boolean $frag should we cause UDP fragmentation? (Warning: makes use of Operator-Name!)
263
     * @return int returncode
264
     */
265
    public function udpReachability($probeindex, $opnameCheck = TRUE, $frag = TRUE) {
266
        // for EAP-TLS to be a viable option, we need to pass a random client cert to make eapol_test happy
267
        // the following PEM data is one of the SENSE EAPLab client certs (not secret at all)
268
        $clientcert = file_get_contents(dirname(__FILE__) . "/clientcert.p12");
269
        if ($clientcert === FALSE) {
270
            throw new Exception("A dummy client cert is part of the source distribution, but could not be loaded!");
271
        }
272
        // if we are in thorough opMode, use our knowledge for a more clever check
273
        // otherwise guess
274
        if ($this->opMode == self::RADIUS_TEST_OPERATION_MODE_THOROUGH) {
275
            return $this->udpLogin($probeindex, $this->supportedEapTypes[0]->getArrayRep(), $this->outerUsernameForChecks, 'eaplab', $opnameCheck, $frag, $clientcert);
276
        }
277
        return $this->udpLogin($probeindex, \core\common\EAP::EAPTYPE_ANY, "cat-connectivity-test@" . $this->realm, 'eaplab', $opnameCheck, $frag, $clientcert);
278
    }
279
280
    /**
281
     * There is a CRL Distribution Point URL in the certificate. So download the
282
     * CRL and attach it to the cert structure so that we can later find out if
283
     * the cert was revoked
284
     * @param array $cert by-reference: the cert data we are writing into
285
     * @return int result code whether we were successful in retrieving the CRL
286
     */
287
    private function addCrltoCert(&$cert) {
288
        $crlUrl = [];
289
        $returnresult = 0;
290
        if (!isset($cert['full_details']['extensions']['crlDistributionPoints'])) {
291
            return RADIUSTests::CERTPROB_NO_CDP;
292
        }
293
        if (!preg_match("/^.*URI\:(http)(.*)$/", str_replace(["\r", "\n"], ' ', $cert['full_details']['extensions']['crlDistributionPoints']), $crlUrl)) {
294
            return RADIUSTests::CERTPROB_NO_CDP_HTTP;
295
        }
296
        // first and second sub-match is the full URL... check it
297
        $crlcontent = \core\common\OutsideComm::downloadFile(trim($crlUrl[1] . $crlUrl[2]));
298
        if ($crlcontent === FALSE) {
299
            return RADIUSTests::CERTPROB_NO_CRL_AT_CDP_URL;
300
        }
301
        /* CRLs are always in DER form, so need encoding
302
         * note that what we ACTUALLY got can be arbitrary junk; we just deposit
303
         * it on the filesystem and let openssl figure out if it is usable or not
304
         *
305
         * Unfortunately, that freaks out Scrutinizer because we write unvetted
306
         * data to the filesystem. Let's see if we can make things better.
307
         */
308
        
309
        // $pem = chunk_split(base64_encode($crlcontent), 64, "\n");
310
        
311
        // inspired by https://stackoverflow.com/questions/2390604/how-to-pass-variables-as-stdin-into-command-line-from-php
312
        $proc = CONFIG['PATHS']['openssl']." crl -inform der";
313
        $descriptorspec = [
314
          0 => ["pipe", "r"],
315
          1 => ["pipe", "w"],
316
          2 => ["pipe", "w"],
317
        ];
318
        $process = proc_open($proc, $descriptorspec, $pipes);
319
        if (!is_resource($process)) {
320
            throw new Exception("Unable to execute openssl cmdline for CRL conversion!");
321
        }
322
        fwrite($pipes[0], $crlcontent);
323
        fclose($pipes[0]);
324
        $pem = stream_get_contents($pipes[1]);
325
        fclose($pipes[1]);
326
        fclose($pipes[2]);
327
        $retval = proc_close($process);
328
        if ($retval != 0 || !preg_match("/BEGIN X509 CRL/",$pem)) {
329
            // this was not a real CRL
330
            return RADIUSTests::CERTPROB_NO_CRL_AT_CDP_URL;
331
        }
332
        $cert['CRL'] = [];
333
        $cert['CRL'][] = $pem;
334
        return $returnresult;
335
    }
336
337
    /**
338
     * We don't want to write passwords of the live login test to our logs. Filter them out
339
     * @param string $stringToRedact what should be redacted
340
     * @param array $inputarray array of strings (outputs of eapol_test command)
341
     * @return string[] the output of eapol_test with the password redacted
342
     */
343
    private function redact($stringToRedact, $inputarray) {
344
        $temparray = preg_replace("/^.*$stringToRedact.*$/", "LINE CONTAINING PASSWORD REDACTED", $inputarray);
345
        $hex = bin2hex($stringToRedact);
346
        $spaced = "";
347
        $origLength = strlen($hex);
348
        for ($i = 1; $i < $origLength; $i++) {
349
            if ($i % 2 == 1 && $i != strlen($hex)) {
350
                $spaced .= $hex[$i] . " ";
351
            } else {
352
                $spaced .= $hex[$i];
353
            }
354
        }
355
        return preg_replace("/$spaced/", " HEX ENCODED PASSWORD REDACTED ", $temparray);
356
    }
357
358
    /**
359
     * Filters eapol_test output and finds out the packet codes out of which the conversation was comprised of
360
     * 
361
     * @param array $inputarray array of strings (outputs of eapol_test command)
362
     * @return array the packet codes which were exchanged, in sequence
363
     */
364
    private function filterPackettype($inputarray) {
365
        $retarray = [];
366
        foreach ($inputarray as $line) {
367
            if (preg_match("/RADIUS message:/", $line)) {
368
                $linecomponents = explode(" ", $line);
369
                $packettypeExploded = explode("=", $linecomponents[2]);
370
                $packettype = $packettypeExploded[1];
371
                $retarray[] = $packettype;
372
            }
373
        }
374
        return $retarray;
375
    }
376
377
    const LINEPARSE_CHECK_REJECTIGNORE = 1;
378
    const LINEPARSE_CHECK_691 = 2;
379
    const LINEPARSE_EAPACK = 3;
380
381
    /**
382
     * this function checks for various special conditions which can be found 
383
     * only by parsing eapol_test output line by line. Checks currently 
384
     * implemented are:
385
     * * if the ETLRs sent back an Access-Reject because there appeared to
386
     *   be a timeout further downstream
387
     * * did the server send an MSCHAP Error 691 - Retry Allowed in a Challenge
388
     *   instead of an outright reject?
389
     * * was an EAP method ever acknowledged by both sides during the EAP
390
     *   conversation
391
     * 
392
     * @param array $inputarray array of strings (outputs of eapol_test command)
393
     * @param int $desiredCheck which test should be run (see constants above)
394
     * @return boolean returns TRUE if ETLR Reject logic was detected; FALSE if not
395
     */
396
    private function checkLineparse($inputarray, $desiredCheck) {
397
        foreach ($inputarray as $lineid => $line) {
398
            switch ($desiredCheck) {
399
                case self::LINEPARSE_CHECK_REJECTIGNORE:
400
                    if (preg_match("/Attribute 18 (Reply-Message)/", $line) && preg_match("/Reject instead of Ignore at eduroam.org/", $inputarray[$lineid + 1])) {
401
                        return TRUE;
402
                    }
403
                    break;
404
                case self::LINEPARSE_CHECK_691:
405
                    if (preg_match("/MSCHAPV2: error 691/", $line) && preg_match("/MSCHAPV2: retry is allowed/", $inputarray[$lineid + 1])) {
406
                        return TRUE;
407
                    }
408
                    break;
409
                case self::LINEPARSE_EAPACK:
410
                    if (preg_match("/CTRL-EVENT-EAP-PROPOSED-METHOD/", $line) && !preg_match("/NAK$/", $line)) {
411
                        return TRUE;
412
                    }
413
                    break;
414
                default:
415
                    throw new Exception("This lineparse test does not exist.");
416
            }
417
        }
418
        return FALSE;
419
    }
420
421
    /**
422
     * 
423
     * @param array $eaptype array representation of the EAP type
424
     * @param string $inner inner username
425
     * @param string $outer outer username
426
     * @param string $password the password
427
     * @return string[] [0] is the actual config for wpa_supplicant, [1] is a redacted version for logs
428
     */
429
    private function wpaSupplicantConfig(array $eaptype, string $inner, string $outer, string $password) {
430
        $eapText = \core\common\EAP::eapDisplayName($eaptype);
431
        $config = '
432
network={
433
  ssid="' . CONFIG['APPEARANCE']['productname'] . ' testing"
434
  key_mgmt=WPA-EAP
435
  proto=WPA2
436
  pairwise=CCMP
437
  group=CCMP
438
  ';
439
// phase 1
440
        $config .= 'eap=' . $eapText['OUTER'] . "\n";
441
        $logConfig = $config;
442
// phase 2 if applicable; all inner methods have passwords
443
        if (isset($eapText['INNER']) && $eapText['INNER'] != "") {
444
            $config .= '  phase2="auth=' . $eapText['INNER'] . "\"\n";
445
            $logConfig .= '  phase2="auth=' . $eapText['INNER'] . "\"\n";
446
        }
447
// all methods set a password, except EAP-TLS
448
        if ($eaptype != \core\common\EAP::EAPTYPE_TLS) {
449
            $config .= "  password=\"$password\"\n";
450
            $logConfig .= "  password=\"not logged for security reasons\"\n";
451
        }
452
// for methods with client certs, add a client cert config block
453
        if ($eaptype == \core\common\EAP::EAPTYPE_TLS || $eaptype == \core\common\EAP::EAPTYPE_ANY) {
454
            $config .= "  private_key=\"./client.p12\"\n";
455
            $logConfig .= "  private_key=\"./client.p12\"\n";
456
            $config .= "  private_key_passwd=\"$password\"\n";
457
            $logConfig .= "  private_key_passwd=\"not logged for security reasons\"\n";
458
        }
459
460
// inner identity
461
        $config .= '  identity="' . $inner . "\"\n";
462
        $logConfig .= '  identity="' . $inner . "\"\n";
463
// outer identity, may be equal
464
        $config .= '  anonymous_identity="' . $outer . "\"\n";
465
        $logConfig .= '  anonymous_identity="' . $outer . "\"\n";
466
// done
467
        $config .= "}";
468
        $logConfig .= "}";
469
470
        return [$config, $logConfig];
471
    }
472
473
    private function packetCountEvaluation(&$testresults, $packetcount) {
474
        $reqs = $packetcount[1] ?? 0;
475
        $accepts = $packetcount[2] ?? 0;
476
        $rejects = $packetcount[3] ?? 0;
477
        $challenges = $packetcount[11] ?? 0;
478
        $testresults['packetflow_sane'] = TRUE;
479
        if ($reqs - $accepts - $rejects - $challenges != 0 || $accepts > 1 || $rejects > 1) {
480
            $testresults['packetflow_sane'] = FALSE;
481
        }
482
483
        $this->loggerInstance->debug(5, "XYZ: Counting req, acc, rej, chal: $reqs, $accepts, $rejects, $challenges");
484
485
// calculate the main return values that this test yielded
486
487
        $finalretval = RADIUSTests::RETVAL_INVALID;
488
        if ($accepts + $rejects == 0) { // no final response. hm.
489
            if ($challenges > 0) { // but there was an Access-Challenge
490
                $finalretval = RADIUSTests::RETVAL_SERVER_UNFINISHED_COMM;
491
            } else {
492
                $finalretval = RADIUSTests::RETVAL_NO_RESPONSE;
493
            }
494
        } else // either an accept or a reject
495
// rejection without EAP is fishy
496
        if ($rejects > 0) {
497
            if ($challenges == 0) {
498
                $finalretval = RADIUSTests::RETVAL_IMMEDIATE_REJECT;
499
            } else { // i.e. if rejected with challenges
500
                $finalretval = RADIUSTests::RETVAL_CONVERSATION_REJECT;
501
            }
502
        } else if ($accepts > 0) {
503
            $finalretval = RADIUSTests::RETVAL_OK;
504
        }
505
506
        return $finalretval;
507
    }
508
509
    /**
510
     * generate an eapol_test command-line config for the fixed config filename 
511
     * ./udp_login_test.conf
512
     * @param int $probeindex number of the probe to check against
513
     * @param boolean $opName include Operator-Name in request?
514
     * @param boolean $frag make request so large that fragmentation is needed?
515
     * @return string the command-line for eapol_test
516
     */
517
    private function eapolTestConfig($probeindex, $opName, $frag) {
518
        $cmdline = CONFIG_DIAGNOSTICS['PATHS']['eapol_test'] .
519
                " -a " . CONFIG_DIAGNOSTICS['RADIUSTESTS']['UDP-hosts'][$probeindex]['ip'] .
520
                " -s " . CONFIG_DIAGNOSTICS['RADIUSTESTS']['UDP-hosts'][$probeindex]['secret'] .
521
                " -o serverchain.pem" .
522
                " -c ./udp_login_test.conf" .
523
                " -M 22:44:66:CA:20:" . sprintf("%02d", $probeindex) . " " .
524
                " -t " . CONFIG_DIAGNOSTICS['RADIUSTESTS']['UDP-hosts'][$probeindex]['timeout'] . " ";
525
        if ($opName) {
526
            $cmdline .= '-N126:s:"1cat.eduroam.org" ';
527
        }
528
        if ($frag) {
529
            for ($i = 0; $i < 6; $i++) { // 6 x 250 bytes means UDP fragmentation will occur - good!
530
                $cmdline .= '-N26:x:0000625A0BF961616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161 ';
531
            }
532
        }
533
        return $cmdline;
534
    }
535
536
    private function createCArepository($tmpDir, &$intermOdditiesCAT, $servercert, $eapIntermediates, $eapIntermediateCRLs) {
537
        // collect CA certificates, both the incoming EAP chain and from CAT config
538
        // Write the root CAs into a trusted root CA dir
539
        // and intermediate and first server cert into a PEM file
540
        // for later chain validation
541
542
        if (!mkdir($tmpDir . "/root-ca-allcerts/", 0700, true)) {
543
            throw new Exception("unable to create root CA directory (RADIUS Tests): $tmpDir/root-ca-allcerts/\n");
544
        }
545
        if (!mkdir($tmpDir . "/root-ca-eaponly/", 0700, true)) {
546
            throw new Exception("unable to create root CA directory (RADIUS Tests): $tmpDir/root-ca-eaponly/\n");
547
        }
548
// make a copy of the EAP-received chain and add the configured intermediates, if any
549
        $catIntermediates = [];
550
        $catRoots = [];
551
        foreach ($this->expectedCABundle as $oneCA) {
552
            $x509 = new \core\common\X509();
553
            $decoded = $x509->processCertificate($oneCA);
554
            if (is_bool($decoded)) {
555
                throw new Exception("Unable to parse an expected CA certificate.");
556
            }
557
            if ($decoded['ca'] == 1) {
558
                if ($decoded['root'] == 1) { // save CAT roots to the root directory
559
                    file_put_contents($tmpDir . "/root-ca-eaponly/configuredroot" . count($catRoots) . ".pem", $decoded['pem']);
560
                    file_put_contents($tmpDir . "/root-ca-allcerts/configuredroot" . count($catRoots) . ".pem", $decoded['pem']);
561
                    $catRoots[] = $decoded['pem'];
562
                } else { // save the intermediates to allcerts directory
563
                    file_put_contents($tmpDir . "/root-ca-allcerts/cat-intermediate" . count($catIntermediates) . ".pem", $decoded['pem']);
564
                    $intermOdditiesCAT = array_merge($intermOdditiesCAT, $this->propertyCheckIntermediate($decoded));
565
                    if (isset($decoded['CRL']) && isset($decoded['CRL'][0])) {
566
                        $this->loggerInstance->debug(4, "got an intermediate CRL; adding them to the chain checks. (Remember: checking end-entity cert only, not the whole chain");
567
                        file_put_contents($tmpDir . "/root-ca-allcerts/crl_cat" . count($catIntermediates) . ".pem", $decoded['CRL'][0]);
568
                    }
569
                    $catIntermediates[] = $decoded['pem'];
570
                }
571
            }
572
        }
573
        // save all intermediate certificates and CRLs to separate files in 
574
        // both root-ca directories
575
        foreach ($eapIntermediates as $index => $onePem) {
576
            file_put_contents($tmpDir . "/root-ca-eaponly/intermediate$index.pem", $onePem);
577
            file_put_contents($tmpDir . "/root-ca-allcerts/intermediate$index.pem", $onePem);
578
        }
579
        foreach ($eapIntermediateCRLs as $index => $onePem) {
580
            file_put_contents($tmpDir . "/root-ca-eaponly/intermediateCRL$index.pem", $onePem);
581
            file_put_contents($tmpDir . "/root-ca-allcerts/intermediateCRL$index.pem", $onePem);
582
        }
583
584
        $checkstring = "";
585
        if (isset($servercert['CRL']) && isset($servercert['CRL'][0])) {
586
            $this->loggerInstance->debug(4, "got a server CRL; adding them to the chain checks. (Remember: checking end-entity cert only, not the whole chain");
587
            $checkstring = "-crl_check_all";
588
            file_put_contents($tmpDir . "/root-ca-eaponly/crl-server.pem", $servercert['CRL'][0]);
589
            file_put_contents($tmpDir . "/root-ca-allcerts/crl-server.pem", $servercert['CRL'][0]);
590
        }
591
592
593
// now c_rehash the root CA directory ...
594
        system(CONFIG_DIAGNOSTICS['PATHS']['c_rehash'] . " $tmpDir/root-ca-eaponly/ > /dev/null");
595
        system(CONFIG_DIAGNOSTICS['PATHS']['c_rehash'] . " $tmpDir/root-ca-allcerts/ > /dev/null");
596
        return $checkstring;
597
    }
598
599
    private function thoroughChainChecks(&$testresults, &$intermOdditiesCAT, $tmpDir, $servercert, $eapIntermediates, $eapIntermediateCRLs) {
600
601
        $crlCheckString = $this->createCArepository($tmpDir, $intermOdditiesCAT, $servercert, $eapIntermediates, $eapIntermediateCRLs);
602
603
// ... and run the verification test
604
        $verifyResultEaponly = [];
605
        $verifyResultAllcerts = [];
606
// the error log will complain if we run this test against an empty file of certs
607
// so test if there's something PEMy in the file at all
608
        if (filesize("$tmpDir/incomingserver.pem") > 10) {
609
            exec(CONFIG['PATHS']['openssl'] . " verify $crlCheckString -CApath $tmpDir/root-ca-eaponly/ -purpose any $tmpDir/incomingserver.pem", $verifyResultEaponly);
610
            $this->loggerInstance->debug(4, CONFIG['PATHS']['openssl'] . " verify $crlCheckString -CApath $tmpDir/root-ca-eaponly/ -purpose any $tmpDir/incomingserver.pem\n");
611
            $this->loggerInstance->debug(4, "Chain verify pass 1: " . print_r($verifyResultEaponly, TRUE) . "\n");
612
            exec(CONFIG['PATHS']['openssl'] . " verify $crlCheckString -CApath $tmpDir/root-ca-allcerts/ -purpose any $tmpDir/incomingserver.pem", $verifyResultAllcerts);
613
            $this->loggerInstance->debug(4, CONFIG['PATHS']['openssl'] . " verify $crlCheckString -CApath $tmpDir/root-ca-allcerts/ -purpose any $tmpDir/incomingserver.pem\n");
614
            $this->loggerInstance->debug(4, "Chain verify pass 2: " . print_r($verifyResultAllcerts, TRUE) . "\n");
615
        }
616
617
618
// now we do certificate verification against the collected parents
619
// this is done first for the server and then for each of the intermediate CAs
620
// any oddities observed will 
621
// openssl should havd returned exactly one line of output,
622
// and it should have ended with the string "OK", anything else is fishy
623
// The result can also be an empty array - this means there were no
624
// certificates to check. Don't complain about chain validation errors
625
// in that case.
626
// we have the following test result possibilities:
627
// 1. test against allcerts failed
628
// 2. test against allcerts succeded, but against eaponly failed - warn admin
629
// 3. test against eaponly succeded, in this case critical errors about expired certs
630
//    need to be changed to notices, since these certs obviously do tot participate
631
//    in server certificate validation.
632
        if (count($verifyResultAllcerts) == 0 || count($verifyResultEaponly) == 0) {
633
            throw new Exception("No output at all from openssl?");
634
        }
635
        if (!preg_match("/OK$/", $verifyResultAllcerts[0])) { // case 1
636
            if (preg_match("/certificate revoked$/", $verifyResultAllcerts[1])) {
637
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_SERVER_CERT_REVOKED;
638
            } elseif (preg_match("/unable to get certificate CRL/", $verifyResultAllcerts[1])) {
639
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_UNABLE_TO_GET_CRL;
640
            } else {
641
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_TRUST_ROOT_NOT_REACHED;
642
            }
643
            return 1;
644
        }
645
        if (!preg_match("/OK$/", $verifyResultEaponly[0])) { // case 2
646
            if (preg_match("/certificate revoked$/", $verifyResultEaponly[1])) {
647
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_SERVER_CERT_REVOKED;
648
            } elseif (preg_match("/unable to get certificate CRL/", $verifyResultEaponly[1])) {
649
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_UNABLE_TO_GET_CRL;
650
            } else {
651
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_TRUST_ROOT_REACHED_ONLY_WITH_OOB_INTERMEDIATES;
652
            }
653
            return 2;
654
        }
655
        return 3;
656
    }
657
658
    private function thoroughNameChecks($servercert, &$testresults) {
659
        // check the incoming hostname (both Subject:CN and subjectAltName:DNS
660
        // against what is configured in the profile; it's a significant error
661
        // if there is no match!
662
        // FAIL if none of the configured names show up in the server cert
663
        // WARN if the configured name is only in either CN or sAN:DNS
664
        // Strategy for checks: we are TOTALLY happy if any one of the
665
        // configured names shows up in both the CN and a sAN
666
        // This is the primary check.
667
        // If that was not the case, we are PARTIALLY happy if any one of
668
        // the configured names was in either of the CN or sAN lists.
669
        // we are UNHAPPY if no names match!
670
671
        $happiness = "UNHAPPY";
672
        foreach ($this->expectedServerNames as $expectedName) {
673
            $this->loggerInstance->debug(4, "Managing expectations for $expectedName: " . print_r($servercert['CN'], TRUE) . print_r($servercert['sAN_DNS'], TRUE));
674
            if (array_search($expectedName, $servercert['CN']) !== FALSE && array_search($expectedName, $servercert['sAN_DNS']) !== FALSE) {
675
                $this->loggerInstance->debug(4, "Totally happy!");
676
                $happiness = "TOTALLY";
677
                break;
678
            } else {
679
                if (array_search($expectedName, $servercert['CN']) !== FALSE || array_search($expectedName, $servercert['sAN_DNS']) !== FALSE) {
680
                    $happiness = "PARTIALLY";
681
// keep trying with other expected names! We could be happier!
682
                }
683
            }
684
        }
685
        switch ($happiness) {
686
            case "UNHAPPY":
687
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_SERVER_NAME_MISMATCH;
688
                return;
689
            case "PARTIALLY":
690
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_SERVER_NAME_PARTIAL_MATCH;
691
                return;
692
            default: // nothing to complain about!
693
                return;
694
        }
695
    }
696
697
    private function executeEapolTest($tmpDir, $probeindex, $eaptype, $innerUser, $password, $opnameCheck, $frag) {
698
        $finalInner = $innerUser;
699
        $finalOuter = $this->outerUsernameForChecks;
700
701
        $theconfigs = $this->wpaSupplicantConfig($eaptype, $finalInner, $finalOuter, $password);
702
        // the config intentionally does not include CA checking. We do this
703
        // ourselves after getting the chain with -o.
704
        file_put_contents($tmpDir . "/udp_login_test.conf", $theconfigs[0]);
705
706
        $cmdline = $this->eapolTestConfig($probeindex, $opnameCheck, $frag);
707
        $this->loggerInstance->debug(4, "Shallow reachability check cmdline: $cmdline\n");
708
        $this->loggerInstance->debug(4, "Shallow reachability check config: $tmpDir\n" . $theconfigs[1] . "\n");
709
        $time_start = microtime(true);
710
        $pflow = [];
711
        exec($cmdline, $pflow);
712
        if ($pflow === NULL) {
713
            throw new Exception("The output of an exec() call really can't be NULL!");
714
        }
715
        $time_stop = microtime(true);
716
        $this->loggerInstance->debug(5, print_r($this->redact($password, $pflow), TRUE));
717
        return [
718
            "time" => ($time_stop - $time_start) * 1000,
719
            "output" => $pflow,
720
        ];
721
    }
722
723
    private function checkRadiusPacketFlow(&$testresults, $packetflow_orig) {
724
725
        $packetflow = $this->filterPackettype($packetflow_orig);
726
727
728
// when MS-CHAPv2 allows retry, we never formally get a reject (just a 
729
// Challenge that PW was wrong but and we should try a different one; 
730
// but that effectively is a reject
731
// so change the flow results to take that into account
732
        if ($packetflow[count($packetflow) - 1] == 11 && $this->checkLineparse($packetflow_orig, self::LINEPARSE_CHECK_691)) {
733
            $packetflow[count($packetflow) - 1] = 3;
734
        }
735
// also, the ETLRs sometimes send a reject when the server is not 
736
// responding. This should not be considered a real reject; it's a middle
737
// box unduly altering the end-to-end result. Do not consider this final
738
// Reject if it comes from ETLR
739
        if ($packetflow[count($packetflow) - 1] == 3 && $this->checkLineparse($packetflow_orig, self::LINEPARSE_CHECK_REJECTIGNORE)) {
740
            array_pop($packetflow);
741
        }
742
        $this->loggerInstance->debug(5, "Packetflow: " . print_r($packetflow, TRUE));
743
        $packetcount = array_count_values($packetflow);
744
        $testresults['packetcount'] = $packetcount;
745
        $testresults['packetflow'] = $packetflow;
746
747
// calculate packet counts and see what the overall flow was
748
        return $this->packetCountEvaluation($testresults, $packetcount);
749
    }
750
751
    /**
752
     * parses the eapol_test output to determine whether we got to a point where
753
     * an EAP type was mutually agreed
754
     * 
755
     * @param array $testresults by-reference, we add our findings if something is noteworthy
756
     * @param array $packetflow_orig the array of text output from eapol_test
757
     * @return bool
758
     */
759
    private function wasEapTypeNegotiated(&$testresults, $packetflow_orig) {
760
        $testresults['cert_oddities'] = [];
761
762
        $negotiatedEapType = $this->checkLineparse($packetflow_orig, self::LINEPARSE_EAPACK);
763
        if (!$negotiatedEapType) {
764
            $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_NO_COMMON_EAP_METHOD;
765
        }
766
767
        return $negotiatedEapType;
768
    }
769
770
    const SERVER_NO_CA_EXTENSION = 1;
771
    const SERVER_CA_SELFSIGNED = 2;
772
    const CA_INTERMEDIATE = 3;
773
    const CA_ROOT = 4;
774
775
    private function determineCertificateType(&$cert, $totalCertCount) {
776
        if ($cert['ca'] == 0 && $cert['root'] == 0) {
777
            return RADIUSTests::SERVER_NO_CA_EXTENSION;
778
        }
779
        if ($cert['ca'] == 1 && $cert['root'] == 1) {
780
            if ($totalCertCount == 1) {
781
                $cert['full_details']['type'] = 'totally_selfsigned';
782
                return RADIUSTests::SERVER_CA_SELFSIGNED;
783
            } else {
784
                return RADIUSTests::CA_ROOT;
785
            }
786
        }
787
        return RADIUSTests::CA_INTERMEDIATE;
788
    }
789
790
    private function extractIncomingCertsfromEAP(&$testresults, $tmpDir) {
791
792
        /*
793
         *  EAP's house rules:
794
         * 1) it is unnecessary to include the root CA itself (adding it has
795
         *    detrimental effects on performance)
796
         * 2) TLS Web Server OID presence (Windows OSes need that)
797
         * 3) MD5 signature algorithm disallowed (iOS barks if so)
798
         * 4) CDP URL (Windows Phone 8 barks if not present)
799
         * 5) there should be exactly one server cert in the chain
800
         */
801
802
        $x509 = new \core\common\X509();
803
        $eapCertArray = [];
804
// $eap_certarray holds all certs received in EAP conversation
805
        $incomingData = file_get_contents($tmpDir . "/serverchain.pem");
806
        if ($incomingData !== FALSE) {
807
            $eapCertArray = $x509->splitCertificate($incomingData);
808
        }
809
        $eapIntermediates = [];
810
        $eapIntermediateCRLs = [];
811
        $servercert = [];
812
        $intermOdditiesEAP = [];
813
814
        $testresults['certdata'] = [];
815
816
817
        foreach ($eapCertArray as $certPem) {
818
            $cert = $x509->processCertificate($certPem);
819
            if ($cert === FALSE) {
820
                continue;
821
            }
822
// consider the certificate a server cert 
823
// a) if it is not a CA and is not a self-signed root
824
// b) if it is a CA, and self-signed, and it is the only cert in
825
//    the incoming cert chain
826
//    (meaning the self-signed is itself the server cert)
827
            switch ($this->determineCertificateType($cert, count($eapCertArray))) {
828
                case RADIUSTests::SERVER_NO_CA_EXTENSION: // both are handled same, fall-through
829
                case RADIUSTests::SERVER_CA_SELFSIGNED:
830
                    $servercert[] = $cert;
831
                    if (count($servercert) == 1) {
832
                        if (file_put_contents($tmpDir . "/incomingserver.pem", $certPem . "\n") === FALSE) {
833
                            $this->loggerInstance->debug(4, "The (first) server certificate could not be written to $tmpDir/incomingserver.pem!\n");
834
                        }
835
                        $this->loggerInstance->debug(4, "This is the (first) server certificate, with CRL content if applicable: " . print_r($servercert[0], true));
836
                    } elseif (!in_array(RADIUSTests::CERTPROB_TOO_MANY_SERVER_CERTS, $testresults['cert_oddities'])) {
837
                        $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_TOO_MANY_SERVER_CERTS;
838
                    }
839
                    break;
840
                case RADIUSTests::CA_ROOT:
841
                    if (!in_array(RADIUSTests::CERTPROB_ROOT_INCLUDED, $testresults['cert_oddities'])) {
842
                        $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_ROOT_INCLUDED;
843
                    }
844
// do not save the root CA, it serves no purpose
845
// chain checks need to be against the UPLOADED CA of the
846
// IdP/profile, not against an EAP-discovered CA
847
                    break;
848
                case RADIUSTests::CA_INTERMEDIATE:
849
                    $intermOdditiesEAP = array_merge($intermOdditiesEAP, $this->propertyCheckIntermediate($cert));
850
                    $eapIntermediates[] = $certPem;
851
852
                    if (isset($cert['CRL']) && isset($cert['CRL'][0])) {
853
                        $eapIntermediateCRLs[] = $cert['CRL'][0];
854
                    }
855
                    break;
856
                default:
857
                    throw new Exception("Status of certificate could not be determined!");
858
            }
859
            $testresults['certdata'][] = $cert['full_details'];
860
        }
861
        switch (count($servercert)) {
862
            case 0:
863
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_NO_SERVER_CERT;
864
                break;
865
            default:
866
// check (first) server cert's properties
867
                $testresults['cert_oddities'] = array_merge($testresults['cert_oddities'], $this->propertyCheckServercert($servercert[0]));
868
                $testresults['incoming_server_names'] = $servercert[0]['incoming_server_names'];
869
        }
870
        return [
871
            "SERVERCERT" => $servercert,
872
            "INTERMEDIATE_CA" => $eapIntermediates,
873
            "INTERMEDIATE_CRL" => $eapIntermediateCRLs,
874
            "INTERMEDIATE_OBSERVED_ODDITIES" => $intermOdditiesEAP,
875
        ];
876
    }
877
878
    /**
879
     * The big Guy. This performs an actual login with EAP and records how far 
880
     * it got and what oddities were observed along the way
881
     * @param int $probeindex the probe we are connecting to (as set in product config)
882
     * @param array $eaptype EAP type to use for connection
883
     * @param string $innerUser inner username to try
884
     * @param string $password password to try
885
     * @param boolean $opnameCheck whether or not we check with Operator-Name set
886
     * @param boolean $frag whether or not we check with an oversized packet forcing fragmentation
887
     * @param string $clientcertdata client certificate credential to try
888
     * @return int overall return code of the login test
889
     * @throws Exception
890
     */
891
    public function udpLogin($probeindex, $eaptype, $innerUser, $password, $opnameCheck = TRUE, $frag = TRUE, $clientcertdata = NULL) {
892
        /** preliminaries */
893
        $eapText = \core\common\EAP::eapDisplayName($eaptype);
894
        // no host to send probes to? Nothing to do then
895
        if (!isset(CONFIG_DIAGNOSTICS['RADIUSTESTS']['UDP-hosts'][$probeindex])) {
896
            $this->UDP_reachability_executed = RADIUSTests::RETVAL_NOTCONFIGURED;
897
            return RADIUSTests::RETVAL_NOTCONFIGURED;
898
        }
899
        // if we need client certs but don't have one, return
900
        if (($eaptype == \core\common\EAP::EAPTYPE_ANY || $eaptype == \core\common\EAP::EAPTYPE_TLS) && $clientcertdata === NULL) {
901
            $this->UDP_reachability_executed = RADIUSTests::RETVAL_NOTCONFIGURED;
902
            return RADIUSTests::RETVAL_NOTCONFIGURED;
903
        }
904
        // if we don't have a string for outer EAP method name, give up
905
        if (!isset($eapText['OUTER'])) {
906
            $this->UDP_reachability_executed = RADIUSTests::RETVAL_NOTCONFIGURED;
907
            return RADIUSTests::RETVAL_NOTCONFIGURED;
908
        }
909
        // we will need a config blob for wpa_supplicant, in a temporary directory
910
        $temporary = $this->createTemporaryDirectory('test');
911
        $tmpDir = $temporary['dir'];
912
        chdir($tmpDir);
913
        $this->loggerInstance->debug(4, "temp dir: $tmpDir\n");
914
        if ($clientcertdata !== NULL) {
915
            file_put_contents($tmpDir . "/client.p12", $clientcertdata);
916
        }
917
        $testresults = [];
918
        // initialise the sub-array for cleaner parsing
919
        $testresults['cert_oddities'] = [];
920
        // execute RADIUS/EAP converation
921
        $runtime_results = $this->executeEapolTest($tmpDir, $probeindex, $eaptype, $innerUser, $password, $opnameCheck, $frag);
922
        $testresults['time_millisec'] = $runtime_results['time'];
923
        $packetflow_orig = $runtime_results['output'];
924
        $radiusResult = $this->checkRadiusPacketFlow($testresults, $packetflow_orig);
925
        $negotiatedEapType = $this->wasEapTypeNegotiated($testresults, $packetflow_orig);
926
        // now let's look at the server cert+chain, if we got a cert at all
927
        // that's not the case if we do EAP-pwd or could not negotiate an EAP method at
928
        // all
929
        if (
930
                $eaptype != \core\common\EAP::EAPTYPE_PWD &&
931
                (($radiusResult == RADIUSTests::RETVAL_CONVERSATION_REJECT && $negotiatedEapType) || $radiusResult == RADIUSTests::RETVAL_OK)
932
        ) {
933
            $bundle = $this->extractIncomingCertsfromEAP($testresults, $tmpDir);
934
// FOR OWN REALMS check:
935
// 1) does the incoming chain have a root in one of the configured roots
936
//    if not, this is a signficant configuration error
937
// return this with one or more of the CERTPROB_ constants (see defs)
938
// TRUST_ROOT_NOT_REACHED
939
// TRUST_ROOT_REACHED_ONLY_WITH_OOB_INTERMEDIATES
940
// then check the presented names
941
// check intermediate ca cert properties
942
// check trust chain for completeness
943
// works only for thorough checks, not shallow, so:
944
            $intermOdditiesCAT = [];
945
            $verifyResult = 0;
946
947
            if ($this->opMode == self::RADIUS_TEST_OPERATION_MODE_THOROUGH) {
948
                $verifyResult = $this->thoroughChainChecks($testresults, $intermOdditiesCAT, $tmpDir, $bundle["SERVERCERT"], $bundle["INTERMEDIATE_CA"], $bundle["INTERMEDIATE_CRL"]);
949
                $this->thoroughNameChecks($bundle["SERVERCERT"][0], $testresults);
950
            }
951
952
            $testresults['cert_oddities'] = array_merge($testresults['cert_oddities'], $bundle["INTERMEDIATE_OBSERVED_ODDITIES"]);
953
            if (in_array(RADIUSTests::CERTPROB_OUTSIDE_VALIDITY_PERIOD, $intermOdditiesCAT) && $verifyResult == 3) {
954
                $key = array_search(RADIUSTests::CERTPROB_OUTSIDE_VALIDITY_PERIOD, $intermOdditiesCAT);
955
                $intermOdditiesCAT[$key] = RADIUSTests::CERTPROB_OUTSIDE_VALIDITY_PERIOD_WARN;
956
            }
957
958
            $testresults['cert_oddities'] = array_merge($testresults['cert_oddities'], $intermOdditiesCAT);
959
960
// mention trust chain failure only if no expired cert was in the chain; otherwise path validation will trivially fail
961
            if (in_array(RADIUSTests::CERTPROB_OUTSIDE_VALIDITY_PERIOD, $testresults['cert_oddities'])) {
962
                $this->loggerInstance->debug(4, "Deleting trust chain problem report, if present.");
963
                if (($key = array_search(RADIUSTests::CERTPROB_TRUST_ROOT_NOT_REACHED, $testresults['cert_oddities'])) !== false) {
964
                    unset($testresults['cert_oddities'][$key]);
965
                }
966
                if (($key = array_search(RADIUSTests::CERTPROB_TRUST_ROOT_REACHED_ONLY_WITH_OOB_INTERMEDIATES, $testresults['cert_oddities'])) !== false) {
967
                    unset($testresults['cert_oddities'][$key]);
968
                }
969
            }
970
        }
971
        $this->UDP_reachability_result[$probeindex] = $testresults;
972
        $this->UDP_reachability_executed = $radiusResult;
973
        return $radiusResult;
974
    }
975
976
    public function setOuterIdentity($id) {
977
        $this->outerUsernameForChecks = $id;
978
    }
979
980
    public function consolidateUdpResult($host) {
981
        $ret = [];
982
        $serverCert = [];
983
        $udpResult = $this->UDP_reachability_result[$host];
984
        if (isset($udpResult['certdata']) && count($udpResult['certdata'])) {
985
            foreach ($udpResult['certdata'] as $certdata) {
986
                if ($certdata['type'] != 'server' && $certdata['type'] != 'totally_selfsigned') {
987
                    continue;
988
                }
989
                if (isset($certdata['extensions'])) {
990
                    foreach ($certdata['extensions'] as $k => $v) {
991
                        $certdata['extensions'][$k] = iconv('UTF-8', 'UTF-8//IGNORE', $certdata['extensions'][$k]);
992
                    }
993
                }
994
                $serverCert = [
995
                    'subject' => $this->printDN($certdata['subject']),
996
                    'issuer' => $this->printDN($certdata['issuer']),
997
                    'validFrom' => $this->printTm($certdata['validFrom_time_t']),
998
                    'validTo' => $this->printTm($certdata['validTo_time_t']),
999
                    'serialNumber' => $certdata['serialNumber'] . sprintf(" (0x%X)", $certdata['serialNumber']),
1000
                    'sha1' => $certdata['sha1'],
1001
                    'extensions' => $certdata['extensions']
1002
                ];
1003
            }
1004
        }
1005
        $ret['server_cert'] = $serverCert;
1006
        $ret['server'] = 0;
1007
        if (isset($udpResult['incoming_server_names'][0])) {
1008
            $ret['server'] = sprintf(_("Connected to %s."), $udpResult['incoming_server_names'][0]);
1009
        }
1010
        $ret['level'] = \core\common\Entity::L_OK;
1011
        $ret['time_millisec'] = sprintf("%d", $udpResult['time_millisec']);
1012
        if (empty($udpResult['cert_oddities'])) {
1013
            $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.");
1014
            return $ret;
1015
        }
1016
1017
        $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.");
1018
        $ret['cert_oddities'] = [];
1019
        foreach ($udpResult['cert_oddities'] as $oddity) {
1020
            $o = [];
1021
            $o['code'] = $oddity;
1022
            $o['message'] = isset($this->returnCodes[$oddity]["message"]) && $this->returnCodes[$oddity]["message"] ? $this->returnCodes[$oddity]["message"] : $oddity;
1023
            $o['level'] = $this->returnCodes[$oddity]["severity"];
1024
            $ret['level'] = max($ret['level'], $this->returnCodes[$oddity]["severity"]);
1025
            $ret['cert_oddities'][] = $o;
1026
        }
1027
1028
        return $ret;
1029
    }
1030
1031
}
1032