Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PHP-32] Add support for signature verification using JWKS json #58

Merged
merged 8 commits into from
May 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions php/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

[comment]: <> (## 0.1.0)
[comment]: <> (* Added `truelayer_signing::{sign_with_pem, verify_with_pem, extract_jws_header}`.)
## [Unreleased]

## [0.1.0] - 2022-05-05
### Added
- Support for verifying a signature using JSON keys.
- Support for verifying a signature using `Jose\Component\Core\JWK` keys.

### Changed
- Methods that enable signature verification using PEM or PEM files can now receive multiple strings or paths (i.e multiple keys).
The signature is verified if at least one key verification succeeds.

## [0.0.2] - 2022-01-05
### Changed
- Excluded build/test files when publishing on packagist.

## [0.0.1] - 2021-12-09
### Added
- Support for signing using a PEM string, PEM file, PEM base64 string or `Jose\Component\Core\JWK` key.
- Support for verifying a signature using a PEM string, PEM file, PEM base64 string or `Jose\Component\Core\JWK` key.
1 change: 1 addition & 0 deletions php/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,6 @@ Signer::signWithKey('kid-value', new \Jose\Component\Core\JWK());
Verifier::verifyWithPemFile('path/to/publickey');
Verifier::verifyWithPem('publickey-pem-text');
Verifier::verifyWithPemBase64('base64-publickey-pem-text');
Verifier::verifyWithJsonKeys(...$arrayOfMultipleJsonKeys);
Verifier::verifyWithKey(new \Jose\Component\Core\JWK());
```
32 changes: 25 additions & 7 deletions php/src/Contracts/Verifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,24 @@
namespace TrueLayer\Signing\Contracts;

use Jose\Component\Core\JWK;
use TrueLayer\Signing\Exceptions\InvalidArgumentException;

interface Verifier extends Jws
{
/**
* @param array<string, string> ...$decodedJsonObjects
*
* @return static
*/
public static function verifyWithJsonKeys(array ...$decodedJsonObjects): self;

/**
* @param JWK ...$jwks
*
* @return static
*/
public static function verifyWithKeys(JWK ...$jwks): self;

/**
* @param JWK $jwk
*
Expand All @@ -16,28 +31,31 @@ interface Verifier extends Jws
public static function verifyWithKey(JWK $jwk): self;

/**
* @param string $pem
* @param string ...$pems
*
* @return static
* @throws InvalidArgumentException
*/
public static function verifyWithPem(string $pem): self;
public static function verifyWithPem(string ...$pems): self;

/**
* @param string $pemBase64
* @param string ...$pemsBase64
*
* @return static
* @throws InvalidArgumentException
*/
public static function verifyWithPemBase64(string $pemBase64): self;
public static function verifyWithPemBase64(string ...$pemsBase64): self;

/**
* @param string $path
* @param string ...$paths
*
* @return static
* @throws InvalidArgumentException
*/
public static function verifyWithPemFile(string $path): self;
public static function verifyWithPemFile(string ...$paths): self;

/**
* @param array<string, string> $headers
* @param string[] $headers
*
* @return $this
*/
Expand Down
9 changes: 9 additions & 0 deletions php/src/Exceptions/InvalidArgumentException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace TrueLayer\Signing\Exceptions;

use Exception;

class InvalidArgumentException extends Exception
{
}
138 changes: 108 additions & 30 deletions php/src/Verifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use TrueLayer\Signing\Constants\TrueLayerSignatures;
use TrueLayer\Signing\Contracts\Verifier as IVerifier;
use TrueLayer\Signing\Exceptions\InvalidAlgorithmException;
use TrueLayer\Signing\Exceptions\InvalidArgumentException;
use TrueLayer\Signing\Exceptions\InvalidSignatureException;
use TrueLayer\Signing\Exceptions\InvalidTrueLayerSignatureVersionException;
use TrueLayer\Signing\Exceptions\RequestPathNotFoundException;
Expand All @@ -22,79 +23,148 @@

final class Verifier extends AbstractJws implements IVerifier
{
/**
* @var JWSSerializerManager
*/
private JWSSerializerManager $serializerManager;

/**
* @var JWSVerifier
*/
private JWSVerifier $verifier;
private JWK $jwk;

/**
* @var array<JWK>
*/
private array $jwks;

/**
* @var string[]
*/
private array $requiredHeaders = [];

/**
* @param array<string, string> ...$jsonObjects
*
* @return IVerifier
* @throws InvalidArgumentException
*/
public static function verifyWithJsonKeys(array ...$jsonObjects): IVerifier
{
$jwks = [];
try {
foreach ($jsonObjects as $jsonObject) {
$encoded = json_encode($jsonObject);
if (!is_string($encoded)) {
throw new InvalidArgumentException('One or multiple keys are invalid');
}

$jwks[] = JWK::createFromJson($encoded);
}
} catch (\InvalidArgumentException $e) {
throw new InvalidArgumentException('One or multiple keys are invalid');
}

return new self($jwks);
}

/**
* @param JWK ...$jwks
*
* @return IVerifier
*/
public static function verifyWithKeys(JWK ...$jwks): IVerifier
{
return new self($jwks);
}

/**
* @param JWK $jwk
*
* @return Verifier
* @return IVerifier
*/
public static function verifyWithKey(JWK $jwk): Verifier
public static function verifyWithKey(JWK $jwk): IVerifier
{
return new self($jwk);
return self::verifyWithKeys($jwk);
}

/**
* @param string $pem
* @param string ...$pems
*
* @return Verifier
* @return IVerifier
* @throws InvalidArgumentException
*/
public static function verifyWithPem(string $pem): Verifier
public static function verifyWithPem(string ...$pems): IVerifier
{
$jwk = JWKFactory::createFromKey($pem, null, [
'use' => 'sig',
]);
$jwks = [];
try {
foreach ($pems as $pem) {
$jwks[] = JWKFactory::createFromKey($pem, null, [
'use' => 'sig',
]);
}
} catch (\Exception $e) {
throw new InvalidArgumentException('One or multiple PEM keys could not be deserialized');
}

return new self($jwk);
return new self($jwks);
}

/**
* @param string $pemBase64
* @param string ...$pemsBase64
*
* @return Verifier
* @return IVerifier
* @throws InvalidArgumentException
*/
public static function verifyWithPemBase64(string $pemBase64): Verifier
public static function verifyWithPemBase64(string ...$pemsBase64): IVerifier
{
return self::verifyWithPem(\base64_decode($pemBase64));
$decodedPems = [];
foreach ($pemsBase64 as $pemBase64) {
$decodedPems[] = \base64_decode($pemBase64);
}

return self::verifyWithPem(...$decodedPems);
}

/**
* @param string $path
* @param string ...$paths
*
* @return Verifier
* @return IVerifier
* @throws InvalidArgumentException
*/
public static function verifyWithPemFile(string $path): Verifier
public static function verifyWithPemFile(string ...$paths): IVerifier
{
$jwk = JWKFactory::createFromKeyFile($path, null, [
'use' => 'sig',
]);
$jwks = [];

return new self($jwk);
try {
foreach ($paths as $path) {
$jwks[] = JWKFactory::createFromKeyFile($path, null, [
'use' => 'sig',
]);
}
} catch (\Exception $e) {
throw new InvalidArgumentException('One or multiple files contain invalid keys');
}

return new self($jwks);
}

/**
* @param JWK $jwk
* @param array<JWK> $jwks
*/
private function __construct(JWK $jwk)
private function __construct(array $jwks)
{
$this->jwk = $jwk;
$this->jwks = $jwks;
$this->serializerManager = new JWSSerializerManager([new CompactSerializer()]);
$this->verifier = new JWSVerifier(new AlgorithmManager([new ES512()]));
}

/**
* @param string[] $headers
*
* @return $this
* @return IVerifier
*/
public function requireHeaders(array $headers): Verifier
public function requireHeaders(array $headers): IVerifier
{
\array_push($this->requiredHeaders, ...$headers);

Expand Down Expand Up @@ -126,10 +196,14 @@ public function verify(string $signature): void
throw new InvalidAlgorithmException();
}

if ($jwsHeaders['tl_version'] !== TrueLayerSignatures::SIGNING_VERSION) {
if (!empty($jwsHeaders['tl_version']) && $jwsHeaders['tl_version'] !== TrueLayerSignatures::SIGNING_VERSION) {
throw new InvalidTrueLayerSignatureVersionException();
}

if (empty($jwsHeaders['kid'])) {
throw new InvalidSignatureException('The kid is missing from the signature headers');
}

$tlHeaders = !empty($jwsHeaders['tl_headers']) ? \explode(',', $jwsHeaders['tl_headers']) : [];
$normalisedTlHeaders = Util::normaliseHeaderKeys($tlHeaders);
foreach ($this->requiredHeaders as $header) {
Expand All @@ -138,8 +212,12 @@ public function verify(string $signature): void
}
}

if (!$this->verifier->verifyWithKey($jws, $this->jwk, TrueLayerSignatures::SIGNATURE_INDEX, $this->buildPayload($tlHeaders))) {
throw new InvalidSignatureException();
foreach ($this->jwks as $jwk) {
if ($this->verifier->verifyWithKey($jws, $jwk, TrueLayerSignatures::SIGNATURE_INDEX, $this->buildPayload($tlHeaders))) {
return;
}
}

throw new InvalidSignatureException();
}
}
10 changes: 8 additions & 2 deletions php/tests/MockData.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@

use Jose\Component\Core\JWK;
use Jose\Component\KeyManagement\JWKFactory;
use Ramsey\Uuid\Uuid;

class MockData
{
/**
* @param ?string $kid
*
* @return array<string, JWK>
*/
public static function generateKeyPair(): array
public static function generateKeyPair(?string $kid = null): array
{
$jwk = JWKFactory::createECKey('P-521');
if (empty($kid)) {
$kid = Uuid::uuid4()->toString();
}
$jwk = JWKFactory::createECKey('P-521', ['kid' => $kid]);

return [
'private' => $jwk,
Expand Down
Loading