Passed
Push — master ( 5c87ed...e4bb53 )
by Stefan
04:05
created

ProfileSilverbullet   F

Complexity

Total Complexity 69

Size/Duplication

Total Lines 655
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 69
dl 0
loc 655
rs 2.5188
c 0
b 0
f 0

21 Methods

Rating   Name   Duplication   Size   Complexity  
B enumerateCertDetails() 0 22 5
A listActiveUsers() 0 15 2
A setUserExpiryDate() 0 4 1
A findUserIdFromCert() 0 6 2
A refreshEligibility() 0 3 1
A revokeCertificate() 0 15 2
D triggerNewOCSPStatement() 0 86 11
A listAllUsers() 0 8 2
A getUserExpiryDate() 0 5 2
A addUser() 0 5 1
A addSupportedEapMethod() 0 7 3
A randomString() 0 12 3
B updateCache() 0 6 6
A deactivateUser() 0 19 3
B __construct() 0 83 4
A httpRequest() 0 6 1
C issueCertificate() 0 79 8
A userStatus() 0 8 2
B signCsr() 0 36 4
B generateCsr() 0 35 4
A setAnonymousIDSupport() 0 6 2

How to fix   Complexity   

Complex Class

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
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 the ProfileSilverbullet class.
14
 *
15
 * @author Stefan Winter <[email protected]>
16
 * @author Tomasz Wolniewicz <[email protected]>
17
 *
18
 * @package Developer
19
 *
20
 */
21
22
namespace core;
23
24
use \Exception;
25
26
/**
27
 * Silverbullet (marketed as "Managed IdP") is a RADIUS profile which 
28
 * corresponds directly to a built-in RADIUS server and CA. 
29
 * It provides all functions needed for a admin-side web interface where users
30
 * can be added and removed, and new devices be enabled.
31
 * 
32
 * When downloading a Silverbullet based profile, the profile includes per-user
33
 * per-device client certificates which can be immediately used to log into 
34
 * eduroam.
35
 *
36
 * @author Stefan Winter <[email protected]>
37
 * @author Tomasz Wolniewicz <[email protected]>
38
 *
39
 * @license see LICENSE file in root directory
40
 *
41
 * @package Developer
42
 */
