A więc .NET

Dawno mnie tu nie było, ale był też istotny tego powód. Niby człowiek wiedział, ale się łudził i fakt przejęcia Idea Banku przez Pekao spadł na mnie dość nieoczekiwanie, zwłaszcza, że miało to miejsce w południe w sylwestra 2020. Do tamtej pory pracowałem rozwijając aplikacje backoffice’owe w PHP, w Pekao ten język praktycznie nie istnieje poza jakimiś marginalnymi zastosowaniami (zresztą nie wiem, czy ma on rację bytu gdziekolwiek indziej w bankowości poza grupą Leszka Czarneckiego), więc pierwsze tygodnie oznaczały dla mnie miotanie się pomiędzy decyzją o pozostaniu w Pekao i zmianą języka, a zmianą pracy na inną i pozostaniu przy PHP. „Miotanie się” to bardzo adekwatne w tym wypadku określenie – nastrój w tej kwestii zmieniał mi się czasem co tydzień w skrajnych kierunkach. W Pekao dominuje szeroko pojęty .NET, więc dla mnie oznaczałoby to nie tylko zmianę języka, ale zaczynanie praktycznie od zera w większości powiązanych technologii. Tak na szybko listując: język ze słabo typowanego, skryptowego na silnie typowany, kompilowany, DBMS z PostgreSQL na SQL Server, serwery HTTP z Apache/nginx na IIS, środowisko uruchomieniowe z linuksowego na windowsowe, IDE z Eclipse na Visual Studio, do tego dużo drobniejszych rzeczy, jak razor pages, inny ORM, powszechnie używane zasoby sieciowe zamiast NFS/SFTP, itd. Dużo tego. Zupełnie inaczej wyobrażałem sobie też przepięcie na nowe technologie – część kolegów zaczęła swoją technologiczną konwersję praktycznie z dnia na dzień. W moim przypadku ostatecznie było inaczej – system, który tworzyliśmy w IB, podlegał migracji do systemów Pekao, więc było to priorytetem i potrwało do listopada 2021, a ja w tym czasie mogłem się spokojnie oswoić z nowym stackiem technologicznym i zacząć przyswajać wiedzę z tego zakresu, jeszcze wtedy na wolnych obrotach.

Pracę w projektach .NET-owych zacząłem na poważnie w grudniu 2021, więc ostatnio minęły pełne 4 miesiące pracy przy nich, taki większy okres próbny. Postanowiłem więc, że póki mam to jeszcze na świeżo, to opiszę moje wrażenie z takiej przesiadki.

Pierwszy miesiąc to głównie przyzwyczajanie się do silnie typowanego języka. Po ponad 10 lat pracy w PHP trzeba było zmienić przyzwyczajenia nawet w tak podstawowych kwestiach, jak dobór typów danych (i wyleczenie się ze wszechobecnych w PHP, mieszanych tablic asocjacyjnych). Nie było to jednak jedyne przyzwyczajenie do zmiany: samo środowisko uruchomieniowe rządzi się innymi prawami i niektóre rzeczy są rozwiązane w sposób, który nigdy we wcześniejszym stacku nie przyszedłby mi do głowy (koordynator transakcji rozproszonych jako osobna usługa jest moim faworytem do tego rankingu).

Z silnym typowaniem z perspektywy czasu jest jednak jak z przejściem ze skrzyni manualnej na automatyczną – na początku wkurza zahaczanie nogą o szeroki hamulec i odruchowe wpadanie lewej nogi w dziurę, gdzie powinno być sprzęgło, jednak po czasie docenia się to, że pewne rzeczy wyłapywane są już na etapie kompilacji. Początkowo było to dla mnie nieco uporczywe i zdawało mi się zbędnym narzutem, jednak nie zauważałem pewnego istotnego faktu: wcześniejsze projekty znałem dość dobrze i w przypadku zmian na poziomie modelu wiedziałem, gdzie szukać obszarów do refaktoryzacji. Gdy jednak nie ma się jeszcze takiej wiedzy, to w przypadku PHP często błędy wychodziły dopiero podczas uruchomienia pominiętego kodu. Tutaj projekty są dla mnie nowe, więc takiej wiedzy nie mam, a przeszukiwanie kodu źródłowego nie zawsze jest na tyle efektywne, aby zidentyfikować wszystkie konieczne miejsca.

Silne typowanie ma jeszcze jeden plus – dzięki niemu autouzupełnianie w IDE i ogólnie asystent kodu stoją na zupełnie innym poziomie, niż w PHP. Tutaj na każdym etapie zmienne mają znany edytorowi typ, więc praktycznie wszędzie mamy dostęp do podpowiedzi, co w przypadku PHP – nie było tak oczywiste.

