198 lines
6.3 KiB
C#
198 lines
6.3 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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é.
|
|
/// </summary>
|
|
public class EnrollmentQrCodecTests
|
|
{
|
|
private const string Kid = "test-kid-1";
|
|
|
|
private readonly string _publicKey;
|
|
private readonly string _privateKey;
|
|
private readonly IReadOnlyDictionary<string, string> _registry;
|
|
|
|
public EnrollmentQrCodecTests()
|
|
{
|
|
using var rsa = RSA.Create(2048);
|
|
_publicKey = rsa.ToXmlString(false);
|
|
_privateKey = rsa.ToXmlString(true);
|
|
_registry = new Dictionary<string, string> { [Kid] = _publicKey };
|
|
}
|
|
|
|
private string Sign(string code, string numero, string url)
|
|
=> EnrollmentQrCodec.Sign(
|
|
EnrollmentQrCodec.BuildPayloadJson(Kid, code, numero, url), _privateKey);
|
|
|
|
/// <summary>Signe un payload JSON arbitraire (pour forger des cas invalides).</summary>
|
|
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<string, string>
|
|
{
|
|
["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);
|
|
}
|
|
}
|