Passed
Push — master ( 20098a...a08f00 )
by Stefan
06:22
created

generateCompatiblePrivateKey()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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