Elektronický podpis XML: XMLDSig a XAdES

Podepsaný XML dokument nese podpis přímo v sobě — v elementu Signature. Kapitola vysvětluje, co podpis obsahuje, proč se dokument před podpisem kanonizuje, co přidává XAdES a proč podepsané XML nesmíš ani naformátovat.

Autor: Aktualizováno

K čemu je podpis XML

Elektronický podpis dokazuje dvě věci: kdo dokument podepsal (podpis jde ověřit jen jeho certifikátem) a že se dokument od podpisu nezměnil (i změna jediného znaku podpis zneplatní). U XML se k tomu používá standard W3C XML Signature (XMLDSig). Podpis je v něm zapsaný jako XML, takže může být přímo součástí podepsaného dokumentu a ten zůstane čitelný pro člověka i pro program.

V Česku se s podepsaným XML potkáš u elektronických faktur ISDOC, u dokumentů pro úřady a u zpráv z datových schránek — tam je ale XML podepsané jinou technikou (viz níže).

Element Signature

Podpis je element <Signature> ve jmenném prostoru http://www.w3.org/2000/09/xmldsig#. Ukázka má podpis uvnitř podepsaného dokumentu, jako je to u faktury ISDOC:

Zjednodušeno — hodnoty v Base64 jsou vynechané
<faktura xmlns="http://example.cz/faktura">
  <cislo>FV2026-042</cislo>
  <castka mena="CZK">12100.00</castka>
  <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
      <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
      <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
      <Reference URI="">
        <Transforms>
          <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
        </Transforms>
        <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
        <DigestValue>…otisk dokumentu v Base64…</DigestValue>
      </Reference>
    </SignedInfo>
    <SignatureValue>…podpis v Base64…</SignatureValue>
    <KeyInfo>
      <X509Data>
        <X509Certificate>…certifikát podepisujícího v Base64…</X509Certificate>
      </X509Data>
    </KeyInfo>
  </Signature>
</faktura>
ElementCo obsahuje
SignedInfoco se podepisuje a jak — právě tenhle element se ve skutečnosti podepisuje klíčem
CanonicalizationMethodjak převést SignedInfo na jednotný tvar, než se podepíše
SignatureMethodalgoritmus podpisu, např. RSA nebo ECDSA se SHA-256
Referenceodkaz na podepsaná data (URI="" je celý dokument) a jejich otisk
Transformsúpravy dat před výpočtem otisku, např. vynechání samotného podpisu
DigestMethod, DigestValuehashovací algoritmus a otisk podepsaných dat
SignatureValuesamotný podpis prvku SignedInfo
KeyInfocertifikát nebo klíč, kterým se podpis ověří

Program při podpisu nejdřív spočítá otisk (hash) podepisovaných dat a zapíše ho do DigestValue. Potom soukromým klíčem podepíše celý SignedInfo, takže podpis chrání otisk dat i volbu algoritmů. Při ověření se postup opakuje: program znovu spočítá otisk dat, porovná ho s DigestValue a veřejným klíčem z certifikátu ověří SignatureValue.

Obálkovaný, obalující a oddělený podpis

Podle toho, kde podpis leží vůči datům, rozlišuje XMLDSig tři případy:

DruhKde je podpisTypické použití
Obálkovaný (enveloped)uvnitř podepsaného dokumentufaktury ISDOC, dokumenty, které mají zůstat čitelné jako dřív
Obalující (enveloping)data jsou uvnitř elementu Signaturepodpis libovolných dat včetně ne-XML
Oddělený (detached)vedle dat nebo v jiném souborupodpis souboru, který se nesmí měnit, nebo části zprávy (SOAP)

U obálkovaného podpisu nastává zdánlivý paradox: podpis je součástí dokumentu, který podepisuje. Řeší ho transformace enveloped-signature, která element Signature před výpočtem otisku z dat vyřadí.

Kanonizace: proč na zápisu záleží

Stejná data jdou v XML zapsat mnoha způsoby: jiné pořadí atributů, jednoduché místo dvojitých uvozovek, <poznamka/> místo <poznamka></poznamka>, číselný odkaz místo znaku. Pro parser jde o totéž, pro hashovací funkci o úplně jiné bajty. Proto se data před výpočtem otisku převádějí do kanonického tvaru (Canonical XML, C14N). Tyhle dva zápisy:

