Add per-license customer secrets for API response verification

- Add static methods to ResponseSigner for deriving customer-specific secrets
- Display "API Verification Secret" in customer account licenses page
- Add collapsible secret section with copy button
- Update server-implementation.md with per-license secret documentation
- Update translations with new strings

Each customer now gets a unique verification secret derived from their
license key, eliminating the need to share the master server secret.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-26 15:29:57 +01:00
parent 7d02105284
commit 549a58dc5d
10 changed files with 1306 additions and 1030 deletions

View File

@@ -147,9 +147,52 @@ final class ResponseSigner
*/
private function deriveKey(string $licenseKey): string
{
// HKDF-like key derivation
$prk = hash_hmac('sha256', $licenseKey, $this->serverSecret, true);
return self::deriveCustomerSecret($licenseKey, $this->serverSecret);
}
return hash_hmac('sha256', $prk . "\x01", $this->serverSecret);
/**
* Derive a customer-specific secret from a license key
*
* This secret is unique per license and can be shared with the customer
* to verify signed API responses. Each customer gets their own secret
* derived from their license key.
*
* @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);
return hash_hmac('sha256', $prk . "\x01", $serverSecret);
}
/**
* Get the customer secret for a license key using the configured server secret
*
* @param string $licenseKey The customer's license key
* @return string|null The derived secret, or null if server secret is not configured
*/
public static function getCustomerSecretForLicense(string $licenseKey): ?string
{
$serverSecret = defined('WC_LICENSE_SERVER_SECRET') ? WC_LICENSE_SERVER_SECRET : '';
if (empty($serverSecret)) {
return null;
}
return self::deriveCustomerSecret($licenseKey, $serverSecret);
}
/**
* Check if response signing is enabled
*
* @return bool True if server secret is configured
*/
public static function isSigningEnabled(): bool
{
return defined('WC_LICENSE_SERVER_SECRET') && !empty(WC_LICENSE_SERVER_SECRET);
}
}