Adresát: epodpora@fs.gov.cz
Předmět: Podpora CORS na SOAP endpointu EET 2.0 (a dotaz k podepisování přes Web Crypto v prohlížeči)
Dobrý den,
píšu kvůli dvěma věcem kolem EET 2.0 rozhraní – jedno je konkrétní technické zjištění, druhé spíš dotaz. Vyvíjím klientské řešení pro elektronickou evidenci tržeb a rád bych ho mohl provozovat i jako čistě webovou aplikaci (PWA), bez nutnosti stavět vlastní backend jen kvůli přeposílání požadavků.
Při testování jsem narazil na to, že SOAP endpoint (https://pg.trzbyeet.gov.cz:443/eet/services/EETServiceSOAP/v4, produkční by na tom měl být stejně) nevrací žádné CORS hlavičky. Zkoušel jsem poslat preflight požadavek:
OPTIONS /eet/services/EETServiceSOAP/v4 HTTP/1.1
Origin: https://libovolna-domena.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type,soapaction
V odpovědi nejsou vůbec žádné Access-Control-* hlavičky. Protože request OdeslaniTrzby posílá Content-Type: text/xml a vlastní hlavičku SOAPAction, nejde o tzv. „simple request“ – prohlížeč proto vždy pošle napřed tenhle preflight a bez správné odpovědi na něj neodešle ani ten samotný POST. Endpoint tak dnes nejde zavolat přímo z JavaScriptu v prohlížeči, ať už z jakékoli domény.
Podle mě by stačilo doplnit (klidně jen na úrovni reverse proxy, bez zásahu do aplikace samotné):
Access-Control-Allow-Origin: *– vzhledem k tomu, že autorizace stojí na podpisu těla zprávy, ne na Origin nebo cookies, by wildcard neměl znamenat bezpečnostní riziko (rozvádím níže)Access-Control-Allow-Methods: POST, OPTIONSAccess-Control-Allow-Headers: Content-Type, SOAPActionAccess-Control-Expose-Headers: X-Global-Transaction-Id, ať se k němu dostane i klient běžící v prohlížeči- případně
Access-Control-Max-Age, ať se preflight nemusí opakovat při každém requestu
Druhá věc je spíš dotaz než hlášení chyby. Všiml jsem si, že EET 2.0 už klienta neověřuje přes mTLS jako starší verze, ale certifikát i podpis (wsse:BinarySecurityToken, ds:Signature nad soap:Body) jsou součástí samotné SOAP zprávy – jde tedy o podpis na úrovni aplikace, ne o TLS handshake. Pokud tomu rozumím správně, znamená to, že sestavení a podpis zprávy (SHA-256 otisk, RSA-SHA256 přes crypto.subtle) by šlo spustit stejně dobře v prohlížeči jako na serveru.
Byl bych vděčný za písemné potvrzení, že tohle je z vaší strany podporovaný způsob integrace, ať vím, že se na to jako dodavatel pokladního softwaru můžu spolehnout. A pokud proti tomu něco mluví, budu rád za vysvětlení, ať to nezjistím až za provozu.
Bez CORS jsem prakticky pořád nucený stavět mezi pokladnu a EET vlastní backend, i tam, kde by čistě webové řešení bylo pro obchodníka jednodušší a levnější – hlavně u drobných a sezónních prodejců. Proč mi na tom tolik záleží, rozepisuji v příloze, ať e-mail zbytečně nenafukuji; rád to doplním i telefonicky.
Díky za posouzení. Test, kterým jsem ověřoval chybějící CORS hlavičky, mám uložený a můžu poslat, ať si to můžete sami zopakovat.
Pár věcí, které bych uvedl, kdyby se ptali, proč to řeším takhle a ne přes běžný backend.
Appky do App Store i Google Play jsou pro pokladní software reálná komplikace, ne jen papírování. Apple má pravidlo 4.2.6 (a celou sekci o „minimum functionality“), které míří přesně na aplikace postavené jako obal kolem webu – a to je přesně případ pokladní PWA v nativním kabátu. Riziko, že review appku zamítne, je reálné, ne teoretické. K tomu Developer Program stojí 99 dolarů ročně za právnickou osobu – u dodavatele s desítkami klientů nebo franšíz se to buď násobí s každým dalším účtem, nebo to narazí na zákaz duplicitních aplikací, pokud by chtěl vystačit s jedním. Google Play zase od roku 2023 vyžaduje u nových účtů uzavřené testování (aspoň 20 testerů, 14 dní), než appku vůbec pustí ven, což první nasazení protáhne o týdny. A samotný review cyklus – běžně 24 až 48 hodin, ale při zamítnutí klidně déle – je v přímém rozporu s tím, jak rychle je občas potřeba zareagovat na změnu v EET rozhraní nebo opravit chybu.
K tomu se váže ještě jedna věc, která není o rychlosti ani o penězích, ale o tom, že to pro někoho nejde. U řady kategorií aplikací – typicky přesně u finančního nebo platebního softwaru – Apple i Google vyžadují organizační účet: s D-U-N-S číslem, registrovanou firmou a ověřením, ne běžný „individuální“ účet fyzické osoby. Spousta menších dodavatelů pokladního softwaru, jednotlivých OSVČ i part-time/open-source vývojářů ale žádnou takovou firmu nemá a zakládat ji jen kvůli jedné appce nedává smysl. Pro tyhle případy PWA není jen pohodlnější varianta, ale jediná reálně dostupná – web může publikovat kdokoli, bez ohledu na to, jestli je OSVČ, firma, nebo jen nadšenec, co si udělal řešení pro sebe nebo pár známých.
Další věc je rozsah zařízení, na kterých pokladny běžně jedou – staré Windows tablety, levné Android krabičky, Chromebooky, obyčejné PC s prohlížečem. Nativní appka znamená tři samostatné buildy a tři QA cykly místo jednoho, a u menších dodavatelů (a těch je u EET spousta) to náklady znásobí. Instalace appky mimo store navíc u běžného uživatele (OSVČ, stánkaře, prodejce na farmářském trhu) vyvolá bezpečnostní varování a nedůvěru – otevření webové stránky ne.
Web/PWA se dá aktualizovat okamžitě na straně serveru, appka čeká na schválení storu. Při další změně EET rozhraní (a přechod z EET 1.0 na 2.0 ukázal, že se to stát může) je tohle zásadní rozdíl v tom, jak rychle dokáže celý ekosystém pokladen zareagovat.
Nižší vstupní bariéra taky znamená víc konkurence a levnější řešení pro drobné podnikatele. Nativní vývoj vyžaduje Apple hardware, Xcode, roční poplatky a znalost dvou dalších platforem navíc k webu, což zvýhodňuje velké dodavatele a vytlačuje menší nebo open-source projekty přesně z těch levných řešení, která malí podnikatelé potřebují nejvíc.
Sezónní a příležitostní prodejci – farmářské trhy, food trucky, jednorázové akce – jsou další případ: nikdo z nich nechce instalovat appku na půjčený tablet kvůli pár prodejům do roka. Webová stránka jim stačí a funguje okamžitě na čemkoli po ruce.
Je v tom i obecnější riziko: stavět kus státní daňové infrastruktury na tom, že Apple nebo Google appku neshodí ze storu kvůli změně pravidel (nebo omylem), je zbytečná závislost, které se dá webem/PWA z principu vyhnout.
A není to bez precedentu ani v rámci EU – řada bankovních a finančních API (typicky v rámci PSD2/open bankingu) CORS podporuje záměrně, aby šly volat přímo z prohlížeče, a to u autentizace, která je řádově citlivější než u EET, protože stojí na cookies/session. U EET je identita daná podpisem zprávy, takže je to ve výsledku jednodušší případ.
A nakonec – EET 2.0 sám udělal podobný krok, když opustil mTLS ve prospěch podpisu na úrovni SOAP zprávy. Povolení CORS je tak spíš pokračování stejné cesty ke snazší integraci, ne nějaká výjimka z ní.
„Bez CORS je to bezpečnější, brání to volání z cizí domény.“ – Jenže CORS tu vůbec nefunguje jako autentizace. O tom, jestli je request platný, rozhoduje výhradně podpis těla zprávy privátním klíčem poplatníka, a ten útočník bez klíče nepadělá, ať je CORS povolený nebo ne. CORS řeší jen to, jestli si JavaScript na stránce smí přečíst odpověď a jestli prohlížeč vůbec pošle preflight – na platnost requestu nemá žádný vliv. Zákaz CORS tedy bezpečnost nijak nezvyšuje, jen brání legitimním integracím.
„Nechceme, aby soukromý klíč poplatníka byl vystavený v prohlížeči.“ – To je rozumná obava obecně, ale řešit by ji měl dodavatel pokladního softwaru svým key managementem, ne API tím, že plošně zablokuje CORS. Web Crypto navíc umožňuje klíče s extractable: false, takže syrová data klíče z prohlížeče nikdy neodejdou – JavaScript vidí jen výstup operace sign(). A bez CORS se dnes stejně skoro vždycky staví backend proxy, který typicky ten samý klíč drží na serveru, takže zákaz CORS realitu key managementu nijak neřeší, jen všem přidává povinnou infrastrukturu navíc.
„CORS přidává provozní komplexitu a riziko špatné konfigurace.“ – Reálně jde o pár statických hlaviček na proxy vrstvě, ne o zásah do aplikace. A protože autorizace nestojí na Origin ani na cookies, Access-Control-Allow-Origin: * je tu bezpečně použitelný stejně jako u spousty veřejných REST/SOAP API bez session.
„Chceme mít přehled o tom, kdo s API komunikuje.“ – Origin hlavička k tomu nikdy sloužit nemohla, jde si ji nastavit z jakéhokoli klienta mimo prohlížeč (třeba z tohohle SDK, když běží na serveru). Skutečná identita klienta je dnes daná certifikátem v podpisu a dá se dohledat přes uuid_zpravy a X-Global-Transaction-Id, ať už je CORS povolený, nebo ne.
„Nemáme kapacitu měnit produkční infrastrukturu kvůli okrajovému use-case.“ – Nejde o okrajovku, týká se to celého segmentu malých a sezónních prodejců, pro které je appka nepřiměřená bariéra (viz výše). A technicky jde o jednorázovou úpravu hlaviček na existujícím endpointu, ne o přepracování API.
„Naše API je pro server-to-server integraci, ne pro prohlížeč.“ – Přesně o rozšíření tohohle rozsahu žádám, ne o to, že současný návrh je špatně. Rád doplním use-case, nebo se sejdu na technické konzultaci, pokud by to pomohlo se rozhodnout.
„Ať si dodavatelé bez firmy založí organizační účet, to je jejich problém, ne náš.“ – Tím se ale jen přesouvá přesně ta bariéra, o které mluvím výše – malé a nezávislé řešení tím rovnou vypadává ze hry a zůstávají jen ti, kdo si registraci a provoz organizačního účtu u Applu nebo Googlu můžou dovolit. Nežádám, aby Finanční správa řešila něco za Apple nebo Google, jen aby fungování EET nestálo na tom, že si každý dodavatel musí napřed vyřídit firemní účet u dvou zahraničních platforem.
S pozdravem, Josef Kříž