Pisanie aplikacji webowej w C# z użyciem ASP.NET MVC to też pole do zmiany przyzwyczajeń. Dużo się tu dzieje w middleware’ach. W PHP korzystałem z Zenda i operowanie na surowej tablicy wartości z POST, lekko tylko przerobionej (np. przefiltrowanej) przez framework było dla mnie naturalnym podejściem. Tutaj królują modele, które są automatycznie mapowane z danych POST/GET. Za tym idzie mnóstwo innych rzeczy, jak np. sprzęgnięta walidacja, itd. Po krótkim przyzwyczajeniu jest to dużo wygodniejsze, niż praca na surowych danych z POST.

Konstrukcja języka C# mi się podoba. Są rzeczy, których brakowało mi w PHP (część z nich jednak w międzyczasie została wprowadzona w wersji 8 – gdzie one były przez tyle lat mojej pracy? :P), na przykład named parameters. Dłużej musiałem przysiąść nad powszechnie używanymi wyrażeniami lambda, których czasem w PHP się używało, ale w zupełnie inny sposób. Podobnie z typami generycznymi, z których prototypem (szablonami) miałem do czynienia dawno temu, gdy pisałem w C++. Do tego mnóstwo różnych, drobnych rzeczy, których nie znałem, a których używa się na co dzień (jak chociażby postępowanie z Nullable, bardziej zaawansowane używanie list, itd.).

A propos list, nie można zapomnieć o LINQ. Bardzo spodobało mi się to rozwiązanie, po opisanym wcześniej ogarnięciu wyrażeń lambda można wiele zrobić z kolekcjami w łatwy i krótki sposób. Dodatkowo, w zastosowaniach bazodanowych, w połączeniu z IQueryable czułem jak się w domu ze względu na przyzwyczajenie do zendowych selectów.

Polubiłem też wykorzystywany tu ORM i sposób tworzenia modelu. I choć widzę, że mam w tym temacie jeszcze mnóstwo do nauczenia, to jest przede wszystkim dużo szybciej niż w PHP, zarówno z mapowaniem struktury bazy danych na model, jak i chociażby z migracjami bazodanowymi. Pamiętam, że podobnie miałem, gdy w 2012 przesiadłem się z CodeIgnitera na Zenda i jego active record i Zend_Db_Select. Zresztą ostatnie miesiące bardzo przypominają mi 2012 – przyswajam dużo nowej wiedzy w krótkim czasie, wtedy zdaje się, że porównywałem pierwsze pół roku pierwszej pracy na etacie do całego drugiego stopnia studiów pod kątem przyswojonej wiedzy.

Nie mogę w tym zestawieniu pominąć debugowania aplikacji w Visual Studio. Coś, co działa z automatu i czego używa się codziennie to dla mnie zdecydowanie nowość. Wcześniej, w PHP, używałem xdebug do debugowania kodu krok po kroku lub do profilowania tylko w wyjątkowych sytuacjach, gdy logika była na tyle zaawansowana, że ciężko było ją debugować przez wypluwanie wartości lub wtedy, gdy trzeba było przyjrzeć się z wysokiego poziomu wykonywaniu kodu i systemowego i systemowo go zoptymalizować. Zresztą sama konfiguracja xdebug w PHP była dość problematyczna, szczególnie na obwarowanych zabezpieczeniami komputerach bankowych i takowej infrastrukturze sieciowej. Nie mówiąc już o szybkości działania tak debugowanej aplikacji.

Co do samego Visual Studio, to spodobał mi się gitowy merge tool, który jest znacznie wygodniejszy, niż wcześniej preferowane przeze mnie ręczne rozwiązywanie konfliktów – tutaj mam kod z dwóch stron konfliktu na podzielonym ekranie, mogę pojedyncze linie włączać do rozwiązania konfliktu checkboksami, a dodatkowo edytować go w połączonej wersji. Coś wspaniałego. Rozczarował mnie natomiast brak narzędzia do częściowego stage’owania zmian w gicie. Zostało podobno wprowadzone w VS 2022, więc chętnie wypróbuję, jednak póki co korzystam z VS 2019, więc pozostaje mi nadal dobrze znana obsługa gita z konsoli.

