Crypto - Define CIVICRM_SIGN_KEYS as way to register signing keys
authorTim Otten <totten@civicrm.org>
Mon, 15 Feb 2021 04:04:57 +0000 (20:04 -0800)
committerTim Otten <totten@civicrm.org>
Mon, 15 Feb 2021 04:33:49 +0000 (20:33 -0800)
CRM/Upgrade/Incremental/php/FiveThirtySix.php
Civi/Crypto/CryptoRegistry.php
setup/plugins/installFiles/GenerateSignKey.civi-setup.php [new file with mode: 0644]
setup/plugins/installFiles/InstallSettingsFile.civi-setup.php
setup/src/Setup/Model.php
templates/CRM/common/civicrm.settings.php.template

index e006058e6f598f623dcdf154f147cbdd81a84fe8..61494ada18f017a54fa97a78e028197de84a97fa 100644 (file)
@@ -29,6 +29,15 @@ class CRM_Upgrade_Incremental_php_FiveThirtySix extends CRM_Upgrade_Incremental_
     // if ($rev == '5.12.34') {
     //   $preUpgradeMessage .= '<p>' . ts('A new permission, "%1", has been added. This permission is now used to control access to the Manage Tags screen.', array(1 => ts('manage tags'))) . '</p>';
     // }
+    if ($rev === '5.36.alpha1') {
+      if (empty(CRM_Utils_Constant::value('CIVICRM_SIGN_KEYS'))) {
+        // NOTE: We don't re-encrypt automatically because the old "civicrm.settings.php" lacks a good key, and we don't keep the old encryption because the format is ambiguous.
+        // The admin may forget to re-enable. That's OK -- this only affects 1 field, this is a secondary defense, and (in the future) we can remind the admin via status-checks.
+        $preUpgradeMessage .= '<p>' . ts('CiviCRM v5.36 introduces a new configuration option to support digital signatures. You may <a href="%1" target="_blank">setup CIVICRM_SIGN_KEYS</a> before or after upgrading. The option is not critical in v5.36, but it may be required for extensions or future upgrades.', [
+          1 => 'https://docs.civicrm.org/sysadmin/en/latest/upgrade/version-specific/#sign-key',
+        ]) . '</p>';
+      }
+    }
   }
 
   /**
index a26bd4359c2860abf2c0f899d71c38ca89b61316..393e234b974881f183c713ff149b279aeb3107b1 100644 (file)
@@ -78,6 +78,13 @@ class CryptoRegistry {
       }
     }
 
+    if (defined('CIVICRM_SIGN_KEYS') && CIVICRM_SIGN_KEYS !== '') {
+      foreach (explode(' ', CIVICRM_SIGN_KEYS) as $n => $keyExpr) {
+        $key = ['tags' => ['SIGN'], 'weight' => $n];
+        $registry->addSymmetricKey($registry->parseKey($keyExpr) + $key);
+      }
+    }
+
     //if (isset($_COOKIE['CIVICRM_FORM_KEY'])) {
     //  $crypto->addSymmetricKey([
     //    'key' => base64_decode($_COOKIE['CIVICRM_FORM_KEY']),
diff --git a/setup/plugins/installFiles/GenerateSignKey.civi-setup.php b/setup/plugins/installFiles/GenerateSignKey.civi-setup.php
new file mode 100644 (file)
index 0000000..6847076
--- /dev/null
@@ -0,0 +1,31 @@
+<?php
+/**
+ * @file
+ *
+ * Generate the signing key(s).
+ */
+
+if (!defined('CIVI_SETUP')) {
+  exit("Installation plugins must only be loaded by the installer.\n");
+}
+
+\Civi\Setup::dispatcher()
+  ->addListener('civi.setup.installFiles', function (\Civi\Setup\Event\InstallFilesEvent $e) {
+    \Civi\Setup::log()->info(sprintf('[%s] Handle %s', basename(__FILE__), 'installFiles'));
+
+    $toAlphanum = function($bits) {
+      return preg_replace(';[^a-zA-Z0-9];', '', base64_encode($bits));
+    };
+
+  if (empty($e->getModel()->signKeys)) {
+    $e->getModel()->signKeys = ['jwt-hs256:hkdf-sha256:' . $toAlphanum(random_bytes(40))];
+    // toAlpanum() occasionally loses a few bits of entropy, but random_bytes() has significant excess, so it's still more than ample for 256 bit hkdf.
+  }
+
+  if (is_string($e->getModel()->signKeys)) {
+    $e->getModel()->signKeys = [$e->getModel()->signKeys];
+  }
+
+    \Civi\Setup::log()->info(sprintf('[%s] Done %s', basename(__FILE__), 'installFiles'));
+
+  }, \Civi\Setup::PRIORITY_PREPARE);
index 62c3c814190e085d32297b220333a88a450334cb..447889a873210f837e73b0df7d5292a7abe0371f 100644 (file)
@@ -99,6 +99,7 @@ if (!defined('CIVI_SETUP')) {
     $params['CMSdbSSL'] = empty($m->cmsDb['ssl_params']) ? '' : addslashes('&' . http_build_query($m->cmsDb['ssl_params'], '', '&', PHP_QUERY_RFC3986));
     $params['siteKey'] = addslashes($m->siteKey);
     $params['credKeys'] = addslashes(implode(' ', $m->credKeys));
+    $params['signKeys'] = addslashes(implode(' ', $m->signKeys));
 
     $extraSettings = array();
 
index 710c3dcbbf61fdbdbfa3300c3a90ec2b3e470c7a..3b740edc185e66c435e30cca88ebd16f015b4a99 100644 (file)
@@ -30,6 +30,8 @@ namespace Civi\Setup;
  *   Ex: 'abcd1234ABCD9876'.
  * @property string[] $credKeys
  *   Ex: ['::abcd1234ABCD9876'].
+ * @property string[] $signKeys
+ *   Ex: ['jwt-hs256::abcd1234ABCD9876'].
  * @property string|NULL $lang
  *   The language of the default dataset.
  *   Ex: 'fr_FR'.
@@ -116,6 +118,11 @@ class Model {
       'name' => 'credKeys',
       'type' => 'array',
     ));
