diff --git a/docs/server-implementation.md b/docs/server-implementation.md index b2f453c..644653a 100644 --- a/docs/server-implementation.md +++ b/docs/server-implementation.md @@ -188,22 +188,28 @@ Also include the `Retry-After` HTTP header. ### Key Derivation -Each license key gets a unique signing key derived from the server secret: +Each license key gets a unique signing key derived from the server secret using RFC 5869 HKDF: ```php /** * Derive a unique signing key for a license. * + * Uses RFC 5869 HKDF for secure key derivation. + * * @param string $licenseKey The license key * @param string $serverSecret The server's master secret - * @return string The derived key (hex encoded) + * @return string The derived key (hex encoded, 64 chars) */ function derive_signing_key(string $licenseKey, string $serverSecret): string { - // HKDF-like key derivation - $prk = hash_hmac('sha256', $licenseKey, $serverSecret, true); + // Use PHP's native HKDF implementation (RFC 5869) + // - IKM (input keying material): server secret + // - Length: 32 bytes (256 bits for SHA-256) + // - Info: license key (context-specific info) + // - Salt: empty (uses hash-length zero bytes as per RFC 5869) + $binaryKey = hash_hkdf('sha256', $serverSecret, 32, $licenseKey); - return hash_hmac('sha256', $prk . "\x01", $serverSecret); + return bin2hex($binaryKey); } ``` @@ -278,7 +284,7 @@ signature = HMAC-SHA256( Where: -- `derive_signing_key` uses HKDF-like derivation +- `derive_signing_key` uses RFC 5869 HKDF via `hash_hkdf()` - `canonical_json` sorts keys recursively, uses `JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE` - Result is hex-encoded (64 characters) @@ -678,9 +684,10 @@ final class LicenseApi private function deriveKey(string $licenseKey): string { - $prk = hash_hmac('sha256', $licenseKey, $this->serverSecret, true); + // RFC 5869 HKDF key derivation + $binaryKey = hash_hkdf('sha256', $this->serverSecret, 32, $licenseKey); - return hash_hmac('sha256', $prk . "\x01", $this->serverSecret); + return bin2hex($binaryKey); } // ========================================================================= diff --git a/src/Security/ResponseSignature.php b/src/Security/ResponseSignature.php index 78fa25e..b8942e9 100644 --- a/src/Security/ResponseSignature.php +++ b/src/Security/ResponseSignature.php @@ -129,11 +129,11 @@ final class ResponseSignature private function buildSignaturePayload(array $responseData, int $timestamp): string { - // Sort keys for consistent ordering - ksort($responseData); + // Sort keys recursively for consistent ordering (matches server implementation) + $sortedData = $this->sortKeysRecursive($responseData); // Create deterministic JSON representation - $jsonBody = json_encode($responseData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $jsonBody = json_encode($sortedData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if ($jsonBody === false) { throw new \RuntimeException( @@ -145,6 +145,22 @@ final class ResponseSignature return $timestamp . ':' . $jsonBody; } + /** + * Recursively sort array keys for consistent JSON output. + */ + private function sortKeysRecursive(array $data): array + { + ksort($data); + + foreach ($data as $key => $value) { + if (is_array($value)) { + $data[$key] = $this->sortKeysRecursive($value); + } + } + + return $data; + } + private function isTimestampValid(int $timestamp): bool { $now = time();