bug 1255: Check for other algorithms than RSA in check_weak_keys()
[cacert-devel.git] / includes / lib / check_weak_key.php
1 <?php /*
2 LibreSSL - CAcert web application
3 Copyright (C) 2004-2011 CAcert Inc.
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; version 2 of the License.
8
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with this program; if not, write to the Free Software
16 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 */
18
19 // failWithId()
20 require_once 'general.php';
21
22
23 /**
24 * Checks whether the given CSR contains a vulnerable key
25 *
26 * @param $csr string
27 * The CSR to be checked
28 * @param $encoding string [optional]
29 * The encoding the CSR is in (for the "-inform" parameter of OpenSSL,
30 * currently only "PEM" (default) or "DER" allowed)
31 * @return string containing the reason if the key is considered weak,
32 * empty string otherwise
33 */
34 function checkWeakKeyCSR($csr, $encoding = "PEM")
35 {
36 $encoding = escapeshellarg($encoding);
37 $status = runCommand("openssl req -inform $encoding -text -noout",
38 $csr, $csrText);
39 if ($status === true) {
40 return failWithId("checkWeakKeyCSR(): Failed to start OpenSSL");
41 }
42
43 if ($status !== 0 || $csrText === "") {
44 return _("I didn't receive a valid Certificate Request. Hit ".
45 "the back button and try again.");
46 }
47
48 return checkWeakKeyText($csrText);
49 }
50
51 /**
52 * Checks whether the given X509 certificate contains a vulnerable key
53 *
54 * @param $cert string
55 * The X509 certificate to be checked
56 * @param $encoding string [optional]
57 * The encoding the certificate is in (for the "-inform" parameter of
58 * OpenSSL, currently only "PEM" (default), "DER" or "NET" allowed)
59 * @return string containing the reason if the key is considered weak,
60 * empty string otherwise
61 */
62 function checkWeakKeyX509($cert, $encoding = "PEM")
63 {
64 $encoding = escapeshellarg($encoding);
65 $status = runCommand("openssl x509 -inform $encoding -text -noout",
66 $cert, $certText);
67 if ($status === true) {
68 return failWithId("checkWeakKeyX509(): Failed to start OpenSSL");
69 }
70
71 if ($status !== 0 || $certText === "") {
72 return _("I didn't receive a valid Certificate Request. Hit ".
73 "the back button and try again.");
74 }
75
76 return checkWeakKeyText($certText);
77 }
78
79 /**
80 * Checks whether the given SPKAC contains a vulnerable key
81 *
82 * @param $spkac string
83 * The SPKAC to be checked
84 * @param $spkacname string [optional]
85 * The name of the variable that contains the SPKAC. The default is
86 * "SPKAC"
87 * @return string containing the reason if the key is considered weak,
88 * empty string otherwise
89 */
90 function checkWeakKeySPKAC($spkac, $spkacname = "SPKAC")
91 {
92 $spkacname = escapeshellarg($spkacname);
93 $status = runCommand("openssl spkac -spkac $spkacname", $spkac, $spkacText);
94 if ($status === true) {
95 return failWithId("checkWeakKeySPKAC(): Failed to start OpenSSL");
96 }
97
98 if ($status !== 0 || $spkacText === "") {
99 return _("I didn't receive a valid Certificate Request. Hit the ".
100 "back button and try again.");
101 }
102
103 return checkWeakKeyText($spkacText);
104 }
105
106 /**
107 * Checks whether the given text representation of a CSR or a SPKAC contains
108 * a weak key
109 *
110 * @param $text string
111 * The text representation of a key as output by the
112 * "openssl <foo> -text -noout" commands
113 * @return string containing the reason if the key is considered weak,
114 * empty string otherwise
115 */
116 function checkWeakKeyText($text)
117 {
118 /* Which public key algorithm? */
119 if (!preg_match('/^\s*Public Key Algorithm: ([^\s]+)$/m', $text,
120 $algorithm))
121 {
122 return failWithId("checkWeakKeyText(): Couldn't extract the ".
123 "public key algorithm used.\nData:\n$text");
124 } else {
125 $algorithm = $algorithm[1];
126 }
127
128
129 switch ((string)$algorithm) {
130 case "rsaEncryption":
131 if (!preg_match('/^\s*RSA Public Key: \((\d+) bit\)$/m', $text, $keysize))
132 {
133 return failWithId("checkWeakKeyText(): Couldn't parse the RSA ".
134 "key size.\nData:\n$text");
135 } else {
136 $keysize = intval($keysize[1]);
137 }
138
139 if ($keysize < 2048)
140 {
141 return sprintf(_("The keys that you use are very small ".
142 "and therefore insecure. Please generate stronger ".
143 "keys. More information about this issue can be ".
144 "found in %sthe wiki%s"),
145 "<a href='//wiki.cacert.org/WeakKeys#SmallKey'>",
146 "</a>");
147 }
148
149 $debianVuln = checkDebianVulnerability($text, $keysize);
150 if ($debianVuln === true)
151 {
152 return sprintf(_("The keys you use have very likely been ".
153 "generated with a vulnerable version of OpenSSL which ".
154 "was distributed by debian. Please generate new keys. ".
155 "More information about this issue can be found in ".
156 "%sthe wiki%s"),
157 "<a href='//wiki.cacert.org/WeakKeys#DebianVulnerability'>",
158 "</a>");
159 } elseif ($debianVuln === false) {
160 // not vulnerable => do nothing
161 } else {
162 return failWithId("checkWeakKeyText(): Something went wrong in".
163 "checkDebianVulnerability().\nKeysize: $keysize\n".
164 "Data:\n$text");
165 }
166
167 if (!preg_match('/^\s*Exponent: (\d+) \(0x[0-9a-fA-F]+\)$/m', $text,
168 $exponent))
169 {
170 return failWithId("checkWeakKeyText(): Couldn't parse the RSA ".
171 "exponent.\nData:\n$text");
172 } else {
173 $exponent = $exponent[1]; // exponent might be very big =>
174 //handle as string using bc*()
175
176 if (bccomp($exponent, "3") === 0)
177 {
178 return sprintf(_("The keys you use might be insecure. ".
179 "Although there is currently no known attack for ".
180 "reasonable encryption schemes, we're being ".
181 "cautious and don't allow certificates for such ".
182 "keys. Please generate stronger keys. More ".
183 "information about this issue can be found in ".
184 "%sthe wiki%s"),
185 "<a href='//wiki.cacert.org/WeakKeys#SmallExponent'>",
186 "</a>");
187 } elseif (!(bccomp($exponent, "65537") >= 0 &&
188 (bccomp($exponent, "100000") === -1 ||
189 // speed things up if way smaller than 2^256
190 bccomp($exponent, bcpow("2", "256")) === -1) )) {
191 // 65537 <= exponent < 2^256 recommended by NIST
192 // not critical but log so we have some statistics about
193 // affected users
194 trigger_error("checkWeakKeyText(): Certificate for ".
195 "unsuitable exponent '$exponent' requested",
196 E_USER_NOTICE);
197 }
198 }
199
200 break;
201
202 /*
203 //Fails to work due to outdated OpenSSL 0.9.8o
204 //For this to work OpenSSL 1.0.1f or newer is required
205 //which is currently unavailable on the systems
206 //If DSA2048 or longer is used the CSR hangs pending on the signer.
207 case "dsaEncryption":
208 if (!preg_match('/^\s*Public Key Algorithm:\s+dsaEncryption\s+pub:\s+([0-9a-fA-F:\s]+)\s+P:\s+([0-9a-fA-F:\s]+)\s+Q:\s+([0-9a-fA-F:\s]+)\s+G:\s+([0-9a-fA-F:\s]+)\s+$/sm', $text, $keydetail))
209 {
210 return failWithId("checkWeakKeyText(): Couldn't parse the DSA ".
211 "key size.\nData:\n$text");
212 }
213
214 $key_pub = strtr(preg_replace("/[^0-9a-fA-F]/", "", $keydetail[1]), "ABCDEF", "abcdef");
215 $key_P = strtr(preg_replace("/[^0-9a-fA-F]/", "", $keydetail[2]), "ABCDEF", "abcdef");
216 $key_Q = strtr(preg_replace("/[^0-9a-fA-F]/", "", $keydetail[3]), "ABCDEF", "abcdef");
217 $key_G = strtr(preg_replace("/[^0-9a-fA-F]/", "", $keydetail[4]), "ABCDEF", "abcdef");
218
219 //Verify the numbers provided by the client
220 $num_pub = @gmp_init($key_pub, 16);
221 $num_P = @gmp_init($key_P, 16);
222 $num_Q = @gmp_init($key_Q, 16);
223 $num_G = @gmp_init($key_G, 16);
224
225 $bit_pub = ltrim(gmp_strval($num_pub, 2), "0");
226 $bit_P = ltrim(gmp_strval($num_P, 2), "0");
227 $bit_Q = ltrim(gmp_strval($num_Q, 2), "0");
228 $bit_G = ltrim(gmp_strval($num_G, 2), "0");
229
230 $keysize = strlen($bit_P);
231
232 if ($keysize < 2048) {
233 return sprintf(_("The keys that you use are very small ".
234 "and therefore insecure. Please generate stronger ".
235 "keys. More information about this issue can be ".
236 "found in %sthe wiki%s"),
237 "<a href='//wiki.cacert.org/WeakKeys#SmallKey'>",
238 "</a>");
239 }
240
241 //Following checks based on description of key generation in Wikipedia
242 //These checks do not ensure a strong key, but at least check for enough sanity in the key material
243 // cf. https://en.wikipedia.org/wiki/Digital_Signature_Algorithm#Key_generation
244
245 //Check that P is prime
246 if(!gmp_testprime($num_P)) {
247 return failWithId("checkWeakKeyText(): The supplied DSA ".
248 "key does seem to have a non-prime public modulus.\nData:\n$text");
249 }
250
251 //Check that Q is prime
252 if(!gmp_testprime($num_Q)) {
253 return failWithId("checkWeakKeyText(): The supplied DSA ".
254 "key does seem to have a non-prime Q-value.\nData:\n$text");
255 }
256
257 //Check if P-1 is diviseable by Q
258 if(0 !== gmp_cmp("1", gmp_mod($num_P, $num_Q))) {
259 return failWithId("checkWeakKeyText(): The supplied DSA ".
260 "key does seem to have P mod Q === 1 (i.e. P-1 is not diviseable by Q).\nData:\n$text");
261 }
262
263 //Check the numbers are all less than the public modulus P
264 if(0 <= gmp_cmp($num_Q, $num_P)) || 0 <= gmp_cmp($num_G, $num_P)) || 0 <= gmp_cmp($num_Y, $num_P))) {
265 return failWithId("checkWeakKeyText(): The supplied DSA ".
266 "key does seem to be normalized to have Q < P, G < P and Y < P.\nData:\n$text");
267 }
268
269 break;
270 */
271
272 default:
273 return _("The keys you supplied use an unrecognized algorithm. ".
274 "For security reasons these keys can not be signed by CAcert.");
275 }
276
277 /* No weakness found */
278 return "";
279 }
280
281 /**
282 * Reimplement the functionality of the openssl-vulnkey tool
283 *
284 * @param $text string
285 * The text representation of a key as output by the
286 * "openssl <foo> -text -noout" commands
287 * @param $keysize int [optional]
288 * If the key size is already known it can be provided so it doesn't
289 * have to be parsed again. This also skips the check whether the key
290 * is an RSA key => use wisely
291 * @return TRUE if key is vulnerable, FALSE otherwise, NULL in case of error
292 */
293 function checkDebianVulnerability($text, $keysize = 0)
294 {
295 $keysize = intval($keysize);
296
297 if ($keysize === 0)
298 {
299 /* Which public key algorithm? */
300 if (!preg_match('/^\s*Public Key Algorithm: ([^\s]+)$/m', $text,
301 $algorithm))
302 {
303 trigger_error("checkDebianVulnerability(): Couldn't extract ".
304 "the public key algorithm used.\nData:\n$text",
305 E_USER_WARNING);
306 return null;
307 } else {
308 $algorithm = $algorithm[1];
309 }
310
311 if ($algorithm !== "rsaEncryption") return false;
312
313 /* Extract public key size */
314 if (!preg_match('/^\s*RSA Public Key: \((\d+) bit\)$/m', $text,
315 $keysize))
316 {
317 trigger_error("checkDebianVulnerability(): Couldn't parse the ".
318 "RSA key size.\nData:\n$text", E_USER_WARNING);
319 return null;
320 } else {
321 $keysize = intval($keysize[1]);
322 }
323 }
324
325 // $keysize has been made sure to contain an int
326 $blacklist = "/usr/share/openssl-blacklist/blacklist.RSA-$keysize";
327 if (!(is_file($blacklist) && is_readable($blacklist)))
328 {
329 if (in_array($keysize, array(512, 1024, 2048, 4096)))
330 {
331 trigger_error("checkDebianVulnerability(): Blacklist for ".
332 "$keysize bit keys not accessible. Expected at ".
333 "$blacklist", E_USER_ERROR);
334 return null;
335 }
336
337 trigger_error("checkDebianVulnerability(): $blacklist is not ".
338 "readable. Unsupported key size?", E_USER_WARNING);
339 return false;
340 }
341
342
343 /* Extract RSA modulus */
344 if (!preg_match('/^\s*Modulus \(\d+ bit\):\n'.
345 '((?:\s*[0-9a-f][0-9a-f]:(?:\n)?)+[0-9a-f][0-9a-f])$/m',
346 $text, $modulus))
347 {
348 trigger_error("checkDebianVulnerability(): Couldn't extract the ".
349 "RSA modulus.\nData:\n$text", E_USER_WARNING);
350 return null;
351 } else {
352 $modulus = $modulus[1];
353 // strip whitespace and colon leftovers
354 $modulus = str_replace(array(" ", "\t", "\n", ":"), "", $modulus);
355
356 // when using "openssl xxx -text" first byte was 00 in all my test
357 // cases but 00 not present in the "openssl xxx -modulus" output
358 if ($modulus[0] === "0" && $modulus[1] === "0")
359 {
360 $modulus = substr($modulus, 2);
361 } else {
362 trigger_error("checkDebianVulnerability(): First byte is not ".
363 "zero", E_USER_NOTICE);
364 }
365
366 $modulus = strtoupper($modulus);
367 }
368
369
370 /* calculate checksum and look it up in the blacklist */
371 $checksum = substr(sha1("Modulus=$modulus\n"), 20);
372
373 // $checksum and $blacklist should be safe, but just to make sure
374 $checksum = escapeshellarg($checksum);
375 $blacklist = escapeshellarg($blacklist);
376 $debianVuln = runCommand("grep $checksum $blacklist");
377 if ($debianVuln === 0) // grep returned something => it is on the list
378 {
379 return true;
380 } elseif ($debianVuln === 1) {
381 // grep returned nothing
382 return false;
383 } else {
384 trigger_error("checkDebianVulnerability(): Something went wrong ".
385 "when looking up the key with checksum $checksum in the ".
386 "blacklist $blacklist", E_USER_ERROR);
387 return null;
388 }
389
390 // Should not get here
391 return null;
392 }