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

CertificationAuthorityEmbeddedECDSA::signRequest()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 21
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 17
dl 0
loc 21
rs 9.7
c 0
b 0
f 0
cc 4
nc 4
nop 2
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
16
class CertificationAuthorityEmbeddedECDSA extends EntityWithDBProperties implements CertificationAuthorityInterface {
17
18
    private const LOCATION_ROOT_CA = ROOT . "/config/SilverbulletClientCerts/rootca-ECDSA.pem";
19
    private const LOCATION_ISSUING_CA = ROOT . "/config/SilverbulletClientCerts/real-ECDSA.pem";
20
    private const LOCATION_ISSUING_KEY = ROOT . "/config/SilverbulletClientCerts/real-ECDSA.key";
21
    private const LOCATION_CONFIG = ROOT . "/config/SilverbulletClientCerts/openssl-ECDSA.cnf";
22
23
    /**
24
     * string with the PEM variant of the root CA
25
     * 
26
     * @var string
27
     */
28
    public $rootPem;
29
30
    /**
31
     * string with the PEM variant of the issuing CA
32
     * 
33
     * @var string
34
     */
35
    public $issuingCertRaw;
36
37
    /**
38
     * resource of the issuing CA
39
     * 
40
     * @var resource
41
     */
42
    private $issuingCert;
43
44
    /**
45
     * filename of the openssl.cnf file we use
46
     * @var string
47
     */
48
    private $conffile;
49
50
    /**
51
     * resource for private key
52
     * 
53
     * @var resource
54
     */
55
    private $issuingKey;
56
57
    public function __construct() {
58
        $this->databaseType = "INST";
59
        parent::__construct();
60
        $this->rootPem = file_get_contents(CertificationAuthorityEmbeddedECDSA::LOCATION_ROOT_CA);
61
        if ($this->rootPem === FALSE) {
62
            throw new Exception("Root CA PEM file not found: " . CertificationAuthorityEmbeddedECDSA::LOCATION_ROOT_CA);
63
        }
64
        $this->issuingCertRaw = file_get_contents(CertificationAuthorityEmbeddedECDSA::LOCATION_ISSUING_CA);
65
        if ($this->issuingCertRaw === FALSE) {
66
            throw new Exception("Issuing CA PEM file not found: " . CertificationAuthorityEmbeddedECDSA::LOCATION_ISSUING_CA);
67
        }
68
        $rootParsed = openssl_x509_read($this->rootPem);
69
        $this->issuingCert = openssl_x509_read($this->issuingCertRaw);
70
        if ($this->issuingCert === FALSE || $rootParsed === FALSE) {
71
            throw new Exception("At least one CA PEM file did not parse correctly!");
72
        }
73
        if (stat(CertificationAuthorityEmbeddedECDSA::LOCATION_ISSUING_KEY) === FALSE) {
74
            throw new Exception("Private key not found: " . CertificationAuthorityEmbeddedECDSA::LOCATION_ISSUING_KEY);
75
        }
76
        $issuingKeyTemp = openssl_pkey_get_private("file://" . CertificationAuthorityEmbeddedECDSA::LOCATION_ISSUING_KEY);
77
        if ($issuingKeyTemp === FALSE) {
78
            throw new Exception("The private key did not parse correctly!");
79
        }
80
        $this->issuingKey = $issuingKeyTemp;
81
        if (stat(CertificationAuthorityEmbeddedECDSA::LOCATION_CONFIG) === FALSE) {
82
            throw new Exception("openssl configuration not found: " . CertificationAuthorityEmbeddedECDSA::LOCATION_CONFIG);
83
        }
84
        $this->conffile = CertificationAuthorityEmbeddedECDSA::LOCATION_CONFIG;
85
    }
86
87
    public function triggerNewOCSPStatement(SilverbulletCertificate $cert): string {
88
        $certstatus = "";
89
        // get all relevant info from object properties
90
        if ($cert->serial >= 0) { // let's start with the assumption that the cert is valid
91
            if ($cert->revocationStatus == "REVOKED") {
92
                // already revoked, simply return canned OCSP response
93
                $certstatus = "R";
94
            } else {
95
                $certstatus = "V";
96
            }
97
        }
98
99
        $originalExpiry = date_create_from_format("Y-m-d H:i:s", $cert->expiry);
100
        if ($originalExpiry === FALSE) {
101
            throw new Exception("Unable to calculate original expiry date, input data bogus!");
102
        }
103
        $validity = date_diff(/** @scrutinizer ignore-type */ date_create(), $originalExpiry);
104
        if ($validity->invert == 1) {
105
            // negative! Cert is already expired, no need to revoke. 
106
            // No need to return anything really, but do return the last known OCSP statement to prevent special case
107
            $certstatus = "E";
108
        }
109
        $profile = new ProfileSilverbullet($cert->profileId);
110
        $inst = new IdP($profile->institution);
111
        $federation = strtoupper($inst->federation);
112
        // generate stub index.txt file
113
        $tempdirArray = \core\common\Entity::createTemporaryDirectory("test");
114
        $tempdir = $tempdirArray['dir'];
115
        $nowIndexTxt = (new \DateTime())->format("ymdHis") . "Z";
116
        $expiryIndexTxt = $originalExpiry->format("ymdHis") . "Z";
117
        $serialHex = strtoupper(dechex($cert->serial));
118
        if (strlen($serialHex) % 2 == 1) {
119
            $serialHex = "0" . $serialHex;
120
        }
121
122
        $indexStatement = "$certstatus\t$expiryIndexTxt\t" . ($certstatus == "R" ? "$nowIndexTxt,unspecified" : "") . "\t$serialHex\tunknown\t/O=" . CONFIG_CONFASSISTANT['CONSORTIUM']['name'] . "/OU=$federation/CN=$cert->username\n";
123
        $this->loggerInstance->debug(4, "index.txt contents-to-be: $indexStatement");
124
        if (!file_put_contents($tempdir . "/index.txt", $indexStatement)) {
125
            $this->loggerInstance->debug(1, "Unable to write openssl index.txt file for revocation handling!");
126
        }
127
        // index.txt.attr is dull but needs to exist
128
        file_put_contents($tempdir . "/index.txt.attr", "unique_subject = yes\n");
129
        // call "openssl ocsp" to manufacture our own OCSP statement
130
        // adding "-rmd sha1" to the following command-line makes the
131
        // choice of signature algorithm for the response explicit
132
        // but it's only available from openssl-1.1.0 (which we do not
133
        // want to require just for that one thing).
134
        $execCmd = CONFIG['PATHS']['openssl'] . " ocsp -issuer " . CertificationAuthorityEmbeddedECDSA::LOCATION_ISSUING_CA . " -sha1 -ndays 10 -no_nonce -serial 0x$serialHex -CA " . CertificationAuthorityEmbeddedECDSA::LOCATION_ISSUING_CA . " -rsigner " . CertificationAuthorityEmbeddedECDSA::LOCATION_ISSUING_CA . " -rkey " . CertificationAuthorityEmbeddedECDSA::LOCATION_ISSUING_KEY . " -index $tempdir/index.txt -no_cert_verify -respout $tempdir/$serialHex.response.der";
135
        $this->loggerInstance->debug(2, "Calling openssl ocsp with following cmdline: $execCmd\n");
136
        $output = [];
137
        $return = 999;
138
        exec($execCmd, $output, $return);
139
        if ($return !== 0) {
140
            throw new Exception("Non-zero return value from openssl ocsp!");
141
        }
142
        $ocsp = file_get_contents($tempdir . "/$serialHex.response.der");
143
        // remove the temp dir!
144
        unlink($tempdir . "/$serialHex.response.der");
145
        unlink($tempdir . "/index.txt.attr");
146
        unlink($tempdir . "/index.txt");
147
        rmdir($tempdir);
148
        $this->databaseHandle->exec("UPDATE silverbullet_certificate SET OCSP = ?, OCSP_timestamp = NOW() WHERE serial_number = ?", "si", $ocsp, $cert->serial);
149
        return $ocsp;
150
    }
151
152
    public function signRequest($csr, $expiryDays) {
153
        $nonDupSerialFound = FALSE;
154
        do {
155
            $serial = random_int(1000000000, PHP_INT_MAX);
156
            $ecdsa = \devices\Devices::SUPPORT_EMBEDDED_ECDSA;
157
            $dupeQuery = $this->databaseHandle->exec("SELECT serial_number FROM silverbullet_certificate WHERE serial_number = ? AND ca_type = ?", "is", $serial, $ecdsa);
158
            // SELECT -> resource, not boolean
159
            if (mysqli_num_rows(/** @scrutinizer ignore-type */$dupeQuery) == 0) {
160
                $nonDupSerialFound = TRUE;
161
            }
162
        } while (!$nonDupSerialFound);
163
        $this->loggerInstance->debug(5, "generateCertificate: signing imminent with unique serial $serial, cert type ECDSA.\n");
164
        $cert = openssl_csr_sign($csr, $this->issuingCert, $this->issuingCaKey, $expiryDays, ['digest_alg' => 'ecdsa-with-SHA1', 'config' => $this->conffile], $serial);
165
        if ($cert === FALSE) {
166
            throw new Exception("Unable to sign the request and generate the certificate!");
167
        }
168
        return [
169
            "CERT" => $cert,
170
            "SERIAL" => $serial,
171
            "ISSUER" => $this->issuingCertRaw,
172
            "ROOT" => $this->rootPem,
173
        ];
174
    }
175
176
    public function revokeCertificate(SilverbulletCertificate $cert): void {
177
        // the generic caller in SilverbulletCertificate::revokeCertificate
178
        // has already updated the DB. So all is done; we simply create a new
179
        // OCSP statement based on the updated DB content
180
        $this->triggerNewOCSPStatement($cert);
181
    }
182
183
    public function generateCompatibleCsr($privateKey, $fed, $username) {
184
        $newCsr = openssl_csr_new(
185
                ['O' => CONFIG_CONFASSISTANT['CONSORTIUM']['name'],
186
                    'OU' => $fed,
187
                    'CN' => $username,
188
                // 'emailAddress' => $username,
189
                ], $privateKey, [
190
            'digest_alg' => "ecdsa-with-SHA1",
191
            'req_extensions' => 'v3_req',
192
                ]
193
        );
194
        if ($newCsr === FALSE) {
195
            throw new Exception("Unable to create a CSR!");
196
        }
197
        return [
198
            "CSR" => $newCsr, // resource
199
            "USERNAME" => $username,
200
            "FED" => $fed
201
        ];
202
    }
203
204
    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...
205
        $key = openssl_pkey_new(['curve_name' => 'secp384r1', 'private_key_type' => OPENSSL_KEYTYPE_EC, 'encrypt_key' => FALSE]);
206
        if ($key === FALSE) {
207
            throw new Exception("Unable to generate a private key.");
208
        }
209
        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...
210
    }
211
212
    /**
213
     * CAs don't have any local caching or other freshness issues
214
     * 
215
     * @return void
216
     */
217
    public function updateFreshness() {
218
        // nothing to be done here.
219
    }
220
221
}
222