2 Commits
Author SHA1 Message Date
magdevandClaude Opus 4.5 a11aa4260a Update session history with server alignment learnings
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 16:34:18 +01:00
magdevandClaude Opus 4.5 8062e1be77 Align client and server signature implementation
- Update server docs to use RFC 5869 hash_hkdf() for key derivation
- Add recursive key sorting to client ResponseSignature
- Ensures client and server produce matching signatures for nested objects

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 16:33:44 +01:00
3 changed files with 51 additions and 12 deletions

No files matched your search

+17 -1
View File
@@ -31,7 +31,7 @@ No known bugs at the moment
### Version 0.2.0 ### Version 0.2.0
No changes at the moment. No pending tasks at the moment.
## Technical Stack ## Technical Stack
@@ -202,3 +202,19 @@ When editing CLAUDE.md or other markdown files, follow these rules to avoid lint
- Empty header values should be treated as missing (null) not empty strings - Empty header values should be treated as missing (null) not empty strings
- Constructor parameters added: `allowInsecureHttp` for HTTP on non-localhost in dev mode - Constructor parameters added: `allowInsecureHttp` for HTTP on non-localhost in dev mode
- Private IP ranges to block: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16, 0.0.0.0/8 - Private IP ranges to block: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, 169.254.0.0/16, 0.0.0.0/8
### 2026-01-26 - Server Implementation Alignment
**Completed:**
- Verified client implementation against server documentation
- Updated server docs to use RFC 5869 `hash_hkdf()` for key derivation (matching client)
- Added recursive key sorting (`sortKeysRecursive()`) to client `ResponseSignature`
- Client and server now use identical signature algorithms
**Learnings:**
- Server and client must use identical key derivation and JSON canonicalization
- Recursive key sorting is essential for nested objects like the `license` object in validate responses
- When updating cryptographic implementations, both client and server documentation must be aligned
- The remote server documentation URL was 404 - local `docs/server-implementation.md` is the source of truth
+15 -8
View File
@@ -188,22 +188,28 @@ Also include the `Retry-After` HTTP header.
### Key Derivation ### 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 ```php
/** /**
* Derive a unique signing key for a license. * Derive a unique signing key for a license.
* *
* Uses RFC 5869 HKDF for secure key derivation.
*
* @param string $licenseKey The license key * @param string $licenseKey The license key
* @param string $serverSecret The server's master secret * @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 function derive_signing_key(string $licenseKey, string $serverSecret): string
{ {
// HKDF-like key derivation // Use PHP's native HKDF implementation (RFC 5869)
$prk = hash_hmac('sha256', $licenseKey, $serverSecret, true); // - 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: 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` - `canonical_json` sorts keys recursively, uses `JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE`
- Result is hex-encoded (64 characters) - Result is hex-encoded (64 characters)
@@ -678,9 +684,10 @@ final class LicenseApi
private function deriveKey(string $licenseKey): string 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);
} }
// ========================================================================= // =========================================================================
+19 -3
View File
@@ -129,11 +129,11 @@ final class ResponseSignature
private function buildSignaturePayload(array $responseData, int $timestamp): string private function buildSignaturePayload(array $responseData, int $timestamp): string
{ {
// Sort keys for consistent ordering // Sort keys recursively for consistent ordering (matches server implementation)
ksort($responseData); $sortedData = $this->sortKeysRecursive($responseData);
// Create deterministic JSON representation // 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) { if ($jsonBody === false) {
throw new \RuntimeException( throw new \RuntimeException(
@@ -145,6 +145,22 @@ final class ResponseSignature
return $timestamp . ':' . $jsonBody; 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 private function isTimestampValid(int $timestamp): bool
{ {
$now = time(); $now = time();