Podsumowując, z przesiadką na nowy stack technologiczny jest lepiej, niż przewidywałem. Nadal jestem na samym początku drogi, więc najbliższe miesiące to na pewno czas dalszej nauki w tym kierunku, jednak sam jestem zdziwiony, że wcześniejsze przyzwyczajenia i uprzedzenia w sumie dość szybko odpuściły. Zawsze byłem też z tych, którzy mocno się do takich rzeczy przyzwyczajają i ciężko było mi zwizualizować sobie trwałą, nieincydentalną przesiadkę np. na zupełnie inny język programowania. Nie doceniałem w tej kwestii jednak doświadczenia, które na pewno ma tu spore znaczenie – znając dobrze zasadę działania aplikacji internetowych, język to tylko narzędzie, sam sposób myślenia, programowanie obiektowe i sposób rozwiązywania problemów są w zasadzie takie same i cieszę się, że przekonałem się o tym na własnej skórze.

Traits w PHP

Traits w PHP są obecne już od wersji 5.4, więc od dość długiego czasu (2012). Sam pierwszy raz czytałem o nich parę lat później, jednak nie wydały mi się atrakcyjne – są próbą wypełnienia braku dziedziczenia wielokrotnego, które nigdy w praktyce mi się nie przydawało jeszcze w czasach C++, a rodziło problemy, które komplikowały czasem bardziej niż jego brak. Zapomniałem więc o traits na wiele lat, nie widziałem też w tym czasie dla nich zastosowania (podobnie, jak wyparłem kiedyś dziedziczenie wielobazowe).

Aż do zeszłego roku. Trafiłem wtedy na zagadnienie, w którym przydatne byłoby wykorzystanie tego samego kodu w trzech zupełnie różnych klasach, z których każda miała już swoją dość złożoną strukturę i odpadało stworzenie klasy nadrzędnej, po której dziedziczyłyby wszystkie z nich.

Można było niby skorzystać z klasy helpera i pobierać jego instancję lub wstrzykiwać, jednak to też wprowadzało pewien nieład zwłaszcza, że jedna z tych klas modyfikowała działanie części kodu, którą potrzebowałem „uwspólnić”.

Nie było więc innego wyjścia, niż zainteresować się traitsami. I był to strzał w dziesiątkę. Traitsy to tak naprawdę kawałki kodu do wielokrotnego użytku. Wyglądają jak klasy, jednak nie wprowadzają zmian w strukturze typów. Nie narzucają też praktycznie niczego – używane w nich pola i metody nie muszą istnieć w nich samych, jeżeli wprowadzają konflikty, to można je łatwo rozwiązać. Nie można utworzyć ich samodzielnych instancji. Teoretycznie blisko jest im więc do klas abstrakcyjnych, z tą różnicą, że w PHP możemy dziedziczyć tylko jednej klasie abstrakcyjnej, natomiast traitstów możemy użyć tyle, ile potrzebujemy.

Korzystanie z nich jest więc podobne do skopiowania ich treści do bieżącej klasy. Dodatkowo, możemy aliasować poszczególne traitsy, a także metody z nich tak, aby nie zachodziły konflikty z już istniejącymi metodami, do których zaciągamy trait. Metody zaciągane z traits mogą być nadpisywane lokalnie, działa to wtedy tak, jak byśmy nadpisywali metodę z klasy nadrzędnej. Nadal można przy tym korzystać z nadpisywanej metody z trait.

Jeżeli chodzi o wady, to należy pamiętać o konfliktach w przypadku współistnienia pól.

Przyznam, że od czasu pierwszego użycia, pomijając początkowo neoficką fascynację, zacząłem widzieć coraz więcej obszarów, w których mógłbym stosować traitsy, i coraz częściej pojawiają się one w moim kodzie.

Widoczność pól i metod innych obiektów tego samego typu w PHP

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

Ostatnio, refaktoryzując kod, dowiedziałem się o pewnej ciekawej właściwości PHP. Otóż obiekty tych samych typów mają dostęp do prywatnych i chronionych metod oraz pól nawet, jeżeli stanowią oddzielne instancje tej samej klasy. Początkowo myślałem, że to jakiś błąd w kodzie, który analizowałem, jednak znalazłem potwierdzenie w manualu PHP:

Objects of the same type will have access to each others private and protected members even though they are not the same instances. This is because the implementation specific details are already known when inside those objects.

http://php.net/manual/en/language.oop5.visibility.php#language.oop5.visibility-other-objects

Co ciekawsze, w innych popularnych językach jest podobnie, więc nie jest to swoistość samego PHP, na co nigdy nie zwróciłem uwagi…

PSR-1 i PSR-2

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

