# KSeF Authentication Guide

Complete guide for KSeF (Krajowy System e-Faktur) authentication using XAdES digital signatures and API tokens.

## Overview

KSeF is Poland's national e-invoicing system operated by the Ministry of Finance. It requires authenticated API calls using either:

1. **XAdES Certificate Authentication** - Sign XML with PFX certificate (no permissions by default)
2. **KSeF Token Authentication** - Use pre-generated API token with RSA-OAEP encryption (has permissions)

## Critical: Two API Base Paths

KSeF API uses **different base paths** for different endpoint types:

| Endpoint Type | Base Path | Example |
|---------------|-----------|---------|
| Auth endpoints | `/api/v2/` | `/api/v2/auth/challenge` |
| Invoice/Session endpoints | `/v2/` | `/v2/invoices/query/metadata` |

This is critical for REST connection configuration.

## Official Resources

- **Main Documentation**: <https://ksef.mf.gov.pl>
- **API Test Environment**: <https://api-test.ksef.mf.gov.pl>
- **API Production**: <https://api.ksef.mf.gov.pl>
- **GitHub Docs**: <https://github.com/CIRFMF/ksef-docs>
- **Certificate Guide**: <https://github.com/CIRFMF/ksef-docs/blob/main/certyfikaty-KSeF.md>
- **C# Reference Implementation**: <https://github.com/CIRFMF/ksef-client-csharp>

## Architecture

### XAdES Certificate Auth Flow

```mermaid
sequenceDiagram
    participant App
    participant KSeF API

    Note over App,KSeF API: XAdES Certificate Auth (no permissions)
    App->>KSeF API: POST /api/v2/auth/challenge
    KSeF API-->>App: challenge string
    App->>App: Build AuthTokenRequest XML
    App->>App: Sign XML with XAdES (PFX)
    App->>KSeF API: POST /api/v2/auth/xades-signature
    KSeF API-->>App: referenceNumber, authenticationToken
    loop Poll until status 200
        App->>KSeF API: GET /api/v2/auth/{ref}
        KSeF API-->>App: status code
    end
    App->>KSeF API: POST /api/v2/auth/token/redeem
    KSeF API-->>App: accessToken (NO permissions)
```

### KSeF Token Auth Flow

```mermaid
sequenceDiagram
    participant App
    participant KSeF API

    Note over App,KSeF API: KSeF Token Auth (with permissions)
    App->>KSeF API: POST /api/v2/auth/challenge
    KSeF API-->>App: challenge string
    App->>KSeF API: GET /v2/security/public-key-certificates
    KSeF API-->>App: certificates array (with KsefTokenEncryption usage)
    App->>App: Encrypt token|timestamp with RSA-OAEP
    App->>KSeF API: POST /api/v2/auth/ksef-token
    KSeF API-->>App: accessToken (WITH permissions)
```

## Permissions System

### Why Permissions Matter

XAdES certificate auth gives access token **without permissions**. To access invoice/session endpoints, you need:

- **KSeF Token Auth** - Access token inherits permissions from the KSeF token
- **Create KSeF Token first** - Use XAdES auth once, create token with permissions, then use token auth

### Permission Types

| Permission | Description |
|------------|-------------|
| `InvoiceRead` | Read invoice data |
| `InvoiceWrite` | Create and modify invoices |
| `CredentialsRead` | Read credential/token information |
| `CredentialsManage` | Create/manage API tokens |
| `SubunitManage` | Manage organizational subunits |
| `VatUeManage` | VAT UE operations |
| `SelfInvoicing` | Self-invoicing |
| `RRInvoicing` | RR invoice operations |
| `TaxRepresentative` | Tax representative |
| `Introspection` | Token introspection |
| `EnforcementOperations` | Enforcement operations |

### Permissions Required Per Endpoint

| Endpoint | Method | Permission | Query File |
|----------|--------|------------|------------|
| `/v2/invoices/query/metadata` | POST | `InvoiceRead` | `invoice-ksef-out.json` |
| `/v2/invoices/ksef/{ksefNumber}` | GET | `InvoiceRead` | `invoice.json` |
| `/v2/sessions/online` | POST | `InvoiceWrite` | `session-online-open.json` |
| `/v2/sessions/{referenceNumber}` | GET | Session access | `session-status.json` |
| `/v2/tokens` | GET | `CredentialsRead` | `token.json` |
| `/v2/tokens` | POST | `CredentialsManage` | - |

## Certificate Requirements

### Why Self-Signed Certificates Work

KSeF accepts self-signed certificates because authentication is based on:

