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

CertificationAuthorityEmbeddedRSA::signRequest()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 16
dl 0
loc 20
rs 9.7333
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
class CertificationAuthorityEmbeddedRSA extends EntityWithDBProperties implements CertificationAuthorityInterface {
15
16
    private const LOCATION_ROOT_CA = ROOT . "/config/SilverbulletClientCerts/rootca-RSA.pem";
17
    private const LOCATION_ISSUING_CA = ROOT . "/config/SilverbulletClientCerts/real-RSA.pem";
18
    private const LOCATION_ISSUING_KEY = ROOT . "/config/SilverbulletClientCerts/real-RSA.key";
19
    private const LOCATION_CONFIG = ROOT . "/config/SilverbulletClientCerts/openssl-RSA.cnf";
20
21
    /**
22
     * string with the PEM variant of the root CA
23
     * 
24
     * @var string
25
     */
26
    public $rootPem;
27
28
    /**
29
     * string with the PEM variant of the issuing CA
30
     * 
31
     * @var resource
32
     */
33
    public $issuingCertRaw;
34
35
    /**
36
     * resource of the issuing CA
37
     * 
38
     * @var resource
39
     */
40
    private $issuingCert;
41
42
    /**
43
     * filename of the openssl.cnf file we use
44
     * @var string
45
     */
46
    private $conffile;
47
48
    /**
49
     * resource for private key
50
     * 
51
     * @var resource
52
     */
53
    private $issuingKey;
54
55
    public function __construct() {
56
        $this->databaseType = "INST";
57
        parent::__construct();
58
        $this->rootPem = file_get_contents(CertificationAuthorityEmbeddedRSA::LOCATION_ROOT_CA);
59
        if ($this->rootPem === FALSE) {
60
            throw new Exception("Root CA PEM file not found: " . CertificationAuthorityEmbeddedRSA::LOCATION_ROOT_CA);
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...
61
        }
62
        $this->issuingCertRaw = file_get_contents(CertificationAuthorityEmbeddedRSA::LOCATION_ISSUING_CA);
0 ignored issues
show
Documentation Bug introduced by
It seems like file_get_contents(core\C...A::LOCATION_ISSUING_CA) of type string is incompatible with the declared type resource of property $issuingCertRaw.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
63
        if ($this->issuingCertRaw === FALSE) {
64
            throw new Exception("Issuing CA PEM file not found: " . CertificationAuthorityEmbeddedRSA::LOCATION_ISSUING_CA);
65
        }
66
        $rootParsed = openssl_x509_read($this->rootPem);
67
        $this->issuingCert = openssl_x509_read($this->issuingCertRaw);
68
        if ($this->issuingCert === FALSE || $rootParsed === FALSE) {
69
            throw new Exception("At least one CA PEM file did not parse correctly!");
70
        }
71
        if (stat(CertificationAuthorityEmbeddedRSA::LOCATION_ISSUING_KEY) === FALSE) {
72
            throw new Exception("Private key not found: " . CertificationAuthorityEmbeddedRSA::LOCATION_ISSUING_KEY);
73
        }
74
        $this->issuingKey = openssl_pkey_get_private("file://" . CertificationAuthorityEmbeddedRSA::LOCATION_ISSUING_KEY);
0 ignored issues
show
Documentation Bug introduced by
It seems like openssl_pkey_get_private...::LOCATION_ISSUING_KEY) can also be of type false. However, the property $issuingKey is declared as type resource. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
75
        if ($this->issuingKey === FALSE) {
76
            throw new Exception("The private key did not parse correctly!");
77
        }
78
        if (stat(CertificationAuthorityEmbeddedRSA::LOCATION_CONFIG) === FALSE) {
79
            throw new Exception("openssl configuration not found: " . CertificationAuthorityEmbeddedRSA::LOCATION_CONFIG);
80
        }
81
        $this->conffile = CertificationAuthorityEmbeddedRSA::LOCATION_CONFIG;
82
    }
