<?php
declare(strict_types=1);
namespace ParagonIE\Chronicle\Middleware;
use ParagonIE\Chronicle\{
Chronicle,
Exception\ClientNotFound,
Exception\FilesystemException,
Exception\SecurityViolation,
MiddlewareInterface
};
use ParagonIE\Sapient\CryptographyKeys\SigningPublicKey;
use Psr\Http\Message\{
RequestInterface,
ResponseInterface
};
use Slim\Http\Request;
/**
* Class CheckClientSignature
*
* Checks the client signature on a RequestInterface
*
* @package ParagonIE\Chronicle\Middleware
*/
class CheckClientSignature implements MiddlewareInterface
{
const PROPERTIES_TO_SET = ['authenticated'];
/**
* @param RequestInterface $request
* @return string
*
* @throws ClientNotFound
* @throws SecurityViolation
*/
public function getClientId(RequestInterface $request): string
{
$header = $request->getHeader(Chronicle::CLIENT_IDENTIFIER_HEADER);
if (!$header) {
throw new ClientNotFound('No client header provided');
}
if (\count($header) !== 1) {
throw new SecurityViolation('Only one client header may be provided');
}
return (string) \array_shift($header);
}
/**
* Only selects a valid result if the client has isAdmin set to TRUE.
*
* @param string $clientId
* @return SigningPublicKey
*
* @throws ClientNotFound
*/
public function getPublicKey(string $clientId): SigningPublicKey
{
// The second parameter gets overridden in CheckAdminSignature to TRUE:
return Chronicle::getClientsPublicKey($clientId, false);
}
/**
* @param RequestInterface $request
* @param ResponseInterface $response
* @param callable $next
* @return ResponseInterface
*
* @throws FilesystemException
*/
public function __invoke(
RequestInterface $request,
ResponseInterface $response,
callable $next
): ResponseInterface {
try {
// Get the client ID from the request
/** @var string $clientId */
$clientId = $this->getClientId($request);
} catch (\Exception $ex) {
return Chronicle::errorResponse($response, $ex->getMessage(), 403);
}
try {
/** @var SigningPublicKey $publicKey */
$publicKey = $this->getPublicKey($clientId);
} catch (ClientNotFound $ex) {
return Chronicle::errorResponse($response, $ex->getMessage(), 403);
}
try {
$request = Chronicle::getSapient()
->verifySignedRequest($request, $publicKey);
if ($request instanceof Request) {
$serverPublicKey = Chronicle::getSigningKey()
->getPublicKey()
->getString();
if (\hash_equals($serverPublicKey, $publicKey->getString())) {
return Chronicle::errorResponse(
$response,
"The server's signing keys cannot be used by clients.",
403
);
}
// Cache authenticated status in the request
/** @var string $prop */
foreach (static::PROPERTIES_TO_SET as $prop) {
$request = $request->withAttribute($prop, true);
}
// Store the public key in the request as well
$request = $request->withAttribute('publicKey', $publicKey);
}
} catch (\Throwable $ex) {
return Chronicle::errorResponse($response, $ex->getMessage(), 403);
}
/** @var ResponseInterface|null $nextOut */
$nextOut = $next($request, $response);
if ($nextOut instanceof ResponseInterface) {
return $nextOut;
}
return $response;
}
}
|