1. **NIP embedded in certificate** - The Polish tax ID (NIP) is encoded in `serialNumber` field as `TINPL-{nip}` using OID 2.5.4.97
2. **XAdES signature verification** - KSeF verifies the signature matches the certificate
3. **Challenge-response** - Server provides unique challenge that must be signed

This means you don't need certificates from trusted CA authorities - self-signed certificates are sufficient.

### Certificate Structure

```text
Subject:
  GN = A                           (given name - arbitrary)
  SN = R                           (surname - arbitrary)
  serialNumber = TINPL-1234567890  (tax ID with TINPL prefix)
  CN = Company Name                (common name)
  C = PL                           (country code)
```

### File Types

| File | Format | Purpose |
|------|--------|---------|
| `-key.pem` | PEM | RSA private key (2048/4096 bit) |
| `-cert.pem` | PEM | X.509 certificate |
| `.pfx` | PKCS#12 | Combined key + certificate (password protected) |

## Configuration

### Certificate Config

File: `nc-preference/certificate/ksef/cert.json`

```json
{
  "ksef-test": {
    "api_url": "https://api-test.ksef.mf.gov.pl",
    "certificate_password": "your-password",
    "tax_id": "1234567890",
    "common_name": "Company KSeF Certificate",
    "country": "PL",
    "state": "Mazowieckie",
    "locality": "Warsaw",
    "organization_name": "Company Sp. z o.o",
    "organizational_unit": "IT",
    "email_address": "it@company.pl",
    "output_dir": "nc-preference/certificate/ksef/",
    "days_valid": 365,
    "key_size": 2048,
    "mode": "test",
    "ksef_token": "optional-api-token"
  }
}
```

### REST Connection Config

File: `nc-backend/preference/database/connection/rest-ksef-test.json`

```json
{
  "name": "database/connection/rest-ksef-test.json",
  "dbtype": "rest_call",
  "driver": "rest_call",
  "connection": [
    {
      "name": "KSeF Poland test",
      "auth_preference": "database/connection/auth-ksef-test.json",
      "host": "https://api-test.ksef.mf.gov.pl",
      "call_path": "/v2/",
      "bearer_authorization": true,
      "auth": {
        "auth_tag": "authorization",
        "call_array": [
          {
            "call_path": "/api/v2/",
            "call_url": "auth/challenge"
          },
          {
            "content_type": "json",
            "ksef_token_encrypt": true,
            "call_path": "/api/v2/",
            "call_url": "auth/ksef-token",
            "previous_call_param": {
              "challenge": "challenge"
            }
          },
          {
            "call_method": "GET",
            "call_path": "/api/v2/",
            "call_url": "auth/{{referenceNumber}}",
            "auth_tag": "authorization",
            "previous_call_param": {
              "referenceNumber": "referenceNumber",
              "authorization": "authenticationToken.token"
            },
            "poll_until": {
              "field": "status.code",
              "value": 200
            }
          },
          {
            "call_path": "/api/v2/",
            "call_url": "auth/token/redeem"
          }
        ]
      }
    }
  ]
}
```

**Key points:**

- `call_path`: `/v2/` - default for invoice/session endpoints
- Auth `call_array` uses `call_path: "/api/v2/"` override for auth endpoints
- `ksef_token_encrypt: true` - enables KSeF token auth with RSA-OAEP encryption
- Fast path: if `authorization` already exists, auth flow is skipped

### Auth Config

File: `nc-backend/preference/database/connection/auth-ksef-test.json`

```json
{
  "name": "database/connection/auth-ksef-test.json",
  "output_dir": "nc-preference/certificate/ksef/",
  "pfx_path": "nc-preference/certificate/ksef/ksef-9351336700-20260309165909.pfx",
  "pfx_password": "test-ksef912",
  "nip": "9351336700",
  "ksef_token": "20260312-EC-xxx|nip-9351336700|...",
  "authorization": ""
}
```

**Fields:**

| Field | Description |
|-------|-------------|
| `output_dir` | Directory for cached public key |
| `pfx_path` | Path to PFX certificate (for XAdES fallback) |
| `pfx_password` | Password for PFX file |
| `nip` | Polish tax ID |
| `ksef_token` | KSeF API token (created via `ksef-token-create`) |
| `authorization` | Cached access token (auto-filled) |

## CLI Commands

### Generate Certificate

```bash
cd /Volumes/nc/nc-server/lib/xml
lj certificate.lua generate ksef-test 1234567890
```

Creates:

- `{output_dir}/ksef-{nip}-{timestamp}-key.pem`
- `{output_dir}/ksef-{nip}-{timestamp}-cert.pem`
- `{output_dir}/ksef-{nip}-{timestamp}.pfx`

### Test XAdES Authentication

