decode($parts[1]); $signature = $this->decode($parts[2]); // Derive the verification key from the same private seed used by LicenseSigner. // Keeping a separate SONARDESK_LICENSE_PUBLIC_KEY allowed the two keys to drift: // activation would sign a perfectly valid token with the new private key, then // /validate would reject that same token using a stale public key. $public = $this->publicKeyFromPrivateSeed(); if (! sodium_crypto_sign_verify_detached($signature, $payload, $public)) { throw new RuntimeException('Licence token signature is invalid.'); } $decoded = json_decode($payload, true, flags: JSON_THROW_ON_ERROR); return is_array($decoded) ? $decoded : throw new RuntimeException('Licence token payload is invalid.'); } private function publicKeyFromPrivateSeed(): string { $path = config('sonardesk.private_key_path'); if (! $path || ! is_file($path)) { throw new RuntimeException('SONARDESK_LICENSE_PRIVATE_KEY_PATH is not configured or does not exist.'); } $seed = base64_decode(trim((string) file_get_contents($path)), true); if ($seed === false || strlen($seed) !== SODIUM_CRYPTO_SIGN_SEEDBYTES) { throw new RuntimeException('The SonarDesk private key must be a Base64-encoded 32-byte Ed25519 seed.'); } $pair = sodium_crypto_sign_seed_keypair($seed); return sodium_crypto_sign_publickey($pair); } private function decode(string $value): string { $value = strtr($value, '-_', '+/'); $value .= str_repeat('=', (4 - strlen($value) % 4) % 4); $out = base64_decode($value, true); if ($out === false) { throw new RuntimeException('Invalid Base64URL value.'); } return $out; } }