CertificationAuthorityEduPki::signRequest()   F
last analyzed

Complexity

Conditions 17
Paths 131

Size

Total Lines 152
Code Lines 106

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 106
c 0
b 0
f 0
dl 0
loc 152
rs 3.9666
cc 17
nc 131
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
namespace core;
13
14
use \Exception;
0 ignored issues
show
Bug introduced by
The type \Exception was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
15
use \SoapFault;
0 ignored issues
show
Bug introduced by
The type \SoapFault was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
16
17
class CertificationAuthorityEduPki extends EntityWithDBProperties implements CertificationAuthorityInterface
18
{
19
20
    private const LOCATION_RA_CERT = ROOT . "/config/SilverbulletClientCerts/edupki-test-ra.pem";
21
    private const LOCATION_RA_KEY = ROOT . "/config/SilverbulletClientCerts/edupki-test-ra.clearkey";
22
    private const LOCATION_WEBROOT = ROOT . "/config/SilverbulletClientCerts/eduPKI-webserver-root.pem";
23
    private const EDUPKI_RA_ID = 700;
24
    private const EDUPKI_CERT_PROFILE = "User SOAP";
25
    private const EDUPKI_RA_PKEY_PASSPHRASE = "...";
26
27
    /**
28
     * sets up the environment so that we can talk to eduPKI
29
     * 
30
     * @throws Exception
31
     */
32
    public function __construct()
33
    {
34
        $this->databaseType = "INST";
35
        parent::__construct();
36
37
        if (stat(CertificationAuthorityEduPki::LOCATION_RA_CERT) === FALSE) {
38
            throw new Exception("RA operator PEM file not found: " . CertificationAuthorityEduPki::LOCATION_RA_CERT);
39
        }
40
        if (stat(CertificationAuthorityEduPki::LOCATION_RA_KEY) === FALSE) {
41
            throw new Exception("RA operator private key file not found: " . CertificationAuthorityEduPki::LOCATION_RA_KEY);
42
        }
43
        if (stat(CertificationAuthorityEduPki::LOCATION_WEBROOT) === FALSE) {
44
            throw new Exception("CA website root CA file not found: " . CertificationAuthorityEduPki::LOCATION_WEBROOT);
45
        }
46
    }
47
48
    /**
49
     * Creates an updated OCSP statement. Nothing to be done here - eduPKI have
50
     * their own OCSP responder and the certs point to it. So we are not in the 
51
     * loop.
52
     * 
53
     * @param string $serial serial number of the certificate. Serials are 128 bit, so forcibly a string.
54
     * @return string a dummy string instead of a real statement
55
     */
56
    public function triggerNewOCSPStatement($serial): string
57
    {
58
        unset($serial); // not needed
59
        return "EXTERNAL";
60
    }
61
62
    /**
63
     * signs a CSR
64
     * 
65
     * @param array   $csr        the request structure, an array
66
     * @param integer $expiryDays how many days should the certificate be valid
67
     * @return array the certificate with some meta info
68
     * @throws Exception
69
     */
70
    public function signRequest($csr, $expiryDays): array
71
    {
72
        if ($csr["CSR_STRING"] === NULL) {
73
            throw new Exception("This CA needs the CSR in a string (PEM)!");
74
        }
75
        // initialise connection to eduPKI CA / eduroam RA and send the request to them
76
        try {
77
            $altArray = [# Array mit den Subject Alternative Names
78
                "email:" . $csr["USERNAME"]
79
            ];
80
            $soapPub = $this->initEduPKISoapSession("PUBLIC");
81
            $this->loggerInstance->debug(5, "FIRST ACTUAL SOAP REQUEST (Public, newRequest)!\n");
82
            $this->loggerInstance->debug(5, "PARAM_1: " . CertificationAuthorityEduPki::EDUPKI_RA_ID . "\n");
83
            $this->loggerInstance->debug(5, "PARAM_2: " . $csr["CSR_STRING"] . "\n");
84
            $this->loggerInstance->debug(5, "PARAM_3: ");
85
            $this->loggerInstance->debug(5, $altArray);
86
            $this->loggerInstance->debug(5, "PARAM_4: " . CertificationAuthorityEduPki::EDUPKI_CERT_PROFILE . "\n");
87
            $this->loggerInstance->debug(5, "PARAM_5: " . sha1("notused") . "\n");
88
            $this->loggerInstance->debug(5, "PARAM_6: " . $csr["USERNAME"] . "\n");
89
            $this->loggerInstance->debug(5, "PARAM_7: " . $csr["USERNAME"] . "\n");
90
            $this->loggerInstance->debug(5, "PARAM_8: " . \config\ConfAssistant::SILVERBULLET['product_name'] . "\n");
91
            $this->loggerInstance->debug(5, "PARAM_9: false\n");
92
            $soapNewRequest = $soapPub->newRequest(
93
                    CertificationAuthorityEduPki::EDUPKI_RA_ID, # RA-ID
94
                    $csr["CSR_STRING"], # Request im PEM-Format
95
                    $altArray, # altNames
96
                    CertificationAuthorityEduPki::EDUPKI_CERT_PROFILE, # Zertifikatprofil
97
                    sha1("notused"), # PIN
98
                    $csr["USERNAME"], # Name des Antragstellers
99
                    $csr["USERNAME"], # Kontakt-E-Mail
100
                    \config\ConfAssistant::SILVERBULLET['product_name'], # Organisationseinheit des Antragstellers
101
                    false                   # Veröffentlichen des Zertifikats?
102
            );
103
            $this->loggerInstance->debug(5, $soapPub->__getLastRequest());
104
            $this->loggerInstance->debug(5, $soapPub->__getLastResponse());
105
            if ($soapNewRequest == 0) {
106
                throw new Exception("Error when sending SOAP request (request serial number was zero). No further details available.");
107
            }
108
            $soapReqnum = intval($soapNewRequest);
109
        } catch (Exception $e) {
110
            // PHP 7.1 can do this much better
111
            if (is_soap_fault($e)) {
112
                throw new Exception("Error when sending SOAP request: " . "{$e->faultcode}:  {
113
                    $e->faultstring
114
                }\n");
115
            }
116
            throw new Exception("Something odd happened while doing the SOAP request:" . $e->getMessage());
117
        }
118
        try {
119
            $soap = $this->initEduPKISoapSession("RA");
120
            // tell the CA the desired expiry date of the new certificate
121
            $expiry = new \DateTime();
122
            $expiry->modify("+$expiryDays day");
123
            $expiry->setTimezone(new \DateTimeZone("UTC"));
124
            $soapExpiryChange = $soap->setRequestParameters(
125
                    $soapReqnum, [
126
                "RaID" => CertificationAuthorityEduPki::EDUPKI_RA_ID,
127
                "Role" => CertificationAuthorityEduPki::EDUPKI_CERT_PROFILE,
128
                "Subject" => "DC=eduroam,DC=test,DC=test,C=" . $csr["FED"] . ",O=" . \config\ConfAssistant::CONSORTIUM['name'] . ",OU=" . $csr["FED"] . ",CN=" . $csr['USERNAME'] . ",emailAddress=" . $csr['USERNAME'],
129
                "SubjectAltNames" => ["email:" . $csr["USERNAME"]],
130
                "NotBefore" => (new \DateTime())->format('c'),
131
                "NotAfter" => $expiry->format('c'),
132
                    ]
133
            );
134
            if ($soapExpiryChange === FALSE) {
135
                throw new Exception("Error when sending SOAP request (unable to change expiry date).");
136
            }
137
            // retrieve the raw request to prepare for signature and approval
138
            // this seems to come out base64-decoded already; maybe PHP
139
            // considers this "convenience"? But we need it as sent on
140
            // the wire, so re-encode it!
141
            $soapCleartext = $soap->getRawRequest($soapReqnum);
142
143
            $this->loggerInstance->debug(5, "Actual received SOAP response for getRawRequest was:\n\n");
144
            $this->loggerInstance->debug(5, $soap->__getLastResponse());
145
            // for obnoxious reasons, we have to dump the request into a file and let pkcs7_sign read from the file
146
            // rather than just using the string. Grr.
147
            $tempdir = \core\common\Entity::createTemporaryDirectory("test");
148
            file_put_contents($tempdir['dir'] . "/content.txt", $soapCleartext);
149
            // retrieve our RA cert from filesystem                    
150
            // the RA certificates are not needed right now because we
151
            // have resorted to S/MIME signatures with openssl command-line
152
            // rather than the built-in functions. But that may change in
153
            // the future, so let's park these two lines for future use.
154
            // $raCertFile = file_get_contents(ROOT . "/config/SilverbulletClientCerts/edupki-test-ra.pem");
155
            // $raCert = openssl_x509_read($raCertFile);
156
            // $raKey = openssl_pkey_get_private("file://" . ROOT . "/config/SilverbulletClientCerts/edupki-test-ra.clearkey");
157
            // sign the data, using cmdline because openssl_pkcs7_sign produces strange results
158
            // -binary didn't help, nor switch -md to sha1 sha256 or sha512
159
            $this->loggerInstance->debug(5, "Actual content to be signed is this:\n  $soapCleartext\n");
160
            $execCmd = \config\Master::PATHS['openssl'] . " smime -sign -binary -in " . $tempdir['dir'] . "/content.txt -out " . $tempdir['dir'] . "/signature.txt -outform pem -inkey " . ROOT . "/config/SilverbulletClientCerts/edupki-test-ra.clearkey -signer " . ROOT . "/config/SilverbulletClientCerts/edupki-test-ra.pem";
161
            $this->loggerInstance->debug(2, "Calling openssl smime with following cmdline:   $execCmd\n");
162
            $output = [];
163
            $return = 999;
164
            exec($execCmd, $output, $return);
165
            if ($return !== 0) {
166
                throw new Exception("Non-zero return value from openssl smime!");
167
            }
168
            // and get the signature blob back from the filesystem
169
            $detachedSig = trim(file_get_contents($tempdir['dir'] . "/signature.txt"));
170
            $this->loggerInstance->debug(5, "Request for server approveRequest has parameters:\n");
171
            $this->loggerInstance->debug(5, $soapReqnum . "\n");
172
            $this->loggerInstance->debug(5, $soapCleartext . "\n"); // PHP magically encodes this as base64 while sending!
173
            $this->loggerInstance->debug(5, $detachedSig . "\n");
174
            $soapIssueCert = $soap->approveRequest($soapReqnum, $soapCleartext, $detachedSig);
175
            $this->loggerInstance->debug(5, "approveRequest Request was: \n" . $soap->__getLastRequest());
176
            $this->loggerInstance->debug(5, "approveRequest Response was: \n" . $soap->__getLastResponse());
177
            if ($soapIssueCert === FALSE) {
178
                throw new Exception("The locally approved request was NOT processed by the CA.");
179
            }
180
            // now, get the actual cert from the CA
181
            sleep(55);
182
            $counter = 55;
183
            $parsedCert = FALSE;
184
            do {
185
                $counter += 5;
186
                sleep(5); // always start with a wait. Signature round-trip on the server side is at least one minute.
187
                $soapCert = $soap->getCertificateByRequestSerial($soapReqnum);
188
                $x509 = new common\X509();
189
                if (strlen($soapCert) > 10) {
190
                    $parsedCert = $x509->processCertificate($soapCert);
191
                }
192
            } while ($parsedCert === FALSE && $counter < 500);
193
            // we should now have an array
194
            if ($parsedCert === FALSE) {
195
                throw new Exception("We did not actually get a certificate after waiting for 5 minutes.");
196
            }
197
            // let's get the CA certificate chain
198
199
            $caInfo = $soap->getCAInfo();
200
            $certList = $x509->splitCertificate($caInfo->CAChain[0]);
201
            // find the root
202
            $theRoot = "";
203
            foreach ($certList as $oneCert) {
204
                $content = $x509->processCertificate($oneCert);
205
                if ($content['root'] == 1) {
206
                    $theRoot = $content;
207
                }
208
            }
209
            if ($theRoot == "") {
210
                throw new Exception("CAInfo has no root certificate for us!");
211
            }
212
        } catch (SoapFault $e) {
213
            throw new Exception("SoapFault: Error when sending or receiving SOAP message: " . "{$e->faultcode}: {$e->faultname}: {$e->faultstring}: {$e->faultactor}: {$e->detail}: {$e->headerfault}\n");
214
        } catch (Exception $e) {
215
            throw new Exception("Exception: Something odd happened between the SOAP requests:" . $e->getMessage());
216
        }
217
        return [
218
            "CERT" => openssl_x509_read($parsedCert['pem']),
219
            "SERIAL" => $parsedCert['full_details']['serialNumber'],
220
            "ISSUER" => $theRoot,
221
            "ROOT" => $theRoot,
222
        ];
223
    }
224
225
    /**
226
     * revokes a certificate
227
     * 
228
     * @param string $serial the serial, as a string because it is a 128 bit number
229
     * @return void
230
     * @throws Exception
231
     */
232
    public function revokeCertificate($serial): void
233
    {
234
        try {
235
            $soap = $this->initEduPKISoapSession("RA");
236
            $soapRevocationSerial = $soap->newRevocationRequest(["Serial", $serial], "");
237
            if ($soapRevocationSerial == 0) {
238
                throw new Exception("Unable to create revocation request, serial number was zero.");
239
            }
240
            // retrieve the raw request to prepare for signature and approval
241
            $soapRawRevRequest = $soap->getRawRevocationRequest($soapRevocationSerial);
242
            if (strlen($soapRawRevRequest) < 10) { // very basic error handling
243
                throw new Exception("Suspiciously short data to sign!");
244
            }
245
            // for obnoxious reasons, we have to dump the request into a file and let pkcs7_sign read from the file
246
            // rather than just using the string. Grr.
247
            $tempdir = \core\common\Entity::createTemporaryDirectory("test");
248
            file_put_contents($tempdir['dir'] . "/content.txt", $soapRawRevRequest);
249
            // retrieve our RA cert from filesystem
250
            // sign the data, using cmdline because openssl_pkcs7_sign produces strange results
251
            // -binary didn't help, nor switch -md to sha1 sha256 or sha512
252
            $this->loggerInstance->debug(5, "Actual content to be signed is this:\n$soapRawRevRequest\n");
253
            $execCmd = \config\Master::PATHS['openssl'] . " smime -sign -binary -in " . $tempdir['dir'] . "/content.txt -out " . $tempdir['dir'] . "/signature.txt -outform pem -inkey " . CertificationAuthorityEduPki::LOCATION_RA_KEY . " -signer " . CertificationAuthorityEduPki::LOCATION_RA_CERT;
254
            $this->loggerInstance->debug(2, "Calling openssl smime with following cmdline: $execCmd\n");
255
            $output = [];
256
            $return = 999;
257
            exec($execCmd, $output, $return);
258
            if ($return !== 0) {
259
                throw new Exception("Non-zero return value from openssl smime!");
260
            }
261
            // and get the signature blob back from the filesystem
262
            $detachedSig = trim(file_get_contents($tempdir['dir'] . "/signature.txt"));
263
            $soapIssueRev = $soap->approveRevocationRequest($soapRevocationSerial, $soapRawRevRequest, $detachedSig);
264
            if ($soapIssueRev === FALSE) {
265
                throw new Exception("The locally approved revocation request was NOT processed by the CA.");
266
            }
267
        } catch (Exception $e) {
268
            // PHP 7.1 can do this much better
269
            if (is_soap_fault($e)) {
270
                throw new Exception("Error when sending SOAP request: " . "{$e->faultcode}: {$e->faultstring}\n");
271
            }
272
            throw new Exception("Something odd happened while doing the SOAP request:" . $e->getMessage());
273
        }
274
    }
275
276
    /**
277
     * sets up a connection to the eduPKI SOAP interfaces
278
     * There is a public interface and an RA-restricted interface;
279
     * the latter needs an RA client certificate to identify the operator
280
     * 
281
     * @param string $type to which interface should we connect to - "PUBLIC" or "RA"
282
     * @return \SoapClient the connection object
283
     * @throws Exception
284
     */
285
    private function initEduPKISoapSession($type)
286
    {
287
        // set context parameters common to both endpoints
288
        $context_params = [
289
            'http' => [
290
                'timeout' => 60,
291
                'user_agent' => 'Stefan',
292
                'protocol_version' => 1.1
293
            ],
294
            'ssl' => [
295
                'verify_peer' => true,
296
                'verify_peer_name' => true,
297
                // below is the CA "/C=DE/O=Deutsche Telekom AG/OU=T-TeleSec Trust Center/CN=Deutsche Telekom Root CA 2"
298
                'cafile' => CertificationAuthorityEduPki::LOCATION_WEBROOT,
299
                'verify_depth' => 5,
300
                'capture_peer_cert' => true,
301
            ],
302
        ];
303
        $url = "";
304
        switch ($type) {
305
            case "PUBLIC":
306
                $url = "https://pki.edupki.org/edupki-test-ca/cgi-bin/pub/soap?wsdl=1";
307
                $context_params['ssl']['peer_name'] = 'pki.edupki.org';
308
                break;
309
            case "RA":
310
                $url = "https://ra.edupki.org/edupki-test-ca/cgi-bin/ra/soap?wsdl=1";
311
                $context_params['ssl']['peer_name'] = 'ra.edupki.org';
312
                break;
313
            default:
314
                throw new Exception("Unknown type of eduPKI interface requested.");
315
        }
316
        if ($type == "RA") { // add client auth parameters to the context
317
            $context_params['ssl']['local_cert'] = CertificationAuthorityEduPki::LOCATION_RA_CERT;
318
            $context_params['ssl']['local_pk'] = CertificationAuthorityEduPki::LOCATION_RA_KEY;
319
            // $context_params['ssl']['passphrase'] = SilverbulletCertificate::EDUPKI_RA_PKEY_PASSPHRASE;
320
        }
321
        // initialise connection to eduPKI CA / eduroam RA
322
        $soap = new \SoapClient($url, [
323
            'soap_version' => SOAP_1_1,
324
            'trace' => TRUE,
325
            'exceptions' => TRUE,
326
            'connection_timeout' => 5, // if can't establish the connection within 5 sec, something's wrong
327
            'cache_wsdl' => WSDL_CACHE_NONE,
328
            'user_agent' => 'eduroam CAT to eduPKI SOAP Interface',
329
            'features' => SOAP_SINGLE_ELEMENT_ARRAYS,
330
            'stream_context' => stream_context_create($context_params),
331
            'typemap' => [
332
                [
333
                    'type_ns' => 'http://www.w3.org/2001/XMLSchema',
334
                    'type_name' => 'integer',
335
                    'from_xml' => 'core\CertificationAuthorityEduPki::soapFromXmlInteger',
336
                    'to_xml' => 'core\CertificationAuthorityEduPki::soapToXmlInteger',
337
                ],
338
            ],
339
                ]
340
        );
341
        return $soap;
342
    }
343
344
    /**
345
     * a function that converts integers beyond PHP_INT_MAX to strings for
346
     * sending in XML messages
347
     *
348
     * taken and adapted from 
349
     * https://www.uni-muenster.de/WWUCA/de/howto-special-phpsoap.html
350
     * 
351
     * @param string $x the integer as an XML fragment
352
     * @return array the integer in array notation
353
     */
354
    public function soapFromXmlInteger($x)
355
    {
356
        $y = simplexml_load_string($x);
357
        return array(
358
            $y->getName(),
359
            $y->__toString()
360
        );
361
    }
362
363
    /**
364
     * a function that converts integers beyond PHP_INT_MAX to strings for
365
     * sending in XML messages
366
     * 
367
     * @param array $x the integer in array notation
368
     * @return string the integer as string in an XML fragment
369
     */
370
    public function soapToXmlInteger($x)
371
    {
372
        return '<' . $x[0] . '>'
373
                . htmlentities($x[1], ENT_NOQUOTES | ENT_XML1)
374
                . '</' . $x[0] . '>';
375
    }
376
377
    /**
378
     * generates a CSR which eduPKI likes (DC components etc.)
379
     * 
380
     * @param \OpenSSLAsymmetricKey $privateKey a private key
381
     * @param string                $fed        name of the federation, for C= field
382
     * @param string                $username   username, for CN= field
383
     * @return array the CSR along with some meta information
384
     * @throws Exception
385
     */
386
    public function generateCompatibleCsr($privateKey, $fed, $username): array
387
    {
388
        $tempdirArray = \core\common\Entity::createTemporaryDirectory("test");
389
        $tempdir = $tempdirArray['dir'];
390
        // dump private key into directory
391
        $outstring = "";
392
        openssl_pkey_export($privateKey, $outstring);
393
        file_put_contents($tempdir . "/pkey.pem", $outstring);
394
        // PHP can only do one DC in the Subject. But we need three.
395
        $execCmd = \config\Master::PATHS['openssl'] . " req -new -sha256 -key $tempdir/pkey.pem -out $tempdir/request.csr -subj /DC=test/DC=test/DC=eduroam/C=$fed/O=" . \config\ConfAssistant::CONSORTIUM['name'] . "/OU=$fed/CN=$username/emailAddress=$username";
396
        $this->loggerInstance->debug(2, "Calling openssl req with following cmdline: $execCmd\n");
397
        $output = [];
398
        $return = 999;
399
        exec($execCmd, $output, $return);
400
        if ($return !== 0) {
401
            throw new Exception("Non-zero return value from openssl req!");
402
        }
403
        $newCsr = file_get_contents("$tempdir/request.csr");
404
        // remove the temp dir!
405
        unlink("$tempdir/pkey.pem");
406
        unlink("$tempdir/request.csr");
407
        rmdir($tempdir);
408
        if ($newCsr === FALSE) {
409
            throw new Exception("Unable to create a CSR!");
410
        }
411
        return [
412
            "CSR_STRING" => $newCsr, // a string
413
            "CSR_OBJECT" => NULL,
414
            "USERNAME" => $username,
415
            "FED" => $fed
416
        ];
417
    }
418
419
    /**
420
     * generates a private key eduPKI can handle
421
     * 
422
     * @return \OpenSSLAsymmetricKey the key
423
     * @throws Exception
424
     */
425
    public function generateCompatiblePrivateKey()
426
    {
427
        $key = openssl_pkey_new(['private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA, 'encrypt_key' => FALSE]);
428
        if ($key === FALSE || is_resource($key)) {
429
            throw new Exception("Unable to generate a private key / not a PHP8 object.");
430
        }
431
        return $key;
432
    }
433
434
    /**
435
     * CAs don't have any local caching or other freshness issues
436
     * 
437
     * @return void
438
     */
439
    public function updateFreshness()
440
    {
441
        // nothing to be done here.
442
    }
443
}