43
class ProfileSilverbullet extends AbstractProfile {
44
45
    const SB_CERTSTATUS_VALID = 1;
46
    const SB_CERTSTATUS_EXPIRED = 2;
47
    const SB_CERTSTATUS_REVOKED = 3;
48
    const SB_ACKNOWLEDGEMENT_REQUIRED_DAYS = 365;
49
50
    public $termsAndConditions;
51
52
    /*
53
     * 
54
     */
55
56
    const PRODUCTNAME = "Managed IdP";
57
58
    /**
59
     * produces a random string
60
     * @param int $length the length of the string to produce
61
     * @param string $keyspace the pool of characters to use for producing the string
62
     * @return string
63
     * @throws Exception
64
     */
65
    public static function randomString(
66
    $length, $keyspace = '23456789abcdefghijkmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
67
    ) {
68
        $str = '';
69
        $max = strlen($keyspace) - 1;
70
        if ($max < 1) {
71
            throw new Exception('$keyspace must be at least two characters long');
72
        }
73
        for ($i = 0; $i < $length; ++$i) {
74
            $str .= $keyspace[random_int(0, $max)];
75
        }
76
        return $str;
77
    }
78
79
    /**
80
     * Class constructor for existing profiles (use IdP::newProfile() to actually create one). Retrieves all attributes and 
81
     * supported EAP types from the DB and stores them in the priv_ arrays.
82
     * 
83
     * @param int $profileId identifier of the profile in the DB
84
     * @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.
85
     */
86
    public function __construct($profileId, $idpObject = NULL) {
87
        parent::__construct($profileId, $idpObject);
88
89
        $this->entityOptionTable = "profile_option";
90
        $this->entityIdColumn = "profile_id";
91
        $this->attributes = [];
92
93
        $tempMaxUsers = 200; // abolutely last resort fallback if no per-fed and no config option
94
// set to global config value
95
96
        if (isset(CONFIG_CONFASSISTANT['SILVERBULLET']['default_maxusers'])) {
0 ignored issues
show
Bug introduced by
The constant core\CONFIG_CONFASSISTANT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
97
            $tempMaxUsers = CONFIG_CONFASSISTANT['SILVERBULLET']['default_maxusers'];
98
        }
99
        $myInst = new IdP($this->institution);
100
        $myFed = new Federation($myInst->federation);
101
        $fedMaxusers = $myFed->getAttributes("fed:silverbullet-maxusers");
102
        if (isset($fedMaxusers[0])) {
103
            $tempMaxUsers = $fedMaxusers[0]['value'];
104
        }
105
106
// realm is automatically calculated, then stored in DB
107
108
        $this->realm = "opaquehash@$myInst->identifier-$this->identifier." . strtolower($myInst->federation) . CONFIG_CONFASSISTANT['SILVERBULLET']['realm_suffix'];
109
        $localValueIfAny = "";
110
111
// but there's some common internal attributes populated directly
112
        $internalAttributes = [
113
            "internal:profile_count" => $this->idpNumberOfProfiles,
114
            "internal:realm" => preg_replace('/^.*@/', '', $this->realm),
115
            "internal:use_anon_outer" => FALSE,
116
            "internal:checkuser_outer" => TRUE,
117
            "internal:checkuser_value" => "anonymous",
118
            "internal:anon_local_value" => $localValueIfAny,
119
            "internal:silverbullet_maxusers" => $tempMaxUsers,
120
            "profile:production" => "on",
121
        ];
122
123
// and we need to populate eap:server_name and eap:ca_file with the NRO-specific EAP information
124
        $silverbulletAttributes = [
125
            "eap:server_name" => "auth." . strtolower($myFed->tld) . CONFIG_CONFASSISTANT['SILVERBULLET']['server_suffix'],
126
        ];
127
        $x509 = new \core\common\X509();
128
        $caHandle = fopen(dirname(__FILE__) . "/../config/SilverbulletServerCerts/" . strtoupper($myFed->tld) . "/root.pem", "r");
129
        if ($caHandle !== FALSE) {
130
            $cAFile = fread($caHandle, 16000000);
131
            $silverbulletAttributes["eap:ca_file"] = $x509->der2pem(($x509->pem2der($cAFile)));
132
        }
133
134
        $temp = array_merge($this->addInternalAttributes($internalAttributes), $this->addInternalAttributes($silverbulletAttributes));
135
        $tempArrayProfLevel = array_merge($this->addDatabaseAttributes(), $temp);
136
137
// now, fetch and merge IdP-wide attributes
138
139
        $this->attributes = $this->levelPrecedenceAttributeJoin($tempArrayProfLevel, $this->idpAttributes, "IdP");
140
141
        $this->privEaptypes = $this->fetchEAPMethods();
142
143
        $this->name = ProfileSilverbullet::PRODUCTNAME;
144
145
        $this->loggerInstance->debug(3, "--- END Constructing new Profile object ... ---\n");
146
147
        $this->termsAndConditions = "<h2>Product Definition</h2>
148
        <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>
149
            <ul>
150
                <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>
151
                <li>a technical infrastructure ('CA') which issues and revokes credentials</li>
152
                <li>a technical infrastructure ('RADIUS') which verifies access credentials and subsequently grants access to " . CONFIG_CONFASSISTANT['CONSORTIUM']['display_name'] . "</li>
153
                <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>
154
            </ul>
155
        <h2>User Account Liability</h2>
156
        <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>";
157
        $this->termsAndConditions .= "<p>Your responsibilities include that you</p>
158
        <ul>
159
            <li>only issue accounts to members of your " . CONFIG_CONFASSISTANT['CONSORTIUM']['nomenclature_institution'] . ", as defined by your local policy.</li>
160
            <li>must make sure that all accounts that you issue can be linked by you to actual human end users</li>
161
            <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>
162
            <li>will act upon notifications about possible network abuse by your users and will appropriately sanction them</li>
163
        </ul>
164
        <p>";
165
        $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.";
166
        $this->termsAndConditions .= "</p>
167
        <h2>Privacy</h2>
168
        <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>
169
        <ul>
170
            <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>
171
            <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>
172
            <li>Each access credential carries a different pseudonym, even if it pertains to the same username.</li>
173
            <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).
174
        </ul>";
175
    }
176
    
177
    /**
178
     * Updates database with new installer location; NOOP because we do not
179
     * cache anything in Silverbullet
180
     * 
181
     * @param string $device the device identifier string
182
     * @param string $path the path where the new installer can be found
183
     * @param string $mime the mime type of the new installer
184
     * @param int $integerEapType the inter-representation of the EAP type that is configured in this installer
185
     */
186
    public function updateCache($device, $path, $mime, $integerEapType) {
187
        // caching is not supported in SB (private key in installers)
188
        // the following merely makes the "unused parameter" warnings go away
189
        // the FALSE in condition one makes sure it never gets executed
190
        if (FALSE || $device == "Macbeth" || $path == "heath" || $mime == "application/witchcraft" || $integerEapType == 0) {
191
            throw new Exception("FALSE is TRUE, and TRUE is FALSE! Hover through the browser and filthy code!");
192
        }
193
    }
194
195
    /**
196
     * register new supported EAP method for this profile
197
     *
198
     * @param \core\common\EAP $type The EAP Type, as defined in class EAP
199
     * @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.
200
     *
201
     */
202
    public function addSupportedEapMethod(\core\common\EAP $type, $preference) {
203
        // the parameters really should only list SB and with prio 1 - otherwise,
204
        // something fishy is going on
205
        if ($type->getIntegerRep() != \core\common\EAP::INTEGER_SILVERBULLET || $preference != 1) {
206
            throw new Exception("Silverbullet::addSupportedEapMethod was called for a non-SP EAP type or unexpected priority!");
207
        }
208
        parent::addSupportedEapMethod($type, 1);
209
    }
210
211
    /**
212
     * It's EAP-TLS and there is no point in anonymity
213
     * @param boolean $shallwe
214
     */
215
    public function setAnonymousIDSupport($shallwe) {
216
        // we don't do anonymous outer IDs in SB
217
        if ($shallwe === TRUE) {
218
            throw new Exception("Silverbullet: attempt to add anonymous outer ID support to a SB profile!");
219
        }
220
        $this->databaseHandle->exec("UPDATE profile SET use_anon_outer = 0 WHERE profile_id = $this->identifier");
221
    }
222
223
    /**
224
     * create a CSR
225
     * 
226
     * @param resource $privateKey the private key to create the CSR with
227
     * @return array with the CSR and some meta info
228
     */
229
    private function generateCsr($privateKey) {
230
        // token leads us to the NRO, to set the OU property of the cert
231
        $inst = new IdP($this->institution);
232
        $federation = strtoupper($inst->federation);
233
        $usernameIsUnique = FALSE;
234
        $username = "";
235
        $realmsuffix = $this->getAttributes("internal:realm");
236
        while ($usernameIsUnique === FALSE) {
237
            $usernameLocalPart = self::randomString(64 - 1 - strlen($realmsuffix[0]['value']), "0123456789abcdefghijklmnopqrstuvwxyz");
238
            $username = $usernameLocalPart . "@" . $realmsuffix[0]['value'];
239
            $uniquenessQuery = $this->databaseHandle->exec("SELECT cn from silverbullet_certificate WHERE cn = ?", "s", $username);
240
            // SELECT -> resource, not boolean
241
            if (mysqli_num_rows(/** @scrutinizer ignore-type */ $uniquenessQuery) == 0) {
242
                $usernameIsUnique = TRUE;
243
            }
244
        }
245
246
        $this->loggerInstance->debug(5, "generateCertificate: generating private key.\n");
247
248
        $newCsr = openssl_csr_new(
249
                    ['O' => CONFIG_CONFASSISTANT['CONSORTIUM']['name'],
0 ignored issues
show
Bug introduced by
The constant core\CONFIG_CONFASSISTANT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
250
                'OU' => $federation,
251
                'CN' => $username,
252
                'emailAddress' => $username,
253
                    ], $privateKey, [
254
                'digest_alg' => 'sha256',
255
                'req_extensions' => 'v3_req',
256
                    ]
257
            );
258
        if ($newCsr === FALSE) {
259
            throw new Exception("Unable to create a CSR!");
260
        }
261
        return [
262
            "CSR" => $newCsr,
263
            "USERNAME" => $username
264
        ];
265
    }
266
267
    /**
268
     * take a CSR and sign it with our issuing CA's certificate
269
     * 
270
     * @param mixed $csr the CSR
271
     * @param int $expiryDays the number of days until the cert is going to expire
272
     * @return array the cert and some meta info
273
     */