```bash
lj certificate.lua ksef-test [nip]
```

Full XAdES authentication flow:

1. Get challenge from KSeF
2. Build AuthTokenRequest XML
3. Sign with XAdES using PFX
4. Submit signed XML
5. Poll authentication status
6. Redeem access token

### Create KSeF API Token

```bash
lj certificate.lua ksef-token-create ksef-test "My token description"
```

Creates a persistent API token with permissions:

- `InvoiceRead` - Read invoices
- `InvoiceWrite` - Create/update invoices
- `CredentialsRead` - Read credentials

The token is saved to config as `ksef_token`.

### Authenticate with KSeF Token

```bash
lj certificate.lua ksef-token-auth <token> ksef-test
```

Uses RSA-OAEP encrypted token authentication instead of certificate.

## Query Files

Location: `nc-backend/preference/form/nc/erp-sync/ksef/query/`

| File | Endpoint | Method | Permission |
|------|----------|--------|------------|
| `invoice-ksef-out.json` | `invoices/query/metadata` | POST | `InvoiceRead` |
| `invoice.json` | `invoices/ksef/{ksefNumber}` | GET | `InvoiceRead` |
| `session-online-open.json` | `sessions/online` | POST | `InvoiceWrite` |
| `session-status.json` | `sessions/{referenceNumber}` | GET | Session access |
| `token.json` | `tokens` | GET | `CredentialsRead` |

## XAdES Signature Details

### XML Template

File: `lib/xml/ksef-auth-token.xml`

```xml
<?xml version="1.0" encoding="UTF-8"?>
<AuthTokenRequest xmlns="http://ksef.mf.gov.pl/auth/token/2.0">
  <Challenge>{{challenge}}</Challenge>
  <ContextIdentifier>
    <Nip>{{nip}}</Nip>
  </ContextIdentifier>
  <SubjectIdentifierType>certificateSubject</SubjectIdentifierType>
  <Signature xmlns="http://www.w3.org/2000/09/xmldsig#" Id="Signature">
    <!-- XAdES qualifying properties -->
    <Object>
      <xades:QualifyingProperties>
        <xades:SignedProperties Id="SignedProperties">
          <xades:SignedSignatureProperties>
            <xades:SigningTime>{{signingTime}}</xades:SigningTime>
            <xades:SigningCertificate>
              <xades:Cert>
                <xades:CertDigest>
                  <DigestValue>{{digest}}</DigestValue>
                </xades:CertDigest>
                <xades:IssuerSerial>
                  <X509IssuerName>{{issuer}}</X509IssuerName>
                  <X509SerialNumber>{{serial}}</X509SerialNumber>
                </xades:IssuerSerial>
              </xades:Cert>
            </xades:SigningCertificate>
          </xades:SignedSignatureProperties>
        </xades:SignedProperties>
      </xades:QualifyingProperties>
    </Object>
  </Signature>
</AuthTokenRequest>
```

### Signing Process

1. **Build unsigned XML** - Replace template placeholders
2. **Canonicalize** - Exclusive XML canonicalization (exc-c14n)
3. **Calculate digests** - SHA-256 for references
4. **Sign** - RSA-SHA256 signature value
5. **Insert certificate** - Base64-encoded X.509 certificate

### Implementation

```lua
local xmlsec = require "xml/xmlsec"
local signedXml = xmlsec.action("sign", unsignedXml, pfxPath, password)
```

Fallback to command line:

```bash
xmlsec1 --sign --pkcs12 cert.pfx --pwd password --lax-key-search \
  --enabled-key-data x509 input.xml
```

## RSA-OAEP Token Encryption

KSeF token auth requires encrypting the token with KSeF's public key:

### Get Public Key

Endpoint: `GET /v2/security/public-key-certificates`

Response (array of certificates):

```json
[
  {
    "certificate": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...",
    "validFrom": "2024-07-11T12:23:56.0154302+00:00",
    "validTo": "2028-07-11T12:23:56.0154302+00:00",
    "usage": ["KsefTokenEncryption", "SymmetricKeyEncryption"]
  }
]
```

Select certificate with `KsefTokenEncryption` in `usage` array.

### Encryption Process

```lua
-- Download KSeF public key certificates
local url = config.api_url .. "/v2/security/public-key-certificates"
local response = curl.getUrl({url = url})

-- Find certificate with KsefTokenEncryption usage
local certPem = nil
for _, cert in ipairs(dataArray) do
    if cert.usage then
        for _, usage in ipairs(cert.usage) do
            if usage == "KsefTokenEncryption" then
                certPem = cert.certificate
                break
            end
        end
    end
end

-- Encrypt token|timestamp
local tokenData = ksefToken .. "|" .. timestamp
local encrypted = openssl.pkeyutl({
  encrypt = true,
  pubin = true,
  inkey = publicKeyPath,
  pkeyopt = {
    rsa_padding_mode = "oaep",
    rsa_oaep_md = "sha256"
  }
})
```

