Some checks are pending
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Blocked by required conditions
CI / Build og unit tests (push) Successful in 1m13s
deploy community / deploy (push) Successful in 0s
deploy hub / deploy (push) Successful in 20s
CI / MSI-installer (push) Successful in 43s
- trusted-keys.json: the public key from keygen (k20261001). The hub and every agent built from now on verify licenses with it; no TrustedKeys configuration is needed any more. - The test that required an empty key file now checks what it was guarding against: every embedded key must be keygen output (id k<yyyyMMdd>, P-256 public key), never a test key. - Agent: default Hub:Url https://hub.sqlinsight.seosoft.dk/ (was hub.seosoft.dk, which does not exist). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
137 lines
5.1 KiB
C#
137 lines
5.1 KiB
C#
using System.Security.Cryptography;
|
|
|
|
namespace SQLInsight.Licensing.Tests;
|
|
|
|
public class LicenseTests
|
|
{
|
|
private static readonly DateTimeOffset Now = new(2026, 9, 29, 12, 0, 0, TimeSpan.Zero);
|
|
|
|
private sealed class FixedTime(DateTimeOffset now) : TimeProvider
|
|
{
|
|
public override DateTimeOffset GetUtcNow() => now;
|
|
}
|
|
|
|
private static (ECDsa Key, TrustedKeys Trusted) NewKey(string kid = "test1")
|
|
{
|
|
var key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
|
return (key, new TrustedKeys([KeyValuePair.Create(kid, Convert.ToBase64String(key.ExportSubjectPublicKeyInfo()))]));
|
|
}
|
|
|
|
private static License Sample(DateTimeOffset expires, LicensePlan plan = LicensePlan.Professional, string kid = "test1") => new()
|
|
{
|
|
LicenseId = "lic-1", Customer = "Kunde A/S", Plan = plan, IssuedAt = Now.AddDays(-10), ExpiresAt = expires, MaxInstances = 5, KeyId = kid,
|
|
};
|
|
|
|
private static LicenseStatus Check(string text, TrustedKeys keys, DateTimeOffset? now = null) => new LicenseVerifier(keys, new FixedTime(now ?? Now)).Check(text);
|
|
|
|
[Fact]
|
|
public void Valid_license_round_trips()
|
|
{
|
|
var (key, trusted) = NewKey();
|
|
var text = SignedDocument.Sign(Sample(Now.AddYears(1)), key);
|
|
|
|
var status = Check(text, trusted);
|
|
|
|
Assert.StartsWith("SQLI1.", text, StringComparison.Ordinal);
|
|
Assert.Equal(LicenseState.Valid, status.State);
|
|
Assert.Equal("Kunde A/S", status.License!.Customer);
|
|
Assert.True(status.AllowsFindingsSync);
|
|
}
|
|
|
|
[Fact]
|
|
public void No_license_is_community_and_not_an_error()
|
|
{
|
|
var status = Check("", NewKey().Trusted);
|
|
|
|
Assert.Equal(LicenseState.None, status.State);
|
|
Assert.False(status.AllowsFindingsSync);
|
|
}
|
|
|
|
[Fact]
|
|
public void Tampered_payload_is_invalid()
|
|
{
|
|
var (key, trusted) = NewKey();
|
|
var parts = SignedDocument.Sign(Sample(Now.AddYears(1)), key).Split('.');
|
|
var forged = SignedDocument.Sign(Sample(Now.AddYears(10), LicensePlan.Enterprise), ECDsa.Create(ECCurve.NamedCurves.nistP256)).Split('.');
|
|
|
|
// Enterprise payload with the original, genuine signature.
|
|
Assert.Equal(LicenseState.Invalid, Check($"{parts[0]}.{forged[1]}.{parts[2]}", trusted).State);
|
|
}
|
|
|
|
[Fact]
|
|
public void License_signed_with_an_untrusted_key_is_invalid()
|
|
{
|
|
var (_, trusted) = NewKey();
|
|
var (other, _) = NewKey();
|
|
|
|
Assert.Equal(LicenseState.Invalid, Check(SignedDocument.Sign(Sample(Now.AddYears(1)), other), trusted).State);
|
|
}
|
|
|
|
[Fact]
|
|
public void Unknown_key_id_is_invalid()
|
|
{
|
|
var (key, trusted) = NewKey("test1");
|
|
|
|
Assert.Equal(LicenseState.Invalid, Check(SignedDocument.Sign(Sample(Now.AddYears(1), kid: "other"), key), trusted).State);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("garbage")]
|
|
[InlineData("SQLI1.x.y")]
|
|
[InlineData("SQLI1.@@@.###")]
|
|
[InlineData("SQLI2.a.b")]
|
|
public void Garbage_is_invalid_not_an_exception(string text)
|
|
{
|
|
Assert.Equal(LicenseState.Invalid, Check(text, NewKey().Trusted).State);
|
|
}
|
|
|
|
[Fact]
|
|
public void Expired_license_keeps_syncing_during_grace_then_stops()
|
|
{
|
|
var (key, trusted) = NewKey();
|
|
var text = SignedDocument.Sign(Sample(Now), key);
|
|
|
|
var grace = Check(text, trusted, Now.AddDays(10));
|
|
var expired = Check(text, trusted, Now.AddDays(31));
|
|
|
|
Assert.Equal(LicenseState.ExpiredInGrace, grace.State);
|
|
Assert.True(grace.AllowsFindingsSync);
|
|
Assert.Equal(LicenseState.Expired, expired.State);
|
|
Assert.False(expired.AllowsFindingsSync);
|
|
}
|
|
|
|
[Fact]
|
|
public void Community_plan_does_not_include_sync()
|
|
{
|
|
var (key, trusted) = NewKey();
|
|
|
|
Assert.False(Check(SignedDocument.Sign(Sample(Now.AddYears(1), LicensePlan.Community), key), trusted).AllowsFindingsSync);
|
|
}
|
|
|
|
[Fact]
|
|
public void Plans()
|
|
{
|
|
Assert.True(Sample(Now, LicensePlan.Enterprise).IncludesPhoneSupport);
|
|
Assert.False(Sample(Now, LicensePlan.Professional).IncludesPhoneSupport);
|
|
Assert.True(Sample(Now, LicensePlan.Professional).IncludesPrioritySupport);
|
|
}
|
|
|
|
[Fact]
|
|
public void Embedded_keys_are_vendor_public_keys()
|
|
{
|
|
// Only SEOSoft's keygen public keys may ship. A test key (whose private half is in a repository) would let
|
|
// anyone mint licenses, so every entry must look like keygen output: id k<yyyyMMdd> and a P-256 public key.
|
|
using var stream = typeof(TrustedKeys).Assembly.GetManifestResourceStream("SQLInsight.Licensing.trusted-keys.json")!;
|
|
using var document = System.Text.Json.JsonDocument.Parse(stream);
|
|
var keys = document.RootElement.GetProperty("keys").EnumerateArray().ToList();
|
|
|
|
Assert.Equal(keys.Count, TrustedKeys.Embedded().Count);
|
|
foreach (var entry in keys)
|
|
{
|
|
Assert.Matches(@"^k\d{8}$", entry.GetProperty("id").GetString());
|
|
using var key = ECDsa.Create();
|
|
key.ImportSubjectPublicKeyInfo(Convert.FromBase64String(entry.GetProperty("publicKey").GetString()!), out _);
|
|
Assert.Equal(256, key.KeySize);
|
|
}
|
|
}
|
|
}
|