You've already forked wc-licensed-product
Release v0.2.0 - Security and integrity features
- Add REST API response signing using HMAC-SHA256 - Add SHA256 hash validation for version file uploads - Add ResponseSigner class for automatic API response signing - Add file_hash column to database schema - Remove external URL support from version uploads - Update translations with all fuzzy strings resolved Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
128
src/Api/ResponseSigner.php
Normal file
128
src/Api/ResponseSigner.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
/**
|
||||
* Response Signer
|
||||
*
|
||||
* Signs REST API responses to prevent tampering and replay attacks.
|
||||
*
|
||||
* @package Jeremias\WcLicensedProduct\Api
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Jeremias\WcLicensedProduct\Api;
|
||||
|
||||
/**
|
||||
* Signs license API responses using HMAC-SHA256
|
||||
*
|
||||
* The security model:
|
||||
* 1. Server generates a unique signature for each response using HMAC-SHA256
|
||||
* 2. Signature includes a timestamp to prevent replay attacks
|
||||
* 3. Client verifies the signature using a shared secret
|
||||
* 4. Invalid signatures cause the client to reject the response
|
||||
*/
|
||||
final class ResponseSigner
|
||||
{
|
||||
private string $serverSecret;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->serverSecret = defined('WC_LICENSE_SERVER_SECRET')
|
||||
? WC_LICENSE_SERVER_SECRET
|
||||
: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Register WordPress hooks
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
add_filter('rest_post_dispatch', [$this, 'signResponse'], 10, 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign REST API response
|
||||
*
|
||||
* @param \WP_REST_Response $response The response object
|
||||
* @param \WP_REST_Server $server The REST server
|
||||
* @param \WP_REST_Request $request The request object
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function signResponse($response, $server, $request)
|
||||
{
|
||||
// Only sign license API responses
|
||||
if (!$this->shouldSign($request)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$data = $response->get_data();
|
||||
$licenseKey = $request->get_param('license_key');
|
||||
|
||||
if (empty($licenseKey) || !is_array($data) || empty($this->serverSecret)) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$headers = $this->createSignatureHeaders($data, $licenseKey);
|
||||
|
||||
foreach ($headers as $name => $value) {
|
||||
$response->header($name, $value);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if request should be signed
|
||||
*/
|
||||
private function shouldSign(\WP_REST_Request $request): bool
|
||||
{
|
||||
$route = $request->get_route();
|
||||
|
||||
return str_starts_with($route, '/wc-licensed-product/v1/validate')
|
||||
|| str_starts_with($route, '/wc-licensed-product/v1/status')
|
||||
|| str_starts_with($route, '/wc-licensed-product/v1/activate');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create signature headers for response
|
||||
*
|
||||
* @param array $data The response data
|
||||
* @param string $licenseKey The license key from the request
|
||||
* @return array Associative array of headers
|
||||
*/
|
||||
private function createSignatureHeaders(array $data, string $licenseKey): array
|
||||
{
|
||||
$timestamp = time();
|
||||
$signingKey = $this->deriveKey($licenseKey);
|
||||
|
||||
// Sort keys for consistent ordering
|
||||
ksort($data);
|
||||
|
||||
// Build signature payload
|
||||
$payload = $timestamp . ':' . json_encode(
|
||||
$data,
|
||||
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
|
||||
);
|
||||
|
||||
return [
|
||||
'X-License-Signature' => hash_hmac('sha256', $payload, $signingKey),
|
||||
'X-License-Timestamp' => (string) $timestamp,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a unique signing key for a license
|
||||
*
|
||||
* Uses HKDF-like key derivation to create a unique signing key
|
||||
* for each license key, preventing cross-license signature attacks.
|
||||
*
|
||||
* @param string $licenseKey The license key
|
||||
* @return string The derived signing key (hex encoded)
|
||||
*/
|
||||
private function deriveKey(string $licenseKey): string
|
||||
{
|
||||
// HKDF-like key derivation
|
||||
$prk = hash_hmac('sha256', $licenseKey, $this->serverSecret, true);
|
||||
|
||||
return hash_hmac('sha256', $prk . "\x01", $this->serverSecret);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user