Skip to content

Commit 091398c

Browse files
feat: decrypt jwe with keyset (#27)
* 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
1 parent 871c31f commit 091398c

File tree

7 files changed

+272
-111
lines changed

7 files changed

+272
-111
lines changed

composer.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
"autoload-dev": {
1818
"psr-4": {
1919
"MinVWS\\OpenIDConnectLaravel\\Tests\\": "tests"
20-
}
20+
},
21+
"files": [
22+
"tests/TestFunctions.php"
23+
]
2124
},
2225
"extra": {
2326
"laravel": {

config/oidc.php

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
/**
2222
* Only needed when response of user info endpoint is encrypted.
2323
* This is the path to the JWE decryption key.
24+
*
25+
* You could add multiple decryption key paths comma separated.
2426
*/
2527
'decryption_key_path' => env('OIDC_DECRYPTION_KEY_PATH', ''),
2628

src/OpenIDConnectServiceProvider.php

+27-8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Illuminate\Foundation\Application;
88
use Illuminate\Support\Facades\Route;
99
use Illuminate\Support\ServiceProvider;
10+
use Jose\Component\Core\JWKSet;
1011
use Jose\Component\KeyManagement\JWKFactory;
1112
use MinVWS\OpenIDConnectLaravel\Http\Responses\LoginResponseHandler;
1213
use MinVWS\OpenIDConnectLaravel\Http\Responses\LoginResponseHandlerInterface;
@@ -121,16 +122,13 @@ protected function registerClient(): void
121122

122123
protected function registerJweDecryptInterface(): void
123124
{
124-
if (empty(config('oidc.decryption_key_path'))) {
125-
$this->app->singleton(JweDecryptInterface::class, function () {
125+
$this->app->singleton(JweDecryptInterface::class, function () {
126+
$decryptionKeySet = $this->parseDecryptionKeySet();
127+
if ($decryptionKeySet === null) {
126128
return null;
127-
});
128-
return;
129-
}
129+
}
130130

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

@@ -142,4 +140,25 @@ protected function registerResponseHandler(): void
142140
{
143141
$this->app->bind(LoginResponseHandlerInterface::class, LoginResponseHandler::class);
144142
}
143+
144+
/**
145+
* Parse decryption keys from config
146+
* @return ?JWKSet
147+
*/
148+
protected function parseDecryptionKeySet(): ?JWKSet
149+
{
150+
$value = config('oidc.decryption_key_path');
151+
if (empty($value)) {
152+
return null;
153+
}
154+
155+
$keys = [];
156+
157+
$paths = explode(',', $value);
158+
foreach ($paths as $path) {
159+
$keys[] = JWKFactory::createFromKeyFile($path);
160+
}
161+
162+
return new JWKSet($keys);
163+
}
145164
}

src/Services/JWE/JweDecryptService.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
namespace MinVWS\OpenIDConnectLaravel\Services\JWE;
66

77
use Jose\Component\Core\AlgorithmManager;
8-
use Jose\Component\Core\JWK;
8+
use Jose\Component\Core\JWKSet;
99
use Jose\Component\Encryption\Algorithm\ContentEncryption\A128CBCHS256;
1010
use Jose\Component\Encryption\Algorithm\KeyEncryption\RSAOAEP;
1111
use Jose\Component\Encryption\Compression\CompressionMethodManager;
@@ -17,13 +17,13 @@
1717
class JweDecryptService implements JweDecryptInterface
1818
{
1919
/**
20-
* @param JWK $decryptionKey
20+
* @param JWKSet $decryptionKeySet
2121
* @param JWESerializerManager $serializerManager
2222
* @param JWEDecrypter $jweDecrypter
2323
* phpcs:disable Squiz.Functions.MultiLineFunctionDeclaration.Indent -- waiting for phpcs 3.8.0
2424
*/
2525
public function __construct(
26-
protected JWK $decryptionKey,
26+
protected JWKSet $decryptionKeySet,
2727
protected JWESerializerManager $serializerManager = new JWESerializerManager([new CompactSerializer()]),
2828
protected JWEDecrypter $jweDecrypter = new JWEDecrypter(
2929
new AlgorithmManager([new RSAOAEP()]),
@@ -42,7 +42,7 @@ public function decrypt(string $jweString): string
4242
$jwe = $this->serializerManager->unserialize($jweString);
4343

4444
// Success of decryption, $jwe is now decrypted
45-
$success = $this->jweDecrypter->decryptUsingKey($jwe, $this->decryptionKey, 0);
45+
$success = $this->jweDecrypter->decryptUsingKeySet($jwe, $this->decryptionKeySet, 0);
4646
if (!$success) {
4747
throw new JweDecryptException('Failed to decrypt JWE');
4848
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MinVWS\OpenIDConnectLaravel\Tests\Feature;
6+
7+
use Jose\Component\KeyManagement\JWKFactory;
8+
use MinVWS\OpenIDConnectLaravel\Services\JWE\JweDecryptInterface;
9+
use MinVWS\OpenIDConnectLaravel\Tests\TestCase;
10+
use OpenSSLCertificate;
11+
12+
use function MinVWS\OpenIDConnectLaravel\Tests\{
13+
generateOpenSSLKey,
14+
generateX509Certificate,
15+
buildJweString,
16+
buildExamplePayload
17+
};
18+
19+
class JweDecryptInterfaceBindingTest extends TestCase
20+
{
21+
/**
22+
* @var resource
23+
*/
24+
protected $decryptionKeyResource;
25+
protected OpenSSLCertificate $recipient;
26+
27+
28+
public function setUp(): void
29+
{
30+
[$key, $keyResource] = generateOpenSSLKey();
31+
$this->decryptionKeyResource = $keyResource;
32+
$this->recipient = generateX509Certificate($key);
33+
34+
parent::setUp();
35+
}
36+
37+
/**
38+
* @throws \JsonException
39+
*/
40+
public function testJweDecrypter(): void
41+
{
42+
$payload = buildExamplePayload();
43+
44+
$jwe = buildJweString(
45+
payload: $payload,
46+
recipient: JWKFactory::createFromX509Resource($this->recipient)
47+
);
48+
49+
$decrypter = $this->app->make(JweDecryptInterface::class);
50+
$decryptedData = $decrypter->decrypt($jwe);
51+
52+
$this->assertSame($payload, $decryptedData);
53+
}
54+
55+
56+
protected function getEnvironmentSetUp($app): void
57+
{
58+
$app['config']->set('oidc.decryption_key_path', stream_get_meta_data($this->decryptionKeyResource)['uri']);
59+
}
60+
}

tests/TestFunctions.php

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MinVWS\OpenIDConnectLaravel\Tests;
6+
7+
use Jose\Component\Core\AlgorithmManager;
8+
use Jose\Component\Core\JWK;
9+
use Jose\Component\Encryption\Algorithm\ContentEncryption\A128CBCHS256;
10+
use Jose\Component\Encryption\Algorithm\KeyEncryption\RSAOAEP;
11+
use Jose\Component\Encryption\Compression\CompressionMethodManager;
12+
use Jose\Component\Encryption\Compression\Deflate;
13+
use Jose\Component\Encryption\JWEBuilder;
14+
use Jose\Component\Encryption\Serializer\CompactSerializer;
15+
use Jose\Component\KeyManagement\JWKFactory;
16+
use JsonException;
17+
use OpenSSLAsymmetricKey;
18+
use OpenSSLCertificate;
19+
use OpenSSLCertificateSigningRequest;
20+
use RuntimeException;
21+
22+
function buildJweString(string $payload, JWK $recipient): string
23+
{
24+
// Create the JWE builder object
25+
$jweBuilder = new JWEBuilder(
26+
new AlgorithmManager([new RSAOAEP()]),
27+
new AlgorithmManager([new A128CBCHS256()]),
28+
new CompressionMethodManager([new Deflate()])
29+
);
30+
31+
// Build the JWE
32+
$jwe = $jweBuilder
33+
->create()
34+
->withPayload($payload)
35+
->withSharedProtectedHeader([
36+
'alg' => 'RSA-OAEP',
37+
'enc' => 'A128CBC-HS256',
38+
'zip' => 'DEF',
39+
])
40+
->addRecipient($recipient)
41+
->build();
42+
43+
// Get the compact serialization of the JWE
44+
return (new CompactSerializer())->serialize($jwe, 0);
45+
}
46+
47+
/**
48+
* @throws JsonException
49+
*/
50+
function buildExamplePayload(): string
51+
{
52+
return json_encode([
53+
'iat' => time(),
54+
'nbf' => time(),
55+
'exp' => time() + 3600,
56+
'iss' => 'My service',
57+
'aud' => 'Your application',
58+
], JSON_THROW_ON_ERROR);
59+
}
60+
61+
/**
62+
* Generate OpenSSL Key and return the tempfile resource
63+
* @return array{OpenSSLAsymmetricKey, resource}
64+
*/
65+
function generateOpenSSLKey(): array
66+
{
67+
$file = tmpfile();
68+
if (!is_resource($file)) {
69+
throw new RuntimeException('Could not create temporary file');
70+
}
71+
72+
$key = openssl_pkey_new([
73+
'private_key_bits' => 512,
74+
'private_key_type' => OPENSSL_KEYTYPE_RSA,
75+
]);
76+
if (!$key instanceof OpenSSLAsymmetricKey) {
77+
throw new RuntimeException('Could not generate private key');
78+
}
79+
80+
openssl_pkey_export($key, $privateKey);
81+
fwrite($file, $privateKey);
82+
83+
return [$key, $file];
84+
}
85+
86+
/**
87+
* Generate X509 certificate
88+
* @param OpenSSLAsymmetricKey $key
89+
* @return OpenSSLCertificate
90+
*/
91+
function generateX509Certificate(OpenSSLAsymmetricKey $key): OpenSSLCertificate
92+
{
93+
$csr = openssl_csr_new([], $key);
94+
if (!$csr instanceof OpenSSLCertificateSigningRequest) {
95+
throw new RuntimeException('Could not generate CSR');
96+
}
97+
98+
$certificate = openssl_csr_sign($csr, null, $key, 365);
99+
if (!$certificate instanceof OpenSSLCertificate) {
100+
throw new RuntimeException('Could not generate X509 certificate');
101+
}
102+
103+
return $certificate;
104+
}
105+
106+
/**
107+
* Get JWK from resource
108+
* @param $resource resource
109+
* @return JWK
110+
*/
111+
function getJwkFromResource($resource): JWK
112+
{
113+
if (!is_resource($resource)) {
114+
throw new RuntimeException('Could not create temporary file');
115+
}
116+
117+
return JWKFactory::createFromKeyFile(stream_get_meta_data($resource)['uri']);
118+
}

0 commit comments

Comments
 (0)