|
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 { |
|
|
|
|
|
|
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; |
|
|
|
|
|
|
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
|
|
|
|
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:For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths