license: force UTC + fixed-Z date format on both sides
Symptom: "Réponse serveur non authentifiée (signature invalide)" on
license activation. Root cause: PHP \DateTimeInterface::ATOM emits the
server's local timezone offset (e.g. +02:00 on a CET host), while the
C# canonicalizer emitted +00:00 after ToUniversalTime(). Same instant,
different string, different bytes, signature mismatch.
Server (ValidateLicense.php):
- All timestamps now built with `new DateTimeZone('UTC')` and formatted
as 'Y-m-d\TH:i:s\Z' — fixed string, no offset variation.
- Reads issued_at / download_entitlement_until from MySQL as UTC; the
display is consistent with what the client sees.
Client (LicenseService.cs):
- FormatDateAtom now produces "yyyy-MM-ddTHH:mm:ssZ" with literal Z and
handles null safely (previous version would have produced "Z" alone
for a null input thanks to string + null concatenation).
Both sides therefore agree on the canonical bytes for any datetime,
including across daylight savings transitions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -71,16 +71,22 @@ final class ValidateLicense
|
|||||||
)->execute([$lic['id'], $machineId, $machineLabel ?: null, $now, $now]);
|
)->execute([$lic['id'], $machineId, $machineLabel ?: null, $now, $now]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construit la réponse, signe-la, renvoie
|
// Construit la réponse, signe-la, renvoie.
|
||||||
$entUntil = (new \DateTimeImmutable($lic['download_entitlement_until']))->format(\DateTimeInterface::ATOM);
|
// Tous les timestamps sont émis en UTC + format figé "yyyy-MM-ddTHH:mm:ssZ"
|
||||||
$serverTime = (new \DateTimeImmutable('now'))->format(\DateTimeInterface::ATOM);
|
// pour que la canonicalisation côté client produise EXACTEMENT les mêmes bytes.
|
||||||
|
$utc = new \DateTimeZone('UTC');
|
||||||
|
$fmt = 'Y-m-d\TH:i:s\Z';
|
||||||
|
|
||||||
|
$issuedAt = (new \DateTimeImmutable($lic['issued_at'], $utc))->format($fmt);
|
||||||
|
$entUntil = (new \DateTimeImmutable($lic['download_entitlement_until'], $utc))->format($fmt);
|
||||||
|
$serverTime = (new \DateTimeImmutable('now', $utc))->format($fmt);
|
||||||
$expired = strtotime($lic['download_entitlement_until']) < time();
|
$expired = strtotime($lic['download_entitlement_until']) < time();
|
||||||
|
|
||||||
$payload = [
|
$payload = [
|
||||||
'status' => $expired ? 'expired' : 'valid',
|
'status' => $expired ? 'expired' : 'valid',
|
||||||
'licenseId' => 'lic_' . $lic['id'],
|
'licenseId' => 'lic_' . $lic['id'],
|
||||||
'ownerName' => $lic['owner_name'],
|
'ownerName' => $lic['owner_name'],
|
||||||
'issuedAt' => (new \DateTimeImmutable($lic['issued_at']))->format(\DateTimeInterface::ATOM),
|
'issuedAt' => $issuedAt,
|
||||||
'downloadEntitlementUntil' => $entUntil,
|
'downloadEntitlementUntil' => $entUntil,
|
||||||
'maxMachines' => (int)$lic['max_machines'],
|
'maxMachines' => (int)$lic['max_machines'],
|
||||||
'serverTime' => $serverTime,
|
'serverTime' => $serverTime,
|
||||||
|
|||||||
43
server/tools/config.example.php
Normal file
43
server/tools/config.example.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
// === À RENSEIGNER LORS DU DÉPLOIEMENT ===
|
||||||
|
// Copier ce fichier en config.php sur le serveur, puis le compléter.
|
||||||
|
// Le fichier config.php est gitignored.
|
||||||
|
|
||||||
|
return [
|
||||||
|
// Base path public sous lequel le launcher est servi
|
||||||
|
'base_url' => 'https://asterionvr.com/PS_Launcher',
|
||||||
|
|
||||||
|
// MySQL (depuis le manager OVH : Hébergements -> Bases de données)
|
||||||
|
'db' => [
|
||||||
|
// Format DSN OVH typique : mysql:host=<bdd>.mysql.db;dbname=<bdd>;charset=utf8mb4
|
||||||
|
'dsn' => 'mysql:host=localhost;dbname=replace_me;charset=utf8mb4',
|
||||||
|
'username' => 'replace_me',
|
||||||
|
'password' => 'replace_me',
|
||||||
|
],
|
||||||
|
|
||||||
|
// HMAC secret pour les URLs présignées de /builds/ (v0.6, optionnel)
|
||||||
|
// Génère 64 hex chars : `php -r "echo bin2hex(random_bytes(32));"`
|
||||||
|
'hmac_secret' => 'replace_with_random_64_bytes_hex',
|
||||||
|
|
||||||
|
// Clés Ed25519 pour signer la réponse de validation license et le manifest.
|
||||||
|
// Génère le keypair via : `php tools/generate-keypair.php`
|
||||||
|
// Recopie les hex strings ci-dessous, et embarque la public_key_hex dans le launcher.
|
||||||
|
'ed25519' => [
|
||||||
|
'private_key_hex' => '', // 128 hex chars (sodium secret key, contient la pub key)
|
||||||
|
'public_key_hex' => '', // 64 hex chars (32 bytes)
|
||||||
|
],
|
||||||
|
|
||||||
|
// (v0.4-β) JWT pour les URLs de download protégées (optionnel pour le moment)
|
||||||
|
'jwt_secret' => 'replace_with_random_secret',
|
||||||
|
'jwt_ttl_seconds' => 900, // 15 min
|
||||||
|
|
||||||
|
// Limites de validation
|
||||||
|
'validate_max_per_minute_per_ip' => 10,
|
||||||
|
|
||||||
|
// === BACKOFFICE ADMIN ===
|
||||||
|
// Mot de passe bcrypt pour la connexion à /PS_Launcher/admin/
|
||||||
|
// Génère le hash via SSH OVH :
|
||||||
|
// php -r "echo password_hash('motdepasse_choisi', PASSWORD_DEFAULT);"
|
||||||
|
// puis colle le résultat ci-dessous.
|
||||||
|
'admin_password_hash' => '',
|
||||||
|
];
|
||||||
@@ -203,8 +203,15 @@ public sealed class LicenseService : ILicenseService
|
|||||||
return JsonSerializer.SerializeToUtf8Bytes(dict, opts);
|
return JsonSerializer.SerializeToUtf8Bytes(dict, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? FormatDateAtom(DateTime? d) =>
|
/// <summary>
|
||||||
d?.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:sszzz");
|
/// Format figé "yyyy-MM-ddTHH:mm:ssZ" en UTC, identique à ce que produit côté serveur
|
||||||
|
/// PHP `(new DateTimeImmutable($s, new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z')`.
|
||||||
|
/// </summary>
|
||||||
|
private static string? FormatDateAtom(DateTime? d)
|
||||||
|
{
|
||||||
|
if (d is null) return null;
|
||||||
|
return d.Value.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss") + "Z";
|
||||||
|
}
|
||||||
|
|
||||||
public static bool VerifySignature(LicenseValidationResponse response, string base64Signature, string publicKeyHex)
|
public static bool VerifySignature(LicenseValidationResponse response, string base64Signature, string publicKeyHex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1 @@
|
|||||||
# Clé publique Ed25519 du serveur PS_Launcher.
|
a32cacfd6f3d52943c6403690fce44bab4933408f49827cb43fa0d3eda267a8f
|
||||||
# Génère-la via : `php tools/generate-keypair.php` sur le serveur OVH.
|
|
||||||
# Recopie ici les 64 hex chars de public_key_hex (rien d'autre, pas de commentaires sur la même ligne),
|
|
||||||
# puis recompile le launcher.
|
|
||||||
# Tant que ce fichier commence par '#' ou est vide, la vérification de signature
|
|
||||||
# côté client est skippée (le launcher accepte la réponse serveur sans vérifier).
|
|
||||||
Reference in New Issue
Block a user