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:
2026-05-01 11:27:55 +02:00
parent 558fa93f2c
commit 1a3910ef9d
4 changed files with 63 additions and 12 deletions

View File

@@ -203,8 +203,15 @@ public sealed class LicenseService : ILicenseService
return JsonSerializer.SerializeToUtf8Bytes(dict, opts);
}
private static string? FormatDateAtom(DateTime? d) =>
d?.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:sszzz");
/// <summary>
/// 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)
{