Test Failed
Push — release_2_1 ( e8413e...48bff4 )
by Tomasz
18:48 queued 02:55
created

RADIUSTests::executeEapolTest()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 25
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 19
dl 0
loc 25
rs 9.6333
c 0
b 0
f 0
cc 2
nc 2
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
37
38
/**
39
 * Test suite to verify that an EAP setup is actually working as advertised in
40
 * the real world. Can only be used if \config\Diagnostics::RADIUSTESTS is configured.
41
 *
42
 * @author Stefan Winter <[email protected]>
43
 * @author Tomasz Wolniewicz <[email protected]>
44
 *
45
 * @license see LICENSE file in root directory
46
 *
47
 * @package Developer
48
 */
49
class RADIUSTests extends AbstractTest {
50
51
    /**
52
     * Was the reachability check executed already?
53
     * 
54
     * @var integer
55
     */
56
    private $UDP_reachability_executed;
57
58
    /**
59
     * the issues we found
60
     * 
61
     * @var array 
62
     */
63
    private $errorlist;
64
65
    /**
66
     * This private variable contains the realm to be checked. Is filled in the
67
     * class constructor.
68
     * 
69
     * @var string
70
     */
71
    private $realm;
72
73
    /**
74
     * which username to use as outer identity
75
     * 
76
     * @var string
77
     */
78
    private $outerUsernameForChecks;
79
80
    /**
81
     * list of CAs we expect incoming server certs to be from
82
     * 
83
     * @var array
84
     */
85
    private $expectedCABundle;
86
87
    /**
88
     * list of expected server names
89
     * 
90
     * @var array
91
     */
92
    private $expectedServerNames;
93
94
    /**
95
     * the list of EAP types which the IdP allegedly supports.
96
     * 
97
     * @var array
98
     */
99
    private $supportedEapTypes;
100
101
    /**
102
     * Do we run thorough or shallow checks?
103
     * 
104
     * @var integer
105
     */
106
    private $opMode;
107
108
    /**
109
     * result of the reachability tests
110
     * 
111
     * @var array
112
     */
113
    public $UDP_reachability_result;
114
115
    const RADIUS_TEST_OPERATION_MODE_SHALLOW = 1;
116
    const RADIUS_TEST_OPERATION_MODE_THOROUGH = 2;
117
118
    /**
119
     * Constructor for the EAPTests class. The single mandatory parameter is the
120
     * realm for which the tests are to be carried out.
121
     * 
122
     * @param string $realm                  the realm to check
123
     * @param string $outerUsernameForChecks outer username to use
124
     * @param array  $supportedEapTypes      array of integer representations of EAP types
125
     * @param array  $expectedServerNames    array of strings
126
     * @param array  $expectedCABundle       array of PEM blocks
127
     * @throws Exception
128
     */
129
    public function __construct($realm, $outerUsernameForChecks, $supportedEapTypes = [], $expectedServerNames = [], $expectedCABundle = []) {
130
        parent::__construct();
131
132
        $this->realm = $realm;
133
        $this->outerUsernameForChecks = $outerUsernameForChecks;
134
        $this->expectedCABundle = $expectedCABundle;
135
        $this->expectedServerNames = $expectedServerNames;
136
        $this->supportedEapTypes = $supportedEapTypes;
137
138
        $this->opMode = self::RADIUS_TEST_OPERATION_MODE_SHALLOW;
139
140
        $caNeeded = FALSE;
141
        $serverNeeded = FALSE;
142
        foreach ($supportedEapTypes as $oneEapType) {
143
            if ($oneEapType->needsServerCACert()) {
144
                $caNeeded = TRUE;
145
            }
146
            if ($oneEapType->needsServerName()) {
147
                $serverNeeded = TRUE;
148
            }
149
        }
150
151
        if ($caNeeded) {
152
            // we need to have info about at least one CA cert and server names
153
            if (count($this->expectedCABundle) == 0) {
154
                throw new Exception("Thorough checks for an EAP type needing CAs were requested, but the required parameters were not given.");
155
            } else {
156
                $this->opMode = self::RADIUS_TEST_OPERATION_MODE_THOROUGH;
157
            }
158
        }
159
160
        if ($serverNeeded) {
161
            if (count($this->expectedServerNames) == 0) {
162
                throw new Exception("Thorough checks for an EAP type needing server names were requested, but the required parameter was not given.");
163
            } else {
164
                $this->opMode = self::RADIUS_TEST_OPERATION_MODE_THOROUGH;
165
            }
166
        }
167
168
        $this->loggerInstance->debug(4, "RADIUSTests is in opMode " . $this->opMode . ", parameters were: $realm, $outerUsernameForChecks, " . /** @scrutinizer ignore-type */ print_r($supportedEapTypes, true));
169
        $this->loggerInstance->debug(4, /** @scrutinizer ignore-type */ print_r($expectedServerNames, true));
170
        $this->loggerInstance->debug(4, /** @scrutinizer ignore-type */ print_r($expectedCABundle, true));
171
172
        $this->UDP_reachability_result = [];
173
        $this->errorlist = [];
174
    }
175
176
    /**
177
     * creates a string with the DistinguishedName (comma-separated name=value fields)
178
     * 
179
     * @param array $distinguishedName the components of the DN
180
     * @return string
181
     */
182
    private function printDN($distinguishedName) {
183
        $out = '';
184
        foreach (array_reverse($distinguishedName) as $nameType => $nameValue) { // to give an example: "CN" => "some.host.example" 
185
            if (!is_array($nameValue)) { // single-valued: just a string
186
                $nameValue = ["$nameValue"]; // convert it to a multi-value attrib with just one value :-) for unified processing later on
187
            }
188
            foreach ($nameValue as $oneValue) {
189
                if ($out) {
190
                    $out .= ',';
191
                }
192
                $out .= "$nameType=$oneValue";
193
            }
194
        }
195
        return($out);
196
    }
197
198
    /**
199
     * prints a timestamp in gmdate formatting
200
     * 
201
     * @param int $time time in UNIX timestamp
202
     * @return string
203
     */
204
    private function printTm($time) {
205
        return(gmdate(\DateTime::COOKIE, $time));
206
    }
207
208
    /**
209
     * This function parses a X.509 server cert and checks if it finds client device incompatibilities
210
     * 
211
     * @param array $servercert the properties of the certificate as returned by
212
     *                          processCertificate(), $servercert is modified, 
213
     *                          if CRL is defied, it is downloaded and added to
214
     *                          the array incoming_server_names, sAN_DNS and CN 
215
     *                          array values are also defined
216
     * @return array of oddities; the array is empty if everything is fine
217
     */
218
    private function propertyCheckServercert(&$servercert) {
219
// we share the same checks as for CAs when it comes to signature algorithm and basicconstraints
220
// so call that function and memorise the outcome
221
        $returnarray = $this->propertyCheckIntermediate($servercert, TRUE);
222
        $sANdns = [];
223
        if (!isset($servercert['full_details']['extensions'])) {
224
            $returnarray[] = RADIUSTests::CERTPROB_NO_TLS_WEBSERVER_OID;
225
            $returnarray[] = RADIUSTests::CERTPROB_NO_CDP_HTTP;
226
        } else { // Extensions are present...
227
            if (!isset($servercert['full_details']['extensions']['extendedKeyUsage']) || !preg_match("/TLS Web Server Authentication/", $servercert['full_details']['extensions']['extendedKeyUsage'])) {
228
                $returnarray[] = RADIUSTests::CERTPROB_NO_TLS_WEBSERVER_OID;
229
            }
230
            if (isset($servercert['full_details']['extensions']['subjectAltName'])) {
231
                $sANlist = explode(", ", $servercert['full_details']['extensions']['subjectAltName']);
232
                foreach ($sANlist as $subjectAltName) {
233
                    if (preg_match("/^DNS:/", $subjectAltName)) {
234
                        $sANdns[] = substr($subjectAltName, 4);
235
                    }
236
                }
237
            }
238
        }
239
240
        // often, there is only one name, so we store it in an array of one member
241
        $commonName = [$servercert['full_details']['subject']['CN']];
242
        // if we got an array of names instead, then that is already an array, so override
243
        if (isset($servercert['full_details']['subject']['CN']) && is_array($servercert['full_details']['subject']['CN'])) {
244
            $commonName = $servercert['full_details']['subject']['CN'];
245
            $returnarray[] = RADIUSTests::CERTPROB_MULTIPLE_CN;
246
        }
247
        $allnames = array_values(array_unique(array_merge($commonName, $sANdns)));
248
// check for wildcards
249
// check for real hostnames, and whether there is a wildcard in a name
250
        foreach ($allnames as $onename) {
251
            if (preg_match("/\*/", $onename)) {
252
                $returnarray[] = RADIUSTests::CERTPROB_WILDCARD_IN_NAME;
253
                continue; // otherwise we'd ALSO complain that it's not a real hostname
254
            }
255
            if ($onename != "" && filter_var("foo@" . idn_to_ascii($onename), FILTER_VALIDATE_EMAIL) === FALSE) {
256
                $returnarray[] = RADIUSTests::CERTPROB_NOT_A_HOSTNAME;
257
            }
258
        }
259
        $servercert['incoming_server_names'] = $allnames;
260
        $servercert['sAN_DNS'] = $sANdns;
261
        $servercert['CN'] = $commonName;
262
        return $returnarray;
263
    }
264
265
    /**
266
     * This function parses a X.509 intermediate CA cert and checks if it finds client device incompatibilities
267
     * 
268
     * @param array   $intermediateCa the properties of the certificate as returned by processCertificate()
269
     * @param boolean $serverCert     treat as servercert?
270
     * @return array of oddities; the array is empty if everything is fine
271
     */
272
    private function propertyCheckIntermediate(&$intermediateCa, $serverCert = FALSE) {
273
        $returnarray = [];
274
        if (preg_match("/md5/i", $intermediateCa['full_details']['signatureTypeSN'])) {
275
            $returnarray[] = RADIUSTests::CERTPROB_MD5_SIGNATURE;
276
        }
277
        if (preg_match("/sha1/i", $intermediateCa['full_details']['signatureTypeSN'])) {
278
            $probValue = RADIUSTests::CERTPROB_SHA1_SIGNATURE;
279
            $returnarray[] = $probValue;
280
        }
281
        $this->loggerInstance->debug(4, "CERT IS: " . /** @scrutinizer ignore-type */ print_r($intermediateCa, TRUE));
282
        if ($intermediateCa['basicconstraints_set'] == 0) {
283
            $returnarray[] = RADIUSTests::CERTPROB_NO_BASICCONSTRAINTS;
284
        }
285
        if ($intermediateCa['full_details']['public_key_algorithm'] == \core\common\X509::KNOWN_PUBLIC_KEY_ALGORITHMS[0] && $intermediateCa['full_details']['public_key_length'] < 2048) {
286
            $returnarray[] = RADIUSTests::CERTPROB_LOW_KEY_LENGTH;
287
        }
288
        if (!in_array($intermediateCa['full_details']['public_key_algorithm'], \core\common\X509::KNOWN_PUBLIC_KEY_ALGORITHMS)) {
289
            $returnarray[] = RADIUSTests::CERTPROB_UNKNOWN_PUBLIC_KEY_ALGORITHM;
290
        }
291
        $validFrom = $intermediateCa['full_details']['validFrom_time_t'];
292
        $now = time();
293
        $validTo = $intermediateCa['full_details']['validTo_time_t'];
294
        if ($validFrom > $now || $validTo < $now) {
295
            $returnarray[] = RADIUSTests::CERTPROB_OUTSIDE_VALIDITY_PERIOD;
296
        }
297
        $addCertCrlResult = $this->addCrltoCert($intermediateCa);
298
        if ($addCertCrlResult !== 0 && $serverCert) {
299
            $returnarray[] = $addCertCrlResult;
300
        }
301
302
        return $returnarray;
303
    }
304
305
    /**
306
     * This function returns an array of errors which were encountered in all the tests.
307
     * 
308
     * @return array all the errors
309
     */
310
    public function listerrors() {
311
        return $this->errorlist;
312
    }
313
314
    /**
315
     * This function performs actual authentication checks with MADE-UP credentials.
316
     * Its purpose is to check if a RADIUS server is reachable and speaks EAP.
317
     * The function fills array RADIUSTests::UDP_reachability_result[$probeindex] with all check detail
318
     * in case more than the return code is needed/wanted by the caller
319
     * 
320
     * @param int     $probeindex  refers to the specific UDP-host in the config that should be checked
321
     * @param boolean $opnameCheck should we check choking on Operator-Name?
322
     * @param boolean $frag        should we cause UDP fragmentation? (Warning: makes use of Operator-Name!)
323
     * @return integer returncode
324
     * @throws Exception
325
     */
326
    public function udpReachability($probeindex, $opnameCheck = TRUE, $frag = TRUE) {
327
        // for EAP-TLS to be a viable option, we need to pass a random client cert to make eapol_test happy
328
        // the following PEM data is one of the SENSE EAPLab client certs (not secret at all)
329
        $clientcert = file_get_contents(dirname(__FILE__) . "/clientcert.p12");
330
        if ($clientcert === FALSE) {
331
            throw new Exception("A dummy client cert is part of the source distribution, but could not be loaded!");
332
        }
333
        // if we are in thorough opMode, use our knowledge for a more clever check
334
        // otherwise guess
335
        if ($this->opMode == self::RADIUS_TEST_OPERATION_MODE_THOROUGH) {
336
            return $this->udpLogin($probeindex, $this->supportedEapTypes[0]->getArrayRep(), $this->outerUsernameForChecks, 'eaplab', $opnameCheck, $frag, $clientcert);
337
        }
338
        return $this->udpLogin($probeindex, \core\common\EAP::EAPTYPE_ANY, "cat-connectivity-test@" . $this->realm, 'eaplab', $opnameCheck, $frag, $clientcert);
339
    }
340
341
    /**
342
     * There is a CRL Distribution Point URL in the certificate. So download the
343
     * CRL and attach it to the cert structure so that we can later find out if
344
     * the cert was revoked
345
     * @param array $cert by-reference: the cert data we are writing into
346
     * @return integer result code whether we were successful in retrieving the CRL
347
     * @throws Exception
348
     */
349
    private function addCrltoCert(&$cert) {
350
        $crlUrl = [];
351
        $returnresult = 0;
352
        if (!isset($cert['full_details']['extensions']['crlDistributionPoints'])) {
353
            return RADIUSTests::CERTPROB_NO_CDP;
354
        }
355
        if (!preg_match("/^.*URI\:(http)(.*)$/", str_replace(["\r", "\n"], ' ', $cert['full_details']['extensions']['crlDistributionPoints']), $crlUrl)) {
356
            return RADIUSTests::CERTPROB_NO_CDP_HTTP;
357
        }
358
        // first and second sub-match is the full URL... check it
359
        $crlcontent = \core\common\OutsideComm::downloadFile(trim($crlUrl[1] . $crlUrl[2]), \config\Diagnostics::TIMEOUTS['crl_download']);
360
        if ($crlcontent === FALSE) {
361
            return RADIUSTests::CERTPROB_NO_CRL_AT_CDP_URL;
362
        }
363
        /* CRLs are always in DER form, so need encoding
364
         * note that what we ACTUALLY got can be arbitrary junk; we just deposit
365
         * it on the filesystem and let openssl figure out if it is usable or not
366
         *
367
         * Unfortunately, that freaks out Scrutinizer because we write unvetted
368
         * data to the filesystem. Let's see if we can make things better.
369
         */
370
371
        // $pem = chunk_split(base64_encode($crlcontent), 64, "\n");
372
        // inspired by https://stackoverflow.com/questions/2390604/how-to-pass-variables-as-stdin-into-command-line-from-php
373
374
        $proc = \config\Master::PATHS['openssl'] . " crl -inform der";
375
        $descriptorspec = [
376
            0 => ["pipe", "r"],
377
            1 => ["pipe", "w"],
378
            2 => ["pipe", "w"],
379
        ];
380
        $process = proc_open($proc, $descriptorspec, $pipes);
381
        if (!is_resource($process)) {
382
            throw new Exception("Unable to execute openssl cmdline for CRL conversion!");
383
        }
384
        fwrite($pipes[0], $crlcontent);
385
        fclose($pipes[0]);
386
        $pem = stream_get_contents($pipes[1]);
387
        fclose($pipes[1]);
388
        fclose($pipes[2]);
389
        $retval = proc_close($process);
390
        if ($retval != 0 || !preg_match("/BEGIN X509 CRL/", $pem)) {
391
            // this was not a real CRL
392
            return RADIUSTests::CERTPROB_NO_CRL_AT_CDP_URL;
393
        }
394
        $cert['CRL'] = [];
395
        $cert['CRL'][] = $pem;
396
        return $returnresult;
397
    }
398
399
    /**
400
     * We don't want to write passwords of the live login test to our logs. Filter them out
401
     * @param string $stringToRedact what should be redacted
402
     * @param array  $inputarray     array of strings (outputs of eapol_test command)
403
     * @return string[] the output of eapol_test with the password redacted
404
     */
405
    private function redact($stringToRedact, $inputarray) {
406
        $temparray = preg_replace("/^.*$stringToRedact.*$/", "LINE CONTAINING PASSWORD REDACTED", $inputarray);
407
        $hex = bin2hex($stringToRedact);
408
        $spaced = "";
409
        $origLength = strlen($hex);
410
        for ($i = 1; $i < $origLength; $i++) {
411
            if ($i % 2 == 1 && $i != strlen($hex)) {
412
                $spaced .= $hex[$i] . " ";
413
            } else {
414
                $spaced .= $hex[$i];
415
            }
416
        }
417
        return preg_replace("/$spaced/", " HEX ENCODED PASSWORD REDACTED ", $temparray);
418
    }
419
420
    /**
421
     * Filters eapol_test output and finds out the packet codes out of which the conversation was comprised of
422
     * 
423
     * @param array $inputarray array of strings (outputs of eapol_test command)
424
     * @return array the packet codes which were exchanged, in sequence
425
     */
426
    private function filterPackettype($inputarray) {
427
        $retarray = [];
428
        foreach ($inputarray as $line) {
429
            if (preg_match("/RADIUS message:/", $line)) {
430
                $linecomponents = explode(" ", $line);
431
                $packettypeExploded = explode("=", $linecomponents[2]);
432
                $packettype = $packettypeExploded[1];
433
                $retarray[] = $packettype;
434
            }
435
        }
436
        return $retarray;
437
    }
438
439
    const LINEPARSE_CHECK_REJECTIGNORE = 1;
440
    const LINEPARSE_CHECK_691 = 2;
441
    const LINEPARSE_EAPACK = 3;
442
    const LINEPARSE_TLSVERSION = 4;
443
    const TLS_VERSION_ANCIENT = "OTHER";
444
    const TLS_VERSION_1_0 = "TLSv1";
445
    const TLS_VERSION_1_1 = "TLSv1.1";
446
    const TLS_VERSION_1_2 = "TLSv1.2";
447
    const TLS_VERSION_1_3 = "TLSv1.3";
448
449
    /**
450
     * this function checks for various special conditions which can be found 
451
     * only by parsing eapol_test output line by line. Checks currently 
452
     * implemented are:
453
     * * if the ETLRs sent back an Access-Reject because there appeared to
454
     *   be a timeout further downstream
455
     * * did the server send an MSCHAP Error 691 - Retry Allowed in a Challenge
456
     *   instead of an outright reject?
457
     * * was an EAP method ever acknowledged by both sides during the EAP
458
     *   conversation
459
     * 
460
     * @param array   $inputarray   array of strings (outputs of eapol_test command)
461
     * @param integer $desiredCheck which test should be run (see constants above)
462
     * @return boolean|string returns TRUE if ETLR Reject logic was detected; FALSE if not; strings are returned for TLS versions
463
     * @throws Exception
464
     */
465
    private function checkLineparse($inputarray, $desiredCheck) {
466
        foreach ($inputarray as $lineid => $line) {
467
            switch ($desiredCheck) {
468
                case self::LINEPARSE_CHECK_REJECTIGNORE:
469
                    if (preg_match("/Attribute 18 (Reply-Message)/", $line) && preg_match("/Reject instead of Ignore at eduroam.org/", $inputarray[$lineid + 1])) {
470
                        return TRUE;
471
                    }
472
                    break;
473
                case self::LINEPARSE_CHECK_691:
474
                    if (preg_match("/MSCHAPV2: error 691/", $line) && preg_match("/MSCHAPV2: retry is allowed/", $inputarray[$lineid + 1])) {
475
                        return TRUE;
476
                    }
477
                    break;
478
                case self::LINEPARSE_EAPACK:
479
                    if (preg_match("/CTRL-EVENT-EAP-PROPOSED-METHOD/", $line) && !preg_match("/NAK$/", $line)) {
480
                        return TRUE;
481
                    }
482
                    break;
483
                case self::LINEPARSE_TLSVERSION:
484
                    break;
485
                default:
486
                    throw new Exception("This lineparse test does not exist.");
487
            }
488
        }
489
        // for TLS version checks, we need to search from bottom to top 
490
        // eapol_test will always try its highest version first, and can be
491
        // persuaded later on to do less. So look at the end result.
492
        for ($counter = count($inputarray); $counter > 0; $counter--) {
493
            switch ($desiredCheck) {
494
                case self::LINEPARSE_TLSVERSION:
495
                    $version = [];
496
                    if (isset($inputarray[$counter]) && preg_match("/Using TLS version (.*)$/", $inputarray[$counter], $version)) {
497
                        switch (trim($version[1])) {
498
                            case self::TLS_VERSION_1_3:
499
                                return self::TLS_VERSION_1_3;
500
                            case self::TLS_VERSION_1_2:
501
                                return self::TLS_VERSION_1_2;
502
                            case self::TLS_VERSION_1_1:
503
                                return self::TLS_VERSION_1_1;
504
                            case self::TLS_VERSION_1_0:
505
                                return self::TLS_VERSION_1_0;
506
                            default:
507
                                return self::TLS_VERSION_ANCIENT;
508
                        }
509
                    }
510
                    break;
511
                case self::LINEPARSE_CHECK_691:
512
                /* fall-through intentional */
513
                case self::LINEPARSE_CHECK_REJECTIGNORE:
514
                /* fall-through intentional */
515
                case self::LINEPARSE_EAPACK:
516
                    /* fall-through intentional */
517
                    break;
518
                default:
519
                    throw new Exception("This lineparse test does not exist.");
520
            }
521
        }
522
        return FALSE;
523
    }
524
525
    /**
526
     * 
527
     * @param array  $eaptype  array representation of the EAP type
528
     * @param string $inner    inner username
529
     * @param string $outer    outer username
530
     * @param string $password the password
531
     * @return string[] [0] is the actual config for wpa_supplicant, [1] is a redacted version for logs
532
     */
533
    private function wpaSupplicantConfig(array $eaptype, string $inner, string $outer, string $password) {
534
        $eapText = \core\common\EAP::eapDisplayName($eaptype);
535
        $config = '
536
network={
537
  ssid="' . \config\Master::APPEARANCE['productname'] . ' testing"
538
  key_mgmt=WPA-EAP
539
  proto=WPA2
540
  pairwise=CCMP
541
  group=CCMP
542
  ';
543
// phase 1
544
        $config .= 'eap=' . $eapText['OUTER'] . "\n";
545
        $logConfig = $config;
546
// phase 2 if applicable; all inner methods have passwords
547
        if (isset($eapText['INNER']) && $eapText['INNER'] != "") {
548
            $config .= '  phase2="auth=' . $eapText['INNER'] . "\"\n";
549
            $logConfig .= '  phase2="auth=' . $eapText['INNER'] . "\"\n";
550
        }
551
// all methods set a password, except EAP-TLS
552
        if ($eaptype != \core\common\EAP::EAPTYPE_TLS) {
553
            $config .= "  password=\"$password\"\n";
554
            $logConfig .= "  password=\"not logged for security reasons\"\n";
555
        }
556
// for methods with client certs, add a client cert config block
557
        if ($eaptype == \core\common\EAP::EAPTYPE_TLS || $eaptype == \core\common\EAP::EAPTYPE_ANY) {
558
            $config .= "  private_key=\"./client.p12\"\n";
559
            $logConfig .= "  private_key=\"./client.p12\"\n";
560
            $config .= "  private_key_passwd=\"$password\"\n";
561
            $logConfig .= "  private_key_passwd=\"not logged for security reasons\"\n";
562
        }
563
564
// inner identity
565
        $config .= '  identity="' . $inner . "\"\n";
566
        $logConfig .= '  identity="' . $inner . "\"\n";
567
// outer identity, may be equal
568
        $config .= '  anonymous_identity="' . $outer . "\"\n";
569
        $logConfig .= '  anonymous_identity="' . $outer . "\"\n";
570
// done
571
        $config .= "}";
572
        $logConfig .= "}";
573
574
        return [$config, $logConfig];
575
    }
576
577
    /**
578
     * Checks whether the packets received are as expected in numbers
579
     * 
580
     * @param array $testresults by-reference array of the testresults so far
581
     *                           function adds its own findings to that array
582
     * @param array $packetcount the count of incoming packets
583
     * @return int
584
     */
585
    private function packetCountEvaluation(&$testresults, $packetcount) {
586
        $reqs = $packetcount[1] ?? 0;
587
        $accepts = $packetcount[2] ?? 0;
588
        $rejects = $packetcount[3] ?? 0;
589
        $challenges = $packetcount[11] ?? 0;
590
        $testresults['packetflow_sane'] = TRUE;
591
        if ($reqs - $accepts - $rejects - $challenges != 0 || $accepts > 1 || $rejects > 1) {
592
            $testresults['packetflow_sane'] = FALSE;
593
        }
594
595
        $this->loggerInstance->debug(5, "XYZ: Counting req, acc, rej, chal: $reqs, $accepts, $rejects, $challenges");
596
597
// calculate the main return values that this test yielded
598
599
        $finalretval = RADIUSTests::RETVAL_INVALID;
600
        if ($accepts + $rejects == 0) { // no final response. hm.
601
            if ($challenges > 0) { // but there was an Access-Challenge
602
                $finalretval = RADIUSTests::RETVAL_SERVER_UNFINISHED_COMM;
603
            } else {
604
                $finalretval = RADIUSTests::RETVAL_NO_RESPONSE;
605
            }
606
        } else // either an accept or a reject
607
// rejection without EAP is fishy
608
        if ($rejects > 0) {
609
            if ($challenges == 0) {
610
                $finalretval = RADIUSTests::RETVAL_IMMEDIATE_REJECT;
611
            } else { // i.e. if rejected with challenges
612
                $finalretval = RADIUSTests::RETVAL_CONVERSATION_REJECT;
613
            }
614
        } else if ($accepts > 0) {
615
            $finalretval = RADIUSTests::RETVAL_OK;
616
        }
617
618
        return $finalretval;
619
    }
620
621
    /**
622
     * generate an eapol_test command-line config for the fixed config filename 
623
     * ./udp_login_test.conf
624
     * @param int     $probeindex number of the probe to check against
625
     * @param boolean $opName     include Operator-Name in request?
626
     * @param boolean $frag       make request so large that fragmentation is needed?
627
     * @return string the command-line for eapol_test
628
     */
629
    private function eapolTestConfig($probeindex, $opName, $frag) {
630
        $cmdline = \config\Diagnostics::PATHS['eapol_test'] .
631
                " -a " . \config\Diagnostics::RADIUSTESTS['UDP-hosts'][$probeindex]['ip'] .
632
                " -s " . \config\Diagnostics::RADIUSTESTS['UDP-hosts'][$probeindex]['secret'] .
633
                " -o serverchain.pem" .
634
                " -c ./udp_login_test.conf" .
635
                " -M 22:44:66:CA:20:" . sprintf("%02d", $probeindex) . " " .
636
                " -t " . \config\Diagnostics::RADIUSTESTS['UDP-hosts'][$probeindex]['timeout'] . " ";
637
        if ($opName) {
638
            $cmdline .= '-N126:s:"1cat.eduroam.org" ';
639
        }
640
        if ($frag) {
641
            for ($i = 0; $i < 6; $i++) { // 6 x 250 bytes means UDP fragmentation will occur - good!
642
                $cmdline .= '-N26:x:0000625A0BF961616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161 ';
643
            }
644
        }
645
        return $cmdline;
646
    }
647
648
    /**
649
     * collects CA certificates, both from the incoming EAP chain and from CAT
650
     * config. Writes the root CAs into a trusted root CA dir and intermediate 
651
     * and first server cert into a PEM file for later chain validation
652
     * 
653
     * @param string $tmpDir              working directory
654
     * @param array  $intermOdditiesCAT   by-reference array of already found 
655
     *                                    oddities; adds its own
656
     * @param array  $servercert          the servercert to validate
657
     * @param array  $eapIntermediates    list of intermediate CA certs that came
658
     *                                    in via EAP
659
     * @param array  $eapIntermediateCRLs list of CRLs for the EAP-supplied
660
     *                                    intermediate CAs
661
     * @return string
662
     * @throws Exception
663
     */
664
    private function createCArepository($tmpDir, &$intermOdditiesCAT, $servercert, $eapIntermediates, $eapIntermediateCRLs) {
665
        if (!mkdir($tmpDir . "/root-ca-allcerts/", 0700, true)) {
666
            throw new Exception("unable to create root CA directory (RADIUS Tests): $tmpDir/root-ca-allcerts/\n");
667
        }
668
        if (!mkdir($tmpDir . "/root-ca-eaponly/", 0700, true)) {
669
            throw new Exception("unable to create root CA directory (RADIUS Tests): $tmpDir/root-ca-eaponly/\n");
670
        }
671
// make a copy of the EAP-received chain and add the configured intermediates, if any
672
        $catIntermediates = [];
673
        $catRoots = [];
674
        foreach ($this->expectedCABundle as $oneCA) {
675
            $x509 = new \core\common\X509();
676
            $decoded = $x509->processCertificate($oneCA);
677
            if (is_bool($decoded)) {
678
                throw new Exception("Unable to parse an expected CA certificate.");
679
            }
680
            if ($decoded['ca'] == 1) {
681
                if ($decoded['root'] == 1) { // save CAT roots to the root directory
682
                    file_put_contents($tmpDir . "/root-ca-eaponly/configuredroot" . count($catRoots) . ".pem", $decoded['pem']);
683
                    file_put_contents($tmpDir . "/root-ca-allcerts/configuredroot" . count($catRoots) . ".pem", $decoded['pem']);
684
                    $catRoots[] = $decoded['pem'];
685
                } else { // save the intermediates to allcerts directory
686
                    file_put_contents($tmpDir . "/root-ca-allcerts/cat-intermediate" . count($catIntermediates) . ".pem", $decoded['pem']);
687
                    $intermOdditiesCAT = array_merge($intermOdditiesCAT, $this->propertyCheckIntermediate($decoded));
688
                    if (isset($decoded['CRL']) && isset($decoded['CRL'][0])) {
689
                        $this->loggerInstance->debug(4, "got an intermediate CRL; adding them to the chain checks. (Remember: checking end-entity cert only, not the whole chain");
690
                        file_put_contents($tmpDir . "/root-ca-allcerts/crl_cat" . count($catIntermediates) . ".pem", $decoded['CRL'][0]);
691
                    }
692
                    $catIntermediates[] = $decoded['pem'];
693
                }
694
            }
695
        }
696
        // save all intermediate certificates and CRLs to separate files in 
697
        // both root-ca directories
698
        foreach ($eapIntermediates as $index => $onePem) {
699
            file_put_contents($tmpDir . "/root-ca-eaponly/intermediate$index.pem", $onePem);
0 ignored issues
show
Security File Manipulation introduced by
$onePem can contain request data and is used in file manipulation context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_REQUEST, and InputValidation::existingProfile() is called
    in web/diag/radius_tests.php on line 70
  2. Enters via parameter $input
    in web/lib/common/InputValidation.php on line 195
  3. core\diag\RADIUSTests::CERTPROB_MD5_SIGNATURE is assigned to $returnarray
    in core/diag/RADIUSTests.php on line 275
  4. $returnarray is returned
    in core/diag/RADIUSTests.php on line 302
  5. Data is passed through array_merge(), and array_merge($intermOdditiesEAP, $this->propertyCheckIntermediate($cert)) is assigned to $intermOdditiesEAP
    in core/diag/RADIUSTests.php on line 1061
  6. array('SERVERCERT' => $servercert, 'INTERMEDIATE_CA' => $eapIntermediates, 'INTERMEDIATE_CRL' => $eapIntermediateCRLs, 'INTERMEDIATE_OBSERVED_ODDITIES' => $intermOdditiesEAP, 'UNTRUSTED_ROOT_INCLUDED' => $rootIncluded) is returned
    in core/diag/RADIUSTests.php on line 1082
  7. $this->extractIncomingCertsfromEAP($testresults, $tmpDir) is assigned to $bundle
    in core/diag/RADIUSTests.php on line 1259
  8. RADIUSTests::thoroughChainChecks() is called
    in core/diag/RADIUSTests.php on line 1274
  9. Enters via parameter $eapIntermediates
    in core/diag/RADIUSTests.php on line 739
  10. RADIUSTests::createCArepository() is called
    in core/diag/RADIUSTests.php on line 741
  11. Enters via parameter $eapIntermediates
    in core/diag/RADIUSTests.php on line 664
  12. $eapIntermediates is assigned to $onePem
    in core/diag/RADIUSTests.php on line 698

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
700
            file_put_contents($tmpDir . "/root-ca-allcerts/intermediate$index.pem", $onePem);
0 ignored issues
show
Security File Manipulation introduced by
$onePem can contain request data and is used in file manipulation context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_REQUEST, and InputValidation::existingProfile() is called
    in web/diag/radius_tests.php on line 70
  2. Enters via parameter $input
    in web/lib/common/InputValidation.php on line 195
  3. core\diag\RADIUSTests::CERTPROB_MD5_SIGNATURE is assigned to $returnarray
    in core/diag/RADIUSTests.php on line 275
  4. $returnarray is returned
    in core/diag/RADIUSTests.php on line 302
  5. Data is passed through array_merge(), and array_merge($intermOdditiesEAP, $this->propertyCheckIntermediate($cert)) is assigned to $intermOdditiesEAP
    in core/diag/RADIUSTests.php on line 1061
  6. array('SERVERCERT' => $servercert, 'INTERMEDIATE_CA' => $eapIntermediates, 'INTERMEDIATE_CRL' => $eapIntermediateCRLs, 'INTERMEDIATE_OBSERVED_ODDITIES' => $intermOdditiesEAP, 'UNTRUSTED_ROOT_INCLUDED' => $rootIncluded) is returned
    in core/diag/RADIUSTests.php on line 1082
  7. $this->extractIncomingCertsfromEAP($testresults, $tmpDir) is assigned to $bundle
    in core/diag/RADIUSTests.php on line 1259
  8. RADIUSTests::thoroughChainChecks() is called
    in core/diag/RADIUSTests.php on line 1274
  9. Enters via parameter $eapIntermediates
    in core/diag/RADIUSTests.php on line 739
  10. RADIUSTests::createCArepository() is called
    in core/diag/RADIUSTests.php on line 741
  11. Enters via parameter $eapIntermediates
    in core/diag/RADIUSTests.php on line 664
  12. $eapIntermediates is assigned to $onePem
    in core/diag/RADIUSTests.php on line 698

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
701
        }
702
        foreach ($eapIntermediateCRLs as $index => $onePem) {
703
            file_put_contents($tmpDir . "/root-ca-eaponly/intermediateCRL$index.pem", $onePem);
0 ignored issues
show
Security File Manipulation introduced by
$onePem can contain request data and is used in file manipulation context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_REQUEST, and InputValidation::existingProfile() is called
    in web/diag/radius_tests.php on line 70
  2. Enters via parameter $input
    in web/lib/common/InputValidation.php on line 195
  3. core\diag\RADIUSTests::CERTPROB_MD5_SIGNATURE is assigned to $returnarray
    in core/diag/RADIUSTests.php on line 275
  4. $returnarray is returned
    in core/diag/RADIUSTests.php on line 302
  5. Data is passed through array_merge(), and array_merge($intermOdditiesEAP, $this->propertyCheckIntermediate($cert)) is assigned to $intermOdditiesEAP
    in core/diag/RADIUSTests.php on line 1061
  6. array('SERVERCERT' => $servercert, 'INTERMEDIATE_CA' => $eapIntermediates, 'INTERMEDIATE_CRL' => $eapIntermediateCRLs, 'INTERMEDIATE_OBSERVED_ODDITIES' => $intermOdditiesEAP, 'UNTRUSTED_ROOT_INCLUDED' => $rootIncluded) is returned
    in core/diag/RADIUSTests.php on line 1082
  7. $this->extractIncomingCertsfromEAP($testresults, $tmpDir) is assigned to $bundle
    in core/diag/RADIUSTests.php on line 1259
  8. RADIUSTests::thoroughChainChecks() is called
    in core/diag/RADIUSTests.php on line 1274
  9. Enters via parameter $eapIntermediateCRLs
    in core/diag/RADIUSTests.php on line 739
  10. RADIUSTests::createCArepository() is called
    in core/diag/RADIUSTests.php on line 741
  11. Enters via parameter $eapIntermediateCRLs
    in core/diag/RADIUSTests.php on line 664
  12. $eapIntermediateCRLs is assigned to $onePem
    in core/diag/RADIUSTests.php on line 702

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
704
            file_put_contents($tmpDir . "/root-ca-allcerts/intermediateCRL$index.pem", $onePem);
0 ignored issues
show
Security File Manipulation introduced by
$onePem can contain request data and is used in file manipulation context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_REQUEST, and InputValidation::existingProfile() is called
    in web/diag/radius_tests.php on line 70
  2. Enters via parameter $input
    in web/lib/common/InputValidation.php on line 195
  3. core\diag\RADIUSTests::CERTPROB_MD5_SIGNATURE is assigned to $returnarray
    in core/diag/RADIUSTests.php on line 275
  4. $returnarray is returned
    in core/diag/RADIUSTests.php on line 302
  5. Data is passed through array_merge(), and array_merge($intermOdditiesEAP, $this->propertyCheckIntermediate($cert)) is assigned to $intermOdditiesEAP
    in core/diag/RADIUSTests.php on line 1061
  6. array('SERVERCERT' => $servercert, 'INTERMEDIATE_CA' => $eapIntermediates, 'INTERMEDIATE_CRL' => $eapIntermediateCRLs, 'INTERMEDIATE_OBSERVED_ODDITIES' => $intermOdditiesEAP, 'UNTRUSTED_ROOT_INCLUDED' => $rootIncluded) is returned
    in core/diag/RADIUSTests.php on line 1082
  7. $this->extractIncomingCertsfromEAP($testresults, $tmpDir) is assigned to $bundle
    in core/diag/RADIUSTests.php on line 1259
  8. RADIUSTests::thoroughChainChecks() is called
    in core/diag/RADIUSTests.php on line 1274
  9. Enters via parameter $eapIntermediateCRLs
    in core/diag/RADIUSTests.php on line 739
  10. RADIUSTests::createCArepository() is called
    in core/diag/RADIUSTests.php on line 741
  11. Enters via parameter $eapIntermediateCRLs
    in core/diag/RADIUSTests.php on line 664
  12. $eapIntermediateCRLs is assigned to $onePem
    in core/diag/RADIUSTests.php on line 702

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
705
        }
706
707
        $checkstring = "";
708
        if (isset($servercert['CRL']) && isset($servercert['CRL'][0])) {
709
            $this->loggerInstance->debug(4, "got a server CRL; adding them to the chain checks. (Remember: checking end-entity cert only, not the whole chain");
710
            $checkstring = "-crl_check_all";
711
            file_put_contents($tmpDir . "/root-ca-eaponly/crl-server.pem", $servercert['CRL'][0]);
0 ignored issues
show
Security File Manipulation introduced by
$servercert['CRL'][0] can contain request data and is used in file manipulation context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_REQUEST, and InputValidation::existingProfile() is called
    in web/diag/radius_tests.php on line 70
  2. Enters via parameter $input
    in web/lib/common/InputValidation.php on line 195
  3. core\diag\RADIUSTests::CERTPROB_MD5_SIGNATURE is assigned to $returnarray
    in core/diag/RADIUSTests.php on line 275
  4. $returnarray is returned
    in core/diag/RADIUSTests.php on line 302
  5. Data is passed through array_merge(), and array_merge($intermOdditiesEAP, $this->propertyCheckIntermediate($cert)) is assigned to $intermOdditiesEAP
    in core/diag/RADIUSTests.php on line 1061
  6. array('SERVERCERT' => $servercert, 'INTERMEDIATE_CA' => $eapIntermediates, 'INTERMEDIATE_CRL' => $eapIntermediateCRLs, 'INTERMEDIATE_OBSERVED_ODDITIES' => $intermOdditiesEAP, 'UNTRUSTED_ROOT_INCLUDED' => $rootIncluded) is returned
    in core/diag/RADIUSTests.php on line 1082
  7. $this->extractIncomingCertsfromEAP($testresults, $tmpDir) is assigned to $bundle
    in core/diag/RADIUSTests.php on line 1259
  8. RADIUSTests::thoroughChainChecks() is called
    in core/diag/RADIUSTests.php on line 1274
  9. Enters via parameter $servercert
    in core/diag/RADIUSTests.php on line 739
  10. RADIUSTests::createCArepository() is called
    in core/diag/RADIUSTests.php on line 741
  11. Enters via parameter $servercert
    in core/diag/RADIUSTests.php on line 664

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
712
            file_put_contents($tmpDir . "/root-ca-allcerts/crl-server.pem", $servercert['CRL'][0]);
0 ignored issues
show
Security File Manipulation introduced by
$servercert['CRL'][0] can contain request data and is used in file manipulation context(s) leading to a potential security vulnerability.

1 path for user data to reach this point

  1. Read from $_REQUEST, and InputValidation::existingProfile() is called
    in web/diag/radius_tests.php on line 70
  2. Enters via parameter $input
    in web/lib/common/InputValidation.php on line 195
  3. core\diag\RADIUSTests::CERTPROB_MD5_SIGNATURE is assigned to $returnarray
    in core/diag/RADIUSTests.php on line 275
  4. $returnarray is returned
    in core/diag/RADIUSTests.php on line 302
  5. Data is passed through array_merge(), and array_merge($intermOdditiesEAP, $this->propertyCheckIntermediate($cert)) is assigned to $intermOdditiesEAP
    in core/diag/RADIUSTests.php on line 1061
  6. array('SERVERCERT' => $servercert, 'INTERMEDIATE_CA' => $eapIntermediates, 'INTERMEDIATE_CRL' => $eapIntermediateCRLs, 'INTERMEDIATE_OBSERVED_ODDITIES' => $intermOdditiesEAP, 'UNTRUSTED_ROOT_INCLUDED' => $rootIncluded) is returned
    in core/diag/RADIUSTests.php on line 1082
  7. $this->extractIncomingCertsfromEAP($testresults, $tmpDir) is assigned to $bundle
    in core/diag/RADIUSTests.php on line 1259
  8. RADIUSTests::thoroughChainChecks() is called
    in core/diag/RADIUSTests.php on line 1274
  9. Enters via parameter $servercert
    in core/diag/RADIUSTests.php on line 739
  10. RADIUSTests::createCArepository() is called
    in core/diag/RADIUSTests.php on line 741
  11. Enters via parameter $servercert
    in core/diag/RADIUSTests.php on line 664

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
713
        }
714
715
716
// now c_rehash the root CA directory ...
717
        system(\config\Diagnostics::PATHS['c_rehash'] . " $tmpDir/root-ca-eaponly/ > /dev/null");
718
        system(\config\Diagnostics::PATHS['c_rehash'] . " $tmpDir/root-ca-allcerts/ > /dev/null");
719
        return $checkstring;
720
    }
721
722
    /**
723
     * for checks which have a known trust root CA (i.e. a valid CAT profile 
724
     * exists), check against those known-good roots
725
     * 
726
     * @param array  $testresults         by-reference list of testresults so far
727
     *                                    Function adds its own.
728
     * @param array  $intermOdditiesCAT   by-reference list of oddities in the CA
729
     *                                    certs which are configured in CAT
730
     * @param string $tmpDir              working directory
731
     * @param array  $servercert          the server certificate to validate
732
     * @param array  $eapIntermediates    list of intermediate CA certs that came
733
     *                                    in via EAP
734
     * @param array  $eapIntermediateCRLs list of CRLs for the EAP-supplied
735
     *                                    intermediate CAs
736
     * @return int
737
     * @throws Exception
738
     */
739
    private function thoroughChainChecks(&$testresults, &$intermOdditiesCAT, $tmpDir, $servercert, $eapIntermediates, $eapIntermediateCRLs) {
740
741
        $crlCheckString = $this->createCArepository($tmpDir, $intermOdditiesCAT, $servercert, $eapIntermediates, $eapIntermediateCRLs);
742
// ... and run the verification test
743
        $verifyResultEaponly = [];
744
        $verifyResultAllcerts = [];
745
// the error log will complain if we run this test against an empty file of certs
746
// so test if there's something PEMy in the file at all
747
// serverchain.pem is the output from eapol_test; incomingserver.pem is written by extractIncomingCertsfromEAP() if there was at least one server cert.
748
        if (filesize("$tmpDir/serverchain.pem") > 10 && filesize("$tmpDir/incomingserver.pem") > 10) {
749
            $cmdString = \config\Master::PATHS['openssl'] . " verify $crlCheckString  -no-CAstore -no-CApath -CApath $tmpDir/root-ca-eaponly/ -purpose any $tmpDir/incomingserver.pem 2>&1";
750
            exec($cmdString, $verifyResultEaponly);
751
            $this->loggerInstance->debug(4, $cmdString."\n");
752
            $this->loggerInstance->debug(4, "Chain verify pass 1: " . /** @scrutinizer ignore-type */ print_r($verifyResultEaponly, TRUE) . "\n");
753
            $cmdString = \config\Master::PATHS['openssl'] . " verify $crlCheckString  -no-CAstore -no-CApath -CApath $tmpDir/root-ca-allcerts/ -purpose any $tmpDir/incomingserver.pem 2>&1";
754
            exec($cmdString, $verifyResultAllcerts);
755
            $this->loggerInstance->debug(4, $cmdString."\n");
756
            $this->loggerInstance->debug(4, "Chain verify pass 2: " . /** @scrutinizer ignore-type */ print_r($verifyResultAllcerts, TRUE) . "\n");
757
        }
758
759
// now we do certificate verification against the collected parents
760
// this is done first for the server and then for each of the intermediate CAs
761
// any oddities observed will 
762
// openssl should havd returned exactly one line of output,
763
// and it should have ended with the string "OK", anything else is fishy
764
// The result can also be an empty array - this means there were no
765
// certificates to check. Don't complain about chain validation errors
766
// in that case.
767
// we have the following test result possibilities:
768
// 1. test against allcerts failed
769
// 2. test against allcerts succeeded, but against eaponly failed - warn admin
770
// 3. test against eaponly succeeded, in this case critical errors about expired certs
771
//    need to be changed to notices, since these certs obviously do tot participate
772
//    in server certificate validation.
773
        if (count($verifyResultAllcerts) == 0 || count($verifyResultEaponly) == 0) {
774
            throw new Exception("No output at all from openssl?");
775
        }
776
        if (!isset($testresults['cert_oddities'])) {
777
            $testresults['cert_oddities'] = array();
778
        }
779
        if (!preg_match("/OK$/", $verifyResultAllcerts[0])) { // case 1
780
            if (preg_match("/certificate revoked$/", $verifyResultAllcerts[1])) {
781
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_SERVER_CERT_REVOKED;
782
            } elseif (preg_match("/unable to get certificate CRL/", $verifyResultAllcerts[1])) {
783
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_UNABLE_TO_GET_CRL;
784
            } else {
785
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_TRUST_ROOT_NOT_REACHED;
786
            }
787
            return 1;
788
        }
789
        if (!preg_match("/OK$/", $verifyResultEaponly[0])) { // case 2
790
            if (preg_match("/certificate revoked$/", $verifyResultEaponly[1])) {
791
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_SERVER_CERT_REVOKED;
792
            } elseif (preg_match("/unable to get certificate CRL/", $verifyResultEaponly[1])) {
793
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_UNABLE_TO_GET_CRL;
794
            } else {
795
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_TRUST_ROOT_REACHED_ONLY_WITH_OOB_INTERMEDIATES;
796
            }
797
            return 2;
798
        }
799
        return 3;
800
    }
801
802
    /**
803
     * check the incoming hostname (both Subject:CN and subjectAltName:DNS
804
     * against what is configured in the profile; it's a significant error
805
     * if there is no match!
806
     * 
807
     * FAIL if none of the configured names show up in the server cert
808
     * WARN if the configured name is only in either CN or sAN:DNS
809
     * 
810
     * @param array $servercert  the server certificate to check
811
     * @param array $testresults by-reference the existing testresults. Function
812
     *                           adds its own findings.
813
     * @return void
814
     */
815
    private function thoroughNameChecks($servercert, &$testresults) {
816
        // Strategy for checks: we are TOTALLY happy if any one of the
817
        // configured names shows up in both the CN and a sAN
818
        // This is the primary check.
819
        // If that was not the case, we are PARTIALLY happy if any one of
820
        // the configured names was in either of the CN or sAN lists.
821
        // we are UNHAPPY if no names match!
822
        $happiness = "UNHAPPY";
823
        foreach ($this->expectedServerNames as $expectedName) {
824
            $this->loggerInstance->debug(4, "Managing expectations for $expectedName: " . /** @scrutinizer ignore-type */ print_r($servercert['CN'], TRUE) . /** @scrutinizer ignore-type */ print_r($servercert['sAN_DNS'], TRUE));
825
            if (array_search($expectedName, $servercert['CN']) !== FALSE && array_search($expectedName, $servercert['sAN_DNS']) !== FALSE) {
826
                $this->loggerInstance->debug(4, "Totally happy!");
827
                $happiness = "TOTALLY";
828
                break;
829
            } else {
830
                if (array_search($expectedName, $servercert['CN']) !== FALSE || array_search($expectedName, $servercert['sAN_DNS']) !== FALSE) {
831
                    $happiness = "PARTIALLY";
832
// keep trying with other expected names! We could be happier!
833
                }
834
            }
835
        }
836
        switch ($happiness) {
837
            case "UNHAPPY":
838
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_SERVER_NAME_MISMATCH;
839
                return;
840
            case "PARTIALLY":
841
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_SERVER_NAME_PARTIAL_MATCH;
842
                return;
843
            default: // nothing to complain about!
844
                return;
845
        }
846
    }
847
848
    /**
849
     * run eapol_test
850
     * 
851
     * @param string  $tmpDir      working directory
852
     * @param int     $probeindex  number of the probe this test should run through
853
     * @param array   $eaptype     EAP type in array representation
854
     * @param string  $innerUser   EAP method inner username to use
855
     * @param string  $password    password to use
856
     * @param boolean $opnameCheck inject Operator-Name?
857
     * @param boolean $frag        provoke UDP fragmentation?
858
     * @return array timing information of the executed eapol_test run
859
     * @throws Exception
860
     */
861
    private function executeEapolTest($tmpDir, $probeindex, $eaptype, $outerUser, $innerUser, $password, $opnameCheck, $frag) {
862
        $finalInner = $innerUser;
863
        $finalOuter = $outerUser;
864
865
        $theconfigs = $this->wpaSupplicantConfig($eaptype, $finalInner, $finalOuter, $password);
866
        // the config intentionally does not include CA checking. We do this
867
        // ourselves after getting the chain with -o.
868
        file_put_contents($tmpDir . "/udp_login_test.conf", $theconfigs[0]);
869
870
        $cmdline = $this->eapolTestConfig($probeindex, $opnameCheck, $frag);
871
        $this->loggerInstance->debug(4, "Shallow reachability check cmdline: $cmdline\n");
872
        $this->loggerInstance->debug(4, "Shallow reachability check config: $tmpDir\n" . $theconfigs[1] . "\n");
873
        $time_start = microtime(true);
874
        $pflow = [];
875
        exec($cmdline, $pflow);
876
        if ($pflow === NULL) {
877
            throw new Exception("The output of an exec() call really can't be NULL!");
878
        }
879
        $time_stop = microtime(true);
880
        $output = print_r($this->redact($password, $pflow), TRUE);
881
        file_put_contents($tmpDir . "/eapol_test_output_redacted_$probeindex.txt", $output);
882
        $this->loggerInstance->debug(5, "eapol_test output saved to eapol_test_output_redacted_$probeindex.txt\n");
883
        return [
884
            "time" => ($time_stop - $time_start) * 1000,
885
            "output" => $pflow,
886
        ];
887
    }
888
889
    /**
890
     * checks if the RADIUS packets were coming in in the order they are 
891
     * expected. The function massages the raw result for some known oddities.
892
     * 
893
     * @param array $testresults     by-reference array of test results so far.
894
     *                               function adds its own.
895
     * @param array $packetflow_orig original flow of packets
896
     * @return int
897
     */
898
    private function checkRadiusPacketFlow(&$testresults, $packetflow_orig) {
899
900
        $packetflow = $this->filterPackettype($packetflow_orig);
901
902
// when MS-CHAPv2 allows retry, we never formally get a reject (just a 
903
// Challenge that PW was wrong but and we should try a different one; 
904
// but that effectively is a reject
905
// so change the flow results to take that into account
906
        if ($packetflow[count($packetflow) - 1] == 11 && $this->checkLineparse($packetflow_orig, self::LINEPARSE_CHECK_691)) {
907
            $packetflow[count($packetflow) - 1] = 3;
908
        }
909
// also, the ETLRs sometimes send a reject when the server is not 
910
// responding. This should not be considered a real reject; it's a middle
911
// box unduly altering the end-to-end result. Do not consider this final
912
// Reject if it comes from ETLR
913
        if ($packetflow[count($packetflow) - 1] == 3 && $this->checkLineparse($packetflow_orig, self::LINEPARSE_CHECK_REJECTIGNORE)) {
914
            array_pop($packetflow);
915
        }
916
        $this->loggerInstance->debug(5, "Packetflow: " . /** @scrutinizer ignore-type */ print_r($packetflow, TRUE));
917
        $packetcount = array_count_values($packetflow);
918
        $testresults['packetcount'] = $packetcount;
919
        $testresults['packetflow'] = $packetflow;
920
921
// calculate packet counts and see what the overall flow was
922
        return $this->packetCountEvaluation($testresults, $packetcount);
923
    }
924
925
    /**
926
     * parses the eapol_test output to determine whether we got to a point where
927
     * an EAP type was mutually agreed
928
     * 
929
     * @param array $testresults     by-reference, we add our findings if 
930
     *                               something is noteworthy
931
     * @param array $packetflow_orig the array of text output from eapol_test
932
     * @return bool
933
     * @throws Exception
934
     */
935
    private function wasEapTypeNegotiated(&$testresults, $packetflow_orig) {
936
        $negotiatedEapType = $this->checkLineparse($packetflow_orig, self::LINEPARSE_EAPACK);
937
        if (!is_bool($negotiatedEapType)) {
938
            throw new Exception("checkLineparse should only ever return a boolean in this case!");
939
        }
940
        if (!$negotiatedEapType) {
941
            $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_NO_COMMON_EAP_METHOD;
942
        }
943
944
        return $negotiatedEapType;
945
    }
946
947
    /**
948
     * parses eapol_test to find the TLS version used during the EAP conversation
949
     * @param array $testresults     by-reference, we add our findings if 
950
     *                               something is noteworthy
951
     * @param array $packetflow_orig the array of text output from eapol_test
952
     * @return string|bool the version as a string or FALSE if TLS version could not be determined
953
     */
954
    private function wasModernTlsNegotiated(&$testresults, $packetflow_orig) {
955
        $negotiatedTlsVersion = $this->checkLineparse($packetflow_orig, self::LINEPARSE_TLSVERSION);
956
        $this->loggerInstance->debug(4, "TLS version found is: $negotiatedTlsVersion" . "\n");
957
        if ($negotiatedTlsVersion === FALSE) {
958
            $testresults['cert_oddities'][] = RADIUSTests::TLSPROB_UNKNOWN_TLS_VERSION;
959
        } elseif ($negotiatedTlsVersion != self::TLS_VERSION_1_2 && $negotiatedTlsVersion != self::TLS_VERSION_1_3) {
960
            $testresults['cert_oddities'][] = RADIUSTests::TLSPROB_DEPRECATED_TLS_VERSION;
961
        }
962
963
        return $negotiatedTlsVersion;
964
    }
965
966
    const SERVER_NO_CA_EXTENSION = 1;
967
    const SERVER_CA_SELFSIGNED = 2;
968
    const CA_INTERMEDIATE = 3;
969
    const CA_ROOT = 4;
970
971
    /**
972
     * what is the incoming certificate - root, intermediate, or server?
973
     * @param array $cert           the certificate to check
974
     * @param int   $totalCertCount number of certs in total in chain
975
     * @return int
976
     */
977
    private function determineCertificateType(&$cert, $totalCertCount) {
978
        if ($cert['ca'] == 0 && $cert['root'] == 0) {
979
            return RADIUSTests::SERVER_NO_CA_EXTENSION;
980
        }
981
        if ($cert['ca'] == 1 && $cert['root'] == 1) {
982
            if ($totalCertCount == 1) {
983
                $cert['full_details']['type'] = 'totally_selfsigned';
984
                return RADIUSTests::SERVER_CA_SELFSIGNED;
985
            } else {
986
                return RADIUSTests::CA_ROOT;
987
            }
988
        }
989
        return RADIUSTests::CA_INTERMEDIATE;
990
    }
991
992
    /**
993
     * pull out the certificates that were sent during the EAP conversation
994
     * 
995
     * @param array  $testresults by-reference, add our findings if any
996
     * @param string $tmpDir      working directory
997
     * @return array|FALSE an array with all the certs, CRLs and oddities, or FALSE if the EAP conversation did not yield a certificate at all
998
     * @throws Exception
999
     */
1000
    private function extractIncomingCertsfromEAP(&$testresults, $tmpDir) {
1001
        /*
1002
         *  EAP's house rules:
1003
         * 1) it is unnecessary to include the root CA itself (adding it has
1004
         *    detrimental effects on performance)
1005
         * 2) TLS Web Server OID presence (Windows OSes need that)
1006
         * 3) MD5 signature algorithm disallowed (iOS barks if so)
1007
         * 4) CDP URL (Windows Phone 8 barks if not present)
1008
         * 5) there should be exactly one server cert in the chain
1009
         */
1010
1011
        $x509 = new \core\common\X509();
1012
// $eap_certarray holds all certs received in EAP conversation
1013
        $incomingData = file_get_contents($tmpDir . "/serverchain.pem");
1014
        if ($incomingData !== FALSE && strlen($incomingData) > 0) {
1015
            $eapCertArray = $x509->splitCertificate($incomingData);
1016
        } else {
1017
            $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_NO_CERTIFICATE_IN_CONVERSATION;
1018
            return FALSE;
1019
        }
1020
        $rootIncluded = [];
1021
        $eapIntermediates = [];
1022
        $eapIntermediateCRLs = [];
1023
        $servercert = [];
1024
        $intermOdditiesEAP = [];
1025
1026
        $testresults['certdata'] = [];
1027
1028
        foreach ($eapCertArray as $certPem) {
1029
            $cert = $x509->processCertificate($certPem);
1030
            if ($cert === FALSE) {
1031
                continue;
1032
            }
1033
// consider the certificate a server cert 
1034
// a) if it is not a CA and is not a self-signed root
1035
// b) if it is a CA, and self-signed, and it is the only cert in
1036
//    the incoming cert chain
1037
//    (meaning the self-signed is itself the server cert)
1038
            switch ($this->determineCertificateType($cert, count($eapCertArray))) {
1039
                case RADIUSTests::SERVER_NO_CA_EXTENSION: // both are handled same, fall-through
1040
                case RADIUSTests::SERVER_CA_SELFSIGNED:
1041
                    $servercert[] = $cert;
1042
                    if (count($servercert) == 1) {
1043
                        if (file_put_contents($tmpDir . "/incomingserver.pem", $cert['pem'] . "\n") === FALSE) {
1044
                            $this->loggerInstance->debug(4, "The (first) server certificate could not be written to $tmpDir/incomingserver.pem!\n");
1045
                        }
1046
                        $this->loggerInstance->debug(4, "This is the (first) server certificate, with CRL content if applicable: " . /** @scrutinizer ignore-type */ print_r($servercert[0], true));
1047
                    } elseif (!in_array(RADIUSTests::CERTPROB_TOO_MANY_SERVER_CERTS, $testresults['cert_oddities'])) {
1048
                        $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_TOO_MANY_SERVER_CERTS;
1049
                    }
1050
                    break;
1051
                case RADIUSTests::CA_ROOT:
1052
                    if (!in_array(RADIUSTests::CERTPROB_ROOT_INCLUDED, $testresults['cert_oddities'])) {
1053
                        $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_ROOT_INCLUDED;
1054
                    }
1055
// chain checks need to be against the UPLOADED CA of the
1056
// IdP/profile, not against an EAP-discovered CA
1057
                    // save it anyway, but only for feature "root CA autodetection" is executed
1058
                    $rootIncluded[] = $cert['pem'];
1059
                    break;
1060
                case RADIUSTests::CA_INTERMEDIATE:
1061
                    $intermOdditiesEAP = array_merge($intermOdditiesEAP, $this->propertyCheckIntermediate($cert));
1062
                    $eapIntermediates[] = $cert['pem'];
1063
1064
                    if (isset($cert['CRL']) && isset($cert['CRL'][0])) {
1065
                        $eapIntermediateCRLs[] = $cert['CRL'][0];
1066
                    }
1067
                    break;
1068
                default:
1069
                    throw new Exception("Status of certificate could not be determined!");
1070
            }
1071
            $testresults['certdata'][] = $cert['full_details'];
1072
        }
1073
        switch (count($servercert)) {
1074
            case 0:
1075
                $testresults['cert_oddities'][] = RADIUSTests::CERTPROB_NO_SERVER_CERT;
1076
                break;
1077
            default:
1078
// check (first) server cert's properties
1079
                $testresults['cert_oddities'] = array_merge($testresults['cert_oddities'], $this->propertyCheckServercert($servercert[0]));
1080
                $testresults['incoming_server_names'] = $servercert[0]['incoming_server_names'];
1081
        }
1082
        return [
1083
            "SERVERCERT" => $servercert,
1084
            "INTERMEDIATE_CA" => $eapIntermediates,
1085
            "INTERMEDIATE_CRL" => $eapIntermediateCRLs,
1086
            "INTERMEDIATE_OBSERVED_ODDITIES" => $intermOdditiesEAP,
1087
            "UNTRUSTED_ROOT_INCLUDED" => $rootIncluded,
1088
        ];
1089
    }
1090
1091
    private function udpLoginPreliminaries($probeindex, $eaptype, $clientcertdata) {
1092
        /** preliminaries */
1093
        $eapText = \core\common\EAP::eapDisplayName($eaptype);
1094
        // no host to send probes to? Nothing to do then
1095
        if (!isset(\config\Diagnostics::RADIUSTESTS['UDP-hosts'][$probeindex])) {
1096
            $this->UDP_reachability_executed = RADIUSTests::RETVAL_NOTCONFIGURED;
1097
            return RADIUSTests::RETVAL_NOTCONFIGURED;
1098
        }
1099
1100
        // if we need client certs but don't have one, return
1101
        if (($eaptype == \core\common\EAP::EAPTYPE_ANY || $eaptype == \core\common\EAP::EAPTYPE_TLS) && $clientcertdata === NULL) {
1102
            $this->UDP_reachability_executed = RADIUSTests::RETVAL_NOTCONFIGURED;
1103
            return RADIUSTests::RETVAL_NOTCONFIGURED;
1104
        }
1105
        // if we don't have a string for outer EAP method name, give up
1106
        if (!isset($eapText['OUTER'])) {
1107
            $this->UDP_reachability_executed = RADIUSTests::RETVAL_NOTCONFIGURED;
1108
            return RADIUSTests::RETVAL_NOTCONFIGURED;
1109
        }
1110
        return TRUE;
1111
    }
1112
1113
    public function autodetectCAWithProbe($outerId) {
1114
        // for EAP-TLS to be a viable option, we need to pass a random client cert to make eapol_test happy
1115
        // the following PEM data is one of the SENSE EAPLab client certs (not secret at all)
1116
        $clientcert = file_get_contents(dirname(__FILE__) . "/clientcert.p12");
1117
        if ($clientcert === FALSE) {
1118
            throw new Exception("A dummy client cert is part of the source distribution, but could not be loaded!");
1119
        }
1120
        // which probe should we use? First is probably okay...
1121
        $probeindex = 0;
1122
        $preliminaries = $this->udpLoginPreliminaries($probeindex, \core\common\EAP::EAPTYPE_ANY, $clientcert);
1123
        if ($preliminaries !== TRUE) {
1124
            return $preliminaries;
1125
        }
1126
        // we will need a config blob for wpa_supplicant, in a temporary directory
1127
        $temporary = \core\common\Entity::createTemporaryDirectory('test');
1128
        $tmpDir = $temporary['dir'];
1129
        chdir($tmpDir);
1130
        $this->loggerInstance->debug(4, "temp dir: $tmpDir\n");
1131
        file_put_contents($tmpDir . "/client.p12", $clientcert);
1132
        $testresults = ['cert_oddities' => []];
1133
        $runtime_results = $this->executeEapolTest($tmpDir, $probeindex, \core\common\EAP::EAPTYPE_ANY, $outerId, $outerId, "eaplab", FALSE, FALSE);
1134
        $packetflow_orig = $runtime_results['output'];
1135
        $radiusResult = $this->checkRadiusPacketFlow($testresults, $packetflow_orig);
1136
        $negotiatedEapType = FALSE;
1137
        if ($radiusResult != RADIUSTests::RETVAL_IMMEDIATE_REJECT) {
1138
            $negotiatedEapType = $this->wasEapTypeNegotiated($testresults, $packetflow_orig);
1139
            $testresults['negotiated_eaptype'] = $negotiatedEapType;
1140
            $negotiatedTlsVersion = $this->wasModernTlsNegotiated($testresults, $packetflow_orig);
1141
            $testresults['tls_version_eap'] = $negotiatedTlsVersion;
1142
        }
1143
        // now let's look at the server cert+chain, if we got a cert at all
1144
        // that's not the case if we do EAP-pwd or could not negotiate an EAP method at
1145
        // all
1146
        // in that case: no server CA guess possible
1147
        if (!
1148
                ($radiusResult == RADIUSTests::RETVAL_CONVERSATION_REJECT && $negotiatedEapType) || $radiusResult == RADIUSTests::RETVAL_OK
1149
        ) {
1150
            return RADIUSTests::RETVAL_INVALID;
1151
        }
1152
        $bundle = $this->extractIncomingCertsfromEAP($testresults, $tmpDir);
1153
        // we need to check if we know the issuer of the server cert
1154
        // assume we have only one server cert - anything else is a 
1155
        // misconfiguration on the EAP server side
1156
        $previousHighestKnownIssuer = [];
1157
        $currentHighestKnownIssuer = $bundle['SERVERCERT'][0]['full_details']['issuer'];
1158
        $serverName = $bundle['SERVERCERT'][0]['CN'][0];
1159
        // maybe there is an intermediate and the EAP server sent it. If so,
1160
        // go and look at that, going one level higher
1161
        $x509 = new \core\common\X509();
1162
        $allCACerts = array_merge($bundle['INTERMEDIATE_CA'], $bundle['UNTRUSTED_ROOT_INCLUDED']);
1163
        while ($previousHighestKnownIssuer != $currentHighestKnownIssuer) {
1164
            $previousHighestKnownIssuer = $currentHighestKnownIssuer;
1165
            foreach ($allCACerts as $oneCACert) {
1166
                $certDetails = $x509->processCertificate($oneCACert);
1167
                if ($certDetails['full_details']['subject'] == $previousHighestKnownIssuer) {
1168
                    $currentHighestKnownIssuer = $certDetails['full_details']['issuer'];
1169
                }
1170
                if ($certDetails['full_details']['subject'] == $certDetails['full_details']['issuer']) {
1171
                    // if we see a subject == issuer, then the EAP server even
1172
                    // sent a root certificate. We'll propose that then.
1173
                    return [
1174
                        "NAME" => $serverName,
1175
                        "INTERMEDIATE_CA" => $bundle['INTERMEDIATE_CA'],
1176
                        "HIGHEST_ISSUER" => $currentHighestKnownIssuer,
1177
                        "ROOT_CA" => $certDetails['pem'],
1178
                    ];
1179
                }
1180
            }
1181
        }
1182
        // we now know the "highest" issuer name we got from the EAP 
1183
        // conversation - ideally the name of a root CA we know. Let's look at 
1184
        // our own system store to get a list of all commercial CAs with browser
1185
        // trust, and custom ones we may have configured
1186
        $ourRoots = file_get_contents(\config\ConfAssistant::PATHS['trust-store-custom']);
1187
        $mozillaRoots = file_get_contents(\config\ConfAssistant::PATHS['trust-store-mozilla']);
1188
        $allRoots = $x509->splitCertificate($ourRoots . "\n" . $mozillaRoots);
1189
        foreach ($allRoots as $oneRoot) {
1190
            $processedRoot = $x509->processCertificate($oneRoot);
1191
            if ($processedRoot['full_details']['subject'] == $currentHighestKnownIssuer) {
1192
                return [
1193
            "NAME" => $serverName,
1194
            "INTERMEDIATE_CA" => $bundle['INTERMEDIATE_CA'],
1195
            "HIGHEST_ISSUER" => $currentHighestKnownIssuer,
1196
            "ROOT_CA" => $oneRoot,
1197
        ];
1198
            }
1199
        }
1200
        return [
1201
            "NAME" => $serverName,
1202
            "INTERMEDIATE_CA" => $bundle['INTERMEDIATE_CA'],
1203
            "HIGHEST_ISSUER" => $currentHighestKnownIssuer,
1204
            "ROOT_CA" => NULL,
1205
        ];
1206
    }
1207
1208
    /**
1209
     * The big Guy. This performs an actual login with EAP and records how far 
1210
     * it got and what oddities were observed along the way
1211
     * @param int     $probeindex     the probe we are connecting to (as set in product config)
1212
     * @param array   $eaptype        EAP type to use for connection
1213
     * @param string  $innerUser      inner username to try
1214
     * @param string  $password       password to try
1215
     * @param boolean $opnameCheck    whether or not we check with Operator-Name set
1216
     * @param boolean $frag           whether or not we check with an oversized packet forcing fragmentation
1217
     * @param string  $clientcertdata client certificate credential to try
1218
     * @return int overall return code of the login test
1219
     * @throws Exception
1220
     */
1221
    public function udpLogin($probeindex, $eaptype, $innerUser, $password, $opnameCheck = TRUE, $frag = TRUE, $clientcertdata = NULL) {
1222
        $preliminaries = $this->udpLoginPreliminaries($probeindex, $eaptype, $clientcertdata);
1223
        if ($preliminaries !== TRUE) {
1224
            return $preliminaries;
1225
        }
1226
        // we will need a config blob for wpa_supplicant, in a temporary directory
1227
        $temporary = \core\common\Entity::createTemporaryDirectory('test');
1228
        $tmpDir = $temporary['dir'];
1229
        chdir($tmpDir);
1230
        $this->loggerInstance->debug(4, "temp dir: $tmpDir\n");
1231
        if ($clientcertdata !== NULL) {
1232
            file_put_contents($tmpDir . "/client.p12", $clientcertdata);
1233
        }
1234
        $testresults = [];
1235
        // initialise the sub-array for cleaner parsing
1236
        $testresults['cert_oddities'] = [];
1237
        // execute RADIUS/EAP conversation
1238
        $runtime_results = $this->executeEapolTest($tmpDir, $probeindex, $eaptype, $this->outerUsernameForChecks, $innerUser, $password, $opnameCheck, $frag);
1239
        $testresults['time_millisec'] = $runtime_results['time'];
1240
        $packetflow_orig = $runtime_results['output'];
1241
        $radiusResult = $this->checkRadiusPacketFlow($testresults, $packetflow_orig);
1242
        // if the RADIUS conversation was immediately rejected, it is trivially
1243
        // true that no EAP type was negotiated, and that TLS didn't negotiate
1244
        // a version. Don't get excited about that then.
1245
        $negotiatedEapType = FALSE;
1246
        if ($radiusResult != RADIUSTests::RETVAL_IMMEDIATE_REJECT) {
1247
            $negotiatedEapType = $this->wasEapTypeNegotiated($testresults, $packetflow_orig);
1248
            $testresults['negotiated_eaptype'] = $negotiatedEapType;
1249
            $negotiatedTlsVersion = $this->wasModernTlsNegotiated($testresults, $packetflow_orig);
1250
            $testresults['tls_version_eap'] = $negotiatedTlsVersion;
1251
        }
1252
        // now let's look at the server cert+chain, if we got a cert at all
1253
        // that's not the case if we do EAP-pwd or could not negotiate an EAP method at
1254
        // all
1255
        if (
1256
                $eaptype != \core\common\EAP::EAPTYPE_PWD &&
1257
                (($radiusResult == RADIUSTests::RETVAL_CONVERSATION_REJECT && $negotiatedEapType) || $radiusResult == RADIUSTests::RETVAL_OK)
1258
        ) {
1259
            $bundle = $this->extractIncomingCertsfromEAP($testresults, $tmpDir);
1260
// FOR OWN REALMS check:
1261
// 1) does the incoming chain have a root in one of the configured roots
1262
//    if not, this is a significant configuration error
1263
// return this with one or more of the CERTPROB_ constants (see defs)
1264
// TRUST_ROOT_NOT_REACHED
1265
// TRUST_ROOT_REACHED_ONLY_WITH_OOB_INTERMEDIATES
1266
// then check the presented names
1267
// check intermediate ca cert properties
1268
// check trust chain for completeness
1269
// works only for thorough checks, not shallow, so:
1270
            $intermOdditiesCAT = [];
1271
            $verifyResult = 0;
1272
1273
            if ($this->opMode == self::RADIUS_TEST_OPERATION_MODE_THOROUGH && $bundle !== FALSE && !in_array(RADIUSTests::CERTPROB_NO_SERVER_CERT, $testresults['cert_oddities'])) {
1274
                $verifyResult = $this->thoroughChainChecks($testresults, $intermOdditiesCAT, $tmpDir, $bundle["SERVERCERT"], $bundle["INTERMEDIATE_CA"], $bundle["INTERMEDIATE_CRL"]);
1275
                $this->thoroughNameChecks($bundle["SERVERCERT"][0], $testresults);       
1276
            }
1277
1278
            $testresults['cert_oddities'] = array_merge($testresults['cert_oddities'], $bundle["INTERMEDIATE_OBSERVED_ODDITIES"] ?? []);
1279
            if (in_array(RADIUSTests::CERTPROB_OUTSIDE_VALIDITY_PERIOD, $intermOdditiesCAT) && $verifyResult == 3) {
1280
                $key = array_search(RADIUSTests::CERTPROB_OUTSIDE_VALIDITY_PERIOD, $intermOdditiesCAT);
1281
                $intermOdditiesCAT[$key] = RADIUSTests::CERTPROB_OUTSIDE_VALIDITY_PERIOD_WARN;
1282
            }
1283
1284
            $testresults['cert_oddities'] = array_merge($testresults['cert_oddities'], $intermOdditiesCAT);
1285
1286
// mention trust chain failure only if no expired cert was in the chain; otherwise path validation will trivially fail
1287
            if (in_array(RADIUSTests::CERTPROB_OUTSIDE_VALIDITY_PERIOD, $testresults['cert_oddities'])) {
1288
                $this->loggerInstance->debug(4, "Deleting trust chain problem report, if present.");
1289
                if (($key = array_search(RADIUSTests::CERTPROB_TRUST_ROOT_NOT_REACHED, $testresults['cert_oddities'])) !== false) {
1290
                    unset($testresults['cert_oddities'][$key]);
1291
                }
1292
                if (($key = array_search(RADIUSTests::CERTPROB_TRUST_ROOT_REACHED_ONLY_WITH_OOB_INTERMEDIATES, $testresults['cert_oddities'])) !== false) {
1293
                    unset($testresults['cert_oddities'][$key]);
1294
                }
1295
            }
1296
        }
1297
        $this->UDP_reachability_result[$probeindex] = $testresults;
1298
        $this->UDP_reachability_executed = $radiusResult;
1299
        return $radiusResult;
1300
    }
1301
1302
    /**
1303
     * sets the outer identity to use in the checks
1304
     * 
1305
     * @param string $id the outer ID to use
1306
     * @return void
1307
     */
1308
    public function setOuterIdentity($id) {
1309
        $this->outerUsernameForChecks = $id;
1310
    }
1311
1312
    /**
1313
     * pull together all sub tests into a cohesive test result
1314
     * @param int $host index of the probe for which the results are collated
1315
     * @return array
1316
     */
1317
    public function consolidateUdpResult($host) {
1318
        \core\common\Entity::intoThePotatoes();
1319
        $ret = [];
1320
        $serverCert = [];
1321
        $udpResult = $this->UDP_reachability_result[$host];
1322
        if (isset($udpResult['certdata']) && count($udpResult['certdata'])) {
1323
            foreach ($udpResult['certdata'] as $certdata) {
1324
                if ($certdata['type'] != 'server' && $certdata['type'] != 'totally_selfsigned') {
1325
                    continue;
1326
                }
1327
                if (isset($certdata['extensions'])) {
1328
                    foreach ($certdata['extensions'] as $k => $v) {
1329
                        $certdata['extensions'][$k] = iconv('UTF-8', 'UTF-8//IGNORE', $certdata['extensions'][$k]);
1330
                    }
1331
                }
1332
                $serverCert = [
1333
                    'subject' => $this->printDN($certdata['subject']),
1334
                    'issuer' => $this->printDN($certdata['issuer']),
1335
                    'validFrom' => $this->printTm($certdata['validFrom_time_t']),
1336
                    'validTo' => $this->printTm($certdata['validTo_time_t']),
1337
                    'serialNumber' => $certdata['serialNumber'] . sprintf(" (0x%X)", $certdata['serialNumber']),
1338
                    'sha1' => $certdata['sha1'],
1339
                    'public_key_length' => $certdata['public_key_length'],
1340
                    'extensions' => $certdata['extensions']
1341
                ];
1342
            }
1343
        }
1344
        $ret['server_cert'] = $serverCert;
1345
        $ret['server'] = 0;
1346
        if (isset($udpResult['incoming_server_names'][0])) {
1347
            $ret['server'] = sprintf(_("Connected to %s."), $udpResult['incoming_server_names'][0]);
1348
        }
1349
        $ret['level'] = \core\common\Entity::L_OK;
1350
        $ret['time_millisec'] = sprintf("%d", $udpResult['time_millisec']);
1351
        if (empty($udpResult['cert_oddities'])) {
1352
            $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.");
1353
            \core\common\Entity::outOfThePotatoes();
1354
            return $ret;
1355
        }
1356
1357
        $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.");
1358
        $ret['cert_oddities'] = [];
1359
        foreach ($udpResult['cert_oddities'] as $oddity) {
1360
            $o = [];
1361
            $o['code'] = $oddity;
1362
            $o['message'] = isset($this->returnCodes[$oddity]["message"]) && $this->returnCodes[$oddity]["message"] ? $this->returnCodes[$oddity]["message"] : $oddity;
1363
            if (isset($this->returnCodes[$oddity]['severity'])) {
1364
                $o['level'] = $this->returnCodes[$oddity]["severity"];
1365
                $ret['level'] = max($ret['level'], $this->returnCodes[$oddity]["severity"]);
1366
            }
1367
            $ret['cert_oddities'][] = $o;
1368
        }
1369
        \core\common\Entity::outOfThePotatoes();
1370
        return $ret;
1371
    }
1372
1373
}
1374