Passed
Push — master ( fcf5a1...20098a )
by Stefan
07:00
created

CertificationAuthorityEduPki   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 371
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 36
eloc 219
dl 0
loc 371
rs 9.52
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 4
A soapToXmlInteger() 0 4 1
A generateCompatibleCsr() 0 28 3
D signRequest() 0 148 15
A triggerNewOCSPStatement() 0 4 1
A initEduPKISoapSession() 0 56 4
A soapFromXmlInteger() 0 5 1
B revokeCertificate() 0 40 7
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
class CertificationAuthorityEduPki extends EntityWithDBProperties implements CertificationAuthorityInterface {
15
16
    private const LOCATION_RA_CERT = ROOT . "/config/SilverbulletClientCerts/edupki-test-ra.pem";
17
    private const LOCATION_RA_KEY = ROOT . "/config/SilverbulletClientCerts/edupki-test-ra.clearkey";
18
    private const LOCATION_WEBROOT = ROOT . "/config/SilverbulletClientCerts/eduPKI-webserver-root.pem";
19
    private const EDUPKI_RA_ID = 700;
20
    private const EDUPKI_CERT_PROFILE = "User SOAP";
21
    private const EDUPKI_RA_PKEY_PASSPHRASE = "...";
22
23
    /**
24
     * RA operator certificate in PEM format
25
     * 
26
     * @var string
27
     */
28
    private $raFile;
0 ignored issues
show
introduced by
The private property $raFile is not used, and could be removed.
Loading history...
29
30
    /**
31
     * resource holding the RA operator certificate
32
     * 
33
     * @var type 
0 ignored issues
show
Bug introduced by
The type core\type 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...
34
     */
35
    private $raResource;
0 ignored issues
show
introduced by
The private property $raResource is not used, and could be removed.
Loading history...
36
37
    /**
38
     * resource holding the private key to the RA cert
39
     * 
40
     * @var resource
41
     */
42
    private $raKey;
0 ignored issues
show
introduced by
The private property $raKey is not used, and could be removed.
Loading history...
43
44
    public function __construct() {
45
        $this->databaseType = "INST";
46
        parent::__construct();
47
48
        if (stat(CertificationAuthorityEduPki::LOCATION_RA_CERT) === FALSE) {
49
            throw new Exception("RA operator PEM file not found: " . CertificationAuthorityEduPki::LOCATION_RA_CERT);
0 ignored issues
show
Bug introduced by
The type core\Exception was not found. Did you mean Exception? If so, make sure to prefix the type with \.
Loading history...
50
        }
51
        if (stat(CertificationAuthorityEduPki::LOCATION_RA_KEY) === FALSE) {
52
            throw new Exception("RA operator private key file not found: " . CertificationAuthorityEduPki::LOCATION_RA_KEY);
53
        }
54
        if (stat(CertificationAuthorityEduPki::LOCATION_WEBROOT) === FALSE) {
55
            throw new Exception("CA website root CA file not found: " . CertificationAuthorityEduPki::LOCATION_WEBROOT);
56
        }
57
    }
58
59
    public function triggerNewOCSPStatement(SilverbulletCertificate $cert): string {
60
        // nothing to be done here - eduPKI have their own OCSP responder
61
        // and the certs point to it. So we are not in the loop.
62
        return "EXTERNAL";
63
    }
64
65
    public function signRequest($csr, $expiryDays): array {
66
        // initialise connection to eduPKI CA / eduroam RA and send the request to them
67
        try {
68
            $altArray = [# Array mit den Subject Alternative Names
69
                "email:" . $csr["USERNAME"]
70
            ];
71
            $soapPub = $this->initEduPKISoapSession("PUBLIC");
72
            $this->loggerInstance->debug(5, "FIRST ACTUAL SOAP REQUEST (Public, newRequest)!\n");
73
            $this->loggerInstance->debug(5, "PARAM_1: " . SilverbulletCertificate::EDUPKI_RA_ID . "\n");
0 ignored issues
show
Bug introduced by
The constant core\SilverbulletCertificate::EDUPKI_RA_ID was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
74
            $this->loggerInstance->debug(5, "PARAM_2: " . $csr["CSR"] . "\n");
75
            $this->loggerInstance->debug(5, "PARAM_3: ");
76
            $this->loggerInstance->debug(5, $altArray);
77
            $this->loggerInstance->debug(5, "PARAM_4: " . SilverbulletCertificate::EDUPKI_CERT_PROFILE . "\n");
0 ignored issues
show
Bug introduced by
The constant core\SilverbulletCertificate::EDUPKI_CERT_PROFILE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
78
            $this->loggerInstance->debug(5, "PARAM_5: " . sha1("notused") . "\n");
79
            $this->loggerInstance->debug(5, "PARAM_6: " . $csr["USERNAME"] . "\n");
80
            $this->loggerInstance->debug(5, "PARAM_7: " . $csr["USERNAME"] . "\n");
81
            $this->loggerInstance->debug(5, "PARAM_8: " . ProfileSilverbullet::PRODUCTNAME . "\n");
82
            $this->loggerInstance->debug(5, "PARAM_9: false\n");
83
            $soapNewRequest = $soapPub->newRequest(
84
                    SilverbulletCertificate::EDUPKI_RA_ID, # RA-ID
85
                    $csr["CSR"], # Request im PEM-Format
86
                    $altArray, # altNames
87
                    SilverbulletCertificate::EDUPKI_CERT_PROFILE, # Zertifikatprofil
88
                    sha1("notused"), # PIN
89
                    $csr["USERNAME"], # Name des Antragstellers
90
                    $csr["USERNAME"], # Kontakt-E-Mail
91
                    ProfileSilverbullet::PRODUCTNAME, # Organisationseinheit des Antragstellers
92
                    false                   # Veröffentlichen des Zertifikats?
93
            );
94
            $this->loggerInstance->debug(5, $soapPub->__getLastRequest());
95
            $this->loggerInstance->debug(5, $soapPub->__getLastResponse());
96
            if ($soapNewRequest == 0) {
97
                throw new Exception("Error when sending SOAP request (request serial number was zero). No further details available.");
98
            }
99
            $soapReqnum = intval($soapNewRequest);
100
        } catch (Exception $e) {
101
            // PHP 7.1 can do this much better
102
            if (is_soap_fault($e)) {
103
                throw new Exception("Error when sending SOAP request: " . "{$e->faultcode}:  {
104
                    $e->faultstring
105
                }\n");
106
            }
107
            throw new Exception("Something odd happened while doing the SOAP request:" . $e->getMessage());
108
        }
109
        try {
110
            $soap = SilverbulletCertificate::initEduPKISoapSession("RA");
0 ignored issues
show
Bug introduced by
The method initEduPKISoapSession() does not exist on core\SilverbulletCertificate. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

110
            /** @scrutinizer ignore-call */ 
111
            $soap = SilverbulletCertificate::initEduPKISoapSession("RA");

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
111
            // tell the CA the desired expiry date of the new certificate
112
            $expiry = new \DateTime();
113
            $expiry->modify("+$expiryDays day");
114
            $expiry->setTimezone(new \DateTimeZone("UTC"));
115
            $soapExpiryChange = $soap->setRequestParameters(
116
                    $soapReqnum, [
117
                "RaID" => SilverbulletCertificate::EDUPKI_RA_ID,
118
                "Role" => SilverbulletCertificate::EDUPKI_CERT_PROFILE,
119
                "Subject" => "DC=eduroam,DC=test,DC=test,C=" . $csr["FED"] . ",O=" . CONFIG_CONFASSISTANT['CONSORTIUM']['name'] . ",OU=" . $csr["FED"] . ",CN=" . $csr['USERNAME'] . ",emailAddress=" . $csr['USERNAME'],
120
                "SubjectAltNames" => ["email:" . $csr["USERNAME"]],
121
                "NotBefore" => (new \DateTime())->format('c'),
122
                "NotAfter" => $expiry->format('c'),
123
                    ]
124
            );
125
            if ($soapExpiryChange === FALSE) {
126
                throw new Exception("Error when sending SOAP request (unable to change expiry date).");
127
            }
128
            // retrieve the raw request to prepare for signature and approval
129
            // this seems to come out base64-decoded already; maybe PHP
130
            // considers this "convenience"? But we need it as sent on
131
            // the wire, so re-encode it!
132
            $soapCleartext = $soap->getRawRequest($soapReqnum);
133
134
            $this->loggerInstance->debug(5, "Actual received SOAP resonse for getRawRequest was:\n\n");
135
            $this->loggerInstance->debug(5, $soap->__getLastResponse());
136
            // for obnoxious reasons, we have to dump the request into a file and let pkcs7_sign read from the file
137
            // rather than just using the string. Grr.
138
            $tempdir = \core\common\Entity::createTemporaryDirectory("test");
139
            file_put_contents($tempdir['dir'] . "/content.txt", $soapCleartext);
140
            // retrieve our RA cert from filesystem                    
141
            // the RA certificates are not needed right now because we
142
            // have resorted to S/MIME signatures with openssl command-line
143
            // rather than the built-in functions. But that may change in
144
            // the future, so let's park these two lines for future use.
145
            // $raCertFile = file_get_contents(ROOT . "/config/SilverbulletClientCerts/edupki-test-ra.pem");
146
            // $raCert = openssl_x509_read($raCertFile);
147
            // $raKey = openssl_pkey_get_private("file://" . ROOT . "/config/SilverbulletClientCerts/edupki-test-ra.clearkey");
148
            // sign the data, using cmdline because openssl_pkcs7_sign produces strange results
149
            // -binary didn't help, nor switch -md to sha1 sha256 or sha512
150
            $this->loggerInstance->debug(5, "Actual content to be signed is this:\n  $soapCleartext\n");
151
            $execCmd = CONFIG['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";
152
            $this->loggerInstance->debug(2, "Calling openssl smime with following cmdline:   $execCmd\n");
153
            $output = [];
154
            $return = 999;
155
            exec($execCmd, $output, $return);
156
            if ($return !== 0) {
157
                throw new Exception("Non-zero return value from openssl smime!");
158
            }
159
            // and get the signature blob back from the filesystem
160
            $detachedSig = trim(file_get_contents($tempdir['dir'] . "/signature.txt"));
161
            $this->loggerInstance->debug(5, "Request for server approveRequest has parameters:\n");
162
            $this->loggerInstance->debug(5, $soapReqnum . "\n");
163
            $this->loggerInstance->debug(5, $soapCleartext . "\n"); // PHP magically encodes this as base64 while sending!
164
            $this->loggerInstance->debug(5, $detachedSig . "\n");
165
            $soapIssueCert = $soap->approveRequest($soapReqnum, $soapCleartext, $detachedSig);
166
            $this->loggerInstance->debug(5, "approveRequest Request was: \n" . $soap->__getLastRequest());
167
            $this->loggerInstance->debug(5, "approveRequest Response was: \n" . $soap->__getLastResponse());
168
            if ($soapIssueCert === FALSE) {
169
                throw new Exception("The locally approved request was NOT processed by the CA.");
170
            }
171
            // now, get the actual cert from the CA
172
            sleep(55);
173
            $counter = 55;
174
            $parsedCert = NULL;
175
            do {
176
                $counter += 5;
177
                sleep(5); // always start with a wait. Signature round-trip on the server side is at least one minute.
178
                $soapCert = $soap->getCertificateByRequestSerial($soapReqnum);
179
                $x509 = new common\X509();
180
                if (strlen($soapCert) > 10) {
181
                    $parsedCert = $x509->processCertificate($soapCert);
182
                }
183
            } while (!is_array($parsedCert) && $counter < 500);
184
185
            if (!is_array($parsedCert)) {
1 ignored issue
show
introduced by
The condition is_array($parsedCert) is always false.
Loading history...
186
                throw new Exception("We did not actually get a certificate after waiting for 5 minutes.");
187
            }
188
            // let's get the CA certificate chain
189
190
            $caInfo = $soap->getCAInfo();
191
            $certList = $x509->splitCertificate($caInfo->CAChain[0]);
192
            // find the root
193
            $theRoot = "";
194
            foreach ($certList as $oneCert) {
195
                $content = $x509->processCertificate($oneCert);
196
                if ($content['root'] == 1) {
197
                    $theRoot = $content;
198
                }
199
            }
200
            if ($theRoot == "") {
201
                throw new Exception("CAInfo has no root certificate for us!");
202
            }
203
        } catch (SoapFault $e) {
0 ignored issues
show
Bug introduced by
The type core\SoapFault was not found. Did you mean SoapFault? If so, make sure to prefix the type with \.
Loading history...
204
            throw new Exception("SoapFault: Error when sending or receiving SOAP message: " . "{$e->faultcode}: {$e->faultname}: {$e->faultstring}: {$e->faultactor}: {$e->detail}: {$e->headerfault}\n");
205
        } catch (Exception $e) {
206
            throw new Exception("Exception: Something odd happened between the SOAP requests:" . $e->getMessage());
207
        }
208
        return [
209
            "CERT" => openssl_x509_read($parsedCert['pem']),
210
            "SERIAL" => $parsedCert['full_details']['serialNumber'],
211
            "ISSUER" => $theRoot,
212
            "ROOT" => $theRoot,
213
        ];
214
    }
215
216
    public function revokeCertificate(SilverbulletCertificate $cert): void {
217
        try {
218
            $soap = $this->initEduPKISoapSession("RA");
219
            $soapRevocationSerial = $soap->newRevocationRequest(["Serial", $cert->serial], "");
220
            if ($soapRevocationSerial == 0) {
221
                throw new Exception("Unable to create revocation request, serial number was zero.");
222
            }
223
            // retrieve the raw request to prepare for signature and approval
224
            $soapRawRevRequest = $soap->getRawRevocationRequest($soapRevocationSerial);
225
            if (strlen($soapRawRevRequest) < 10) { // very basic error handling
226
                throw new Exception("Suspiciously short data to sign!");
227
            }
228
            // for obnoxious reasons, we have to dump the request into a file and let pkcs7_sign read from the file
229
            // rather than just using the string. Grr.
230
            $tempdir = \core\common\Entity::createTemporaryDirectory("test");
231
            file_put_contents($tempdir['dir'] . "/content.txt", $soapRawRevRequest);
232
            // retrieve our RA cert from filesystem
233
            // sign the data, using cmdline because openssl_pkcs7_sign produces strange results
234
            // -binary didn't help, nor switch -md to sha1 sha256 or sha512
235
            $this->loggerInstance->debug(5, "Actual content to be signed is this:\n$soapRawRevRequest\n");
236
            $execCmd = CONFIG['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;
237
            $this->loggerInstance->debug(2, "Calling openssl smime with following cmdline: $execCmd\n");
238
            $output = [];
239
            $return = 999;
240
            exec($execCmd, $output, $return);
241
            if ($return !== 0) {
242
                throw new Exception("Non-zero return value from openssl smime!");
243
            }
244
            // and get the signature blob back from the filesystem
245
            $detachedSig = trim(file_get_contents($tempdir['dir'] . "/signature.txt"));
246
            $soapIssueRev = $soap->approveRevocationRequest($soapRevocationSerial, $soapRawRevRequest, $detachedSig);
247
            if ($soapIssueRev === FALSE) {
248
                throw new Exception("The locally approved revocation request was NOT processed by the CA.");
249
            }
250
        } catch (Exception $e) {
251
            // PHP 7.1 can do this much better
252
            if (is_soap_fault($e)) {
253
                throw new Exception("Error when sending SOAP request: " . "{$e->faultcode}: {$e->faultstring}\n");
254
            }
255
            throw new Exception("Something odd happened while doing the SOAP request:" . $e->getMessage());
256
        }
257
    }
258
259
    /**
260
     * sets up a connection to the eduPKI SOAP interfaces
261
     * There is a public interface and an RA-restricted interface;
262
     * the latter needs an RA client certificate to identify the operator
263
     * 
264
     * @param string $type to which interface should we connect to - "PUBLIC" or "RA"
265
     * @return \SoapClient the connection object
266
     * @throws Exception
267
     */
268
    private function initEduPKISoapSession($type) {
269
        // set context parameters common to both endpoints
270
        $context_params = [
271
            'http' => [
272
                'timeout' => 60,
273
                'user_agent' => 'Stefan',
274
                'protocol_version' => 1.1
275
            ],
276
            'ssl' => [
277
                'verify_peer' => true,
278
                'verify_peer_name' => true,
279
                // below is the CA "/C=DE/O=Deutsche Telekom AG/OU=T-TeleSec Trust Center/CN=Deutsche Telekom Root CA 2"
280
                'cafile' => CertificationAuthorityEduPki::LOCATION_WEBROOT,
281
                'verify_depth' => 5,
282
                'capture_peer_cert' => true,
283
            ],
284
        ];
285
        $url = "";
286
        switch ($type) {
287
            case "PUBLIC":
288
                $url = "https://pki.edupki.org/edupki-test-ca/cgi-bin/pub/soap?wsdl=1";
289
                $context_params['ssl']['peer_name'] = 'pki.edupki.org';
290
                break;
291
            case "RA":
292
                $url = "https://ra.edupki.org/edupki-test-ca/cgi-bin/ra/soap?wsdl=1";
293
                $context_params['ssl']['peer_name'] = 'ra.edupki.org';
294
                break;
295
            default:
296
                throw new Exception("Unknown type of eduPKI interface requested.");
297
        }
298
        if ($type == "RA") { // add client auth parameters to the context
299
            $context_params['ssl']['local_cert'] = CertificationAuthorityEduPki::LOCATION_RA_CERT;
300
            $context_params['ssl']['local_pk'] = CertificationAuthorityEduPki::LOCATION_RA_KEY;
301
            // $context_params['ssl']['passphrase'] = SilverbulletCertificate::EDUPKI_RA_PKEY_PASSPHRASE;
302
        }
303
        // initialse connection to eduPKI CA / eduroam RA
304
        $soap = new \SoapClient($url, [
305
            'soap_version' => SOAP_1_1,
306
            'trace' => TRUE,
307
            'exceptions' => TRUE,
308
            'connection_timeout' => 5, // if can't establish the connection within 5 sec, something's wrong
309
            'cache_wsdl' => WSDL_CACHE_NONE,
310
            'user_agent' => 'eduroam CAT to eduPKI SOAP Interface',
311
            'features' => SOAP_SINGLE_ELEMENT_ARRAYS,
312
            'stream_context' => stream_context_create($context_params),
313
            'typemap' => [
314
                [
315
                    'type_ns' => 'http://www.w3.org/2001/XMLSchema',
316
                    'type_name' => 'integer',
317
                    'from_xml' => 'core\CertificationAuthorityEduPki::soapFromXmlInteger',
318
                    'to_xml' => 'core\CertificationAuthorityEduPki::soapToXmlInteger',
319
                ],
320
            ],
321
                ]
322
        );
323
        return $soap;
324
    }
325
326
    /**
327
     * a function that converts integers beyond PHP_INT_MAX to strings for
328
     * sending in XML messages
329
     *
330
     * taken and adapted from 
331
     * https://www.uni-muenster.de/WWUCA/de/howto-special-phpsoap.html
332
     * 
333
     * @param string $x the integer as an XML fragment
334
     * @return array the integer in array notation
335
     */
336
    public function soapFromXmlInteger($x) {
337
        $y = simplexml_load_string($x);
338
        return array(
339
            $y->getName(),
340
            $y->__toString()
341
        );
342
    }
343
344
    /**
345
     * a function that converts integers beyond PHP_INT_MAX to strings for
346
     * sending in XML messages
347
     * 
348
     * @param array $x the integer in array notation
349
     * @return string the integer as string in an XML fragment
350
     */
351
    public function soapToXmlInteger($x) {
352
        return '<' . $x[0] . '>'
353
                . htmlentities($x[1], ENT_NOQUOTES | ENT_XML1)
354
                . '</' . $x[0] . '>';
355
    }
356
357
    public function generateCompatibleCsr($privateKey, $fed, $username): array {
358
        $tempdirArray = \core\common\Entity::createTemporaryDirectory("test");
359
        $tempdir = $tempdirArray['dir'];
360
        // dump private key into directory
361
        $outstring = "";
362
        openssl_pkey_export($privateKey, $outstring);
363
        file_put_contents($tempdir . "/pkey.pem", $outstring);
364
        // PHP can only do one DC in the Subject. But we need three.
365
        $execCmd = CONFIG['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";
366
        $this->loggerInstance->debug(2, "Calling openssl req with following cmdline: $execCmd\n");
367
        $output = [];
368
        $return = 999;
369
        exec($execCmd, $output, $return);
370
        if ($return !== 0) {
371
            throw new Exception("Non-zero return value from openssl req!");
372
        }
373
        $newCsr = file_get_contents("$tempdir/request.csr");
374
        // remove the temp dir!
375
        unlink("$tempdir/pkey.pem");
376
        unlink("$tempdir/request.csr");
377
        rmdir($tempdir);
378
        if ($newCsr === FALSE) {
379
            throw new Exception("Unable to create a CSR!");
380
        }
381
        return [
382
            "CSR" => $newCsr, // a string
383
            "USERNAME" => $username,
384
            "FED" => $fed
385
        ];
386
    }
387
388
}
389