Skip to content

Commit 3725069

Browse files
committed
Add permitted algorithms to validation context
1 parent 6b69a8c commit 3725069

File tree

5 files changed

+199
-2
lines changed

5 files changed

+199
-2
lines changed

examples/jwe-consume.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
declare(strict_types = 1);
1010

1111
use Sop\CryptoEncoding\PEM;
12+
use Sop\JWX\JWA\JWA;
1213
use Sop\JWX\JWK\RSA\RSAPrivateKeyJWK;
1314
use Sop\JWX\JWT\JWT;
1415
use Sop\JWX\JWT\ValidationContext;
@@ -21,7 +22,10 @@
2122
$jwk = RSAPrivateKeyJWK::fromPEM(
2223
PEM::fromFile(dirname(__DIR__) . '/test/assets/rsa/private_key.pem'));
2324
// create validation context containing only key for decryption
24-
$ctx = ValidationContext::fromJWK($jwk);
25+
$ctx = ValidationContext::fromJWK($jwk)
26+
// NOTE: asymmetric key derivation algorithms are not enabled by default
27+
// due to sign/encrypt confusion vulnerability!
28+
->withPermittedAlgorithmsAdded(JWA::ALGO_RSA1_5);
2529
// decrypt claims from the encrypted JWT
2630
$claims = $jwt->claims($ctx);
2731
// print all claims

lib/JWX/JWT/JWT.php

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Sop\JWX\JWT\Header\Header;
1717
use Sop\JWX\JWT\Header\JOSE;
1818
use Sop\JWX\JWT\Parameter\ContentTypeParameter;
19+
use Sop\JWX\Parameter\Parameter;
1920
use Sop\JWX\Util\Base64;
2021

