XAdES-BES – algorytm zorientowany na PHP

UWAGA! Ten wpis ma już 8 lat. Pewne stwierdzenia i poglądy w nim zawarte mogą być nieaktualne.

Niedawno w pracy zetknąłem się z koniecznością podpisywania cyfrowego komunikatów przesyłanych przez jedną z naszych aplikacji. Akceptowanym przez odbiorcę formatem podpisu jest XAdES-BES. Po dłuższym przeszukiwaniu Google’a sprawa nie wyglądała dobrze – nie ma żadnej gotowego rozwiązania dla PHP umożliwiającego złożenie takiego podpisu. Co najgorsze, nie mieliśmy pojęcia o sposobie “ręcznego” tworzenia takiego podpisu, a kilkudziesięciostronicowa specyfikacja bez choćby jednego przykładu nie zachęcała do lektury. W sieci nie było też ani jednego snippeta, na którym można by się oprzeć.

tl;dr
Co znajdziesz w tym wpisie?

  • opis sposobu wygenerowania podpisu w formacie XAdES-BES enveloping (w którym do podpisu dołączana jest podpisywana treść) na konkretnym przykładzie, krok po kroku,
  • przykłady na realizację poszczególnych kroków w języku PHP,
  • wybór przydatnych linków.

Czego nie znajdziesz w tym wpisie?

  • gotowej implementacji generowania podpisu w PHP,
  • opisu generowania podpisu enveloped (takiego, który dołączany jest do dokumentu).

Temat nie rokował zbyt dobrze, a czas gonił. Po głębszym zbadaniu temat wydawał się ciekawy, więc postanowiłem przebić się przez oficjalną specyfikację. Dokumentu jednak nie polecam – jak się później okazało, była to najmniej przydatna dla mnie lektura.

Co ciekawe, dużo poukładał mi w głowie lakoniczny artykuł na Wikipedii, który w zasadzie w jednym zdaniu opisywał XAdES:

XAdES jest rozwinięciem XML-DSig, wypełnia pole ds:Object, powiązany jest z ds:SignedInfo za pomocą elementu ds:Reference wskazującego na SignedProperties.

Skierowałem się więc w stronę XML-DSig. Tutaj już z materiałami było znacznie lepiej, a wiedząc o tym, że XAdES to w zasadzie XML-DSig z dodatkowymi informacjami, mogłem przystąpić do własnych prób podpisania czegokolwiek, na próbę.

Nieocenioną pomocą był ten opis, który zawierał strukturę przykładowego podpisu wraz ze sposobem wyliczania wartości digest. Dokładnie przeanalizowałem ten dokument i udało mi się (po dłuższych walkach) odtworzyć identyczne wartości. Potem sam podpisałem dokument za pomocą aplikacji Szafir Krajowej Izby Rozliczeniowej (głównie przez to, że jest używana przez odbiorcę do weryfikacji oraz ze względu na istnienie wersji pod Linuksa).

Podpisywana treść

Będziemy podpisywać następującego XML-a:

<?xml version="1.0" encoding="utf-8" ?>
<test>
  <test>testowy węzeł</test>
  <test>drugi testowy węzeł</test>
</test>

Sama treść nie jest jednak ważna, ponieważ całość zakodujemy i tak w BASE64.

Struktura podpisu

Aby nie przedłużać, tak wygląda podpisana wyżej treść po sformatowaniu:

