Skip to content

Commit

Permalink
Added support for Encryption Key Rotation
Browse files Browse the repository at this point in the history
  • Loading branch information
rhertogh committed May 30, 2022
1 parent ed169e0 commit 30cc2f5
Show file tree
Hide file tree
Showing 26 changed files with 646 additions and 40 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@ jobs:
# These variables are provided for testing use only. NEVER use them for other purposes!
YII2_OAUTH2_SERVER_PRIVATE_KEY_PASSPHRASE: secret
YII2_OAUTH2_SERVER_CODES_ENCRYPTION_KEY: def00000761b6fce5b2c1721c37602e82effb785154c3bb0db93bfb3f413012bd85d46461e28f156a3a5afab910a64d5b2665276d45f24b1085d90e12ab3d38ee47b4337
YII2_OAUTH2_SERVER_STORAGE_ENCRYPTION_KEY: def00000cb36fd6ed6641e0ad70805b28da86192765eb73daae7306acc537ca5e9678db80e92dbfcb489debbac0ed96139e6ff210fc0281078e99c1420d2d18e2c7388ac
YII2_OAUTH2_SERVER_STORAGE_ENCRYPTION_KEYS: >
{
"2021-01-01": "def00000cb36fd6ed6641e0ad70805b28da86192765eb73daae7306acc537ca5e9678db80e92dbfcb489debbac0ed96139e6ff210fc0281078e99c1420d2d18e2c7388ac",
"2022-01-01": "def00000c8fc3b1b8d017afc6a645f94e6d2f5fc9d71e8b3eb26e5b2de6ef23232dd19446bbeef26fbd51dd2fd4cd5641e68db28ec76f8460bb3f33aaab3cff7b9fcfe62"
}
MYSQL_HOST: localhost
MYSQL_PORT: ${{ job.services.mysql.ports[3306] }}
MYSQL_DB_NAME: Yii2Oauth2ServerTest
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Yii2-Oauth2-Server Change Log
--------------------------------

- Enh: Added support for Client Secret Rotation (rhertogh)
- Enh: Added support for Encryption Key Rotation (rhertogh)

1.0.0-alpha2 (2022-05-27)
-------------------------
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ services:
# Oauth2 Server
- YII2_OAUTH2_SERVER_PRIVATE_KEY_PASSPHRASE=${YII2_OAUTH2_SERVER_PRIVATE_KEY_PASSPHRASE:?err}
- YII2_OAUTH2_SERVER_CODES_ENCRYPTION_KEY=${YII2_OAUTH2_SERVER_CODES_ENCRYPTION_KEY:?err}
- YII2_OAUTH2_SERVER_STORAGE_ENCRYPTION_KEY=${YII2_OAUTH2_SERVER_STORAGE_ENCRYPTION_KEY:?err}
- YII2_OAUTH2_SERVER_STORAGE_ENCRYPTION_KEYS=${YII2_OAUTH2_SERVER_STORAGE_ENCRYPTION_KEYS:?err}
# Database
- MYSQL_HOST=${MYSQL_HOST:?err}
- MYSQL_DB_NAME=${MYSQL_DB_NAME:?err}
Expand Down
5 changes: 5 additions & 0 deletions docs/guide/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@ Security
--------
* [Introduction](security-introduction.md)
* [Client Secret Rotation](security-client-secret-rotation.md)
* [Encryption Key Rotation](security-encryption-key-rotation.md)

Development
-----------
* [Development Guide](../internals/README.md)

FAQ
--------
* [Frequently Asked Questions](faq.md)

Appendix
--------
* [Client Configuration](appendix-client-configuration.md)
19 changes: 16 additions & 3 deletions docs/guide/faq-errors.md → docs/guide/faq.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
F.A.Q - Error Messages
======================
Frequently Asked Questions
==========================

This F.A.Q. describes common error messages and provides possible solutions for them.
This FAQ describes common questions and errors and provides possible solutions for them.

Error Messages
--------------

