Skip to content

Commit

Permalink
Add proper PHPDoc tags
Browse files Browse the repository at this point in the history
  • Loading branch information
martin-helmich committed Oct 29, 2024
1 parent d045796 commit 1165cf9
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 21 deletions.
13 changes: 13 additions & 0 deletions src/Security/BadSignatureException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Mittwald\MStudio\Webhooks\Security;

use Exception;

/**
* Describes a request signature so invalid that it caused an error.
*/
class BadSignatureException extends Exception
{

}
8 changes: 8 additions & 0 deletions src/Security/CachingKeyLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@

use Psr\Cache\CacheItemPoolInterface;

/**
* CachingKeyLoader is a helper class that can be used to decorate a key loader
* with a PSR-6 compatible cache.
*/
class CachingKeyLoader implements KeyLoader
{
private KeyLoader $inner;
private CacheItemPoolInterface $cache;

/**
* @param KeyLoader $inner The key loader to decorate
* @param CacheItemPoolInterface $cache The cache implementation
*/
public function __construct(KeyLoader $inner, CacheItemPoolInterface $cache)
{
$this->inner = $inner;
Expand Down
10 changes: 10 additions & 0 deletions src/Security/KeyLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,17 @@

namespace Mittwald\MStudio\Webhooks\Security;

/**
* Interface definition for loading webhook verification public keys.
*/
interface KeyLoader
{
/**
* Loads a public key by its key serial number. Implementations should
* return null when no key with the given serial number is found.
*
* @param string $serial Key serial number
* @return string|null Key in base64 encoding, or `null` when no key with the given serial number exists.
*/
public function loadPublicKey(string $serial): string|null;
}
3 changes: 3 additions & 0 deletions src/Security/RemoteKeyLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
use Mittwald\ApiClient\Generated\V2\Clients\Marketplace\ExtensionGetPublicKey\ExtensionGetPublicKeyRequest;
use Mittwald\ApiClient\MittwaldAPIV2Client;

/**
* Loads a webhook verification key from the mittwald mStudio API.
*/
readonly class RemoteKeyLoader implements KeyLoader
{
public function __construct(private MittwaldAPIV2Client $client)
Expand Down
36 changes: 25 additions & 11 deletions src/Security/SignatureVerifier.php
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
<?php
namespace Mittwald\MStudio\Webhooks\Security;

use Exception;
use Psr\Http\Message\RequestInterface;
use SodiumException;

/**
* Service class for verifying the request signature of mStudio webhook requests.
*/
class SignatureVerifier
{
public function __construct(private readonly KeyLoader $keyLoader)
private readonly KeyLoader $keyLoader;

public function __construct(KeyLoader $keyLoader)
{
$this->keyLoader = $keyLoader;
}

/**
* @param RequestInterface $request
* @param non-empty-string $signature
* @param non-empty-string $serial
* @return bool
* @throws \SodiumException
* Verifies if the signature of a request is valid.
*
* @param RequestInterface $request The raw request object; the signature will be verified using the request body.
* @param non-empty-string $signature The request signature, in base64 encoding
* @param non-empty-string $serial The signature key serial
* @return bool `true` if the request signature is valid, otherwise `false`
* @throws BadSignatureException
*/
public function verifyRequestSignature(RequestInterface $request, string $signature, string $serial): bool
{
Expand All @@ -30,10 +40,14 @@ public function verifyRequestSignature(RequestInterface $request, string $signat
throw new \InvalidArgumentException("signature and key must be in valid base64 encoding");
}

return sodium_crypto_sign_verify_detached(
$binSignature,
$request->getBody()->getContents(),
$binKey,
);
try {
return sodium_crypto_sign_verify_detached(
$binSignature,
$request->getBody()->getContents(),
$binKey,
);
} catch (SodiumException $err) {
throw new BadSignatureException('error while verifying request signature', previous: $err);
}
}
}
9 changes: 8 additions & 1 deletion src/Security/StaticKeyLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@

namespace Mittwald\MStudio\Webhooks\Security;

/**
* Implements the KeyLoader interface with static keys.
*
* NOTE: This is intended for use in unit testing, ONLY.
*
* @internal
*/
readonly class StaticKeyLoader implements KeyLoader
{
/**
* @param string[] $keys
* @param string[] $keys An associative array of base64-encoded keys, using the key serials as key.
*/
public function __construct(private array $keys)
{
Expand Down
40 changes: 31 additions & 9 deletions src/Security/WebhookAuthorizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,36 @@
use Psr\Http\Message\RequestInterface;
use Psr\Log\LoggerInterface;

/**
* Authorizes webhook requests based on the signature in their headers.
*/
readonly class WebhookAuthorizer
{
private const HEADER_SIGNATURE = "x-marketplace-signature";
private const HEADER_SERIAL = "x-marketplace-signature-serial";

private SignatureVerifier $signatureVerifier;
private LoggerInterface $logger;

public function __construct(
private SignatureVerifier $signatureVerifier,
private LoggerInterface $logger,
SignatureVerifier $signatureVerifier,
LoggerInterface $logger,
)
{
$this->signatureVerifier = $signatureVerifier;
$this->logger = $logger;
}

/**
* Verifies if the given request has a valid webhook signature.
*
* @param RequestInterface $request The request to verify.
* @return bool true if the request has a valid signature, otherwise false.
*/
public function authorize(RequestInterface $request): bool
{
$signature = $request->getHeader('x-marketplace-signature');
$serial = $request->getHeader('x-marketplace-signature-serial');
$signature = $request->getHeader(self::HEADER_SIGNATURE);
$serial = $request->getHeader(self::HEADER_SERIAL);

if (count($signature) === 0 || count($serial) === 0) {
$this->logger->warning('received request without signature or serial');
Expand All @@ -29,10 +46,15 @@ public function authorize(RequestInterface $request): bool
return false;
}

return $this->signatureVerifier->verifyRequestSignature(
$request,
$signature[0],
$serial[0],
);
try {
return $this->signatureVerifier->verifyRequestSignature(
$request,
$signature[0],
$serial[0],
);
} catch (BadSignatureException $err) {
$this->logger->error('bad request signature', ['err' => $err]);
return false;
}
}
}

0 comments on commit 1165cf9

Please sign in to comment.