‹ Zpět na přehledStáhnout .md

Příručka pro vývojáře — Remote Signing Service

Tato příručka popisuje, jak integrovat aplikaci třetí strany se službou Remote Signing. Služba je kompatibilní s Cloud Signature Consortium API v2 (dále jen CSC API v2) a vystavuje dokumentový podpis s klíči chráněnými v hardwarovém modulu (HSM) operovaném důvěryhodným poskytovatelem (TSP).

Slovník použitých termínů

  • AS — Authorization Server, tato služba.
  • SCA — Signature Creation Application, vaše aplikace.
  • SAM — Signature Activation Module, modul řídící aktivaci klíče v HSM.
  • SAD — Signature Activation Data, doklad o autorizaci konkrétního podpisu.
  • SCAL2 — Signature Activation Conformance Level 2; uživatelská autorizace probíhá u AS, nikoli u SCA.
  • DTBSR — Data To Be Signed Representation, vstup do podpisu (obvykle hash dokumentu obalený DigestInfo).

Obsah

  1. Předpoklady a registrace
  2. Autentizace klienta — OAuth2 + PKCE přes Keycloak
  3. Signing Sessions (SCAL2)
  4. Referenční tabulka endpointů
  5. Chybové stavy a kódy
  6. Testovací prostředí a demo
  7. Strojově čitelná OpenAPI specifikace

1. Předpoklady a registrace

Než začnete, požádejte operátora služby o tyto údaje:

Položka Hodnota dodá operátor
client_id Identifikátor vašeho klienta v Keycloaku.
client_secret Pouze pro confidential klienty. Public klienti (PKCE) client_secret nepoužívají.
Povolené redirect_uri Seznam návratových URL registrovaných u Keycloaku. Tytéž URL akceptuje AS i v parametru redirect_uri při založení signing-session.
scope service — nese oprávnění volat CSC API endpointy služby.
URL Keycloak realmu https://<idp-host>/realms/<realm> — autorizační i token endpoint vaší SCA.
URL prostředí https://<as-host>/. V testovacím prostředí je to https://remote-signing.staging.eidentity.cz/.
credential_id Identifikátor podpisového klíče, který pro uživatele vystavíte.

Vaše aplikace je v terminologii OAuth2 obvykle public client s PKCE, případně confidential klient s client_secret (rozhoduje operátor při registraci).

2. Autentizace klienta — OAuth2 + PKCE přes Keycloak

SCA získá access token z Keycloaku standardním OAuth2 Authorization Code flow s PKCE (RFC 6749 + RFC 7636). Tento token slouží jako Authorization: Bearer <access_token> pro všechny CSC API endpointy služby Remote Signing.

2.1 Code request

Přesměrujte prohlížeč uživatele na autorizační endpoint Keycloak realmu:

https://<idp-host>/realms/<realm>/protocol/openid-connect/auth
  ?response_type=code
  &client_id=<client_id>
  &redirect_uri=<vaše návratové URL>
  &scope=service
  &state=<náhodný 32-bajtový nonce>
  &code_challenge=<base64url(sha256(code_verifier))>
  &code_challenge_method=S256

Po úspěšném loginu Keycloak přesměruje prohlížeč zpět na vaše redirect_uri s parametry code a state.

Hodnotu code_verifier si SCA uloží do session (např. sessionStorage). Hodnotu state ověřte při návratu — chrání před CSRF.

Hodnota scope musí obsahovat service.

2.2 Token exchange

Po návratu z Keycloaku na redirect_uri pošle SCA code na token endpoint Keycloaku:

POST /realms/<realm>/protocol/openid-connect/token HTTP/1.1
Host: <idp-host>
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=<code>
&redirect_uri=<vaše návratové URL>
&client_id=<client_id>
&code_verifier=<původní code_verifier>

Confidential klienti navíc přiloží &client_secret=<secret>.

Odpověď (zkráceně):

{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 600,
  "refresh_token": "eyJhbGciOi...",
  "scope": "service"
}

access_token je podepsaný JWT — claim sub nese identifikátor uživatele, azp (resp. client_id) identifikátor klienta.

2.3 Životnost tokenu

Životnost tokenu je nastavená v Keycloak realmu (ve staging 10 minut). Token endpoint vrací též refresh_token — pro tiché obnovení pošlete na stejný endpoint:

