Skip to content

Commit 707cb79

Browse files
[PHP-32] Add support for signature verification using JWKS json (#58)
* [PHP-32] Add support for signature verification using JWKS json * [PHP-32] Formatting * [PHP-32] PHPStan * [PHP-32] Changelog * [PHP-32] Readme * [PHP-32] Backfill changelog with v0.0.2 * [PHP-32] Backfill changelog with v0.0.1
1 parent 8802561 commit 707cb79

File tree

7 files changed

+205
-51
lines changed

7 files changed

+205
-51
lines changed

php/CHANGELOG.md

+19-2
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,22 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7-
[comment]: <> (## 0.1.0)
8-
[comment]: <> (* Added `truelayer_signing::{sign_with_pem, verify_with_pem, extract_jws_header}`.)
7+
## [Unreleased]
8+
9+
## [0.1.0] - 2022-05-05
10+
### Added
11+
- Support for verifying a signature using JSON keys.
12+
- Support for verifying a signature using `Jose\Component\Core\JWK` keys.
13+
14+
### Changed
15+
- Methods that enable signature verification using PEM or PEM files can now receive multiple strings or paths (i.e multiple keys).
16+
The signature is verified if at least one key verification succeeds.
17+
18+
## [0.0.2] - 2022-01-05
19+
### Changed
20+
- Excluded build/test files when publishing on packagist.
21+
22+
## [0.0.1] - 2021-12-09
23+
### Added
24+
- Support for signing using a PEM string, PEM file, PEM base64 string or `Jose\Component\Core\JWK` key.
25+
- Support for verifying a signature using a PEM string, PEM file, PEM base64 string or `Jose\Component\Core\JWK` key.

php/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,6 @@ Signer::signWithKey('kid-value', new \Jose\Component\Core\JWK());
8383
Verifier::verifyWithPemFile('path/to/publickey');
8484
Verifier::verifyWithPem('publickey-pem-text');
8585
Verifier::verifyWithPemBase64('base64-publickey-pem-text');
86+
Verifier::verifyWithJsonKeys(...$arrayOfMultipleJsonKeys);
8687
Verifier::verifyWithKey(new \Jose\Component\Core\JWK());
8788
```

php/src/Contracts/Verifier.php

+25-7
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,24 @@
55
namespace TrueLayer\Signing\Contracts;
66

77
use Jose\Component\Core\JWK;
8+
use TrueLayer\Signing\Exceptions\InvalidArgumentException;
89

910
interface Verifier extends Jws
1011
{
12+
/**
13+
* @param array<string, string> ...$decodedJsonObjects
14+
*
15+
* @return static
16+
*/
17+
public static function verifyWithJsonKeys(array ...$decodedJsonObjects): self;
18+
19+
/**
20+
* @param JWK ...$jwks
21+
*
22+
* @return static
23+
*/
24+
public static function verifyWithKeys(JWK ...$jwks): self;
25+
1126
/**
1227
* @param JWK $jwk
1328
*
@@ -16,28 +31,31 @@ interface Verifier extends Jws
1631
public static function verifyWithKey(JWK $jwk): self;
1732

1833
/**
19-
* @param string $pem
34+
* @param string ...$pems
2035
*
2136
* @return static
37+
* @throws InvalidArgumentException
2238
*/
23-
public static function verifyWithPem(string $pem): self;
39+
public static function verifyWithPem(string ...$pems): self;
2440

2541
/**
26-
* @param string $pemBase64
42+
* @param string ...$pemsBase64
2743
*
2844
* @return static
45+
* @throws InvalidArgumentException
2946
*/
30-
public static function verifyWithPemBase64(string $pemBase64): self;
47+
public static function verifyWithPemBase64(string ...$pemsBase64): self;
3148

3249
/**
33-
* @param string $path
50+
* @param string ...$paths
3451
*
3552
* @return static
53+
* @throws InvalidArgumentException
3654
*/
37-
public static function verifyWithPemFile(string $path): self;
55+
public static function verifyWithPemFile(string ...$paths): self;
3856

3957
/**
40-
* @param array<string, string> $headers
58+
* @param string[] $headers
4159
*
4260
* @return $this
4361
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace TrueLayer\Signing\Exceptions;
4+
5+
use Exception;
6+
7+
class InvalidArgumentException extends Exception
8+
{
9+
}

php/src/Verifier.php

+108-30
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use TrueLayer\Signing\Constants\TrueLayerSignatures;
1515
use TrueLayer\Signing\Contracts\Verifier as IVerifier;
1616
use TrueLayer\Signing\Exceptions\InvalidAlgorithmException;
17+
use TrueLayer\Signing\Exceptions\InvalidArgumentException;
1718
use TrueLayer\Signing\Exceptions\InvalidSignatureException;
1819
use TrueLayer\Signing\Exceptions\InvalidTrueLayerSignatureVersionException;
1920
use TrueLayer\Signing\Exceptions\RequestPathNotFoundException;
@@ -22,79 +23,148 @@
2223

2324
final class Verifier extends AbstractJws implements IVerifier
2425
{
26+
/**
27+
* @var JWSSerializerManager
28+
*/
2529
private JWSSerializerManager $serializerManager;
30+
31+
/**
32+
* @var JWSVerifier
33+
*/
2634
private JWSVerifier $verifier;
27-
private JWK $jwk;
35+
36+
/**
37+
* @var array<JWK>
38+
*/
39+
private array $jwks;
2840

2941
/**
3042
* @var string[]
3143
*/
3244
private array $requiredHeaders = [];
3345

46+
/**
47+
* @param array<string, string> ...$jsonObjects
48+
*
49+
* @return IVerifier
50+
* @throws InvalidArgumentException
51+
*/
52+
public static function verifyWithJsonKeys(array ...$jsonObjects): IVerifier
53+
{
54+
$jwks = [];
55+
try {
56+
foreach ($jsonObjects as $jsonObject) {
57+
$encoded = json_encode($jsonObject);
58+
if (!is_string($encoded)) {
59+
throw new InvalidArgumentException('One or multiple keys are invalid');
60+
}
61+
62+
$jwks[] = JWK::createFromJson($encoded);
63+
}
64+
} catch (\InvalidArgumentException $e) {
65+
throw new InvalidArgumentException('One or multiple keys are invalid');
66+
}
67+
68+
return new self($jwks);
69+
}
70+
71+
/**
72+
* @param JWK ...$jwks
73+
*
74+
* @return IVerifier
75+
*/
76+
public static function verifyWithKeys(JWK ...$jwks): IVerifier
77+
{
78+
return new self($jwks);
79+
}
80+
3481
/**
3582
* @param JWK $jwk
3683
*
37-
* @return Verifier
84+
* @return IVerifier
3885
*/
39-
public static function verifyWithKey(JWK $jwk): Verifier
86+
public static function verifyWithKey(JWK $jwk): IVerifier
4087
{
41-
return new self($jwk);
88+
return self::verifyWithKeys($jwk);
4289
}
4390

4491
/**
45-
* @param string $pem
92+
* @param string ...$pems
4693
*
47-
* @return Verifier
94+
* @return IVerifier
95+
* @throws InvalidArgumentException
4896
*/
49-
public static function verifyWithPem(string $pem): Verifier
97+
public static function verifyWithPem(string ...$pems): IVerifier
5098
{
51-
$jwk = JWKFactory::createFromKey($pem, null, [
52-
'use' => 'sig',
53-
]);
99+
$jwks = [];
100+
try {
101+
foreach ($pems as $pem) {
102+
$jwks[] = JWKFactory::createFromKey($pem, null, [
103+
'use' => 'sig',
104+
]);
105+
}
106+
} catch (\Exception $e) {
107+
throw new InvalidArgumentException('One or multiple PEM keys could not be deserialized');
108+
}
54109

55-
return new self($jwk);
110+
return new self($jwks);
56111
}
57112

58113
/**
59-
* @param string $pemBase64
114+
* @param string ...$pemsBase64
60115
*
61-
* @return Verifier
116+
* @return IVerifier
117+
* @throws InvalidArgumentException
62118
*/
63-
public static function verifyWithPemBase64(string $pemBase64): Verifier
119+
public static function verifyWithPemBase64(string ...$pemsBase64): IVerifier
64120
{
65-
return self::verifyWithPem(\base64_decode($pemBase64));
121+
$decodedPems = [];
122+
foreach ($pemsBase64 as $pemBase64) {
123+
$decodedPems[] = \base64_decode($pemBase64);
124+
}
125+
126+
return self::verifyWithPem(...$decodedPems);
66127
}
67128

68129
/**
69-
* @param string $path
130+
* @param string ...$paths
70131
*
71-
* @return Verifier
132+
* @return IVerifier
133+
* @throws InvalidArgumentException
72134
*/
73-
public static function verifyWithPemFile(string $path): Verifier
135+
public static function verifyWithPemFile(string ...$paths): IVerifier
74136
{
75-
$jwk = JWKFactory::createFromKeyFile($path, null, [
76-
'use' => 'sig',
77-
]);
137+
$jwks = [];
78138

79-
return new self($jwk);
139+
try {
140+
foreach ($paths as $path) {
141+
$jwks[] = JWKFactory::createFromKeyFile($path, null, [
142+
'use' => 'sig',
143+
]);
144+
}
145+
} catch (\Exception $e) {
146+
throw new InvalidArgumentException('One or multiple files contain invalid keys');
147+
}
148+
149+
return new self($jwks);
80150
}
81151

82152
/**
83-
* @param JWK $jwk
153+
* @param array<JWK> $jwks
84154
*/
85-
private function __construct(JWK $jwk)
155+
private function __construct(array $jwks)
86156
{
87-
$this->jwk = $jwk;
157+
$this->jwks = $jwks;
88158
$this->serializerManager = new JWSSerializerManager([new CompactSerializer()]);
89159
$this->verifier = new JWSVerifier(new AlgorithmManager([new ES512()]));
90160
}
91161

92162
/**
93163
* @param string[] $headers
94164
*
95-
* @return $this
165+
* @return IVerifier
96166
*/
97-
public function requireHeaders(array $headers): Verifier
167+
public function requireHeaders(array $headers): IVerifier
98168
{
99169
\array_push($this->requiredHeaders, ...$headers);
100170

@@ -126,10 +196,14 @@ public function verify(string $signature): void
126196
throw new InvalidAlgorithmException();
127197
}
128198

129-
if ($jwsHeaders['tl_version'] !== TrueLayerSignatures::SIGNING_VERSION) {
199+
if (!empty($jwsHeaders['tl_version']) && $jwsHeaders['tl_version'] !== TrueLayerSignatures::SIGNING_VERSION) {
130200
throw new InvalidTrueLayerSignatureVersionException();
131201
}
132202

203+
if (empty($jwsHeaders['kid'])) {
204+
throw new InvalidSignatureException('The kid is missing from the signature headers');
205+
}
206+
133207
$tlHeaders = !empty($jwsHeaders['tl_headers']) ? \explode(',', $jwsHeaders['tl_headers']) : [];
134208
$normalisedTlHeaders = Util::normaliseHeaderKeys($tlHeaders);
135209
foreach ($this->requiredHeaders as $header) {
@@ -138,8 +212,12 @@ public function verify(string $signature): void
138212
}
139213
}
140214

141-
if (!$this->verifier->verifyWithKey($jws, $this->jwk, TrueLayerSignatures::SIGNATURE_INDEX, $this->buildPayload($tlHeaders))) {
142-
throw new InvalidSignatureException();
215+
foreach ($this->jwks as $jwk) {
216+
if ($this->verifier->verifyWithKey($jws, $jwk, TrueLayerSignatures::SIGNATURE_INDEX, $this->buildPayload($tlHeaders))) {
217+
return;
218+
}
143219
}
220+
221+
throw new InvalidSignatureException();
144222
}
145223
}

php/tests/MockData.php

+8-2
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,21 @@
66

77
use Jose\Component\Core\JWK;
88
use Jose\Component\KeyManagement\JWKFactory;
9+
use Ramsey\Uuid\Uuid;
910

1011
class MockData
1112
{
1213
/**
14+
* @param ?string $kid
15+
*
1316
* @return array<string, JWK>
1417
*/
15-
public static function generateKeyPair(): array
18+
public static function generateKeyPair(?string $kid = null): array
1619
{
17-
$jwk = JWKFactory::createECKey('P-521');
20+
if (empty($kid)) {
21+
$kid = Uuid::uuid4()->toString();
22+
}
23+
$jwk = JWKFactory::createECKey('P-521', ['kid' => $kid]);
1824

1925
return [
2026
'private' => $jwk,

0 commit comments

Comments
 (0)