Zápis A
<?xml version="1.0" encoding="UTF-8"?>
<faktura  mena='CZK' cislo="42" >
  <poznamka/>
  <castka>1&#48;0</castka>
</faktura>
Zápis B
<faktura cislo="42" mena="CZK">
  <poznamka></poznamka>
  <castka>100</castka>
</faktura>

mají po kanonizaci shodný tvar — ten, který ukazuje zápis B. Kanonizace odstraní deklaraci XML, seřadí atributy, sjednotí uvozovky a prázdné elementy a nahradí číselné odkazy znaky. Vyzkoušet si to můžeš příkazem xmllint --c14n soubor.xml.

Pro podpis části dokumentu se používá exkluzivní kanonizace (Exclusive C14N). Ta do podepsaného fragmentu nezahrne deklarace jmenných prostorů, které fragment nepoužívá, takže podpis vydrží i přesunutí fragmentu do jiného dokumentu — typicky u zpráv SOAP.

Podepsané XML neformátuj. Kanonizace neřeší bílé znaky v obsahu elementů. Když podepsaný soubor odsadíš ve formátovači, uložíš s jiným kódováním nebo ho otevřeš a uložíš v editoru, který zapisuje po svém, podpis přestane platit. Ke kontrole obsahu formátuj kopii a originál nech beze změny.

XAdES: podpis s časem a dlouhou platností

Samotný XMLDSig neříká, kdy byl dokument podepsán, a po vypršení certifikátu je těžké podpis ověřit. Rozšíření XAdES (XML Advanced Electronic Signatures) od evropského institutu ETSI přidává do elementu Signature podepsané vlastnosti a časová razítka. Spolu s CAdES a PAdES je to jeden z formátů, které pro zaručené a kvalifikované podpisy uznává nařízení eIDAS.

Úroveň (ETSI EN 319 132)Starší označeníCo přidává
XAdES-B-BXAdES-BESpodepsané vlastnosti: čas podpisu a otisk podpisového certifikátu
XAdES-B-TXAdES-Tčasové razítko od důvěryhodné autority — dokazuje, že podpis existoval v daném okamžiku
XAdES-B-LTXAdES-X-Lcertifikáty a informace o jejich platnosti, aby šel podpis ověřit i po letech
XAdES-B-LTAXAdES-Aarchivní razítka, která se obnovují dřív, než zastarají použité algoritmy

Vlastnosti XAdES leží v elementu <xades:QualifyingProperties> uvnitř <Object> a mají vlastní jmenný prostor http://uri.etsi.org/01903/v1.3.2#.

Příklad z praxe: podpis faktury ISDOC

Specifikace ISDOC 6.0.2 předepisuje, jak má podepsaná faktura vypadat:

  • podpis odpovídá XMLDSig a je obálkovaný (transformace enveloped-signature),
  • element Signature je vždy posledním elementem uvnitř kořene faktury,
  • otisk se počítá algoritmem z rodiny SHA-2,
  • fakturu může podepsat vystavitel i příjemce; každý další podpis pomocí transformace XPath vynechá podpisy přidané po něm, takže nový podpis neporuší ty předchozí,
  • časové razítko má formu XAdES-T.

Pozor: datová zpráva ZFO je podepsaná jinak