grant_type=refresh_token
&refresh_token=<refresh_token>
&client_id=<client_id>

Pokud i refresh selže (invalid_grant), uživatele přihlaste znovu §2.1 → §2.2.

3. Signing Sessions (SCAL2)

SCA pouze zaregistruje záměr; AS si sám převezme uživatele, ověří PIN a OTP přes vlastní formulář (uvnitř důvěryhodné zóny TSP) a vrátí podpis. SCA se nikdy nedotkne autorizačních faktorů, čímž splníte SCAL2.

3.1 Schéma

SCA           AS              SAM/HSM
 │            │                  │
 │ 1) /info   │                  │
 │───────────>│                  │
 │            │                  │
 │ 2) /credentials/list          │
 │───────────>│                  │
 │            │                  │
 │ 3) POST /signing-sessions     │
 │───────────>│                  │
 │ 201 + authorize_url           │
 │<───────────│                  │
 │            │                  │
 │ 4) přesměrování prohlížeče na authorize_url
 │    (uživatel pokračuje v UI AS: PIN, OTP, potvrzení hashe)
 │            │                  │
 │            │ 5) SAM.sign      │
 │            │─────────────────>│
 │            │      podpis      │
 │            │<─────────────────│
 │            │                  │
 │ 6) GET /signing-sessions/{id} (polling)
 │───────────>│                  │
 │ signed + signed_payload       │
 │<───────────│                  │

3.2 Krok za krokem

1) Získejte ID podpisového klíče uživatele

POST /csc/v2/credentials/list HTTP/1.1
Authorization: Bearer <access_token>
Content-Type: application/json

{}
{ "credentialIDs": ["demo-credential-rsa"] }

2) (Volitelně) Načtěte detail podpisového klíče

POST /csc/v2/credentials/info HTTP/1.1
Authorization: Bearer <access_token>
Content-Type: application/json

{ "credentialID": "demo-credential-rsa", "certificates": "single" }

Parametr certificates určuje, kolik certifikátů z řetězce důvěry server vrátí v poli cert.certificates:

  • none — žádný certifikát (pole je vynecháno),
  • single — pouze podepisující certifikát,
  • chain — celý řetězec důvěry až po kořenovou CA.

Příklad odpovědi (zkráceně):

{
  "description": "Demo RSA credential",
  "key": {
    "status": "enabled",
    "algo": ["1.2.840.113549.1.1.11"],
    "len": 2048,
    "curve": null
  },
  "cert": {
    "status": "valid",
    "certificates": ["MIIDXjCCAk...base64..."],
    "issuerDN": "CN=Demo CA, O=Iterative Works",
    "serialNumber": "01",
    "subjectDN": "CN=Demo User, O=Iterative Works",
    "validFrom": "2026-01-01T00:00:00Z",
    "validTo": "2027-01-01T00:00:00Z"
  },
  "auth": { "mode": "implicit", "expression": null },
  "SCAL": "2",
  "multisign": 5,
  "lang": "cs",
  "authFactors": ["otp", "pin"]
}

V poli key.algo najdete seznam OID podpisových algoritmů, které klíč podporuje — jednu z těchto hodnot pošlete v poli signAlgo při zakládání signing-session (např. 1.2.840.113549.1.1.11 = SHA-256 s RSA). Délka podpisu v bajtech pro RSA je key.len / 8. Úplné JSON schéma odpovědi najdete v OpenAPI specifikaci (§7).

3) Vypočtěte SHA-256 hash dokumentu

V SCA spočítejte hash dokumentu, který chce uživatel podepsat. Předáváte pouze hash, nikoli celý dokument — dokument nikdy neopouští SCA.

async function sha256Base64(blob) {
  const buf = await crypto.subtle.digest("SHA-256", await blob.arrayBuffer());
  return btoa(String.fromCharCode(...new Uint8Array(buf)));
}

Tento příklad počítá hash z celého souboru, což je vhodné pro syrová data (např. CMS detached signature nad libovolným payloadem). Pro PAdES a CAdES obal je vstupem hash spočítaný nad jinou hodnotou — typicky CMS signedAttributes, případně byte-range PDF dokumentu s prázdným místem pro podpis. Volba vstupního hashe je odpovědnost SCA dle příslušné normy (ETSI EN 319 142 pro PAdES, EN 319 122 pro CAdES).

