bug 978: Move things around (common functions moved to a lib file)
[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 // non-PEM-encodings may be binary so don't use echo
37 $descriptorspec = array(
38 0 => array("pipe", "r"), // STDIN for child
39 1 => array("pipe", "w"), // STDOUT for child
40 );
41 $encoding = escapeshellarg($encoding);
42 $proc = proc_open("openssl req -inform $encoding -text -noout",
43 $descriptorspec, $pipes);
44
45 if (is_resource($proc))
46 {
47 fwrite($pipes[0], $csr);
48 fclose($pipes[0]);
49
50 $csrText = "";
51 while (!feof($pipes[1]))
52 {
53 $csrText .= fread($pipes[1], 8192);
54 }
55 fclose($pipes[1]);
56
57 if (($status = proc_close($proc)) !== 0 || $csrText === "")
58 {
59 return _("I didn't receive a valid Certificate Request, hit ".
60 "the back button and try again.");
61 }
62 } else {
63 return failWithId("checkWeakKeyCSR(): Failed to start OpenSSL");
64 }
65
66
67 return checkWeakKeyText($csrText);
68 }
69
70 /**
71 * Checks whether the given X509 certificate contains a vulnerable key
72 *
73 * @param $cert string
74 * The X509 certificate to be checked
75 * @param $encoding string [optional]
76 * The encoding the certificate is in (for the "-inform" parameter of
77 * OpenSSL, currently only "PEM" (default), "DER" or "NET" allowed)
78 * @return string containing the reason if the key is considered weak,
79 * empty string otherwise
80 */
81 function checkWeakKeyX509($cert, $encoding = "PEM")
82 {
83 // non-PEM-encodings may be binary so don't use echo
84 $descriptorspec = array(
85 0 => array("pipe", "r"), // STDIN for child
86 1 => array("pipe", "w"), // STDOUT for child
87 );
88 $encoding = escapeshellarg($encoding);
89 $proc = proc_open("openssl x509 -inform $encoding -text -noout",
90 $descriptorspec, $pipes);
91
92 if (is_resource($proc))
93 {
94 fwrite($pipes[0], $cert);
95 fclose($pipes[0]);
96
97 $certText = "";
98 while (!feof($pipes[1]))
99 {
100 $certText .= fread($pipes[1], 8192);
101 }
102 fclose($pipes[1]);
103
104 if (($status = proc_close($proc)) !== 0 || $certText === "")
105 {
106 return _("I didn't receive a valid Certificate Request, hit ".
107 "the back button and try again.");
108 }
109 } else {
110 return failWithId("checkWeakKeyCSR(): Failed to start OpenSSL");
111 }
112
113
114 return checkWeakKeyText($certText);
115 }
116
117 /**
118 * Checks whether the given SPKAC contains a vulnerable key
119 *
120 * @param $spkac string
121 * The SPKAC to be checked
122 * @param $spkacname string [optional]
123 * The name of the variable that contains the SPKAC. The default is
124 * "SPKAC"
125 * @return string containing the reason if the key is considered weak,
126 * empty string otherwise
127 */
128 function checkWeakKeySPKAC($spkac, $spkacname = "SPKAC")
129 {
130 /* Check for the debian OpenSSL vulnerability */
131
132 $spkac = escapeshellarg($spkac);
133 $spkacname = escapeshellarg($spkacname);
134 $spkacText = `echo $spkac | openssl spkac -spkac $spkacname`;
135 if ($spkacText === null) {
136 return _("I didn't receive a valid Certificate Request, hit the ".
137 "back button and try again.");
138 }
139
140 return checkWeakKeyText($spkacText);
141 }
142
143 /**
144 * Checks whether the given text representation of a CSR or a SPKAC contains
145 * a weak key
146 *
147 * @param $text string
148 * The text representation of a key as output by the
149 * "openssl <foo> -text -noout" commands
150 * @return string containing the reason if the key is considered weak,
151 * empty string otherwise
152 */
153 function checkWeakKeyText($text)
154 {
155 /* Which public key algorithm? */
156 if (!preg_match('/^\s*Public Key Algorithm: ([^\s]+)$/m', $text,
157 $algorithm))
158 {
159 return failWithId("checkWeakKeyText(): Couldn't extract the ".
160 "public key algorithm used");
161 } else {
162 $algorithm = $algorithm[1];
163 }
164
165
166 if ($algorithm === "rsaEncryption")
167 {
168 if (!preg_match('/^\s*RSA Public Key: \((\d+) bit\)$/m', $text,
169 $keysize))
170 {
171 return failWithId("checkWeakKeyText(): Couldn't parse the RSA ".
172 "key size");
173 } else {
174 $keysize = intval($keysize[1]);
175 }
176
177 if ($keysize < 1024)
178 {
179 return sprintf(_("The keys that you use are very small ".
180 "and therefore insecure. Please generate stronger ".
181 "keys. More information about this issue can be ".
182 "found in %sthe wiki%s"),
183 "<a href='//wiki.cacert.org/WeakKeys#SmallKey'>",
184 "</a>");
185 } elseif ($keysize < 2048) {
186 // not critical but log so we have some statistics about
187 // affected users
188 trigger_error("checkWeakKeyText(): Certificate for small ".
189 "key (< 2048 bit) requested", E_USER_NOTICE);
190 }
191
192
193 $debianVuln = checkDebianVulnerability($text, $keysize);
194 if ($debianVuln === true)
195 {
196 return sprintf(_("The keys you use have very likely been ".
197 "generated with a vulnerable version of OpenSSL which ".
198 "was distributed by debian. Please generate new keys. ".
199 "More information about this issue can be found in ".
200 "%sthe wiki%s"),
201 "<a href='//wiki.cacert.org/WeakKeys#DebianVulnerability'>",
202 "</a>");
203 } elseif ($debianVuln === false) {
204 // not vulnerable => do nothing
205 } else {
206 return failWithId("checkWeakKeyText(): Something went wrong in".
207 "checkDebianVulnerability()");
208 }
209
210 if (!preg_match('/^\s*Exponent: (\d+) \(0x[0-9a-fA-F]+\)$/m', $text,
211 $exponent))
212 {
213 return failWithId("checkWeakKeyText(): Couldn't parse the RSA ".
214 "exponent");
215 } else {
216 $exponent = $exponent[1]; // exponent might be very big =>
217 //handle as string using bc*()
218
219 if (bccomp($exponent, "3") === 0)
220 {
221 return sprintf(_("The keys you use might be insecure. ".
222 "Although there is currently no known attack for ".
223 "reasonable encryption schemes, we're being ".
224 "cautious and don't allow certificates for such ".
225 "keys. Please generate stronger keys. More ".
226 "information about this issue can be found in ".
227 "%sthe wiki%s"),
228 "<a href='//wiki.cacert.org/WeakKeys#SmallExponent'>",
229 "</a>");
230 } elseif (!(bccomp($exponent, "65537") >= 0 &&
231 (bccomp($exponent, "100000") === -1 ||
232 // speed things up if way smaller than 2^256
233 bccomp($exponent, bcpow("2", "256")) === -1) )) {
234 // 65537 <= exponent < 2^256 recommended by NIST
235 // not critical but log so we have some statistics about
236 // affected users
237 trigger_error("checkWeakKeyText(): Certificate for ".
238 "unsuitable exponent '$exponent' requested",
239 E_USER_NOTICE);
240 }
241 }
242 }
243
244 /* No weakness found */
245 return "";
246 }
247
248 /**
249 * Reimplement the functionality of the openssl-vulnkey tool
250 *
251 * @param $text string
252 * The text representation of a key as output by the
253 * "openssl <foo> -text -noout" commands
254 * @param $keysize int [optional]
255 * If the key size is already known it can be provided so it doesn't
256 * have to be parsed again. This also skips the check whether the key
257 * is an RSA key => use wisely
258 * @return TRUE if key is vulnerable, FALSE otherwise, NULL in case of error
259 */
260 function checkDebianVulnerability($text, $keysize = 0)
261 {
262 $keysize = intval($keysize);
263
264 if ($keysize === 0)
265 {
266 /* Which public key algorithm? */
267 if (!preg_match('/^\s*Public Key Algorithm: ([^\s]+)$/m', $text,
268 $algorithm))
269 {
270 trigger_error("checkDebianVulnerability(): Couldn't extract ".
271 "the public key algorithm used", E_USER_WARNING);
272 return null;
273 } else {
274 $algorithm = $algorithm[1];
275 }
276
277 if ($algorithm !== "rsaEncryption") return false;
278
279 /* Extract public key size */
280 if (!preg_match('/^\s*RSA Public Key: \((\d+) bit\)$/m', $text,
281 $keysize))
282 {
283 trigger_error("checkDebianVulnerability(): Couldn't parse the ".
284 "RSA key size", E_USER_WARNING);
285 return null;
286 } else {
287 $keysize = intval($keysize[1]);
288 }
289 }
290
291 // $keysize has been made sure to contain an int
292 $blacklist = "/usr/share/openssl-blacklist/blacklist.RSA-$keysize";
293 if (!(is_file($blacklist) && is_readable($blacklist)))
294 {
295 if (in_array($keysize, array(512, 1024, 2048, 4096)))
296 {
297 trigger_error("checkDebianVulnerability(): Blacklist for ".
298 "$keysize bit keys not accessible. Expected at ".
299 "$blacklist", E_USER_ERROR);
300 return null;
301 }
302
303 trigger_error("checkDebianVulnerability(): $blacklist is not ".
304 "readable. Unsupported key size?", E_USER_WARNING);
305 return false;
306 }
307
308
309 /* Extract RSA modulus */
310 if (!preg_match('/^\s*Modulus \(\d+ bit\):\n'.
311 '((?:\s*[0-9a-f][0-9a-f]:(?:\n)?)+[0-9a-f][0-9a-f])$/m',
312 $text, $modulus))
313 {
314 trigger_error("checkDebianVulnerability(): Couldn't extract the ".
315 "RSA modulus", E_USER_WARNING);
316 return null;
317 } else {
318 $modulus = $modulus[1];
319 // strip whitespace and colon leftovers
320 $modulus = str_replace(array(" ", "\t", "\n", ":"), "", $modulus);
321
322 // when using "openssl xxx -text" first byte was 00 in all my test
323 // cases but 00 not present in the "openssl xxx -modulus" output
324 if ($modulus[0] === "0" && $modulus[1] === "0")
325 {
326 $modulus = substr($modulus, 2);
327 } else {
328 trigger_error("checkDebianVulnerability(): First byte is not ".
329 "zero", E_USER_NOTICE);
330 }
331
332 $modulus = strtoupper($modulus);
333 }
334
335
336 /* calculate checksum and look it up in the blacklist */
337 $checksum = substr(sha1("Modulus=$modulus\n"), 20);
338
339 // $checksum and $blacklist should be safe, but just to make sure
340 $checksum = escapeshellarg($checksum);
341 $blacklist = escapeshellarg($blacklist);
342 exec("grep $checksum $blacklist", $dummy, $debianVuln);
343 if ($debianVuln === 0) // grep returned something => it is on the list
344 {
345 return true;
346 } elseif ($debianVuln === 1) {
347 // grep returned nothing
348 return false;
349 } else {
350 trigger_error("checkDebianVulnerability(): Something went wrong ".
351 "when looking up the key with checksum $checksum in the ".
352 "blacklist $blacklist", E_USER_ERROR);
353 return null;
354 }
355
356 // Should not get here
357 return null;
358 }