Podpisywanie plików XML w Javie. Część pierwsza - obsługa podpisu elektronicznego

Przez Rafał Pydyniak | 2018-03-08

W poniższym artykule zaprezentuję możliwości podpisywania plików XML za pomocą podpisów elektronicznych z wykorzystaniem Javy, a także opiszę, czym właściwie są podpisy elektroniczne, oraz jakie mają zastosowania.

Artykuł składa się z dwóch części:

  1. Podpis elektroniczny - czym jest oraz jak obsłużyć go w Javie
  2. Podpisywanie plików XML - jakie są standardy co do tego jak powinny wyglądać podpisane pliki oraz jak wygenerować taki plik za pomocą Javy

Poniżej znajduje się pierwsza część dotycząca podpisu elektronicznego.

W artykule przedstawię kolejne kroki tworzenia aplikacji (choć w niektórych przypadkach w uproszczonej wersji, np. bez informacji o importach), ale całość można także znaleźć na Githubie pod adresem https://github.com/RafalPydyniak/XmlDigitalSignatureDemo

Podpisy elektroniczne

Podpisy elektroniczne a podpisy cyfrowe

Na początku warto rozróżnić czym różnią się podpisy elektroniczne od cyfrowych.

Podpis cyfrowy (ang. digital signature) - w uproszczeniu jest to po prostu matematyczny sposób sprawdzenia autentyczności danego pliku. Protokół podpisu cyfrowego opiera się zazwyczaj na kryptografii asymetrycznej, czyli dostępne mamy dwa klucze - publiczny oraz prywatny. Klucz prywatny służy do podpisania danych, natomiast klucz publiczny do potwierdzenia autentyczności danego pliku. Do generowania kluczy używa się zazwyczaj protokołu RSA lub DSA.

Podpis elektroniczny (ang. electronic signature) - w porównaniu do podpisu cyfrowego termin ten odnosi się do całego mechanizmu składania podpisu pod plikami w formie elektronicznej. Plik podpisany elektronicznie niesie ze sobą takie same skutki prawne jak podpis własnoręczny, choć dokładne regulacje prawne mogą się różnić w poszczególnych państwach. Podpis elektroniczny jest zazwyczaj realizowany za pomocą podpisu cyfrowego w celu zapewnienia jego autentyczności.

Podsumowując, podpisy cyfrowe oraz podpisy elektroniczne nie są tym samym, jednak należy mieć na uwadze, że w niektórych źródłach terminy te w niewłaściwy sposób używane są zamiennie.

Sam proces generowania i sprawdzania autentyczności podpisów cyfrowych jest stosunkowo prosty i jego uproszczony opis można znaleźć nawet na Wikipedii.

Do czego mogą być użyte podpisy elektroniczne

Podpisy elektroniczne mają taką samą moc prawną jak podpisy własnoręczne. Za ich pomocą można na przykład podpisać umowy (co potwierdza art. 78 Kodeksu Cywilnego), załatwić sprawy urzędowe przez Internet, czy też podpisać deklaracje podatkową.

Właśnie do tego ostatniego, czyli do podpisywania deklaracji podatkowych, może się przydać możliwość podpisywania plików XML. Ministerstwo Finansów udostępnia bramką E-Deklaracje, za pomocą której można złożyć deklaracje online. Bramka ta przyjmuje podpisane pliki XML, a w zamian otrzymujemy identyfikator naszej przesyłki, dzięki któremu możemy śledzić status wysyłki, a następnie pobrać dla niej UPO czyli potwierdzenie złożenia deklaracji podatkowej przez Internet.

Obsługa podpisu elektronicznego w Javie - przykład

Biblioteki oraz standard PKCS

Do obsługi podpisu elektronicznego wykorzystamy bibliotekę IAIK PKCS#11, choć możliwe jest także skorzystanie z implementacji Sun PKCS#11, która jednak według mojej najlepszej wiedzy nie oferuje tak wiele jak IAIK.

Warto na początku wspomnieć także o tym, czym jest PKCS#11. Sam akronim PKCS oznacza "Public Key Cryptography Standards" i jest zbiorem standardów kryptograficznych publikowanych przez firmę RSA Security. PKCS#11 jest własnie jednym z tych standardów i dotyczy interfejsu do obsługi tokenów kryptograficznych, takich jak karty z podpisami elektronicznymi, na których znajduje się certyfikat do podpisu elektronicznego. Certyfikaty te wydawane są w Polsce przez kilka instytucji na przykład KIR.