274
    private function signCsr($csr, $expiryDays) {
275
        switch (CONFIG_CONFASSISTANT['SILVERBULLET']['CA']['type']) {
0 ignored issues
show
Bug introduced by
The constant core\CONFIG_CONFASSISTANT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
276
            case "embedded":
277
                $rootCaPem = file_get_contents(ROOT . "/config/SilverbulletClientCerts/rootca.pem");
278
                $issuingCaPem = file_get_contents(ROOT . "/config/SilverbulletClientCerts/real.pem");
279
                $issuingCa = openssl_x509_read($issuingCaPem);
280
                $issuingCaKey = openssl_pkey_get_private("file://" . ROOT . "/config/SilverbulletClientCerts/real.key");
281
                $nonDupSerialFound = FALSE;
282
                do {
283
                    $serial = random_int(1000000000, PHP_INT_MAX);
284
                    $dupeQuery = $this->databaseHandle->exec("SELECT serial_number FROM silverbullet_certificate WHERE serial_number = ?", "i", $serial);
285
                    // SELECT -> resource, not boolean
286
                    if (mysqli_num_rows(/** @scrutinizer ignore-type */$dupeQuery) == 0) {
287
                        $nonDupSerialFound = TRUE;
288
                    }
289
                } while (!$nonDupSerialFound);
290
                $this->loggerInstance->debug(5, "generateCertificate: signing imminent with unique serial $serial.\n");
291
                return [
292
                    "CERT" => openssl_csr_sign($csr, $issuingCa, $issuingCaKey, $expiryDays, ['digest_alg' => 'sha256'], $serial),
293
                    "SERIAL" => $serial,
294
                    "ISSUER" => $issuingCaPem,
295
                    "ROOT" => $rootCaPem,
296
                ];
297
            default:
298
                /* HTTP POST the CSR to the CA with the $expiryDays as parameter
299
                 * on successful execution, gets back a PEM file which is the
300
                 * certificate (structure TBD)
301
                 * $httpResponse = httpRequest("https://clientca.hosted.eduroam.org/issue/", ["csr" => $csr, "expiry" => $expiryDays ] );
302
                 *
303
                 * The result of this if clause has to be a certificate in PHP's 
304
                 * "openssl_object" style (like the one that openssl_csr_sign would 
305
                 * produce), to be stored in the variable $cert; we also need the
306
                 * serial - which can be extracted from the received cert and has
307
                 * to be stored in $serial.
308
                 */
309
                throw new Exception("External silverbullet CA is not implemented yet!");
310
        }
311
    }
312
313
    /**
314
     * issue a certificate based on a token
315
     *
316
     * @param string $token
317
     * @param string $importPassword
318
     * @return array
319
     */