4) Založte signing session

POST /csc/v2/signing-sessions HTTP/1.1
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "credentialID": "demo-credential-rsa",
  "hash": ["sTOgwOm+474gFj0q0x1iSNspKqbcse4IeiqlDg/HWuI="],
  "signAlgo": "1.2.840.113549.1.1.11",
  "numSignatures": 1,
  "redirect_uri": "https://sca.example.com/sign-callback",
  "expires_in": 900
}

numSignatures musí být shodné s délkou pole hash — služba vystaví právě jeden podpis pro každý hash a v právě jednom volání. Pole hash může obsahovat víc položek (podpis víc dokumentů v rámci jedné autorizace uživatele), pak nastavte numSignatures na stejnou hodnotu.

Více souběžných signing-sessions na jednoho uživatele je podporováno — každá relace má vlastní session_id a uživatel autorizuje každou samostatně.

Odpověď 201 Created:

{
  "session_id": "8a52b3d4-9c1e-4f6a-b2d7-1c3e5b7d9a1f",
  "status": "pending",
  "delivery": "redirect",
  "expires_at": "2026-05-15T14:32:18.413Z",
  "authorize_url": "https://<as-host>/sign/Y3JlZGVudGlhbDFfdG9rZW4-opaque",
  "signed_payload": null
}

redirect_uri musí být na seznamu povolených URL pro váš client_id registrovaných v Keycloaku. Pokud není, dostanete 400 invalid_request s error_description: "redirect_uri is not allowed for this client".

authorize_url je neprůhledný řetězec — jeho strukturu SCA nesmí parsovat. Pole zůstává v odpovědi vyplněné i po dosažení terminálního stavu (signed, denied, expired); SCA na něj v té chvíli nesmí znovu navigovat — vede na již ukončenou relaci a uživateli by se zobrazila chybová stránka.

5) Přesměrujte uživatele na authorize_url

Od tohoto okamžiku až do dokončení relace ovládá UI AS. AS si vyžádá:

  • PIN ke klíči (pokud je nastaven),
  • OTP kód (TOTP z aplikace nebo e-mail/SMS dle nastavení),
  • potvrzení podpisu zobrazením hashe.

Po podpisu AS přesměruje prohlížeč zpět na vaše redirect_uri:

  • úspěch: ?session_id=<id>,
  • odmítnutí uživatelem: ?error=access_denied&session_id=<id>.

Když relace vyprší nebo dojde k jiné terminální chybě, AS nepřesměrovává a zobrazí vlastní stránku — SCA stav v tom případě zjistí pollingem (viz krok 6).

6) Polling stavu relace

Doporučený způsob získání podpisu je polling — SCA tak zachytí i pozdější stavy (denied, expired):

const POLL_INTERVAL_MS = 1500;
const MAX_WAIT_MS = 5 * 60 * 1000;
const TERMINAL = new Set(["signed", "denied", "expired"]);

async function pollSession(sessionId, bearerToken) {
  const start = Date.now();
  while (Date.now() - start < MAX_WAIT_MS) {
    if (document.visibilityState === "hidden") {
      await sleep(POLL_INTERVAL_MS);
      continue;
    }
    const res = await fetch(
      `/csc/v2/signing-sessions/${encodeURIComponent(sessionId)}`,
      { headers: { Authorization: `Bearer ${bearerToken}` } }
    );
    if (!res.ok) throw new Error(`Polling selhalo: ${res.status}`);
    const body = await res.json();
    if (TERMINAL.has(body.status)) return body;
    await sleep(POLL_INTERVAL_MS);
  }
  throw new Error("Polling timed out");
}

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

Životnost access tokenu (typicky 10 minut) je nižší než maximální doba, po kterou uživatel může na autorizační stránce AS prodlévat. Pokud během pollingu narazíte na error: "invalid_token" (jakýkoli HTTP status — viz §5), token předtím vypršel — SCA jej obnoví přes refresh_token (§2.3) a polling pokračuje se stejným session_id. Polling neukončujte; relace na straně AS běží nezávisle na tokenu SCA.

Terminální odpověď s podpisem:

{
  "session_id": "8a52b3d4-...",
  "status": "signed",
  "delivery": "redirect",
  "expires_at": "2026-05-15T14:32:18.413Z",
  "authorize_url": "https://<as-host>/sign/...-opaque",
  "signed_payload": {
    "signatures": ["MIIE...base64-raw-signature..."]
  }
}

signatures je pole base64 řetězců v pořadí vstupních hashů. Pro RSA je každý prvek raw PKCS#1 v1.5 hodnota o délce key.len / 8 bajtů (např. 256 B pro 2048-bit klíč, 384 B pro 3072-bit klíč). Doplnění do CMS / XAdES / PAdES je odpovědnost SCA.

3.3 Stavy relace

Stav Terminální Význam
pending Ne Vytvořeno, čeká na uživatele
approved Ne Interní přechodný stav mezi ověřením faktorů a podpisem; při pollingu ho SCA zpravidla nezachytí.
signed Ano Podpis vystaven, signed_payload naplněn
denied Ano Uživatel zamítl nebo formulář nedokončil
expired Ano Vypršela platnost relace

4. Referenční tabulka endpointů

Všechny endpointy mají prefix /csc/v2. Autentizace je Bearer <access_token> v hlavičce Authorization, není-li uvedeno jinak.

Metoda Cesta Auth Popis
GET /info Metadata služby a podporované algoritmy.
POST /credentials/list Bearer Seznam ID podpisových klíčů uživatele.
POST /credentials/info Bearer Detail klíče a certifikát.
POST /signing-sessions Bearer (service) Založení SCAL2 relace.
GET /signing-sessions/{id} Bearer (service) Stav relace + výsledek.

Pod authorize_url (krok 5) běží sada interních endpointů /sign/* (autorizační formulář a OIDC callback) — volá je prohlížeč uživatele, SCA je nikdy nevolá přímo.

5. Chybové stavy a kódy

Odpovědi následují tvar OAuth2 chyb:

{ "error": "<kód>", "error_description": "<lidsky čitelný popis>" }

Rozhodujte podle pole error v JSON odpovědi — kód je stabilní součást kontraktu. error_description je diagnostické a může se měnit.

error Význam
invalid_request Špatně tvarovaný JSON, chybějící či neznámé pole, redirect_uri mimo Keycloak allowlist, nepodporovaný signAlgo, neznámý credentialID v request body.
invalid_token Bearer token chybí, vypršel, má neplatný podpis nebo špatného vystavovatele.
invalid_scope Token nemá scope service.
not_found Neznámé session_id při GET /signing-sessions/{id}.
server_error Vnitřní chyba služby (úložiště, downstream HSM).

HTTP status code závisí na endpointu (/credentials/list vrací 401 pro všechny chybové stavy, /credentials/info 400, POST /signing-sessions 500 pro server_error a 400 pro ostatní, GET /signing-sessions/{id} 404). SCA proto rozhoduje podle pole error, nikoli podle HTTP statusu.

Detail viz signing-sessions příklady.

6. Testovací prostředí a demo

Pro vývoj a integrační testy je k dispozici staging prostředí:

  • AS: https://remote-signing.staging.eidentity.cz
  • Demo (vzorová SCA): https://test-remote-signing.staging.eidentity.cz
  • IdP (Keycloak): https://keycloak.staging.eidentity.cz/realms/remote-signing

Demo aplikace plní dvě role:

  1. Sandbox pro koncové uživatele — umožňuje vyzkoušet kompletní flow bez integrace.
  2. Referenční implementace pro vývojáře SCA — zdrojový kód je veřejný a ukazuje minimální flow PKCE → list credentials → signing-session → polling → zobrazení podpisu.

Demo používá jeden testovací účet (demo@…) s předkonfigurovaným klíčem demo-credential-rsa. PIN a OTP odpovídají instrukcím v UI.

7. Strojově čitelná OpenAPI specifikace

Aktuální OpenAPI specifikace je generována přímo z definic endpointů v AS (Tapir → OpenAPI 3) a je dostupná na:

  • Swagger UI: https://<as-host>/docs (interaktivní, podpora „Try it out“ s vlastním Bearer tokenem)
  • Surový JSON: https://<as-host>/docs/docs.yaml

Z demo aplikace je odkaz dostupný v menu Dokumentace.


Pokud něco chybí nebo se v praxi narazí na nečekané chování, otevřete issue v repozitáři projektu nebo se obraťte na operátora služby.