Kontytnuję opis korzystania z podpisu elektronicznego w Linuksie.
Poniższy wpis odwołuje się do przykładowej implementacji, którą umieściłem na githubie.
PKCS#11 w Java
Implementacja SunPKCS11
Java od wersji 1.5 ma wbudowaną implementację interfejsów kryptograficznych
(JCE) korzystającą z PKCS#11. Jest to klasa sun.security.pkcs11.SunPKCS11
w implementacji Sun/Oracle oraz OpenJDK.
Implementacja ta jest nieco ułomna.
- brakuje w niej obsługi szyfrowania
RSAES-OAEP. Ten problem wymusił stosowanie Szafir SDK do szyfrowania kart transakcji na stronie GIIF, nawet dla transakcji bez podpisu elektronicznego). - nawet dostęp do publicznych obiektów na tokenie PKCS#11, wymaga zalogowania się do tokena (podania PINu).
- KeyStore odnajduje wyłącznie te wpisy, dla których certyfikat jest przechowywany na karcie (tak konfigurowane są wszystkie obecnie wystawiane podpisy na kartach kryptograficznych, ale w HSMach nCipher i Gemalto na których pracowałem konieczne było dogranie certyfikatów do HSMa.)
Pomimo powyższych problemów wbudowana implementacja jest wystarczająca do składania podpisów.
Implementacja IAIK
Istnieje również komercyjna implementacja JCE korzystająca z PKCS#11, stworzona w Austrii.
Ta impementacja jest droga, ale jej twórcy udostępniają jako freeware wrapper dla bibliotek PKCS#11.
Wrapper jest niskopoziomy, pozwala na wywoływanie poszczególnych funkcji z PKCS#11 API. Użycie wrappera nie jest jednak przedmiotem poniższego wpisu.
BouncyCastle PKIX
Interfejsy kryptograficzne w Java, udostępniają podstawowe operacje kryptograficzne, ale nie umożliwiają tworzenia plików CAdES, czy plików zaszyfrowanych CMS Enveloped.
BouncyCastle PKIX jest biblioteką, służącą (między innymi) do tworzenia plików CMS (Crypto Message Syntax, specyfikacja budowy plików podpisanych i/lub zaszyfrowanych, na tejże specyfikacji oparto format pliku CAdES).
W przykładzie będę wykorzystywał bibliotekę w wersji
org.bouncycastle:bcpkix-jdk15on:1.64.
Biblioteka udostępnia dwie wersje narzędzi do tworzenia plików CMS: zwykłą i strumieniową.
Wersja zwykła wymaga aby zarówno cały źródłowy dokument do podpisania, jak i cały wynikowy dokument podpisany znalazły się się na stosie (heap) JVM.
Wersja strumieniowa jest nieco mniej intuicyjna w konfiguracji, ale nie nakłada takiego ograniczenia.
Przegląd kodu
Zaszyta konfiguracja
Starałem się jak najbardziej uprościć kod, dlatego pliku Main.java w kodzie zaszyłem:
- lokalizacja bibloteki PKCS#11
- numer slotu na tokenie PKCS#11
- PIN do tokena PKCS#11
- numer seryjny certyfikatu do podpisywania
- certyfikat do szyfrowania
1 | public class Main { |
Dostęp do materiału kryptograficznego
Aby uzyskać dostęp do klucza prywatnego na karcie należy:
- załadować provider,
loadProvider - wczytać KeyStore,
getKeyStore - odnaleźć alias dla certyfikatu i klucza,
getAliasByCertificateSerialNumber - pobrać uchwyt klucza.
1 | public class Main { |
Załadowanie proviedera PKCS#11
Konstruktor providera wymaga przekazania mu strumienia z plikiem konfiguracyjnym.
Plik konfiguracyjny ma następującą strukturę.
1 | name = NazwaProvidera |
NazwaProvidera jest wymagana przez Javę, ale do niczego jej potem nie używamy, najlepiej ograniczyć się do liter łacińskich, cyfr i podkreślnika.
slot jest zapisywany dziesiętnie, jest to ten sam numer, który pkcs11-tool
pokazywało w nawiasie w zapisie szestnastkowym.
1 | public class Main { |
Wczytanie KeyStore
KeyStore to obiekt pozwalający zajrzeć jakie certyfikaty i klucze znajdują się na karcie kryptograficznej.
Można go zainicjować na dwa sposoby.
- przekazując wprost PIN (gałąź else)
- przekazując obiekt typu CallbackHandler, który zapyta użytkownika o PIN (gałąź if)
1 | public class Main { |
UWAGA, po trzeciem niepoprawnym podaniu PINu karta zostanie zablokowana!!!
Odnalezienie aliasu dla certyfikatu
KeyStore przypomina trochę słownik (Map), w którym aliasom typu String
przypisane są obiekty typu Certificate oraz Key.
W implementacji SunPKCS11, alias jest pobierany z atrybutu CKA_LABEL.
Jeżeli ten atrybut zawiera polskie znaki diakrytyczne, to alias jest
nieprawidłowo dekodowany, co utrudnia pobranie aliasu wprost.
Zamiast tego przekazuję do metody numer seryjny certyfikatu, a w metodzie iteruję po wszystkich aliasach, aż znajdę certyfikat, o takim numerze seryjnym jak przekazałem.
1 | public class Main { |
Podpisywanie
Podpisywanie wykonywane jest w trybie strumieniowym. Jest to implementacja wzorca projektowego dekorator.
Biblioteka BouncyCastle tworzy strumień (OutputStream), który konfiguruję, wskazując strumień, do którego zapisany zostanie podpisany plik, certyfikat i klucz prywatny używany do podpisu, algorytm podpisu i kilka innych szczegółów. Do tak utworzonego strumienia zapisuję dane, które chę podpisać.
1 | public class Main { |
Konfigurowanie strumienia
Magia kryje się w metodzie SigningStreamBuilder::build.
Poniższy kod opiera się głównie na przykładzie z metody testSHA1WithRSAEncapsulated
w klasie NewSignedDataStreamTest.
1 | private static class SigningStreamBuilder { |
Jak widać strumień potrzebuje:
- podpisywacza
contentSigner, który złoży podpis RSA z użyciem klucza prywatnego z karty. - kalkulatora sumy kontrolnej
digestCalculatorProvider, który wyliczy sumę kontrolną z wiadomości i dodatkowych podpisanych atrybutów. - generatora podpisywanej zawartości
signerInfoGenerator, który połączy sumę kontrolną wiadomości, dodatkowych podpisywanych atrybutów i wywoła na niej podpisywanie. - listy certyfikatów do załączenia w podpisanym dokumencie.
SigningCertificateV2
Dodatkowo przy tworzeniu signerInfoGenerator pojawia się metoda
buildSignedAttributeTableGenerator.
Jej kod opiera się tej odpowiedzi ze StackOverflow.
Europejski podpis elektroniczny wymaga aby jednym z podpisanych atrybutów
podpisu był identyfikator certyfikatu kwalifikowanego użytego do podpisu.
Wymaga to zbudowania elementu ESSCertIDv2 i umieszczenia go wewnątrz
SigningCertificateV2, co robi metoda constructSigningCertificateV2.
Tak przygotowany element, w metodzie buildSignedAttributeTableGenerator,
opakowuje jako atrybut o OID wskazanym przez
PKCSObjectIdentifiers.id_aa_signingCertificateV2 czyli 1.2.840.113549.1.9.16.2.47.
Ten atrybut umieszczam w obiekcie klasy DefaultSignedAttributeTableGenerator.
DefaultSignedAttributeTableGenerator łączy atrybuty przekazane
przez użytkownika z domyślnymi wymaganymi atrybutami (np. signingTime).
1 | private static class SigningStreamBuilder { |
Szyfrowanie
Szyfrowanie również wykonywane jest metodą strumieniową, i również wykorzystuje wzorzec dekoratora.
1. Otwieram strumień wyjściowy result.
2. Konfiguruję i otwieram strumień szyfrujący encrypted, zapisuje on wynik do strumienia result.
3. Konfiguruję i otwieram strumień kompresujący compressed, zapisuje on wynik do strumienia encrypted.
4. Konfiguruję i otwieram strumień podpisujący signed, zapisuje on wynik do strumienia compressed.
5. Otwieram strumień z danymi wejściowymi source.
6. Kopiuję dane ze strumienia source do signed.
1 | public class Main { |
Konfiguracja strumienia kompresującego
Konfigurując strumień przekazuję:
contentOIDczyli informację jakiego typu dane zostały skompresowane, domyślnie jest toCMSObjectIdentifiers.dataczyli typ nieokreślony, wskazanie wprostCMSObjectIdentifiers.signedDatapozwala oprogramowaniu rozszyfrowującemu na użycie parsera dla danych podpisanych CAdES bez konieczności rozpoznawania typu danych na podstawie zawartości.outputCompressorjedynym dostępnym kompresorem jestZLibCompressor
1 | private static class CompressingStreamBuilder { |
Konfiguracja strumienia szyfrującego
Szyfrowanie składa się z następujących faz:
- Generowany jest losowy klucz symetryczny do zaszyfrowania danych.
- Klucz symetryczny jest szyfrowany kluczem publicznym certyfikatu odbiorcy, informacja o certyfikacie odbiorcy (wystawca i numer seryjny) i zaszyfrowany klucz symetryczny są zapisywane na początku pliku zaszyfrowanego.
- Dane zapisywane, które trafiają do strumienia szyfrującego są szyfrowane z użyciem klucza symetrycznego.
Konfigurując strumień przekazuję:
symmetricEncryptionAlgorithm, algorytm dla szyfrowania symetrycznego, domyślnie AES o długości klucza 256 pracujący w trybie CBC.recipientCertificate, certyfikat odbiorcy, jeśli zmodyfikujemy kod, tak aby dodać kolekcję certyfikatów, to możemy stworzyć jeden plik zaszyfrowany do dwóch lub więcej odbiorców, na przykład do siebie, w ten sposób będziemy w stanie sami rozpakować dokładnie taki plik jaki został przesłany do GIIF.contentOIDczyli informację jakiego typu dane zostały skompresowane, domyślnie jest toCMSObjectIdentifiers.dataczyli typ nieokreślony, wskazanie wprostCMSObjectIdentifiers.signedDatalubCMSObjectIdentifiers.compressedDatapozwala oprogramowaniu rozszyfrowującemu na użycie odpowiedniego parsera bez konieczności rozpoznawania typu danych na podstawie zawartości.
1 | private static class EncrypingStreamBuilder { |