Compare commits
3
Commits
64d215cb26
...
v0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fc838ada7 | ||
|
|
a11aa4260a | ||
|
|
8062e1be77 |
No files matched your search
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.2.0] - 2026-01-26
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- SSRF protection with URL validation and private IP range blocking
|
||||||
|
- `allowInsecureHttp` constructor parameter for development environments
|
||||||
|
- Input validation in all DTO `fromArray()` methods
|
||||||
|
- DateTime exception handling in DTOs
|
||||||
|
- Recursive key sorting in `ResponseSignature` for nested objects
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Key derivation now uses RFC 5869 compliant `hash_hkdf()` instead of custom HMAC
|
||||||
|
- Exception messages sanitized to prevent information disclosure
|
||||||
|
- Header normalization treats empty values as null
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- JSON encoding error handling in `ResponseSignature::buildSignaturePayload()`
|
||||||
|
- Header normalization null risk in `SecureLicenseClient`
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Comprehensive security audit performed
|
||||||
|
- SSRF vulnerability mitigated
|
||||||
|
- Information disclosure in error messages fixed
|
||||||
|
- Improved cryptographic key derivation
|
||||||
|
|
||||||
## [0.1.0] - 2026-01-22
|
## [0.1.0] - 2026-01-22
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -29,9 +29,13 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
|
|||||||
|
|
||||||
No known bugs at the moment
|
No known bugs at the moment
|
||||||
|
|
||||||
### Version 0.2.0
|
### Version 0.2.1
|
||||||
|
|
||||||
No changes at the moment.
|
No pending tasks at the moment.
|
||||||
|
|
||||||
|
### Version 0.3.0
|
||||||
|
|
||||||
|
No pending tasks at the moment.
|
||||||
|
|
||||||
## Technical Stack
|
## Technical Stack
|
||||||
|
|
||||||
@@ -202,3 +206,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
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in new issue
Block a user