expectedProtocolPublicFingerprint !== null) { $actual = hash('sha256', $this->protocolPublicKeyPem); if (!hash_equals(strtolower($this->expectedProtocolPublicFingerprint), strtolower($actual))) { throw new RuntimeException('Pinned TrustAuth protocol public-key fingerprint mismatch'); } } } public function activate(string $installationFingerprint, ?string $domain = null, array $metadata = []): array { return $this->post('/licences/activate', $this->payload($installationFingerprint, $domain, $metadata)); } public function check(string $installationFingerprint, ?string $domain = null, array $metadata = []): array { return $this->post('/licences/check', $this->payload($installationFingerprint, $domain, $metadata)); } public function deactivate(string $installationFingerprint): array { return $this->post('/licences/deactivate', $this->payload($installationFingerprint, null, [])); } private function payload(string $fingerprint, ?string $domain, array $metadata): array { return [ 'product_uid' => $this->productUid, 'licence_key' => $this->licenceKey, 'installation_fingerprint' => $fingerprint, 'domain' => $domain, 'environment' => 'production', 'timestamp' => time(), 'nonce' => bin2hex(random_bytes(16)), 'metadata' => $metadata, ]; } private function post(string $path, array $payload): array { $headers = []; $ch = curl_init(rtrim($this->apiBase, '/') . $path); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES), CURLOPT_TIMEOUT => 20, CURLOPT_HEADERFUNCTION => static function ($curl, string $line) use (&$headers): int { $length = strlen($line); $parts = explode(':', $line, 2); if (count($parts) === 2) { $headers[strtolower(trim($parts[0]))] = trim($parts[1]); } return $length; }, ]); $body = curl_exec($ch); if ($body === false) { $error = curl_error($ch); curl_close($ch); throw new RuntimeException($error); } $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); $this->verifyResponse((string) $body, $headers); $json = json_decode((string) $body, true, 512, JSON_THROW_ON_ERROR); if ($status >= 400 || !is_array($json)) { throw new RuntimeException('TrustAuth request failed with HTTP ' . $status); } return $json; } private function verifyResponse(string $body, array $headers): void { $digest = strtolower((string) ($headers['x-content-sha256'] ?? '')); if ($digest === '' || !hash_equals($digest, hash('sha256', $body))) { throw new RuntimeException('TrustAuth response digest mismatch'); } if (($headers['x-csta-signature-algorithm'] ?? '') !== 'RSA-SHA384') { throw new RuntimeException('Unsupported TrustAuth signature algorithm'); } $keyId = (string) ($headers['x-csta-key-id'] ?? ''); if ($this->expectedProtocolKeyId !== null && !hash_equals($this->expectedProtocolKeyId, $keyId)) { throw new RuntimeException('Unexpected TrustAuth protocol key ID'); } $signature = base64_decode((string) ($headers['x-csta-signature'] ?? ''), true); if ($signature === false || openssl_verify($body, $signature, $this->protocolPublicKeyPem, OPENSSL_ALGO_SHA384) !== 1) { throw new RuntimeException('TrustAuth response signature verification failed'); } } }