Co chcemy uzyskać

Początkowo określimy to co właściwie chcemy uzyskać. W naszym podpisie elektronicznym zapisany jest certyfikat oraz klucz prywatny, które razem pozwalają nam dokonać podpisu elektronicznego i właśnie te dane chcemy pobrać z naszego urządzenia.

Certyfikat ten będziemy reprezentować przez następującą klasę:

public class SmartCardCertificate {
    private String alias;
    private X509Certificate x509Certificate;
    private PrivateKey privateKey;

    public SmartCardCertificate(String alias, X509Certificate x509Certificate, PrivateKey privateKey) {
        this.alias = alias;
        this.x509Certificate = x509Certificate;
        this.privateKey = privateKey;
    }

    public String getAlias() {
        return alias;
    }

    public X509Certificate getX509Certificate() {
        return x509Certificate;
    }

    public PrivateKey getPrivateKey() {
        return privateKey;
    }

    @Override
    public String toString() {
        return "SmartCardCertificate{" +
                "alias='" + alias + '\'' +
                ", x509Certificate=" + x509Certificate +
                ", privateKey=" + privateKey +
                '}';
    }
}

Musimy jednak wiedzieć, że w systemie możemy mieć kilka tzw. tokenów, z których każdy może zawierać jeden lub więcej certyfikatów. Przykładowo, w podpisie KIR system wykrywa aż cztery tokeny, z których tylko na jednym dostępny jest certyfikat. Każdy token jest przypisany do danego "slotu" w systemie.

Aby obsłużyć takie sytuacje, chcemy:

  1. Pobrać listę tokenów (wraz z informacją o slocie, na którym się on znajduje)
  2. Pobrać listę certyfikatów dla danego tokenu (a dokładniej slotu, pod który jest podpięty ten token)

Zdefiniujmy więc interfejs, który pozwoli nam zrealizować oba te cele:

public interface SmartCardReader {
    List<Token> getAvaliableTokens() throws TokenException;
    List<SmartCardCertificate> getCertificates(long slotId, char[] pin) throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException;
}

Wcześniej zdefiniowaliśmy już klasę SmartCardCertificate, więc aby ten kod był kompletny, zdefiniujemy jeszcze klasę Token (dla przejrzystości pominąłem gettery):

public class Token {
    private long slotId;
    private String label;

    public Token(long slotId, String label) {
        this.slotId = slotId;
        this.label = label;
    }

    @Override
    public String toString() {
        return slotId + ": " + label;
    }
}

Za pomocą takiego interfejsu możemy już obsłużyć właściwie całą komunikację z naszym podpisem elektronicznym. Zanim przejdziemy do implementacji tego interfejsu, możemy napisać metodę Main, która pobierze i wyświetli certyfikat.

Czyli po kolei:

  1. Pobieramy listę dostępnych tokenów za pomocą metody getAvaliableTokens() wraz z informacją, na którym slocie się znajdują.
  2. Dajemy użytkownikowi możliwość wybrania interesującego go slotu.
  3. Pobieramy listę certyfikatów dostępnych na tym slocie za pomocą getCertificates(long slotId, char[] pin), jako parametr podajemy wybrany slot, a także PIN do tokenu na wybranym slocie.
  4. Wybieramy jeden z dostępnych certyfikatów.
  5. Na ten moment tylko wyświetlamy informacje o wybranym certyfikacie.

Metoda main realizująca wszystkie powyższe punkty wygląda następująco (klasę GeneralSmartCardReader zdefiniujemy za chwile):

public static void main(String... args) throws Exception {
    SmartCardCertificate certificate = getCertificate();
    System.out.println(certificate);
}
private static SmartCardCertificate getCertificate() throws TokenException, IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException {
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    SmartCardReader smartCardReader = new GeneralSmartCardReader("CCPkiP11.dll");
    List<Token> avaliableTokens = smartCardReader.getAvaliableTokens();
    avaliableTokens.forEach(System.out::println);
    System.out.println("Pick which token to use: ");
    String slotId = br.readLine();
    System.out.println("Enter pin:");
    String pin = br.readLine();
    List<SmartCardCertificate> certificates = smartCardReader.getCertificates(Integer.parseInt(slotId), pin.toCharArray());
    for (int i=0; i<certificates.size(); i++) {
        SmartCardCertificate certificate = certificates.get(i);
        System.out.println(i+": " + certificate.getAlias());
    }

    System.out.println("Pick certificate:");
    String pickedCertificate = br.readLine();

    return certificates.get(Integer.parseInt(pickedCertificate));
}

