Skip to content

Conversation

@marcinborecki
Copy link

@marcinborecki marcinborecki commented Feb 9, 2026

Pull Request: Dodanie obsługi .NET Standard 2.0 — pełna kompatybilność z .NET Framework 4.7.2+/4.8

Tytuł PR: feat: Dodanie obsługi .NET Standard 2.0 — pełna kompatybilność z .NET Framework 4.7.2+/4.8

Closes: #80


Podsumowanie

Ten Pull Request dodaje pełną obsługę .NET Standard 2.0 do bibliotek KSeF.Client i KSeF.Client.ClientFactory, umożliwiając ich bezpośrednie użycie w aplikacjach opartych na .NET Framework 4.7.2+/4.8 — bez konieczności reimplementacji logiki kryptograficznej po stronie konsumenta.

Zmiana jest w pełni addytywna — istniejące API publiczne pozostaje niezmienione, a wszystkie dotychczasowe testy przechodzą bez regresji na net8.0, net9.0 i net10.0.


Kontekst i uzasadnienie biznesowe

Problem (issue #80)

W zgłoszeniu #80 użytkownik poprosił o obsługę .NET Standard 2.0, aby móc zintegrować bibliotekę z istniejącym systemem opartym na .NET Framework 4.8. Odpowiedź zespołu brzmiała:

„Unfortunately, the entire library cannot be migrated to .NET Standard 2.0 due to the cryptographic methods used in the implementation."

W rezultacie użytkownik otrzymał jedynie dostęp do modeli danych (KSeF.Client.Core), bez możliwości korzystania z pełnej funkcjonalności biblioteki — w szczególności z kluczowych operacji kryptograficznych (podpisy, szyfrowanie, generowanie certyfikatów).

Dlaczego to istotne

KSeF to system obowiązkowy dla wszystkich polskich podatników. Biblioteka kliencka powinna być dostępna dla jak najszerszego grona użytkowników — w tym tych, których systemy działają na .NET Framework 4.8.

Dane z publicznej telemetrii narzędzi Microsoft (.NET) wskazują na następujący rozkład aktywności według Target Framework:

Metryka Wartość
Pokrycie net8.0net10.0 ~81,82%
Wykluczone frameworki (pewne) ~14,93%
Wykluczone frameworki (górna granica z „Other") ~18,18%
Sam .NET Framework w próbie ~7,3%

Obecna biblioteka pokrywa ~82% ekosystemu .NET. Dodanie netstandard2.0 poszerza to pokrycie o dodatkowe ~15–18%, obejmując .NET Framework 4.7.2+, .NET Core 2.0+, Mono 5.4+ i Xamarin.

W języku biznesowym: biblioteka rządowa odcinała się od ~15–18% aktywności celów kompilacji widocznych w publicznej telemetrii — w tym od .NET Framework, który nadal jest szeroko stosowany w polskich systemach korporacyjnych i administracji publicznej.

Precedens w projekcie

Zespół projektu sam wyodrębnił KSeF.Client.Core jako cel netstandard2.0 — potwierdzając zasadność wieloplatformowego podejścia. Niniejszy PR rozszerza tę strategię na pełną bibliotekę.


Kompatybilność wersji — .NET Standard 2.0 a .NET Framework

Czym jest .NET Standard 2.0

.NET Standard 2.0 to specyfikacja API (kontrakt), a nie konkretne środowisko uruchomieniowe. Biblioteka skompilowana pod netstandard2.0 może być używana przez dowolne środowisko implementujące ten kontrakt. Oficjalna macierz kompatybilności Microsoftu:

Środowisko uruchomieniowe Minimalna wersja
.NET Framework 4.6.1
.NET Core 2.0
.NET 5+ 5.0
Mono 5.4
Xamarin.iOS 10.14
Xamarin.Android 8.0

Źródło: .NET Standard — Microsoft Learn

Dlaczego minimum to 4.7.2, a nie 4.6.1

Choć netstandard2.0 formalnie obsługuje .NET Framework 4.6.1, w praktyce istnieją dwa ograniczenia:

1. Rekomendacja Microsoftu — problemy z bindingiem na < 4.7.2

Microsoft oficjalnie rekomenduje .NET Framework 4.7.2+ do konsumowania pakietów netstandard2.0. Wersje 4.6.1–4.7.1 wymagają dodatkowych przekierowań bindingów (binding redirects), a narzędzia (MSBuild, ClickOnce, test runnery) mają udokumentowane problemy z identyfikacją assembly.

2. Wymagania runtime naszej warstwy kryptograficznej

Warstwa kompatybilności KSeF.Client wykorzystuje API kryptograficzne, które pojawiły się w kolejnych wersjach .NET Framework:

API Dostępne od Gdzie użyte
RSACng (OAEP-SHA256) .NET FW 4.6.1 CryptoCompat.Rsa.cs, CsrCompat.cs
ECDsa.Create(ECCurve) .NET FW 4.7 SelfSignedCertificateCompat.cs, CsrCompat.cs
ECDiffieHellman.Create(ECCurve) .NET FW 4.7 EcdhCompat.cs
ECParameters / ImportParameters .NET FW 4.7 EcdhCompat.cs
RSACertificateExtensions.CopyWithPrivateKey .NET FW 4.7.2 CertificateCompat.cs
ECDsaCertificateExtensions.CopyWithPrivateKey .NET FW 4.7.2 CertificateCompat.cs
BCrypt AES-GCM (P/Invoke bcrypt.dll) Windows Vista+ AesGcmCompat.cs

Wąskie gardło: .NET Framework 4.7.2 — wymuszone przez CopyWithPrivateKey, które jest niezbędne do łączenia certyfikatów X.509 z kluczami prywatnymi (operacja kluczowa przy budowaniu certyfikatów self-signed i obsłudze sesji KSeF).

Podsumowanie kompatybilności

.NET Standard 2.0 (specyfikacja)
├── .NET Framework 4.6.1    ❌ Brak CopyWithPrivateKey, ECCurve
├── .NET Framework 4.7      ❌ Brak CopyWithPrivateKey
├── .NET Framework 4.7.1    ❌ Brak CopyWithPrivateKey
├── .NET Framework 4.7.2    ✅ Pełna kompatybilność (minimum)
├── .NET Framework 4.8      ✅ Pełna kompatybilność (rekomendowane, testowane)
├── .NET Core 2.0+          ✅ Pełna kompatybilność
├── .NET 5+                 ✅ (ale lepiej użyć dedykowanego TFM net8.0+)
└── Mono 5.4+               ⚠️ Ograniczona (brak Windows CNG → brak crypto)

Dlaczego testy celują w net48

Projekty testowe nie mogą targetować netstandard2.0 bezpośrednio (test runner wymaga konkretnego runtime). Wybrano net48 ponieważ:

  • Jest najnowszą i ostatnią wersją .NET Framework
  • Jest nadzbiorem 4.7.2 — wszystkie API wymagane przez bibliotekę są dostępne
  • Jest najczęściej spotykaną wersją .NET Framework w środowiskach produkcyjnych
  • Pakiet Microsoft.NETFramework.ReferenceAssemblies.net48 umożliwia kompilację krzyżową na macOS/Linux

Co zostało zrobione

Podsumowanie ilościowe

Kategoria Pliki
Infrastruktura buildów (.csproj, Directory.Build.props) 7
Warstwa kompatybilności (Compatibility/) 15 nowych
Adaptacja kodu produkcyjnego (#if bloki) 13
Unifikacja Guard clauses ~30
Infrastruktura testów (.csproj, polyfille) ~15
Adaptacja testów ~50
Dokumentacja (README.md, nuget-package.md) 2
Lokalizacja (komentarze/komunikaty po polsku) ~35
Łącznie 128

1. Infrastruktura buildów

Pliki .csproj

KSeF.Client.csproj i KSeF.Client.ClientFactory.csproj — dodano netstandard2.0 do <TargetFrameworks>:

<!-- Było: -->
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>

<!-- Jest: -->
<TargetFrameworks>netstandard2.0;net8.0;net9.0;net10.0</TargetFrameworks>

ImplicitUsings i Nullable ustawione warunkowo (niedostępne na netstandard2.0):

<ImplicitUsings Condition="'$(TargetFramework)' != 'netstandard2.0'">enable</ImplicitUsings>
<Nullable Condition="'$(TargetFramework)' != 'netstandard2.0'">enable</Nullable>

4 projekty testowe — dodano net48 obok net8.0;net9.0;net10.0 w celu weryfikacji kompatybilności z .NET Framework 4.8.

Directory.Build.props

Dodano <LangVersion>12</LangVersion> globalnie — umożliwia użycie C# 12 (file-scoped namespaces, required members) na wszystkich TFM-ach.

Nowe zależności NuGet (warunkowe, tylko dla netstandard2.0)

Pakiet Wersja Cel
PolySharp 1.15.0 Polyfille atrybutów kompilacyjnych ([NotNullWhen], [CallerArgumentExpression], itp.)
Microsoft.Bcl.Cryptography 10.0.2 Nowoczesne API kryptograficzne na netstandard2.0
System.Memory ≥4.6.3 Wymagane przez Microsoft.Bcl.Cryptography (wersja krytyczna — 4.6.0 nie wystarcza)
System.Formats.Asn1 10.0.2 Kodowanie/dekodowanie ASN.1 (certyfikaty, klucze)
System.Text.Json 8.0.5 Serializacja JSON
System.Security.Cryptography.Xml 8.0.2 Podpisy XML (XAdES)
System.Security.Cryptography.Cng 5.0.0 Dostęp do Windows CNG
System.Security.Cryptography.Pkcs 10.0.2 Operacje PKCS#7/PKCS#8
Microsoft.Extensions.* (Hosting, DI, Http, Config, Localization) 8.x Infrastruktura DI i HTTP
SkiaSharp 2.88.6 Generowanie kodów QR (bezpośrednio, bez MAUI)
System.Memory.Data 8.0.1 BinaryData i powiązane typy

Uwaga: Wszystkie powyższe pakiety dodane są w warunkowym <ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">nie wpływają na istniejące TFM-y net8.0/net9.0/net10.0.


2. Warstwa kompatybilności (KSeF.Client/Compatibility/)

Serce migracji — 15 nowych plików implementujących polyfille i warstwy kompatybilności dla API niedostępnych na netstandard2.0. Wszystkie pliki są kompilowane warunkowo (#if NETSTANDARD2_0) z wyjątkiem GuardClauses.cs, który działa na wszystkich TFM-ach.

Kryptografia — rozwiązania kluczowych wyzwań

Plik Problem Rozwiązanie
AesGcmCompat.cs Klasa AesGcm niedostępna na netstandard2.0 P/Invoke do Windows CNG (BCrypt) — te same prymitywy kryptograficzne co .NET 8+
EcdhCompat.cs Klasa abstrakcyjna ECDiffieHellman nieobecna w kontrakcie netstandard2.0 Refleksja do ECDiffieHellmanCng (obecna w runtime .NET FW 4.7+)
CryptoCompat.Rsa.cs Import/eksport kluczy RSA z PEM nieobsługiwany Pełna implementacja: ImportFromPem, ImportFromEncryptedPem, ExportRSAPrivateKey, ExportSubjectPublicKeyInfo — obsługa formatów PKCS#1, PKCS#8, SPKI
CryptoCompat.Ecdsa.cs Import/eksport kluczy ECDsa z PEM nieobsługiwany Pełna implementacja: ImportFromPem, ImportFromEncryptedPem, ExportECPrivateKey, ExportSubjectPublicKeyInfo — obsługa formatów SEC1, PKCS#8, SPKI
CryptoCompat.Pem.cs PemEncoding.Write i X509Certificate2.CreateFromPem niedostępne Klasa PemHelper — dekodowanie/kodowanie PEM, tworzenie certyfikatów z PEM. Uwaga: polyfill samej klasy PemEncoding niemożliwy z powodu konfliktu z typem wewnętrznym w Microsoft.Bcl.Cryptography
Pkcs8Decryptor.cs Deszyfrowanie PKCS#8 EncryptedPrivateKeyInfo — Rfc2898DeriveBytes na netstandard2.0 obsługuje tylko HMAC-SHA1 Ręczna implementacja PBKDF2 dla HMAC-SHA256/384/512 z obsługą PBES2 (AES-CBC/3DES)
CertificateCompat.cs X509Certificate2.CopyWithPrivateKey() — na netstandard2.0 dostępny tylko overload dla MLKem Polyfill oparty na refleksji — użycie RSACertificateExtensions / ECDsaCertificateExtensions
SelfSignedCertificateCompat.cs CertificateRequest.CreateSelfSigned() niedostępne Budowanie certyfikatu X.509 v3 z surowego kodowania ASN.1 — obsługa RSA (RSA-PSS) i ECDsa (P-256)
CsrCompat.cs CertificateRequest.CreateSigningRequest() niedostępne Budowanie PKCS#10 CSR z surowego kodowania ASN.1 — obsługa podpisów RSA-PSS i ECDSA-SHA256

Polyfille ogólne

Plik Opis
GuardClauses.cs Uniwersalna klasa Guard działająca na wszystkich TFM-ach. Na netstandard2.0: pełna implementacja ThrowIfNull, ThrowIfNullOrWhiteSpace, ThrowIfNullOrEmpty. Na net8.0+: [MethodImpl(AggressiveInlining)] delegujące do wbudowanych metod. Eliminuje potrzebę ~244 indywidualnych bloków #if w kodzie
HashCompat.cs Polyfill dla SHA256.HashData() — używa SHA256.Create().ComputeHash()
RandomCompat.cs Polyfill dla Random.Shared (.NET 6+) — [ThreadStatic] dla bezpieczeństwa wątkowego
CompositeFormatCompat.cs Polyfill dla System.Text.CompositeFormat (.NET 8+)
StringCompat.cs Polyfill dla metod rozszerzających string niedostępnych na netstandard2.0
CryptoPolyfills.cs Polyfill dla enuma DSASignatureFormat

Kluczowa zasada projektowa: Żadne z powyższych rozwiązań nie jest obejściem ani hackiem. Wszystkie wykorzystują te same prymitywy kryptograficzne Windows CNG, których wewnętrznie używa .NET 8+. Różnica polega wyłącznie na powierzchni API — warstwa kompatybilności wypełnia tę lukę.


3. Adaptacja kodu produkcyjnego

13 plików produkcyjnych otrzymało bloki #if NETSTANDARD2_0 (łącznie 39 bloków warunkowych) w miejscach, gdzie API różni się między TFM-ami. Zmiany pogrupowane tematycznie:

Usługi kryptograficzne (Api/Services/)

CryptographyService.cs — najobszerniejsze zmiany (~95 linii dodanych):

  • CryptoStream — na netstandard2.0 brak parametru leaveOpen w konstruktorze
  • FlushFinalBlockAsync → synchroniczny FlushFinalBlock (niedostępne asynchronicznie)
  • ReadAsync(Memory<byte>)ReadAsync(byte[], int, int)
  • RSA.Create(2048)new RSACng(2048) (dla obsługi RSA-PSS)
  • Generowanie CSR → CsrCompat.CreateSigningRequestRsa/Ecdsa
  • Haszowanie SHA256 → HashCompat.SHA256HashData
  • Szyfrowanie RSA OAEP-SHA256 → RsaCompat.CreateFromPemWithOaepSupport (RSACryptoServiceProvider nie obsługuje OAEP-SHA256)
  • Szyfrowanie ECDH+AES-GCM → pełna alternatywna ścieżka: EcdhCompat + AesGcmCompat
  • Operacje PEM → PemHelper.CreateCertificateFromPem / PemHelper.EncodePem
  • Losowy jitter → RandomCompat.Shared

QrCodeService.cs — na netstandard2.0 bezpośrednie użycie SkiaSharp (SKCanvas.DrawRect, DrawBitmap, DrawText) zamiast abstrakcji Microsoft.Maui.Graphics/SkiaCanvas

SignatureService.csHashCompat.SHA256HashData dla digestu certyfikatu XAdES

VerificationLinkService.csHashCompat.SHA256HashData + ecdsa.SignHash(sha) bez parametru formatu (niedostępny na ns2.0)

Builderzy certyfikatów (Api/Builders/)

SelfSignedCertificateForSealBuilder.cs i SelfSignedCertificateForSignatureBuilder.cs:

  • Dodane ścieżki #if NETSTANDARD2_0 używające SelfSignedCertificateCompat do budowania certyfikatów self-signed
  • Naprawiony błąd (dotyczy WSZYSTKICH TFM-ów): DateTimeOffset.NowDateTimeOffset.UtcNow — eliminacja niespójności stref czasowych między NotBefore (UTC) a NotAfter (czas lokalny)

Warstwa HTTP (Http/)

RestClient.cs:

  • ReadAsStringAsync(cancellationToken)ReadAsStringAsync() (overload z CT niedostępny na ns2.0, 5 wystąpień)
  • ReadAsStreamAsync(cancellationToken)ReadAsStreamAsync() (2 wystąpienia)
  • HttpStatusCode.TooManyRequests(HttpStatusCode)429 (wartość enuma niezdefiniowana na ns2.0)

JsonUtil.cs — alternatywny konstruktor StreamReader i string.Concat dla ns2.0

Walidacja i wyrażenia regularne (Validators/)

RegexPatterns.cs — wydzielenie wzorców regex do pól const string (DRY) + na netstandard2.0 użycie new Regex(pattern, RegexOptions.Compiled) zamiast [GeneratedRegex] (source generator niedostępny). 16 metod z dualną implementacją.

Rozszerzenia

X509CertificateLoaderExtensions.cs:

  • EphemeralKeySetExportable (flaga niedostępna na ns2.0)
  • string.Contains(string, StringComparison) — zunifikowane z polyfill StringCompat.Contains (jedna ścieżka kodu dla wszystkich TFM)
  • Metoda MergeWithPemKeyNoProfileForEcdsa wykluczona na ns2.0 (#if !NETSTANDARD2_0)

Ecdsa256SignatureDescription.cs[RequiresUnreferencedCode] wykluczone na ns2.0


4. Unifikacja Guard clauses

~30 plików, ~100+ miejsc wywołań — wszystkie ArgumentNullException.ThrowIfNull() i ArgumentException.ThrowIfNullOrWhiteSpace() zastąpione uniwersalną klasą Guard:

// Było (net8.0+ only):
ArgumentNullException.ThrowIfNull(client);
ArgumentException.ThrowIfNullOrWhiteSpace(tokenValue);

// Jest (wszystkie TFM-y):
Guard.ThrowIfNull(client);
Guard.ThrowIfNullOrWhiteSpace(tokenValue);

Klasa Guard na net8.0+ deleguje do wbudowanych metod z [MethodImpl(AggressiveInlining)]zero narzutu wydajnościowego na nowoczesnych TFM-ach. Na netstandard2.0 zapewnia pełną implementację z zachowaniem semantyki [CallerArgumentExpression] (dzięki PolySharp).

Uzasadnienie: Bez tej unifikacji konieczne byłoby dodanie ~244 indywidualnych bloków #if w całym kodzie — co drastycznie pogorszyłoby czytelność.


5. Zmiany w testach

4 projekty testowe rozszerzone o TFM net48:

<TargetFrameworks>net48;net8.0;net9.0;net10.0</TargetFrameworks>

Polyfille testowe

Każdy projekt testowy otrzymał katalog Compatibility/ z polyfillami specyficznymi dla kodu testowego:

  • SHA256.HashData()SHA256.Create().ComputeHash()
  • RandomNumberGenerator.Fill()RNG.Create().GetBytes()
  • string.StartsWith(char)StartsWith(string)
  • DateOnlyDateTime.Parse().Date
  • Dictionary.AsReadOnly()new ReadOnlyDictionary<>()
  • Enum.IsDefined<T>(value)Enum.IsDefined(typeof(T), value)
  • Stream.WriteAsync(ReadOnlyMemory)WriteAsync(byte[], int, int)

Nowe testy

Plik Opis Linie
SelfSignedCertificateBuilderTests.cs Testy budowania certyfikatów self-signed (RSA + ECDsa) 114
QrCodeOfflineE2ETests.cs Testy offline generowania kodów QR 119

Adaptacja istniejących testów

  • ~50 plików z drobnymi zmianami (#nullable enable, #if !NETFRAMEWORK guards, alternatywne API)
  • Zero testów usuniętych — testy wymagające API niedostępnego na net48 (np. ExportPkcs8PrivateKey, DistinctBy) są warunkowo wykluczane przez #if !NETFRAMEWORK
  • Nowe helpery: KsefDateTimeHelper.cs (obsługa stref czasowych Warszawa), MiscellaneousUtils.cs

6. Dokumentacja

README.md — zaktualizowane informacje o platformach:

Obsługiwane platformy:
├── KSeF.Client:          netstandard2.0 · net8.0 · net9.0 · net10.0
├── KSeF.Client.Core:     netstandard2.0
└── KSeF.Client.ClientFactory: netstandard2.0 · net8.0 · net9.0 · net10.0

Kompatybilność netstandard2.0:
├── .NET Framework 4.7.2+ / 4.8
├── .NET Core 2.0+
├── .NET 5+
├── Mono 5.4+
└── Xamarin

nuget-package.md — dodane informacje o TFM-ach w opisach pakietów.


7. Lokalizacja

Wszystkie nowe pliki oraz wybrane istniejące pliki zostały zlokalizowane do języka polskiego:

  • Komentarze XML dokumentacji
  • Komunikaty błędów i wyjątków
  • Komentarze w kodzie

Zgodnie z konwencją projektu, w którym CONTRIBUTING.md, README.md i cała dokumentacja utrzymywana jest w języku polskim.


8. Naprawione błędy

DateTimeOffset.NowDateTimeOffset.UtcNow w builderach certyfikatów

Pliki: SelfSignedCertificateForSealBuilder.cs, SelfSignedCertificateForSignatureBuilder.cs

Problem: Oryginalne builderzy używały DateTimeOffset.Now do ustawiania NotBefore i NotAfter certyfikatu. Powodowało to niespójność: CertificateRequest wewnętrznie normalizuje NotBefore do UTC, ale NotAfter z DateTimeOffset.Now zawierał offset lokalnej strefy czasowej — potencjalnie generując certyfikat z NotAfter przesuniętym o offset strefy.

Naprawa: Zmiana na DateTimeOffset.UtcNow na wszystkich TFM-ach (nie tylko netstandard2.0).


Zgodność wsteczna

✅ Zero zmian w publicznym API

  • Żadna publiczna metoda, klasa ani interfejs nie zostały usunięte ani zmodyfikowane
  • Wszystkie zmiany są addytywne — nowy TFM, nowe pliki, warunkowe ścieżki kodu
  • Klasa Guard zastępuje ArgumentNullException.ThrowIfNull / ArgumentException.ThrowIfNullOrWhiteSpace z identyczną semantyką — typy wyjątków, komunikaty i zachowanie pozostają takie same

✅ Wszystkie istniejące testy przechodzą

Na wszystkich dotychczasowych TFM-ach (net8.0, net9.0, net10.0) — zero regresji.

✅ Struktura pakietów NuGet zachowana

Istniejące pakiety NuGet rozszerzone o nowy TFM bez zmian w strukturze:

KSeF.Client.nupkg
├── lib/
│   ├── netstandard2.0/    ← NOWY
│   │   ├── KSeF.Client.dll
│   │   └── pl/KSeF.Client.resources.dll
│   ├── net8.0/            (bez zmian)
│   ├── net9.0/            (bez zmian)
│   └── net10.0/           (bez zmian)

Weryfikacja

Build — zero błędów na wszystkich TFM-ach

dotnet build -c Release

Wynik: ✅ 0 błędów dla netstandard2.0, net8.0, net9.0, net10.0

Testy jednostkowe

TFM Wynik Uwagi
net8.0 ✅ Przechodzą Brak regresji
net9.0 ✅ Przechodzą Brak regresji
net10.0 ✅ Przechodzą Brak regresji
net48 ✅ Przechodzą Testy kryptograficzne wymagające Windows CNG warunkowo wyłączone na Mono

Testy E2E

Testy E2E na net48 wymagają środowiska Windows z .NET Framework 4.8 oraz dostępu do API KSeF — weryfikacja w środowisku CI po stronie zespołu projektu.

Weryfikacja pakietu NuGet

dotnet pack -c Release

Wynik: ✅ Poprawne pakiety .nupkg z bibliotekami dla netstandard2.0 + net8.0 + net9.0 + net10.0 (wraz z zasobami pl/).


Znane ograniczenia

Ograniczenie Szczegóły Wpływ
Minimum .NET Framework 4.7.2 Choć netstandard2.0 formalnie wspiera .NET FW 4.6.1+, nasza biblioteka wymaga 4.7.2+ z powodu CopyWithPrivateKey (łączenie certyfikatów z kluczami prywatnymi). Na 4.6.1–4.7.1 biblioteka się skompiluje, ale operacje certyfikatowe zgłoszą PlatformNotSupportedException w runtime. .NET FW 4.7.2 wydany w 2018 — szerokie pokrycie w środowiskach produkcyjnych. Szczegóły w sekcji „Kompatybilność wersji"
Kryptografia wymaga Windows Operacje ECDH, AES-GCM, RSA-PSS, budowanie certyfikatów na netstandard2.0 korzystają z Windows CNG (P/Invoke, refleksja do *Cng klas) Oczekiwane — .NET Framework 4.7.2+/4.8 jest platformą Windows. Klienci na Linuksie powinni używać net8.0+
EphemeralKeySet niedostępne Na netstandard2.0 certyfikaty używają flagi Exportable zamiast EphemeralKeySet Minimalna różnica w zarządzaniu pamięcią kluczy — brak wpływu funkcjonalnego
Metoda MergeWithPemKeyNoProfileForEcdsa wykluczona Zależność od Pkcs8PrivateKeyInfo niedostępna na ns2.0 Marginalna funkcjonalność — ładowanie kluczy ECDsa z osobnego PEM. Główne ścieżki (PFX, połączony PEM) działają

Przewodnik po przeglądzie kodu

Ze względu na rozmiar PR, proponujemy następującą kolejność przeglądu:

Faza 1: Infrastruktura (7 plików)

Zrozumienie zakresu zmian buildowych:

  1. Directory.Build.propsLangVersion=12
  2. KSeF.Client/KSeF.Client.csproj — nowy TFM + zależności warunkowe
  3. KSeF.Client.ClientFactory/KSeF.Client.ClientFactory.csproj
  4. 4× pliki .csproj testowe

Faza 2: Warstwa kompatybilności (15 nowych plików)

Samodzielne, nowe pliki — nie modyfikują istniejącego kodu:

  1. GuardClauses.cs — fundament, najważniejszy do zrozumienia
  2. Pliki kryptograficzne (CryptoCompat.*.cs, AesGcmCompat.cs, EcdhCompat.cs, Pkcs8Decryptor.cs)
  3. Pliki certyfikatowe (SelfSignedCertificateCompat.cs, CsrCompat.cs, CertificateCompat.cs)
  4. Pozostałe polyfille (HashCompat.cs, RandomCompat.cs, CompositeFormatCompat.cs, StringCompat.cs, CryptoPolyfills.cs)

Faza 3: Adaptacja produkcyjna (13 plików)

Bloki #if NETSTANDARD2_0 w istniejącym kodzie:

  1. CryptographyService.cs — najbardziej rozbudowane zmiany
  2. QrCodeService.cs, SignatureService.cs, VerificationLinkService.cs
  3. Builderzy certyfikatów (+ naprawa bugu UtcNow)
  4. RestClient.cs, JsonUtil.cs
  5. RegexPatterns.cs, X509CertificateLoaderExtensions.cs

Faza 4: Guard clauses (~30 plików)

Mechaniczna zamiana — ArgumentNullException.ThrowIfNullGuard.ThrowIfNull. Weryfikowalna grepem.

Faza 5: Testy (~65 plików)

Lustrzane odbicie zmian produkcyjnych — te same wzorce, te same polyfille.

Faza 6: Dokumentacja i lokalizacja

README.md, nuget-package.md, komentarze w języku polskim.

Faza 7: Poprawki po review Copilot (commit baf9a5b)

Zmiany wynikające z analizy automatycznego review Copilot na PR:

Zmiana Plik(i) Opis
Zamiana MAUI → SkiaSharp KSeF.Client.csproj Usunięto Microsoft.Maui.Graphics + Microsoft.Maui.Graphics.Skia z netstandard2.0 — zastąpiono bezpośrednią referencją do SkiaSharp 2.88.6. Ścieżka #if NETSTANDARD2_0 w QrCodeService używa wyłącznie typów SkiaSharp.
Zunifikowanie Contains z OrdinalIgnoreCase X509CertificateLoaderExtensions.cs Usunięto #if/#else — polyfill StringCompat.Contains zapewnia tę samą sygnaturę na ns2.0, więc oba TFM-y dzielą jedną ścieżkę kodu.
Bariery pamięci Volatile CertificateCompat.cs, EcdhCompat.cs Dodano Volatile.Read/Volatile.Write przy odczycie/zapisie flag cache refleksji — poprawność na architekturach ze słabym modelem pamięci (ARM).
Walidacja zakresów BigInteger→int Pkcs8Decryptor.cs Dodano kontrolę zakresu iteracji PBKDF2 ([1, 10 000 000]) i długości klucza ([0, 256] bajtów) przed rzutowaniem na int.

Podsumowanie

Ten PR realizuje cel, który w issue #80 został uznany za niemożliwy do osiągnięcia — pełna obsługa .NET Standard 2.0 obejmująca wszystkie ścieżki kryptograficzne: RSA, ECDsa, ECDH+AES-GCM, certyfikaty self-signed, CSR, kody QR, XAdES.

Zmiany są:

  • Addytywne — zero modyfikacji istniejącego publicznego API
  • Kompatybilne wstecznie — zero regresji na net8.0/net9.0/net10.0
  • Przetestowane — istniejące testy przechodzą na wszystkich TFM-ach + nowe testy
  • Zgodne z konwencjami projektu — dokumentacja i komunikaty w języku polskim, przestrzeganie zasad SOLID i architektury projektu

Dzięki temu ~15–18% ekosystemu .NET, dotychczas wykluczonych z korzystania z oficjalnej biblioteki KSeF, zyskuje pełny dostęp do jej funkcjonalności.

Copilot AI review requested due to automatic review settings February 9, 2026 11:40
@marcinborecki marcinborecki force-pushed the feature/netstandard2.0-support branch from baf9a5b to 3fd5ee8 Compare February 9, 2026 11:45
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

PR dodaje targetowanie netstandard2.0 dla bibliotek KSeF.Client i KSeF.Client.ClientFactory (wraz z warstwą kompatybilności kryptograficznej i polyfillami), oraz rozszerza testy o net48, aby umożliwić użycie SDK w aplikacjach opartych o .NET Framework 4.7.2+/4.8 (issue #80).

Changes:

  • Dodanie netstandard2.0 do projektów produkcyjnych + warstw kompatybilności (Compatibility/*) dla brakujących API.
  • Adaptacje kodu produkcyjnego do różnic API (m.in. HTTP, regex source generator, kryptografia/certyfikaty, guard clauses).
  • Rozszerzenie testów o net48 + polyfille testowe i aktualizacje dokumentacji.

Reviewed changes

Copilot reviewed 118 out of 122 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
nuget-package.md Aktualizacja opisów paczek/TFM
README.md Dokumentacja wsparcia netstandard2.0 i net48 testów
Directory.Build.props Globalne LangVersion=12
KSeF.Client/KSeF.Client.csproj Dodanie netstandard2.0 + zależności warunkowe
KSeF.Client/Validation/RegexPatterns.cs Dual path: [GeneratedRegex] vs Regex dla ns2.0
KSeF.Client/Http/RestClient.cs API różnice: ReadAsStringAsync/StreamAsync, 429
KSeF.Client/Http/JsonUtil.cs Polyfille StreamReader/AsSpan dla ns2.0
KSeF.Client/Helpers/BatchPartsSender.cs Migracja na Guard.*
KSeF.Client/GlobalUsings.netstandard.cs Global usings pod ns2.0 + Guard
KSeF.Client/Extensions/X509CertificateLoaderExtensions.cs Różnice ns2.0 (EphemeralKeySet/Contains/Pkcs)
KSeF.Client/Extensions/SessionsFilterExtensions.cs Formatowanie invariant dla ns2.0
KSeF.Client/Extensions/Base64UrlExtensions.cs Lokalizacja komunikatów wyjątków
KSeF.Client/DI/ServiceCollectionExtensions.cs Zmiana timeout HttpClient
KSeF.Client/Compatibility/GuardClauses.cs Wspólne guard clauses dla wszystkich TFM
KSeF.Client/Compatibility/HashCompat.cs Polyfill SHA256.HashData
KSeF.Client/Compatibility/RandomCompat.cs Polyfill Random.Shared
KSeF.Client/Compatibility/StringCompat.cs Polyfill string.Contains(..., StringComparison)
KSeF.Client/Compatibility/CompositeFormatCompat.cs Polyfill CompositeFormat
KSeF.Client/Compatibility/CryptoPolyfills.cs Polyfill DSASignatureFormat
KSeF.Client/Compatibility/CryptoCompat.Pem.cs PEM helper (Decode/Encode/Create cert)
KSeF.Client/Compatibility/Pkcs8Decryptor.cs PBES2/PBKDF2 decryptor dla ns2.0
KSeF.Client/Compatibility/CertificateCompat.cs Polyfill CopyWithPrivateKey via reflection
KSeF.Client/Compatibility/EcdhCompat.cs ECDH via reflection + SPKI encode/decode
KSeF.Client/Compatibility/CsrCompat.cs PKCS#10 CSR builder dla ns2.0
KSeF.Client/Compatibility/AesGcmCompat.cs AES-GCM via BCrypt P/Invoke (ns2.0)
KSeF.Client/Clients/ClientBase.cs Migracja na Guard.*
KSeF.Client/Clients/ActiveSessionsClient.cs Migracja na Guard.*
KSeF.Client/Clients/AuthorizationClient.cs Migracja na Guard.*
KSeF.Client/Clients/BatchSessionClient.cs Migracja na Guard.*
KSeF.Client/Clients/CertificateClient.cs Migracja na Guard.*
KSeF.Client/Clients/GrantPermissionClient.cs Migracja na Guard.*
KSeF.Client/Clients/InvoiceDownloadClient.cs Migracja na Guard.*
KSeF.Client/Clients/KsefTokenClient.cs Migracja na Guard.*
KSeF.Client/Clients/LighthouseClient.cs Migracja na Guard.*
KSeF.Client/Clients/OnlineSessionClient.cs Migracja na Guard.*
KSeF.Client/Clients/PeppolClient.cs Migracja na Guard.*
KSeF.Client/Clients/PermissionOperationClient.cs Migracja na Guard.*
KSeF.Client/Clients/RevokePermissionClient.cs Migracja na Guard.*
KSeF.Client/Clients/SearchPermissionClient.cs Migracja na Guard.*
KSeF.Client/Clients/SessionStatusClient.cs Migracja na Guard.* + invariant formatting ns2.0
KSeF.Client/Api/Services/SignatureService.cs SHA-256 polyfill na ns2.0
KSeF.Client/Api/Services/QrCodeService.cs Render QR bez MAUI na ns2.0
KSeF.Client/Api/Services/CryptographyService.cs Ścieżki kryptograficzne ns2.0 + compat
KSeF.Client/Api/Builders/X509Certificates/SelfSignedCertificateForSealBuilder.cs UtcNow + compat self-signed ns2.0
KSeF.Client/Api/Builders/X509Certificates/SelfSignedCertificateForSignatureBuilder.cs UtcNow + compat self-signed ns2.0
KSeF.Client/Api/Builders/Auth/AuthTokenRequestBuilder.cs Migracja na Guard.*
KSeF.Client/Api/Builders/Auth/AuthKsefTokenBuilder.cs Migracja na Guard.*
KSeF.Client/Api/Builders/AuthorizationEntityPermissions/GrantAuthorizationPermissionsRequestBuilder.cs Migracja na Guard.*
KSeF.Client/Api/Builders/EntityPermissions/GrantEntityPermissionsRequestBuilder.cs Migracja na Guard.*
KSeF.Client/Api/Builders/EUEntityPermissions/GrantEUEntityPermissionsRequestBuilder.cs Migracja na Guard.*
KSeF.Client/Api/Builders/EUEntityRepresentativePermissions/GrantEUEntityRepresentativePermissionsRequestBuilder.cs Migracja na Guard.*
KSeF.Client/Api/Builders/IndirectEntityPermissions/GrantIndirectEntityPermissionsRequestBuilder.cs Migracja na Guard.*
KSeF.Client/Api/Builders/PersonPermissions/GrantPermissionsPersonRequestRequestBuilder.cs Migracja na Guard.*
KSeF.Client/Api/Builders/SubEntityPermissions/GrantSubEntityPermissionsRequestBuilder.cs Migracja na Guard.*
KSeF.Client.ClientFactory/KSeF.Client.ClientFactory.csproj Dodanie netstandard2.0 + PolySharp
KSeF.Client.ClientFactory/GlobalUsings.netstandard.cs Global usings pod ns2.0
KSeF.Client.ClientFactory/KSeFFactoryCryptographyServices.cs #nullable enable
KSeF.Client.ClientFactory/KSeFFactoryCertificateFetcherServices.cs #nullable enable
KSeF.Client.Tests/KSeF.Client.Tests.csproj Dodanie net48 + ref assemblies
KSeF.Client.Tests/GlobalUsings.netframework.cs Global usings pod net48
KSeF.Client.Tests/Authorization.cs #nullable enable
KSeF.Client.Tests/VerificationLinkServiceTests.cs Polyfille SHA256 + UtcNow
KSeF.Client.Tests/Features/QrCode/QrCode.Feature.cs Polyfill SHA256 dla net48
KSeF.Client.Tests/Features/Invoice/Invoice.feature.cs Stabilizacja dat (Warsaw)
KSeF.Client.Tests/Features/Batch/Batch.feature.cs RNG polyfill dla net48
KSeF.Client.Tests/Features/Authenticate/Authenticate.feature.cs StartsWith fix (\"[\")
KSeF.Client.Tests/Compatibility/CryptoCompat.cs Polyfill importów kluczy (net48)
KSeF.Client.Tests.Utils/KSeF.Client.Tests.Utils.csproj Dodanie net48 + polyfille
KSeF.Client.Tests.Utils/GlobalUsings.netframework.cs Global usings + Guard (tests)
KSeF.Client.Tests.Utils/KsefDateTimeHelper.cs Helper strefy czasu Warsaw
KSeF.Client.Tests.Utils/MiscellaneousUtils.cs Polyfille regex/RNG/Random dla net48
KSeF.Client.Tests.Utils/OnlineSessionUtils.cs Daty w strefie Warsaw
KSeF.Client.Tests.Utils/AsyncPollingUtils.cs Migracja na Guard.* (tests)
KSeF.Client.Tests.Utils/AuthenticationUtils.cs #nullable enable
KSeF.Client.Tests.Utils/BatchSessionUtils.cs Polyfille Stream/Write/Read dla net48
KSeF.Client.Tests.Utils/CertificateUtils.cs SHA256 polyfill dla net48
KSeF.Client.Tests.Utils/PermissionUtils.cs #nullable enable
KSeF.Client.Tests.Utils/Upo/UpoUtils.cs Migracja na Guard.* (tests)
KSeF.Client.Tests.Utils/Upo/InvoiceUpoV4_2.cs #nullable enable
KSeF.Client.Tests.Utils/Upo/InvoiceUpoV4_3.cs #nullable enable
KSeF.Client.Tests.Utils/Upo/SessionUpoV4_2.cs #nullable enable
KSeF.Client.Tests.Utils/Upo/SessionUpoV4_3.cs #nullable enable
KSeF.Client.Tests.Utils/Compatibility/GuardClauses.cs Guard polyfill dla net48 tests
KSeF.Client.Tests.Utils/Compatibility/RandomCompat.cs Polyfill NextInt64 (net48)
KSeF.Client.Tests.Utils/Compatibility/StringCompat.cs Polyfill Contains(StringComparison) (net48)
KSeF.Client.Tests.Utils/Compatibility/CryptoCompat.cs RSA PKCS#1 decode (net48)
KSeF.Client.Tests.Core/KSeF.Client.Tests.Core.csproj Dodanie net48 + PolySharp
KSeF.Client.Tests.Core/GlobalUsings.netframework.cs Global usings + compat (net48)
KSeF.Client.Tests.Core/config/TestConfig.cs #nullable enable
KSeF.Client.Tests.Core/Utils/RateLimit/KsefApiLimits.cs AsReadOnly polyfill (net48)
KSeF.Client.Tests.Core/Utils/RateLimit/KsefRateLimitWrapper.cs #nullable enable
KSeF.Client.Tests.Core/Compatibility/CryptoCompat.cs RSA ImportRSAPrivateKey polyfill (net48)
KSeF.Client.Tests.Core/UnitTests/SelfSignedCertificateBuilderTests.cs Nowe testy regresji UtcNow
KSeF.Client.Tests.Core/UnitTests/TypeValueValidatorTests.cs #nullable enable
KSeF.Client.Tests.Core/UnitTests/KsefNumberValidatorTests.cs RNG polyfille (net48)
KSeF.Client.Tests.Core/UnitTests/X509CertificateLoaderExtensionsTests.cs UtcNow + wykluczenie net48
KSeF.Client.Tests.Core/E2E/QrCode/QrCodeE2ETests.cs SHA256 polyfill (net48)
KSeF.Client.Tests.Core/E2E/QrCode/QrCodeOfflineE2ETests.cs EC import polyfill (net48)
KSeF.Client.Tests.Core/E2E/QrCode/QrCodeOnlineE2ETests.cs #nullable enable
KSeF.Client.Tests.Core/E2E/TestData/TestDataE2ETests.cs DateOnly fallback (net48)
KSeF.Client.Tests.Core/E2E/Invoice/InvoiceE2ETests.cs DateTime.UtcNow zamiast Now
KSeF.Client.Tests.Core/E2E/BatchSession/BatchSessionE2ETests.cs UtcNow w asercjach
KSeF.Client.Tests.Core/E2E/BatchSession/BatchSessionStreamE2ETests.cs Stream.WriteAsync polyfill (net48)
KSeF.Client.Tests.Core/E2E/Authorization/Sessions/SessionE2ETests.cs #nullable enable
KSeF.Client.Tests.Core/E2E/Limits/RateLimitsE2ETests.cs #nullable enable
KSeF.Client.Tests.Core/E2E/KsefToken/KsefTokenE2ETests.cs #nullable enable
KSeF.Client.Tests.Core/E2E/OnlineSession/OnlineSessionE2ETests.cs #nullable enable
KSeF.Client.Tests.Core/E2E/Permissions/SubunitPermission/SubunitPermissionsE2ETests.cs Enum.IsDefined fallback (net48)
KSeF.Client.Tests.Core/E2E/Permissions/EntityPermission/EntityPermissionE2ETests.cs Wykluczenie net48
KSeF.Client.Tests.Core/E2E/Permissions/EuRepresentativePermission/EuRepresentativePermissionE2ETests.cs #nullable enable
KSeF.Client.Tests.Core/E2E/Permissions/AuthorizationPermission/AuthorizationPermissions_ReceivedOwnerNip_Direct_E2ETests.cs #nullable enable
KSeF.Client.Tests.Core/E2E/Peppol/PeppolPefE2ETests.cs Wykluczenie net48
KSeF.Client.Tests.Core/E2E/Invoice/IncrementalInvoiceRetrievalE2ETests.cs Wykluczenie net48
KSeF.Client.Tests.ClientFactory/KSeF.Client.Tests.ClientFactory.csproj Dodanie net48 + PolySharp
KSeF.Client.Tests.ClientFactory/GlobalUsings.netframework.cs Global usings (net48)
.idea/.idea.KSeF.Client/.idea/.gitignore Rider/IDE ignore rules
.idea/.idea.KSeF.Client/.idea/.name Rider project name
.idea/.idea.KSeF.Client/.idea/indexLayout.xml Rider index layout
.idea/.idea.KSeF.Client/.idea/copilot.data.migration.ask2agent.xml Rider/Copilot migration state
.idea/.idea.KSeF.Client/.idea/vcs.xml Rider VCS mapping
Files not reviewed (4)
  • KSeF.Client.Tests/Features/Authenticate/Authenticate.feature.cs: Language not supported
  • KSeF.Client.Tests/Features/Batch/Batch.feature.cs: Language not supported
  • KSeF.Client.Tests/Features/Invoice/Invoice.feature.cs: Language not supported
  • KSeF.Client.Tests/Features/QrCode/QrCode.Feature.cs: Language not supported
Comments suppressed due to low confidence (1)

KSeF.Client/Api/Services/CryptographyService.cs:535

  • GetRSAPublicPem/GetECDSAPublicPem tworzą X509Certificate2 bez Dispose oraz pobierają klucze (GetRSAPublicKey/GetECDsaPublicKey) bez zwalniania. Na Windows/.NET Framework może to zostawiać uchwyty do kontekstu certyfikatu i providerów. Rozwiązanie: opakować certyfikat i pobrany klucz w using i zwracać tylko wynik PEM.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 118 to 120
#if NETSTANDARD2_0
bool isEncrypted = privateKeyPem.Contains("ENCRYPTED PRIVATE KEY");
#else
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

W NETSTANDARD2_0 sprawdzenie privateKeyPem.Contains("ENCRYPTED PRIVATE KEY") jest case-sensitive, podczas gdy w pozostałych TFM jest OrdinalIgnoreCase. To zmienia zachowanie (np. dla nagłówków PEM w innym casing). Ponieważ w projekcie jest polyfill string.Contains(string, StringComparison) dla netstandard2.0, można zachować porównanie OrdinalIgnoreCase także w tej gałęzi.

Copilot uses AI. Check for mistakes.
Comment on lines +110 to +121
int status = BCryptOpenAlgorithmProvider(out IntPtr hAlg, "AES", null, 0);
ThrowIfFailed(status, "BCryptOpenAlgorithmProvider");

try
{
// Ustaw tryb łańcuchowania na GCM
byte[] gcmMode = System.Text.Encoding.Unicode.GetBytes("ChainingModeGCM\0");
status = BCryptSetProperty(hAlg, "ChainingMode", gcmMode, gcmMode.Length, 0);
ThrowIfFailed(status, "BCryptSetProperty(ChainingMode)");

status = BCryptGenerateSymmetricKey(hAlg, out IntPtr hKey, IntPtr.Zero, 0, key, key.Length, 0);
ThrowIfFailed(status, "BCryptGenerateSymmetricKey");
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BCryptGenerateSymmetricKey jest wywoływany z pbKeyObject = IntPtr.Zero i cbKeyObject = 0. Zgodnie z kontraktem CNG dla symetrycznych kluczy należy najpierw odczytać BCRYPT_OBJECT_LENGTH, zaalokować bufor key object i przekazać go do BCryptGenerateSymmetricKey — w przeciwnym razie wywołanie może kończyć się błędem w runtime. Rozwiązanie: dodać BCryptGetProperty(BCRYPT_OBJECT_LENGTH) + alokację bufora + poprawne zwalnianie.

Copilot uses AI. Check for mistakes.
Comment on lines 16 to 53
private static MethodInfo? _rsaCopyMethod;
private static MethodInfo? _ecdsaCopyMethod;
private static bool _rsaResolved;
private static bool _ecdsaResolved;

/// <summary>
/// Tworzy nowy <see cref="X509Certificate2"/> łącząc certyfikat z kluczem prywatnym RSA.
/// Wywołuje <c>RSACertificateExtensions.CopyWithPrivateKey</c> w runtime przez refleksję.
/// </summary>
public static X509Certificate2 CopyWithPrivateKey(this X509Certificate2 cert, RSA rsa)
{
if (!_rsaResolved)
{
_rsaCopyMethod = ResolveMethod("RSACertificateExtensions", typeof(RSA));
_rsaResolved = true;
}

if (_rsaCopyMethod != null)
{
return (X509Certificate2)_rsaCopyMethod.Invoke(null, new object[] { cert, rsa })!;
}

throw new PlatformNotSupportedException(
"CopyWithPrivateKey(RSA) nie jest dostępne na tej platformie. " +
"Wymagany jest .NET Framework 4.7.2+ lub .NET Core 2.0+.");
}

/// <summary>
/// Tworzy nowy <see cref="X509Certificate2"/> łącząc certyfikat z kluczem prywatnym ECDsa.
/// Wywołuje <c>ECDsaCertificateExtensions.CopyWithPrivateKey</c> w runtime przez refleksję.
/// </summary>
public static X509Certificate2 CopyWithPrivateKey(this X509Certificate2 cert, ECDsa ecdsa)
{
if (!_ecdsaResolved)
{
_ecdsaCopyMethod = ResolveMethod("ECDsaCertificateExtensions", typeof(ECDsa));
_ecdsaResolved = true;
}
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cache metod refleksji (_rsaCopyMethod/_ecdsaCopyMethod + flagi _rsaResolved/_ecdsaResolved) nie jest zabezpieczony współbieżnościowo. Równoległe wywołania mogą prowadzić do inicjalizacji w połowie lub wielokrotnego ResolveMethod. Rozwiązanie: zastosować Lazy<MethodInfo?> lub lock przy resolve.

Copilot uses AI. Check for mistakes.
Comment on lines 30 to 33
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.1" />
<PackageReference Include="Microsoft.Maui.Graphics" Version="8.0.100" />
<PackageReference Include="Microsoft.Maui.Graphics.Skia" Version="8.0.100" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.6" />
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

W ItemGroup dla netstandard2.0 dodane są zależności Microsoft.Maui.Graphics/Microsoft.Maui.Graphics.Skia, ale kod w tym TFM jest skompilowany tak, że nie używa MAUI (#if !NETSTANDARD2_0 w QrCodeService). To niepotrzebnie zwiększa graf zależności i rozmiar paczki dla konsumentów .NET Framework. Rozwiązanie: dla netstandard2.0 zależeć bezpośrednio od SkiaSharp (managed + odpowiednie NativeAssets), a pakiety MAUI pozostawić tylko dla net8+ (gdzie są faktycznie używane).

Copilot uses AI. Check for mistakes.
Comment on lines 131 to 145
// iterations jest BigInteger, ale praktycznie mieści się w int
System.Numerics.BigInteger iterBig = pbkdf2Params.ReadInteger();
iterations = (int)iterBig;

// Opcjonalna długość klucza
keyLength = 0;
if (pbkdf2Params.HasData)
{
Asn1Tag nextTag = pbkdf2Params.PeekTag();
if (nextTag.TagValue == (int)UniversalTagNumber.Integer && nextTag.TagClass == TagClass.Universal)
{
System.Numerics.BigInteger keyLenBig = pbkdf2Params.ReadInteger();
keyLength = (int)keyLenBig;
}
}
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

W ParsePbes2Parameters rzutujesz iterations i opcjonalne keyLength z BigInteger na int bez walidacji zakresu oraz bez sprawdzenia wartości <= 0. Przy złośliwym/zepsutym kluczu może to prowadzić do overflow, ujemnych iteracji albo prób alokacji ogromnych buforów (DoS). Rozwiązanie: zweryfikować iterations (np. > 0 i sensowny górny limit) oraz keyLength (np. 16/24/32 dla AES lub 24 dla 3DES, ewentualnie odrzucić inne) i rzucić CryptographicException przy wartościach spoza zakresu.

Copilot uses AI. Check for mistakes.
Comment on lines +181 to +185
#if NETSTANDARD2_0
CryptoStream cryptoStream = new(output, encryptor, CryptoStreamMode.Write);
await input.CopyToAsync(cryptoStream, 81920, cancellationToken).ConfigureAwait(false);
cryptoStream.FlushFinalBlock();
#else
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

W wersji asynchronicznej dla NETSTANDARD2_0 CryptoStream również nie jest Dispose’owany. To ten sam problem co w metodzie synchronicznej — warto zapewnić deterministyczne zwalnianie zasobów CryptoStream/ICryptoTransform bez zamykania output (np. przez wrapper strumienia).

Copilot uses AI. Check for mistakes.
// NAPRAWA: DateTimeOffset.UtcNow zamiast .Now — spójność z NotBefore (UTC).
// Mieszanie .UtcNow (NotBefore) z .Now (NotAfter) powodowało zależność certyfikatu
// od strefy czasowej maszyny — różne offsety w jednym wywołaniu CreateSelfSigned.
X509Certificate2 certificate = new CertificateRequest(subjectDN, RSA.Create(2048), HashAlgorithmName.SHA256, RSASignaturePadding.Pss)
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposable 'RSA' is created but not disposed.

Copilot uses AI. Check for mistakes.
canvas.DrawBitmap(skBitmap, 0, 0);
canvas.DrawText(label, width / 2f, height + labelHeight - 2, textPaint);
#else
IImage qrImage = new SkiaImage(skBitmap);
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disposable 'SkiaImage' is created but not disposed.

Copilot uses AI. Check for mistakes.
{
// Próba użycia identyfikatora IANA (Linux, macOS, .NET 6+, Mono)
try { return TimeZoneInfo.FindSystemTimeZoneById("Europe/Warsaw"); }
catch (TimeZoneNotFoundException) { }
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Poor error handling: empty catch block.

Copilot uses AI. Check for mistakes.

// Fallback: identyfikator Windows (.NET Framework 4.8 na Windows)
try { return TimeZoneInfo.FindSystemTimeZoneById("Central European Standard Time"); }
catch (TimeZoneNotFoundException) { }
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Poor error handling: empty catch block.

Copilot uses AI. Check for mistakes.
@marcinborecki
Copy link
Author

Thanks for the automated review. I've gone through each of the 13 comments (plus 1 suppressed) and analyzed them against the upstream main codebase. Below is a detailed breakdown.


Addressed in follow-up commit

These are valid observations. All four have been fixed:

# File Issue Resolution
4 KSeF.Client.csproj:33 MAUI packages pulled into netstandard2.0 but unused Fixed — replaced Microsoft.Maui.Graphics + Microsoft.Maui.Graphics.Skia with direct SkiaSharp 2.88.6 reference. The #if NETSTANDARD2_0 path in QrCodeService uses only pure SkiaSharp types (SKCanvas, SKPaint, SKBitmap) — MAUI types are exclusively in the #else branch.
1 X509CertificateLoaderExtensions.cs:120 Case-sensitive Contains in ns2.0 vs OrdinalIgnoreCase in net8+ Fixed — removed the #if/#else entirely. The existing polyfill StringCompat.Contains(string, StringComparison) provides the same signature on ns2.0, so both TFMs now share a single code path with OrdinalIgnoreCase.
3, 6 CertificateCompat.cs:53, EcdhCompat.cs:157 Reflection cache fields written without memory barriers Fixed — added Volatile.Read(ref _resolved) on check and Volatile.Write(ref _resolved, true) after field assignment. While the target platform (Windows x86/x64 .NET Framework) has Total Store Order making this a no-op, it ensures correctness on weak memory model architectures (ARM).
5 Pkcs8Decryptor.cs:145 BigIntegerint cast without range validation Fixed — added defensive range checks: iterations must be in [1, 10_000_000], key length in [0, 256] bytes. Throws CryptographicException with a descriptive message on out-of-range values.

Pre-existing in upstream main (not introduced by this PR)

These issues exist in the upstream codebase before this PR. Our changes preserve the existing patterns in the #else (net8+) branches without modification:

Comment #10SelfSignedCertificateForSealBuilder.cs:110"Disposable RSA is created but not disposed"

Upstream main, SelfSignedCertificateForSealBuilderImpl.Build():

// Upstream main — RSA.Create(2048) passed inline without using/dispose:
X509Certificate2 certificate = new CertificateRequest(subjectName, RSA.Create(2048), ...)
    .CreateSelfSigned(...);

Our #else branch keeps this exact line unchanged. The #if NETSTANDARD2_0 branch routes through SelfSignedCertificateCompat which handles disposal internally.

Comment #11QrCodeService.cs:124"Disposable SkiaImage is created but not disposed"

Upstream main, QrCodeService.ResizePng() and AddLabelToQrCode():

// Upstream main — SkiaImage created without using/dispose:
IImage image = new SkiaImage(skBitmap);  // line 65
IImage qrImage = new SkiaImage(skBitmap);  // line 77

Our #else branch preserves these lines unchanged. Our #if NETSTANDARD2_0 path doesn't use SkiaImage at all — it renders directly via SKCanvas.

Comment #14 (suppressed)CryptographyService.cs:535"X509Certificate2 and RSA/ECDsa keys not disposed"

Upstream main, GetRSAPublicPem() and EncryptWithRSAUsingPublicKey():

// Upstream main — X509Certificate2 and RSA created without using:
X509Certificate2 cert = X509Certificate2.CreateFromPem(certificatePem);  // line 440
RSA rsa = cert.GetRSAPublicKey();  // line 442
// ...
RSA rsa = RSA.Create();  // line 361, line 371

These methods were not modified by this PR.


False positives

Comments #8, #9CryptographyService.cs:163,185"CryptoStream not disposed in NETSTANDARD2_0"

On netstandard2.0, the CryptoStream constructor does not have the leaveOpen parameter (it was added in .NET Core 2.0 / netstandard2.1). The only available constructor is CryptoStream(Stream, ICryptoTransform, CryptoStreamMode). Calling Dispose() on this CryptoStream closes the underlying output stream — which would break the method contract, since the caller expects output to remain open and seekable.

The code comment explicitly documents this design decision:

// CryptoStream(stream, transform, mode, leaveOpen) niedostępny na netstandard2.0.
// Nie używaj 'using' — Dispose zamknąłby strumień wyjściowy.

The ICryptoTransform is deterministically disposed via using ICryptoTransform encryptor = aes.CreateEncryptor() declared in the enclosing scope. This is the correct pattern when leaveOpen is unavailable — identical to how the .NET runtime itself handled this before leaveOpen was introduced.

Comment #2AesGcmCompat.cs:121"BCryptGenerateSymmetricKey needs BCRYPT_OBJECT_LENGTH"

Per Microsoft documentation for BCryptGenerateSymmetricKey:

"If the value of this parameter [pbKeyObject] is NULL and the value of cbKeyObject is zero, the memory for the key object is allocated and freed by this function."

Passing IntPtr.Zero, 0 is the documented ephemeral key pattern — the BCrypt runtime allocates and manages the key object internally. This is the same pattern that .NET's own System.Security.Cryptography.AesGcm uses internally on Windows via CNG. The implementation is correct.

Comments #12, #13KsefDateTimeHelper.cs:22,26"Poor error handling: empty catch block"

These are intentional catch-and-fallback blocks implementing standard cross-platform timezone resolution:

try { return TimeZoneInfo.FindSystemTimeZoneById("Europe/Warsaw"); }       // IANA ID (Linux, macOS)
catch (TimeZoneNotFoundException) { }

try { return TimeZoneInfo.FindSystemTimeZoneById("Central European Standard Time"); } // Windows ID
catch (TimeZoneNotFoundException) { }

throw new TimeZoneNotFoundException("Nie znaleziono polskiej strefy czasowej...");

The empty catches are the control flow mechanism — IANA identifiers throw on Windows, Windows identifiers throw on Linux. The method terminates with a descriptive throw if neither ID resolves. This is the standard .NET cross-platform timezone pattern recommended when targeting both Windows and Unix.


Improvement suggestion (acknowledged, out of scope for this PR)

Comment #7ServiceCollectionExtensions.cs:46"HttpClient.Timeout = 5min is global"

The timeout is set on a typed HttpClient registered via AddHttpClient<IRestClient, RestClient>(), not on a global singleton — it applies only to RestClient instances resolved from DI. The KSeF API accepts batch uploads up to 100 MB per part; the default HttpClient.Timeout of 100 seconds is insufficient for large uploads on slower connections.

Making this configurable via KSeFClientOptions.HttpTimeout is a reasonable enhancement, but it changes the public API surface and is orthogonal to .NET Standard 2.0 support. Happy to address this in a separate PR if desired.


Summary: 4 comments addressed in follow-up commit, 3 are pre-existing upstream patterns, 5 are false positives, 1 is acknowledged as a future improvement.

…y barriers, harden PKCS#8 parsing

- Replace Microsoft.Maui.Graphics with direct SkiaSharp reference for netstandard2.0 (MAUI types unused in ns2.0 path)
- Unify string.Contains to OrdinalIgnoreCase across all TFMs using existing polyfill
- Add Volatile.Read/Write memory barriers in CertificateCompat and EcdhCompat reflection cache
- Add range validation for PBKDF2 iterations (1–10M) and key length (0–256 bytes) in Pkcs8Decryptor
@marcinborecki marcinborecki force-pushed the feature/netstandard2.0-support branch from 689541d to bd56d76 Compare February 9, 2026 15:51
@Krzysztof318
Copy link

Nice ale biorąc pod uwagę że to projekt rządowy, nie wiem czy ktokolwiek zainteresuje się tym PR... :(

@marcinborecki
Copy link
Author

@Krzysztof318 ja mam wiarę, że ktoś jednak popatrzy. Wydaje mi się, że wycinanie prawie 20% rynku da się załatać (w firmach w PL).

Liczę na konstruktywny feedback - i dołączenie PR do release-u.

Oczywiście każdy może zabrać teraz te elementy i już budować software u siebie - ale migrując to do pełnego repo zyskujemy wsparcie kolejnych przyszłych zmian.

To co najtrudniejsze wydaje się być zrobione - czyli kryptografia. Reszta zmian jaką będzie robić MF. -zakładam będzie czyniona w warstwie wyżej.

marcinborecki and others added 3 commits February 11, 2026 10:41
…from raw ASN.1

On .NET Framework 4.8, CopyWithPrivateKey() + PFX reimport silently drops the AllowPlaintextExport policy from CNG key handles. This causes GetRSAPrivateKey().ExportParameters(true) to throw CryptographicException after the certificate is constructed.

Fix: export RSA/ECDsa key parameters to PKCS#8 immediately after creation (while the CNG key is still exportable), then build the PKCS#12/PFX file manually using AsnWriter (KeyBag + CertBag structure per RFC 7292). Import with X509KeyStorageFlags.Exportable produces a certificate with a fully exportable private key.

Also fixes ArgumentException in AsnWriter.WriteInteger for certificate serial numbers with redundant leading zero bytes (set bit 0 of first byte to guarantee non-zero).

Test results after fix (identical on both frameworks):

- KSeF.Client.Tests:              net48=83/83, net10=83/83

- KSeF.Client.Tests.Core:          net48=162/164, net10=178/180 (same CertificatesLimits API rate-limit failure on both)

- KSeF.Client.Tests.ClientFactory:  net48=9/9, net10=9/9
@marcinborecki
Copy link
Author

marcinborecki commented Feb 11, 2026

Poprawki po audycie kodu — hardening warstwy netstandard2.0

Zmiany z audytu kodu + fix znaleziony przy dodatkowych testach. Wszystko kryptograficzne siedzi pod #if NETSTANDARD2_0 / #if NETFRAMEWORK, nie dotyka net8+. Wyjątek: HttpClient.Timeout = 5min (cross-TFM, celowo).

Co i dlaczego — po kolei.


PlatformGuard

netstandard2.0 to specyfikacja API, nie runtime. Nasza warstwa kryptograficzna wołamy Windows CNG (bcrypt.dll, RSACng, ECDiffieHellmanCng) — kompiluje się wszędzie, ale działa tylko na Windows. Ktoś zainstaluje NuGet na Mono, build przejdzie, a potem DllNotFoundException: bcrypt.dll i zero informacji co z tym zrobić.

Dodany PlatformGuard.EnsureWindowsCng() — 9 call sites na wejściu do kryptografii. Sprawdza Mono + RuntimeInformation.IsOSPlatform. Nie-Windows = PlatformNotSupportedException z komunikatem co zrobić. Wynik cachowany w bool? z Volatile.Write.


RSACng — handle leak + jawna export policy

Dwa tematy:

  1. RSACng bez using = handle CNG wisi do GC przy wyjątku. Dodano using.

  2. Domyślne new RSACng(2048) nie gwarantuje AllowPlaintextExport. Teraz tworzymy CngKey jawnie z flagami — bo bez tego .NET FW 4.8 gubi export policy przy reimporcie PFX (więcej niżej).


Ciche fallbacki w kryptografii

Dwa miejsca, ten sam wzorzec: nieznana wartość → zwróć default i leć dalej.

GetCoordSize() — nieznany OID krzywej EC zwracał 32 (P-256). P-384 ma 48 bajtów — klucz by się po cichu obciął. Teraz throw.

EcdhCompat — krzywa dopasowywana przez Contains("256"), co łapało też brainpoolP256. Zamienione na precyzyjny switch po nazwach CNG. Nieznana krzywa = throw.


Hardcoded P-256

WriteEcdsaPublicKeyInfo() w dwóch plikach wpisywał NistP256Oid na sztywno, choć metoda przyjmuje dowolny ECDsa. Dziś KSeF używa P-256, ale jak ktoś zmieni krzywą — subtelnie uszkodzony certyfikat bez błędu. Zamieniono na EcdsaCompat.CurveToOid(p.Curve).


RFC 4055 — brakujący NULL w RSA-PSS

AlgorithmIdentifier dla SHA-256 w RSA-PSS musi mieć SEQUENCE { OID sha256, NULL } (RFC 4055 §2.1). Pisaliśmy bez NULL. .NET to łyka, HSM-y mogą odrzucić. Dodano WriteNull() w 4 miejscach. Nie dotyczy ECDSA (RFC 5758 §3.2 zakazuje parametrów).


Refleksja w EcdhCompat

EnsureResolved() szuka ECDiffieHellman przez refleksję (niewidoczne w netstandard2.0). Sprawdzał null dla typu i Create, ale nie dla PublicKey i DeriveKeyMaterial. Efekt: s_resolved = true z nullami w polach, NRE gdzieś dalej. Dodane sprawdzenia zanim flaga zostanie ustawiona.


HttpClient.Timeout = 5 minut

Default to 100s. System KSeF MF przy operacjach wsadowych (paczki do 100 MB) odpowiada dłużej. Bez tego batch upload/download timeout-uje na produkcji. Cross-TFM, celowo. Na przyszłość warto dodać KSeFClientOptions.HttpTimeout, ale sensowny default musi być od razu.


CNG export policy — .NET Framework 4.8 gubi klucze prywatne

Znalezione dopiero na prawdziwym Windowsie. Na net8+ ten problem nie istnieje.

Stary flow: CopyWithPrivateKey(rsa)Export(Pfx) → reimport. Na .NET FW 4.8 reimport PFX po cichu gubi AllowPlaintextExport z CNG key handle. Certyfikat wygląda OK, ma klucz prywatny, ale ExportParameters(true) rzuca CryptographicException. Zero ostrzeżeń — po prostu nie działa.

Fix: eksportuj klucz do PKCS#8 od razu (póki CNG key jest świeży), potem buduj PFX ręcznie z AsnWriter (KeyBag + CertBag per RFC 7292) i reimportuj z Exportable. To samo dla RSA i ECDsa.

Przy okazji: serial[0] |= 0x01AsnWriter.WriteInteger nie lubi redundantnych leading zeroes w numerach seryjnych certyfikatów.


Testy — odblokowanie + polyfille

Wcześniej: plik testowy miał jedno API niedostępne na net48 = cały plik pod #if !NETFRAMEWORK. Teraz chirurgicznie polyfillujemy brakujące API:

  • IncrementalInvoiceRetrievalE2ETestsEnum.GetValues<T>(), DistinctBy(), TryAdd()EnumPolyfills, LinqPolyfills
  • EntityPermissionE2ETestsEnum.Parse<T>()EnumPolyfills
  • PeppolPefE2ETestsExportRSAPrivateKey() i pokrewne → CryptoCompat extension methods
  • VerificationLinkServiceTests — dodany ExportPkcs8PrivateKeyPemCompat(), bo wcześniej net48 ustawiał privateKeyPem = null i ćwiczył inną ścieżkę niż net8+

Trick z extension methods: na net48 kompilator bierze polyfill (bo instance method nie istnieje), na net8+ preferuje wbudowaną. Ten sam kod, zero #if.


Wyniki testów

KSeF.Client.Tests:              net48=83/83,   net10=83/83
KSeF.Client.Tests.Core:         net48=162/164, net10=178/180
KSeF.Client.Tests.ClientFactory: net48=9/9,     net10=9/9

Identyczny wzorzec failures na obu frameworkach — jedyny "prawdziwy" failure to CertificatesLimits_WhenExceeded (KSeF API zwraca 25006: Osiągnięto limit wniosków certyfikacyjnych). To rate-limit po stronie API KSeF, identyczny na net48 i net10, pre-existing, nie do naprawienia po stronie kodu.


Kompatybilność — pełna macierz

NuGet wybiera najbliższy kompatybilny TFM. Paczka oferuje netstandard2.0 + net8.0 + net9.0 + net10.0:

Runtime TFM z NuGet Działa? OS Uwagi
.NET Core 1.0–1.1 nie zainstaluje się nie implementują netstandard2.0
.NET Core 2.0 netstandard2.0 częściowo Windows ECDH niedostępny (sportowany dopiero w 2.1)
.NET Core 2.1–3.1 netstandard2.0 tak Windows CNG, pełna funkcjonalność
.NET 5.0–7.0 netstandard2.0 tak Windows j.w. — NuGet nie może dać im net8.0
.NET 8.0 net8.0 tak dowolny natywne API cross-platform
.NET 9.0 net9.0 tak dowolny j.w.
.NET 10.0 net10.0 tak dowolny j.w.
.NET Framework 4.7.2 netstandard2.0 tak Windows minimum
.NET Framework 4.8 netstandard2.0 tak Windows przetestowane
Mono / Xamarin / Unity netstandard2.0 nie PlatformGuard → PlatformNotSupportedException

.NET 5–7 na Linux/macOS dostaną netstandard2.0 target i PlatformGuard je zablokuje. Te runtimes mają cross-platformowe crypto API, ale NuGet nie może im dać net8.0 (wyższa wersja). Wszystkie są EOL — kto potrzebuje Linuxa, powinien przejść na .NET 8+.

PackageDescription w csproj zaktualizowany — informacja o wymaganiu Windows widoczna na nuget.org przed instalacją.


Dlaczego ten PR jest ważny

KSeF to system obowiązkowy dla wszystkich polskich podatników. Dane z telemetrii Microsoftu:

  • Obecne pokrycie (net8.0net10.0): ~82% ekosystemu .NET
  • Frameworki wykluczone bez tego PR: ~15–18% aktywności celów kompilacji
  • Sam .NET Framework: ~7,3%

Biblioteka rządowa odcinała ~15–18% ekosystemu — w tym .NET Framework 4.7.2+/4.8, powszechnie stosowany w polskich systemach korporacyjnych i administracji publicznej. To systemy, które nie mogą "po prostu" przejść na .NET 8.

PR rozwiązuje issue #80, gdzie zespół odpowiedział że migracja na netstandard2.0 jest "impossible due to the cryptographic methods used in the implementation". Da się — wymaga ręcznej warstwy kompatybilności opartej na Windows CNG, ale działa.

@marcinborecki marcinborecki changed the title feat: Dodanie obsługi .NET Standard 2.0 — pełna kompatybilność z .NET Framework 4.7.2+/4.8 feat: Dodanie obsługi .NET Standard 2.0 — kompatybilność z .NET Framework 4.7.2+ oraz .NET Core 2.1–7.0 (Windows) Feb 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants