---
title: "KSeF Crypto Worker: certyfikaty, XAdES i sesje"
description: "Techniczny opis serwisu uwierzytelniającego w KSeF 2.0: ceremonia XAdES, zarządzanie sesjami z lazy refresh, higiena kryptograficzna i Clean Architecture w .NET 10."
author: Maciej Olszewski
date: 2026-09-16
tags:
  - ksef
  - dotnet
  - kryptografia
  - n8n
url: "https://inprojects.ai/blog/ksef-crypto-worker-technikalia/"
language: pl
---

# KSeF Crypto Worker: certyfikaty, XAdES i sesje

> Techniczny opis serwisu uwierzytelniającego w KSeF 2.0: ceremonia XAdES, zarządzanie sesjami z lazy refresh, higiena kryptograficzna i Clean Architecture w .NET 10.

> Część przewodnika [KSeF od środka](https://inprojects.ai/blog/ksef-od-srodka-crypto-worker-i-pdf-api/). Tu rozbieramy `ksef-crypto-worker` na części: jak wygląda uwierzytelnienie certyfikatem w KSeF 2.0, jak serwis zarządza sesjami i dlaczego jest tak mały, jak jest.

## Problem: koniec tokenów statycznych

Od 1 stycznia 2027 Ministerstwo Finansów wyłącza logowanie do KSeF tokenem statycznym. Dostęp maszynowy wymaga od tego momentu Certyfikatu Uwierzytelniającego KSeF (Typ 1): klient buduje dokument `AuthTokenRequest` w XML, podpisuje go podpisem XAdES kluczem prywatnym certyfikatu i wymienia podpisany dokument na parę tokenów JWT: `accessToken` (ważny ok. 15 minut) i `refreshToken` (do 7 dni).

Naszym klientem KSeF jest n8n, a n8n nie wykona kanonikalizacji XML ani podpisu XAdES. `ksef-crypto-worker` to side-car w tej samej prywatnej sieci Docker, który przejmuje całą ceremonię i wystawia n8n proste REST API: „utwórz sesję", „daj token", „odśwież", „odwołaj". Zakres jest świadomie wąski i obejmuje tylko uwierzytelnianie. Wystawianie faktur, batch upload czy weryfikacja UPO pozostają po stronie n8n, już z tokenem w ręku.

## Ceremonia uwierzytelnienia krok po kroku

Sercem serwisu jest `KsefSdkFacade` (warstwa Infrastructure), która orkiestruje pełny przepływ na oficjalnym SDK ministerstwa:

```mermaid
sequenceDiagram
    participant N as n8n
    participant W as crypto-worker
    participant K as KSeF API
    N->>W: POST /api/v1/auth/sessions (NIP, środowisko)
    W->>K: GET /auth/challenge
    K-->>W: challenge + timestamp
    W->>W: budowa AuthTokenRequest XML,<br/>podpis XAdES kluczem certyfikatu
    W->>K: POST /auth/xades-signature
    K-->>W: referenceNumber
    loop co 500 ms, max 60 prób
        W->>K: GET /auth/{referenceNumber}
    end
    W->>K: POST /auth/token/redeem
    K-->>W: accessToken (~15 min) + refreshToken (do 7 dni)
    W-->>N: 201 Created (sessionId, accessToken, terminy)
```

Samego podpisu XAdES nie reimplementujemy. Wykonuje go `SignatureService` z oficjalnego SDK `KSeF.Client` (repozytorium `CIRFMF/ksef-client-csharp`, licencja MIT). To decyzja zapisana w ADR: od zgodności z formatem urzędowym jest kod urzędowy. SDK nie przychodzi jednak z NuGeta, tylko jest vendorowane jako klon w `external/` z pinowanym commitem (tag `2.6.0`). Build jest przez to powtarzalny, a aktualizacja wersji to świadoma zmiana w repo, nie niespodzianka przy restore.

Jeden fragment SDK został skopiowany i przerobiony celowo: pętla odpytywania o status uwierzytelnienia ma w oryginale zaszyte `Task.Delay(1s)`, więc `KsefSdkFacade` zawiera własną wersję z wstrzykniętym `TimeProvider`. Dzięki temu testy przepływu są deterministyczne i nie czekają na prawdziwy zegar.

## API

REST, prefiks `/api/v1/auth/sessions`, autoryzacja nagłówkiem `X-Api-Key` (poza health checkami). Błędy zwracane jako RFC 7807 ProblemDetails ze stabilnym kodem `KCW-XXXX`.

| Metoda | Ścieżka | Po co |
|---|---|---|
| POST | `/api/v1/auth/sessions` | Pełna ceremonia uwierzytelnienia, zwraca `sessionId` + `accessToken` |
| GET | `/api/v1/auth/sessions/{id}/access-token` | Token z cache, z leniwym odświeżeniem gdy zbliża się wygaśnięcie |
| POST | `/api/v1/auth/sessions/{id}/refresh` | Wymuszony refresh |
| DELETE | `/api/v1/auth/sessions/{id}` | Lokalne odwołanie sesji |
| GET | `/health/live`, `/health/ready` | Liveness / readiness (readiness sprawdza m.in. certyfikat) |

## Sesje: pamięć, single-flight i restart jako feature

Sesje są przechowywane w `InMemorySessionStore`, czyli w zwykłym słowniku w pamięci procesu, bez Redisa i bez bazy. Gdy n8n prosi o token, a do wygaśnięcia zostało mniej niż skonfigurowany zapas, handler wykonuje **leniwy, synchroniczny refresh** pod per-sesyjnym `SemaphoreSlim` z double-check lockingiem. Równoległe żądania o ten sam token nie wywołają więc kilku refreshy naraz: pierwsze odświeża, reszta czeka i dostaje świeży token (wzorzec single-flight). Osobnego procesu odświeżającego w tle nie ma, bo nie jest potrzebny.

Restart kontenera kasuje wszystkie sesje i to jest zaakceptowany kompromis: n8n dostaje 404, tworzy nową sesję i po 1-3 sekundach działa dalej. Bezstanowość upraszcza przy okazji „rotację": podmiana certyfikatu to restart kontenera z nowym sekretem.

Ciekawostka kontraktowa: odwołanie sesji po stronie KSeF jest oznaczone jako `NotSupported` (kod `KCW-3098`), bo SDK ministerstwa nie wystawia odwołania refresh-tokenu. `DELETE` usuwa sesję lokalnie, a realnym unieważnieniem pozostaje wygaśnięcie tokenów.

## Certyfikaty i higiena kryptograficzna

`FilesystemCertificateProvider` ładuje certyfikat raz przy starcie, z auto-detekcją formatu PEM (certyfikat + zaszyfrowany klucz + passphrase) lub PKCS#12/PFX. Kilka decyzji wartych odnotowania:

- **Klucz prywatny nie dotyka dysku poza sekretem.** Na Linuksie certyfikat ładowany jest z flagami `EphemeralKeySet | MachineKeySet`, więc klucz istnieje tylko w pamięci procesu i nie trafia do żadnego magazynu systemowego. Same pliki wchodzą do kontenera jako Docker secrets.
- **Zerowanie pamięci.** Bufory z materiałem PFX i znakami passphrase są po użyciu czyszczone przez `CryptographicOperations.ZeroMemory`.
- **Round-trip PEM przez PKCS#12.** Klucz z PEM jest po załadowaniu re-eksportowany i importowany przez PKCS#12, żeby zagwarantować podpisywalny klucz na każdej platformie (Linux w produkcji, macOS/Windows w dev). Koszt płacony raz przy starcie.
- **Walidacja typu certyfikatu.** Worker przyjmuje wyłącznie Certyfikat Typ 1 (keyUsage `DigitalSignature`, RSA-2048 lub EC P-256); certyfikat offline Typ 2 jest odrzucany z kodem `KCW-3004`. Zbliżające się wygaśnięcie generuje zdarzenie audytowe.
- **Klucz API bez porównywania stringów.** Klucz od n8n jest hashowany przy starcie (SHA-256 + pepper), a porównanie per-request wykonuje `CryptographicOperations.FixedTimeEquals`: stały czas, odporność na timing attack.

Do tego dochodzi redakcja danych wrażliwych w logach: tokeny nigdy nie są logowane w całości, a CN certyfikatu (może zawierać NIP) jest skracany do ostatnich trzech znaków.

## Architektura i stack

.NET 10 (LTS), C# 14, ASP.NET Core Minimal API. Układ to klasyczna architektura heksagonalna z twardym kierunkiem zależności `Api → Application → Domain` (Infrastructure adaptuje porty Application). Domain nie zależy od niczego poza standardową biblioteką i modeluje sesję jako agregat `AuthSession` z maszyną stanów i zdarzeniami domenowymi; NIP, tokeny czy identyfikatory to value objects z walidacją w konstruktorze (NIP liczy sumę kontrolną mod-11, tokeny dekodują claim `exp` i odrzucają wygasłe). Layering nie jest tylko konwencją: pilnują go testy architektury na NetArchTest, więc próba dociągnięcia zależności w złą stronę wywala build.

Ruch do KSeF przechodzi przez pipeline Polly v8: timeout całościowy 30 s, trzy retry z jitterem, timeout per próba 10 s i circuit breaker. Pipeline jest wpięty w `HttpClient`, który worker współdzieli z SDK ministerstwa. Resilience obejmuje więc również wywołania, których kod workera sam nie wysyła.

Audyt załatwiają strukturalne logi Serilog (JSON na stdout) z polem `eventType`: `ChallengeFetched`, `XadesSigned`, `TokenRedeemed`, `ApiKeyDenied`, `NipDenied` i tak dalej. Zamiast dedykowanej tabeli audytowej mamy zdarzenia przeszukiwalne dowolnym narzędziem do logów.

## Testy i CI

xUnit v3 z obowiązkowym wykonaniem równoległym, deterministycznym czasem (`FakeTimeProvider`) i zakazem `Thread.Sleep`. Jedyna integracja to WireMock.Net udający API KSeF: test `KsefSdkFacadeFullFlowTests` przepuszcza przez prawdziwe SDK cały przepływ uwierzytelnienia przeciwko zamockowanemu serwerowi. Bramka pokrycia wymaga co najmniej 80% linii dla warstw Domain+Application+Infrastructure i 70% dla Api; faktyczne pokrycie oscyluje między 91 a 98%.

CI działa podwójnie: GitHub Actions (format, build z `warnaserror`, testy, bramka coverage, skan podatności zależności i Trivy na systemie plików, wszystkie akcje pinowane po SHA) oraz GitLab CI, który dodatkowo buduje i wypycha obraz Dockera do rejestru GitLab. Obraz produkcyjny to chiseled Ubuntu (`aspnet:10.0-noble-chiseled-extra`), bez shella i menedżera pakietów, a proces działa jako non-root UID 1001.

## Filozofia: świadomie małe

README zawiera tabelę „Wycięte / Dlaczego", która dokumentuje, czego w serwisie **nie ma** i z jakiego powodu: bez Vaulta (sekrety Dockera wystarczają przy jednym hoście), bez Redisa (jedna instancja, sesje odtwarzalne w sekundy), bez Postgresa (audyt w logach), bez OpenTelemetry i rate-limitingu (jeden klient w zamkniętej sieci). Nawet kody błędów usuniętych funkcji są „wypalone" i nie wolno ich użyć ponownie, bo kontrakt API traktujemy jako wartość pierwszorzędną.

To podejście widać też w konfiguracji: wszystkie ustawienia wchodzą przez zmienne środowiskowe z prefiksem `KSCW_` (klucz API, pepper, ścieżki certyfikatu, środowisko KSeF: Test/Demo/Production, opcjonalna lista dozwolonych NIP-ów), a błędna konfiguracja zatrzymuje start procesu zamiast objawić się w runtime.