### Error: Unable to read key from file -----BEGIN RSA PRIVATE KEY----- ...
This error could appear if the private key is set as string containing a private key with
Expand All @@ -22,3 +25,13 @@ location ~ /\.(?!well-known).* {
}
```
This will deny access to all files and folders starting with `.` except `.well-known`.

Encryption
----------

### Help, I lost my encryption key!
Well, you're on your own. Until quantum computers get powerful enough there is no one that can help you.
And even quantum computers might not be able to crack symmetric encryption[^1].

[^1]: [Will Symmetric and Asymmetric Encryption Withstand the Might of Quantum Computing?](
https://www.toolbox.com/it-security/encryption/articles/will-symmetric-and-asymmetric-encryption-withstand-the-might-of-quantum-computing)
59 changes: 59 additions & 0 deletions docs/guide/security-encryption-key-rotation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
Encryption Key Rotation
=======================

Similar to changing passwords, regularly rotating the server storage encryption keys is a security best
practice. When rotating the encryption keys a seamless transition to the new key is crucial to avoid downtime.

Configuring Multiple Keys
-------------------------

In order to support key rotation the Yii2Oauth2Server supports the configuration of multiple encryption keys.
These can be set in `Oauth2Module::$storageEncryptionKeys`.

> Warning: Keys must remain in `Oauth2Module::$storageEncryptionKeys` as long there is data encrypted with them.
Removing or overwriting a key will result in data loss!
You can check which keys are in use by running `yii oauth2/encryption/key-usage`.
To find the usage for a specific key you can run `yii oauth2/encryption/key-usage --key-name=2022-01-01`.

> Note: New data will be encrypted with the key specified in the `Oauth2Module::$defaultStorageEncryptionKey`.
Exiting data will not be changed until it's actively rotated to the new key (see below).

```php
return [
'modules' => [
'oauth2' => [
'class' => rhertogh\Yii2Oauth2Server\Oauth2Module::class,
// ...
'storageEncryptionKeys' => [ // For ease of use this can also be a JSON encoded string.
// The index represents the name of the key, this can be anything you like.
// However, for keeping track of different keys using (or prefixing it with) a date is advisable.
'2021-01-01' => getenv('MY_OLD_STORAGE_ENCRYPTION_KEY'), // Original Encryption Key
'2022-01-01' => getenv('MY_NEW_STORAGE_ENCRYPTION_KEY'), // New Encryption Key
],
'defaultStorageEncryptionKey' => '2022-01-01', // Using the new key as default
// ...
],
// ...
],
];
```

> Tip: For ease of use `storageEncryptionKeys` can also be a JSON encoded string. This way all different keys can be dynamically loaded
from a single environment variable.

Rotating to a new Key
---------------------

The storage encryption keys can be rotated in the following ways:

* Manually via the console via the `yii oauth2/encryption/rotate-keys` command.
By default the `Oauth2Module::$defaultStorageEncryptionKey` will be used, to use another key you can use the
'key-name' argument, e.g. `yii oauth2/encryption/rotate-keys --key-name=2022-01-01`

* Programmatically by calling `\rhertogh\Yii2Oauth2Server\Oauth2Module::rotateStorageEncryptionKeys()`
optionally specifying the `$newKeyName` parameter (if not specified the
`Oauth2Module::$defaultStorageEncryptionKey` will be used).

After the encryption keys have been rotated to the new key ensure the old key(s) are no longer used by running
`yii oauth2/encryption/key-usage`.
When the old keys are no longer used they can be safely removed from the `Oauth2Module::$storageEncryptionKeys`.
1 change: 1 addition & 0 deletions docs/guide/security-introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ the same applies for (client) secrets.

The Yii2Oauth2Server supports seamless rotation of encryption keys and client secrets. For more information please see:
* [Client Secret Rotation](security-client-secret-rotation.md)
* [Encryption Key Rotation](security-encryption-key-rotation.md)
2 changes: 1 addition & 1 deletion docs/guide/start-installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ Configuration
'publicKey' => 'file:///path/to/public.key', // Path to the public key generated in step 1.
'privateKeyPassphrase' => getenv('YII2_OAUTH2_SERVER_PRIVATE_KEY_PASSPHRASE'), // The private key passphrase (if used in step 1).
'codesEncryptionKey' => getenv('YII2_OAUTH2_SERVER_CODES_ENCRYPTION_KEY'), // The encryption key generated in step 2.
'storageEncryptionKeys' => [
'storageEncryptionKeys' => [ // For ease of use this can also be a JSON encoded string.
// The index represents the name of the key, this can be anything you like.
// However, for keeping track of different keys using (or prefixing it with) a date is advisable.
'2021-01-01' => getenv('YII2_OAUTH2_SERVER_STORAGE_ENCRYPTION_KEY'), // The encryption key generated in step 2.
Expand Down
6 changes: 2 additions & 4 deletions sample/config/main.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,8 @@
'publicKey' => '@app/config/keys/public.key', // Path to the public key.
'privateKeyPassphrase' => getenv('YII2_OAUTH2_SERVER_PRIVATE_KEY_PASSPHRASE'), // The private key passphrase (if used).
'codesEncryptionKey' => getenv('YII2_OAUTH2_SERVER_CODES_ENCRYPTION_KEY'), // The encryption key for authorization and refresh codes.
'storageEncryptionKeys' => [
'2021-01-01' => getenv('YII2_OAUTH2_SERVER_STORAGE_ENCRYPTION_KEY'), // The encryption key for storage like client secrets.
],
'defaultStorageEncryptionKey' => '2021-01-01', // The index of the default key in storageEncryptionKeys.
'storageEncryptionKeys' => getenv('YII2_OAUTH2_SERVER_STORAGE_ENCRYPTION_KEYS'), // The encryption key for storage like client secrets.
'defaultStorageEncryptionKey' => '2022-01-01', // The index of the default key in storageEncryptionKeys.
'grantTypes' => [ // For more information which grant types to use, please see https://oauth2.thephpleague.com/authorization-server/which-grant/.
Oauth2Module::GRANT_TYPE_AUTH_CODE,
Oauth2Module::GRANT_TYPE_CLIENT_CREDENTIALS,
Expand Down
69 changes: 66 additions & 3 deletions src/Oauth2Module.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2ServerControllerInterface;
use rhertogh\Yii2Oauth2Server\interfaces\controllers\web\Oauth2WellKnownControllerInterface;
use rhertogh\Yii2Oauth2Server\interfaces\filters\auth\Oauth2HttpBearerAuthInterface;
use rhertogh\Yii2Oauth2Server\interfaces\models\base\Oauth2EncryptedStorageInterface;
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ClientInterface;
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2ClientScopeInterface;
use rhertogh\Yii2Oauth2Server\interfaces\models\Oauth2OidcUserInterface;
Expand Down Expand Up @@ -114,6 +115,16 @@ class Oauth2Module extends Oauth2BaseModule implements BootstrapInterface
'privateKey',
'publicKey',
];

/**
* Encrypted Models
*
* @since 1.0.0
*/
protected const ENCRYPTED_MODELS = [
Oauth2ClientInterface::class,
];

/**
* Required settings when the server role includes Resource Server
* @since 1.0.0
Expand Down Expand Up @@ -164,6 +175,10 @@ class Oauth2Module extends Oauth2BaseModule implements BootstrapInterface
'controller' => Oauth2ClientController::class,
'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
],
'encryption' => [
'controller' => Oauth2EncryptionController::class,
'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER,
],
'debug' => [
'controller' => Oauth2DebugController::class,
'serverRole' => self::SERVER_ROLE_AUTHORIZATION_SERVER | self::SERVER_ROLE_RESOURCE_SERVER,
Expand Down Expand Up @@ -217,9 +232,13 @@ class Oauth2Module extends Oauth2BaseModule implements BootstrapInterface
public $codesEncryptionKey = null;

/**
* @var string[]|null The encryption keys for storage like client secrets.
* @var string[]|string|null The encryption keys for storage like client secrets.
* Where the array key is the name of the key, and the value the key itself. E.g.
* `['myKey' => 'def00000cb36fd6ed6641e0ad70805b28d....']`
* `['2022-01-01' => 'def00000cb36fd6ed6641e0ad70805b28d....']`
* If a string (instead of an array of strings) is specified it will be JSON decoded
* it should contain an object where each property name is the name of the key, its value the key itself. E.g.
* `{"2022-01-01": "def00000cb36fd6ed6641e0ad70805b28d...."}`
*
* @since 1.0.0
*/
public $storageEncryptionKeys = null;
Expand Down Expand Up @@ -648,7 +667,7 @@ public function getAuthorizationServer()
if (!$this->_authorizationServer) {
$this->ensureProperties(static::REQUIRED_SETTINGS_AUTHORIZATION_SERVER);

if (empty($this->storageEncryptionKeys[$this->defaultStorageEncryptionKey])) {
if (!$this->getEncryptor()->hasKey($this->defaultStorageEncryptionKey)) {
throw new InvalidConfigException(
'Key "' . $this->defaultStorageEncryptionKey . '" is not set in $storageEncryptionKeys'
);
Expand Down Expand Up @@ -823,6 +842,50 @@ public function getEncryptor()
return $this->_encryptor;
}

/**
* @param string|null $newKeyName
* @return array
* @throws InvalidConfigException
*/
public function rotateStorageEncryptionKeys($newKeyName = null)
{
$encryptor = $this->getEncryptor();

$result = [];
foreach (static::ENCRYPTED_MODELS as $modelInterface) {
$modelClass = DiHelper::getValidatedClassName($modelInterface);
if (!is_a($modelClass, Oauth2EncryptedStorageInterface::class, true)) {
throw new InvalidConfigException($modelInterface . ' must implement '
. Oauth2EncryptedStorageInterface::class);
}
$result[$modelClass] = $modelClass::rotateStorageEncryptionKeys($encryptor, $newKeyName);
}

return $result;
}

/**
* @return array
* @throws InvalidConfigException
*/
public function getStorageEncryptionKeyUsage()
{
$encryptor = $this->getEncryptor();

$result = [];
foreach (static::ENCRYPTED_MODELS as $modelInterface) {
$modelClass = DiHelper::getValidatedClassName($modelInterface);
if (!is_a($modelClass, Oauth2EncryptedStorageInterface::class, true)) {
throw new InvalidConfigException($modelInterface . ' must implement '
. Oauth2EncryptedStorageInterface::class);
}

$result[$modelClass] = $modelClass::getUsedStorageEncryptionKeys($encryptor);
}

return $result;
}

/**
* Generates a redirect Response to the client authorization page where the user is prompted to authorize the
* client and requested scope.
Expand Down
29 changes: 28 additions & 1 deletion src/components/encryption/Oauth2Encryptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
use rhertogh\Yii2Oauth2Server\interfaces\components\factories\encryption\Oauth2EncryptionKeyFactoryInterface;
use Yii;
use yii\base\Component;
use yii\base\InvalidArgumentException;
use yii\base\InvalidConfigException;
use yii\helpers\Json;

class Oauth2Encryptor extends Component implements Oauth2EncryptorInterface
{
Expand All @@ -37,6 +39,10 @@ class Oauth2Encryptor extends Component implements Oauth2EncryptorInterface
*/
public function setKeys($keys)
{
if ($keys && is_string($keys)) {
$keys = Json::decode($keys);
}

/** @var Oauth2EncryptionKeyFactoryInterface $keyFactory */
$keyFactory = Yii::createObject(Oauth2EncryptionKeyFactoryInterface::class);
$this->_keys = [];
Expand Down Expand Up @@ -81,6 +87,15 @@ public function setDefaultKeyName($name)
$this->_defaultKeyName = $name;
}

/**
* @inheritDoc
*/
public function hasKey($name)
{
return array_key_exists($name, $this->_keys);
}


/**
* @inheritDoc
* @throws InvalidConfigException
Expand Down Expand Up @@ -111,6 +126,18 @@ public function encryp($data, $keyName = null)
. base64_encode(Crypto::encrypt($data, $this->_keys[$keyName], true));
}

/**
* @inheritDoc
*/
public function parseData($data)
{
$parts = explode($this->dataSeparator, $data);
if (count($parts) !== 2) {
throw new InvalidArgumentException('Could not parse encrypted data: invalid number of parts, expected 2 got ' . count($parts));
}
return array_combine(['keyName', 'ciphertext'], $parts);
}

/**
* @inheritDoc
* @throws EnvironmentIsBrokenException
Expand All @@ -119,7 +146,7 @@ public function encryp($data, $keyName = null)
public function decrypt($data)
{
try {
list($keyName, $ciphertext) = explode($this->dataSeparator, $data, 2);
list('keyName' => $keyName, 'ciphertext' => $ciphertext) = $this->parseData($data);
} catch (\Throwable $e) {
throw new \InvalidArgumentException(
'Unable to decrypt, $data must be in format "keyName' . $this->dataSeparator . 'ciphertext".'
Expand Down
47 changes: 47 additions & 0 deletions src/controllers/console/Oauth2EncryptionController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace rhertogh\Yii2Oauth2Server\controllers\console;

use rhertogh\Yii2Oauth2Server\controllers\console\base\Oauth2BaseConsoleController;
use rhertogh\Yii2Oauth2Server\controllers\console\encryption\Oauth2EncryptionKeyUsageAction;
use rhertogh\Yii2Oauth2Server\controllers\console\encryption\Oauth2RotateEncryptionKeysAction;
use yii\helpers\ArrayHelper;

class Oauth2EncryptionController extends Oauth2BaseConsoleController
{
/**
* @var string|null
*/
public $keyName = null;

/**
* @inheritDoc
*/
public function options($actionID)
{
if (in_array($actionID, ['key-usage', 'rotate-keys'])) {
$options = [
'keyName',
];
}
return ArrayHelper::merge(parent::options($actionID), $options ?? []);
}

public function optionAliases()
{
return ArrayHelper::merge(parent::optionAliases(), [
'k' => 'keyName',
]);
}

/**
* @inheritDoc
*/
public function actions()
{
return [
'key-usage' => Oauth2EncryptionKeyUsageAction::class,
'rotate-keys' => Oauth2RotateEncryptionKeysAction::class,
];
}
}
Loading

0 comments on commit 30cc2f5

Please sign in to comment.