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