Celem artykułu jest zapoznanie Czytelnika z podpisami XML oraz możliwością ich tworzenia i weryfikacji w aplikacjach tworzonych w ramach platformy .NET.
1. Co to są podpisy XML?
Podpisy
XML (ang. XML Signature) są to specjalne podpisy cyfrowe służące
głównie do elektronicznego podpisywania dokumentów XML (technicznie
można podpisać wszystko do czego można uzyskać dostęp przez URL).
Charakterystyczną ich cecha jest to, że podpisany dokument nadal jest
poprawnym dokumentem XML, co ułatwić może jego przetwarzanie. Podpisany
dokument zawiera dodatkowy element
Signature, który zawiera w sobie
właściwy podpis, jak również inne informacje które są potrzebne w celu
jego weryfikacji.
2. Podstawy podpisów cyfrowych
Cyfrowy
podpis pod dokumentem jest to ciąg znaków umożliwiający weryfikację
treści dokumentu. Zmiana choćby jednego znaku w treści dokumentu lub w
podpisie powoduje, że podpis staje się fałszywy.
Aby zrealizować
podpis cyfrowy potrzebna jest para kluczy: prywatny i publiczny. Klucz
prywatny służy do złożenia podpisu pod dokumentem, klucz publiczny
służy do weryfikacji podpisu. Klucz prywatny powinien być szczególnie
chroniony przez właściciela, gdyż jego kompromitacja powoduje, że można
podpisać cyfrowo dowolny i udowodnić, że podpis złożył właściciel
klucza. Klucz publiczny, jak sama nazwa wskazuje, powinien być
publicznie dostępny dla wszystkich, aby możliwa była weryfikacja
podpisu.
W
przypadku podpisów XML klucz prywatny może zostać dołączony do
dokumentu XML (ale może zostać przekazany również inną drogą, np. przez
stronę www). Charakterystyczną cechą pary kluczy jest to, że z klucza
publicznego nie można efektywnie wyliczyć klucza prywatnego i vice
versa.
Podpisy cyfrowe wykorzystują dwie istotne funkcje:
ustaloną funkcję skrótu (inaczej nazywaną funkcją haszującą) oraz
funkcję szyfrującą. Możliwości wyboru obu rodzajów funkcji określone są
standardami dotyczącymi danego rodzaju podpisów cyfrowych. W przypadku
podpisów XML według obowiązujących standardów do szyfrowania można
wykorzystać algorytmy RSA oraz DSA, natomiast jako funkcję skrótu
należy użyć SHA-1.
Algorytmy RSA (nazwa powstała od pierwszych
liter nazwisk twórców: Rivest - Shamir - Adleman) oraz DSA (Digital
Signature Algorytm) różnią się sposobem ustalania pary kluczy oraz
operacjami potrzebnymi do wyliczenia podpisu cyfrowego. Z punktu
widzenia programisty, wystarczy jedynie zadbać, aby para kluczy RSA
była wykorzystywana do podpisów opartych na RSA i analogicznie dla DSA
(kluczami RSA nie można podpisać wiadomości wykorzystując algorytm DSA
i vice versa), nie musi on znać samego algorytmu szyfrowania.
Podpis
nie jest składany pod całą wiadomością, lecz pod jej skrótem, z tego
powodu konieczne jest zastosowanie funkcji skrótu. Funkcja skrótu dla
dowolnej wiadomości wyznacza stałej długości skrót, który ma tę istotną
z punktu widzenia kryptografii własność, że zmiana jednego ze znaków w
dokumencie pociąga za sobą bardzo duże zmiany w skrócie wiadomości
oraz, że nie można w sposób efektywny znaleźć dwóch różnych wiadomości,
którym będzie odpowiadał ten sam skrót (jeśli udałoby się znaleźć takie
wiadomości to podpis złożony pod jedną z nich byłby również ważny pod
drugą wiadomością). W praktyce pojawiły się ataki na stosowany w
podpisach XML algorytm SHA-1, choć na razie nie są wystarczająco
efektywne, aby zagrozić bezpieczeństwu podpisów cyfrowych opartych na
tym algorytmie.
Gdy już mamy odpowiednią parę kluczy, podpisywanie w najogólniejszej postaci przebiega następująco:
1. Policz skrót podpisywanej wiadomości (algorytm SHA-1).
2. Policz podpis pod skrótem wiadomości przy pomocy klucza prywatnego (algorytm RSA lub DSA).
3. Dołącz podpis do dokumentu (lub zapisz podpis w osobnym pliku).
3. Struktura podpisów XML
Aby
zrozumieć w jaki sposób tworzyć i weryfikować podpisy XML trzeba
najpierw poznać ich schemat. Najłatwiej przyjrzeć się przykładowemu
podpisowi:
<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/2000/09/xmldsig#dsa-sha1" />
<Reference URI="">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<DigestValue>RjQyi5Gw10AFZ8FeeWcAHFkplAU=</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>bTo3CR5HyAs5rsl31jIllqmok/CxWWgx/8jGdHWQDUv5SB16S70OdA==</SignatureValue>
</Signature>
Element
Signature w
skazuje miejsce, gdzie znajduje się podpis i inne informacje potrzebne do jego sprawdzenia. Przestrzeń nazw tego elementu (
xmlns)
została zdefiniowana odnośnikiem do odpowiedniego schematu W3.org.
Poniżej tego elementu w drzewie XMLowym znajdują się dwa kolejne
elementy:
SignedInfo oraz
SignatureValue. Pierwszy z nich definiuje dodatkowe informacje, które są przekazywane razem z podpisem, natomiast
SignatureValue to nic innego jak właściwa wartość podpisu.
W przypadku dołączenia klucza publicznego do podpisu poza wspomnianymi dwoma elementami pojawia się trzeci
KeyInfo, zawierający wszystkie potrzebne informacje o kluczu publicznym.
KeyInfo ma różną strukturę w zależności od wyboru metody podpisywania.
Może się tu również pojawić element
Object zawierający treść podpisywanej wiadomości.
CanocalizationMethod oraz
SignatureMethod
odnoszą się do użytych metod kanonizacji jak i zastosowanego algorytmu
do wykonania podpisu. Informacje te są istotne, aby podczas weryfikacji
można było dokonać kanonizacji w ten sam sposób oraz zastosować
odpowiedni algorytm podpisywania. Obie te wartości podawane są jako
odnośniki do specyfikacji W3.org je definiujących. W tym przypadku
wykorzystana została standardowa kanonizacja dokumentu a jako algorytm
podpisywania wykorzystany został DSA/SHA1.
Kanonizacja dokumentu XML
może zostać przeprowadzona na dwa sposoby: typowy (jak powyżej), lub z
pominięciem komentarzy (wtedy jako wartość atrybutu
Algorithm podaje się wartość
http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments
).
Jeśli zaś chodzi o metody podpisywania to poza zaprezentowaną w
powyższym przykładzie metodą DSA/SHA1 istnieje możliwość wykorzystania
metody RSA/SHA1 poprzez zmianę wartości argumentu
Algorithm na
http://www.w3.org/2000/09/xmldsig#rsa-sha1.
Reference to nic innego jak odnośnik do podpisywanego dokumentu z pewnymi dodatkowymi informacjami. W tym przypadku atrybut
URI
jest pusty, co oznacza, że podpisywany dokument znajduje się w tym
samym pliku co podpis. Dodatkowo zdefiniowane zostały przekształcenia (
Transforms),
które muszą zostać dokonane przed weryfikacją podpisu (w tym przypadku
jest jedno przekształcenie definiujące, że wykorzystany został podpis
otaczający - enveloped signature), zastosowana funkcja skrótu (
DigestMethod) oraz wartość funkcji skrótu dla danego dokumentu (
DigestValue).
4. Generowanie kluczy kryptograficznych
4.1 Inicjalizacja generatora
Mając
teraz wystarczające podstawy teoretyczne czas przejść do praktyki.
Podpisywanie dokumentu rozpocząć należy od wygenerowania odpowiedniej
pary kluczy. W tym momencie należy zdecydować czy do wygenerowania
kluczy posłuży algorytm RSA czy DSA. Do generowania kluczy wykorzystuje
się CryptoServiceProvidera (CSP) dla odpowiedniego algorytmu.
Uwaga: Większość wykorzystywanych klas znajduje się w przestrzeniach nazw
System.Xml,
System.Security.Cryptography oraz
System.Security.Cryptography.Xml.
Dla algorytmu RSA odpowiednie instrukcje wyglądają następująco:
[Kod C#]
RSACryptoServiceProvider cryptoKey = new RSACryptoServiceProvider(); // nowy RSA-CSP
|
Natomiast dla DSA:
[Kod C#]
DSACryptoServiceProvider cryptoKey = new DSACryptoServiceProvider();
// nowy DSA-CSP
|
Instrukcje te wygenerują automatycznie parę kluczy. Teraz wygenerowaną parę kluczy warto by wyeksportować:
[Kod C#]
string stringKeyPair = cryptoKey.ToXmlString(true); // zwraca klucze do XMLowego stringa
|
Metoda
ToXmlString()
zwraca wygenerowany klucz w postaci dokumentu XMLa, co jest bardzo
przydatną reprezentacją w przypadku podpisów XML. Metoda ta zawiera
parametr typu
bool, który określa czy ma zostać zwrócony tylko klucz publiczny (wartość
false) czy oba klucze (wartość
true). W tym przypadku potrzebujemy zapisać parę kluczy dlatego parametr ma wartość
true.
4.2 Zapisanie kluczy kryptograficznych
Następnie parę kluczy można zapisać w pliku na dysku:
[Kod C#]
StreamWriter sw = new StreamWriter("sciezka_do_pliku_z_kluczami.xml"); // przygotowuje plik do zapisu sw.Write(stringKeyPair); // zapisuje wyeksportowane klucze sw.Close(); // zamyka plik
|
4.3 Wczytanie kluczy kryptograficznych
Gdy
para kluczy zostanie już zapisana, warto znać możliwość jej wczytania
do CSP, aby zamiast generować nowe pary kluczy, wykorzystać konkretną
parę. Ponownie rozpocząć należy od inicjalizacji odpowiedniego CSP:
Dla RSA:
[Kod C#]
RSACryptoServiceProvider cryptoKey = new RSACryptoServiceProvider(); // nowy RSA-CSP
|
Dla DSA:
[Kod C#]
DSACryptoServiceProvider cryptoKey = new DSACryptoServiceProvider();
// nowy DSA-CSP
|
Następnie należy wczytać zawartość pliku z kluczem(ami) do stringa i wczytać je do odpowiedniego CSP;
[Kod C#]
StreamReader sr = new StreamReader(
"sciezka_do_pliku_z_kluczami.xml"
); // przygotowuje plik do odczytu string stringKeyPair = sr.ReadToEnd(); // wczytuje zawartość całego pliku do stringa sr.Close(); // zamyka plik
cryptoKey.FromXmlString(stringKeyPair); // wczytuje klucze z XMLowego stringa
|
5. Podpisywanie
5.1 Wygenerowanie/wczytanie pary kluczy
Proces podpisywana rozpoczyna się od wygenerowania lub (częściej) wczytania pary kluczy, tak jak to opisano powyżej.
5.2 Przygotowanie dokumentu do podpisania
Teraz należy przygotować (wczytać) dokument XML, który zostanie podpisany:
[Kod C#]
XmlDocument xmlDoc = new XmlDocument(); // tworzy nowy obiekt XmlDocument xmlDoc.Load("plik_do_podpisania.xml"); // wczytuje plik XML do obiektu XmlDocument
|
5.3 Przygotowanie obiektu do przechowania podpisanego dokumentu
Kolejnym krokiem jest przygotowanie obiektu, który będzie przechowywał podpisany dokument XML:
[Kod C#]
SignedXml signedXml = new SignedXml(xmlDoc); // przygotowuje obiekt SignedXml na podstawie obiektu XmlDocument signedXml.SigningKey = cryptoKey; // ustawia klucz do podpisywania
|
5.4 Tworzenie odwołania do podpisywanego dokumentu
Tworzenie odwołania do dokumentu, który jest podpisywany:
[Kod C#]
Reference reference = new Reference(); // nowa referencja reference.URI = ""; // podpis znajdować się będzie w tym samym pliku co podpisany dokument
// transformacja podpisu do podpisu otaczającego (enveloped signature) XmlDsigEnvelopedSignatureTransform envelope = new XmlDsigEnvelopedSignatureTransform(); reference.AddTransform(envelope); // dodanie transformacji do referencji
signedXml.AddReference(reference); // dodanie referencji do podpisanego dokumentu
|
5.5 Złożenie podpisu
Gdy wszystko już zostanie przygotowane można podpisać dokument:
[Kod C#]
signedXml.ComputeSignature(); // wylicza podpis
|
5.6 Dopisanie podpisu do podpisywanego dokumentu
Aby zakończyć cały proces należy jeszcze dopisać podpis do podpisywanego dokumentu:
[Kod C#]
XmlElement xmlDigitalSignature = signedXml.GetXml(); // zwraca element <Signature> jako obiekt XmlElement
// dopisanie elementu <Signature> w odpowiednim miejscu xmlDoc.DocumentElement.AppendChild(xmlDoc.ImportNode(xmlDigitalSignature, true));
xmlDoc.Save(); // zapisanie podpisanego cyfrowo dokumentu
|
6. Weryfikacja podpisu
6.1 Wczytanie klucza
Rozpoczyna się to od inicjalizacji i wczytania do odpowiedniego CryptoServiceProvidera klucza wykorzystywanego do weryfikacji.
Wersja dla RSA:
[Kod C#]
RSACryptoServiceProvider cryptoKey = new RSACryptoServiceProvider();
// nowy RSA-CSP
cryptoKey.FromXmlString(stringKey); // wczytuje klucz(e) z XMLowego stringa
|
Wersja dla DSA:
[Kod C#]
DSACryptoServiceProvider cryptoKey = new DSACryptoServiceProvider(); // nowy DSA-CSP
cryptoKey.FromXmlString(stringKey); // wczytuje klucz(e) z XMLowego stringa
|
W tym przypadku nie potrzeba posiadać pary kluczy, a jedynie sam klucz publiczny.
6.2 Przygotowanie dokumentu do weryfikacji
Następnie przygotować należy dokument do weryfikacji:
[Kod C#]
XmlDocument xmlDoc = new XmlDocument(); // tworzy nowy obiekt XmlDocument xmlDoc.Load("plik.xml"); // wczytuje plik XML do obiektu XmlDocument
SignedXml signedXml = new SignedXml(xmlDoc); // przygotowuje obiekt SignedXml na podstawie obiektu XmlDocument
|
6.3 Odnalezienie podpisu w podpisanym dokumencie
Kluczową operacją jest odnalezienie i wczytanie poddrzewa XML, zawierającego podpis (a więc elementu <Signature>):
[Kod C#]
XmlNodeList nodeList = xmlDoc.GetElementByTagName("Signature");
if (nodeList.Count <= 0) { // NIE ZNALEZIONO ELEMENTU <Signature>. } else if (nodeList.Count >= 2) { // WERYFIKACJA NIE POWIODŁA SIĘ GDYŻ JEST WIĘCEJ NIŻ 1 ELEMENT <Signature> } else { signedXml.LoadXml((XmlElement)nodeList[0]); // wczytanie podpisu do obiektu SignedXml }
|
6.4 Sprawdzenie poprawności podpisu
Ostatnią operacją, którą należy wykonać jest już właściwa walidacja podpisu:
[Kod C#] bool validSignature = signedXml.CheckSignature(cryptoKey);
|
W przypadku gdy wartość
validSignature zawiera
true mamy prawidłowy podpis, w przypadku
false podpis jest sfałszowany.
7. Co jeszcze można zmienić w podpisieOpisana powyżej metoda dotyczy najprostszego możliwego podejścia do podpisu - bez dodawania
KeyInfo oraz dołączając podpis do dokumentu. Warto również wiedzieć w jaki sposób zmodyfikować powyższy kod, aby w bardziej uniwersalny sposób móc posługiwać się podpisami.
7.1 Dodawanie KeyInfo
KeyInfo zawiera informacje o kluczu publicznym, który zamiast przekazywania osobnymi kanałami zostaje przekazany razem z podpisanym dokumentem. Aby dołączyć
KeyInfo do podpisu, należy po utworzeniu obiektu klasy
SignedXml wykonać następujące operacje:
[Kod C#]
KeyInfo keyInfo = new KeyInfo(); // tworzy nowy obiekt KeyInfo
// dla algorytmu DSA: DSAKeyValue keyValue = new DSAKeyValue(cryptoKey); // wartość klucza publicznego w algorytmie DSA // dla algorytmu RSA: RSAKeyValue keyValue = new RSAKeyValue(cryptoKey); // wartość klucza publicznego w algorytmie RSA // dla przypomnienia cryptoKey to obiekt odpowiedniego dla algorytmu CSP
keyInfo.AddClause(keyValue); // dodaje wartość klucza publicznego do KeyInfo signedXml.KeyInfo = keyInfo; // dodaje KeyInfo do obiektu zawierającego podpisany dokument
|
7.2 Dodawanie treści dokumentu do podpisu
Istnieje możliwość dodania treści podpisywanego dokumentu do podpisu, zamiast doklejania podpisu do istniejącego dokumentu. W tym przypadku treść podpisywanego dokumentu znajdować się będzie wewnątrz elementu
Object, który jest potomkiem elementu
Signature, zawierającego podpis. Aby to osiągnąć należy zmodyfikować tworzenie odwołania do podpisywanego dokumentu (punkt 5.4) w następujący sposób:
[Kod C#]
DataObject dataObj = new DataObject(); // nowy obiekt danych dataObj.Data = xmlDoc.ChildNodes; // wstawia dzieci elementu głównego (root) drzewa XML jako dane dataObj.Id = xmlDoc.DocumentElement.Name; // wstawia nazwę elementu głównego (root) drzewa XML jako ID
Reference reference = new Reference(); // nowa referencja reference.Uri = "#" + dataObj.Id; // ustawienie URI na odpowiednią wartość - ID danych obiektu danych
signedXml.AddReference(reference); // dodanie referencji do podpisanego dokumentu
|
Warto zauważyć, że w przypadku takiego podpisywania dokumentów nie ma potrzeby ustawiania transformacji podpisu.
8. Możliwości zastosowaniaOpisane zostały możliwości tworzenia i weryfikacji podpisów XML, jednak wiele osób może zadawać sobie pytanie po co to komu. Zastosowanie podpisów XML może być bardzo szerokie, pomijając oczywistą funkcję jako uwierzytelnianie dokumentów zapisanych w XMLu.
Podpisy te można stosować również w przypadku gdy dana aplikacja umożliwia eksportowanie i importowania danych do i z XMLa, gdy ważne jest zapewnienie, aby żadna zewnętrzna aplikacja w żaden sposób danych nie modyfikowała. Wystarczy wtedy taki eksport podpisać cyfrowo a przy imporcie sprawdzać poprawność podpisu (klucz prywatny w tym momencie jest zaszyty w skompilowanej aplikacji, więc nie można go wydobyć).