Soubor ZFO z datové schránky obsahuje XML, ale podpis v něm není XMLDSig. Celé XML je zabalené do binární obálky s podpisem CAdES (formát odvozený z PKCS#7). Proto ZFO v textovém editoru ani XML nástroji neotevřeš a potřebuješ aplikaci, která obálku rozbalí.

Ověření podpisu a bezpečnost

Podpis XML si nepiš sám. Vedle kryptografie musíš správně provést kanonizaci, transformace a vyhodnocení odkazů a každá odchylka znamená buď falešné chyby, nebo díru. Použij hotovou knihovnu: xmlsec1 (nad libxml2), Apache Santuario v Javě, System.Security.Cryptography.Xml.SignedXml v .NET nebo signxml v Pythonu. Běžný uživatel ověří podpis faktury nebo dokumentu v programu, který s ním pracuje.

Útok přebalením podpisu: platný podpis říká jen to, že odkazovaná data jsou nezměněná. Útočník může dokument upravit tak, že podpis dál ukazuje na původní element, ale aplikace čte jiný, podvržený. Po ověření proto zpracuj přesně ten element, na který podpis odkazuje, a ne první, který najdeš.

Jak zabránit signature wrapping

Úspěšné kryptografické ověření nesmí vracet jen true. Aplikace potřebuje ověřený referencovaný uzel. Odmítni duplicitní ID, omez očekávaný počet a umístění podpisů i povolené transformace. Nepoužívej po ověření nový obecný XPath, který může vybrat podvrženou kopii. Zároveň ověř důvěru certifikátu podle profilu aplikace.

W3C XML Signature Best Practices popisuje tuto vazbu mezi ověřením a zpracováním. Podrobnosti bajtové reprezentace najdeš v kanonizaci XML.

Spustitelný podpis a odmítnutí změněného obsahu

SignDemo.java — standardní JDK XML Digital Signature API
import java.security.*;
import java.util.List;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.crypto.dsig.*;
import javax.xml.crypto.dsig.dom.*;
import javax.xml.crypto.dsig.spec.*;
import org.w3c.dom.*;
public class SignDemo {
  static boolean verify(Document doc, PublicKey key) throws Exception {
    Node signature = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature").item(0);
    var context = new DOMValidateContext(key, signature);
    context.setProperty("org.jcp.xml.dsig.secureValidation", Boolean.TRUE);
    var factory = XMLSignatureFactory.getInstance("DOM");
    return factory.unmarshalXMLSignature(context).validate(context);
  }
  public static void main(String[] args) throws Exception {
    var builder = DocumentBuilderFactory.newDefaultInstance();
    builder.setNamespaceAware(true);
    var doc = builder.newDocumentBuilder().newDocument();
    var root = doc.createElement("zprava"); doc.appendChild(root);
    var text = doc.createElement("text"); text.setTextContent("XML"); root.appendChild(text);
    var generator = KeyPairGenerator.getInstance("RSA"); generator.initialize(2048);
    var pair = generator.generateKeyPair();
    var factory = XMLSignatureFactory.getInstance("DOM");
    var ref = factory.newReference("", factory.newDigestMethod(DigestMethod.SHA256, null),
      List.of(factory.newTransform(Transform.ENVELOPED, (TransformParameterSpec)null)), null, null);
    var info = factory.newSignedInfo(
      factory.newCanonicalizationMethod(CanonicalizationMethod.EXCLUSIVE, (C14NMethodParameterSpec)null),
      factory.newSignatureMethod(SignatureMethod.RSA_SHA256, null), List.of(ref));
    factory.newXMLSignature(info, null).sign(new DOMSignContext(pair.getPrivate(), root));
    System.out.println(verify(doc, pair.getPublic()));
    text.setTextContent("Změněno");
    System.out.println(verify(doc, pair.getPublic()));
  }
}
Bez další knihovny; výstup true a false
javac SignDemo.java
java SignDemo

Program vytvoří vlastní dokument a pár klíčů. Reference s prázdným URI zde odkazuje na dokument; enveloped transform odstraní podpis ze vstupu jeho vlastního otisku. SignedInfo stanoví SHA-256 a RSA-SHA256 i kanonizaci. Ověření používá předem známý veřejný klíč, nikoli slepě převzatý klíč z KeyInfo cizího dokumentu.

První kontrola projde. Změna textu poruší digest a nové ověření vrátí false. Při druhé kontrole vytváříme nový validační objekt, abychom nepoužili dřívější uložený výsledek. Jde o demonstraci integrity s klíčem v paměti, nikoli kvalifikovaný elektronický podpis či ověření certifikačního řetězce.

Pro přijímané dokumenty bezpečně nastav XML parser, omez algoritmy a externí URI a ověř počet, umístění a reference podpisu podle konkrétního profilu. Teprve úspěšně ověřená referencovaná data předávej aplikaci. Tento příklad podepisuje vlastní celý dokument, takže neřeší obecný výběr uzlu podle ID. Zdrojem API je JDK XML Signature tutorial, pravidel XML Signature 1.1.