From b907a5b9ecd868efdbdbee17f2c0dc2a549bba8d Mon Sep 17 00:00:00 2001 From: Luciano Righetti Date: Tue, 10 Oct 2023 10:54:23 +0200 Subject: [PATCH] add: configure gnupg, add gnupg tests --- .gitignore | 3 +- docker-compose.yml | 2 +- docker/.env.dev.dist | 11 +- docker/.env.dist | 23 +- docker/.env.test | 11 +- docker/misp/Dockerfile | 2 +- docker/misp/config/app_local.php | 10 + docker/misp/entrypoint.sh | 60 ++++- src/Lib/Tools/CryptGpgExtended.php | 5 +- src/Lib/Tools/TmpFileTool.php | 258 +++++++++++++++++++ tests/TestCase/Tool/CryptGpgExtendedTest.php | 70 +++++ 11 files changed, 427 insertions(+), 28 deletions(-) create mode 100644 src/Lib/Tools/TmpFileTool.php create mode 100644 tests/TestCase/Tool/CryptGpgExtendedTest.php diff --git a/.gitignore b/.gitignore index d164466af..fc2589b3b 100755 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ docker/run/ config.json phpunit.xml docker-compose.override.yml -.gnupg \ No newline at end of file +.gnupg +*.asc \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 52ca328fa..55caeadf1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: ADMIN_ORG: ${ADMIN_ORG} ADMIN_EMAIL: ${ADMIN_EMAIL} ADMIN_INITIAL_PASSWORD: ${ADMIN_INITIAL_PASSWORD} - ADMIN_USER_API_KEY: ${ADMIN_USER_API_KEY} + ADMIN_API_KEY: ${ADMIN_API_KEY} GPG_PASSPHRASE: ${GPG_PASSPHRASE} DISABLE_BACKGROUND_WORKERS: ${DISABLE_BACKGROUND_WORKERS:-0} NUM_WORKERS_DEFAULT: ${NUM_WORKERS_DEFAULT:-5} diff --git a/docker/.env.dev.dist b/docker/.env.dev.dist index d7b5089bf..a8e64f3f6 100644 --- a/docker/.env.dev.dist +++ b/docker/.env.dev.dist @@ -6,18 +6,19 @@ MISP_COMMIT= MODULES_TAG= MODULES_COMMIT= -MYSQL_ROOT_PASSWORD=root -MYSQL_DATABASE=misp3 -MYSQL_USER=misp -MYSQL_PASSWORD=misp ADMIN_ORG= ADMIN_ORG_UUID= ADMIN_EMAIL= ADMIN_INITIAL_PASSWORD= -ADMIN_USER_API_KEY= +ADMIN_API_KEY= GPG_PASSPHRASE= +MYSQL_ROOT_PASSWORD=root +MYSQL_DATABASE=misp3 +MYSQL_USER=misp +MYSQL_PASSWORD=misp + EMAIL_HOST=mailhog EMAIL_PORT=1025 EMAIL_USERNAME= diff --git a/docker/.env.dist b/docker/.env.dist index 31f1148e6..1f116cba5 100644 --- a/docker/.env.dist +++ b/docker/.env.dist @@ -1,23 +1,32 @@ +# General settings ENV=prod DEBUG=0 DOCKER_HUB_PROXY= + +# MISP version MISP_TAG= MISP_COMMIT= MODULES_TAG= MODULES_COMMIT= +# MISP settings +ADMIN_ORG= +ADMIN_ORG_UUID= +ADMIN_EMAIL= +ADMIN_INITIAL_PASSWORD= +ADMIN_API_KEY= +GPG_PASSPHRASE= +# MISP.email, used for notifications. Also used +# for GnuPG.email and GPG autogeneration. +# MISP_EMAIL= + +# MySQL settings MYSQL_ROOT_PASSWORD= MYSQL_DATABASE=misp3 MYSQL_USER=misp MYSQL_PASSWORD= -ADMIN_ORG= -ADMIN_ORG_UUID= -ADMIN_EMAIL= -ADMIN_INITIAL_PASSWORD= -ADMIN_USER_API_KEY= -GPG_PASSPHRASE= - +# Email and SMTP settings EMAIL_HOST= EMAIL_PORT= EMAIL_USERNAME= diff --git a/docker/.env.test b/docker/.env.test index 1cf02d6e5..70f33744e 100644 --- a/docker/.env.test +++ b/docker/.env.test @@ -6,18 +6,19 @@ MISP_COMMIT= MODULES_TAG= MODULES_COMMIT= -MYSQL_ROOT_PASSWORD=root -MYSQL_DATABASE=misp3_test -MYSQL_USER=misp -MYSQL_PASSWORD=misp ADMIN_ORG=ORGNAME ADMIN_ORG_UUID=a1f2be0f-73a4-4b4a-9a87-26a8ae0511fb ADMIN_EMAIL=admin@admin.test ADMIN_INITIAL_PASSWORD=admin -ADMIN_USER_API_KEY=5E8sFPZa9K6ge1q1INmgkaeVu1mhv6cEg2Hsmx2Y +ADMIN_API_KEY=5E8sFPZa9K6ge1q1INmgkaeVu1mhv6cEg2Hsmx2Y GPG_PASSPHRASE=foobar +MYSQL_ROOT_PASSWORD=root +MYSQL_DATABASE=misp3_test +MYSQL_USER=misp +MYSQL_PASSWORD=misp + EMAIL_HOST=mailhog EMAIL_PORT=1025 EMAIL_USERNAME= diff --git a/docker/misp/Dockerfile b/docker/misp/Dockerfile index c4ea56a1c..5066b9865 100644 --- a/docker/misp/Dockerfile +++ b/docker/misp/Dockerfile @@ -79,7 +79,7 @@ RUN pecl install -f xdebug pcov \ # Install additional packages RUN apt-get update \ && apt-get install -y \ - git sendmail \ + git sendmail sudo \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/docker/misp/config/app_local.php b/docker/misp/config/app_local.php index 163c22056..338c8267e 100644 --- a/docker/misp/config/app_local.php +++ b/docker/misp/config/app_local.php @@ -61,5 +61,15 @@ return [ 'supervisor_port' => '9001', 'supervisor_user' => 'supervisor', 'supervisor_password' => 'supervisor', + ], + 'GnuPG' => [ + 'onlyencrypted' => false, + 'email' => env('MISP_EMAIL', env('ADMIN_EMAIL')), + 'homedir' => env('GPG_DIR', '/var/www/.gnupg'), + 'password' => env('GPG_PASSPHRASE', 'passphrase'), + 'bodyonlyencrypted' => false, + 'sign' => true, + 'obscure_subject' => false, + 'binary' => '/usr/bin/gpg' ] ]; diff --git a/docker/misp/entrypoint.sh b/docker/misp/entrypoint.sh index c1c42b621..6dcd6faf1 100644 --- a/docker/misp/entrypoint.sh +++ b/docker/misp/entrypoint.sh @@ -13,6 +13,11 @@ rm -f "${MISP_READY_STATUS_FLAG}" [ -z "$MISP_DB" ] && MISP_DB=misp3 [ -z "$MYSQL_PWD" ] && MYSQL_PWD=$MISP_DB_PASSWORD [ -z "$MYSQLCMD" ] && MYSQLCMD="mysql --defaults-file=/etc/mysql/conf.d/misp.cnf -P $MYSQL_PORT -h $MYSQL_HOST -r -N $MISP_DB" +[ -z "$GPG_PASSPHRASE" ] && GPG_PASSPHRASE="passphrase" +[ -z "$GPG_DIR" ] && GPG_DIR="/var/www/.gnupg" + +# Switches to selectively disable configuration logic +[ -z "$AUTOCONF_GPG" ] && AUTOCONF_GPG="true" # create mysql default config cat </etc/mysql/conf.d/misp.cnf @@ -70,12 +75,55 @@ init_user() { ADMIN_USER_ID=$(echo "SELECT id FROM users WHERE EMAIL='${ADMIN_EMAIL}';" | ${MYSQLCMD} | tr -d '\n') # Insert Admin user API key - if [ -z "$ADMIN_USER_API_KEY" ]; then + if [ -z "$ADMIN_API_KEY" ]; then echo >&2 "Creating admin user API key..." - ADMIN_USER_API_KEY_START=$(echo ${ADMIN_USER_API_KEY} | head -c 4) - ADMIN_USER_API_KEY_END=$(echo ${ADMIN_USER_API_KEY} | tail -c 5) - export ADMIN_USER_API_KEY_HASH=$(php -r "echo password_hash('${ADMIN_USER_API_KEY}', PASSWORD_DEFAULT);" | tr -d \') - echo "INSERT INTO auth_keys (uuid, authkey, authkey_start, authkey_end, created, expiration, user_id) VALUES ((SELECT uuid()), '${ADMIN_USER_API_KEY_HASH}', '${ADMIN_USER_API_KEY_START}', '${ADMIN_USER_API_KEY_END}', 0, 0, ${ADMIN_USER_ID});" | ${MYSQLCMD} + ADMIN_API_KEY_START=$(echo ${ADMIN_API_KEY} | head -c 4) + ADMIN_API_KEY_END=$(echo ${ADMIN_API_KEY} | tail -c 5) + export ADMIN_API_KEY_HASH=$(php -r "echo password_hash('${ADMIN_API_KEY}', PASSWORD_DEFAULT);" | tr -d \') + echo "INSERT INTO auth_keys (uuid, authkey, authkey_start, authkey_end, created, expiration, user_id) VALUES ((SELECT uuid()), '${ADMIN_API_KEY_HASH}', '${ADMIN_API_KEY_START}', '${ADMIN_API_KEY_END}', 0, 0, ${ADMIN_USER_ID});" | ${MYSQLCMD} + fi +} + +configure_gnupg() { + if [ "$AUTOCONF_GPG" != "true" ]; then + echo "... GPG auto configuration disabled" + return + fi + + GPG_DIR=/var/www/.gnupg + GPG_ASC=/var/www/html/webroot/gpg.asc + GPG_TMP=/tmp/gpg.tmp + + if [ ! -f "${GPG_DIR}/trustdb.gpg" ]; then + echo "... generating new GPG key in ${GPG_DIR}" + cat >${GPG_TMP} <${GPG_ASC} + else + echo "... found exported key ${GPG_ASC}" fi } @@ -91,6 +139,8 @@ done init_user +configure_gnupg + # Test php-fpm config php-fpm -t diff --git a/src/Lib/Tools/CryptGpgExtended.php b/src/Lib/Tools/CryptGpgExtended.php index 266f6f7e3..a244eec18 100644 --- a/src/Lib/Tools/CryptGpgExtended.php +++ b/src/Lib/Tools/CryptGpgExtended.php @@ -2,8 +2,7 @@ namespace App\Lib\Tools; -use Cake\Core\Exception\Exception; -use Cake\Core\Configure; +use Exception; class CryptGpgExtended extends \Crypt_GPG { @@ -64,7 +63,7 @@ class CryptGpgExtended extends \Crypt_GPG $operation = '--export'; $operation .= ' ' . escapeshellarg($fingerprint); - $arguments = array('--export-options', 'export-minimal'); + $arguments = ['--export-options', 'export-minimal']; if ($armor) { $arguments[] = '--armor'; } diff --git a/src/Lib/Tools/TmpFileTool.php b/src/Lib/Tools/TmpFileTool.php new file mode 100644 index 000000000..33a1f9da2 --- /dev/null +++ b/src/Lib/Tools/TmpFileTool.php @@ -0,0 +1,258 @@ +tmpfile = fopen("php://temp/maxmemory:$maxInMemory", "w+"); + if ($this->tmpfile === false) { + throw new Exception('Could not create temporary file.'); + } + } + + /** + * Write data to stream with separator. Separator will be prepend to content for next call. + * @param string|Generator $content + * @param string $separator + * @throws Exception + */ + public function writeWithSeparator($content, $separator) + { + if (isset($this->separator)) { + if ($content instanceof Generator) { + $this->write($this->separator); + foreach ($content as $part) { + $this->write($part); + } + } else { + $this->write($this->separator . $content); + } + } else { + if ($content instanceof Generator) { + foreach ($content as $part) { + $this->write($part); + } + } else { + $this->write($content); + } + } + $this->separator = $separator; + } + + /** + * @param string $content + * @throws Exception + */ + public function write($content) + { + if (fwrite($this->tmpfile, $content) === false) { + if ($this->tmpfile === null) { + throw new Exception('Could not write to finished temporary file.'); + } + $tmpFolder = sys_get_temp_dir(); + $freeSpace = disk_free_space($tmpFolder); + throw new Exception("Could not write to temporary file in $tmpFolder folder. Maybe not enough space? ($freeSpace bytes left)"); + } + } + + /** + * @param string $path + * @throws Exception + */ + public function writeFromFile($path) + { + $file = fopen($path, 'r'); + if (!$file) { + throw new Exception("Could not open file $file."); + } + if (stream_copy_to_stream($file, $this->tmpfile) === false) { + throw new Exception("Could not copy content of file $file into TmpFile."); + } + } + + /** + * Returns generator of parsed CSV line from file. + * + * @param string $delimiter + * @param string $enclosure + * @param string $escape + * @return Generator + * @throws Exception + */ + public function intoParsedCsv($delimiter = ',', $enclosure = '"', $escape = "\\") + { + $this->rewind(); + $line = 0; + while (!feof($this->tmpfile)) { + $result = fgetcsv($this->tmpfile, 0, $delimiter, $enclosure, $escape); + if ($result === false) { + throw new Exception("Could not read line $line from temporary CSV file."); + } + $line++; + yield $result; + } + $this->close(); + } + + /** + * Returns generator of line from file. + * + * @return Generator + * @throws Exception + */ + public function intoLines() + { + $this->rewind(); + while (!feof($this->tmpfile)) { + $result = fgets($this->tmpfile); + if ($result === false) { + throw new Exception('Could not read line from temporary file.'); + } + yield $result; + } + $this->close(); + } + + /** + * @param int $chunkSize In bytes + * @return Generator + * @throws Exception + */ + public function intoChunks($chunkSize = 8192) + { + $this->rewind(); + while (!feof($this->tmpfile)) { + $result = fread($this->tmpfile, $chunkSize); + if ($result === false) { + throw new Exception('Could not read from temporary file.'); + } + yield $result; + } + $this->close(); + } + + /** + * @return string + * @throws Exception + */ + public function intoString() + { + $this->rewind(); + $string = stream_get_contents($this->tmpfile); + if ($string === false) { + throw new Exception('Could not read from temporary file.'); + } + $this->close(); + return $string; + } + + /** + * Pass data to output. + * + * @throws Exception + */ + public function intoOutput() + { + $this->rewind(); + if (fpassthru($this->tmpfile) === false) { + throw new Exception('Could not pass temporary file to output.'); + } + $this->close(); + } + + /** + * @return resource + * @throws Exception + */ + public function resource() + { + $this->rewind(); + return $this->tmpfile; + } + + /** + * @return int + * @throws Exception + */ + public function size() + { + $this->isOpen(); + return fstat($this->tmpfile)['size']; + } + + /** + * @param string $algo + * @return string + * @throws Exception + */ + public function hash($algo) + { + $this->rewind(); + $hash = hash_init($algo); + hash_update_stream($hash, $this->tmpfile); + return hash_final($hash); + } + + /** + * @return string + * @throws Exception + */ + public function __toString() + { + return $this->intoString(); + } + + /** + * @return bool + */ + public function close() + { + if ($this->tmpfile) { + $result = fclose($this->tmpfile); + $this->tmpfile = null; + return $result; + } + return true; + } + + /** + * @throws Exception + */ + private function isOpen() + { + if ($this->tmpfile === null) { + throw new Exception('Temporary file is already closed.'); + } + } + + /** + * Seek to start of file. + * + * @throws Exception + */ + private function rewind() + { + $this->isOpen(); + if (fseek($this->tmpfile, 0) === -1) { + throw new Exception('Could not seek to start of temporary file.'); + } + } +} diff --git a/tests/TestCase/Tool/CryptGpgExtendedTest.php b/tests/TestCase/Tool/CryptGpgExtendedTest.php new file mode 100644 index 000000000..7d04cd635 --- /dev/null +++ b/tests/TestCase/Tool/CryptGpgExtendedTest.php @@ -0,0 +1,70 @@ +init(); + $this->assertInstanceOf('App\Lib\Tools\CryptGpgExtended', $gpg); + $this->assertIsString($gpg->getVersion()); + } + + public function testSignAndVerify() + { + $gpg = $this->init(); + $config = Configure::read('GnuPG'); + + $gpg->addSignKey($config['email'], $config['password']); + + $testString = 'ahojSvete'; + + $signature = $gpg->sign($testString, \Crypt_GPG::SIGN_MODE_DETACHED, \Crypt_GPG::ARMOR_BINARY); + $this->assertIsString($signature); + + $verified = $gpg->verify($testString, $signature); + $this->assertIsArray($verified); + $this->assertCount(1, $verified); + $this->assertTrue($verified[0]->isValid()); + + $signature = $gpg->sign($testString, \Crypt_GPG::SIGN_MODE_DETACHED, \Crypt_GPG::ARMOR_ASCII); + $this->assertIsString($signature); + + $verified = $gpg->verify($testString, $signature); + $this->assertIsArray($verified); + $this->assertCount(1, $verified); + $this->assertTrue($verified[0]->isValid()); + + // Tmp file + $tmpFile = new TmpFileTool(); + $tmpFile->write($testString); + $signature = $gpg->signFile($tmpFile, null, \Crypt_GPG::SIGN_MODE_DETACHED, \Crypt_GPG::ARMOR_BINARY); + $this->assertIsString($signature); + + $verified = $gpg->verify($testString, $signature); + $this->assertIsArray($verified); + $this->assertCount(1, $verified); + $this->assertTrue($verified[0]->isValid()); + } + + private function init(): CryptGpgExtended + { + $config = Configure::read('GnuPG'); + + $options = [ + 'homedir' => $config['homedir'], + 'gpgconf' => $config['gpgconf'] ?? null, + 'binary' => $config['binary'] ?? '/usr/bin/gpg', + ]; + + return new CryptGpgExtended($options); + } +}