Tym razem krótko, bo chciałem tylko zaznaczyć istnienie PSR-1 i PSR-2. Cieszę się, że te dwa zalecenia powstały i że modne jest stosowanie się do nich, przez co istnieje duże prawdopodobieństwo, że przeglądając źródła zarówno swojego projektu, jak i innych, dostępnych publicznie, a często używanych, trafimy na czytelne i zbieżne formatowanie. Sam musiałem wyplenić kilka złych nawyków, ale dzięki kod jest dużo czytelniejszy.

Wojny edycyjne uznajemy więc za zakończone. 😉

Ratchet – WebSockety w PHP w praktyce

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

WebSockety to jeden z najciekawszych od kilku lat tematów, z którymi przyszło mi zmierzyć się w pracy. Pierwszy raz słyszałem o nich gdzieś w 2013 roku i od tego czasu zdążyły zdobyć ogromną popularność w internecie. YouTube, Stack Overflow, Facebook – tam wszędzie WebSockety wykorzystywane są już od dawna do różnych rzeczy (przesyłanie i odświeżanie komentarzy oraz głosów, komunikacja w czasie rzeczywistym, itp.). To tyle słowem wstępu – jeżeli tu trafiłeś, prawdopodobnie wszystko to wiesz i szukasz informacji, jak wykorzystać WebSockety w praktyce.

Do zeszłego roku „WebSockety” i „node.js” stanowiły w zasadzie synonimy, jeżeli chodzi o ich implementację. O uruchomieniu ich w PHP czytałem same złe rzeczy i nawet najbardziej zagorzali programiści używający tego języka odradzali. Tematu jednak nie zgłębiałem i przyglądałem się nieco z boku. Jednak gdy w pracy pojawił się temat obsługi zdarzeń z centrali telefonicznej w naszym systemie, nie było wyjścia – trzeba zacząć się nimi szybko interesować.

Kolega z sąsiedniego zespołu już jakiś czas wcześniej podejmował ten temat w oparciu właśnie od node.js. Jego opinie nie były życzliwe, zwłaszcza, że nie miał wiele czasu na stworzenie tego rozwiązania. Podobnie z czasem było u mnie. Dodatkowo zniechęcał fakt, że w zasadzie nie mamy żadnej infrastruktury pod node.js, a ostatnią rzeczą, której bym chciał, to zarządzanie tym wszystkim od strony administracyjnej. Zacząłem więc szukać czegoś o WebSocketach w PHP.

Natychmiast trafiłem na Ratchet – bibliotekę do prostej implementacji WebSocketów w aplikacjach PHP. Brzmiało świetnie zwłaszcza, że mogłem korzystać z „dobrodziejstw” całego naszego systemu. W praktyce okazało się, że jest jeszcze lepiej i łatwiej, niż mi się wydawało.

Stwórzmy więc wspólnie skręcaną z drutów aplikację z pomocą Ratchet, która będzie niczym innym, jak prostym echotestem. Dodatkowo, oprócz odpowiadania na otrzymane wiadomości, serwer będzie sam wysyłał do wszystkich wiadomość, np. co 10 sekund. Dzięki temu otrzymasz bazę do większości zastosowań – nasza aplikacja będzie zarówno odpowiadała na komunikaty z inicjatywy klientów, jak i sama w pętli wysyłała coś od siebie.

Najpierw potrzebujemy samego Ratchet. Najłatwiej zlecić pobranie go composerowi. Jeżeli nie korzystałeś jeszcze z composera, spróbuj po prostu odwiedzić stronę pobierania i przejdź do sekcji „Manual download”, skąd możesz ściągnąć najnowszą wersję composer.phar. Umieść ten plik w katalogu głównym projektu, po czym możesz przejść do pobierania Ratcheta:

php composer.phar require cboden/ratchet

Kolejnym krokiem będzie stworzenie klasy obsługującej wszystkie potrzebne zdarzenia (połączenie, rozłączenie, wystąpienie błędu) oraz zawierającej metodę, którą serwer będzie cyklicznie wywoływać. Oto jej treść:

<?php
namespace WebSocket;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

class Handler implements MessageComponentInterface
{
    protected $clients;

    public function __construct()
    {
        $this->clients = new \SplObjectStorage;
    }

    public function onOpen(ConnectionInterface $conn)
    {
        $this->clients->attach($conn);

        echo 'Nowe połączenie: ' . $conn->resourceId . PHP_EOL;
    }

    public function onMessage(ConnectionInterface $from, $msg)
    {
        // iteracja po dostępnych połączeniach
        foreach ($this->clients as $client) {
            if ($from !== $client) {
                // bieżące połączenie to nadawca wiadomości
                
                // odsyłamy wiadomość
                $client->send($msg);
            }
        }
    }