### Public Key Caching

The public key is cached locally at:

- Path: `{output_dir}/ksef-public-key-{mode}.pem`
- Saved to `cert.json` as `ksef_public_key_path`

On subsequent calls, the cached key is used instead of re-downloading.

## API Endpoints

### Auth Endpoints (base: `/api/v2/`)

| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/api/v2/auth/challenge` | POST | Get authentication challenge |
| `/api/v2/auth/xades-signature` | POST | Submit signed XML |
| `/api/v2/auth/{referenceNumber}` | GET | Poll auth status |
| `/api/v2/auth/token/redeem` | POST | Get access/refresh tokens |
| `/api/v2/auth/ksef-token` | POST | Token-based auth |

### Data Endpoints (base: `/v2/`)

| Endpoint | Method | Purpose | Permission |
|----------|--------|---------|------------|
| `/v2/invoices/query/metadata` | POST | Query invoice metadata | `InvoiceRead` |
| `/v2/invoices/ksef/{ksefNumber}` | GET | Get invoice by KSeF number | `InvoiceRead` |
| `/v2/sessions/online` | POST | Open session for upload | `InvoiceWrite` |
| `/v2/sessions/{referenceNumber}` | GET | Get session status | - |
| `/v2/tokens` | GET | List API tokens | `CredentialsRead` |
| `/v2/tokens` | POST | Create API token | `CredentialsManage` |
| `/v2/security/public-key-certificates` | GET | Get KSeF public key certificates | - |

## Status Codes

| Code | Description |
|------|-------------|
| 100 | Pending |
| 200 | Success |
| 403 | Missing permissions |
| 404 | Resource not found |
| 415 | Invalid certificate/NIP mismatch |

## Troubleshooting

### "NIP invalid" Error

Certificate NIP doesn't match the NIP in request. Regenerate certificate with correct NIP.

### "Status 415" Error

Authentication signature invalid. Causes:

- Wrong NIP in certificate
- Expired certificate
- Malformed XAdES signature

### "403 Forbidden - missing-permissions"

XAdES auth gives token without permissions. Solution:

1. Use `ksef-token-create` to create KSeF API token with permissions
2. Use KSeF token auth instead of XAdES auth
3. Access token will inherit permissions from KSeF token

### "permissions must not be empty"

POST /v2/tokens requires permissions array with at least one valid permission type.

### "JSON value could not be converted to TokenPermissionType"

Invalid permission string. Use only valid types listed in Permissions section.

### 404 on Invoice Endpoints

Check the URL path. Invoice endpoints use `/v2/` base, not `/api/v2/`.

## Security Notes

1. **Protect PFX files** - Contains private key, use strong password
2. **Store tokens securely** - KSeF API tokens have significant permissions
3. **Use HTTPS** - All KSeF API calls require TLS
4. **Rotate tokens** - Regularly create new tokens and revoke old ones
5. **Test first** - Always test in test environment before production

## File Structure

```text
nc-server/lib/xml/
├── certificate.lua           # Main certificate and auth module
├── ksef-auth-token.xml       # XAdES signature template
└── ksef-openssl.cnf          # OpenSSL config for cert generation

nc-preference/certificate/ksef/
├── cert.json                 # Configuration
├── ksef-{nip}-{timestamp}-key.pem
├── ksef-{nip}-{timestamp}-cert.pem
├── ksef-{nip}-{timestamp}.pfx
└── ksef-public-key-{mode}.pem # Cached KSeF public key

nc-backend/preference/database/connection/
├── rest-ksef-test.json       # REST connection config
├── rest-ksef.json            # Production REST config
├── auth-ksef-test.json       # Auth credentials
└── auth-ksef.json            # Production auth

nc-backend/preference/form/nc/erp-sync/ksef/query/
├── invoice-ksef-out.json     # Query invoice metadata
├── invoice.json              # Get invoice by KSeF number
├── session-online-open.json  # Open upload session
├── session-status.json       # Get session status
└── token.json                # List API tokens
```

## Code Integration

```lua
local certificate = require "certificate"

-- Generate new certificate
local result = certificate.generate("ksef-test", {tax_id = "1234567890"})

-- XAdES authentication (no permissions)
local tokenResult = certificate.ksefAuth("ksef-test")

-- Create API token with permissions
local apiToken = certificate.ksefCreateAndSaveToken("ksef-test", "My token")
```