320
    public function issueCertificate($token, $importPassword) {
321
        $this->loggerInstance->debug(5, "generateCertificate() - starting.\n");
322
        $invitationObject = new SilverbulletInvitation($token);
323
        $this->loggerInstance->debug(5, "tokenStatus: done, got " . $invitationObject->invitationTokenStatus . ", " . $invitationObject->profile . ", " . $invitationObject->userId . ", " . $invitationObject->expiry . ", " . $invitationObject->invitationTokenString . "\n");
324
        if ($invitationObject->invitationTokenStatus != SilverbulletInvitation::SB_TOKENSTATUS_VALID && $invitationObject->invitationTokenStatus != SilverbulletInvitation::SB_TOKENSTATUS_PARTIALLY_REDEEMED) {
325
            throw new Exception("Attempt to generate a SilverBullet installer with an invalid/redeemed/expired token. The user should never have gotten that far!");
326
        }
327
        if ($invitationObject->profile != $this->identifier) {
328
            throw new Exception("Attempt to generate a SilverBullet installer, but the profile ID (constructor) and the profile from token do not match!");
329
        }
330
        // SQL query to find the expiry date of the *user* to find the correct ValidUntil for the cert
331
        $user = $invitationObject->userId;
332
        $userrow = $this->databaseHandle->exec("SELECT expiry FROM silverbullet_user WHERE id = ?", "i", $user);
333
        // SELECT -> resource, not boolean
334
        if ($userrow->num_rows != 1) {
335
            throw new Exception("Despite a valid token, the corresponding user was not found in database or database query error!");
336
        }
337
        $expiryObject = mysqli_fetch_object(/** @scrutinizer ignore-type */ $userrow);
338
        $this->loggerInstance->debug(5, "EXP: " . $expiryObject->expiry . "\n");
339
        $expiryDateObject = date_create_from_format("Y-m-d H:i:s", $expiryObject->expiry);
340
        if ($expiryDateObject === FALSE) {
341
            throw new Exception("The expiry date we got from the DB is bogus!");
342
        }
343
        $this->loggerInstance->debug(5, $expiryDateObject->format("Y-m-d H:i:s") . "\n");
344
        // date_create with no parameters can't fail, i.e. is never FALSE
345
        $validity = date_diff(/** @scrutinizer ignore-type */ date_create(), $expiryDateObject);
346
        $expiryDays = $validity->days + 1;
347
        if ($validity->invert == 1) { // negative! That should not be possible
348
            throw new Exception("Attempt to generate a certificate for a user which is already expired!");
349
        }
350
351
        $privateKey = openssl_pkey_new(['private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA, 'encrypt_key' => FALSE]);
352
        $csr = $this->generateCsr($privateKey);
353
354
        $this->loggerInstance->debug(5, "generateCertificate: proceeding to sign cert.\n");
355
356
        $certMeta = $this->signCsr($csr["CSR"], $expiryDays);
357
        $cert = $certMeta["CERT"];
358
        $issuingCaPem = $certMeta["ISSUER"];
359
        $rootCaPem = $certMeta["ROOT"];
360
        $serial = $certMeta["SERIAL"];
361
362
        $this->loggerInstance->debug(5, "generateCertificate: post-processing certificate.\n");
363
364
        // get the SHA1 fingerprint, this will be handy for Windows installers
365
        $sha1 = openssl_x509_fingerprint($cert, "sha1");
366
        // with the cert, our private key and import password, make a PKCS#12 container out of it
367
        $exportedCertProt = "";
368
        openssl_pkcs12_export($cert, $exportedCertProt, $privateKey, $importPassword, ['extracerts' => [$issuingCaPem /* , $rootCaPem */]]);
369
        $exportedCertClear = "";
370
        openssl_pkcs12_export($cert, $exportedCertClear, $privateKey, "", ['extracerts' => [$issuingCaPem, $rootCaPem]]);
371
        // store resulting cert CN and expiry date in separate columns into DB - do not store the cert data itself as it contains the private key!
372
        // we need the *real* expiry date, not just the day-approximation
373
        $x509 = new \core\common\X509();
374
        $certString = "";
375
        openssl_x509_export($cert, $certString);
376
        $parsedCert = $x509->processCertificate($certString);
377
        $this->loggerInstance->debug(5, "CERTINFO: " . print_r($parsedCert['full_details'], true));
378
        $realExpiryDate = date_create_from_format("U", $parsedCert['full_details']['validTo_time_t'])->format("Y-m-d H:i:s");
379
380
        // store new cert info in DB
381
        $newCertificateResult = $this->databaseHandle->exec("INSERT INTO `silverbullet_certificate` (`profile_id`, `silverbullet_user_id`, `silverbullet_invitation_id`, `serial_number`, `cn` ,`expiry`) VALUES (?, ?, ?, ?, ?, ?)", "iiisss", $invitationObject->profile, $invitationObject->userId, $invitationObject->identifier, $serial, $csr["USERNAME"], $realExpiryDate);
382
        if ($newCertificateResult === false) {
383
            throw new Exception("Unable to update database with new cert details!");
384
        }
385
        $certificateId = $this->databaseHandle->lastID();
386
387
        // newborn cert immediately gets its "valid" OCSP response
388
        ProfileSilverbullet::triggerNewOCSPStatement((int) $serial);
389
// return PKCS#12 data stream
390
        return [
391
            "username" => $csr["USERNAME"],
392
            "certdata" => $exportedCertProt,
393
            "certdataclear" => $exportedCertClear,
394
            "expiry" => $expiryDateObject->format("Y-m-d\TH:i:s\Z"),
395
            "sha1" => $sha1,
396
            'importPassword' => $importPassword,
397
            'serial' => $serial,
398
            'certificateId' => $certificateId,
399
        ];
400
    }
401
402
    /**
403
     * triggers a new OCSP statement for the given serial number
404
     * 
405
     * @param int $serial the serial number of the cert in question (decimal)
406
     * @return string DER-encoded OCSP status info (binary data!)
407
     */
408
    public static function triggerNewOCSPStatement($serial) {
409
        $logHandle = new \core\common\Logging();
410
        $logHandle->debug(2, "Triggering new OCSP statement for serial $serial.\n");
411
        switch (CONFIG_CONFASSISTANT['SILVERBULLET']['CA']['type']) {
0 ignored issues
show
Bug introduced by
The constant core\CONFIG_CONFASSISTANT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
412
            case "embedded":
413
                // get all relevant info from DB
414
                $cn = "";
415
                $federation = NULL;
416
                $certstatus = "";
417
                $originalExpiry = date_create_from_format("Y-m-d H:i:s", "2000-01-01 00:00:00");
418
                $dbHandle = DBConnection::handle("INST");
419
                $originalStatusQuery = $dbHandle->exec("SELECT profile_id, cn, revocation_status, expiry, revocation_time, OCSP FROM silverbullet_certificate WHERE serial_number = ?", "i", $serial);
420
                // SELECT -> resource, not boolean
421
                if (mysqli_num_rows(/** @scrutinizer ignore-type */ $originalStatusQuery) > 0) {
422
                    $certstatus = "V";
423
                }
424
                while ($runner = mysqli_fetch_object(/** @scrutinizer ignore-type */ $originalStatusQuery)) { // there can be only one row
425
                    if ($runner->revocation_status == "REVOKED") {
426
                        // already revoked, simply return canned OCSP response
427
                        $certstatus = "R";
428
                    }
429
                    $originalExpiry = date_create_from_format("Y-m-d H:i:s", $runner->expiry);
430
                    if ($originalExpiry === FALSE) {
431
                        throw new Exception("Unable to calculate original expiry date, input data bogus!");
432
                    }
433
                    $validity = date_diff(/** @scrutinizer ignore-type */ date_create(), $originalExpiry);
434
                    if ($validity->invert == 1) {
435
                        // negative! Cert is already expired, no need to revoke. 
436
                        // No need to return anything really, but do return the last known OCSP statement to prevent special case
437
                        $certstatus = "E";
438
                    }
439
                    $cn = $runner->cn;
440
                    $profile = new ProfileSilverbullet($runner->profile_id);
441
                    $inst = new IdP($profile->institution);
442
                    $federation = strtoupper($inst->federation);
443
                }
444
445
                // generate stub index.txt file
446
                $cat = new CAT();
447
                $tempdirArray = $cat->createTemporaryDirectory("test");
448
                $tempdir = $tempdirArray['dir'];
449
                $nowIndexTxt = (new \DateTime())->format("ymdHis") . "Z";
450
                $expiryIndexTxt = $originalExpiry->format("ymdHis") . "Z";
451
                $serialHex = strtoupper(dechex($serial));
452
                if (strlen($serialHex) % 2 == 1) {
453
                    $serialHex = "0" . $serialHex;
454
                }
455
                
456
                $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";
457
                $logHandle->debug(4, "index.txt contents-to-be: $indexStatement");
458
                if (!file_put_contents($tempdir . "/index.txt", $indexStatement)) {
459
                $logHandle->debug(1,"Unable to write openssl index.txt file for revocation handling!");
460
                }
461
                // index.txt.attr is dull but needs to exist
462
                file_put_contents($tempdir . "/index.txt.attr", "unique_subject = yes\n");
463
                // call "openssl ocsp" to manufacture our own OCSP statement
464
                // adding "-rmd sha1" to the following command-line makes the
465
                // choice of signature algorithm for the response explicit
466
                // but it's only available from openssl-1.1.0 (which we do not
467
                // want to require just for that one thing).
468
                $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";
0 ignored issues
show
Bug introduced by
The constant core\CONFIG was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
469
                $logHandle->debug(2, "Calling openssl ocsp with following cmdline: $execCmd\n");
470
                $output = [];
471
                $return = 999;
472
                exec($execCmd, $output, $return);
473
                if ($return !== 0) {
474
                    throw new Exception("Non-zero return value from openssl ocsp!");
475
                }
476
                $ocspFile = fopen($tempdir . "/$serialHex.response.der", "r");
477
                $ocsp = fread($ocspFile, 1000000);
478
                fclose($ocspFile);
479
                break;
480
            default:
481
                /* HTTP POST the serial to the CA. The CA knows about the state of
482
                 * the certificate.
483
                 *
484
                 * $httpResponse = httpRequest("https://clientca.hosted.eduroam.org/ocsp/", ["serial" => $serial ] );
485
                 *
486
                 * The result of this if clause has to be a DER-encoded OCSP statement
487
                 * to be stored in the variable $ocsp
488
                 */
489
                throw new Exception("External silverbullet CA is not implemented yet!");
490
        }
491
        // write the new statement into DB
492
        $dbHandle->exec("UPDATE silverbullet_certificate SET OCSP = ?, OCSP_timestamp = NOW() WHERE serial_number = ?", "si", $ocsp, $serial);
493
        return $ocsp;
494
    }
495
496
    /**
497
     * revokes a certificate
498
     * @param int $serial the serial number of the cert to revoke (decimal!)
499
     * @return array with revocation information
500
     */
501
    public function revokeCertificate($serial) {
502
503
504
// TODO for now, just mark as revoked in the certificates table (and use the stub OCSP updater)
505
        $nowSql = (new \DateTime())->format("Y-m-d H:i:s");
506
        if (CONFIG_CONFASSISTANT['SILVERBULLET']['CA']['type'] != "embedded") {
0 ignored issues
show
Bug introduced by
The constant core\CONFIG_CONFASSISTANT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
507
            // send revocation request to CA.
508
            // $httpResponse = httpRequest("https://clientca.hosted.eduroam.org/revoke/", ["serial" => $serial ] );
0 ignored issues
show
Unused Code Comprehensibility introduced by
53% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
509
            throw new Exception("External silverbullet CA is not implemented yet!");
510
        }
511
        // regardless if embedded or not, always keep local state in our own DB
512
        $this->databaseHandle->exec("UPDATE silverbullet_certificate SET revocation_status = 'REVOKED', revocation_time = ? WHERE serial_number = ?", "si", $nowSql, $serial);
513
        $this->loggerInstance->debug(2, "Certificate revocation status updated, about to call triggerNewOCSPStatement($serial).\n");
514
        $ocsp = ProfileSilverbullet::triggerNewOCSPStatement($serial);
515
        return ["OCSP" => $ocsp];
516
    }
517
518
    /**
519
     * performs an HTTP request. Currently unused, will be for external CA API calls.
520
     * 
521
     * @param string $url the URL to send the request to
522
     * @param array $postValues POST values to send
523
     * @return string the returned HTTP content
524
     */
525
    private function httpRequest($url, $postValues) {
0 ignored issues
show
Unused Code introduced by
The method httpRequest() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
526
        $options = [
527
            'http' => ['header' => 'Content-type: application/x-www-form-urlencoded\r\n', "method" => 'POST', 'content' => http_build_query($postValues)]
528
        ];
529
        $context = stream_context_create($options);
530
        return file_get_contents($url, false, $context);
531
    }
532
533
    /**
534
     * checks a certificate's status in the database and delivers its properties in an array
535
     * 
536
     * @param \mysqli_result $certQuery
537
     * @return array properties of the cert in questions
538
     */
539
    public 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
    /**
564
     * For a given certificate username, find the profile and username in CAT
565
     * this needs to be static because we do not have a known profile instance
566
     * 
567
     * @param string $certUsername a username from CN or sAN:email
568
     * @return array
569
     */
570
    public static function findUserIdFromCert($certUsername) {
571
        $dbHandle = \core\DBConnection::handle("INST");
572
        $userrows = $dbHandle->exec("SELECT silverbullet_user_id AS user_id, profile_id AS profile FROM silverbullet_certificate WHERE cn = ?", "s", $certUsername);
573
        // SELECT -> resource, not boolean
574
        while ($returnedData = mysqli_fetch_object(/** @scrutinizer ignore-type */ $userrows)) { // only one
575
            return ["profile" => $returnedData->profile, "user" => $returnedData->user_id];
576
        }
577
    }
578
579
    /**
580
     * find out about the status of a given SB user; retrieves the info regarding all his tokens (and thus all his certificates)
581
     * @param int $userId
582
     * @return array of invitationObjects
583
     */
584
    public function userStatus($userId) {
585
        $retval = [];
586
        $userrows = $this->databaseHandle->exec("SELECT `token` FROM `silverbullet_invitation` WHERE `silverbullet_user_id` = ? AND `profile_id` = ? ", "ii", $userId, $this->identifier);
587
        // SELECT -> resource, not boolean
588
        while ($returnedData = mysqli_fetch_object(/** @scrutinizer ignore-type */ $userrows)) {
589
            $retval[] = new SilverbulletInvitation($returnedData->token);
590
        }
591
        return $retval;
592
    }
593
594
    /**
595
     * finds out the expiry date of a given user
596
     * @param int $userId
597
     * @return string
598
     */
599
    public function getUserExpiryDate($userId) {
600
        $query = $this->databaseHandle->exec("SELECT expiry FROM silverbullet_user WHERE id = ? AND profile_id = ? ", "ii", $userId, $this->identifier);
601
        // SELECT -> resource, not boolean
602
        while ($returnedData = mysqli_fetch_object(/** @scrutinizer ignore-type */ $query)) {
603
            return $returnedData->expiry;
604
        }
605
    }
606
    
607
    /**
608
     * sets the expiry date of a user to a new date of choice
609
     * @param int $userId
610
     * @param \DateTime $date
611
     */
612
    public function setUserExpiryDate($userId, $date) {
613
        $query = "UPDATE silverbullet_user SET expiry = ? WHERE profile_id = ? AND id = ?";
614
        $theDate = $date->format("Y-m-d");
615
        $this->databaseHandle->exec($query, "sii", $theDate, $this->identifier, $userId);
616
    }
617
618
    /**
619
     * lists all users of this SB profile
620
     * @return array
621
     */
622
    public function listAllUsers() {
623
        $userArray = [];
624
        $users = $this->databaseHandle->exec("SELECT `id`, `username` FROM `silverbullet_user` WHERE `profile_id` = ? ", "i", $this->identifier);
625
        // SELECT -> resource, not boolean
626
        while ($res = mysqli_fetch_object(/** @scrutinizer ignore-type */ $users)) {
627
            $userArray[$res->id] = $res->username;
628
        }
629
        return $userArray;
630
    }
631
632
    /**
633
     * lists all users which are currently active (i.e. have pending invitations and/or valid certs)
634
     * @return array
635
     */
636
    public function listActiveUsers() {
637
        // users are active if they have a non-expired invitation OR a non-expired, non-revoked certificate
638
        $userCount = [];
639
        $users = $this->databaseHandle->exec("SELECT DISTINCT u.id AS usercount FROM silverbullet_user u, silverbullet_invitation i, silverbullet_certificate c "
640
                . "WHERE u.profile_id = ? "
641
                . "AND ( "
642
                . "( u.id = i.silverbullet_user_id AND i.expiry >= NOW() )"
643
                . "     OR"
644
                . "  ( u.id = c.silverbullet_user_id AND c.expiry >= NOW() AND c.revocation_status != 'REVOKED' ) "
645
                . ")", "i", $this->identifier);
646
        // SELECT -> resource, not boolean
647
        while ($res = mysqli_fetch_object(/** @scrutinizer ignore-type */ $users)) {
648
            $userCount[] = $res->usercount;
649
        }
650
        return $userCount;
651
    }
652
653
    /**
654
     * adds a new user to the profile
655
     * 
656
     * @param string $username
657
     * @param \DateTime $expiry
658
     * @return int row ID of the new user in the database
659
     */
660
    public function addUser($username, \DateTime $expiry) {
661
        $query = "INSERT INTO silverbullet_user (profile_id, username, expiry) VALUES(?,?,?)";
662
        $date = $expiry->format("Y-m-d");
663
        $this->databaseHandle->exec($query, "iss", $this->identifier, $username, $date);
664
        return $this->databaseHandle->lastID();
665
    }
666
667
    /**
668
     * revoke all active certificates and pending invitations of a user
669
     * @param int $userId
670
     */
671
    public function deactivateUser($userId) {
672
        // set the expiry date of any still valid invitations to NOW()
673
        $query = "SELECT id FROM silverbullet_invitation WHERE profile_id = $this->identifier AND silverbullet_user_id = ? AND expiry >= NOW()";
674
        $exec = $this->databaseHandle->exec($query, "i", $userId);
675
        // SELECT -> resource, not boolean
676
        while ($result = mysqli_fetch_object(/** @scrutinizer ignore-type */ $exec)) {
677
            $invitation = new SilverbulletInvitation($result->id);
678
            $invitation->revokeInvitation();
679
        }
680
        // and revoke all certificates
681
        $query2 = "SELECT serial_number FROM silverbullet_certificate WHERE profile_id = $this->identifier AND silverbullet_user_id = ? AND expiry >= NOW() AND revocation_status = 'NOT_REVOKED'";
682
        $exec2 = $this->databaseHandle->exec($query2, "i", $userId);
683
        // SELECT -> resource, not boolean
684
        while ($result = mysqli_fetch_object(/** @scrutinizer ignore-type */ $exec2)) {
685
            $this->revokeCertificate($result->serial_number);
686
        }
687
        // and finally set the user expiry date to NOW(), too
688
        $query3 = "UPDATE silverbullet_user SET expiry = NOW() WHERE profile_id = $this->identifier AND id = ?";
689
        $this->databaseHandle->exec($query3, "i", $userId);
690
    }
691
    
692
    /**
693
     * updates the last_ack for all users (invoked when the admin claims to have re-verified continued eligibility of all users)
694
     */
695
    public function refreshEligibility() {
696
        $query = "UPDATE silverbullet_user SET last_ack = NOW() WHERE profile_id = ?";
697
        $this->databaseHandle->exec($query, "i", $this->identifier);
698
    }
699
}
700