<?xml version="1.0" encoding="UTF-8"?>
<Signatures Id="ID-36e3801c-b360-4958-86e5-0b82cbd2d366">
  <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="ID-88c32693-8f48-4cc8-9764-d10f0fea51b5">
    <ds:SignedInfo Id="ID-4c61b0d9-e5da-4eea-97a1-623b9e4fa2b2">
      <ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
      <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
      <ds:Reference Id="ID-888d7eba-e17b-4f0c-8d5b-efa29359ebd7" URI="#ID-78a661eb-d6b2-4776-9b06-3d9e0afadab6">
        <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
        <ds:DigestValue>MLaZQl/toVO57dJsz1JfLIyrrd4=</ds:DigestValue>
      </ds:Reference>
      <ds:Reference Id="ID-ad71d14c-447c-44a3-a00c-2e902bb73a03" URI="#ID-9542235a-b703-477a-8702-8999387d943e" Type="http://uri.etsi.org/01903#SignedProperties">
        <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
        <ds:DigestValue>R2SnnM9tlyt4HZtC2qGWKB2XIUY=</ds:DigestValue>
      </ds:Reference>
    </ds:SignedInfo>
    <ds:SignatureValue Id="ID-c83aa77f-c39b-45ba-9368-25ba362bc57a">Td43...</ds:SignatureValue>
    <ds:KeyInfo>
      <ds:KeyValue>
        <ds:RSAKeyValue>
          <ds:Modulus>...</ds:Modulus>
          <ds:Exponent>...</ds:Exponent>
        </ds:RSAKeyValue>
      </ds:KeyValue>
      <ds:X509Data>
        <ds:X509Certificate>...</ds:X509Certificate>
      </ds:X509Data>
    </ds:KeyInfo>
    <ds:Object>
      <xades:QualifyingProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" Id="ID-e42ab543-3f7f-4473-9dba-6cb105b5ce27" Target="#ID-88c32693-8f48-4cc8-9764-d10f0fea51b5">
        <xades:SignedProperties Id="ID-9542235a-b703-477a-8702-8999387d943e">
          <xades:SignedSignatureProperties>
            <xades:SigningTime>2016-07-29T19:03:50Z</xades:SigningTime>
            <xades:SigningCertificate>
              <xades:Cert>
                <xades:CertDigest>
                  <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
                  <ds:DigestValue>...</ds:DigestValue>
                </xades:CertDigest>
                <xades:IssuerSerial>
                  <ds:X509IssuerName>...</ds:X509IssuerName>
                  <ds:X509SerialNumber>...</ds:X509SerialNumber>
                </xades:IssuerSerial>
              </xades:Cert>
            </xades:SigningCertificate>
          </xades:SignedSignatureProperties>
          <xades:SignedDataObjectProperties>
            <xades:DataObjectFormat ObjectReference="#ID-888d7eba-e17b-4f0c-8d5b-efa29359ebd7">
              <xades:Description>Dokument w formacie xml [XML]</xades:Description>
              <xades:MimeType>text/plain</xades:MimeType>
              <xades:Encoding>http://www.w3.org/2000/09/xmldsig#base64</xades:Encoding>
            </xades:DataObjectFormat>
          </xades:SignedDataObjectProperties>
        </xades:SignedProperties>
      </xades:QualifyingProperties>
    </ds:Object>
    <ds:Object Encoding="http://www.w3.org/2000/09/xmldsig#base64" Id="ID-78a661eb-d6b2-4776-9b06-3d9e0afadab6" MimeType="text/plain">PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+Cjx0ZXN0PgogIDx0ZXN0PnRlc3Rv
d3kgd8SZemXFgjwvdGVzdD4KICA8dGVzdD5kcnVnaSB0ZXN0b3d5IHfEmXplxYI8L3Rlc3Q+CjwvdGVz
dD4=</ds:Object>
  </ds:Signature>
</Signatures>

Pozwoliłem sobie wyciąć dane o certyfikacie i wartości innych węzłów, których nie chcę upubliczniać.

Treść dokumentu w podpisie

Treść zawiera się w węźle:

<ds:Object Encoding="http://www.w3.org/2000/09/xmldsig#base64" Id="ID-78a661eb-d6b2-4776-9b06-3d9e0afadab6" MimeType="text/plain">PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+Cjx0ZXN0PgogIDx0ZXN0PnRlc3Rv
d3kgd8SZemXFgjwvdGVzdD4KICA8dGVzdD5kcnVnaSB0ZXN0b3d5IHfEmXplxYI8L3Rlc3Q+CjwvdGVz
dD4=</ds:Object>

Zgodnie z atrybutami, jest zakodowana przy użyciu base64 – możesz sprawdzić, czy powyższa wartość jest zgodna z wynikiem funkcji base64_encode()  z PHP.

Węzeł posiada unikalne ID – nie musi być akurat w takim formacie, jak powyżej, jednak zapisz sobie gdzieś to ID – będzie potrzebne do odwołań podpisu.

Pierwsze odwołanie w SignedInfo

Pierwsze odwołanie w SignedInfo dotyczy właśnie opisanego wyżej węzła z treścią i wygląda następująco:

<ds:Reference Id="ID-888d7eba-e17b-4f0c-8d5b-efa29359ebd7" URI="#ID-78a661eb-d6b2-4776-9b06-3d9e0afadab6">
  <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
  <ds:DigestValue>MLaZQl/toVO57dJsz1JfLIyrrd4=</ds:DigestValue>
</ds:Reference>

