Commit 4229457068d6a28cc11b506f127a7bb650ab18c1
1 parent
25687ddd
Security: use a secure random number generator
If not available, give an error. The user may also configure qpdf to use an insecure random number generator.
Showing
8 changed files
with
187 additions
and
20 deletions
ChangeLog
| 1 | 2013-10-05 Jay Berkenbilt <ejb@ql.org> | 1 | 2013-10-05 Jay Berkenbilt <ejb@ql.org> |
| 2 | 2 | ||
| 3 | + * Use cryptographically secure random number generation when | ||
| 4 | + available. See additional notes in README. | ||
| 5 | + | ||
| 3 | * Replace some assert() calls with std::logic_error exceptions. | 6 | * Replace some assert() calls with std::logic_error exceptions. |
| 4 | Ideally there shouldn't be assert() calls outside of testing. | 7 | Ideally there shouldn't be assert() calls outside of testing. |
| 5 | This change may make a few more potential code errors in handling | 8 | This change may make a few more potential code errors in handling |
README
| @@ -167,3 +167,22 @@ the test suite fails, test failure detail will be included in the | @@ -167,3 +167,22 @@ the test suite fails, test failure detail will be included in the | ||
| 167 | build output. Otherwise, you will have to have access to the | 167 | build output. Otherwise, you will have to have access to the |
| 168 | qtest.log file from the build to view test failures. The debian | 168 | qtest.log file from the build to view test failures. The debian |
| 169 | packages for qpdf enable this option, for example. | 169 | packages for qpdf enable this option, for example. |
| 170 | + | ||
| 171 | + | ||
| 172 | +Random Number Generation | ||
| 173 | +======================== | ||
| 174 | + | ||
| 175 | +When the qpdf detects either the Windows cryptography API or the | ||
| 176 | +existence of /dev/urandom, /dev/arandom, or /dev/random, it uses them | ||
| 177 | +to generate cryptography secure random numbers. If none of these | ||
| 178 | +conditions are true, the build will fail with an error. It is | ||
| 179 | +possible to configure qpdf with the --enable-insecure-random option, | ||
| 180 | +in which case it will generate random numbers with stdlib's random() | ||
| 181 | +or rand() calls instead. These random numbers are not cryptography | ||
| 182 | +secure, but the qpdf library is fully functional using them. Using | ||
| 183 | +non-secure random numbers means that it's easier in some cases to | ||
| 184 | +guess encryption keys. If you're not generating encrypted files, | ||
| 185 | +there's no advantage to using secure random numbers. | ||
| 186 | + | ||
| 187 | +If you are building qpdf on a platform that qpdf doesn't know how to | ||
| 188 | +generate secure random numbers on, a patch would be welcome. |
TODO
| @@ -76,12 +76,11 @@ General | @@ -76,12 +76,11 @@ General | ||
| 76 | and replace the /Pages key of the root dictionary with the new | 76 | and replace the /Pages key of the root dictionary with the new |
| 77 | tree. | 77 | tree. |
| 78 | 78 | ||
| 79 | - * Improve the random number seed to make it more secure so that we | ||
| 80 | - have stronger random numbers, particularly when multiple files are | ||
| 81 | - generated in the same second. This code may need to be | ||
| 82 | - OS-specific. Probably we should add a method in QUtil to seed with | ||
| 83 | - a strong random number and call this automatically the first time | ||
| 84 | - QUtil::random() is called. | 79 | + * Secure random number generation could be made more efficient by |
| 80 | + using a local static to ensure a single random device or crypt | ||
| 81 | + provider as long as this can be done in a thread-safe fashion. In | ||
| 82 | + the initial implementation, this is being skipped to avoid having | ||
| 83 | + to add any dependencies on threading libraries. | ||
| 85 | 84 | ||
| 86 | * Study what's required to support savable forms that can be saved by | 85 | * Study what's required to support savable forms that can be saved by |
| 87 | Adobe Reader. Does this require actually signing the document with | 86 | Adobe Reader. Does this require actually signing the document with |
configure.ac
| @@ -16,6 +16,23 @@ AC_PROG_CXX | @@ -16,6 +16,23 @@ AC_PROG_CXX | ||
| 16 | AC_HEADER_STDC | 16 | AC_HEADER_STDC |
| 17 | LT_INIT([win32-dll]) | 17 | LT_INIT([win32-dll]) |
| 18 | 18 | ||
| 19 | +AC_ARG_ENABLE(insecure-random, | ||
| 20 | + AS_HELP_STRING([--enable-insecure-random], | ||
| 21 | + [whether to use stdlib's random number generator (default is no)]), | ||
| 22 | + [if test "$enableval" = "yes"; then | ||
| 23 | + qpdf_INSECURE_RANDOM=1; | ||
| 24 | + else | ||
| 25 | + qpdf_INSECURE_RANDOM=0; | ||
| 26 | + fi], [qpdf_INSECURE_RANDOM=0]) | ||
| 27 | +if test "$qpdf_INSECURE_RANDOM" = "1"; then | ||
| 28 | + AC_MSG_RESULT(yes) | ||
| 29 | + AC_DEFINE([USE_INSECURE_RANDOM], [1], [Whether to use inscure random numbers]) | ||
| 30 | +else | ||
| 31 | + AC_MSG_RESULT(no) | ||
| 32 | +fi | ||
| 33 | + | ||
| 34 | +AX_RANDOM_DEVICE | ||
| 35 | + | ||
| 19 | USE_EXTERNAL_LIBS=0 | 36 | USE_EXTERNAL_LIBS=0 |
| 20 | AC_MSG_CHECKING(for whether to use external libraries distribution) | 37 | AC_MSG_CHECKING(for whether to use external libraries distribution) |
| 21 | AC_ARG_ENABLE(external-libs, | 38 | AC_ARG_ENABLE(external-libs, |
| @@ -54,6 +71,23 @@ if test "$BUILD_INTERNAL_LIBS" = "0"; then | @@ -54,6 +71,23 @@ if test "$BUILD_INTERNAL_LIBS" = "0"; then | ||
| 54 | AC_SEARCH_LIBS(pcre_compile,pcre,,[MISSING_PCRE=1; MISSING_ANY=1]) | 71 | AC_SEARCH_LIBS(pcre_compile,pcre,,[MISSING_PCRE=1; MISSING_ANY=1]) |
| 55 | fi | 72 | fi |
| 56 | 73 | ||
| 74 | +if test "x$qpdf_INSECURE_RANDOM" != "x1"; then | ||
| 75 | + OLIBS=$LIBS | ||
| 76 | + LIBS="$LIBS Advapi32.lib" | ||
| 77 | + AC_MSG_CHECKING(for Advapi32 library) | ||
| 78 | + AC_LINK_IFELSE([AC_LANG_PROGRAM( | ||
| 79 | + [[#pragma comment(lib, "crypt32.lib") | ||
| 80 | + #include <windows.h> | ||
| 81 | + #include <Wincrypt.h> | ||
| 82 | + HCRYPTPROV cp;]], | ||
| 83 | + [CryptAcquireContext(&cp, NULL, NULL, PROV_RSA_FULL, 0);] | ||
| 84 | + )], | ||
| 85 | + [AC_MSG_RESULT(yes) | ||
| 86 | + LIBS="$OLIBS -lAdvapi32"], | ||
| 87 | + [AC_MSG_RESULT(no) | ||
| 88 | + LIBS=$OLIBS]) | ||
| 89 | +fi | ||
| 90 | + | ||
| 57 | QPDF_LARGE_FILE_TEST_PATH= | 91 | QPDF_LARGE_FILE_TEST_PATH= |
| 58 | AC_SUBST(QPDF_LARGE_FILE_TEST_PATH) | 92 | AC_SUBST(QPDF_LARGE_FILE_TEST_PATH) |
| 59 | AC_ARG_WITH(large-file-test-path, | 93 | AC_ARG_WITH(large-file-test-path, |
copy_dlls
| @@ -20,7 +20,7 @@ while (<O>) | @@ -20,7 +20,7 @@ while (<O>) | ||
| 20 | { | 20 | { |
| 21 | my $dll = $1; | 21 | my $dll = $1; |
| 22 | $dll =~ tr/A-Z/a-z/; | 22 | $dll =~ tr/A-Z/a-z/; |
| 23 | - next if $dll =~ m/^(kernel32|user32|msvcrt)\.dll$/; | 23 | + next if $dll =~ m/^(kernel32|user32|msvcrt|advapi32)\.dll$/; |
| 24 | $dlls{$dll} = 1; | 24 | $dlls{$dll} = 1; |
| 25 | } | 25 | } |
| 26 | elsif (m/^Magic.*\((PE.+?)\)/) | 26 | elsif (m/^Magic.*\((PE.+?)\)/) |
include/qpdf/QUtil.hh
| @@ -108,12 +108,18 @@ namespace QUtil | @@ -108,12 +108,18 @@ namespace QUtil | ||
| 108 | QPDF_DLL | 108 | QPDF_DLL |
| 109 | std::string toUTF8(unsigned long uval); | 109 | std::string toUTF8(unsigned long uval); |
| 110 | 110 | ||
| 111 | - // Wrapper around random from stdlib. Calls srandom automatically | ||
| 112 | - // the first time it is called. | 111 | + // If secure random number generation is supported on your |
| 112 | + // platform and qpdf was not compiled with insecure random number | ||
| 113 | + // generation, this returns a crytographically secure random | ||
| 114 | + // number. Otherwise it falls back to random from stdlib and | ||
| 115 | + // calls srandom automatically the first time it is called. | ||
| 113 | QPDF_DLL | 116 | QPDF_DLL |
| 114 | long random(); | 117 | long random(); |
| 115 | 118 | ||
| 116 | - // Wrapper around srandom from stdlib. | 119 | + // Wrapper around srandom from stdlib. Seeds the standard library |
| 120 | + // weak random number generator, which is not used if secure | ||
| 121 | + // random number generation is being used. You never need to call | ||
| 122 | + // this method as it is called automatically if needed. | ||
| 117 | QPDF_DLL | 123 | QPDF_DLL |
| 118 | void srandom(unsigned int seed); | 124 | void srandom(unsigned int seed); |
| 119 | 125 |
libqpdf/QUtil.cc
| @@ -17,6 +17,7 @@ | @@ -17,6 +17,7 @@ | ||
| 17 | #include <Windows.h> | 17 | #include <Windows.h> |
| 18 | #include <direct.h> | 18 | #include <direct.h> |
| 19 | #include <io.h> | 19 | #include <io.h> |
| 20 | +#include <Wincrypt.h> | ||
| 20 | #else | 21 | #else |
| 21 | #include <unistd.h> | 22 | #include <unistd.h> |
| 22 | #endif | 23 | #endif |
| @@ -377,6 +378,8 @@ QUtil::toUTF8(unsigned long uval) | @@ -377,6 +378,8 @@ QUtil::toUTF8(unsigned long uval) | ||
| 377 | return result; | 378 | return result; |
| 378 | } | 379 | } |
| 379 | 380 | ||
| 381 | +#ifdef USE_INSECURE_RANDOM | ||
| 382 | + | ||
| 380 | long | 383 | long |
| 381 | QUtil::random() | 384 | QUtil::random() |
| 382 | { | 385 | { |
| @@ -390,28 +393,100 @@ QUtil::random() | @@ -390,28 +393,100 @@ QUtil::random() | ||
| 390 | seeded_random = true; | 393 | seeded_random = true; |
| 391 | } | 394 | } |
| 392 | 395 | ||
| 393 | -#ifdef HAVE_RANDOM | 396 | +# ifdef HAVE_RANDOM |
| 394 | return ::random(); | 397 | return ::random(); |
| 395 | -#else | 398 | +# else |
| 396 | return rand(); | 399 | return rand(); |
| 397 | -#endif | 400 | +# endif |
| 398 | } | 401 | } |
| 399 | 402 | ||
| 400 | void | 403 | void |
| 401 | -QUtil::srandom(unsigned int seed) | 404 | +QUtil::initializeWithRandomBytes(unsigned char* data, size_t len) |
| 402 | { | 405 | { |
| 403 | -#ifdef HAVE_RANDOM | ||
| 404 | - ::srandom(seed); | 406 | + for (size_t i = 0; i < len; ++i) |
| 407 | + { | ||
| 408 | + data[i] = static_cast<unsigned char>((QUtil::random() & 0xff0) >> 4); | ||
| 409 | + } | ||
| 410 | +} | ||
| 411 | + | ||
| 405 | #else | 412 | #else |
| 406 | - srand(seed); | ||
| 407 | -#endif | 413 | + |
| 414 | +long | ||
| 415 | +QUtil::random() | ||
| 416 | +{ | ||
| 417 | + long result = 0L; | ||
| 418 | + initializeWithRandomBytes( | ||
| 419 | + reinterpret_cast<unsigned char*>(&result), | ||
| 420 | + sizeof(result)); | ||
| 421 | + return result; | ||
| 408 | } | 422 | } |
| 409 | 423 | ||
| 424 | +#ifdef _WIN32 | ||
| 425 | +class WindowsCryptProvider | ||
| 426 | +{ | ||
| 427 | + public: | ||
| 428 | + WindowsCryptProvider() | ||
| 429 | + { | ||
| 430 | + if (! CryptAcquireContext(&crypt_prov, NULL, NULL, PROV_RSA_FULL, 0)) | ||
| 431 | + { | ||
| 432 | + throw std::runtime_error("unable to acquire crypt context"); | ||
| 433 | + } | ||
| 434 | + } | ||
| 435 | + ~WindowsCryptProvider() | ||
| 436 | + { | ||
| 437 | + // Ignore error | ||
| 438 | + CryptReleaseContext(crypt_prov, 0); | ||
| 439 | + } | ||
| 440 | + | ||
| 441 | + HCRYPTPROV crypt_prov; | ||
| 442 | +}; | ||
| 443 | +#endif | ||
| 444 | + | ||
| 410 | void | 445 | void |
| 411 | QUtil::initializeWithRandomBytes(unsigned char* data, size_t len) | 446 | QUtil::initializeWithRandomBytes(unsigned char* data, size_t len) |
| 412 | { | 447 | { |
| 413 | - for (size_t i = 0; i < len; ++i) | 448 | +#if defined(_WIN32) |
| 449 | + | ||
| 450 | + // Optimization: make the WindowsCryptProvider static as long as | ||
| 451 | + // it can be done in a thread-safe fashion. | ||
| 452 | + WindowsCryptProvider c; | ||
| 453 | + if (! CryptGenRandom(c.crypt_prov, len, reinterpret_cast<BYTE*>(data))) | ||
| 414 | { | 454 | { |
| 415 | - data[i] = static_cast<unsigned char>((QUtil::random() & 0xff0) >> 4); | 455 | + throw std::runtime_error("unable to generate secure random data"); |
| 456 | + } | ||
| 457 | + | ||
| 458 | +#elif defined(RANDOM_DEVICE) | ||
| 459 | + | ||
| 460 | + // Optimization: wrap the file open and close in a class so that | ||
| 461 | + // the file is closed in a destructor, then make this static to | ||
| 462 | + // keep the file handle open. Only do this if it can be done in a | ||
| 463 | + // thread-safe fashion. | ||
| 464 | + FILE* f = QUtil::safe_fopen(RANDOM_DEVICE, "rb"); | ||
| 465 | + size_t fr = fread(data, 1, len, f); | ||
| 466 | + fclose(f); | ||
| 467 | + if (fr != len) | ||
| 468 | + { | ||
| 469 | + throw std::runtime_error( | ||
| 470 | + "unable to read " + | ||
| 471 | + QUtil::int_to_string(len) + | ||
| 472 | + " bytes from " + std::string(RANDOM_DEVICE)); | ||
| 416 | } | 473 | } |
| 474 | + | ||
| 475 | +#else | ||
| 476 | + | ||
| 477 | +# error "Don't know how to generate secure random numbers on this platform. See random number generation in the top-level README" | ||
| 478 | + | ||
| 479 | +#endif | ||
| 480 | +} | ||
| 481 | + | ||
| 482 | +#endif | ||
| 483 | + | ||
| 484 | +void | ||
| 485 | +QUtil::srandom(unsigned int seed) | ||
| 486 | +{ | ||
| 487 | +#ifdef HAVE_RANDOM | ||
| 488 | + ::srandom(seed); | ||
| 489 | +#else | ||
| 490 | + srand(seed); | ||
| 491 | +#endif | ||
| 417 | } | 492 | } |
m4/ax_random_device.m4
0 → 100644
| 1 | +dnl @synopsis AX_RANDOM_DEVICE | ||
| 2 | +dnl | ||
| 3 | +dnl This macro will check for a random device, allowing the user to explicitly | ||
| 4 | +dnl set the path. The user uses '--with-random=FILE' as an argument to | ||
| 5 | +dnl configure. | ||
| 6 | +dnl | ||
| 7 | +dnl If A random device is found then HAVE_RANDOM_DEVICE is set to 1 and | ||
| 8 | +dnl RANDOM_DEVICE contains the path. | ||
| 9 | +dnl | ||
| 10 | +dnl @category Miscellaneous | ||
| 11 | +dnl @author Martin Ebourne | ||
| 12 | +dnl @version 2005/07/01 | ||
| 13 | +dnl @license AllPermissive | ||
| 14 | + | ||
| 15 | +AC_DEFUN([AX_RANDOM_DEVICE], [ | ||
| 16 | + AC_ARG_WITH([random], | ||
| 17 | + [AC_HELP_STRING([--with-random=FILE], [Use FILE as random number seed [auto-detected]])], | ||
| 18 | + [RANDOM_DEVICE="$withval"], | ||
| 19 | + [AC_CHECK_FILE("/dev/urandom", [RANDOM_DEVICE="/dev/urandom";], | ||
| 20 | + [AC_CHECK_FILE("/dev/arandom", [RANDOM_DEVICE="/dev/arandom";], | ||
| 21 | + [AC_CHECK_FILE("/dev/random", [RANDOM_DEVICE="/dev/random";])] | ||
| 22 | + )]) | ||
| 23 | + ]) | ||
| 24 | + if test "x$RANDOM_DEVICE" != "x" ; then | ||
| 25 | + AC_DEFINE([HAVE_RANDOM_DEVICE], 1, | ||
| 26 | + [Define to 1 (and set RANDOM_DEVICE) if a random device is available]) | ||
| 27 | + AC_SUBST([RANDOM_DEVICE]) | ||
| 28 | + AC_DEFINE_UNQUOTED([RANDOM_DEVICE], ["$RANDOM_DEVICE"], | ||
| 29 | + [Define to the filename of the random device (and set HAVE_RANDOM_DEVICE)]) | ||
| 30 | + fi | ||
| 31 | +])dnl |