Fix critical signature compatibility with client library (v0.5.5)

CRITICAL: Key derivation now uses native hash_hkdf() for RFC 5869
compliance. Previous custom implementation was incompatible with
the magdev/wc-licensed-product-client library.

Changes:
- ResponseSigner::deriveCustomerSecret() now uses hash_hkdf()
- Added missing domain validation to /activate endpoint
- Customer secrets will change after upgrade (breaking change)

The signature algorithm now matches the client's ResponseSignature::deriveKey():
- IKM: server_secret
- Length: 32 bytes
- Info: license_key

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 17:06:18 +01:00
parent ae49b262fa
commit 0b58de193e
5 changed files with 64 additions and 5 deletions

View File

@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.5.5] - 2026-01-26
### Fixed
- **CRITICAL:** Response signing key derivation now uses native `hash_hkdf()` for RFC 5869 compliance
- Key derivation now matches client library (`SecureLicenseClient`) exactly
- Added missing domain validation to `/activate` endpoint (1-255 characters)
### Changed
- `ResponseSigner::deriveCustomerSecret()` now uses `hash_hkdf('sha256', $serverSecret, 32, $licenseKey)`
- Previous custom HKDF-like implementation was incompatible with client library
### Security
- Signatures generated by server now verify correctly with `magdev/wc-licensed-product-client`
- All three API endpoints now have consistent parameter validation
## [0.5.4] - 2026-01-26
### Fixed

View File

@@ -1406,3 +1406,34 @@ Bug fix release aligning server implementation with client documentation at `mag
- New `getStatusCodeForResult()` method maps error codes to HTTP status codes
- License key validation callback added to all three endpoints (validate, status, activate)
- Uses PHP 8 match expression for status code mapping
### 2026-01-26 - Version 0.5.5 - Critical Signature Fix
**Overview:**
Critical bug fix for response signing. The key derivation algorithm was incompatible with the client library, causing signature verification failures.
**Critical Fix:**
- Key derivation now uses PHP's native `hash_hkdf()` function per RFC 5869
- Previous custom implementation produced different keys than the client library
- Signatures now verify correctly with `magdev/wc-licensed-product-client`
**Additional Fix:**
- Added missing domain validation to `/activate` endpoint (1-255 characters)
**Modified files:**
- `src/Api/ResponseSigner.php` - Fixed key derivation to use `hash_hkdf()`
- `src/Api/RestApiController.php` - Added domain validation to `/activate` endpoint
**Technical notes:**
- Old implementation: `hash_hmac('sha256', $prk . "\x01", $serverSecret)` - custom HKDF-like
- New implementation: `bin2hex(hash_hkdf('sha256', $serverSecret, 32, $licenseKey))` - RFC 5869
- Parameters match client's `ResponseSignature::deriveKey()` exactly:
- IKM (input keying material): server_secret
- Length: 32 bytes (256 bits)
- Info: license_key (context-specific info)
- **Breaking change for existing signatures** - customer secrets will change after upgrade

View File

@@ -157,16 +157,23 @@ final class ResponseSigner
* to verify signed API responses. Each customer gets their own secret
* derived from their license key.
*
* Uses RFC 5869 HKDF via PHP's native hash_hkdf() function.
* Parameters match the client library (SecureLicenseClient):
* - IKM (input keying material): server_secret
* - Length: 32 bytes (256 bits for SHA-256)
* - Info: license_key (context-specific info)
*
* @param string $licenseKey The customer's license key
* @param string $serverSecret The server's master secret
* @return string The derived secret (64 hex characters)
*/
public static function deriveCustomerSecret(string $licenseKey, string $serverSecret): string
{
// HKDF-like key derivation
$prk = hash_hmac('sha256', $licenseKey, $serverSecret, true);
// RFC 5869 HKDF using PHP's native implementation
// Must match client's ResponseSignature::deriveKey() exactly
$binaryKey = hash_hkdf('sha256', $serverSecret, 32, $licenseKey);
return hash_hmac('sha256', $prk . "\x01", $serverSecret);
return bin2hex($binaryKey);
}
/**

View File

@@ -331,6 +331,9 @@ final class RestApiController
'required' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => function ($value): bool {
return !empty($value) && strlen($value) <= 255;
},
],
],
]);

View File

@@ -3,7 +3,7 @@
* Plugin Name: WooCommerce Licensed Product
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-licensed-product
* Description: WooCommerce plugin to sell software products using license keys with domain-based validation.
* Version: 0.5.4
* Version: 0.5.5
* Author: Marco Graetsch
* Author URI: https://src.bundespruefstelle.ch/magdev
* License: GPL-2.0-or-later
@@ -28,7 +28,7 @@ if (!defined('ABSPATH')) {
}
// Plugin constants
define('WC_LICENSED_PRODUCT_VERSION', '0.5.4');
define('WC_LICENSED_PRODUCT_VERSION', '0.5.5');
define('WC_LICENSED_PRODUCT_PLUGIN_FILE', __FILE__);
define('WC_LICENSED_PRODUCT_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('WC_LICENSED_PRODUCT_PLUGIN_URL', plugin_dir_url(__FILE__));