Atrybut “URI” wskazuje właśnie na ID węzła z treścią, który kazałem Ci zapisać. Odwołanie zawiera dwa węzły: DigestMethod oraz DigestValue. Wartość DigestValue jest obliczana przy pomocy funkcji skrótu określonej w DigestMethod, czyli w tym przypadku SHA1.

Zanim jednak zaczniesz liczyć skrót z treści, musisz zwrócić na węzeł znajdujący się trochę wyżej:

<ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />

Mówi on o tym, że musisz doprowadzić węzeł do formy kanonicznej, w tym przypadku za pomocą C14N.

Jak to zrobić? W PHP DOMDocument posiada do tego gotową metodę. Wystarczy, że wczytasz treść gałęzi i użyjesz metody C14N() :

$dom = new DOMDocument();
$dom->loadXML($content);
$canonicalizedXml = $dom->C14N();

Taką treść możemy już poddać funkcji skrótu – w PHP sha1(), po czym należy zakodować wynik w BASE64. Jeżeli jednak zrobisz to w ten sposób, nie otrzymasz prawidłowego wyniku:

$hash = sha1($canonicalizedXml);
$digestValue = base64_encode($hash);

W BASE64 musimy zakodować binarną wartość skrótu, więc musisz podać true jako drugi argument funkcji sha1() :

$hash = sha1($canonicalizedXml, true);
$digestValue = base64_encode($hash);

Wartość w $digestValue w moim przypadku powinna być zgodna z tą w treści XML:

MLaZQl/toVO57dJsz1JfLIyrrd4=

DigestValue pierwszego odwołania za nami!

Drugie odwołanie w SignedInfo

Drugie odwołanie w SignedInfo wskazuje na węzeł SignedProperties.

Jako, iż mamy wyliczyć skrót, zakładamy, że na tym etapie mamy już wypełnioną całą treść tego węzła. Jak ją wypełnić pokażę później.

I tutaj ważna uwaga – ze względu na to, że w treści SignedProperties występują elementy zarówno z węzła z namespace xades, jak i ds, to nie możemy po prostu wyciąć go tak, jak przy poprzednim odwołaniu. Musimy wybrać przy pomocy DOMDocument tę konkretną wartość i przekształcić na formę kanoniczną tak, aby metoda C14N()  miała dostęp do kontekstu XML-a i mogła na tej podstawie wstawić w SignedProperties dwa atrybuty xmlns. Powinno wyglądać to mniej więcej tak:

$dom = new DOMDocument();
$dom->loadXML($fullContent);
$node = $dom->getElementsByTagName('SignedProperties')->item(0)->C14N();

Z takiej formy węzła SignedProperties znów liczymy skrót przy pomocy SHA1 (bo DigestMethod ma taką samą wartość, jak w poprzednim odwołaniu):

$hash = sha1($node, true);
$digestValue = base64_encode($hash);

W $digestValue powinienem otrzymać (dla danych mojego certyfikatu, itp.):

R2SnnM9tlyt4HZtC2qGWKB2XIUY=

Kolejne DigestValue za nami.

Właściwy podpis

Mając SignedInfo z wyliczonymi wartościami digest, możemy przejść do liczenia właściwego podpisu.

Metoda jego liczenia jest określona wewnątrz SignedInfo poprzez:

<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />

W tym wypadku nie użyjemy już zwykłego SHA1, jednak jest to najprostsza część wyliczeń – w PHP możemy skorzystać z funkcji openssl_sign() :

$privateKey = openssl_pkey_get_private('file://path');
        
$actualDigest = null;
openssl_sign(
    $signedInfo, 
    $actualDigest, 
    $privateKey, 
    'sha1WithRSAEncryption'
);

$actualDigestEncoded = base64_encode($actualDigest);