2122
/**
@@ -161,7 +162,9 @@ public static function encryptedFromClaims(Claims $claims,
161162
*/
162163
public function claims(ValidationContext $ctx): Claims
163164
{
164-
// check signature or decrypt depending on the JWT type.
165+
// check that the token uses only permitted algorithms
166+
$this->_validateAlgorithms($ctx);
167+
// check signature or decrypt depending on the JWT type
165168
if ($this->isJWS()) {
166169
$payload = self::_validatedPayloadFromJWS($this->JWS(), $ctx);
167170
} else {
@@ -338,6 +341,43 @@ private function _claimsFromNestedPayload(string $payload,
338341
return $jwt->claims($ctx);
339342
}
340343

344+
/**
345+
* Validate that the token uses only permitted algorithms.
346+
*
347+
* @param ValidationContext $ctx Validation context
348+
*/
349+
private function _validateAlgorithms(ValidationContext $ctx): void
350+
{
351+
$headers = $this->header();
352+
if ($headers->hasAlgorithm()) {
353+
$this->_validateAlgorithmParameter($headers->algorithm(), $ctx);
354+
}
355+
if ($headers->hasEncryptionAlgorithm()) {
356+
$this->_validateAlgorithmParameter($headers->encryptionAlgorithm(), $ctx);
357+
}
358+
if ($headers->hasCompressionAlgorithm()) {
359+
$this->_validateAlgorithmParameter($headers->compressionAlgorithm(), $ctx);
360+
}
361+
}
362+
363+
/**
364+
* Check that given algorithm parameter value is permitted.
365+
*
366+
* @param Parameter $param Header parameter
367+
* @param ValidationContext $ctx Validation context
368+
*
369+
* @throws ValidationException If algorithm is prohibited
370+
*/
371+
private function _validateAlgorithmParameter(Parameter $param,
372+
ValidationContext $ctx): void
373+
{
374+
if (!$ctx->isPermittedAlgorithm($param->value())) {
375+
throw new ValidationException(sprintf(
376+
'%s algorithm %s is not permitted.',
377+
$param->name(), $param->value()));
378+
}
379+
}
380+
341381
/**
342382
* Get validated payload from JWS.
343383
*

lib/JWX/JWT/ValidationContext.php

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Sop\JWX\JWT;
66

7+
use Sop\JWX\JWA\JWA;
78
use Sop\JWX\JWK\JWK;
89
use Sop\JWX\JWK\JWKSet;
910
use Sop\JWX\JWT\Claim\RegisteredClaim;
@@ -72,6 +73,54 @@ class ValidationContext
7273
*/
7374
protected $_allowUnsecured;
7475

76+
/**
77+
* List of permitted algorithms.
78+
*
79+
* By default only asymmetric key derivation algorithms are prohibited.
80+
*
81+
* @var string[]
82+
*/
83+
protected $_permittedAlgoritms = [
84+
// all signature algorithms are safe to use
85+
JWA::ALGO_HS256 => true,
86+
JWA::ALGO_HS384 => true,
87+
JWA::ALGO_HS512 => true,
88+
JWA::ALGO_RS256 => true,
89+
JWA::ALGO_RS384 => true,
90+
JWA::ALGO_RS512 => true,
91+
JWA::ALGO_ES256 => true,
92+
JWA::ALGO_ES384 => true,
93+
JWA::ALGO_ES512 => true,
94+
// unsecured JWS may be used when explicitly allowed
95+
JWA::ALGO_NONE => true,
96+
// all symmetric key derivation algorithms are safe to use
97+
JWA::ALGO_A128KW => true,
98+
JWA::ALGO_A192KW => true,
99+
JWA::ALGO_A256KW => true,
100+
JWA::ALGO_A128GCMKW => true,
101+
JWA::ALGO_A192GCMKW => true,
102+
JWA::ALGO_A256GCMKW => true,
103+
JWA::ALGO_PBES2_HS256_A128KW => true,
104+
JWA::ALGO_PBES2_HS384_A192KW => true,
105+
JWA::ALGO_PBES2_HS512_A256KW => true,
106+
JWA::ALGO_DIR => true,
107+
/* asymmetric key derivation algorithms are subject to
108+
"sign/encrypt confusion" vulnerability, in which the JWT consumer
109+
can be tricked to accept a JWE token instead of a JWS by using
110+
the public key for encryption. */
111+
JWA::ALGO_RSA1_5 => false,
112+
JWA::ALGO_RSA_OAEP => false,
113+
// all encryption algorithms are safe to use
114+
JWA::ALGO_A128CBC_HS256 => true,
115+
JWA::ALGO_A192CBC_HS384 => true,
116+
JWA::ALGO_A256CBC_HS512 => true,
117+
JWA::ALGO_A128GCM => true,
118+
JWA::ALGO_A192GCM => true,
119+
JWA::ALGO_A256GCM => true,
120+
// all compression algorithms are safe to use
121+
JWA::ALGO_DEFLATE => true,
122+
];
123+
75124
/**
76125
* Constructor.
77126
*
@@ -296,6 +345,65 @@ public function isUnsecuredAllowed(): bool
296345
return $this->_allowUnsecured;
297346
}
298347

348+
/**
349+
* Get self with only given permitted algorithms.
350+
*
351+
* @param string ...$names Algorithm name
352+
*
353+
* @see \Sop\JWX\JWA\JWA
354+
*/
355+
public function withPermittedAlgorithms(string ...$names): self
356+
{
357+
$obj = clone $this;
358+
$obj->_permittedAlgoritms = [];
359+
return $obj->withPermittedAlgorithmsAdded(...$names);
360+
}
361+
362+
/**
363+
* Get self with given algorithms added to the permitted set.
364+
*
365+
* @param string ...$names Algorithm name
366+
*
367+
* @see \Sop\JWX\JWA\JWA
368+
*/
369+
public function withPermittedAlgorithmsAdded(string ...$names): self
370+
{
371+
$obj = clone $this;
372+
foreach ($names as $name) {
373+
$obj->_permittedAlgoritms[$name] = true;
374+
}
375+
return $obj;
376+
}
377+
378+
/**
379+
* Get self with given algorithms removed from the permitted set.
380+
*
381+
* @param string ...$names Algorithm name
382+
*
383+
* @see \Sop\JWX\JWA\JWA
384+
*/
385+
public function withProhibitedAlgorithms(string ...$names): self
386+
{
387+
$obj = clone $this;
388+
foreach ($names as $name) {
389+
$obj->_permittedAlgoritms[$name] = false;
390+
}
391+
return $obj;
392+
}
393+
394+
/**
395+
* Check whether given algorithm is permitted.
396+
*
397+
* @param string $name Algorithm name
398+
*
399+
* @see \Sop\JWX\JWA\JWA
400+
*/
401+
public function isPermittedAlgorithm(string $name): bool
402+
{
403+
return isset($this->_permittedAlgoritms[$name])
404+
&& true === $this->_permittedAlgoritms[$name];
405+
}
406+
299407
/**
300408
* Validate claims.
301409
*

test/unit/jwt/JWTTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,17 @@ public function testInvalidJWT()
293293
new JWT('');
294294
}
295295

296+
public function testProhibitedAlgorithm()
297+
{
298+
$jwt = JWT::unsecuredFromClaims(
299+
self::$_claims,
300+
new Header(new JWTParameter(JWTParameter::P_ZIP, 'dummy'))
301+
);
302+
$this->expectException(ValidationException::class);
303+
$this->expectExceptionMessage('zip algorithm dummy is not permitted');
304+
$jwt->claims(new ValidationContext());
305+
}
306+
296307
/**
297308
* @depends testCreateJWS
298309
*/

test/unit/jwt/ValidationContextTest.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
declare(strict_types = 1);
44

55
use PHPUnit\Framework\TestCase;
6+
use Sop\JWX\JWA\JWA;
67
use Sop\JWX\JWT\Claim\IssuerClaim;
78
use Sop\JWX\JWT\Claim\RegisteredClaim;
89
use Sop\JWX\JWT\Claims;
@@ -157,4 +158,37 @@ public function testValidateRequiredFail(ValidationContext $ctx)
157158
$this->expectExceptionMessage('failed');
158159
$ctx->validate($claims);
159160
}
161+
162+
/**
163+
* @depends testCreate
164+
*/
165+
public function testAddPermittedAlgorithm(ValidationContext $ctx)
166+
{
167+
$this->assertTrue($ctx->isPermittedAlgorithm(JWA::ALGO_RS256));
168+
$this->assertFalse($ctx->isPermittedAlgorithm('test'));
169+
$ctx = $ctx->withPermittedAlgorithmsAdded('test');
170+
$this->assertTrue($ctx->isPermittedAlgorithm(JWA::ALGO_RS256));
171+
$this->assertTrue($ctx->isPermittedAlgorithm('test'));
172+
}
173+
174+
/**
175+
* @depends testCreate
176+
*/
177+
public function testNewPermittedAlgorithm(ValidationContext $ctx)
178+
{
179+
$this->assertTrue($ctx->isPermittedAlgorithm(JWA::ALGO_RS256));
180+
$ctx = $ctx->withPermittedAlgorithms('test');
181+
$this->assertFalse($ctx->isPermittedAlgorithm(JWA::ALGO_RS256));
182+
$this->assertTrue($ctx->isPermittedAlgorithm('test'));
183+
}
184+
185+
/**
186+
* @depends testCreate
187+
*/
188+
public function testProhibitedAlgorithm(ValidationContext $ctx)
189+
{
190+
$this->assertTrue($ctx->isPermittedAlgorithm(JWA::ALGO_RS256));
191+
$ctx = $ctx->withProhibitedAlgorithms(JWA::ALGO_RS256);
192+
$this->assertFalse($ctx->isPermittedAlgorithm(JWA::ALGO_RS256));
193+
}
160194
}

0 commit comments

Comments
 (0)