| Total Complexity | 86 |
| Total Lines | 734 |
| Duplicated Lines | 1.63 % |
| Changes | 0 | ||
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like ProfileSilverbullet often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use ProfileSilverbullet, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 39 | class ProfileSilverbullet extends AbstractProfile { |
||
| 40 | |||
| 41 | const SB_TOKENSTATUS_VALID = 0; |
||
| 42 | const SB_TOKENSTATUS_PARTIALLY_REDEEMED = 1; |
||
| 43 | const SB_TOKENSTATUS_REDEEMED = 2; |
||
| 44 | const SB_TOKENSTATUS_EXPIRED = 3; |
||
| 45 | const SB_TOKENSTATUS_INVALID = 4; |
||
| 46 | const SB_CERTSTATUS_VALID = 1; |
||
| 47 | const SB_CERTSTATUS_EXPIRED = 2; |
||
| 48 | const SB_CERTSTATUS_REVOKED = 3; |
||
| 49 | const SB_ACKNOWLEDGEMENT_REQUIRED_DAYS = 365; |
||
| 50 | |||
| 51 | public $termsAndConditions; |
||
| 52 | |||
| 53 | /* |
||
| 54 | * |
||
| 55 | */ |
||
| 56 | |||
| 57 | const PRODUCTNAME = "Managed IdP"; |
||
| 58 | |||
| 59 | public static function randomString( |
||
| 60 | $length, $keyspace = '23456789abcdefghijkmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' |
||
| 61 | ) { |
||
| 62 | $str = ''; |
||
| 63 | $max = strlen($keyspace) - 1; |
||
| 64 | if ($max < 1) { |
||
| 65 | throw new Exception('$keyspace must be at least two characters long'); |
||
| 66 | } |
||
| 67 | for ($i = 0; $i < $length; ++$i) { |
||
| 68 | $str .= $keyspace[random_int(0, $max)]; |
||
| 69 | } |
||
| 70 | return $str; |
||
| 71 | } |
||
| 72 | |||
| 73 | /** |
||
| 74 | * Class constructor for existing profiles (use IdP::newProfile() to actually create one). Retrieves all attributes and |
||
| 75 | * supported EAP types from the DB and stores them in the priv_ arrays. |
||
| 76 | * |
||
| 77 | * @param int $profileId identifier of the profile in the DB |
||
| 78 | * @param IdP $idpObject optionally, the institution to which this Profile belongs. Saves the construction of the IdP instance. If omitted, an extra query and instantiation is executed to find out. |
||
| 79 | */ |
||
| 80 | public function __construct($profileId, $idpObject = NULL) { |
||
| 81 | parent::__construct($profileId, $idpObject); |
||
| 82 | |||
| 83 | $this->entityOptionTable = "profile_option"; |
||
| 84 | $this->entityIdColumn = "profile_id"; |
||
| 85 | $this->attributes = []; |
||
| 86 | |||
| 87 | $tempMaxUsers = 200; // abolutely last resort fallback if no per-fed and no config option |
||
| 88 | // set to global config value |
||
| 89 | |||
| 90 | if (isset(CONFIG_CONFASSISTANT['SILVERBULLET']['default_maxusers'])) { |
||
| 91 | $tempMaxUsers = CONFIG_CONFASSISTANT['SILVERBULLET']['default_maxusers']; |
||
| 92 | } |
||
| 93 | $myInst = new IdP($this->institution); |
||
| 94 | $myFed = new Federation($myInst->federation); |
||
| 95 | $fedMaxusers = $myFed->getAttributes("fed:silverbullet-maxusers"); |
||
| 96 | if (isset($fedMaxusers[0])) { |
||
| 97 | $tempMaxUsers = $fedMaxusers[0]['value']; |
||
| 98 | } |
||
| 99 | |||
| 100 | // realm is automatically calculated, then stored in DB |
||
| 101 | |||
| 102 | $this->realm = "opaquehash@$myInst->identifier-$this->identifier." . strtolower($myInst->federation) . CONFIG_CONFASSISTANT['SILVERBULLET']['realm_suffix']; |
||
| 103 | $this->setRealm($myInst->identifier."-".$this->identifier."." . strtolower($myInst->federation) . strtolower(CONFIG_CONFASSISTANT['SILVERBULLET']['realm_suffix'])); |
||
| 104 | $localValueIfAny = ""; |
||
| 105 | |||
| 106 | // but there's some common internal attributes populated directly |
||
| 107 | $internalAttributes = [ |
||
| 108 | "internal:profile_count" => $this->idpNumberOfProfiles, |
||
| 109 | "internal:realm" => preg_replace('/^.*@/', '', $this->realm), |
||
| 110 | "internal:use_anon_outer" => FALSE, |
||
| 111 | "internal:checkuser_outer" => TRUE, |
||
| 112 | "internal:checkuser_value" => "anonymous", |
||
| 113 | "internal:anon_local_value" => $localValueIfAny, |
||
| 114 | "internal:silverbullet_maxusers" => $tempMaxUsers, |
||
| 115 | "profile:production" => "on", |
||
| 116 | ]; |
||
| 117 | |||
| 118 | // and we need to populate eap:server_name and eap:ca_file with the NRO-specific EAP information |
||
| 119 | $silverbulletAttributes = [ |
||
| 120 | "eap:server_name" => "auth." . strtolower($myFed->identifier) . CONFIG_CONFASSISTANT['SILVERBULLET']['server_suffix'], |
||
| 121 | ]; |
||
| 122 | $x509 = new \core\common\X509(); |
||
| 123 | $caHandle = fopen(dirname(__FILE__) . "/../config/SilverbulletServerCerts/" . strtoupper($myFed->identifier) . "/root.pem", "r"); |
||
| 124 | if ($caHandle !== FALSE) { |
||
| 125 | $cAFile = fread($caHandle, 16000000); |
||
| 126 | $silverbulletAttributes["eap:ca_file"] = $x509->der2pem(($x509->pem2der($cAFile))); |
||
|
|
|||
| 127 | } |
||
| 128 | |||
| 129 | $temp = array_merge($this->addInternalAttributes($internalAttributes), $this->addInternalAttributes($silverbulletAttributes)); |
||
| 130 | $tempArrayProfLevel = array_merge($this->addDatabaseAttributes(), $temp); |
||
| 131 | |||
| 132 | // now, fetch and merge IdP-wide attributes |
||
| 133 | |||
| 134 | $this->attributes = $this->levelPrecedenceAttributeJoin($tempArrayProfLevel, $this->idpAttributes, "IdP"); |
||
| 135 | |||
| 136 | $this->privEaptypes = $this->fetchEAPMethods(); |
||
| 137 | |||
| 138 | $this->name = ProfileSilverbullet::PRODUCTNAME; |
||
| 139 | |||
| 140 | $this->loggerInstance->debug(3, "--- END Constructing new Profile object ... ---\n"); |
||
| 141 | |||
| 142 | $this->termsAndConditions = "<h2>Product Definition</h2> |
||
| 143 | <p>" . \core\ProfileSilverbullet::PRODUCTNAME . " outsources the technical setup of " . CONFIG_CONFASSISTANT['CONSORTIUM']['display_name'] . " " . CONFIG_CONFASSISTANT['CONSORTIUM']['nomenclature_institution'] . " functions to the " . CONFIG_CONFASSISTANT['CONSORTIUM']['display_name'] . " Operations Team. The system includes</p> |
||
| 144 | <ul> |
||
| 145 | <li>a web-based user management interface where user accounts and access credentials can be created and revoked (there is a limit to the number of active users)</li> |
||
| 146 | <li>a technical infrastructure ('CA') which issues and revokes credentials</li> |
||
| 147 | <li>a technical infrastructure ('RADIUS') which verifies access credentials and subsequently grants access to " . CONFIG_CONFASSISTANT['CONSORTIUM']['display_name'] . "</li> |
||
| 148 | <li><span style='color: red;'>TBD: a lookup/notification system which informs you of network abuse complaints by " . CONFIG_CONFASSISTANT['CONSORTIUM']['display_name'] . " Service Providers that pertain to your users</span></li> |
||
| 149 | </ul> |
||
| 150 | <h2>User Account Liability</h2> |
||
| 151 | <p>As an " . CONFIG_CONFASSISTANT['CONSORTIUM']['display_name'] . " " . CONFIG_CONFASSISTANT['CONSORTIUM']['nomenclature_institution'] . " administrator using this system, you are authorized to create user accounts according to your local " . CONFIG_CONFASSISTANT['CONSORTIUM']['nomenclature_institution'] . " policy. You are fully responsible for the accounts you issue and are the data controller for all user information you deposit in this system; the system is a data processor.</p>"; |
||
| 152 | $this->termsAndConditions .= "<p>Your responsibilities include that you</p> |
||
| 153 | <ul> |
||
| 154 | <li>only issue accounts to members of your " . CONFIG_CONFASSISTANT['CONSORTIUM']['nomenclature_institution'] . ", as defined by your local policy.</li> |
||
| 155 | <li>must make sure that all accounts that you issue can be linked by you to actual human end users</li> |
||
| 156 | <li>have to immediately revoke accounts of users when they leave or otherwise stop being a member of your " . CONFIG_CONFASSISTANT['CONSORTIUM']['nomenclature_institution'] . "</li> |
||
| 157 | <li>will act upon notifications about possible network abuse by your users and will appropriately sanction them</li> |
||
| 158 | </ul> |
||
| 159 | <p>"; |
||
| 160 | $this->termsAndConditions .= "Failure to comply with these requirements may make your " . CONFIG_CONFASSISTANT['CONSORTIUM']['nomenclature_federation'] . " act on your behalf, which you authorise, and will ultimately lead to the deletion of your " . CONFIG_CONFASSISTANT['CONSORTIUM']['nomenclature_institution'] . " (and all the users you create inside) in this system."; |
||
| 161 | $this->termsAndConditions .= "</p> |
||
| 162 | <h2>Privacy</h2> |
||
| 163 | <p>With " . \core\ProfileSilverbullet::PRODUCTNAME . ", we are necessarily storing personally identifiable information about the end users you create. While the actual human is only identifiable with your help, we consider all the user data as relevant in terms of privacy jurisdiction. Please note that</p> |
||
| 164 | <ul> |
||
| 165 | <li>You are the only one who needs to be able to make a link to the human behind the usernames you create. The usernames you create in the system have to be rich enough to allow you to make that identification step. Also consider situations when you are unavailable or leave the organisation and someone else needs to perform the matching to an individual.</li> |
||
| 166 | <li>The identifiers we create in the credentials are not linked to the usernames you add to the system; they are randomly generated pseudonyms.</li> |
||
| 167 | <li>Each access credential carries a different pseudonym, even if it pertains to the same username.</li> |
||
| 168 | <li>If you choose to deposit users' email addresses in the system, you authorise the system to send emails on your behalf regarding operationally relevant events to the users in question (e.g. notification of nearing expiry dates of credentials, notification of access revocation). |
||
| 169 | </ul>"; |
||
| 170 | } |
||
| 171 | |||
| 172 | public function invitationMailSubject() { |
||
| 173 | return sprintf(_("Your %s access is ready"), CONFIG_CONFASSISTANT['CONSORTIUM']['display_name']); |
||
| 174 | } |
||
| 175 | |||
| 176 | public function invitationMailBody($invitationLink) { |
||
| 190 | } |
||
| 191 | |||
| 192 | /** |
||
| 193 | * Updates database with new installer location; NOOP because we do not |
||
| 194 | * cache anything in Silverbullet |
||
| 195 | * |
||
| 196 | * @param string $device the device identifier string |
||
| 197 | * @param string $path the path where the new installer can be found |
||
| 198 | * @param string $mime the mime type of the new installer |
||
| 199 | * @param int $integerEapType the inter-representation of the EAP type that is configured in this installer |
||
| 200 | */ |
||
| 201 | public function updateCache($device, $path, $mime, $integerEapType) { |
||
| 202 | // caching is not supported in SB (private key in installers) |
||
| 203 | // the following merely makes the "unused parameter" warnings go away |
||
| 204 | // the FALSE in condition one makes sure it never gets executed |
||
| 205 | if (FALSE || $device == "Macbeth" || $path == "heath" || $mime == "application/witchcraft" || $integerEapType == 0) { |
||
| 206 | throw new Exception("FALSE is TRUE, and TRUE is FALSE! Hover through the browser and filthy code!"); |
||
| 207 | } |
||
| 208 | } |
||
| 209 | |||
| 210 | /** |
||
| 211 | * register new supported EAP method for this profile |
||
| 212 | * |
||
| 213 | * @param \core\common\EAP $type The EAP Type, as defined in class EAP |
||
| 214 | * @param int $preference preference of this EAP Type. If a preference value is re-used, the order of EAP types of the same preference level is undefined. |
||
| 215 | * |
||
| 216 | */ |
||
| 217 | public function addSupportedEapMethod(\core\common\EAP $type, $preference) { |
||
| 224 | } |
||
| 225 | |||
| 226 | /** |
||
| 227 | * It's EAP-TLS and there is no point in anonymity |
||
| 228 | * @param boolean $shallwe |
||
| 229 | */ |
||
| 230 | public function setAnonymousIDSupport($shallwe) { |
||
| 231 | // we don't do anonymous outer IDs in SB |
||
| 232 | if ($shallwe === TRUE) { |
||
| 233 | throw new Exception("Silverbullet: attempt to add anonymous outer ID support to a SB profile!"); |
||
| 234 | } |
||
| 235 | $this->databaseHandle->exec("UPDATE profile SET use_anon_outer = 0 WHERE profile_id = $this->identifier"); |
||
| 236 | } |
||
| 237 | |||
| 238 | /** |
||
| 239 | * create a CSR |
||
| 240 | * |
||
| 241 | * @return |
||
| 242 | */ |
||
| 243 | private function generateCsr($privateKey) { |
||
| 244 | // token leads us to the NRO, to set the OU property of the cert |
||
| 245 | $inst = new IdP($this->institution); |
||
| 246 | $federation = strtoupper($inst->federation); |
||
| 247 | $usernameIsUnique = FALSE; |
||
| 248 | $username = ""; |
||
| 249 | while ($usernameIsUnique === FALSE) { |
||
| 250 | $usernameLocalPart = self::randomString(64 - 1 - strlen($this->realm), "0123456789abcdefghijklmnopqrstuvwxyz"); |
||
| 251 | $username = $usernameLocalPart . "@" . $this->realm; |
||
| 252 | $uniquenessQuery = $this->databaseHandle->exec("SELECT cn from silverbullet_certificate WHERE cn = ?", "s", $username); |
||
| 253 | // SELECT -> resource, not boolean |
||
| 254 | if (mysqli_num_rows(/** @scrutinizer ignore-type */ $uniquenessQuery) == 0) { |
||
| 255 | $usernameIsUnique = TRUE; |
||
| 256 | } |
||
| 257 | } |
||
| 258 | |||
| 259 | $this->loggerInstance->debug(5, "generateCertificate: generating private key.\n"); |
||
| 260 | |||
| 261 | return [ |
||
| 262 | "CSR" => openssl_csr_new( |
||
| 263 | ['O' => CONFIG_CONFASSISTANT['CONSORTIUM']['name'], |
||
| 264 | 'OU' => $federation, |
||
| 265 | 'CN' => $username, |
||
| 266 | 'emailAddress' => $username, |
||
| 267 | ], $privateKey, [ |
||
| 268 | 'digest_alg' => 'sha256', |
||
| 269 | 'req_extensions' => 'v3_req', |
||
| 270 | ] |
||
| 271 | ), |
||
| 272 | "USERNAME" => $username |
||
| 273 | ]; |
||
| 274 | } |
||
| 275 | |||
| 276 | /** |
||
| 277 | * take a CSR and sign it with our issuing CA's certificate |
||
| 278 | * |
||
| 279 | * @param mixed $csr the CSR |
||
| 280 | */ |
||
| 281 | private function signCsr($csr, $expiryDays) { |
||
| 282 | switch (CONFIG_CONFASSISTANT['SILVERBULLET']['CA']['type']) { |
||
| 283 | case "embedded": |
||
| 284 | $rootCaPem = file_get_contents(ROOT . "/config/SilverbulletClientCerts/rootca.pem"); |
||
| 285 | $issuingCaPem = file_get_contents(ROOT . "/config/SilverbulletClientCerts/real.pem"); |
||
| 286 | $issuingCa = openssl_x509_read($issuingCaPem); |
||
| 287 | $issuingCaKey = openssl_pkey_get_private("file://" . ROOT . "/config/SilverbulletClientCerts/real.key"); |
||
| 288 | $nonDupSerialFound = FALSE; |
||
| 289 | do { |
||
| 290 | $serial = random_int(1000000000, PHP_INT_MAX); |
||
| 291 | $dupeQuery = $this->databaseHandle->exec("SELECT serial_number FROM silverbullet_certificate WHERE serial_number = ?", "i", $serial); |
||
| 292 | // SELECT -> resource, not boolean |
||
| 293 | if (mysqli_num_rows(/** @scrutinizer ignore-type */$dupeQuery) == 0) { |
||
| 294 | $nonDupSerialFound = TRUE; |
||
| 295 | } |
||
| 296 | } while (!$nonDupSerialFound); |
||
| 297 | $this->loggerInstance->debug(5, "generateCertificate: signing imminent with unique serial $serial.\n"); |
||
| 298 | return [ |
||
| 299 | "CERT" => openssl_csr_sign($csr, $issuingCa, $issuingCaKey, $expiryDays, ['digest_alg' => 'sha256'], $serial), |
||
| 300 | "SERIAL" => $serial, |
||
| 301 | "ISSUER" => $issuingCaPem, |
||
| 302 | "ROOT" => $rootCaPem, |
||
| 303 | ]; |
||
| 304 | default: |
||
| 305 | /* HTTP POST the CSR to the CA with the $expiryDays as parameter |
||
| 306 | * on successful execution, gets back a PEM file which is the |
||
| 307 | * certificate (structure TBD) |
||
| 308 | * $httpResponse = httpRequest("https://clientca.hosted.eduroam.org/issue/", ["csr" => $csr, "expiry" => $expiryDays ] ); |
||
| 309 | * |
||
| 310 | * The result of this if clause has to be a certificate in PHP's |
||
| 311 | * "openssl_object" style (like the one that openssl_csr_sign would |
||
| 312 | * produce), to be stored in the variable $cert; we also need the |
||
| 313 | * serial - which can be extracted from the received cert and has |
||
| 314 | * to be stored in $serial. |
||
| 315 | */ |
||
| 316 | throw new Exception("External silverbullet CA is not implemented yet!"); |
||
| 317 | } |
||
| 318 | } |
||
| 319 | |||
| 320 | /** |
||
| 321 | * issue a certificate based on a token |
||
| 322 | * |
||
| 323 | * @param string $token |
||
| 324 | * @param string $importPassword |
||
| 325 | * @return array |
||
| 326 | */ |
||
| 327 | public function issueCertificate($token, $importPassword) { |
||
| 406 | ]; |
||
| 407 | } |
||
| 408 | |||
| 409 | /** |
||
| 410 | * triggers a new OCSP statement for the given serial number |
||
| 411 | * |
||
| 412 | * @param int $serial the serial number of the cert in question (decimal) |
||
| 413 | * @return string DER-encoded OCSP status info (binary data!) |
||
| 414 | */ |
||
| 415 | public static function triggerNewOCSPStatement($serial) { |
||
| 416 | $logHandle = new \core\common\Logging(); |
||
| 417 | $logHandle->debug(2, "Triggering new OCSP statement for serial $serial.\n"); |
||
| 418 | $ocsp = ""; // the statement |
||
| 419 | switch (CONFIG_CONFASSISTANT['SILVERBULLET']['CA']['type']) { |
||
| 420 | case "embedded": |
||
| 421 | // get all relevant info from DB |
||
| 422 | $cn = ""; |
||
| 423 | $federation = NULL; |
||
| 424 | $certstatus = ""; |
||
| 425 | $originalExpiry = date_create_from_format("Y-m-d H:i:s", "2000-01-01 00:00:00"); |
||
| 426 | $dbHandle = DBConnection::handle("INST"); |
||
| 427 | $originalStatusQuery = $dbHandle->exec("SELECT profile_id, cn, revocation_status, expiry, revocation_time, OCSP FROM silverbullet_certificate WHERE serial_number = ?", "i", $serial); |
||
| 428 | // SELECT -> resource, not boolean |
||
| 429 | if (mysqli_num_rows(/** @scrutinizer ignore-type */ $originalStatusQuery) > 0) { |
||
| 430 | $certstatus = "V"; |
||
| 431 | } |
||
| 432 | while ($runner = mysqli_fetch_object(/** @scrutinizer ignore-type */ $originalStatusQuery)) { // there can be only one row |
||
| 433 | if ($runner->revocation_status == "REVOKED") { |
||
| 434 | // already revoked, simply return canned OCSP response |
||
| 435 | $certstatus = "R"; |
||
| 436 | } |
||
| 437 | $originalExpiry = date_create_from_format("Y-m-d H:i:s", $runner->expiry); |
||
| 438 | if ($originalExpiry === FALSE) { |
||
| 439 | throw new Exception("Unable to calculate original expiry date, input data bogus!"); |
||
| 440 | } |
||
| 441 | $validity = date_diff(/** @scrutinizer ignore-type */ date_create(), $originalExpiry); |
||
| 442 | if ($validity->invert == 1) { |
||
| 443 | // negative! Cert is already expired, no need to revoke. |
||
| 444 | // No need to return anything really, but do return the last known OCSP statement to prevent special case |
||
| 445 | $certstatus = "E"; |
||
| 446 | } |
||
| 447 | $cn = $runner->cn; |
||
| 448 | $profile = new ProfileSilverbullet($runner->profile_id); |
||
| 449 | $inst = new IdP($profile->institution); |
||
| 450 | $federation = strtoupper($inst->federation); |
||
| 451 | } |
||
| 452 | |||
| 453 | // generate stub index.txt file |
||
| 454 | $cat = new CAT(); |
||
| 455 | $tempdirArray = $cat->createTemporaryDirectory("test"); |
||
| 456 | $tempdir = $tempdirArray['dir']; |
||
| 457 | $nowIndexTxt = (new \DateTime())->format("ymdHis") . "Z"; |
||
| 458 | $expiryIndexTxt = $originalExpiry->format("ymdHis") . "Z"; |
||
| 459 | $serialHex = strtoupper(dechex($serial)); |
||
| 460 | if (strlen($serialHex) % 2 == 1) { |
||
| 461 | $serialHex = "0" . $serialHex; |
||
| 462 | } |
||
| 463 | |||
| 464 | $indexStatement = "$certstatus\t$expiryIndexTxt\t" . ($certstatus == "R" ? "$nowIndexTxt,unspecified" : "") . "\t$serialHex\tunknown\t/O=" . CONFIG_CONFASSISTANT['CONSORTIUM']['name'] . "/OU=$federation/CN=$cn/emailAddress=$cn\n"; |
||
| 465 | $logHandle->debug(4, "index.txt contents-to-be: $indexStatement"); |
||
| 466 | if (!file_put_contents($tempdir . "/index.txt", $indexStatement)) { |
||
| 467 | $logHandle->debug(1,"Unable to write openssl index.txt file for revocation handling!"); |
||
| 468 | } |
||
| 469 | // index.txt.attr is dull but needs to exist |
||
| 470 | file_put_contents($tempdir . "/index.txt.attr", "unique_subject = yes\n"); |
||
| 471 | // call "openssl ocsp" to manufacture our own OCSP statement |
||
| 472 | // adding "-rmd sha1" to the following command-line makes the |
||
| 473 | // choice of signature algorithm for the response explicit |
||
| 474 | // but it's only available from openssl-1.1.0 (which we do not |
||
| 475 | // want to require just for that one thing). |
||
| 476 | $execCmd = CONFIG['PATHS']['openssl'] . " ocsp -issuer " . ROOT . "/config/SilverbulletClientCerts/real.pem -sha1 -ndays 10 -no_nonce -serial 0x$serialHex -CA " . ROOT . "/config/SilverbulletClientCerts/real.pem -rsigner " . ROOT . "/config/SilverbulletClientCerts/real.pem -rkey " . ROOT . "/config/SilverbulletClientCerts/real.key -index $tempdir/index.txt -no_cert_verify -respout $tempdir/$serialHex.response.der"; |
||
| 477 | $logHandle->debug(2, "Calling openssl ocsp with following cmdline: $execCmd\n"); |
||
| 478 | $output = []; |
||
| 479 | $return = 999; |
||
| 480 | exec($execCmd, $output, $return); |
||
| 481 | if ($return !== 0) { |
||
| 482 | throw new Exception("Non-zero return value from openssl ocsp!"); |
||
| 483 | } |
||
| 484 | $ocspFile = fopen($tempdir . "/$serialHex.response.der", "r"); |
||
| 485 | $ocsp = fread($ocspFile, 1000000); |
||
| 486 | fclose($ocspFile); |
||
| 487 | break; |
||
| 488 | default: |
||
| 489 | /* HTTP POST the serial to the CA. The CA knows about the state of |
||
| 490 | * the certificate. |
||
| 491 | * |
||
| 492 | * $httpResponse = httpRequest("https://clientca.hosted.eduroam.org/ocsp/", ["serial" => $serial ] ); |
||
| 493 | * |
||
| 494 | * The result of this if clause has to be a DER-encoded OCSP statement |
||
| 495 | * to be stored in the variable $ocsp |
||
| 496 | */ |
||
| 497 | throw new Exception("External silverbullet CA is not implemented yet!"); |
||
| 498 | } |
||
| 499 | // write the new statement into DB |
||
| 500 | $dbHandle->exec("UPDATE silverbullet_certificate SET OCSP = ?, OCSP_timestamp = NOW() WHERE serial_number = ?", "si", $ocsp, $serial); |
||
| 501 | return $ocsp; |
||
| 502 | } |
||
| 503 | |||
| 504 | /** |
||
| 505 | * revokes a certificate |
||
| 506 | * @param int $serial the serial number of the cert to revoke (decimal!) |
||
| 507 | * @return array with revocation information |
||
| 508 | */ |
||
| 509 | public function revokeCertificate($serial) { |
||
| 510 | |||
| 511 | |||
| 512 | // TODO for now, just mark as revoked in the certificates table (and use the stub OCSP updater) |
||
| 513 | $nowSql = (new \DateTime())->format("Y-m-d H:i:s"); |
||
| 514 | if (CONFIG_CONFASSISTANT['SILVERBULLET']['CA']['type'] != "embedded") { |
||
| 515 | // send revocation request to CA. |
||
| 516 | // $httpResponse = httpRequest("https://clientca.hosted.eduroam.org/revoke/", ["serial" => $serial ] ); |
||
| 517 | throw new Exception("External silverbullet CA is not implemented yet!"); |
||
| 518 | } |
||
| 519 | // regardless if embedded or not, always keep local state in our own DB |
||
| 520 | $this->databaseHandle->exec("UPDATE silverbullet_certificate SET revocation_status = 'REVOKED', revocation_time = ? WHERE serial_number = ?", "si", $nowSql, $serial); |
||
| 521 | $this->loggerInstance->debug(2, "Certificate revocation status updated, about to call triggerNewOCSPStatement($serial).\n"); |
||
| 522 | $ocsp = ProfileSilverbullet::triggerNewOCSPStatement($serial); |
||
| 523 | return ["OCSP" => $ocsp]; |
||
| 524 | } |
||
| 525 | |||
| 526 | /** |
||
| 527 | * |
||
| 528 | * @param string $url the URL to send the request to |
||
| 529 | * @param array $postValues POST values to send |
||
| 530 | */ |
||
| 531 | private function httpRequest($url, $postValues) { |
||
| 532 | $options = [ |
||
| 533 | 'http' => ['header' => 'Content-type: application/x-www-form-urlencoded\r\n', "method" => 'POST', 'content' => http_build_query($postValues)] |
||
| 534 | ]; |
||
| 535 | $context = stream_context_create($options); |
||
| 536 | return file_get_contents($url, false, $context); |
||
| 537 | } |
||
| 538 | |||
| 539 | private static function enumerateCertDetails($certQuery) { |
||
| 540 | $retval = []; |
||
| 541 | while ($resource = mysqli_fetch_object($certQuery)) { |
||
| 542 | // is the cert expired? |
||
| 543 | $now = new \DateTime(); |
||
| 544 | $cert_expiry = new \DateTime($resource->expiry); |
||
| 545 | $delta = $now->diff($cert_expiry); |
||
| 546 | $certStatus = ($delta->invert == 1 ? self::SB_CERTSTATUS_EXPIRED : self::SB_CERTSTATUS_VALID); |
||
| 547 | // expired is expired; even if it was previously revoked. But do update status for revoked ones... |
||
| 548 | if ($certStatus == self::SB_CERTSTATUS_VALID && $resource->revocation_status == "REVOKED") { |
||
| 549 | $certStatus = self::SB_CERTSTATUS_REVOKED; |
||
| 550 | } |
||
| 551 | $retval[] = [ |
||
| 552 | "status" => $certStatus, |
||
| 553 | "serial" => $resource->serial_number, |
||
| 554 | "name" => $resource->cn, |
||
| 555 | "issued" => $resource->issued, |
||
| 556 | "expiry" => $resource->expiry, |
||
| 557 | "device" => $resource->device, |
||
| 558 | ]; |
||
| 559 | } |
||
| 560 | return $retval; |
||
| 561 | } |
||
| 562 | |||
| 563 | public static function tokenStatus($tokenvalue) { |
||
| 564 | $databaseHandle = DBConnection::handle("INST"); |
||
| 565 | $loggerInstance = new \core\common\Logging(); |
||
| 566 | |||
| 567 | /* |
||
| 568 | * Finds invitation by its token attribute and loads all certificates generated using the token. |
||
| 569 | * Certificate details will always be empty, since code still needs to be adapted to return multiple certificates information. |
||
| 570 | */ |
||
| 571 | $invColumnNames = "`id`, `profile_id`, `silverbullet_user_id`, `token`, `quantity`, `expiry`"; |
||
| 572 | $invitationsResult = $databaseHandle->exec("SELECT $invColumnNames FROM `silverbullet_invitation` WHERE `token`=? ORDER BY `expiry` DESC", "s", $tokenvalue); |
||
| 573 | // SELECT -> resource, no boolean |
||
| 574 | if ($invitationsResult->num_rows == 0) { |
||
| 575 | $loggerInstance->debug(2, "Token $tokenvalue not found in database or database query error!\n"); |
||
| 576 | return ["status" => self::SB_TOKENSTATUS_INVALID, |
||
| 577 | "cert_status" => [],]; |
||
| 578 | } |
||
| 579 | // if not returned, we found the token in the DB |
||
| 580 | $invitationRow = mysqli_fetch_object(/** @scrutinizer ignore-type */ $invitationsResult); |
||
| 581 | $rowId = $invitationRow->id; |
||
| 582 | $certColumnNames = "`id`, `profile_id`, `silverbullet_user_id`, `silverbullet_invitation_id`, `serial_number`, `cn`, `issued`, `expiry`, `device`, `revocation_status`, `revocation_time`, `OCSP`, `OCSP_timestamp`"; |
||
| 583 | $certificatesResult = $databaseHandle->exec("SELECT $certColumnNames FROM `silverbullet_certificate` WHERE `silverbullet_invitation_id` = ? ORDER BY `revocation_status`, `expiry` DESC", "i", $rowId); |
||
| 584 | $certificatesNumber = ($certificatesResult ? $certificatesResult->num_rows : 0); |
||
| 585 | $loggerInstance->debug(5, "At token validation level, " . $certificatesNumber . " certificates exist.\n"); |
||
| 586 | |||
| 587 | $retArray = [ |
||
| 588 | "cert_status" => \core\ProfileSilverbullet::enumerateCertDetails($certificatesResult), |
||
| 589 | "profile" => $invitationRow->profile_id, |
||
| 590 | "user" => $invitationRow->silverbullet_user_id, |
||
| 591 | "expiry" => $invitationRow->expiry, |
||
| 592 | "activations_remaining" => $invitationRow->quantity - $certificatesNumber, |
||
| 593 | "activations_total" => $invitationRow->quantity, |
||
| 594 | "value" => $invitationRow->token, |
||
| 595 | "db_id" => $invitationRow->id, |
||
| 596 | ]; |
||
| 597 | |||
| 598 | switch ($certificatesNumber) { |
||
| 599 | case 0: |
||
| 600 | // find out if it has expired |
||
| 601 | $now = new \DateTime(); |
||
| 602 | $expiryObject = new \DateTime($invitationRow->expiry); |
||
| 603 | $delta = $now->diff($expiryObject); |
||
| 604 | if ($delta->invert == 1) { |
||
| 605 | $retArray['status'] = self::SB_TOKENSTATUS_EXPIRED; |
||
| 606 | $retArray['activations_remaining'] = 0; |
||
| 607 | break; |
||
| 608 | } |
||
| 609 | $retArray['status'] = self::SB_TOKENSTATUS_VALID; |
||
| 610 | break; |
||
| 611 | case $invitationRow->quantity: |
||
| 612 | $retArray['status'] = self::SB_TOKENSTATUS_REDEEMED; |
||
| 613 | break; |
||
| 614 | default: |
||
| 615 | assert($certificatesNumber > 0); // no negatives allowed |
||
| 616 | assert($certificatesNumber < $invitationRow->quantity || $invitationRow->quantity == 0); // not more than max quantity allowed (unless quantity is zero) |
||
| 617 | $retArray['status'] = self::SB_TOKENSTATUS_PARTIALLY_REDEEMED; |
||
| 618 | } |
||
| 619 | |||
| 620 | // now, look up certificate details and put them all in the cert_status property |
||
| 621 | |||
| 622 | $loggerInstance->debug(5, "tokenStatus: done, returning " . $retArray['status'] . ", " . count($retArray['cert_status']) . ", " . $retArray['profile'] . ", " . $retArray['user'] . ", " . $retArray['expiry'] . ", " . $retArray['value'] . "\n"); |
||
| 623 | return $retArray; |
||
| 624 | } |
||
| 625 | |||
| 626 | /** |
||
| 627 | * For a given certificate username, find the profile and username in CAT |
||
| 628 | * this needs to be static because we do not have a known profile instance |
||
| 629 | * |
||
| 630 | * @param string $certUsername a username from CN or sAN:email |
||
| 631 | */ |
||
| 632 | public static function findUserIdFromCert($certUsername) { |
||
| 633 | $dbHandle = \core\DBConnection::handle("INST"); |
||
| 634 | $userrows = $dbHandle->exec("SELECT silverbullet_user_id AS user_id, profile_id AS profile FROM silverbullet_certificate WHERE cn = ?", "s", $certUsername); |
||
| 635 | // SELECT -> resource, not boolean |
||
| 636 | while ($returnedData = mysqli_fetch_object(/** @scrutinizer ignore-type */ $userrows)) { // only one |
||
| 637 | return ["profile" => $returnedData->profile, "user" => $returnedData->user_id]; |
||
| 638 | } |
||
| 639 | } |
||
| 640 | |||
| 641 | public function userStatus($userId) { |
||
| 642 | $retval = []; |
||
| 643 | $userrows = $this->databaseHandle->exec("SELECT `token` FROM `silverbullet_invitation` WHERE `silverbullet_user_id` = ? AND `profile_id` = ? ", "ii", $userId, $this->identifier); |
||
| 644 | // SELECT -> resource, not boolean |
||
| 645 | while ($returnedData = mysqli_fetch_object(/** @scrutinizer ignore-type */ $userrows)) { |
||
| 646 | $retval[] = ProfileSilverbullet::tokenStatus($returnedData->token); |
||
| 647 | } |
||
| 648 | return $retval; |
||
| 649 | } |
||
| 650 | |||
| 651 | public function getUserExpiryDate($userId) { |
||
| 652 | $query = $this->databaseHandle->exec("SELECT expiry FROM silverbullet_user WHERE id = ? AND profile_id = ? ", "ii", $userId, $this->identifier); |
||
| 653 | // SELECT -> resource, not boolean |
||
| 654 | while ($returnedData = mysqli_fetch_object(/** @scrutinizer ignore-type */ $query)) { |
||
| 655 | return $returnedData->expiry; |
||
| 656 | } |
||
| 657 | } |
||
| 658 | |||
| 659 | public function setUserExpiryDate($userId, $date) { |
||
| 660 | $query = "UPDATE silverbullet_user SET expiry = ? WHERE profile_id = ? AND id = ?"; |
||
| 661 | $theDate = $date->format("Y-m-d"); |
||
| 662 | $this->databaseHandle->exec($query, "sii", $theDate, $this->identifier, $userId); |
||
| 663 | } |
||
| 664 | |||
| 665 | public function listAllUsers() { |
||
| 666 | $userArray = []; |
||
| 667 | $users = $this->databaseHandle->exec("SELECT `id`, `username` FROM `silverbullet_user` WHERE `profile_id` = ? ", "i", $this->identifier); |
||
| 668 | // SELECT -> resource, not boolean |
||
| 669 | while ($res = mysqli_fetch_object(/** @scrutinizer ignore-type */ $users)) { |
||
| 670 | $userArray[$res->id] = $res->username; |
||
| 671 | } |
||
| 672 | return $userArray; |
||
| 673 | } |
||
| 674 | |||
| 675 | public function listActiveUsers() { |
||
| 690 | } |
||
| 691 | |||
| 692 | public function addUser($user, \DateTime $expiry) { |
||
| 693 | $query = "INSERT INTO silverbullet_user (profile_id, username, expiry) VALUES(?,?,?)"; |
||
| 694 | $date = $expiry->format("Y-m-d"); |
||
| 695 | $this->databaseHandle->exec($query, "iss", $this->identifier, $user, $date); |
||
| 696 | return $this->databaseHandle->lastID(); |
||
| 697 | } |
||
| 698 | |||
| 699 | public function deactivateUser($userId) { |
||
| 700 | // set the expiry date of any still valid invitations to NOW() |
||
| 701 | $query = "SELECT id FROM silverbullet_invitation WHERE profile_id = $this->identifier AND silverbullet_user_id = ? AND expiry >= NOW()"; |
||
| 702 | $exec = $this->databaseHandle->exec($query, "s", $userId); |
||
| 703 | // SELECT -> resource, not boolean |
||
| 704 | while ($result = mysqli_fetch_object(/** @scrutinizer ignore-type */ $exec)) { |
||
| 705 | $this->revokeInvitation($result->id); |
||
| 706 | } |
||
| 707 | // and revoke all certificates |
||
| 708 | $query2 = "SELECT serial_number FROM silverbullet_certificate WHERE profile_id = $this->identifier AND silverbullet_user_id = ? AND expiry >= NOW() AND revocation_status = 'NOT_REVOKED'"; |
||
| 709 | $exec2 = $this->databaseHandle->exec($query2, "i", $userId); |
||
| 710 | // SELECT -> resource, not boolean |
||
| 711 | while ($result = mysqli_fetch_object(/** @scrutinizer ignore-type */ $exec2)) { |
||
| 712 | $this->revokeCertificate($result->serial_number); |
||
| 713 | } |
||
| 714 | // and finally set the user expiry date to NOW(), too |
||
| 715 | $query3 = "UPDATE silverbullet_user SET expiry = NOW() WHERE profile_id = $this->identifier AND id = ?"; |
||
| 716 | $exec3 = $this->databaseHandle->exec($query3, "i", $userId); |
||
| 717 | } |
||
| 718 | |||
| 719 | /** |
||
| 720 | * |
||
| 721 | * @param string $token |
||
| 722 | * @return string |
||
| 723 | */ |
||
| 724 | public static function generateTokenLink(string $token) { |
||
| 725 | |||
| 726 | if (isset($_SERVER['HTTPS'])) { |
||
| 727 | $link = 'https://'; |
||
| 728 | } else { |
||
| 729 | $link = 'http://'; |
||
| 730 | } |
||
| 731 | $link .= $_SERVER['SERVER_NAME']; |
||
| 732 | $relPath = dirname(dirname($_SERVER['SCRIPT_NAME'])); |
||
| 733 | View Code Duplication | if (substr($relPath, -1) == '/') { |
|
| 734 | $relPath = substr($relPath, 0, -1); |
||
| 735 | if ($relPath === FALSE) { |
||
| 736 | throw new Exception("Uh. Something went seriously wrong with URL path mangling."); |
||
| 737 | } |
||
| 738 | } |
||
| 739 | $link = $link . $relPath; |
||
| 740 | |||
| 741 | View Code Duplication | if (preg_match('/admin$/', $link)) { |
|
| 742 | $link = substr($link, 0, -6); |
||
| 743 | if ($link === FALSE) { |
||
| 744 | throw new Exception("Impossible: the string ends with '/admin' but it's not possible to cut six characters from the end?!"); |
||
| 745 | } |
||
| 746 | } |
||
| 747 | $link .= '/accountstatus/accountstatus.php?token='.$token; |
||
| 748 | return $link; |
||
| 749 | } |
||
| 750 | |||
| 751 | /** |
||
| 752 | * |
||
| 753 | * @return string |
||
| 754 | */ |
||
| 755 | private function generateToken() { |
||
| 756 | return hash("sha512", base_convert(rand(0, (int) 10e16), 10, 36)); |
||
| 757 | } |
||
| 758 | |||
| 759 | public function createInvitation($userId, $activationCount) { |
||
| 760 | $query = "INSERT INTO silverbullet_invitation (profile_id, silverbullet_user_id, token, quantity, expiry) VALUES (?, ?, ?, ?, DATE_ADD(NOW(), INTERVAL 7 DAY))"; |
||
| 761 | $newToken = $this->generateToken(); |
||
| 762 | $this->databaseHandle->exec($query, "iisi", $this->identifier, $userId, $newToken, $activationCount); |
||
| 763 | } |
||
| 764 | |||
| 765 | public function revokeInvitation($invitationId) { |
||
| 766 | $query = "UPDATE silverbullet_invitation SET expiry = NOW() WHERE id = ? AND profile_id = ?"; |
||
| 767 | $this->databaseHandle->exec($query, "ii", $invitationId, $this->identifier); |
||
| 768 | } |
||
| 769 | |||
| 770 | public function refreshEligibility() { |
||
| 773 | } |
||
| 774 | } |
||
| 775 |