+    $this->addField(array(
+      'description' => 'Signing keys',
+      'name' => 'signKeys',
+      'type' => 'array',
+    ));
     $this->addField(array(
       'description' => 'Load example data',
       'name' => 'loadGenerated',
index 1250b81ff066761ea531b1f578fa8c6b6d9185ca..29c97b69c557239a24c306bf07b7dca8a1922100 100644 (file)
@@ -324,6 +324,27 @@ if (!defined('CIVICRM_CRED_KEYS') ) {
   // Feel free to simplify post-install.
 }
 
+/**
+ * The signing key is used to generate and verify shareable tokens.
+ *
+ * This is a space-delimited list of keys (ordered by priority). Put the preferred
+ * key first. Any old/deprecated keys may be listed after.
+ *
+ * Each key is in format "<cipher-suite>:<key-encoding>:<key-content>", as in:
+ *
+ * Ex: define('CIVICRM_SIGN_KEYS', 'jwt-hs256:hkdf-sha256:RANDOM_1')
+ * Ex: define('CIVICRM_SIGN_KEYS', 'jwt-hs256::RANDOM_2 jwt-hs256::RANDOM_3')
+ * Ex: define('CIVICRM_SIGN_KEYS', 'jwt-hs256:b64:RANDOM_4 jwt-hs256:b64:RANDOM_5')
+ *
+ * If key-encoding is blank, it will default to "hkdf-sha256".
+ */
+if (!defined('CIVICRM_SIGN_KEYS') ) {
+  define( '_CIVICRM_SIGN_KEYS', '%%signKeys%%');
+  define( 'CIVICRM_SIGN_KEYS', _CIVICRM_SIGN_KEYS === '%%' . 'signKeys' . '%%' ? '' : _CIVICRM_SIGN_KEYS );
+  // Some old installers may not set a decent value, and this extra complexity is a failsafe.
+  // Feel free to simplify post-install.
+}
+
 /**
  * Enable this constant, if you want to send your email through the smarty
  * templating engine(allows you to do conditional and more complex logic)