using System.Security.Cryptography; using System.Text; using System.Text.Json; using Omika.Core.Contracts.Mobile.Enrollment; using Omika.Core.Services.Mobile; namespace Omika.Tests.Mobile.Local; /// /// Tests du codec de QR d'inscription (signature RSA-SHA256 + décodage/vérification /// par kid). Utilise une paire de clés éphémère : aucun secret serveur embarqué. /// public class EnrollmentQrCodecTests { private const string Kid = "test-kid-1"; private readonly string _publicKey; private readonly string _privateKey; private readonly IReadOnlyDictionary _registry; public EnrollmentQrCodecTests() { using var rsa = RSA.Create(2048); _publicKey = rsa.ToXmlString(false); _privateKey = rsa.ToXmlString(true); _registry = new Dictionary { [Kid] = _publicKey }; } private string Sign(string code, string numero, string url) => EnrollmentQrCodec.Sign( EnrollmentQrCodec.BuildPayloadJson(Kid, code, numero, url), _privateKey); /// Signe un payload JSON arbitraire (pour forger des cas invalides). private string SignJson(string json) => EnrollmentQrCodec.Sign(json, _privateKey); private string Envelope(string payloadB64Url, string signatureB64Url) => EnrollmentQrCodec.Prefix + payloadB64Url + "." + signatureB64Url; private static string B64Url(string raw) => Convert.ToBase64String(Encoding.UTF8.GetBytes(raw)).TrimEnd('=').Replace('+', '-').Replace('/', '_'); // ===== Cas acceptés ===== [Fact] public void QrSigne_Accepte_CodeClient() { EnrollmentQrPayload? p = EnrollmentQrCodec.Decode(Sign("BATIK.01", "LIC-001", "https://licence.example.com"), _registry); Assert.NotNull(p); Assert.Equal("BATIK.01", p.CodeClient); } [Fact] public void QrSigne_Accepte_NumeroLicence() { EnrollmentQrPayload? p = EnrollmentQrCodec.Decode(Sign("BATIK.01", "LIC-001", "https://licence.example.com"), _registry); Assert.NotNull(p); Assert.Equal("LIC-001", p.NumeroLicence); } [Fact] public void QrSigne_Accepte_UrlLicence() { EnrollmentQrPayload? p = EnrollmentQrCodec.Decode(Sign("BATIK.01", "LIC-001", "https://licence.example.com"), _registry); Assert.NotNull(p); Assert.Equal("https://licence.example.com", p.UrlLicence); } // ===== Cas refusés ===== [Fact] public void PayloadModifie_Refuse() { string qr = Sign("BATIK.01", "LIC-001", "https://licence.example.com"); // Altération du contenu signé. Assert.Null(EnrollmentQrCodec.Decode(qr + "x", _registry)); } [Fact] public void SignatureModifiee_Refuse() { string qr = Sign("BATIK.01", "LIC-001", "https://licence.example.com"); int dot = qr.LastIndexOf('.'); string tampered = qr[..(dot + 1)] + "AAAA"; // signature corrompue Assert.Null(EnrollmentQrCodec.Decode(tampered, _registry)); } [Fact] public void MauvaiseCle_Refuse() { using var other = RSA.Create(2048); string qr = EnrollmentQrCodec.Sign( EnrollmentQrCodec.BuildPayloadJson(Kid, "BATIK.01", "LIC-001", "https://licence.example.com"), other.ToXmlString(true)); Assert.Null(EnrollmentQrCodec.Decode(qr, _registry)); } [Fact] public void Base64Invalide_Refuse() { Assert.Null(EnrollmentQrCodec.Decode(Envelope("!!!", "!!!"), _registry)); } [Fact] public void JsonInvalide_Refuse() { string qr = SignJson("{not-json"); Assert.Null(EnrollmentQrCodec.Decode(qr, _registry)); } [Fact] public void VersionInconnue_Refuse() { string json = JsonSerializer.Serialize(new { version = 99, keyId = Kid, codeClient = "BATIK.01", numeroLicence = "LIC-001", urlLicence = "https://licence.example.com" }); Assert.Null(EnrollmentQrCodec.Decode(SignJson(json), _registry)); } [Fact] public void CodeClientVide_Refuse() { string json = JsonSerializer.Serialize(new { version = 1, keyId = Kid, codeClient = "", numeroLicence = "LIC-001", urlLicence = "https://licence.example.com" }); Assert.Null(EnrollmentQrCodec.Decode(SignJson(json), _registry)); } [Fact] public void NumeroLicenceVide_Refuse() { string json = JsonSerializer.Serialize(new { version = 1, keyId = Kid, codeClient = "BATIK.01", numeroLicence = "", urlLicence = "https://licence.example.com" }); Assert.Null(EnrollmentQrCodec.Decode(SignJson(json), _registry)); } [Fact] public void UrlLicenceVide_Refuse() { string json = JsonSerializer.Serialize(new { version = 1, keyId = Kid, codeClient = "BATIK.01", numeroLicence = "LIC-001", urlLicence = "" }); Assert.Null(EnrollmentQrCodec.Decode(SignJson(json), _registry)); } [Fact] public void KidInconnu_Refuse() { using var other = RSA.Create(2048); string qr = EnrollmentQrCodec.Sign( EnrollmentQrCodec.BuildPayloadJson("autre-kid", "BATIK.01", "LIC-001", "https://licence.example.com"), other.ToXmlString(true)); Assert.Null(EnrollmentQrCodec.Decode(qr, _registry)); } [Fact] public void RotationKid_DeuxCles_VerifieAvecBonneCle() { // Simule une rotation : le registre contient l'ancienne ET la nouvelle clé. using var oldRsa = RSA.Create(2048); using var newRsa = RSA.Create(2048); var registry = new Dictionary { ["kid-old"] = oldRsa.ToXmlString(false), ["kid-new"] = newRsa.ToXmlString(false) }; string qrNew = EnrollmentQrCodec.Sign( EnrollmentQrCodec.BuildPayloadJson("kid-new", "BATIK.01", "LIC-001", "https://licence.example.com"), newRsa.ToXmlString(true)); EnrollmentQrPayload? p = EnrollmentQrCodec.Decode(qrNew, registry); Assert.NotNull(p); Assert.Equal("BATIK.01", p.CodeClient); } }