Skip to content

Commit

Permalink
feat: decrypt jwe with keyset (#27)
Browse files Browse the repository at this point in the history
* feat: decrypt jwe with keyset

* chore: update tests to use jwkset

* chore: add test for decryption with multiple keys in key set

* chore: add test for JweDecryptInterface binding

* chore: added comma separated explanation for decryption_key_path
  • Loading branch information
ricklambrechts authored Oct 11, 2023
1 parent 871c31f commit 091398c
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 111 deletions.
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
"autoload-dev": {
"psr-4": {
"MinVWS\\OpenIDConnectLaravel\\Tests\\": "tests"
}
},
"files": [
"tests/TestFunctions.php"
]
},
"extra": {
"laravel": {
Expand Down
2 changes: 2 additions & 0 deletions config/oidc.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
/**
* Only needed when response of user info endpoint is encrypted.
* This is the path to the JWE decryption key.
*
* You could add multiple decryption key paths comma separated.
*/
'decryption_key_path' => env('OIDC_DECRYPTION_KEY_PATH', ''),

Expand Down
35 changes: 27 additions & 8 deletions src/OpenIDConnectServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Jose\Component\Core\JWKSet;
use Jose\Component\KeyManagement\JWKFactory;
use MinVWS\OpenIDConnectLaravel\Http\Responses\LoginResponseHandler;
use MinVWS\OpenIDConnectLaravel\Http\Responses\LoginResponseHandlerInterface;
Expand Down Expand Up @@ -121,16 +122,13 @@ protected function registerClient(): void

protected function registerJweDecryptInterface(): void
{
if (empty(config('oidc.decryption_key_path'))) {
$this->app->singleton(JweDecryptInterface::class, function () {
$this->app->singleton(JweDecryptInterface::class, function () {
$decryptionKeySet = $this->parseDecryptionKeySet();
if ($decryptionKeySet === null) {
return null;
});
return;
}
}

$this->app->singleton(JweDecryptInterface::class, function (Application $app) {
$jwk = JWKFactory::createFromKeyFile($app['config']->get('oidc.decryption_key_path'));
return new JweDecryptService(decryptionKey: $jwk);
return new JweDecryptService(decryptionKeySet: $decryptionKeySet);
});
}

Expand All @@ -142,4 +140,25 @@ protected function registerResponseHandler(): void
{
$this->app->bind(LoginResponseHandlerInterface::class, LoginResponseHandler::class);
}

/**
* Parse decryption keys from config
* @return ?JWKSet
*/
protected function parseDecryptionKeySet(): ?JWKSet
{
$value = config('oidc.decryption_key_path');
if (empty($value)) {
return null;
}

$keys = [];

$paths = explode(',', $value);
foreach ($paths as $path) {
$keys[] = JWKFactory::createFromKeyFile($path);
}

return new JWKSet($keys);
}
}
8 changes: 4 additions & 4 deletions src/Services/JWE/JweDecryptService.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace MinVWS\OpenIDConnectLaravel\Services\JWE;

use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Core\JWK;
use Jose\Component\Core\JWKSet;
use Jose\Component\Encryption\Algorithm\ContentEncryption\A128CBCHS256;
use Jose\Component\Encryption\Algorithm\KeyEncryption\RSAOAEP;
use Jose\Component\Encryption\Compression\CompressionMethodManager;
Expand All @@ -17,13 +17,13 @@
class JweDecryptService implements JweDecryptInterface
{
/**
* @param JWK $decryptionKey
* @param JWKSet $decryptionKeySet
* @param JWESerializerManager $serializerManager
* @param JWEDecrypter $jweDecrypter
* phpcs:disable Squiz.Functions.MultiLineFunctionDeclaration.Indent -- waiting for phpcs 3.8.0
*/
public function __construct(
protected JWK $decryptionKey,
protected JWKSet $decryptionKeySet,
protected JWESerializerManager $serializerManager = new JWESerializerManager([new CompactSerializer()]),
protected JWEDecrypter $jweDecrypter = new JWEDecrypter(
new AlgorithmManager([new RSAOAEP()]),
Expand All @@ -42,7 +42,7 @@ public function decrypt(string $jweString): string
$jwe = $this->serializerManager->unserialize($jweString);

// Success of decryption, $jwe is now decrypted
$success = $this->jweDecrypter->decryptUsingKey($jwe, $this->decryptionKey, 0);
$success = $this->jweDecrypter->decryptUsingKeySet($jwe, $this->decryptionKeySet, 0);
if (!$success) {
throw new JweDecryptException('Failed to decrypt JWE');
}
Expand Down
60 changes: 60 additions & 0 deletions tests/Feature/JweDecryptInterfaceBindingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace MinVWS\OpenIDConnectLaravel\Tests\Feature;

use Jose\Component\KeyManagement\JWKFactory;
use MinVWS\OpenIDConnectLaravel\Services\JWE\JweDecryptInterface;
use MinVWS\OpenIDConnectLaravel\Tests\TestCase;
use OpenSSLCertificate;

use function MinVWS\OpenIDConnectLaravel\Tests\{
generateOpenSSLKey,
generateX509Certificate,
buildJweString,
buildExamplePayload
};

class JweDecryptInterfaceBindingTest extends TestCase
{
/**
* @var resource
*/
protected $decryptionKeyResource;
protected OpenSSLCertificate $recipient;


public function setUp(): void
{
[$key, $keyResource] = generateOpenSSLKey();
$this->decryptionKeyResource = $keyResource;
$this->recipient = generateX509Certificate($key);

parent::setUp();
}

/**
* @throws \JsonException
*/
public function testJweDecrypter(): void
{
$payload = buildExamplePayload();

$jwe = buildJweString(
payload: $payload,
recipient: JWKFactory::createFromX509Resource($this->recipient)
);

$decrypter = $this->app->make(JweDecryptInterface::class);
$decryptedData = $decrypter->decrypt($jwe);

$this->assertSame($payload, $decryptedData);
}


protected function getEnvironmentSetUp($app): void
{
$app['config']->set('oidc.decryption_key_path', stream_get_meta_data($this->decryptionKeyResource)['uri']);
}
}
118 changes: 118 additions & 0 deletions tests/TestFunctions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

declare(strict_types=1);

namespace MinVWS\OpenIDConnectLaravel\Tests;

use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Core\JWK;
use Jose\Component\Encryption\Algorithm\ContentEncryption\A128CBCHS256;
use Jose\Component\Encryption\Algorithm\KeyEncryption\RSAOAEP;
use Jose\Component\Encryption\Compression\CompressionMethodManager;
use Jose\Component\Encryption\Compression\Deflate;
use Jose\Component\Encryption\JWEBuilder;
use Jose\Component\Encryption\Serializer\CompactSerializer;
use Jose\Component\KeyManagement\JWKFactory;
use JsonException;
use OpenSSLAsymmetricKey;
use OpenSSLCertificate;
use OpenSSLCertificateSigningRequest;
use RuntimeException;

function buildJweString(string $payload, JWK $recipient): string
{
// Create the JWE builder object
$jweBuilder = new JWEBuilder(
new AlgorithmManager([new RSAOAEP()]),
new AlgorithmManager([new A128CBCHS256()]),
new CompressionMethodManager([new Deflate()])
);

// Build the JWE
$jwe = $jweBuilder
->create()
->withPayload($payload)
->withSharedProtectedHeader([
'alg' => 'RSA-OAEP',
'enc' => 'A128CBC-HS256',
'zip' => 'DEF',
])
->addRecipient($recipient)
->build();

// Get the compact serialization of the JWE
return (new CompactSerializer())->serialize($jwe, 0);
}

/**
* @throws JsonException
*/
function buildExamplePayload(): string
{
return json_encode([
'iat' => time(),
'nbf' => time(),
'exp' => time() + 3600,
'iss' => 'My service',
'aud' => 'Your application',
], JSON_THROW_ON_ERROR);
}

/**
* Generate OpenSSL Key and return the tempfile resource
* @return array{OpenSSLAsymmetricKey, resource}
*/
function generateOpenSSLKey(): array
{
$file = tmpfile();
if (!is_resource($file)) {
throw new RuntimeException('Could not create temporary file');
}

$key = openssl_pkey_new([
'private_key_bits' => 512,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);
if (!$key instanceof OpenSSLAsymmetricKey) {
throw new RuntimeException('Could not generate private key');
}

openssl_pkey_export($key, $privateKey);
fwrite($file, $privateKey);

return [$key, $file];
}

/**
* Generate X509 certificate
* @param OpenSSLAsymmetricKey $key
* @return OpenSSLCertificate
*/
function generateX509Certificate(OpenSSLAsymmetricKey $key): OpenSSLCertificate
{
$csr = openssl_csr_new([], $key);
if (!$csr instanceof OpenSSLCertificateSigningRequest) {
throw new RuntimeException('Could not generate CSR');
}

$certificate = openssl_csr_sign($csr, null, $key, 365);
if (!$certificate instanceof OpenSSLCertificate) {
throw new RuntimeException('Could not generate X509 certificate');
}

return $certificate;
}

/**
* Get JWK from resource
* @param $resource resource
* @return JWK
*/
function getJwkFromResource($resource): JWK
{
if (!is_resource($resource)) {
throw new RuntimeException('Could not create temporary file');
}

return JWKFactory::createFromKeyFile(stream_get_meta_data($resource)['uri']);
}
Loading

0 comments on commit 091398c

Please sign in to comment.