Kilka uwag:

  • otwieramy plik w formacie PEM zawierający klucz prywatny, którym podpiszemy zawartość SignedInfo (pamiętaj o “file://” na początku, zaś przy podawaniu ścieżki na uniksach o tym, że powinny tam być trzy slashe, np.: “file:///var/cert.pem”),
  • do $actualDigest zostanie zwrócona binarny wynik szyfrowanej funkcji skrótu,
  • w $signedInfo podajemy formę kanoniczną węzła SignedInfo.

Na tym kończy się generowanie podpisu cyfrowego XAdES-BES. Z racji tego, że nie przedstawiłem wcześniej szczegółów wypełniania wartości dot. certyfikatu, informacje na ten temat zostawiam na koniec.

Retrospekcja – dane certyfikatu

We wpisie chciałem skoncentrować się na sposobie wyliczania wartości digest dla XAdES-BES, dlatego przedstawiłem te informacje w pierwszej kolejności. Jeżeli jednak masz problem z uzyskaniem danych certyfikatu występujących w pozostałych węzłach, to ta część może być dla Ciebie przydatna.

  • DigestValue w CertDigest

Wartość liczymy z treści certyfikatu w PEM po wycięciu markerów:

$certResource = openssl_x509_read('file://path');
$certPem = null;
openssl_x509_export($certResource, $certPem);
$certContent = str_replace('-----BEGIN CERTIFICATE-----', '', $certPem);
$certContent = trim(str_replace('-----END CERTIFICATE-----', '', $certContent));

$certFingerprint = base64_encode(sha1(base64_decode($certContent), true));
  • X509Certificate

Wartość mamy w zmiennej $certContent z poprzedniego punktu.

  • Modulus i Exponent z RSAKeyValue
$publicKey = openssl_pkey_get_public('file://path');
                
$data = openssl_pkey_get_details($publicKey);
        
$modulusHex = '00' . bin2hex($data['rsa']['n']);
$modulusEncoded = base64_encode(hex2bin($modulusHex));
        
$exponentEncoded = base64_encode($data['rsa']['e']);

Zauważyłem, że zwrócone $data[‘rsa’][‘n’] nie zawiera bajtu zerowego na początku, dlatego dopisałem go ręcznie. Nie udało mi się jednak ustalić, z czego to wynika – czy z używanej przez nas wersji PHP, z jakiegoś błędu, czy po prostu tak ma być, dlatego porównaj wyliczone wartości z wyjściem openssl:

openssl rsa -noout -text -in [public_key_filename]
  • X509SerialNumber
$certData = openssl_x509_parse($certResource);
$serialNumber = $certData['serialNumber'];
  • IssuerName
$issuerArray = array();
foreach($certData['issuer'] as $issueKey => $issue) {
    $issuerArray[] = $issueKey . '=' . $issue;
}
$issuer = implode(',', $issuerArray);

Podsumowanie

Mam nadzieję, że powyższe informacje przydadzą się chociaż częściowo osobom, które zupełnie nie znają formatu XAdES-BES, a muszą wprowadzić go w swoich aplikacjach.

Pamiętaj, że podane w treści wpisu fragmenty kodu mają na celu jedynie zobrazowanie sposobu uzyskiwania celu, zaś produkcyjny kod powinien brać pod uwagę wszelkie możliwe błędy, które mogą wystąpić podczas całego procesu.

Jeżeli coś jest niejasne lub o czymś zapomniałem – daj znać w komentarzu, postaram się to poprawić.

36 komentarzy do “XAdES-BES – algorytm zorientowany na PHP”

  1. Świetna robota!!! Artykuł bardzo pomocny!

    Mały błąd w przykładowym kodzie, zamiast “$node->” powinno być “$dom->”:

    $node = $node->getElementsByTagName(‘SignedProperties’)->item(0)->C14N();

    Pozdrawiam 🙂

  2. Próbuję powtórzyć digestvalue, używając C#, pierwsze odwołanie w signedInfo, w końcu się udało. C# XmlDocument.looad nie przenosi spacji;
    trzeba użyć PreserveWhitespace = true; Wartość wyliczonego w ten sposób skrótu zgadza się z wyliczoną przez oprogramowaniem Pemi.

    Teraz ładuję wygenrowany przez Pemi węzeł “xades:SignedProperties”, przepuszczam przez XmlDsigC14NTransform, wyliczam hash SHA1, konwwertuję do base64 i niestety mam inny wynik.
    Zastanawiałem się jak powina wyglądać forma kanoniczna węzła signedProperties; piszesz, że to “Musimy wybrać przy pomocy DOMDocument tę konkretną wartość i przekształcić na formę kanoniczną tak, aby metoda C14N() miała dostęp do kontekstu XML-a i mogła na tej podstawie wstawić w SignedProperties dwa atrybuty xmlns.”
    W twoim przykładowym kodzie nie widzę przekształcenia węzła na formę kanoniczną przed czytanie przez metodę C14N().
    Nie wiem niestety jak powinna wyglądać ta forma węzła ???
    Mój kod w C# – podobny do twojego a jednak wynik daje inny:

    XmlDocument myDoc = new XmlDocument();
    myDoc.PreserveWhitespace = true;
    myDoc.Load(“C:\\PIT_11\\2011\\101352154.xml.xades”);
    XmlDocument doc = new XmlDocument();
    doc.PreserveWhitespace = true;
    doc.LoadXml(myDoc.OuterXml);

    XmlNodeList list = doc.GetElementsByTagName(“xades:SignedProperties”);
    XmlElement node = (XmlElement)list[0];
    string s = node.OuterXml;
    using (MemoryStream msIn = new MemoryStream(Encoding.UTF8.GetBytes(s)))
    {
    XmlDsigC14NTransform t = new XmlDsigC14NTransform(true);

    t.LoadInput(msIn);
    Stream st = (Stream) t.GetOutput(typeof(Stream));
    using (var cryptoService = new SHA1CryptoServiceProvider())
    {
    var hash = cryptoService.ComputeHash(st);
    var hashString = Convert.ToBase64String(hash);
    MessageBox.Show(hashString);

    }
    }

    Czy jestm na dobrym tropie?

    Niżej SignedProperties z wygenerowanego pliku xades;
    2017-03-10T10:33:45ZM1roh5UkuBE+LsBK7yUG5Cb2m3Q=17397885708855712043403751717868631362

    1. W twoim przykładowym kodzie nie widzę przekształcenia węzła na formę kanoniczną przed czytanie przez metodę C14N().

      Źle się wyraziłem – przez przekształcenie węzła na formę kanoniczną miałem na myśli właśnie przekazanie go do C14N() i jeżeli w wyniku masz rozjazd, to prawdopodobnie przyczyna leży w sprowadzaniu do formy kanonicznej (o czym zresztą pisałeś na początku komentarza).

      Najłatwiej będzie Ci porównać treści zwracane po C14N z używanego przez Ciebie kodu z treścią zwracaną przez PHP – postaw sobie na boku jakiś serwer z interpretatorem tego języka i porównaj.

    2. Ja własnie skończyłem pisać klasę do podpisywania xml-a w xades-bes i jako że jest to dla mnie temat z cyklu fire & forget, to po zabawie z klasą XmlDsigC14NTransform i węzłem SignedProperties, podejrzałem jak to wygląda, to ni jak się to miało do wyniku funkcji C14n z php.
      Główny problem polega na tym że namespace ds jest dodawane jako atrybut do każdego węzła z przestrzeni nazw ds.

      Jako że leniwy jestem zrezygnowałem w ogóle z używania klas xml i stworzyłem dokument ze stringów i odpowiedniki (dokładnie 3) do liczenia digestów z fragmentami xml w posatci kanonicznej. Nie jest to szczyt inżnieri programistycznej, a raczej brutalna metoda, ale generuje poprawnie podpisany dokument.

  3. Niżej SignedProperties z wygenerowanego pliku xades;
    2017-03-10T10:33:45ZM1roh5UkuBE+LsBK7yUG5Cb2m3Q=17397885708855712043403751717868631362

  4. Bardzo fajny artykuł, fajnie wszystko wyjaśnione wyjaśnia. 🙂
    Mam jednak problemy z weryfikacją wartości skrótów jakie pojawiają się na stronie 🙁 i nie wiem czy dobrze zrozumiałem jak one powstają.

    Chodzi na razie o pierwsze DigestValue ?
    MLaZQl/toVO57dJsz1JfLIyrrd4=

    Trzeba ją wygenerować takim kodem ?
    $dom = new DOMDocument();
    $dom->loadXML($content);
    $canonicalizedXml = $dom->C14N();
    $hash = sha1($canonicalizedXml, true);
    $digestValue = base64_encode($hash);

    Tylko jaką wartość mam wstawić w zmienną $content ?
    To :
    PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiID8+Cjx0ZXN0PgogIDx0ZXN0PnRlc3Rv
    d3kgd8SZemXFgjwvdGVzdD4KICA8dGVzdD5kcnVnaSB0ZXN0b3d5IHfEmXplxYI8L3Rlc3Q+CjwvdGVz
    dD4=

    Bo gdy wstawiam to nie dostaje tego skrótu: MLaZQl/toVO57dJsz1JfLIyrrd4=
    ? 🙁

    Poproszę o pomoc 😉

    1. W $content powinna być po prostu pierwotna treść dokumentu, który podpisujesz – w moim przykładzie to ten testowy XML.

  5. Podstawiłem do zmiennej $content wartość $xml.
    Mój testowy kod wygląda teraz tak:

    $xml = ‘

    testowy węzeł
    drugi testowy węzeł

    ‘;

    echo ‘XML:’;
    echo “

    \n".htmlentities($xml)."\n

    \n”;
    echo “Base64 z XML:”;
    echo base64_encode($xml);

    echo ‘Węzeł do skrótu:’;
    echo “

    \n".htmlentities($xml)."\n

    \n”;

    $content = $xml;
    $dom = new DOMDocument();
    $dom->loadXML($content);
    $canonicalizedXml = $dom->C14N();

    echo ‘Węzeł do skrótu, postać kanoniczna:’;
    echo “

    \n".htmlentities($canonicalizedXml)."\n

    \n”;

    $hash = sha1($canonicalizedXml, true);
    $digestValue = base64_encode($hash);

    echo ‘Skrót (digestValue):’;
    echo $digestValue.”;

    Wartość skrót dostaje:
    4jYUfs46wp95RKzQE4WP0pvAHQQ=

  6. Pisze, żeby podziękować za wpis bo bardzo dużo ułatwił mi w napisaniu własnej implementacji do generowania podpisu. Także dzięki za podzielenie się wiedzą.

  7. Hey thank you very much for this article, I had some issues with the english translation.
    is there any link to download your final code?

    Thanks in advance

    1. Unfortunately not. This article is to show you steps of XAdES-BES document signing with some code snippets, but it doesn’t provide complete solution.

  8. Świetne omówienie 🙂

    Ale po wnikliwej lekturze nasuwają się dwa pytania:

    1. Do funkcji openssl_sign() podajesz jako pierwszy parametr wartość $signedInfo, która jest skanonizowaną postacią tagu “SignedInfo” (string), czy sha1 na tym stringu?
    2. Funkcja c14N() ma parametr exclusive i zauważyłem, że większość gotowych rozwiązań ustawia go na true. Czy w Twoim przypadku przyjęcie wartości domyślnej false jest zamierzone?

    1. Cześć,
      poniżej odpowiedzi:

      1. Do openssl_sign() podaję w $signedInfo po prostu kanoniczną postać SignedInfo.

      2. Wszędzie wywołuję C14N() bez żadnych parametrów. Parametr, o którym piszesz, ma sens tylko wtedy, gdy podajesz też parametr trzeci lub czwarty (zgodnie z http://php.net/manual/en/domnode.c14n.php). Tutaj nie ma potrzeby wybiórczego tworzenia formy kanonicznej, a więc i przekazywania do C14N() żadnych parametrów.

  9. Witam,
    weryfikując własny podpis programem szafir otrzymuje następujący komunikat o błędzie:
    “Podpis został negatywnie zweryfikowany – Nieprawidłowy skrót w obiekcie Reference w podpisie XML.-#ID-object1”.
    Może ktoś z tu z obecnych miał taki problem i potrafi pomóc?????

    1. Hej
      Ja dokładnie tez mam taki sam problem i nie umiem sobie z tym poradzić.
      Ja tworze podpis dla dokumentów dla ZUS i własnie dostaję bardzo podobną odpowiedź.
      Czy jest ktoś kto może nam pomóc?

  10. cześć,
    próbuję wygenerować podpisany dokument, analizuję Twój jakże przydatny post i napotykam na problem, mianowicie w podpisanym przez Ciebie pliku xml występują trzy pozycje DigestValue, opisujesz pierwsze dwa odwołania. czy trzecie odwołanie, to wewnątrz SignedProperties jest takie samo jak drugie odwołanie?
    oraz drugie pytanie odnośnie id poszczególnych pól, ponieważ generuje samodzielnie tekst, wielkość ‘id’ ma znaczenie? czy może to być  po prostu ciąg 16 znaków?
    pozdrawiam

    1. próbuję wygenerować podpisany dokument, analizuję Twój jakże przydatny post i napotykam na problem, mianowicie w podpisanym przez Ciebie pliku xml występują trzy pozycje DigestValue, opisujesz pierwsze dwa odwołania. czy trzecie odwołanie, to wewnątrz SignedProperties jest takie samo jak drugie odwołanie?

      Każde DigestValue zawiera inną wartość, bo odwołuje się do innego węzła. Opis trzeciego odwołania znajduje się w sekcji: “Właściwy podpis” “Retrospekcja – dane certyfikatu”.

      oraz drugie pytanie odnośnie id poszczególnych pól, ponieważ generuje samodzielnie tekst, wielkość ‘id’ ma znaczenie? czy może to być po prostu ciąg 16 znaków?
      pozdrawiam

      Tak, spokojnie może być to ciąg 16 znaków. Jest to walidowane wobec xsd:anyURI, którego zasady masz opisane tutaj: http://www.datypic.com/sc/xsd/t-xsd_anyURI.html

  11. dzięki za komentarz, bardzo pomocny!
    mam jeszcze jedną zagwozdkę, mam urządzenie z certyfikatem sprzętowym.
    chciałbym ten podpis generować totalnie po stronie serwisu, do tego potrzebuję plik certyfikat oraz klucz prywatny, który jest na tym urządzeniu.
    wiesz może jak wyciągnąć z tego USB sprzętowego ten klucz? – czy może ja źle do tego podchodzę
    pozdrawiam i raz jeszcze dzięki za artykuł – był dla mnie nieocenioną pomocą!

  12. na wstępie – wielkie dzięki za ten post, skierował mnie na dobre tory jak podejść do tematu podpisu po stronie PHP 🙂 szacun!

    po ilości komentarzy widzę, że ktoś tutaj z googli trafia, więc dam tylko znać, że jedna sprawa może wam (tak jak i mi 😉 ) mocno uprzykrzyć życie – chodzi o nieszczęsną funkcję c14n(). Do poprawnego wyliczenia digest value musi zostać uzyty xml z definicjami namespace’ów. Niestety, PHPowa implementacja (a przynajmniej wersja, której ja używam) nie dodaje ich do wynikowego stringa. Stąd też najlepiej rozszerzyć węzły, by miały taką postać (nie mam pojęcia jak zareaguje system komentarzy z tagami xml):

    <ds:Object xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Encoding="http://www.w3.org/2000/09/xmldsig#base64" ...

    <ds:SignedInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="ID-4c61b0d9-e5da-4eea-97a1-623b9e4fa2b2" ...

    <xades:SignedProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="ID-9542235a-b703-477a-8702-8999387d943e" ...

    z tak utworzonymi węzłami nawet nie trzeba kombinować z getElementsByTagName() na pełnym dokumencie – wystarczy załadowanie tylko konkretnego fragmentu 🙂

    pozdrawiam i życzę owocnego kodowania 🙂

  13. Witam

    Z góry dziękuję za ten post, jest bardzo pomocny. Jednak jest jeszcze jedna rzecz, którą nie umiem napisać, a mianowicie ID do podpisu i do innych rzeczy np:

    Jak się generuje takie ID?

    Pozdrawiam
    Piotrek

      1. Chyba się źle coś wstawiło:
        ds:DigestMethod Algorithm=”http://www.w3.org/2000/09/xmldsig#sha1″ /
        ds:DigestValue…/ds:DigestValue
        wstawić w xades:CertDigest ?

  14. Dziękuję za pomoc.
    Teraz jestem na etapie złożenia całego obiektu ds:Object lecz borykam się z problemem typu:

    Nie wiem jak wstawić NODE :


    do elementu NS . Do wstawiania elementów używam DOMDocument

  15. Dzięki Ci za ten opis. Gdyby nie to, pewnie jeszcze długo bym się głowił nad walidacją poprawności podpisu pliku. 🙂

  16. Potrzebujemy ogarnąć temat szyfrowania i wysyłki plików JPK na serwer e-dokumenty.mf.gov.pl

    Nasz program działa online w PHP, aktualnie tworzy pliki JPK w XMLu zgodne ze specyfikacją. Plik może być u nas podpisany w programie certyfikatem (mamy API z Certum, które już to robi) lub pobrany przez użytkownika do podpisu lokalnie i załadowany z powrotem już podpisany.
    Tą część już mamy gotową.

    Chcemy natomiast zlecić wykonanie:

    1 . Przygotowania pliku do wysyłki (pakowanie zipem, i tworzenie pliku InitUpload.xml), tak jak to pokazane tutaj na 6 stronie: https://www.finanse.mf.gov.pl[…]jsow_uslug_JPK_wersja_2_3.pdf

    2. Inicjacja połączenia i przesłanie pliku.

    Wygląda na to, że jest Pan doświadczoną osobą w tym zakresie.
    Bardzo prosimy o kontakt zwrotnie na e-mail.

  17. Witam.

    $dom = new DOMDocument();
    $dom->loadXML($fullContent);
    $node = $dom->getElementsByTagName(‘SignedProperties’)->item(0)->C14N();

    $fullContent – czy w tym $fullContent jest zawarta cała treść (podpis + obiekt xades) podpisywanego przykładu?

    Problem polega na tym, iż mam poprawnie podpisane oświadczenie ZUS tylko muszę zaimplementować to w PHP. Pierwszy DigestValue w elemencie SignedInfo mam już wyliczony, oraz DigestValue w elemencie SignedPropertiesties.
    Został mi tylko drugi DigestValue w elemencie SignedIfo oraz SignatureValue.
    Wiem z opisu powyżej, że drugi DigestValue oblicza się z całego drzewa SignedPropertiesties, lecz ciągle wychodzi mi zły hash.
    Stąd moje drugie pytanie. Czy w całym drzewie SignedPropertiesties muszą być wypełnione wszystkie wartość takie jak: data i czas podpisy, digestValue, issuerName, issuerNumber oraz wszystkie atrybuty takie jak id, target itp?

  18. Dzięki wielkie za cenną wiedzę. Ciężko coś na ten temat znaleźć.
    Udało mi się podpisać XML certyfikatem PEM.
    Problemy jakie napotkałem i rozwiązania:

    1. Numer seryjny certyfikatu jest w postaci hex, nie wiem czy jest wymóg zapisu w postaci int, ale można go przekonwertować na int za pomocą bcmath:

    private function bcHexDec(string $hex): string
    {
    $dec = 0;
    $len = strlen($hex);
    for ($i = 1; $i pemFilePath);
    return $this->bcHexDec($certInfo[‘serialNumberHex’]);
    }

    2. W moim przypadku w ogóle nie wstawiałem RSAKeyValue (Modulus i Exponent). Szafir się o to nie upominał. W przykładowym podpisanym XMLu nie było też tych wartości.

    3. Najważniejsze! Pierwsze DigestValue liczę z “całego” elementu ds:Object zawierającego wejściowe dane w base64.
    Robię to w taki sposób, że najpierw tworzę sobie object (payload to wejściowy xml zwrócony przez C14N):

    $this->dom->createElement(‘ds:Object’);

    $objectContainer->appendChild($this->dom->createTextNode(base64_encode($payload)));
    $this->dom->getElementsByTagName(‘Signature’)->item(0)->appendChild($objectContainer);

    $objectContainer->setAttribute(‘Id’, $id);
    $objectContainer->setAttribute(‘MimeType’, ‘text/plain’);
    $objectContainer->setAttribute(‘Encoding’, ‘http://www.w3.org/2000/09/xmldsig#base64’);

    A później liczę z sumę z $objectContainer->C14N().

    Ale powinno zadziałać też coś takiego:

    $node = $dom->getElementsByTagName(‘ds:Object’)->item(1)->C14N();
    $hash = sha1($node, true);
    $digestValue = base64_encode($hash);

    Po tych “zabiegach” Szafir pomyślnie weryfikuje podpis. Dodam, że używam SHA256 a nie SHA1. Ale to tylko kwestia użytych funkcji i deklaracji w XMLu. Jak liczyłem sumę z samego XMLA wejściowego (bez tagu ds:Object), to weryfikacja była negatywna.

  19. i have this error:
    Invalid signed properties hashing, SignedProperties with id=\’xadesSignedProperties\’
    $dom = new DOMDocument();
    $dom->loadXML($xmlContent);
    // $dom->formatOutput = true;
    $node = $dom->getElementsByTagName(‘SignedProperties’)->item(0)->C14N();
    return base64_encode(hash(‘sha256’, $node));
    what is the problem with zatca sa.

    1. Zatca’s implementation of einvoicing is not great. While you may implement the hashing correctly, it’s obviously highly dependent on what you are hashing and in no way is it clear what exactly you need to hash. And by this I mean the finer details such as formatting of the XML including the indentation. It shouldn’t be a problem, there should be a consistency with what gets hashed, but what is actually hashed would be hilarious if it wasn’t so frustrating. I think you may be better off doing what I did and decompiling their SDK and printing out the XML that is being hashed. You’ll be surprised at what you find and once you mimic their structure it will all work out.

      Zatca devs: do better. If you want to persist with hashing poorly formatted XML then document this!

Skomentuj dandys Anuluj pisanie odpowiedzi

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Time limit is exhausted. Please reload CAPTCHA.