RADIUSTests   F
last analyzed

Complexity

Total Complexity 216

Size/Duplication

Total Lines 1319
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 216
eloc 618
c 1
b 0
f 0
dl 0
loc 1319
rs 1.982

28 Methods

Rating   Name   Duplication   Size   Complexity  
A udpReachability() 0 13 3
A printTm() 0 2 1
B __construct() 0 45 8
A printDN() 0 14 5
C propertyCheckServercert() 0 45 13
A filterPackettype() 0 11 3
A listerrors() 0 2 1
A redact() 0 13 4
B addCrltoCert() 0 48 7
A wasModernTlsNegotiated() 0 10 4
A determineCertificateType() 0 13 6
B thoroughNameChecks() 0 30 8
C extractIncomingCertsfromEAP() 0 89 16
B thoroughChainChecks() 0 57 11
C createCArepository() 0 56 13
A eapolTestConfig() 0 17 4
A checkRadiusPacketFlow() 0 25 5
B packetCountEvaluation() 0 34 9
A executeEapolTest() 0 25 2
C udpLogin() 0 79 16
B wpaSupplicantConfig() 0 42 6
A wasEapTypeNegotiated() 0 10 3
A setOuterIdentity() 0 2 1
C consolidateUdpResult() 0 54 14
D checkLineparse() 0 58 23
A udpLoginPreliminaries() 0 20 6
B propertyCheckIntermediate() 0 31 11
C autodetectCAWithProbe() 0 92 13

How to fix   Complexity   

Complex Class

Complex classes like RADIUSTests often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use RADIUSTests, and based on these observations, apply Extract Interface, too.

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