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; |
37
|
|
|
|
38
|
|
|
require_once dirname(dirname(__DIR__)) . "/config/_config.php"; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Test suite to verify that an EAP setup is actually working as advertised in |
42
|
|
|
* the real world. Can only be used if CONFIG_DIAGNOSTICS['RADIUSTESTS'] is configured. |
43
|
|
|
* |
44
|
|
|
* @author Stefan Winter <[email protected]> |
45
|
|
|
* @author Tomasz Wolniewicz <[email protected]> |
46
|
|
|
* |
47
|
|
|
* @license see LICENSE file in root directory |
48
|
|
|
* |
49
|
|
|
* @package Developer |
50
|
|
|
*/ |
51
|
|
|
class RADIUSTests extends AbstractTest |
52
|
|
|
{ |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Was the reachability check executed already? |
56
|
|
|
* |
57
|
|
|
* @var integer |
58
|
|
|
*/ |
59
|
|
|
private $UDP_reachability_executed; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* the issues we found |
63
|
|
|
* |
64
|
|
|
* @var array |
65
|
|
|
*/ |
66
|
|
|
private $errorlist; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* This private variable contains the realm to be checked. Is filled in the |
70
|
|
|
* class constructor. |
71
|
|
|
* |
72
|
|
|
* @var string |
73
|
|
|
*/ |
74
|
|
|
private $realm; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* which username to use as outer identity |
78
|
|
|
* |
79
|
|
|
* @var string |
80
|
|
|
*/ |
81
|
|
|
private $outerUsernameForChecks; |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* list of CAs we expect incoming server certs to be from |
85
|
|
|
* |
86
|
|
|
* @var array |
87
|
|
|
*/ |
88
|
|
|
private $expectedCABundle; |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* list of expected server names |
92
|
|
|
* |
93
|
|
|
* @var array |
94
|
|
|
*/ |
95
|
|
|
private $expectedServerNames; |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* the list of EAP types which the IdP allegedly supports. |
99
|
|
|
* |
100
|
|
|
* @var array |
101
|
|
|
*/ |
102
|
|
|
private $supportedEapTypes; |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* Do we run throrough or shallow checks? |
106
|
|
|
* |
107
|
|
|
* @var integer |
108
|
|
|
*/ |
109
|
|
|
private $opMode; |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* result of the reachability tests |
113
|
|
|
* |
114
|
|
|
* @var array |
115
|
|
|
*/ |
116
|
|
|
public $UDP_reachability_result; |
117
|
|
|
|
118
|
|
|
const RADIUS_TEST_OPERATION_MODE_SHALLOW = 1; |
119
|
|
|
const RADIUS_TEST_OPERATION_MODE_THOROUGH = 2; |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* Constructor for the EAPTests class. The single mandatory parameter is the |
123
|
|
|
* realm for which the tests are to be carried out. |
124
|
|
|
* |
125
|
|
|
* @param string $realm the realm to check |
126
|
|
|
* @param string $outerUsernameForChecks outer username to use |
127
|
|
|
* @param array $supportedEapTypes array of integer representations of EAP types |
128
|
|
|
* @param array $expectedServerNames array of strings |
129
|
|
|
* @param array $expectedCABundle array of PEM blocks |
130
|
|
|
*/ |
|
|
|
|
131
|
|
|
public function __construct($realm, $outerUsernameForChecks, $supportedEapTypes = [], $expectedServerNames = [], $expectedCABundle = []) |
132
|
|
|
{ |
133
|
|
|
parent::__construct(); |
134
|
|
|
|
135
|
|
|
$this->realm = $realm; |
136
|
|
|
$this->outerUsernameForChecks = $outerUsernameForChecks; |
137
|
|
|
$this->expectedCABundle = $expectedCABundle; |
138
|
|
|
$this->expectedServerNames = $expectedServerNames; |
139
|
|
|
$this->supportedEapTypes = $supportedEapTypes; |
140
|
|
|
|
141
|
|
|
$this->opMode = self::RADIUS_TEST_OPERATION_MODE_SHALLOW; |
142
|
|
|
|
143
|
|
|
$caNeeded = FALSE; |
144
|
|
|
$serverNeeded = FALSE; |
145
|
|
|
foreach ($supportedEapTypes as $oneEapType) { |
146
|
|
|
if ($oneEapType->needsServerCACert()) { |
147
|
|
|
$caNeeded = TRUE; |
148
|
|
|
} |
149
|
|
|
if ($oneEapType->needsServerName()) { |
150
|
|
|
$serverNeeded = TRUE; |
151
|
|
|
} |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
if ($caNeeded) { |
155
|
|
|
// we need to have info about at least one CA cert and server names |
156
|
|
|
if (count($this->expectedCABundle) == 0) { |
157
|
|
|
Throw new Exception("Thorough checks for an EAP type needing CAs were requested, but the required parameters were not given."); |
158
|
|
|
} else { |
159
|
|
|
$this->opMode = self::RADIUS_TEST_OPERATION_MODE_THOROUGH; |
160
|
|
|
} |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
if ($serverNeeded) { |
164
|
|
|
if (count($this->expectedServerNames) == 0) { |
165
|
|
|
Throw new Exception("Thorough checks for an EAP type needing server names were requested, but the required parameter was not given."); |
166
|
|
|
} else { |
167
|
|
|
$this->opMode = self::RADIUS_TEST_OPERATION_MODE_THOROUGH; |
168
|
|
|
} |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
$this->loggerInstance->debug(4, "RADIUSTests is in opMode " . $this->opMode . ", parameters were: $realm, $outerUsernameForChecks, " . print_r($supportedEapTypes, true)); |
172
|
|
|
$this->loggerInstance->debug(4, print_r($expectedServerNames, true)); |
173
|
|
|
$this->loggerInstance->debug(4, print_r($expectedCABundle, true)); |
174
|
|
|
|
175
|
|
|
$this->UDP_reachability_result = []; |
176
|
|
|
$this->errorlist = []; |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* creates a string with the DistinguishedName (comma-separated name=value fields) |
181
|
|
|
* |
182
|
|
|
* @param array $distinguishedName the components of the DN |
183
|
|
|
* @return string |
184
|
|
|
*/ |
185
|
|
|
private function printDN($distinguishedName) |
186
|
|
|
{ |
187
|
|
|
$out = ''; |
188
|
|
|
foreach (array_reverse($distinguishedName) as $nameType => $nameValue) { // to give an example: "CN" => "some.host.example" |
189
|
|
|
if (!is_array($nameValue)) { // single-valued: just a string |
190
|
|
|
$nameValue = ["$nameValue"]; // convert it to a multi-value attrib with just one value :-) for unified processing later on |
191
|
|
|
} |
192
|
|
|
foreach ($nameValue as $oneValue) { |
193
|
|
|
if ($out) { |
194
|
|
|
$out .= ','; |
195
|
|
|
} |
196
|
|
|
$out .= "$nameType=$oneValue"; |
197
|
|
|
} |
198
|
|
|
} |
199
|
|
|
return($out); |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
/** |
203
|
|
|
* prints a timestamp in gmdate formatting |
204
|
|
|
* |
205
|
|
|
* @param int $time time in UNIX timestamp |
206
|
|
|
* @return string |
207
|
|
|
*/ |
208
|
|
|
private function printTm($time) |
209
|
|
|
{ |
210
|
|
|
return(gmdate(\DateTime::COOKIE, $time)); |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* This function parses a X.509 server cert and checks if it finds client device incompatibilities |
215
|
|
|
* |
216
|
|
|
* @param array $servercert the properties of the certificate as returned by |
217
|
|
|
* processCertificate(), $servercert is modified, |
218
|
|
|
* if CRL is defied, it is downloaded and added to |
219
|
|
|
* the array incoming_server_names, sAN_DNS and CN |
220
|
|
|
* array values are also defined |
221
|
|
|
* @return array of oddities; the array is empty if everything is fine |
222
|
|
|
*/ |
223
|
|
|
private function propertyCheckServercert(&$servercert) |
224
|
|
|
{ |
225
|
|
|
// we share the same checks as for CAs when it comes to signature algorithm and basicconstraints |
226
|
|
|
// so call that function and memorise the outcome |
227
|
|
|
$returnarray = $this->propertyCheckIntermediate($servercert, TRUE); |
228
|
|
|
$sANdns = []; |
229
|
|
|
if (!isset($servercert['full_details']['extensions'])) { |
230
|
|
|
$returnarray[] = RADIUSTests::CERTPROB_NO_TLS_WEBSERVER_OID; |
231
|
|
|
$returnarray[] = RADIUSTests::CERTPROB_NO_CDP_HTTP; |
232
|
|
|
} else { // Extensions are present... |
233
|
|
|
if (!isset($servercert['full_details']['extensions']['extendedKeyUsage']) || !preg_match("/TLS Web Server Authentication/", $servercert['full_details']['extensions']['extendedKeyUsage'])) { |
234
|
|
|
$returnarray[] = RADIUSTests::CERTPROB_NO_TLS_WEBSERVER_OID; |
235
|
|
|
} |
236
|
|
|
if (isset($servercert['full_details']['extensions']['subjectAltName'])) { |
237
|
|
|
$sANlist = explode(", ", $servercert['full_details']['extensions']['subjectAltName']); |
238
|
|
|
foreach ($sANlist as $subjectAltName) { |
239
|
|
|
if (preg_match("/^DNS:/", $subjectAltName)) { |
240
|
|
|
$sANdns[] = substr($subjectAltName, 4); |
241
|
|
|
} |
242
|
|
|
} |
243
|
|
|
} |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
// often, there is only one name, so we store it in an array of one member |
247
|
|
|
$commonName = [$servercert['full_details']['subject']['CN']]; |
248
|
|
|
// if we got an array of names instead, then that is already an array, so override |
249
|
|
|
if (isset($servercert['full_details']['subject']['CN']) && is_array($servercert['full_details']['subject']['CN'])) { |
250
|
|
|
$commonName = $servercert['full_details']['subject']['CN']; |
251
|
|
|
$returnarray[] = RADIUSTests::CERTPROB_MULTIPLE_CN; |
252
|
|
|
} |
253
|
|
|
$allnames = array_values(array_unique(array_merge($commonName, $sANdns))); |
254
|
|
|
// check for wildcards |
255
|
|
|
// check for real hostnames, and whether there is a wildcard in a name |
256
|
|
|
foreach ($allnames as $onename) { |
257
|
|
|
if (preg_match("/\*/", $onename)) { |
258
|
|
|
$returnarray[] = RADIUSTests::CERTPROB_WILDCARD_IN_NAME; |
259
|
|
|
continue; // otherwise we'd ALSO complain that it's not a real hostname |
260
|
|
|
} |
261
|
|
|
if ($onename != "" && filter_var("foo@" . idn_to_ascii($onename), FILTER_VALIDATE_EMAIL) === FALSE) { |
262
|
|
|
$returnarray[] = RADIUSTests::CERTPROB_NOT_A_HOSTNAME; |
263
|
|
|
} |
264
|
|
|
} |
265
|
|
|
$servercert['incoming_server_names'] = $allnames; |
266
|
|
|
$servercert['sAN_DNS'] = $sANdns; |
267
|
|
|
$servercert['CN'] = $commonName; |
268
|
|
|
return $returnarray; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
/** |
272
|
|
|
* This function parses a X.509 intermediate CA cert and checks if it finds client device incompatibilities |
273
|
|
|
* |
274
|
|
|
* @param array $intermediateCa the properties of the certificate as returned by processCertificate() |
275
|
|
|
* @param boolean $serverCert treat as servercert? |
276
|
|
|
* @return array of oddities; the array is empty if everything is fine |
277
|
|
|
*/ |
278
|
|
|
private function propertyCheckIntermediate(&$intermediateCa, $serverCert = FALSE) |
279
|
|
|
{ |
280
|
|
|
$returnarray = []; |
281
|
|
|
if (preg_match("/md5/i", $intermediateCa['full_details']['signatureTypeSN'])) { |
282
|
|
|
$returnarray[] = RADIUSTests::CERTPROB_MD5_SIGNATURE; |
283
|
|
|
} |
284
|
|
|
if (preg_match("/sha1/i", $intermediateCa['full_details']['signatureTypeSN'])) { |
285
|
|
|
$returnarray[] = RADIUSTests::CERTPROB_SHA1_SIGNATURE; |
286
|
|
|
} |
287
|
|
|
$this->loggerInstance->debug(4, "CERT IS: " . print_r($intermediateCa, TRUE)); |
288
|
|
|
if ($intermediateCa['basicconstraints_set'] == 0) { |
289
|
|
|
$returnarray[] = RADIUSTests::CERTPROB_NO_BASICCONSTRAINTS; |
290
|
|
|
} |
291
|
|
|
if ($intermediateCa['full_details']['public_key_algorithm'] == \core\common\X509::KNOWN_PUBLIC_KEY_ALGORITHMS[0] && $intermediateCa['full_details']['public_key_length'] < 2048) { |
292
|
|
|
$returnarray[] = RADIUSTests::CERTPROB_LOW_KEY_LENGTH; |
293
|
|
|
} |
294
|
|
|
if (!in_array($intermediateCa['full_details']['public_key_algorithm'], \core\common\X509::KNOWN_PUBLIC_KEY_ALGORITHMS)) { |
295
|
|
|
$returnarray[] = RADIUSTests::CERTPROB_UNKNOWN_PUBLIC_KEY_ALGORITHM; |
296
|
|
|
} |
297
|
|
|
$validFrom = $intermediateCa['full_details']['validFrom_time_t']; |
298
|
|
|
$now = time(); |
299
|
|
|
$validTo = $intermediateCa['full_details']['validTo_time_t']; |
300
|
|
|
if ($validFrom > $now || $validTo < $now) { |
301
|
|
|
$returnarray[] = RADIUSTests::CERTPROB_OUTSIDE_VALIDITY_PERIOD; |
302
|
|
|
} |
303
|
|
|
$addCertCrlResult = $this->addCrltoCert($intermediateCa); |
304
|
|
|
if ($addCertCrlResult !== 0 && $serverCert) { |
305
|
|
|
$returnarray[] = $addCertCrlResult; |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
return $returnarray; |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
/** |
312
|
|
|
* This function returns an array of errors which were encountered in all the tests. |
313
|
|
|
* |
314
|
|
|
* @return array all the errors |
315
|
|
|
*/ |
316
|
|
|
public function listerrors() |
317
|
|
|
{ |
318
|
|
|
return $this->errorlist; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
/** |
322
|
|
|
* This function performs actual authentication checks with MADE-UP credentials. |
323
|
|
|
* Its purpose is to check if a RADIUS server is reachable and speaks EAP. |
324
|
|
|
* The function fills array RADIUSTests::UDP_reachability_result[$probeindex] with all check detail |
325
|
|
|
* in case more than the return code is needed/wanted by the caller |
326
|
|
|
* |
327
|
|
|
* @param int $probeindex refers to the specific UDP-host in the config that should be checked |
328
|
|
|
* @param boolean $opnameCheck should we check choking on Operator-Name? |
329
|
|
|
* @param boolean $frag should we cause UDP fragmentation? (Warning: makes use of Operator-Name!) |
330
|
|
|
* @return int returncode |
331
|
|
|
*/ |
|
|
|
|
332
|
|
|
public function udpReachability($probeindex, $opnameCheck = TRUE, $frag = TRUE) |
333
|
|
|
{ |
334
|
|
|
// for EAP-TLS to be a viable option, we need to pass a random client cert to make eapol_test happy |
335
|
|
|
// the following PEM data is one of the SENSE EAPLab client certs (not secret at all) |
336
|
|
|
$clientcert = file_get_contents(dirname(__FILE__) . "/clientcert.p12"); |
337
|
|
|
if ($clientcert === FALSE) { |
338
|
|
|
throw new Exception("A dummy client cert is part of the source distribution, but could not be loaded!"); |
339
|
|
|
} |
340
|
|
|
// if we are in thorough opMode, use our knowledge for a more clever check |
341
|
|
|
// otherwise guess |
342
|
|
|
if ($this->opMode == self::RADIUS_TEST_OPERATION_MODE_THOROUGH) { |
343
|
|
|
return $this->udpLogin($probeindex, $this->supportedEapTypes[0]->getArrayRep(), $this->outerUsernameForChecks, 'eaplab', $opnameCheck, $frag, $clientcert); |
344
|
|
|
} |
345
|
|
|
return $this->udpLogin($probeindex, \core\common\EAP::EAPTYPE_ANY, "cat-connectivity-test@" . $this->realm, 'eaplab', $opnameCheck, $frag, $clientcert); |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
/** |
349
|
|
|
* There is a CRL Distribution Point URL in the certificate. So download the |
350
|
|
|
* CRL and attach it to the cert structure so that we can later find out if |
351
|
|
|
* the cert was revoked |
352
|
|
|
* @param array $cert by-reference: the cert data we are writing into |
353
|
|
|
* @return int result code whether we were successful in retrieving the CRL |
354
|
|
|
*/ |
|
|
|
|
355
|
|
|
private function addCrltoCert(&$cert) |
356
|
|
|
{ |
357
|
|
|
$crlUrl = []; |
358
|
|
|
$returnresult = 0; |
359
|
|
|
if (!isset($cert['full_details']['extensions']['crlDistributionPoints'])) { |
360
|
|
|
return RADIUSTests::CERTPROB_NO_CDP; |
361
|
|
|
} |
362
|
|
|
if (!preg_match("/^.*URI\:(http)(.*)$/", str_replace(["\r", "\n"], ' ', $cert['full_details']['extensions']['crlDistributionPoints']), $crlUrl)) { |
363
|
|
|
return RADIUSTests::CERTPROB_NO_CDP_HTTP; |
364
|
|
|
} |
365
|
|
|
// first and second sub-match is the full URL... check it |
366
|
|
|
$crlcontent = \core\common\OutsideComm::downloadFile(trim($crlUrl[1] . $crlUrl[2])); |
367
|
|
|
if ($crlcontent === FALSE) { |
368
|
|
|
return RADIUSTests::CERTPROB_NO_CRL_AT_CDP_URL; |
369
|
|
|
} |
370
|
|
|
/* CRLs are always in DER form, so need encoding |
371
|
|
|
* note that what we ACTUALLY got can be arbitrary junk; we just deposit |
372
|
|
|
* it on the filesystem and let openssl figure out if it is usable or not |
373
|
|
|
* |
374
|
|
|
* Unfortunately, that freaks out Scrutinizer because we write unvetted |
375
|
|
|
* data to the filesystem. Let's see if we can make things better. |
376
|
|
|
*/ |
377
|
|
|
|
378
|
|
|
// $pem = chunk_split(base64_encode($crlcontent), 64, "\n"); |
379
|
|
|
// inspired by https://stackoverflow.com/questions/2390604/how-to-pass-variables-as-stdin-into-command-line-from-php |
380
|
|
|
$proc = CONFIG['PATHS']['openssl'] . " crl -inform der"; |
381
|
|
|
$descriptorspec = [ |
382
|
|
|
0 => ["pipe", "r"], |
383
|
|
|
1 => ["pipe", "w"], |
384
|
|
|
2 => ["pipe", "w"], |
385
|
|
|
]; |
386
|
|
|
$process = proc_open($proc, $descriptorspec, $pipes); |
387
|
|
|
if (!is_resource($process)) { |
388
|
|
|
throw new Exception("Unable to execute openssl cmdline for CRL conversion!"); |
389
|
|
|
} |
390
|
|
|
fwrite($pipes[0], $crlcontent); |
391
|
|
|
fclose($pipes[0]); |
392
|
|
|
$pem = stream_get_contents($pipes[1]); |
393
|
|
|
fclose($pipes[1]); |
394
|
|
|
fclose($pipes[2]); |
395
|
|
|
$retval = proc_close($process); |
396
|
|
|
if ($retval != 0 || !preg_match("/BEGIN X509 CRL/", $pem)) { |
397
|
|
|
// this was not a real CRL |
398
|
|
|
return RADIUSTests::CERTPROB_NO_CRL_AT_CDP_URL; |
399
|
|
|
} |
400
|
|
|
$cert['CRL'] = []; |
401
|
|
|
$cert['CRL'][] = $pem; |
402
|
|
|
return $returnresult; |
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* We don't want to write passwords of the live login test to our logs. Filter them out |
407
|
|
|
* @param string $stringToRedact what should be redacted |
408
|
|
|
* @param array $inputarray array of strings (outputs of eapol_test command) |
409
|
|
|
* @return string[] the output of eapol_test with the password redacted |
410
|
|
|
*/ |
411
|
|
|
private function redact($stringToRedact, $inputarray) |
412
|
|
|
{ |
413
|
|
|
$temparray = preg_replace("/^.*$stringToRedact.*$/", "LINE CONTAINING PASSWORD REDACTED", $inputarray); |
414
|
|
|
$hex = bin2hex($stringToRedact); |
415
|
|
|
$spaced = ""; |
416
|
|
|
$origLength = strlen($hex); |
417
|
|
|
for ($i = 1; $i < $origLength; $i++) { |
418
|
|
|
if ($i % 2 == 1 && $i != strlen($hex)) { |
419
|
|
|
$spaced .= $hex[$i] . " "; |
420
|
|
|
} else { |
421
|
|
|
$spaced .= $hex[$i]; |
422
|
|
|
} |
423
|
|
|
} |
424
|
|
|
return preg_replace("/$spaced/", " HEX ENCODED PASSWORD REDACTED ", $temparray); |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
/** |
428
|
|
|
* Filters eapol_test output and finds out the packet codes out of which the conversation was comprised of |
429
|
|
|
* |
430
|
|
|
* @param array $inputarray array of strings (outputs of eapol_test command) |
431
|
|
|
* @return array the packet codes which were exchanged, in sequence |
432
|
|
|
*/ |
433
|
|
|
private function filterPackettype($inputarray) |
434
|
|
|
{ |
435
|
|
|
$retarray = []; |
436
|
|
|
foreach ($inputarray as $line) { |
437
|
|
|
if (preg_match("/RADIUS message:/", $line)) { |
438
|
|
|
$linecomponents = explode(" ", $line); |
439
|
|
|
$packettypeExploded = explode("=", $linecomponents[2]); |
440
|
|
|
$packettype = $packettypeExploded[1]; |
441
|
|
|
$retarray[] = $packettype; |
442
|
|
|
} |
443
|
|
|
} |
444
|
|
|
return $retarray; |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
const LINEPARSE_CHECK_REJECTIGNORE = 1; |
448
|
|
|
const LINEPARSE_CHECK_691 = 2; |
449
|
|
|
const LINEPARSE_EAPACK = 3; |
450
|
|
|
const LINEPARSE_TLSVERSION = 4; |
451
|
|
|
const TLS_VERSION_ANCIENT = "OTHER"; |
452
|
|
|
const TLS_VERSION_1_0 = "TLSv1"; |
453
|
|
|
const TLS_VERSION_1_1 = "TLSv1.1"; |
454
|
|
|
const TLS_VERSION_1_2 = "TLSv1.2"; |
455
|
|
|
const TLS_VERSION_1_3 = "TLSv1.3"; |
456
|
|
|
|
457
|
|
|
/** |
458
|
|
|
* this function checks for various special conditions which can be found |
459
|
|
|
* only by parsing eapol_test output line by line. Checks currently |
460
|
|
|
* implemented are: |
461
|
|
|
* * if the ETLRs sent back an Access-Reject because there appeared to |
462
|
|
|
* be a timeout further downstream |
463
|
|
|
* * did the server send an MSCHAP Error 691 - Retry Allowed in a Challenge |
464
|
|
|
* instead of an outright reject? |
465
|
|
|
* * was an EAP method ever acknowledged by both sides during the EAP |
466
|
|
|
* conversation |
467
|
|
|
* |
468
|
|
|
* @param array $inputarray array of strings (outputs of eapol_test command) |
469
|
|
|
* @param int $desiredCheck which test should be run (see constants above) |
470
|
|
|
* @return boolean returns TRUE if ETLR Reject logic was detected; FALSE if not |
471
|
|
|
*/ |
|
|
|
|
472
|
|
|
private function checkLineparse($inputarray, $desiredCheck) |
473
|
|
|
{ |
474
|
|
|
foreach ($inputarray as $lineid => $line) { |
475
|
|
|
switch ($desiredCheck) { |
476
|
|
|
case self::LINEPARSE_CHECK_REJECTIGNORE: |
477
|
|
|
if (preg_match("/Attribute 18 (Reply-Message)/", $line) && preg_match("/Reject instead of Ignore at eduroam.org/", $inputarray[$lineid + 1])) { |
478
|
|
|
return TRUE; |
479
|
|
|
} |
480
|
|
|
break; |
481
|
|
|
case self::LINEPARSE_CHECK_691: |
482
|
|
|
if (preg_match("/MSCHAPV2: error 691/", $line) && preg_match("/MSCHAPV2: retry is allowed/", $inputarray[$lineid + 1])) { |
483
|
|
|
return TRUE; |
484
|
|
|
} |
485
|
|
|
break; |
486
|
|
|
case self::LINEPARSE_EAPACK: |
487
|
|
|
if (preg_match("/CTRL-EVENT-EAP-PROPOSED-METHOD/", $line) && !preg_match("/NAK$/", $line)) { |
488
|
|
|
return TRUE; |
489
|
|
|
} |
490
|
|
|
break; |
491
|
|
|
case self::LINEPARSE_TLSVERSION: |
492
|
|
|
break; |
493
|
|
|
default: |
494
|
|
|
throw new Exception("This lineparse test does not exist."); |
495
|
|
|
} |
496
|
|
|
} |
497
|
|
|
// for TLS version checks, we need to search from bottom to top |
498
|
|
|
// eapol_test will always try its highest version first, and can be |
499
|
|
|
// pursuaded later on to do less. So look at the end result. |
500
|
|
|
for ($counter = count($inputarray); $counter > 0; $counter--) { |
501
|
|
|
switch ($desiredCheck) { |
502
|
|
|
case self::LINEPARSE_TLSVERSION: |
503
|
|
|
$version = []; |
504
|
|
|
if (preg_match("/Using TLS version (.*)$/", $inputarray[$counter], $version)) { |
505
|
|
|
switch (trim($version[1])) { |
506
|
|
|
case self::TLS_VERSION_1_3: |
507
|
|
|
return self::TLS_VERSION_1_3; |
|
|
|
|
508
|
|
|
case self::TLS_VERSION_1_2: |
509
|
|
|
return self::TLS_VERSION_1_2; |
|
|
|
|
510
|
|
|
case self::TLS_VERSION_1_1: |
511
|
|
|
return self::TLS_VERSION_1_1; |
|
|
|
|
512
|
|
|
case self::TLS_VERSION_1_0: |
513
|
|
|
return self::TLS_VERSION_1_0; |
|
|
|
|
514
|
|
|
default: |
515
|
|
|
return self::TLS_VERSION_ANCIENT; |
|
|
|
|
516
|
|
|
} |
517
|
|
|
} |
518
|
|
|
break; |
519
|
|
|
case self::LINEPARSE_CHECK_691: /** fall-through intentional * */ |
|
|
|
|
520
|
|
|
case self::LINEPARSE_CHECK_REJECTIGNORE: /** fall-through intentional * */ |
|
|
|
|
521
|
|
|
case self::LINEPARSE_EAPACK: /** fall-through intentional * */ |
|
|
|
|
522
|
|
|
break; |
523
|
|
|
default: |
524
|
|
|
throw new Exception("This lineparse test does not exist."); |
525
|
|
|
} |
526
|
|
|
} |
527
|
|
|
return FALSE; |
528
|
|
|
} |
529
|
|
|
|
530
|
|
|
/** |
531
|
|
|
* |
532
|
|
|
* @param array $eaptype array representation of the EAP type |
533
|
|
|
* @param string $inner inner username |
534
|
|
|
* @param string $outer outer username |
535
|
|
|
* @param string $password the password |
536
|
|
|
* @return string[] [0] is the actual config for wpa_supplicant, [1] is a redacted version for logs |
537
|
|
|
*/ |
538
|
|
|
private function wpaSupplicantConfig(array $eaptype, string $inner, string $outer, string $password) |
539
|
|
|
{ |
540
|
|
|
$eapText = \core\common\EAP::eapDisplayName($eaptype); |
541
|
|
|
$config = ' |
542
|
|
|
network={ |
543
|
|
|
ssid="' . CONFIG['APPEARANCE']['productname'] . ' testing" |
544
|
|
|
key_mgmt=WPA-EAP |
545
|
|
|
proto=WPA2 |
546
|
|
|
pairwise=CCMP |
547
|
|
|
group=CCMP |
548
|
|
|
'; |
549
|
|
|
// phase 1 |
550
|
|
|
$config .= 'eap=' . $eapText['OUTER'] . "\n"; |
551
|
|
|
$logConfig = $config; |
552
|
|
|
// phase 2 if applicable; all inner methods have passwords |
553
|
|
|
if (isset($eapText['INNER']) && $eapText['INNER'] != "") { |
554
|
|
|
$config .= ' phase2="auth=' . $eapText['INNER'] . "\"\n"; |
555
|
|
|
$logConfig .= ' phase2="auth=' . $eapText['INNER'] . "\"\n"; |
556
|
|
|
} |
557
|
|
|
// all methods set a password, except EAP-TLS |
558
|
|
|
if ($eaptype != \core\common\EAP::EAPTYPE_TLS) { |
559
|
|
|
$config .= " password=\"$password\"\n"; |
560
|
|
|
$logConfig .= " password=\"not logged for security reasons\"\n"; |
561
|
|
|
} |
562
|
|
|
// for methods with client certs, add a client cert config block |
563
|
|
|
if ($eaptype == \core\common\EAP::EAPTYPE_TLS || $eaptype == \core\common\EAP::EAPTYPE_ANY) { |
564
|
|
|
$config .= " private_key=\"./client.p12\"\n"; |
565
|
|
|
$logConfig .= " private_key=\"./client.p12\"\n"; |
566
|
|
|
$config .= " private_key_passwd=\"$password\"\n"; |
567
|
|
|
$logConfig .= " private_key_passwd=\"not logged for security reasons\"\n"; |
568
|
|
|
} |
569
|
|
|
|
570
|
|
|
// inner identity |
571
|
|
|
$config .= ' identity="' . $inner . "\"\n"; |
572
|
|
|
$logConfig .= ' identity="' . $inner . "\"\n"; |
573
|
|
|
// outer identity, may be equal |
574
|
|
|
$config .= ' anonymous_identity="' . $outer . "\"\n"; |
575
|
|
|
$logConfig .= ' anonymous_identity="' . $outer . "\"\n"; |
576
|
|
|
// done |
577
|
|
|
$config .= "}"; |
578
|
|
|
$logConfig .= "}"; |
579
|
|
|
|
580
|
|
|
return [$config, $logConfig]; |
581
|
|
|
} |
582
|
|
|
|
583
|
|
|
/** |
584
|
|
|
* Checks whether the packets received are as expected in numbers |
585
|
|
|
* |
586
|
|
|
* @param array $testresults by-reference array of the testresults so far |
587
|
|
|
* function adds its own findings to that array |
588
|
|
|
* @param array $packetcount the count of incoming packets |
589
|
|
|
* @return int |
590
|
|
|
*/ |
591
|
|
|
private function packetCountEvaluation(&$testresults, $packetcount) |
592
|
|
|
{ |
593
|
|
|
$reqs = $packetcount[1] ?? 0; |
594
|
|
|
$accepts = $packetcount[2] ?? 0; |
595
|
|
|
$rejects = $packetcount[3] ?? 0; |
596
|
|
|
$challenges = $packetcount[11] ?? 0; |
597
|
|
|
$testresults['packetflow_sane'] = TRUE; |
598
|
|
|
if ($reqs - $accepts - $rejects - $challenges != 0 || $accepts > 1 || $rejects > 1) { |
599
|
|
|
$testresults['packetflow_sane'] = FALSE; |
600
|
|
|
} |
601
|
|
|
|
602
|
|
|
$this->loggerInstance->debug(5, "XYZ: Counting req, acc, rej, chal: $reqs, $accepts, $rejects, $challenges"); |
603
|
|
|
|
604
|
|
|
// calculate the main return values that this test yielded |
605
|
|
|
|
606
|
|
|
$finalretval = RADIUSTests::RETVAL_INVALID; |
607
|
|
|
if ($accepts + $rejects == 0) { // no final response. hm. |
608
|
|
|
if ($challenges > 0) { // but there was an Access-Challenge |
609
|
|
|
$finalretval = RADIUSTests::RETVAL_SERVER_UNFINISHED_COMM; |
610
|
|
|
} else { |
611
|
|
|
$finalretval = RADIUSTests::RETVAL_NO_RESPONSE; |
612
|
|
|
} |
613
|
|
|
} else // either an accept or a reject |
614
|
|
|
// rejection without EAP is fishy |
615
|
|
|
if ($rejects > 0) { |
616
|
|
|
if ($challenges == 0) { |
617
|
|
|
$finalretval = RADIUSTests::RETVAL_IMMEDIATE_REJECT; |
618
|
|
|
} else { // i.e. if rejected with challenges |
619
|
|
|
$finalretval = RADIUSTests::RETVAL_CONVERSATION_REJECT; |
620
|
|
|
} |
621
|
|
|
} else if ($accepts > 0) { |
622
|
|
|
$finalretval = RADIUSTests::RETVAL_OK; |
623
|
|
|
} |
624
|
|
|
|
625
|
|
|
return $finalretval; |
626
|
|
|
} |
627
|
|
|
|
628
|
|
|
/** |
629
|
|
|
* generate an eapol_test command-line config for the fixed config filename |
630
|
|
|
* ./udp_login_test.conf |
631
|
|
|
* @param int $probeindex number of the probe to check against |
632
|
|
|
* @param boolean $opName include Operator-Name in request? |
633
|
|
|
* @param boolean $frag make request so large that fragmentation is needed? |
634
|
|
|
* @return string the command-line for eapol_test |
635
|
|
|
*/ |
636
|
|
|
private function eapolTestConfig($probeindex, $opName, $frag) |
637
|
|
|
{ |
638
|
|
|
$cmdline = CONFIG_DIAGNOSTICS['PATHS']['eapol_test'] . |
639
|
|
|
" -a " . CONFIG_DIAGNOSTICS['RADIUSTESTS']['UDP-hosts'][$probeindex]['ip'] . |
640
|
|
|
" -s " . CONFIG_DIAGNOSTICS['RADIUSTESTS']['UDP-hosts'][$probeindex]['secret'] . |
641
|
|
|
" -o serverchain.pem" . |
642
|
|
|
" -c ./udp_login_test.conf" . |
643
|
|
|
" -M 22:44:66:CA:20:" . sprintf("%02d", $probeindex) . " " . |
644
|
|
|
" -t " . CONFIG_DIAGNOSTICS['RADIUSTESTS']['UDP-hosts'][$probeindex]['timeout'] . " "; |
645
|
|
|
if ($opName) { |
646
|
|
|
$cmdline .= '-N126:s:"1cat.eduroam.org" '; |
647
|
|
|
} |
648
|
|
|
if ($frag) { |
649
|
|
|
for ($i = 0; $i < 6; $i++) { // 6 x 250 bytes means UDP fragmentation will occur - good! |
650
|
|
|
$cmdline .= '-N26:x:0000625A0BF961616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161 '; |
651
|
|
|
} |
652
|
|
|
} |
653
|
|
|
return $cmdline; |
654
|
|
|
} |
655
|
|
|
|
656
|
|
|
/** |
657
|
|
|
* collects CA certificates, both from the incoming EAP chain and from CAT |
658
|
|
|
* config. Writes the root CAs into a trusted root CA dir and intermediate |
659
|
|
|
* and first server cert into a PEM file for later chain validation |
660
|
|
|
* |
661
|
|
|
* @param string $tmpDir working directory |
662
|
|
|
* @param array $intermOdditiesCAT by-reference array of already found |
663
|
|
|
* oddities; adds its own |
664
|
|
|
* @param array $servercert the servercert to validate |
665
|
|
|
* @param array $eapIntermediates list of intermediate CA certs that came |
666
|
|
|
* in via EAP |
667
|
|
|
* @param array $eapIntermediateCRLs list of CRLs for the EAP-supplied |
668
|
|
|
* intermediate CAs |
669
|
|
|
* @return string |
670
|
|
|
* @throws Exception |
671
|
|
|
*/ |
672
|
|
|
private function createCArepository($tmpDir, &$intermOdditiesCAT, $servercert, $eapIntermediates, $eapIntermediateCRLs) |
673
|
|
|
{ |
674
|
|
|
if (!mkdir($tmpDir . "/root-ca-allcerts/", 0700, true)) { |
675
|
|
|
throw new Exception("unable to create root CA directory (RADIUS Tests): $tmpDir/root-ca-allcerts/\n"); |
676
|
|
|
} |
677
|
|
|
if (!mkdir($tmpDir . "/root-ca-eaponly/", 0700, true)) { |
678
|
|
|
throw new Exception("unable to create root CA directory (RADIUS Tests): $tmpDir/root-ca-eaponly/\n"); |
679
|
|
|
} |
680
|
|
|
// make a copy of the EAP-received chain and add the configured intermediates, if any |
681
|
|
|
$catIntermediates = []; |
682
|
|
|
$catRoots = []; |
683
|
|
|
foreach ($this->expectedCABundle as $oneCA) { |
684
|
|
|
$x509 = new \core\common\X509(); |
685
|
|
|
$decoded = $x509->processCertificate($oneCA); |
686
|
|
|
if (is_bool($decoded)) { |
687
|
|
|
throw new Exception("Unable to parse an expected CA certificate."); |
688
|
|
|
} |
689
|
|
|
if ($decoded['ca'] == 1) { |
690
|
|
|
if ($decoded['root'] == 1) { // save CAT roots to the root directory |
691
|
|
|
file_put_contents($tmpDir . "/root-ca-eaponly/configuredroot" . count($catRoots) . ".pem", $decoded['pem']); |
692
|
|
|
file_put_contents($tmpDir . "/root-ca-allcerts/configuredroot" . count($catRoots) . ".pem", $decoded['pem']); |
693
|
|
|
$catRoots[] = $decoded['pem']; |
694
|
|
|
} else { // save the intermediates to allcerts directory |
695
|
|
|
file_put_contents($tmpDir . "/root-ca-allcerts/cat-intermediate" . count($catIntermediates) . ".pem", $decoded['pem']); |
696
|
|
|
$intermOdditiesCAT = array_merge($intermOdditiesCAT, $this->propertyCheckIntermediate($decoded)); |
697
|
|
|
if (isset($decoded['CRL']) && isset($decoded['CRL'][0])) { |
698
|
|
|
$this->loggerInstance->debug(4, "got an intermediate CRL; adding them to the chain checks. (Remember: checking end-entity cert only, not the whole chain"); |
699
|
|
|
file_put_contents($tmpDir . "/root-ca-allcerts/crl_cat" . count($catIntermediates) . ".pem", $decoded['CRL'][0]); |
700
|
|
|
} |
701
|
|
|
$catIntermediates[] = $decoded['pem']; |
702
|
|
|
} |
703
|
|
|
} |
704
|
|
|
} |
705
|
|
|
// save all intermediate certificates and CRLs to separate files in |
706
|
|
|
// both root-ca directories |
707
|
|
|
foreach ($eapIntermediates as $index => $onePem) { |
708
|
|
|
file_put_contents($tmpDir . "/root-ca-eaponly/intermediate$index.pem", $onePem); |
709
|
|
|
file_put_contents($tmpDir . "/root-ca-allcerts/intermediate$index.pem", $onePem); |
710
|
|
|
} |
711
|
|
|
foreach ($eapIntermediateCRLs as $index => $onePem) { |
712
|
|
|
file_put_contents($tmpDir . "/root-ca-eaponly/intermediateCRL$index.pem", $onePem); |
713
|
|
|
file_put_contents($tmpDir . "/root-ca-allcerts/intermediateCRL$index.pem", $onePem); |
714
|
|
|
} |
715
|
|
|
|
716
|
|
|
$checkstring = ""; |
717
|
|
|
if (isset($servercert['CRL']) && isset($servercert['CRL'][0])) { |
718
|
|
|
$this->loggerInstance->debug(4, "got a server CRL; adding them to the chain checks. (Remember: checking end-entity cert only, not the whole chain"); |
719
|
|
|
$checkstring = "-crl_check_all"; |
720
|
|
|
file_put_contents($tmpDir . "/root-ca-eaponly/crl-server.pem", $servercert['CRL'][0]); |
721
|
|
|
file_put_contents($tmpDir . "/root-ca-allcerts/crl-server.pem", $servercert['CRL'][0]); |
722
|
|
|
} |
723
|
|
|
|
724
|
|
|
|
725
|
|
|
// now c_rehash the root CA directory ... |
726
|
|
|
system(CONFIG_DIAGNOSTICS['PATHS']['c_rehash'] . " $tmpDir/root-ca-eaponly/ > /dev/null"); |
727
|
|
|
system(CONFIG_DIAGNOSTICS['PATHS']['c_rehash'] . " $tmpDir/root-ca-allcerts/ > /dev/null"); |
728
|
|
|
return $checkstring; |
729
|
|
|
} |
730
|
|
|
|
731
|
|
|
/** |
732
|
|
|
* for checks which have a known trust root CA (i.e. a valid CAT profile |
733
|
|
|
* exists), check against those known-good roots |
734
|
|
|
* |
735
|
|
|
* @param array $testresults by-reference list of testresults so far |
736
|
|
|
* Function adds its own. |
737
|
|
|
* @param array $intermOdditiesCAT by-reference list of oddities in the CA |
738
|
|
|
* certs which are configured in CAT |
739
|
|
|
* @param string $tmpDir working directory |
740
|
|
|
* @param array $servercert the server certificate to validate |
741
|
|
|
* @param array $eapIntermediates list of intermediate CA certs that came |
742
|
|
|
* in via EAP |
743
|
|
|
* @param array $eapIntermediateCRLs list of CRLs for the EAP-supplied |
744
|
|
|
* intermediate CAs |
745
|
|
|
* @return int |
746
|
|
|
* @throws Exception |
747
|
|
|
*/ |
748
|
|
|
private function thoroughChainChecks(&$testresults, &$intermOdditiesCAT, $tmpDir, $servercert, $eapIntermediates, $eapIntermediateCRLs) |
749
|
|
|
{ |
750
|
|
|
|
751
|
|
|
$crlCheckString = $this->createCArepository($tmpDir, $intermOdditiesCAT, $servercert, $eapIntermediates, $eapIntermediateCRLs); |
752
|
|
|
|
753
|
|
|
// ... and run the verification test |
754
|
|
|
$verifyResultEaponly = []; |
755
|
|
|
$verifyResultAllcerts = []; |
756
|
|
|
// the error log will complain if we run this test against an empty file of certs |
757
|
|
|
// so test if there's something PEMy in the file at all |
758
|
|
|
if (filesize("$tmpDir/serverchain.pem") > 10) { |
759
|
|
|
exec(CONFIG['PATHS']['openssl'] . " verify $crlCheckString -CApath $tmpDir/root-ca-eaponly/ -purpose any $tmpDir/incomingserver.pem", $verifyResultEaponly); |
760
|
|
|
$this->loggerInstance->debug(4, CONFIG['PATHS']['openssl'] . " verify $crlCheckString -CApath $tmpDir/root-ca-eaponly/ -purpose any $tmpDir/serverchain.pem\n"); |
761
|
|
|
$this->loggerInstance->debug(4, "Chain verify pass 1: " . print_r($verifyResultEaponly, TRUE) . "\n"); |
762
|
|
|
exec(CONFIG['PATHS']['openssl'] . " verify $crlCheckString -CApath $tmpDir/root-ca-allcerts/ -purpose any $tmpDir/incomingserver.pem", $verifyResultAllcerts); |
763
|
|
|
$this->loggerInstance->debug(4, CONFIG['PATHS']['openssl'] . " verify $crlCheckString -CApath $tmpDir/root-ca-allcerts/ -purpose any $tmpDir/serverchain.pem\n"); |
764
|
|
|
$this->loggerInstance->debug(4, "Chain verify pass 2: " . print_r($verifyResultAllcerts, TRUE) . "\n"); |
765
|
|
|
} |
766
|
|
|
|
767
|
|
|
|
768
|
|
|
// now we do certificate verification against the collected parents |
769
|
|
|
// this is done first for the server and then for each of the intermediate CAs |
770
|
|
|
// any oddities observed will |
771
|
|
|
// openssl should havd returned exactly one line of output, |
772
|
|
|
// and it should have ended with the string "OK", anything else is fishy |
773
|
|
|
// The result can also be an empty array - this means there were no |
774
|
|
|
// certificates to check. Don't complain about chain validation errors |
775
|
|
|
// in that case. |
776
|
|
|
// we have the following test result possibilities: |
777
|
|
|
// 1. test against allcerts failed |
778
|
|
|
// 2. test against allcerts succeded, but against eaponly failed - warn admin |
779
|
|
|
// 3. test against eaponly succeded, in this case critical errors about expired certs |
780
|
|
|
// need to be changed to notices, since these certs obviously do tot participate |
781
|
|
|
// in server certificate validation. |
782
|
|
|
if (count($verifyResultAllcerts) == 0 || count($verifyResultEaponly) == 0) { |
783
|
|
|
throw new Exception("No output at all from openssl?"); |
784
|
|
|
} |
785
|
|
|
if (!preg_match("/OK$/", $verifyResultAllcerts[0])) { // case 1 |
786
|
|
|
if (preg_match("/certificate revoked$/", $verifyResultAllcerts[1])) { |
787
|
|
|
$testresults['cert_oddities'][] = RADIUSTests::CERTPROB_SERVER_CERT_REVOKED; |
788
|
|
|
} elseif (preg_match("/unable to get certificate CRL/", $verifyResultAllcerts[1])) { |
789
|
|
|
$testresults['cert_oddities'][] = RADIUSTests::CERTPROB_UNABLE_TO_GET_CRL; |
790
|
|
|
} else { |
791
|
|
|
$testresults['cert_oddities'][] = RADIUSTests::CERTPROB_TRUST_ROOT_NOT_REACHED; |
792
|
|
|
} |
793
|
|
|
return 1; |
794
|
|
|
} |
795
|
|
|
if (!preg_match("/OK$/", $verifyResultEaponly[0])) { // case 2 |
796
|
|
|
if (preg_match("/certificate revoked$/", $verifyResultEaponly[1])) { |
797
|
|
|
$testresults['cert_oddities'][] = RADIUSTests::CERTPROB_SERVER_CERT_REVOKED; |
798
|
|
|
} elseif (preg_match("/unable to get certificate CRL/", $verifyResultEaponly[1])) { |
799
|
|
|
$testresults['cert_oddities'][] = RADIUSTests::CERTPROB_UNABLE_TO_GET_CRL; |
800
|
|
|
} else { |
801
|
|
|
$testresults['cert_oddities'][] = RADIUSTests::CERTPROB_TRUST_ROOT_REACHED_ONLY_WITH_OOB_INTERMEDIATES; |
802
|
|
|
} |
803
|
|
|
return 2; |
804
|
|
|
} |
805
|
|
|
return 3; |
806
|
|
|
} |
807
|
|
|
|
808
|
|
|
/** |
809
|
|
|
* check the incoming hostname (both Subject:CN and subjectAltName:DNS |
810
|
|
|
* against what is configured in the profile; it's a significant error |
811
|
|
|
* if there is no match! |
812
|
|
|
* |
813
|
|
|
* FAIL if none of the configured names show up in the server cert |
814
|
|
|
* WARN if the configured name is only in either CN or sAN:DNS |
815
|
|
|
* |
816
|
|
|
* @param array $servercert the server certificate to check |
817
|
|
|
* @param array $testresults by-reference the existing testresults. Function |
818
|
|
|
* adds its own findings. |
819
|
|
|
* @return void |
820
|
|
|
*/ |
821
|
|
|
private function thoroughNameChecks($servercert, &$testresults) |
822
|
|
|
{ |
823
|
|
|
// Strategy for checks: we are TOTALLY happy if any one of the |
824
|
|
|
// configured names shows up in both the CN and a sAN |
825
|
|
|
// This is the primary check. |
826
|
|
|
// If that was not the case, we are PARTIALLY happy if any one of |
827
|
|
|
// the configured names was in either of the CN or sAN lists. |
828
|
|
|
// we are UNHAPPY if no names match! |
829
|
|
|
$happiness = "UNHAPPY"; |
830
|
|
|
foreach ($this->expectedServerNames as $expectedName) { |
831
|
|
|
$this->loggerInstance->debug(4, "Managing expectations for $expectedName: " . print_r($servercert['CN'], TRUE) . print_r($servercert['sAN_DNS'], TRUE)); |
832
|
|
|
if (array_search($expectedName, $servercert['CN']) !== FALSE && array_search($expectedName, $servercert['sAN_DNS']) !== FALSE) { |
833
|
|
|
$this->loggerInstance->debug(4, "Totally happy!"); |
834
|
|
|
$happiness = "TOTALLY"; |
835
|
|
|
break; |
836
|
|
|
} else { |
837
|
|
|
if (array_search($expectedName, $servercert['CN']) !== FALSE || array_search($expectedName, $servercert['sAN_DNS']) !== FALSE) { |
838
|
|
|
$happiness = "PARTIALLY"; |
839
|
|
|
// keep trying with other expected names! We could be happier! |
840
|
|
|
} |
841
|
|
|
} |
842
|
|
|
} |
843
|
|
|
switch ($happiness) { |
844
|
|
|
case "UNHAPPY": |
845
|
|
|
$testresults['cert_oddities'][] = RADIUSTests::CERTPROB_SERVER_NAME_MISMATCH; |
846
|
|
|
return; |
847
|
|
|
case "PARTIALLY": |
848
|
|
|
$testresults['cert_oddities'][] = RADIUSTests::CERTPROB_SERVER_NAME_PARTIAL_MATCH; |
849
|
|
|
return; |
850
|
|
|
default: // nothing to complain about! |
851
|
|
|
return; |
852
|
|
|
} |
853
|
|
|
} |
854
|
|
|
|
855
|
|
|
/** |
856
|
|
|
* run eapol_test |
857
|
|
|
* |
858
|
|
|
* @param string $tmpDir working directory |
859
|
|
|
* @param int $probeindex number of the probe this test should run through |
860
|
|
|
* @param array $eaptype EAP type in array representation |
861
|
|
|
* @param string $innerUser EAP method inner username to use |
862
|
|
|
* @param string $password password to use |
863
|
|
|
* @param boolean $opnameCheck inject Operator-Name? |
864
|
|
|
* @param boolean $frag provoke UDP fragmentation? |
865
|
|
|
* @return array timing information of the executed eapol_test run |
866
|
|
|
* @throws Exception |
867
|
|
|
*/ |
868
|
|
|
private function executeEapolTest($tmpDir, $probeindex, $eaptype, $innerUser, $password, $opnameCheck, $frag) |
869
|
|
|
{ |
870
|
|
|
$finalInner = $innerUser; |
871
|
|
|
$finalOuter = $this->outerUsernameForChecks; |
872
|
|
|
|
873
|
|
|
$theconfigs = $this->wpaSupplicantConfig($eaptype, $finalInner, $finalOuter, $password); |
874
|
|
|
// the config intentionally does not include CA checking. We do this |
875
|
|
|
// ourselves after getting the chain with -o. |
876
|
|
|
file_put_contents($tmpDir . "/udp_login_test.conf", $theconfigs[0]); |
877
|
|
|
|
878
|
|
|
$cmdline = $this->eapolTestConfig($probeindex, $opnameCheck, $frag); |
879
|
|
|
$this->loggerInstance->debug(4, "Shallow reachability check cmdline: $cmdline\n"); |
880
|
|
|
$this->loggerInstance->debug(4, "Shallow reachability check config: $tmpDir\n" . $theconfigs[1] . "\n"); |
881
|
|
|
$time_start = microtime(true); |
882
|
|
|
$pflow = []; |
883
|
|
|
exec($cmdline, $pflow); |
884
|
|
|
if ($pflow === NULL) { |
885
|
|
|
throw new Exception("The output of an exec() call really can't be NULL!"); |
886
|
|
|
} |
887
|
|
|
$time_stop = microtime(true); |
888
|
|
|
$this->loggerInstance->debug(5, print_r($this->redact($password, $pflow), TRUE)); |
889
|
|
|
return [ |
890
|
|
|
"time" => ($time_stop - $time_start) * 1000, |
891
|
|
|
"output" => $pflow, |
892
|
|
|
]; |
893
|
|
|
} |
894
|
|
|
|
895
|
|
|
/** |
896
|
|
|
* checks if the RADIUS packets were coming in in the order they are |
897
|
|
|
* expected. The function massages the raw result for some known oddities. |
898
|
|
|
* |
899
|
|
|
* @param array $testresults by-reference array of test results so far. |
900
|
|
|
* function adds its own. |
901
|
|
|
* @param array $packetflow_orig original flow of packets |
902
|
|
|
* @return int |
903
|
|
|
*/ |
904
|
|
|
private function checkRadiusPacketFlow(&$testresults, $packetflow_orig) |
905
|
|
|
{ |
906
|
|
|
|
907
|
|
|
$packetflow = $this->filterPackettype($packetflow_orig); |
908
|
|
|
|
909
|
|
|
|
910
|
|
|
// when MS-CHAPv2 allows retry, we never formally get a reject (just a |
911
|
|
|
// Challenge that PW was wrong but and we should try a different one; |
912
|
|
|
// but that effectively is a reject |
913
|
|
|
// so change the flow results to take that into account |
914
|
|
|
if ($packetflow[count($packetflow) - 1] == 11 && $this->checkLineparse($packetflow_orig, self::LINEPARSE_CHECK_691)) { |
915
|
|
|
$packetflow[count($packetflow) - 1] = 3; |
916
|
|
|
} |
917
|
|
|
// also, the ETLRs sometimes send a reject when the server is not |
918
|
|
|
// responding. This should not be considered a real reject; it's a middle |
919
|
|
|
// box unduly altering the end-to-end result. Do not consider this final |
920
|
|
|
// Reject if it comes from ETLR |
921
|
|
|
if ($packetflow[count($packetflow) - 1] == 3 && $this->checkLineparse($packetflow_orig, self::LINEPARSE_CHECK_REJECTIGNORE)) { |
922
|
|
|
array_pop($packetflow); |
923
|
|
|
} |
924
|
|
|
$this->loggerInstance->debug(5, "Packetflow: " . print_r($packetflow, TRUE)); |
925
|
|
|
$packetcount = array_count_values($packetflow); |
926
|
|
|
$testresults['packetcount'] = $packetcount; |
927
|
|
|
$testresults['packetflow'] = $packetflow; |
928
|
|
|
|
929
|
|
|
// calculate packet counts and see what the overall flow was |
930
|
|
|
return $this->packetCountEvaluation($testresults, $packetcount); |
931
|
|
|
} |
932
|
|
|
|
933
|
|
|
/** |
934
|
|
|
* parses the eapol_test output to determine whether we got to a point where |
935
|
|
|
* an EAP type was mutually agreed |
936
|
|
|
* |
937
|
|
|
* @param array $testresults by-reference, we add our findings if |
938
|
|
|
* something is noteworthy |
939
|
|
|
* @param array $packetflow_orig the array of text output from eapol_test |
940
|
|
|
* @return bool |
941
|
|
|
*/ |
942
|
|
|
private function wasEapTypeNegotiated(&$testresults, $packetflow_orig) |
943
|
|
|
{ |
944
|
|
|
$negotiatedEapType = $this->checkLineparse($packetflow_orig, self::LINEPARSE_EAPACK); |
945
|
|
|
if (!$negotiatedEapType) { |
946
|
|
|
$testresults['cert_oddities'][] = RADIUSTests::CERTPROB_NO_COMMON_EAP_METHOD; |
947
|
|
|
} |
948
|
|
|
|
949
|
|
|
return $negotiatedEapType; |
950
|
|
|
} |
951
|
|
|
|
952
|
|
|
private function wasModernTlsNegotiated(&$testresults, $packetflow_orig) |
|
|
|
|
953
|
|
|
{ |
954
|
|
|
$negotiatedTlsVersion = $this->checkLineparse($packetflow_orig, self::LINEPARSE_TLSVERSION); |
955
|
|
|
$this->loggerInstance->debug(4,"TLS version found is: $negotiatedTlsVersion"."\n"); |
956
|
|
|
if ($negotiatedTlsVersion == FALSE) { |
|
|
|
|
957
|
|
|
$testresults['cert_oddities'][] = RADIUSTests::TLSPROB_UNKNOWN_TLS_VERSION; |
958
|
|
|
} elseif ($negotiatedTlsVersion != self::TLS_VERSION_1_2 && $negotiatedTlsVersion != self::TLS_VERSION_1_3) { |
959
|
|
|
$testresults['cert_oddities'][] = RADIUSTests::TLSPROB_DEPRECATED_TLS_VERSION; |
960
|
|
|
} |
961
|
|
|
|
962
|
|
|
return $negotiatedTlsVersion; |
963
|
|
|
} |
964
|
|
|
|
965
|
|
|
const SERVER_NO_CA_EXTENSION = 1; |
966
|
|
|
const SERVER_CA_SELFSIGNED = 2; |
967
|
|
|
const CA_INTERMEDIATE = 3; |
968
|
|
|
const CA_ROOT = 4; |
969
|
|
|
|
970
|
|
|
/** |
971
|
|
|
* what is the incoming certificate - root, intermediate, or server? |
972
|
|
|
* @param array $cert the certificate to check |
973
|
|
|
* @param int $totalCertCount number of certs in total in chain |
974
|
|
|
* @return int |
975
|
|
|
*/ |
976
|
|
|
private function determineCertificateType(&$cert, $totalCertCount) |
977
|
|
|
{ |
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
|
|
|
|
1003
|
|
|
/* |
1004
|
|
|
* EAP's house rules: |
1005
|
|
|
* 1) it is unnecessary to include the root CA itself (adding it has |
1006
|
|
|
* detrimental effects on performance) |
1007
|
|
|
* 2) TLS Web Server OID presence (Windows OSes need that) |
1008
|
|
|
* 3) MD5 signature algorithm disallowed (iOS barks if so) |
1009
|
|
|
* 4) CDP URL (Windows Phone 8 barks if not present) |
1010
|
|
|
* 5) there should be exactly one server cert in the chain |
1011
|
|
|
*/ |
1012
|
|
|
|
1013
|
|
|
$x509 = new \core\common\X509(); |
1014
|
|
|
// $eap_certarray holds all certs received in EAP conversation |
1015
|
|
|
$incomingData = file_get_contents($tmpDir . "/serverchain.pem"); |
1016
|
|
|
if ($incomingData !== FALSE && strlen($incomingData) > 0) { |
1017
|
|
|
$eapCertArray = $x509->splitCertificate($incomingData); |
1018
|
|
|
} else { |
1019
|
|
|
$testresults['cert_oddities'][] = RADIUSTests::CERTPROB_NO_CERTIFICATE_IN_CONVERSATION; |
1020
|
|
|
return FALSE; |
1021
|
|
|
} |
1022
|
|
|
$eapIntermediates = []; |
1023
|
|
|
$eapIntermediateCRLs = []; |
1024
|
|
|
$servercert = []; |
1025
|
|
|
$intermOdditiesEAP = []; |
1026
|
|
|
|
1027
|
|
|
$testresults['certdata'] = []; |
1028
|
|
|
|
1029
|
|
|
|
1030
|
|
|
foreach ($eapCertArray as $certPem) { |
1031
|
|
|
$cert = $x509->processCertificate($certPem); |
1032
|
|
|
if ($cert === FALSE) { |
1033
|
|
|
continue; |
1034
|
|
|
} |
1035
|
|
|
// consider the certificate a server cert |
1036
|
|
|
// a) if it is not a CA and is not a self-signed root |
1037
|
|
|
// b) if it is a CA, and self-signed, and it is the only cert in |
1038
|
|
|
// the incoming cert chain |
1039
|
|
|
// (meaning the self-signed is itself the server cert) |
1040
|
|
|
switch ($this->determineCertificateType($cert, count($eapCertArray))) { |
1041
|
|
|
case RADIUSTests::SERVER_NO_CA_EXTENSION: // both are handled same, fall-through |
1042
|
|
|
case RADIUSTests::SERVER_CA_SELFSIGNED: |
1043
|
|
|
$servercert[] = $cert; |
1044
|
|
|
if (count($servercert) == 1) { |
1045
|
|
|
if (file_put_contents($tmpDir . "/incomingserver.pem", $certPem . "\n") === FALSE) { |
1046
|
|
|
$this->loggerInstance->debug(4, "The (first) server certificate could not be written to $tmpDir/incomingserver.pem!\n"); |
1047
|
|
|
} |
1048
|
|
|
$this->loggerInstance->debug(4, "This is the (first) server certificate, with CRL content if applicable: " . print_r($servercert[0], true)); |
1049
|
|
|
} elseif (!in_array(RADIUSTests::CERTPROB_TOO_MANY_SERVER_CERTS, $testresults['cert_oddities'])) { |
1050
|
|
|
$testresults['cert_oddities'][] = RADIUSTests::CERTPROB_TOO_MANY_SERVER_CERTS; |
1051
|
|
|
} |
1052
|
|
|
break; |
1053
|
|
|
case RADIUSTests::CA_ROOT: |
1054
|
|
|
if (!in_array(RADIUSTests::CERTPROB_ROOT_INCLUDED, $testresults['cert_oddities'])) { |
1055
|
|
|
$testresults['cert_oddities'][] = RADIUSTests::CERTPROB_ROOT_INCLUDED; |
1056
|
|
|
} |
1057
|
|
|
// do not save the root CA, it serves no purpose |
1058
|
|
|
// chain checks need to be against the UPLOADED CA of the |
1059
|
|
|
// IdP/profile, not against an EAP-discovered CA |
1060
|
|
|
break; |
1061
|
|
|
case RADIUSTests::CA_INTERMEDIATE: |
1062
|
|
|
$intermOdditiesEAP = array_merge($intermOdditiesEAP, $this->propertyCheckIntermediate($cert)); |
1063
|
|
|
$eapIntermediates[] = $certPem; |
1064
|
|
|
|
1065
|
|
|
if (isset($cert['CRL']) && isset($cert['CRL'][0])) { |
1066
|
|
|
$eapIntermediateCRLs[] = $cert['CRL'][0]; |
1067
|
|
|
} |
1068
|
|
|
break; |
1069
|
|
|
default: |
1070
|
|
|
throw new Exception("Status of certificate could not be determined!"); |
1071
|
|
|
} |
1072
|
|
|
$testresults['certdata'][] = $cert['full_details']; |
1073
|
|
|
} |
1074
|
|
|
switch (count($servercert)) { |
1075
|
|
|
case 0: |
1076
|
|
|
$testresults['cert_oddities'][] = RADIUSTests::CERTPROB_NO_SERVER_CERT; |
1077
|
|
|
break; |
1078
|
|
|
default: |
1079
|
|
|
// check (first) server cert's properties |
1080
|
|
|
$testresults['cert_oddities'] = array_merge($testresults['cert_oddities'], $this->propertyCheckServercert($servercert[0])); |
1081
|
|
|
$testresults['incoming_server_names'] = $servercert[0]['incoming_server_names']; |
1082
|
|
|
} |
1083
|
|
|
return [ |
1084
|
|
|
"SERVERCERT" => $servercert, |
1085
|
|
|
"INTERMEDIATE_CA" => $eapIntermediates, |
1086
|
|
|
"INTERMEDIATE_CRL" => $eapIntermediateCRLs, |
1087
|
|
|
"INTERMEDIATE_OBSERVED_ODDITIES" => $intermOdditiesEAP, |
1088
|
|
|
]; |
1089
|
|
|
} |
1090
|
|
|
|
1091
|
|
|
/** |
1092
|
|
|
* The big Guy. This performs an actual login with EAP and records how far |
1093
|
|
|
* it got and what oddities were observed along the way |
1094
|
|
|
* @param int $probeindex the probe we are connecting to (as set in product config) |
1095
|
|
|
* @param array $eaptype EAP type to use for connection |
1096
|
|
|
* @param string $innerUser inner username to try |
1097
|
|
|
* @param string $password password to try |
1098
|
|
|
* @param boolean $opnameCheck whether or not we check with Operator-Name set |
1099
|
|
|
* @param boolean $frag whether or not we check with an oversized packet forcing fragmentation |
1100
|
|
|
* @param string $clientcertdata client certificate credential to try |
1101
|
|
|
* @return int overall return code of the login test |
1102
|
|
|
* @throws Exception |
1103
|
|
|
*/ |
1104
|
|
|
public function udpLogin($probeindex, $eaptype, $innerUser, $password, $opnameCheck = TRUE, $frag = TRUE, $clientcertdata = NULL) |
1105
|
|
|
{ |
1106
|
|
|
/** preliminaries */ |
1107
|
|
|
$eapText = \core\common\EAP::eapDisplayName($eaptype); |
1108
|
|
|
// no host to send probes to? Nothing to do then |
1109
|
|
|
if (!isset(CONFIG_DIAGNOSTICS['RADIUSTESTS']['UDP-hosts'][$probeindex])) { |
1110
|
|
|
$this->UDP_reachability_executed = RADIUSTests::RETVAL_NOTCONFIGURED; |
1111
|
|
|
return RADIUSTests::RETVAL_NOTCONFIGURED; |
1112
|
|
|
} |
1113
|
|
|
// if we need client certs but don't have one, return |
1114
|
|
|
if (($eaptype == \core\common\EAP::EAPTYPE_ANY || $eaptype == \core\common\EAP::EAPTYPE_TLS) && $clientcertdata === NULL) { |
1115
|
|
|
$this->UDP_reachability_executed = RADIUSTests::RETVAL_NOTCONFIGURED; |
1116
|
|
|
return RADIUSTests::RETVAL_NOTCONFIGURED; |
1117
|
|
|
} |
1118
|
|
|
// if we don't have a string for outer EAP method name, give up |
1119
|
|
|
if (!isset($eapText['OUTER'])) { |
1120
|
|
|
$this->UDP_reachability_executed = RADIUSTests::RETVAL_NOTCONFIGURED; |
1121
|
|
|
return RADIUSTests::RETVAL_NOTCONFIGURED; |
1122
|
|
|
} |
1123
|
|
|
// we will need a config blob for wpa_supplicant, in a temporary directory |
1124
|
|
|
$temporary = $this->createTemporaryDirectory('test'); |
1125
|
|
|
$tmpDir = $temporary['dir']; |
1126
|
|
|
chdir($tmpDir); |
1127
|
|
|
$this->loggerInstance->debug(4, "temp dir: $tmpDir\n"); |
1128
|
|
|
if ($clientcertdata !== NULL) { |
1129
|
|
|
file_put_contents($tmpDir . "/client.p12", $clientcertdata); |
1130
|
|
|
} |
1131
|
|
|
$testresults = []; |
1132
|
|
|
// initialise the sub-array for cleaner parsing |
1133
|
|
|
$testresults['cert_oddities'] = []; |
1134
|
|
|
// execute RADIUS/EAP converation |
1135
|
|
|
$runtime_results = $this->executeEapolTest($tmpDir, $probeindex, $eaptype, $innerUser, $password, $opnameCheck, $frag); |
1136
|
|
|
$testresults['time_millisec'] = $runtime_results['time']; |
1137
|
|
|
$packetflow_orig = $runtime_results['output']; |
1138
|
|
|
$radiusResult = $this->checkRadiusPacketFlow($testresults, $packetflow_orig); |
1139
|
|
|
$negotiatedEapType = $this->wasEapTypeNegotiated($testresults, $packetflow_orig); |
1140
|
|
|
$testresults['negotiated_eaptype'] = $negotiatedEapType; |
1141
|
|
|
$negotiatedTlsVersion = $this->wasModernTlsNegotiated($testresults, $packetflow_orig); |
1142
|
|
|
$testresults['tls_version_eap'] = $negotiatedTlsVersion; |
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
|
|
|
if ( |
1147
|
|
|
$eaptype != \core\common\EAP::EAPTYPE_PWD && |
1148
|
|
|
(($radiusResult == RADIUSTests::RETVAL_CONVERSATION_REJECT && $negotiatedEapType) || $radiusResult == RADIUSTests::RETVAL_OK) |
1149
|
|
|
) { |
1150
|
|
|
$bundle = $this->extractIncomingCertsfromEAP($testresults, $tmpDir); |
1151
|
|
|
// FOR OWN REALMS check: |
1152
|
|
|
// 1) does the incoming chain have a root in one of the configured roots |
1153
|
|
|
// if not, this is a signficant configuration error |
1154
|
|
|
// return this with one or more of the CERTPROB_ constants (see defs) |
1155
|
|
|
// TRUST_ROOT_NOT_REACHED |
1156
|
|
|
// TRUST_ROOT_REACHED_ONLY_WITH_OOB_INTERMEDIATES |
1157
|
|
|
// then check the presented names |
1158
|
|
|
// check intermediate ca cert properties |
1159
|
|
|
// check trust chain for completeness |
1160
|
|
|
// works only for thorough checks, not shallow, so: |
1161
|
|
|
$intermOdditiesCAT = []; |
1162
|
|
|
$verifyResult = 0; |
1163
|
|
|
|
1164
|
|
|
if ($this->opMode == self::RADIUS_TEST_OPERATION_MODE_THOROUGH && $bundle !== FALSE) { |
1165
|
|
|
$verifyResult = $this->thoroughChainChecks($testresults, $intermOdditiesCAT, $tmpDir, $bundle["SERVERCERT"], $bundle["INTERMEDIATE_CA"], $bundle["INTERMEDIATE_CRL"]); |
1166
|
|
|
$this->thoroughNameChecks($bundle["SERVERCERT"][0], $testresults); |
1167
|
|
|
} |
1168
|
|
|
|
1169
|
|
|
$testresults['cert_oddities'] = array_merge($testresults['cert_oddities'], $bundle["INTERMEDIATE_OBSERVED_ODDITIES"] ?? []); |
1170
|
|
|
if (in_array(RADIUSTests::CERTPROB_OUTSIDE_VALIDITY_PERIOD, $intermOdditiesCAT) && $verifyResult == 3) { |
1171
|
|
|
$key = array_search(RADIUSTests::CERTPROB_OUTSIDE_VALIDITY_PERIOD, $intermOdditiesCAT); |
1172
|
|
|
$intermOdditiesCAT[$key] = RADIUSTests::CERTPROB_OUTSIDE_VALIDITY_PERIOD_WARN; |
1173
|
|
|
} |
1174
|
|
|
|
1175
|
|
|
$testresults['cert_oddities'] = array_merge($testresults['cert_oddities'], $intermOdditiesCAT); |
1176
|
|
|
|
1177
|
|
|
// mention trust chain failure only if no expired cert was in the chain; otherwise path validation will trivially fail |
1178
|
|
|
if (in_array(RADIUSTests::CERTPROB_OUTSIDE_VALIDITY_PERIOD, $testresults['cert_oddities'])) { |
1179
|
|
|
$this->loggerInstance->debug(4, "Deleting trust chain problem report, if present."); |
1180
|
|
|
if (($key = array_search(RADIUSTests::CERTPROB_TRUST_ROOT_NOT_REACHED, $testresults['cert_oddities'])) !== false) { |
1181
|
|
|
unset($testresults['cert_oddities'][$key]); |
1182
|
|
|
} |
1183
|
|
|
if (($key = array_search(RADIUSTests::CERTPROB_TRUST_ROOT_REACHED_ONLY_WITH_OOB_INTERMEDIATES, $testresults['cert_oddities'])) !== false) { |
1184
|
|
|
unset($testresults['cert_oddities'][$key]); |
1185
|
|
|
} |
1186
|
|
|
} |
1187
|
|
|
} |
1188
|
|
|
$this->UDP_reachability_result[$probeindex] = $testresults; |
1189
|
|
|
$this->UDP_reachability_executed = $radiusResult; |
1190
|
|
|
return $radiusResult; |
1191
|
|
|
} |
1192
|
|
|
|
1193
|
|
|
/** |
1194
|
|
|
* sets the outer identity to use in the checks |
1195
|
|
|
* |
1196
|
|
|
* @param string $id the outer ID to use |
1197
|
|
|
* @return void |
1198
|
|
|
*/ |
1199
|
|
|
public function setOuterIdentity($id) |
1200
|
|
|
{ |
1201
|
|
|
$this->outerUsernameForChecks = $id; |
1202
|
|
|
} |
1203
|
|
|
|
1204
|
|
|
/** |
1205
|
|
|
* pull together all sub tests into a cohesive test result |
1206
|
|
|
* @param int $host index of the probe for which the results are collated |
1207
|
|
|
* @return array |
1208
|
|
|
*/ |
1209
|
|
|
public function consolidateUdpResult($host) |
1210
|
|
|
{ |
1211
|
|
|
\core\common\Entity::intoThePotatoes(); |
1212
|
|
|
$ret = []; |
1213
|
|
|
$serverCert = []; |
1214
|
|
|
$udpResult = $this->UDP_reachability_result[$host]; |
1215
|
|
|
if (isset($udpResult['certdata']) && count($udpResult['certdata'])) { |
1216
|
|
|
foreach ($udpResult['certdata'] as $certdata) { |
1217
|
|
|
if ($certdata['type'] != 'server' && $certdata['type'] != 'totally_selfsigned') { |
1218
|
|
|
continue; |
1219
|
|
|
} |
1220
|
|
|
if (isset($certdata['extensions'])) { |
1221
|
|
|
foreach ($certdata['extensions'] as $k => $v) { |
1222
|
|
|
$certdata['extensions'][$k] = iconv('UTF-8', 'UTF-8//IGNORE', $certdata['extensions'][$k]); |
1223
|
|
|
} |
1224
|
|
|
} |
1225
|
|
|
$serverCert = [ |
1226
|
|
|
'subject' => $this->printDN($certdata['subject']), |
1227
|
|
|
'issuer' => $this->printDN($certdata['issuer']), |
1228
|
|
|
'validFrom' => $this->printTm($certdata['validFrom_time_t']), |
1229
|
|
|
'validTo' => $this->printTm($certdata['validTo_time_t']), |
1230
|
|
|
'serialNumber' => $certdata['serialNumber'] . sprintf(" (0x%X)", $certdata['serialNumber']), |
1231
|
|
|
'sha1' => $certdata['sha1'], |
1232
|
|
|
'extensions' => $certdata['extensions'] |
1233
|
|
|
]; |
1234
|
|
|
} |
1235
|
|
|
} |
1236
|
|
|
$ret['server_cert'] = $serverCert; |
1237
|
|
|
$ret['server'] = 0; |
1238
|
|
|
if (isset($udpResult['incoming_server_names'][0])) { |
1239
|
|
|
$ret['server'] = sprintf(_("Connected to %s."), $udpResult['incoming_server_names'][0]); |
1240
|
|
|
} |
1241
|
|
|
$ret['level'] = \core\common\Entity::L_OK; |
1242
|
|
|
$ret['time_millisec'] = sprintf("%d", $udpResult['time_millisec']); |
1243
|
|
|
if (empty($udpResult['cert_oddities'])) { |
1244
|
|
|
$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."); |
1245
|
|
|
\core\common\Entity::outOfThePotatoes(); |
1246
|
|
|
return $ret; |
1247
|
|
|
} |
1248
|
|
|
|
1249
|
|
|
$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."); |
1250
|
|
|
$ret['cert_oddities'] = []; |
1251
|
|
|
foreach ($udpResult['cert_oddities'] as $oddity) { |
1252
|
|
|
$o = []; |
1253
|
|
|
$o['code'] = $oddity; |
1254
|
|
|
$o['message'] = isset($this->returnCodes[$oddity]["message"]) && $this->returnCodes[$oddity]["message"] ? $this->returnCodes[$oddity]["message"] : $oddity; |
1255
|
|
|
$o['level'] = $this->returnCodes[$oddity]["severity"]; |
1256
|
|
|
$ret['level'] = max($ret['level'], $this->returnCodes[$oddity]["severity"]); |
1257
|
|
|
$ret['cert_oddities'][] = $o; |
1258
|
|
|
} |
1259
|
|
|
\core\common\Entity::outOfThePotatoes(); |
1260
|
|
|
return $ret; |
1261
|
|
|
} |
1262
|
|
|
|
1263
|
|
|
} |
1264
|
|
|
|
The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.
The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.
To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.