    public function onClose(ConnectionInterface $conn)
    {
        $this->clients->detach($conn);

        echo 'Zamknięto połączenie: ' . $conn->resourceId . PHP_EOL;
    }

    public function onError(ConnectionInterface $conn, \Exception $e)
    {
        echo 'Wystąpił błąd: ' . $e->getMessage() . PHP_EOL;

        $conn->close();
    }
    
    public function onLoop($options = array())
    {
        foreach($this->clients as $client) {
            $client->send('Cykliczna wiadomość');
    
            echo 'Wysłano komunikat cykliczny' . PHP_EOL;
        }
    }
}

Klasę umieściłem w pliku handler.php.

Metody onOpen, onClose i onError są raczej oczywiste. W onMessage występuje obsługa zdarzenia pojawiającego się w momencie otrzymania wiadomości od klienta. Iterujemy tam po prostu po tablicy dostępnych połączeń i sprawdzamy, czy trafiliśmy na połączenie zgodne z identyfikatorem zasobu nadawcy. Jeżeli tak, to korzystamy z tego zasobu do odesłania odpowiedzi tej samej treści, którą otrzymaliśmy.

Metodę onLoop wykorzystamy natomiast do wywoływania cyklicznego. Wysyła ona do wszystkich połączeń komunikat o tej samej treści.

Mając taką klasę, możemy stworzyć plik, w którym uruchomimy sam serwer. W moim wypadku jest to server.php o następującej treści:

<?php
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use WebSocket\Handler;

require 'vendor/autoload.php';
require 'handler.php';

// utworzenie obiektu klasy obsługującej
$handler = new Handler();

// stworzenie instancji serwera websocketów
$server = IoServer::factory(
    new HttpServer(
        new WsServer($handler)
    ),
    8080    // port
);

// ustawienie pętli
$server->loop->addPeriodicTimer(10, function() use ($server, $handler) {
    // uruchomienie metody obrotu pętli
    $handler->onLoop(array(
        'server' => $server,
    ));
});

echo 'Serwer uruchomiony' . PHP_EOL;

// uruchomienie serwera
$server->run();

Jak widać, tworzymy najpierw instancję serwera nasłuchującego na porcie 8080, podając mu klasę z obsłużonymi zdarzeniami, a następnie dodajemy pętlę, która będzie wywoływana co 10 sekund.

Teraz wystarczy uruchomić serwer poprzez wywołanie:

php server.php

W konsoli powinniśmy dostawać informacje o przebiegu działania serwera.

Jak się połączyć z naszym serwerem? Najprościej uruchamiając konsolę w przeglądarce (w Firefoksie możemy wywołać ją kombinacją CTRL-SHIFT-K) i wklejając:

var conn = new WebSocket('ws://localhost:8080');
conn.onopen = function(e) {
    console.log("Połączono pomyślnie!");
};

conn.onmessage = function(e) {
    console.log(e.data);
};

Po chwili powinniśmy otrzymywać z serwera co 10 sekund wiadomość.

Aby wysłać wiadomość do serwera, wystarczy, że w tej samej konsoli wkleimy:

conn.send('Test');

To wszystko! Mamy już działający serwer.

Oczywiście, docelowa aplikacja musi zawierać obsługę uwierzytelniania, jednak celem tego wpisu było pokazanie prostoty stworzenia serwera WebSocketów.

Wpis był mocno inspirowany oficjalnym tutorialem Ratcheta, który rozbudowałem o cykliczne wywoływanie zdarzeń przez serwer.

Eclipse – utrata możliwości debugowania w trakcie używania Xdebug

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

Po poznaniu rozwiązania problem wydaje się być głupi, ale powstrzymał mnie skutecznie przez bardzo długi czas przed debugowaniem aplikacji w PHP z użyciem Xdebug.

Do sedna: jeżeli w trakcie debugowania nagle tracisz możliwość sterowania debuggerem (dezaktywują się przyciski od nawigowania po kodzie, itp.), ale sama aplikacja i sesja debuggera nadal działa (jesteś w stanie ją pomyślnie zakończyć), to prawdopodobnie Eclipse stracił focus na debugowanym przez Ciebie wątku. Stan taki pokazuje poniższy zrzut ekranu:

eclipse-xdebug-lost-focus

Co w takiej sytuacji? Wystarczy kliknąć na „Remote launch” w karcie „Debug” (u mnie jest to lewa górna część ekranu):

eclipse-xdebug-recovered-focus

Jak widać, przyciski do sterowania debuggerem są znów aktywne i można kontynuować tę sesję Xdebug.

 

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ć.