59 lines
2.2 KiB
PHP
Executable File
59 lines
2.2 KiB
PHP
Executable File
<?php
|
|
|
|
namespace Modules\Licensing\Services;
|
|
|
|
use RuntimeException;
|
|
|
|
final class LicenseTokenVerifier
|
|
{
|
|
public function verify(string $token): array
|
|
{
|
|
$parts = explode('.', trim($token));
|
|
if (count($parts) !== 3 || $parts[0] !== 'RS3') {
|
|
throw new RuntimeException('Invalid licence token.');
|
|
}
|
|
$payload = $this->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;
|
|
}
|
|
}
|