|
| 1 | +<?php |
| 2 | + |
| 3 | +declare(strict_types=1); |
| 4 | + |
| 5 | +namespace MinVWS\OpenIDConnectLaravel\Tests\Unit\Services\JWE; |
| 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\JWE; |
| 14 | +use Jose\Component\Encryption\JWEBuilder; |
| 15 | +use Jose\Component\Encryption\JWEDecrypter; |
| 16 | +use Jose\Component\Encryption\Serializer\CompactSerializer; |
| 17 | +use Jose\Component\Encryption\Serializer\JWESerializerManager; |
| 18 | +use Jose\Component\KeyManagement\JWKFactory; |
| 19 | +use JsonException; |
| 20 | +use MinVWS\OpenIDConnectLaravel\Services\JWE\JweDecryptException; |
| 21 | +use MinVWS\OpenIDConnectLaravel\Services\JWE\JweDecryptService; |
| 22 | +use Mockery; |
| 23 | +use OpenSSLAsymmetricKey; |
| 24 | +use OpenSSLCertificate; |
| 25 | +use OpenSSLCertificateSigningRequest; |
| 26 | +use PHPUnit\Framework\TestCase; |
| 27 | +use RuntimeException; |
| 28 | + |
| 29 | +class JweDecryptServiceTest extends TestCase |
| 30 | +{ |
| 31 | + /** |
| 32 | + * @var resource |
| 33 | + */ |
| 34 | + protected $decryptionKeyResource; |
| 35 | + protected JWK $decryptionKey; |
| 36 | + protected OpenSSLCertificate $x509Certificate; |
| 37 | + |
| 38 | + protected function setUp(): void |
| 39 | + { |
| 40 | + parent::setUp(); |
| 41 | + |
| 42 | + [$key, $keyResource] = $this->generateOpenSSLKey(); |
| 43 | + $this->decryptionKeyResource = $keyResource; |
| 44 | + $this->decryptionKey = JWKFactory::createFromKeyFile(stream_get_meta_data($keyResource)['uri']); |
| 45 | + $this->x509Certificate = $this->generateX509Certificate($key); |
| 46 | + } |
| 47 | + |
| 48 | + protected function tearDown(): void |
| 49 | + { |
| 50 | + if (is_resource($this->decryptionKeyResource)) { |
| 51 | + fclose($this->decryptionKeyResource); |
| 52 | + } |
| 53 | + |
| 54 | + parent::tearDown(); |
| 55 | + } |
| 56 | + |
| 57 | + public function testServiceCanBeCreated(): void |
| 58 | + { |
| 59 | + $jweDecryptService = new JweDecryptService($this->decryptionKey); |
| 60 | + |
| 61 | + $this->assertInstanceOf(JweDecryptService::class, $jweDecryptService); |
| 62 | + } |
| 63 | + |
| 64 | + /** |
| 65 | + * @throws JweDecryptException |
| 66 | + * @throws JsonException |
| 67 | + */ |
| 68 | + public function testJweDecryption(): void |
| 69 | + { |
| 70 | + $payload = $this->buildExamplePayload(); |
| 71 | + |
| 72 | + $jwe = $this->buildJweString( |
| 73 | + payload: $payload, |
| 74 | + recipient: JWKFactory::createFromX509Resource($this->x509Certificate) |
| 75 | + ); |
| 76 | + |
| 77 | + $jweDecryptService = new JweDecryptService($this->decryptionKey); |
| 78 | + $decryptedPayload = $jweDecryptService->decrypt($jwe); |
| 79 | + |
| 80 | + $this->assertEquals($payload, $decryptedPayload); |
| 81 | + } |
| 82 | + |
| 83 | + /** |
| 84 | + * @throws JweDecryptException |
| 85 | + * @throws JsonException |
| 86 | + */ |
| 87 | + public function testJweDecryptionThrowsExceptionWhenKeyIsNotCorrect(): void |
| 88 | + { |
| 89 | + $this->expectException(JweDecryptException::class); |
| 90 | + $this->expectExceptionMessage('Failed to decrypt JWE'); |
| 91 | + |
| 92 | + // Create different key |
| 93 | + [$key, $keyResource] = $this->generateOpenSSLKey(); |
| 94 | + $jwk = JWKFactory::createFromKeyFile(stream_get_meta_data($keyResource)['uri']); |
| 95 | + |
| 96 | + // Build JWE for default certificate |
| 97 | + $payload = $this->buildExamplePayload(); |
| 98 | + $jwe = $this->buildJweString( |
| 99 | + payload: $payload, |
| 100 | + recipient: JWKFactory::createFromX509Resource($this->x509Certificate) |
| 101 | + ); |
| 102 | + |
| 103 | + // Try to decrypt with different key |
| 104 | + $jweDecryptService = new JweDecryptService($jwk); |
| 105 | + $jweDecryptService->decrypt($jwe); |
| 106 | + } |
| 107 | + |
| 108 | + /** |
| 109 | + * @throws JweDecryptException |
| 110 | + * @throws JsonException |
| 111 | + */ |
| 112 | + public function testJweDecryptionThrowsExceptionWhenPayloadIsNull(): void |
| 113 | + { |
| 114 | + $this->expectException(JweDecryptException::class); |
| 115 | + $this->expectExceptionMessage('Payload of JWE is null'); |
| 116 | + |
| 117 | + $jweMock = Mockery::mock(JWE::class); |
| 118 | + $jweMock |
| 119 | + ->shouldReceive('getPayload') |
| 120 | + ->andReturn(null); |
| 121 | + |
| 122 | + $decryptionKey = Mockery::mock(JWK::class); |
| 123 | + $serializerManager = Mockery::mock(JWESerializerManager::class); |
| 124 | + $serializerManager |
| 125 | + ->shouldReceive('unserialize') |
| 126 | + ->with('something') |
| 127 | + ->andReturn($jweMock); |
| 128 | + |
| 129 | + $jweDecrypter = Mockery::mock(JWEDecrypter::class); |
| 130 | + $jweDecrypter |
| 131 | + ->shouldReceive('decryptUsingKey') |
| 132 | + ->andReturn(true); |
| 133 | + |
| 134 | + $decryptService = new JweDecryptService( |
| 135 | + $decryptionKey, |
| 136 | + $serializerManager, |
| 137 | + $jweDecrypter, |
| 138 | + ); |
| 139 | + |
| 140 | + $decryptService->decrypt('something'); |
| 141 | + } |
| 142 | + |
| 143 | + protected function buildJweString(string $payload, JWK $recipient): string |
| 144 | + { |
| 145 | + // Create the JWE builder object |
| 146 | + $jweBuilder = new JWEBuilder( |
| 147 | + new AlgorithmManager([new RSAOAEP()]), |
| 148 | + new AlgorithmManager([new A128CBCHS256()]), |
| 149 | + new CompressionMethodManager([new Deflate()]) |
| 150 | + ); |
| 151 | + |
| 152 | + // Build the JWE |
| 153 | + $jwe = $jweBuilder |
| 154 | + ->create() |
| 155 | + ->withPayload($payload) |
| 156 | + ->withSharedProtectedHeader([ |
| 157 | + 'alg' => 'RSA-OAEP', |
| 158 | + 'enc' => 'A128CBC-HS256', |
| 159 | + 'zip' => 'DEF', |
| 160 | + ]) |
| 161 | + ->addRecipient($recipient) |
| 162 | + ->build(); |
| 163 | + |
| 164 | + // Get the compact serialization of the JWE |
| 165 | + return (new CompactSerializer())->serialize($jwe, 0); |
| 166 | + } |
| 167 | + |
| 168 | + /** |
| 169 | + * @throws JsonException |
| 170 | + */ |
| 171 | + protected function buildExamplePayload(): string |
| 172 | + { |
| 173 | + return json_encode([ |
| 174 | + 'iat' => time(), |
| 175 | + 'nbf' => time(), |
| 176 | + 'exp' => time() + 3600, |
| 177 | + 'iss' => 'My service', |
| 178 | + 'aud' => 'Your application', |
| 179 | + ], JSON_THROW_ON_ERROR); |
| 180 | + } |
| 181 | + |
| 182 | + /** |
| 183 | + * Generate OpenSSL Key and return the tempfile resource |
| 184 | + * @return array{OpenSSLAsymmetricKey, resource} |
| 185 | + */ |
| 186 | + protected function generateOpenSSLKey(): array |
| 187 | + { |
| 188 | + $file = tmpfile(); |
| 189 | + if (!is_resource($file)) { |
| 190 | + throw new RuntimeException('Could not create temporary file'); |
| 191 | + } |
| 192 | + |
| 193 | + $key = openssl_pkey_new([ |
| 194 | + 'private_key_bits' => 512, |
| 195 | + 'private_key_type' => OPENSSL_KEYTYPE_RSA, |
| 196 | + ]); |
| 197 | + if (!$key instanceof OpenSSLAsymmetricKey) { |
| 198 | + throw new RuntimeException('Could not generate private key'); |
| 199 | + } |
| 200 | + |
| 201 | + openssl_pkey_export($key, $privateKey); |
| 202 | + fwrite($file, $privateKey); |
| 203 | + |
| 204 | + return [$key, $file]; |
| 205 | + } |
| 206 | + |
| 207 | + /** |
| 208 | + * Generate X509 certificate |
| 209 | + * @param OpenSSLAsymmetricKey $key |
| 210 | + * @return OpenSSLCertificate |
| 211 | + */ |
| 212 | + protected function generateX509Certificate(OpenSSLAsymmetricKey $key): OpenSSLCertificate |
| 213 | + { |
| 214 | + $csr = openssl_csr_new([], $key); |
| 215 | + if (!$csr instanceof OpenSSLCertificateSigningRequest) { |
| 216 | + throw new RuntimeException('Could not generate CSR'); |
| 217 | + } |
| 218 | + |
| 219 | + $certificate = openssl_csr_sign($csr, null, $key, 365); |
| 220 | + if (!$certificate instanceof OpenSSLCertificate) { |
| 221 | + throw new RuntimeException('Could not generate X509 certificate'); |
| 222 | + } |
| 223 | + |
| 224 | + return $certificate; |
| 225 | + } |
| 226 | +} |
0 commit comments