logger = $logger ?? new NullLogger(); $this->encoder = $encoder ?? new StringEncoder(); $this->apiPath = $this->encoder->decode(self::ENCODED_API_PATH); if ($this->verifyIntegrity) { $this->checkIntegrity(); } } public function validate(string $licenseKey, string $domain): LicenseInfo { $cacheKey = $this->buildCacheKey('validate', $licenseKey, $domain); if ($this->cache !== null) { $item = $this->cache->getItem($cacheKey); if ($item->isHit()) { $this->logger->debug('License validation cache hit', ['domain' => $domain]); return $item->get(); } } $this->logger->info('Validating license', ['domain' => $domain]); $endpoint = $this->encoder->decode(self::ENCODED_VALIDATE); $response = $this->secureRequest($endpoint, [ 'license_key' => $licenseKey, 'domain' => $domain, ], $licenseKey); $result = LicenseInfo::fromArray($response['license']); if ($this->cache !== null) { $item = $this->cache->getItem($cacheKey); $item->set($result); $item->expiresAfter($this->cacheTtl); $this->cache->save($item); } $this->logger->info('License validated successfully', [ 'domain' => $domain, 'product_id' => $result->productId, ]); return $result; } public function status(string $licenseKey): LicenseStatus { $cacheKey = $this->buildCacheKey('status', $licenseKey); if ($this->cache !== null) { $item = $this->cache->getItem($cacheKey); if ($item->isHit()) { $this->logger->debug('License status cache hit'); return $item->get(); } } $this->logger->info('Checking license status'); $endpoint = $this->encoder->decode(self::ENCODED_STATUS); $response = $this->secureRequest($endpoint, [ 'license_key' => $licenseKey, ], $licenseKey); $result = LicenseStatus::fromArray($response); if ($this->cache !== null) { $item = $this->cache->getItem($cacheKey); $item->set($result); $item->expiresAfter($this->cacheTtl); $this->cache->save($item); } $this->logger->info('License status retrieved', [ 'status' => $result->status->value, 'valid' => $result->valid, ]); return $result; } public function activate(string $licenseKey, string $domain): ActivationResult { $this->logger->info('Activating license', ['domain' => $domain]); $endpoint = $this->encoder->decode(self::ENCODED_ACTIVATE); $response = $this->secureRequest($endpoint, [ 'license_key' => $licenseKey, 'domain' => $domain, ], $licenseKey); $result = ActivationResult::fromArray($response); // Invalidate related cache entries after activation if ($this->cache !== null) { $this->cache->deleteItem($this->buildCacheKey('validate', $licenseKey, $domain)); $this->cache->deleteItem($this->buildCacheKey('status', $licenseKey)); } $this->logger->info('License activation completed', [ 'domain' => $domain, 'success' => $result->success, ]); return $result; } /** * @throws LicenseException * @throws SignatureException */ private function secureRequest(string $endpoint, array $payload, string $licenseKey): array { $url = rtrim($this->baseUrl, '/') . $this->apiPath . '/' . $endpoint; try { $response = $this->httpClient->request('POST', $url, [ 'json' => $payload, 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], ]); $statusCode = $response->getStatusCode(); $data = $response->toArray(false); $headers = $this->normalizeHeaders($response->getHeaders(false)); // Verify response signature $this->verifySignature($data, $headers, $licenseKey); if ($statusCode >= 400) { $this->logger->warning('License API error response', [ 'endpoint' => $endpoint, 'status_code' => $statusCode, 'error' => $data['error'] ?? 'unknown', ]); throw LicenseException::fromApiResponse($data, $statusCode); } return $data; } catch (LicenseException | SignatureException $e) { throw $e; } catch (\Throwable $e) { $this->logger->error('License API request failed', [ 'endpoint' => $endpoint, 'error' => $e->getMessage(), ]); throw new LicenseException( 'Failed to communicate with license server: ' . $e->getMessage(), null, 0, $e ); } } private function verifySignature(array $data, array $headers, string $licenseKey): void { $signature = ResponseSignature::extractSignature($headers); $timestamp = ResponseSignature::extractTimestamp($headers); if ($signature === null || $timestamp === null) { $this->logger->warning('Response missing signature headers'); throw new SignatureException('Response is not signed by the server'); } $verifier = ResponseSignature::fromLicenseKey($licenseKey, $this->serverSecret); if (!$verifier->verify($data, $signature, $timestamp)) { $this->logger->warning('Response signature verification failed'); throw new SignatureException(); } $this->logger->debug('Response signature verified'); } private function normalizeHeaders(array $headers): array { $normalized = []; foreach ($headers as $name => $values) { // HTTP client returns arrays of values; take the first one $normalized[$name] = is_array($values) ? ($values[0] ?? '') : $values; } return $normalized; } private function checkIntegrity(): void { $criticalFiles = [ 'src/SecureLicenseClient.php', 'src/Security/ResponseSignature.php', 'src/Security/SignatureException.php', ]; // In production, these hashes would be embedded // This is a placeholder showing the mechanism $hashes = $this->getExpectedHashes(); if (empty($hashes)) { $this->logger->debug('Integrity check skipped: no hashes configured'); return; } $basePath = dirname(__DIR__); $checker = new IntegrityChecker($hashes, $basePath); try { $checker->verify(); $this->logger->debug('Integrity check passed'); } catch (IntegrityException $e) { $this->logger->critical('Integrity check failed', [ 'failures' => $e->failures, ]); throw $e; } } /** * Get expected file hashes for integrity checking. * * In a production obfuscated build, these would be hardcoded. * Override this method or inject hashes via configuration. * * @return array */ protected function getExpectedHashes(): array { // Placeholder: in production, return actual hashes return []; } private function buildCacheKey(string $operation, string $licenseKey, ?string $domain = null): string { $key = 'wc_license_secure_' . $operation . '_' . hash('sha256', $licenseKey); if ($domain !== null) { $key .= '_' . hash('sha256', $domain); } return $key; } }