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
- Předpoklady a registrace
- Autentizace klienta — OAuth2 + PKCE přes Keycloak
- Signing Sessions (SCAL2)
- Referenční tabulka endpointů
- Chybové stavy a kódy
- Testovací prostředí a demo
- 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_tokenje podepsaný JWT — claimsubnese 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:
- Sandbox pro koncové uživatele — umožňuje vyzkoušet kompletní flow bez integrace.
- 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.