83
84
    public function triggerNewOCSPStatement(SilverbulletCertificate $cert): string {
85
        $certstatus = "";
86
        // get all relevant info from object properties
87
        if ($cert->serial >= 0) { // let's start with the assumption that the cert is valid
88
            if ($cert->revocationStatus == "REVOKED") {
89
                // already revoked, simply return canned OCSP response
90
                $certstatus = "R";
91
            } else {
92
                $certstatus = "V";
93
            }
94
        }
95
96
        $originalExpiry = date_create_from_format("Y-m-d H:i:s", $cert->expiry);
97
        if ($originalExpiry === FALSE) {
98
            throw new Exception("Unable to calculate original expiry date, input data bogus!");
99
        }
100
        $validity = date_diff(/** @scrutinizer ignore-type */ date_create(), $originalExpiry);
101
        if ($validity->invert == 1) {
102
            // negative! Cert is already expired, no need to revoke. 
103
            // No need to return anything really, but do return the last known OCSP statement to prevent special case
104
            $certstatus = "E";
105
        }
106
        $profile = new ProfileSilverbullet($cert->profileId);
107
        $inst = new IdP($profile->institution);
108
        $federation = strtoupper($inst->federation);
109
        // generate stub index.txt file
110
        $tempdirArray = \core\common\Entity::createTemporaryDirectory("test");
111
        $tempdir = $tempdirArray['dir'];
112
        $nowIndexTxt = (new \DateTime())->format("ymdHis") . "Z";
113
        $expiryIndexTxt = $originalExpiry->format("ymdHis") . "Z";
114
        $serialHex = strtoupper(dechex($cert->serial));
115
        if (strlen($serialHex) % 2 == 1) {
116
            $serialHex = "0" . $serialHex;
117
        }
118
119
        $indexStatement = "$certstatus\t$expiryIndexTxt\t" . ($certstatus == "R" ? "$nowIndexTxt,unspecified" : "") . "\t$serialHex\tunknown\t/O=" . CONFIG_CONFASSISTANT['CONSORTIUM']['name'] . "/OU=$federation/CN=$cert->username\n";
120
        $this->loggerInstance->debug(4, "index.txt contents-to-be: $indexStatement");
121
        if (!file_put_contents($tempdir . "/index.txt", $indexStatement)) {
122
            $this->loggerInstance->debug(1, "Unable to write openssl index.txt file for revocation handling!");
123
        }
124
        // index.txt.attr is dull but needs to exist
125
        file_put_contents($tempdir . "/index.txt.attr", "unique_subject = yes\n");
126
        // call "openssl ocsp" to manufacture our own OCSP statement
127
        // adding "-rmd sha1" to the following command-line makes the
128
        // choice of signature algorithm for the response explicit
129
        // but it's only available from openssl-1.1.0 (which we do not
130
        // want to require just for that one thing).
131
        $execCmd = CONFIG['PATHS']['openssl'] . " ocsp -issuer " . CertificationAuthorityEmbeddedRSA::LOCATION_ISSUING_CA . " -sha1 -ndays 10 -no_nonce -serial 0x$serialHex -CA " . CertificationAuthorityEmbeddedRSA::LOCATION_ISSUING_CA . " -rsigner " . CertificationAuthorityEmbeddedRSA::LOCATION_ISSUING_CA . " -rkey " . CertificationAuthorityEmbeddedRSA::LOCATION_ISSUING_KEY . " -index $tempdir/index.txt -no_cert_verify -respout $tempdir/$serialHex.response.der";
132
        $this->loggerInstance->debug(2, "Calling openssl ocsp with following cmdline: $execCmd\n");
133
        $output = [];
134
        $return = 999;
135
        exec($execCmd, $output, $return);
136
        if ($return !== 0) {
137
            throw new Exception("Non-zero return value from openssl ocsp!");
138
        }
139
        $ocsp = file_get_contents($tempdir . "/$serialHex.response.der");
140
        // remove the temp dir!
141
        unlink($tempdir . "/$serialHex.response.der");
142
        unlink($tempdir . "/index.txt.attr");
143
        unlink($tempdir . "/index.txt");
144
        rmdir($tempdir);
145
        $this->databaseHandle->exec("UPDATE silverbullet_certificate SET OCSP = ?, OCSP_timestamp = NOW() WHERE serial_number = ?", "si", $ocsp, $cert->serial);
146
        return $ocsp;
147
    }
148
149
    public function signRequest($csr, $expiryDays) {
150
        $nonDupSerialFound = FALSE;
151
        do {
152
            $serial = random_int(1000000000, PHP_INT_MAX);
153
            $dupeQuery = $this->databaseHandle->exec("SELECT serial_number FROM silverbullet_certificate WHERE serial_number = ? AND ca_type = ?", "is", $serial, "RSA");
0 ignored issues
show
Bug introduced by
'RSA' cannot be passed to core\DBConnection::exec() as the parameter $arguments expects a reference. ( Ignorable by Annotation )

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

153
            $dupeQuery = $this->databaseHandle->exec("SELECT serial_number FROM silverbullet_certificate WHERE serial_number = ? AND ca_type = ?", "is", $serial, /** @scrutinizer ignore-type */ "RSA");
Loading history...
154
            // SELECT -> resource, not boolean
155
            if (mysqli_num_rows(/** @scrutinizer ignore-type */$dupeQuery) == 0) {
156
                $nonDupSerialFound = TRUE;
157
            }
158
        } while (!$nonDupSerialFound);
159
        $this->loggerInstance->debug(5, "generateCertificate: signing imminent with unique serial $serial, cert type RSA.\n");
160
        $cert = openssl_csr_sign($csr, $this->issuingCert, $this->issuingCaKey, $expiryDays, ['digest_alg' => 'sha256', 'config' => $this->conffile], $serial);
161
        if ($cert === FALSE) {
162
            throw new Exception("Unable to sign the request and generate the certificate!");
163
        }
164
        return [
165
            "CERT" => $cert,
166
            "SERIAL" => $serial,
167
            "ISSUER" => $this->issuingCertRaw,
168
            "ROOT" => $this->rootPem,
169
        ];
170
    }
171
172
    public function revokeCertificate(SilverbulletCertificate $cert) : void {
173
        // the generic caller in SilverbulletCertificate::revokeCertificate
174
        // has already updated the DB. So all is done; we simply create a new
175
        // OCSP statement based on the updated DB content
176
        $this->triggerNewOCSPStatement($cert);
177
    }
178
179
    public function generateCompatibleCsr($privateKey, $fed, $username) {
180
        $newCsr = openssl_csr_new(
181
                ['O' => CONFIG_CONFASSISTANT['CONSORTIUM']['name'],
182
                    'OU' => $fed,
183
                    'CN' => $username,
184
                // 'emailAddress' => $username,
185
                ], $privateKey, [
186
            'digest_alg' => "sha256",
187
            'req_extensions' => 'v3_req',
188
                ]
189
        );
190
        if ($newCsr === FALSE) {
191
            throw new Exception("Unable to create a CSR!");
192
        }
193
        return [
194
            "CSR" => $newCsr, // resource
195
            "USERNAME" => $username,
196
            "FED" => $fed
197
        ];
198
    }
199
200
}
201