Warto zaznaczyć, że całość odbywa się poprzez konsolę, przez którą podajemy między innymi slot, pin, czy też wybrany certyfikat. Obsługa wyjątków została celowo pominięta, choć oczywiście w rzeczywistości powinny one zostać obsłużone. Jako że jednak jest to tylko i wyłącznie proste demo, to możemy sobie pozwolić na takie niedoskonałości.

Główny element, czyli klasa GeneralSmartCardReader komunikująca się z podpisem

Klasa GeneralSmartCardReader to po prostu implementacja wcześniej zdefiniowanego interfejsu SmartCardReader. Dodatkowo dodajemy pole driver i przypisujemy je w konstruktorze. W polu tym będziemy zapisywać informacje o tym, jaka biblioteka powinna zostać użyta do odczytywania danych z karty. Biblioteki te różnią się w zależności od podpisu elektronicznego i tak przykładowo dla:

  • KIR - driver nazywa się CCPkiP11.dll
  • Sigillum/PWPW - analogicznie jak wyżej - jest to związane z tym, iż KIR oraz Sigillum/PWPW używają tych samych kart
  • Certum - cryptoCertum3PKCS.dll

Biblioteki te znajdują się w katalogach z oprogramowaniem do danego podpisu. Driver do KIR'a (a więc i do Sigillum/PWPW) zostanie także dołączony do aplikacji demo na Githubie.

public class GeneralSmartCardReader implements SmartCardReader {
    private final String driver;

    public GeneralSmartCardReader(String driver) {
        this.driver = driver;
    }

    @Override
    public Lis<Token> getAvaliableTokens() throws TokenException {
        //TODO
    }

    @Override
    public List<SmartCardCertificate> getCertificates(long slotId, char[] pin) throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException {
       //TODO
    }
}

Zaczniemy od pierwszej metody, służącej do pobrania tokenów:

    public List<Token> getAvaliableTokens() throws TokenException {
        Properties properties = new Properties();
        properties.put("PKCS11_NATIVE_MODULE", driver);
        Module module = IAIKPkcs11.getModule(properties);
        Slot[] slotList = module.getSlotList(true);
        List<Token> tokens = Arrays.stream(slotList).map(slot -> {
            try {
                return new Token(slot.getSlotID(), slot.getToken().getTokenInfo().getLabel());
            } catch (TokenException e) {
                return null;
            }
        }).filter(Objects::nonNull).collect(Collectors.toList());
        return tokens;
    }

Po kolei:

  • W linijce 4 musimy pobrać odpowiedni moduł do obsługi interfejsu PKCS#11. Aby tego dokonać, jako parametr musimy podać listę propertiesów, w której zawarta jest nazwa modułu (czyli właściwie biblioteki). Propertiese tworzymy w linijkach 2,3.
  • W linijce 5 pobieramy listę slotów odczytanych z tego modułu. Parametr true oznacza, że pobieramy tylko sloty, na których znajduje się jakiś token.
  • W linjkach 6-12 pobieramy listę tokenów. Dla każdego slotu pobieramy numer slotu oraz opis znajdującego się na nim tokenu. Na podstawie tych informacji tworzymy wcześniej zdefiniowane obiekty klasy Token, które następnie zwracamy.

Mając listę tokenów możemy dać użytkownikowi wybór co do tego, który z nich ma zostać użyty. Przejdźmy teraz do etapu pobrania certyfikatów. Poniżej znajduje się cały kod:

@Override
    public List<SmartCardCertificate> getCertificates(long slotId, char[] pin) throws CertificateException, NoSuchAlgorithmException, IOException, KeyStoreException {
        Security.insertProviderAt(new IAIK(), 2);
        KeyStore keyStore = getPKCS11KeyStore(driver, pin, slotId);
        return getCertificatesFromKeystore(keyStore);
    }

    private KeyStore getPKCS11KeyStore(String driver, char[] pin, long slotId) throws CertificateException, NoSuchAlgorithmException, IOException {
        Properties properties = getProperties(driver, pin, slotId);
        IAIKPkcs11 provider = new IAIKPkcs11(properties);
        Security.insertProviderAt(provider, 2);
        Properties loginProperties = new Properties();
        DefaultLoginManager loginManager = new DefaultLoginManager(loginProperties);
        provider.setLoginManager(loginManager);
        provider.getTokenManager().getKeyStore();
        KeyStore keyStore = provider.getTokenManager().getKeyStore();
        keyStore.load(null, pin);
        return keyStore;
    }

    private Properties getProperties(String driver, char[] pin, long slotID) {
        Properties properties = new Properties();
        properties.setProperty(Constants.KEY_STORE_SUPPORT_PROVIDER, "IAIK");
        properties.setProperty(Constants.SESSION_POOL_MAX_SIZE, "100");
        properties.setProperty(Constants.MULTI_THREAD_INIT, "true");
        properties.setProperty(Constants.LOGIN_KEYSTORE_SESSION_ON_DEMAND, "false");
        properties.setProperty(Constants.PKCS11_NATIVE_MODULE, driver);
        properties.setProperty(Constants.CHECK_MECHANISM_SUPPORTED, "true");
        properties.setProperty(Constants.USER_PIN, String.valueOf(pin));
        properties.setProperty(Constants.SLOT_ID, String.valueOf(slotID));
        return properties;
    }

     /**
     * Returns certificates on keystore.
     * Keep in mind that this method is just a sample method so not all exceptions are handled properly!
     * @param keyStore
     * @return
     * @throws KeyStoreException
     */
    private List<SmartCardCertificate> getCertificatesFromKeystore(KeyStore keyStore) throws KeyStoreException {
        return Collections.list(keyStore.aliases()).stream().map(alias -> {
            try {
                Key key = keyStore.getKey(alias, null);
                if (key instanceof RSAPrivateKey) {
                    Certificate[] certificateChain = keyStore.getCertificateChain(alias);

                    X509Certificate signerCertificate = (X509Certificate) certificateChain\[0\];
                    boolean[] keyUsage = signerCertificate.getKeyUsage();
                    if ((keyUsage == null) || keyUsage\[0\] || keyUsage\[1\]) {
                        return new SmartCardCertificate(alias, signerCertificate, (PrivateKey) key);
                    }
                }
                return null;
            } catch (Exception e) {
                throw new RuntimeException();
            }
        }).filter(Objects::nonNull).collect(Collectors.toList());
    }

Najważniejsze części pobierania listy certyfikatów:

  • W linijce 4 pobieramy obiekt KeyStore, w którym przechowywane są klucze oraz certyfikaty. Całe pobranie jest stosunkowo proste i odbywa się w metodzie getPKCS11KeyStore.
  • W metodzie getProperties określamy listę parametrów potrzebną do stworzenia Providera IAIKPkcs11, którego następnie użyjemy do pobrania obiektu KeyStore. W parametrach podajemy m.in. driver, pin, a także wybrany slot.
  • W metodzie getCertificatesFromKeystore (wywoływanej w linijce 5) pobieramy listę obiektów wcześniej stworzonej klasy SmartCardCertificate. Keystore posiada w sobie jeden lub więcej aliasów - dla każdego z tych aliasów zapisujemy sam alias, klucz prywatny oraz certyfikat X509.

Efekt końcowy - użycie aplikacji

Odpalając aplikację z wcześniej zdefiniowanej metody main, na początku zostaniemy poproszeni o wybranie jednego z tokenów.

Lista z dostępnymi tokenami oraz możliwość wyboru
Wybór tokenu

Następnie wpisujemy pin.

Wpisanie Pinu do podpisu
Pin

I na koniec z listy dostępnych certyfikatów wybieramy ten, który nas interesuje. Następnie jego zawartość jest wyświetlana na ekranie.

Wybór certyfikatu oraz wyświetlenie informacji na nim
Wybór certyfikatu

Co dalej?

Na podstawie pobranych certyfikatów (a dokładniej X509 oraz klucza prywatnego) możemy już dokonywać podpisów. W następnej części artykułu, dostępnej pod adresem https://pydyniak.pl/podpisywanie-plikow-xml-w-javie-2/, opiszę sposób podpisu plików XML, ale oczywiście tak pobrane dane można użyć także do innych celów np. podpisu innego typu plików.

Copyright: Rafał Pydyniak