Artykuły

A A A
Drukuj Ekportuj do PDF
Opublikowane: 2006.03.29 10:33 | CamlanEx | Aktualizacja: 2010.01.21 2:04

Co tam Panie w kryptografii Framework’a 2.0 ?

Artykuł na celu przybliżyć czytelnikowi nowości i usprawnienia wprowadzone do Framework 2.0 w zakresie pracy z kryptografią.

 

Wstęp:

 

         Obecnie już chyba wszyscy przyzwyczailiśmy się do Framework 2.0. Coraz trudniej znaleźć osobę, która nie znałaby rewolucyjnych usprawnień, jakie niesie ze sobą kolejna odsłona tej platformy programistycznej. MasterPages, WebParts, DataGridView, CLR SQL, ... długo by wymieniać. Rozgłos ten nie obejmuje, zmian, których dokonano w usługach kryptograficznych, a szkoda, bowiem pewne mechanizmy są naprawdę interesujące i moim zdaniem warto poświecić parę chwil, aby lepiej się z nimi zapoznać. Dotyczą one głownie pracy z certyfikatami X.509, poza tym poszerzono możliwości pracy z kryptografia XML oraz dodano przestrzeń nazw Pkcs, pozwalająca w bardzo prosty sposób tworzyć wiadomości zgodne ze standardem PKCS #7. Głownie na tych zagadnieniach skupie się w tym artykule. Zatem nie przedłużając już więcej, zaczynajmy.

 

 

1.          Odsłona pierwsza:  Certyfikaty X.509

 

         Początki tego standardu sięgają przełomu lat 80 i 90 ubiegłego wieku. Wtedy to opracowano standard usługi katalogowej X.500 (RFC 1943).  Zgodnie z jej założeniami miał umożliwić on powstanie globalnej usługi katalogowej. Jednocześnie koniecznie było wprowadzenie mechanizmu dostępu do tego katalogu. Rola ta pierwotni przypadła certyfikatowi X.509 v1(wersja pierwsza) z 1988 roku, z tego faktu właśnie wynika schemat nazewnictwa stosowany w certyfikatach, dostosowany do wcześniej wymienionej usługi katalogowej.

         Sam certyfikat jest to struktura danych zawierająca:  

- nazwę podmiotu

- jego klucz publiczny

- przedział czasu na jaki certyfikat jest ważny

- numer seryjny

- nazwę urzędu, który stwierdził jego poprawność

Całość jest podpisana cyfrowo przez urząd certyfikacji (Certification Authority, CA). Ten model gwarantuje, że dany klucz publiczny należy do określonego podmiotu. Uzyskanie takiego certyfikatu, w pewnym uproszczeniu, przebiega następująco:

 

Key

 

         Podmiot najpierw generuje klucz publiczny i prywatny (najczęściej już w formie certyfikatu). Potem klucz publiczny lub jego certyfikat  jest przesyłany do odpowiedniego urzędu certyfikacji. Tam następuje weryfikacja podmiotu, jeśli przebiegnie ona pomyślnie klucz publiczny jest podpisywany i publikowany przez dany urząd w formie pełnego certyfikatu.

 

1.1. Architektura X.509 PKI (Public Key Infrastructure)

 

Na samym szczycie hierarchii znajduje się IPRA (Internet Policy Registration Authority). Ona ustala wszystkie zasady, którymi rządzą się opisywane certyfikaty. Poniżej niej są PCAs (Policy Certification Authorities). Każda z nich jest certyfikowana przez IPRA.   PCAs publikują reguły, które muszą spełnić użytkownicy, aby uzyskać certyfikat. Poniżej nich znajdują się CA (Certification Authorities), których zadaniem jest bezpośrednia obsługa użytkowników końcowych.    Bardzo istotną sprawą w tym modelu są listy CRL, na których umieszczane są certyfikaty, które utraciły swoją ważność lub zostały skompromitowane. Każda weryfikacja certyfikatu musi uwzględnić kontrole certyfikatu względem CRL. Każdy urząd CA musi publikować taką listę. Oprócz tego, aby certyfikat uznać za wiarygodny, podmiot, który go weryfikuje musi ufać wszystkim urzędom, które poświadczają za ten certyfikat.

Przykładowo:  urząd CA1 nadał certyfikat urzędowi CA2, ten z kolei wydał certyfikat pomiotowi P1. Aby uznać certyfikat P1 za ważny należy ufać zarówno CA2, jak i CA1.

 

X

 

 

UK – użytkownik końcowy. Jest to użytkownik certyfikatu.

(osoba, system wykorzystujący certyfikaty przy realizacji usług bezpieczeństwa)

CA – urząd certyfikacji

PCA – (Policy Certification Authorities)  jednostka nadająca uprawnienia CA i określająca politykę bezpieczeństwa.

IPRA – (Internet Policy Registration Authority) Główny urząd zarządzający standardem. Nadaje uprawnienia urzędom PCA.

RA – urząd rejestrujący, opcjonalna jednostka, któremu CA zleca zdania np. weryfikacji podmiotu.

Repozytorium – zbiór systemów umożliwiający przechowywanie certyfikatów i list CRL. Za ich pośrednictwem są one rozpowszechniane u podmiotów końcowych.

 

 

X.509 w tej chwili stał się podstawą uwierzytelniania stosowanym powszechnie w większości systemów, których uwierzytelnianie opiera się na kryptografii asymetrycznej. Obecnie powszechnie stosowaną wersją, także wspieraną przez Framework 2.0, jest X.509 v3, która została zaakceptowana w 1996, pozwalająca na zastosowanie X.509 w Internecie. Wersja 3 umożliwia na tworzenie rozszerzeń certyfikatu, dając szansę organizacjom lub społecznościom na rejestracje lub standaryzowanie własnych pól. Dodatkowo stworzono grupę standardowych rozszerzeń wspierających dodatkowe możliwości identyfikacji podmiotu, przekazywanie polityki bezpieczeństwa czy ograniczenia ścieżki certyfikacji, które są konieczne przy opracowaniu własnych rozszerzeń przez organizacje.

 

1.2. Podstawy

 

Framework 1.1:

 

Do tej pory przestrzeń nazw System.Security.Cryptography.X509Certificates, odpowiadająca za pracę z certyfikatami, była bardzo ograniczona. Wszystko co można było zrobić za pomocą klas w niej zawartych sprowadzało się do utworzenia certyfikatu:

 

X509Certificate cert = new X509Certificate(byteTab);

X509Certificate cert = X509Certificate.CreateFromCertFile("myCert.cer");

X509Certificate cert = X509Certificate.CreateFromSignedFile("myCert.cer");

        

Oczywiście można było sprawdzać zwartość tego certyfikatu za pomocą standardowych metod :

 

string issuer = cert.GetIssuerName();

string key = cert.GetKeyAlgorithm();

 

Dysponowaliśmy także kolekcją X509CertificateCollection. Na tym kończyły się nasze możliwości. Jednakże nowa odsłona Framework 2.0 oferuje o wiele więcej.

 

Framework 2.0:

 

Pierwsza zmiana, jaka rzuca się w oczy to wprowadzenie klasy X509Certificate2. Dziedziczy ona po X509Certificate dla zachowania wstecznej kompatybilności, ale obok grupy metod Get[...] dysponujemy teraz, opowiadającymi konwencjom kodowania w C#, właściwościami. Chyba najważniejszą zmianą jest wprowadzenie metody cert.Verify() pozwalającej na bardzo wygodną weryfikację danego certyfikatu w systemie użytkownika. Oprócz tego, bardzo poprawiono wygodę posługiwania się konstruktorem.

 

X509Certificate2 cert = new X509Certificate2("root2.cer");

 

 Poprzednie dość niewygodne metody odchodzą do lamusa. Jednakże to jest tylko i wyłącznie kosmetyka usprawniająca naszą pracę, zdecydowanie ciekawsza i ważna jest możliwość programowego dostępu do składów certyfikatów w systemie.

 

1.3. Składy certyfikatów

 

Aby uzyskać dostęp do składu „Osobisty” aktualnego użytkownika systemu  wystarczy wykorzystać klasę X509Store. Przykładowo:

 

X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);

 

Potem już wystarczy otworzyć dany skład i można uzyskać dostęp do każdego z zawartych w nim certyfikatów.

 

store.Open(OpenFlags.ReadWrite);

foreach (X509Certificate2 cert in store.Certificates)

{

      Console.WriteLine(cert.Subject);

      Console.WriteLine(cert.NotBefore);

      Console.WriteLine(cert.NotAfter);

      Console.WriteLine(cert.GetNameInfo(X509NameType.DnsName,true));

}

store.Close();

 

Tworzenie nowego składu, także nie sprawia problemu, wystarczy w konstruktorze podać nazwę składu, który nie występuje w systemie.

 

X509Store store = new X509Store("Test",StoreLocation.CurrentUser);

store.Open(OpenFlags.ReadWrite);

X509Certificate2 cert = new X509Certificate2("testCert.cer");

store.Add(cert);

store.Close();

 

Powyższy kod dodaje także wcześniej utworzony certyfikat do nowego składu.

 

 

1.4. Łańcuch uwierzytelniania

 

To chyba najważniejsza nowa cecha Framework 2.0. Za jej pośrednictwem możemy przeanalizować cały łańcuch certyfikacji danego certyfikatu. Mamy możliwość ustawienia polityki zgodnie, z którą weryfikacja będzie przeprowadzana oraz możemy bardzo dokładnie diagnozować wszystkie ewentualne przyczyny, z powodu których dany łańcuch nie przeszedł weryfikacji.

Cała weryfikacja będzie przebiegać względem zainstalowanych w systemie certyfikatów. Przykładowo, aby weryfikowany certyfikat był poprawny, certyfikat jego głównego urzędu powinien być zainstalowany w składzie „Zaufane główne urzędy certyfikacji”.

 

Najpierw tworzymy obiekt łańcucha i certyfikat, który będziemy weryfikować:

 

X509Chain chain = new X509Chain();

X509Certificate2 cert = new X509Certificate2("root2.cer");

 

Potem, jeśli sobie tego życzymy, możemy zmodyfikować politykę weryfikacji.

 

chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;

chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;

 

W powyższym przykładzie nie będziemy sprawdzać listy CRL oraz zezwalamy na nieznane urzędy CA. Potem już tylko pozostaje utworzyć łańcuch:

 

chain.Build(cert);

 

W momencie należy zrezygnować z metody cert.Verify(), ponieważ jest ona zbyt ogólna (zwraca tylko wartość typu bool)  oraz nie uwzględnia ewentualnych modyfikacji w polityce weryfikacji. Zaleca się korzystać z właściwości donoszących się do statusu. Można sprawdzać je zbiorczo dla całego łańcucha:

 

X509ChainStatus[] statusTab = chain.ChainStatus;

 

if (statusTab.Length == 0)

Console.WriteLine("Łańcuch jest poprawny");

else

{

Console.WriteLine("Występują poniższe błędy :");

 

      foreach (X509ChainStatus status in statusTab)

      {

            Console.WriteLine(status.StatusInformation);

      }

}

 

Inną opcją jest kontrola każdego elementu łańcucha oraz indywidualna analiza błędów.

 

foreach (X509ChainElement chElem in chain.ChainElements)

{

Console.WriteLine("Wystawca : " + chElem.Certificate.Issuer);

      Console.WriteLine("Osoba : " + chElem.Certificate.Subject);

                       

      foreach (X509ChainStatus status in chElem.ChainElementStatus)

      {

            Console.WriteLine(status.StatusInformation);

      }

}

 

 

 

Użyteczna może być także właściwość klasy X509ChainStatus: Status (dzięki niej będziemy mogli odpowiednio reagować na błędy).

 

if (status.Status == X509ChainStatusFlags.Cyclic)

{

      ///Akcja1

}

else if (status.Status == X509ChainStatusFlags.NotTimeValid)

{

      ///Akcja2

}

 

1.5. Inne dodatki

 

Ostatnim elementem, o którym chciałbym wspomnieć to X509Certificate2UI. Udostępnia on 2 statyczne metody. Pierwsza wyświetla sam certyfikat w stylu charakterystycznym dla Windows:

 

Certificate

 

Efekt ten uzyskujemy następująco:

 

X509Certificate2UI.DisplayCertificate(myCertificate);

 

Druga metoda X509Certificate2UI.SelectFromCollection pozwala na wyświetlenie grupy certyfikatów i wybranie z niej jednego z nich. Zbiór wybranych przez użytkownika certyfikatów trafi do kolekcji zwracanej przez tą metodę.

 

X509Certificate2Collection fc = store.Certificates;

X509Certificate2Collection c = X509Certificate2UI.SelectFromCollection(fc,

"Wybierz certyfikaty", "Wybierz", X509SelectionFlag.SingleSelection);

                       

if (c.Count > 0)

{

      myCertificate = c[0];

      this.lblCert.Text = myCertificate.SubjectName.Name;       

      X509Certificate2UI.DisplayCertificate(myCertificate);

}

 

Efekt wizualny, jaki uzyskamy po tym wywołaniu znajduje się na rysunku poniżej.

 

Certificates

 

 

Jak widać praca z tą metodą nie jest najwygodniejsza, należy sprawdzać rozmiar zwróconej kolekcji. Mi osobiście brakuje tutaj zdarzeń, które można by wykorzystać do reakcji na działania użytkownika, jak to ma miejsce w oknach dialogowych. 

 

Na zakończenie rozdziału chciałbym wspomnieć o 2 narzędziach, które mogą okazać się przydatne w czasie pracy z certyfikatami.

Pierwsze z nich to Microsoft Management Console (MMC).

Wystarczy wpisać mmc w Uruchom.  Potem : Dodaj/uruchom przystawkę -> Dodaj.

Wybieramy Certyfikaty. Uzyskamy w ten sposób podgląd i możliwość konfigurowania wszystkich certyfikatów i składów w systemie.

Drugie to makecert.

Pozwala ono na tworzenie testowych certyfikatów. Uzyskujemy do niego dostęp poprzez Visual Studio 2005 Command Prompt 

 

Przykładowo: makecert -r -n "CN=slewandowski" -ss my

 

Utworzy certyfikat dla slewandowski (opcja –n), wystawiony przez Lewandowski opcja –r : czyli „samowystawienie”) i umieści go w składzie „Osobiste”

(opcja –ss my). Więcej informacji na temat tego polecenia z łatwością można odnaleźć w dokumentacji Visual Studio.

 

Tak wygląda pewien zarys pracy z certyfikatami w Framework 2.0.  Kolejną nowością wprowadzoną w VS2005 jest możliwość szyfrowania elementów XML.

 

 

 

2.          Odsłona druga: XML

 

We wcześniejszej wersji Framework mogliśmy tylko podpisywać dokumenty XML. Teraz otrzymaliśmy możliwość szyfrowania wybranych elementów dokumentu XML. Cały mechanizm jest zgodny z rekomendacją W3C: XML Encryption Syntax and Processing.

Większość klas, z jakimi będziemy mieli do czynienia są bezpośrednimi odpowiednikami elementów drzewa, które powinny wystąpić w tym dokumencie.

Aby uzyskać dostęp do przestrzeni nazw System.Security.Cryptography.Xml należy dodać referencje do System.Security. Dopiero w tym momencie będziemy mogli rozpocząć prace.

Do szyfrowania elementów XML możemy wykorzystać algorytmy symetryczne, asymetryczne, a także certyfikaty. Szyfrowaniu będzie poddawany wybrany element z drzewa dokumentu, po przeprowadzeniu tego procesu zaszyfrowany element zastąpi swój jawny odpowiednik.

 

Do szyfrowania elementów XML wykorzystam poniższy przykładowy dokument :

 

<?xml version="1.0" encoding="utf-8" ?>

<Person>

<FirstName>John</FirstName>

<LastName>Smith</LastName>

<CreditCard Limit="15,000">

<Number>090931313314</Number>

<Issuer>Bank</Issuer>

<Expiration>04/10</Expiration>

</CreditCard>

</Person>

 

2.1. Symetryczne szyfrowanie elementów XML

 

To chyba najbardziej intuicyjna wersja szyfrowania, w najmniejszym stopniu obciążająca strukturę dokumentu i jego rozmiar.

W pierwszej kolejności należy utworzyć algorytm, za pomocą którego będziemy szyfrować oraz przygotować dokument :

 

RijndaelManaged aes = new RijndaelManaged();

XmlDocument doc = new XmlDocument();

doc.Load("Person.xml");

 

Następnie należy odnaleźć element drzewa, który będzie poddany szyfrowaniu

 

XmlElement elem = doc.GetElementsByTagName("CreditCard")[0] as XmlElement;

 

Potem tworzymy obiekt opakowujący dokument klasy EncryptedXml

 

EncryptedXml eXml = new EncryptedXml(doc);

byte[] data = eXml.EncryptData(elem, aes,true);

 

i przy jego pomocy przeprowadzamy szyfrowanie.

 

Kolejnym krokiem będzie utworzenie obiektu EncryptedData który już odpowiada elementowi zaszyfrowanego dokumentu.

 

EncryptedData enData = new EncryptedData();

enData.Type = EncryptedXml.XmlEncElementUrl;

enData.EncryptionMethod = new EncryptionMethod(EncryptedXml.XmlEncAES128Url);

enData.CipherData.CipherValue = data;

 

Powyżej jest ustawiony atrybut Type tego elementu, a także jego dzieci zgodnie ze składnią ustanowioną przez W3C. Musimy także podać, jaki algorytm wykorzystujemy. Lista wszystkich możliwych algorytmów znajduje się w rekomendacji W3C.

 

Następnie wystarczy podmienić element niezaszyfrowany na zaszyfrowany w drzewie i ewentualnie zapisać dokument.

 

EncryptedXml.ReplaceElement(elem, enData, false);

doc.Save("Person.xml");

 

Efekt naszego szyfrowania znajduje się poniżej :

 

<?xml version="1.0" encoding="utf-8" ?>

<Person>

<FirstName>John</FirstName>

<LastName>Smith</LastName>

<CreditCard Limit="15,000">

<EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element"   xmlns="http://www.w3.org/2001/04/xmlenc#">

<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc" />

<CipherData>

<CipherValue>6ZDPEtEL/7greWzHR5UkxUyLUmpHZ7Ai+4Hd+THY3c5Q93F3XBn

+GpObH8Zrs++anRoFCol/LmTQdXvF/p+NT/KaFJQ/hwKEKnWEqFAIXZFpCNcFl

O7e4kc4EUNx8BkcmuyqXooszbee+pgmD/1aRg==

</CipherValue>

</CipherData>

</EncryptedData>

</CreditCard>

</Person>

 

Warto zwrócić uwagę na to, że element CreditCard i jego atrybuty są nadal widoczne. Można tak postępować, kiedy chcemy, aby informacja o tym, że występuje tam określony obiekt była dostępna, np. dla robotów/pająków sieciowych. Jeżeli jednak chcielibyśmy ukryć informacje o tym elemencie, należy zamienić 2 wywołania i zamiast true podać false.

 

byte[] data = eXml.EncryptData(elem, aes,false);

EncryptedXml.ReplaceElement(elem, enData, false);

 

<?xml version="1.0" encoding="utf-8" ?>

<Person>

<FirstName>John</FirstName>

<LastName>Smith</LastName>

<EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element" xmlns="http://www.w3.org/2001/04/xmlenc#">

<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc" />

<CipherData>

<CipherValue>TOIl97NIlJQVSzki9oIJOSuyLydAGGlCzjhJZloK6TvtLkFPK09woDFvJ

ZS0LQx9a6RmRvlXH2FAT9VTwMzZuszpehtoAtks2neWa0K9TlYgz6v+JLY96DAcJ

GEG8swLM33Sci7QviZbNfGW0qxc4cO/OgsrlZDBbHTEc0KmOCWValVzhQKxTlR4oeswtFD

</CipherValue>

</CipherData>

</EncryptedData>

</Person>

 

W tym momencie element CreditCard jest już niewidoczny.

 

Natomiast sam proces deszyfrowania jest już o wiele prostszy. Ponownie wstępnie tworzymy algorytm, wczytujemy dokument i na końcu pobieramy element, który nas interesuje. W tym momencie wybieramy element EncryptedData.

 

RijndaelManaged aes = new RijndaelManaged();

aes.Key = Key;

XmlDocument doc = new XmlDocument();

doc.Load("Person.xml");

XmlElement encryptedElem = doc.GetElementsByTagName("EncryptedData")[0] as XmlElement;

 

Tworzymy obiekt EncryptedData i wprowadzamy do niego nasz zaszyfrowany element. Następnie tworzymy obiekt pomocniczy EncryptedXml.

 

EncryptedData enData = new EncryptedData();

enData.LoadXml(encryptedElem);

EncryptedXml eXml = new EncryptedXml();

 

Ostatecznie wystarczy deszyfrować elementy. Podmienić je i ewentualnie zapisać.

 

byte[] data = eXml.DecryptData(enData, aes);

eXml.ReplaceData(encryptedElem, data);

doc.Save("Person.xml");

 

Jest to w miarę prosta (z punkt widzenia implementacji i zaangażowanych zasobów) metoda szyfrowania. O wiele bardziej skomplikowaną alternatywą jest asymetryczne szyfrowanie wiadomości XML.

 

2.2. Szyfrowanie asymetryczne elementów XML

 

W sytuacji, kiedy wymagany jest bardzo wysoki poziom bezpieczeństwa zastosować kryptografię asymetryczną. Proces szyfrowania nie przebiega jednak tak, że wykorzystywane jest przekształcenie RSA do zaszyfrowania elementu drzewa. W tym wypadku w pierwszej kolejności tworzony jest symetryczny klucz sesji, którym szyfrowany jest wybrany element, potem klucz sesji jest dołączany do dokumentu i dopiero sam klucz jest szyfrowany kluczem publicznym odbiorcy. Tak, w pewnym uproszczeniu, przebiega ten proces.

 

Kodowanie w tym wypadku, jest trochę bardziej skomplikowane. Początkowa faza przebiega podobnie, jak poprzednio. Sprowadza się do inicjalizacji algorytmów, załadowania dokumentu, odnalezieniu interesującego nas elementu i zaszyfrowaniu go algorytmem symetrycznym.

 

CspParameters csp = new CspParameters();

csp.KeyContainerName = "XMLKeys";

RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(csp);

RijndaelManaged aes = new RijndaelManaged();

XmlDocument doc = new XmlDocument();

doc.Load("Person.xml");

 

XmlElement elem = doc.GetElementsByTagName("CreditCard")[0] as XmlElement;

EncryptedXml eXml = new EncryptedXml();

byte [] data =  eXml.EncryptData(elem,aes,true);

EncryptedData enData = new EncryptedData();

enData.Type = EncryptedXml.XmlEncElementUrl;

enData.EncryptionMethod = new EncryptionMethod(EncryptedXml.XmlEncAES128Url);

 

Zmiany następują od tego miejsca. W tej chwili musimy dołączyć zastosowany w algorytmie klucz sesji i zaszyfrować go odpowiednim kluczem publicznym.

 

EncryptedKey ek = new EncryptedKey();

byte[] encKey = EncryptedXml.EncryptKey(aes.Key, rsa,false);

ek.CipherData = new CipherData(encKey);

ek.EncryptionMethod = new EncryptionMethod(EncryptedXml.XmlEncRSA15Url);

enData.KeyInfo.AddClause(new KeyInfoEncryptedKey(ek));

 

Potem do wcześniej zaszyfrowanego elementu musimy dołączyć identyfikator klucza publicznego, aby można było potem zastosować odpowiedni klucz prywatny do deszyfrowania odpowiedniego klucza sesji.

 

KeyInfoName kin = new KeyInfoName();

kin.Value = "test";

ek.KeyInfo.AddClause(kin);

 

Ostatecznie ładujemy zaszyfrowane dane do elementu CipherData obiektu enData i zamieniamy go z elementem, który szyfrujemy.

 

enData.CipherData.CipherValue = data;

EncryptedXml.ReplaceElement(elem, enData, true);

 

Mamy także możliwość wielokrotnego wykorzystania tego samego klucza sesji i klucza publicznego do zaszyfrowania kilku elementów w dokumencie.

Aby tego dokonać, do klucza sesji, kiedy go wykorzystujemy po raz pierwszy, należy dodać identyfikator.

 

ek.CipherData = new CipherData(encKey);

ek.EncryptionMethod = new EncryptionMethod(EncryptedXml.XmlEncRSA15Url);

ek.Id = "test_key";

 

Drugi element jest podobnie wyszukiwany i szyfrowany:

 

XmlElement elem2 = doc.GetElementsByTagName("LastName")[0] as XmlElement;

byte[] data2 = eXml.EncryptData(elem2, aes, true);

EncryptedData enData2 = new EncryptedData();

enData2.Type = EncryptedXml.XmlEncElementUrl;

 

enData2.EncryptionMethod = new EncryptionMethod(EncryptedXml.XmlEncAES128Url);

 

Różnica występuje przy tworzeniu obiektu EncryptedKey. Musimy dołączyć referencję do obiektu klucza, który już występuje w dokumencie.

 

EncryptedKey ek2 = new EncryptedKey();

DataReference dRef = new DataReference();

dRef.Uri = "#" + ek.Id;

ek2.AddReference(dRef);

 

Finał całej operacji jest podobny jak poprzednio:

 

enData2.KeyInfo.AddClause(new KeyInfoEncryptedKey(ek));

enData2.CipherData.CipherValue = data2;

EncryptedXml.ReplaceElement(elem2, enData2, true);

 

Deszyfrowanie ponownie jest o wiele prostsze. Cała procedura sprowadza się do przypisania algorytmu zawierającego klucz prywatny odbiorcy do odpowiedniego id. Id musi się zgadzać z tym, który przyporządkowaliśmy elementowi KeyInfoName szyfrując dokument. Na końcu wystarczy wywołać funkcje DecryptDocument().

 

doc.Load("Person.xml");

EncryptedXml exml = new EncryptedXml(doc);

exml.AddKeyNameMapping("test", rsa);

exml.DecryptDocument();

doc.Save("Person.xml");

 

Najprościej od strony kodu przebiega szyfrowanie elementów XML z wykorzystaniem certyfikatów X.509. Wystarczy wczytać certyfikat klucza publicznego i wykorzystać go do szyfrowania, jest to niesamowicie wygodne (sama inicjalizacja nie różni się od poprzednich przypadków):

 

EncryptedXml eXml = new EncryptedXml();

EncryptedData enData = eXml.Encrypt(elem, cert);

EncryptedXml.ReplaceElement(elem, enData, false);

doc.Save("Person.xml");

 

Wynik takiego szyfrowania znajuduje się poniżej.

 

<?xml version="1.0" encoding="utf-8" ?>

<Person>

<FirstName>John</FirstName>

<LastName>Smith</LastName>

<EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element" xmlns="http://www.w3.org/2001/04/xmlenc#">

<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-

cbc" />

<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">

<EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">

<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"

/>

<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">

<X509Data> <X509Certificate>[…usunięto…]</X509Certificate>

</X509Data>

</KeyInfo>

<CipherData>

<CipherValue>[…usunięto…]</CipherValue>

</CipherData>

</EncryptedKey>

</KeyInfo>

<CipherData> 

<CipherValue>[…usunięto…]</CipherValue>

</CipherData>

</EncryptedData>

</Person>

 

Analizując ten dokument można zauważyć, że wybrany element jest szyfrowany symetrycznym kluczem sesji. Ten klucz dopiero jest szyfrowany kluczem publicznym z certyfikatu. Sam certyfikat klucza publicznego jest ostatecznie dołączany do dokumentu.

 

Deszyfrowanie jest bardzo proste, przeszukiwane są certyfikaty ze składu dla aktualnego użytkownika. Jeżeli zostanie odnaleziony właściwy certyfikat, dokument zostanie odszyfrowany.

 

doc.Load("Person.xml");

EncryptedXml eXml = new EncryptedXml(doc);

eXml.DecryptDocument();

doc.Save("Person.xml");

 

W tym przypadku należy zwracać uwagę na fakt, że jeżeli rozmiar dokumentu jest niewielki, to przy zastosowaniu tego mechanizmu może on relatywnie bardzo wzrosnąć.

 

 

2.3. Podpisywanie dokumentów XML w wyłącznej postaci kanonicznej

(Exclusive XML Canonicalization)

 

Kanoniczna postać dokumentu XML to w bardzo dużym uproszczeniu także dokument XML, ale pozbawiony pewnych informacji np. tego, że określone wartości są domyślne, wycinane są zbędne białe znaki, może nastąpić wykasowanie komentarzy (jeżeli stosowane jest przekształcenie kanoniczne „without-comments”). Tak przygotowany dokument jest dopiero poddany podpisywaniu. Daje to gwarancje, że postać dokumentu, będzie taka sama przy podpisywaniu i weryfikacji.

Trudności pojawią się w momencie, kiedy podpisywany dokument będzie opakowywany w różne koperty zawierające zmieniające się definicje przestrzeni nazw. Wymagają tego protokoły i aplikacje, które opakowują w kopertę określony dokument XML (np. WebService). Problem związany jest z tym, że metody kanonizacji wykorzystują XPath, które opiera się na strukturze dokumentu. Zastosowanie standardowych metody kanonizacji prowadzi to do tego, że potomkowie, w postaci kanonicznej dokumentu, zawierają definicje przestrzeni nazw swoich rodziców. W sytuacji kiedy dochodzi do zmiany „koperty” podpis jest często błędnie odrzucany.

Rozwiązanie tego problemu przyniosła rekomendacja W3C Exclusive XML Canonicalization version 1.0. Znalazła ona wsparcie we Framework 2.0. Klasy : XmlDsigExcC14NTransform i XmlDsigExcC14NWithCommentsTransform  pozwalają na podpisywanie i weryfikacje wybranych elementów w dokumencie XML niezależnie od kontekstu.

Więcej informacji o metodach kanonizacji i opisanych przeze mnie problemach można odnaleźć w rekomendacjach W3C.

 

Przykładowo chcemy podpisać element LastName w poniższym dokumencie:

 

<?xml version="1.0" encoding="utf-8" ?>

<Person xmlns:t="test">

<FirstName>John</FirstName>

<s:LastName xmlns:s="mySgn" id="signme">Smith</s:LastName>

</Person>

 

Dotychczasowa procedura podpisania wyglądała następująco :

 

XmlDocument doc = new XmlDocument();

doc.Load("Person.xml");

doc.PreserveWhitespace = false;

SignedXml sgn = new SignedXml(doc);

sgn.SigningKey = rsa;

 

Po wczytaniu dokumentu i ustawieniu odpowiedniego algorytmu należało ustawić referencję do elementu, który będzie podpisany oraz dołączyć odpowiednie przekształcenie :

 

Reference reference = new Reference();

reference.Uri = "#signme";

XmlDsigEnvelopedSignatureTransform env = new XmlDsigEnvelopedSignatureTransform();

reference.AddTransform(env);

sgn.AddReference(reference);

 

Ostatni etap to wstawienie informacji o kluczu, podpisanie wiadomości i dołączenie podpisu do dokumentu.

 

KeyInfo keyInfo = new KeyInfo();

keyInfo.AddClause(new RSAKeyValue(rsa));

sgn.KeyInfo = keyInfo;

sgn.ComputeSignature();

XmlElement xmlDgSgn = sgn.GetXml();

doc.DocumentElement.AppendChild(doc.ImportNode(xmlDgSgn,true));

                           

Jednak w sytuacji, kiedy podpisany dokument otoczymy kopertą :

 

<?xml version="1.0" encoding="utf-8" ?>

<e:env xmlns:e="envelope.org">

<e:body>

<Person xmlns:t="test">

<FirstName>John</FirstName>

<s:LastName xmlns:s="mySgn" id="signme">Smith</s:LastName>

<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">

      […usunięto…]

</Signature>

</Person>

</e:body>

</e:env>

 

Nie będzie już on poprawnie przechodził weryfikacji. Aby temu zaradzić należy podjąć kilka prostych kroków. Po pierwsze trzeba ustawić odpowiednią metodę kanonizacji w podpisie :

 

sgn.SigningKey = rsa;

sgn.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl;

XmlDsigExcC14NTransform xmlExcMet = sgn.SignedInfo.CanonicalizationMethodObject as XmlDsigExcC14NTransform;

 

A potem dodać odpowiednią transformacje :

 

XmlDsigEnvelopedSignatureTransform env = new XmlDsigEnvelopedSignatureTransform();

reference.AddTransform(xmlExcMet);

reference.AddTransform(env);

sgn.AddReference(reference);

 

W tej chwili po opakowaniu podpisywanego dokumentu kopertą nie będzie problemów z weryfikacją. Poza tym, należy pamiętać, aby podpisywany element znajdował się w określonej przestrzeni nazw (najlepiej w takiej, której definicje sam zawiera). Jeśli w naszym przypadku podpisywany element wyglądałby następująco:

 

<LastName xmlns="mySgn" id="signme">Smith</LastName>

 

Zastosowanie wyłącznej metody kanonizacji nie przyniosłoby oczekiwanego efektu.

Oprócz tego, klasa XmlDsigExcC14NTransform udostępnia opcjonalną właściwość   InclusiveNamespacesPrefixList, za pomocą której można ustawić przestrzenie nazw, które mają być kanonizowane standardową metodą, np. :

 

xmlExcMet.InclusiveNamespacesPrefixList = "soap #default";

 

Ostatnia nowość we Framework 2.0 to możliwość pracy z wiadomościami zgodnymi z PKCS #7.

 

 

3.          Odsłona trzecia: PKCS

 

Skrót PKCS pochodzi od Public Key Cryptographic Standards. Jest to zbiór standardów publikowanych i rozwijanych przez RSA Security i największe firmy tworzące oprogramowanie (Microsoft, Sun, etc.) . Jego głównym celem jest rozwój kryptografii klucza publicznego. Obecnie jest to chyba najważniejszy i powszechnie stosowany zaakceptowany zbiór standardów, zapewniający możliwość współpracy różnych aplikacji.

Poniższa tabela przedstawia listę wszystkich standardów PKCS, wraz, z krótkim opisem ich zastosowania.

 

Standard

Opis

PKCS #1

Definiuje standard RSA czyli mechanizmy dla szyfrowania i podpisywania przy pomocy tego systemu.

PKCS #3

Definiuje protokół wymiany klucza Diffiego-Hellmana.

PKCS #5

PBE – standard pozwalający generować klucz na podstawie hasła.

PKCS #6

Standard dla certyfikatów, wyparty przez X509v3

PKCS #7

Standard wiadomości kryptograficznych. Definiuje, jak powinny być zbudowane wiadomości, do których stosowana jest kryptografia.

PKCS #8

Informuje o metodach przechowywania klucza prywatnego.

PKCS #9

Definiuje typy atrybutów wykorzystywane w innych standardach.

PKCS #10

Ten standard zawiera definicje żądań o przyznanie certyfikatu.

PKCS #11

Standard niezależnego od sprzętu API dla tokeńow (urządzeń kryptograficznych).

PKCS #12

Ten dokument definiuje składnię wymiany informacji. (składowanie, transport kluczy prywatnych)

PKCS #13

ECC – dokument definiuje szyfrowanie i podpisywanie wiadomości za pomocą kryptografii krzywych eliptycznych.

PKCS #14

Standard generatora liczb pseudolosowych

PKCS #15

Standard dla danych użytkownika przechowywanych na tokenach

 

Standardy #2 i #4 nie istnieją niezależnie, zostały włączone do PKCS #1.

 

Najważniejszy w tym momencie dla nas jest dokument PKCS #7. Ponieważ właśnie on znalazł bezpośrednie wsparcie w Framework 2.0. Określa składnie dla cyfrowych wiadomości CMS (Cryptographic Message Syntax). Można wyróżnić podstawowe typy zdefiniowane w tym dokumencie podpis cyfrowy i kopertę cyfrową.

Podpis cyfrowy zdefiniowany w PKCS #7 ma za zadanie zagwarantować, że dana wiadomość pochodzi od określonej osoby. Aby to zapewnić w pierwszej kolejności liczony jest skrót z podpisywanej wiadomości, potem ten skrót jest podpisywany kluczem prywatnym nadawcy. Ostatecznie podpisany skrót jest dołączany do wiadomości.

         Weryfikacja podpisu cyfrowego rozpoczyna się od policzenia skrótu z przesłanej wiadomości, jednocześnie dołączony skrót jest szyfrowany kluczem publicznym. Jeżeli skrót z wiadomości i wynik szyfrowania zgadzają się, to wiadomość pozytywnie przechodzi weryfikacje. Dodatkowo często korzysta się z usług zewnętrznego podmiotu znakującego czas, w którym wiadomość została podpisana.

         Z kolei koperta cyfrowa zapewnia poufność wiadomości. Schemat szyfrowania (na tym poziomie abstrakcji) nie odbiega od tego zaprezentowanego w przypadku dokumentów XML.

         Weryfikacja wymaga, aby odbiorca podziałał swoim kluczem prywatnym na klucz sesji i dopiero po udanym odszyfrowaniu klucza sesji, może przy jego pomocy odtworzyć wiadomość.

         Standard PKCS pozwala na łączenie tych dwóch mechanizmów. Jeżeli wcześniej podpisaną wiadomość zaszyfrujemy uzyskamy zarówno poufność, jak i integralność wiadomości. (Warto zwrócić uwagę, że odwrotna kolejność nie jest zalecana, ze względu na możliwości ataku).

         Więcej bardzo precyzyjnych informacji odnośnie tego, jakie pola zawiera taka wiadomość, na jakie funkcje skrótu akceptuje itd. można bezpośrednio odnaleźć w dokumentacji standardu.

         PKCS #7 zezwala także, aby klucze (w postaci certyfikatów, np. X.509) były dołączone w większej liczbie niż 1. (Np. jedna wiadomość przeznaczona dla wielu odbiorców. Ten przykład zobaczymy za chwile).

        

Pierwszy przykład jaki zaprezentuję to podpisywanie i weryfikacja pliku w postaci ciągu bajtów. Warto ponownie zwrócić uwagę na fakt, że tworzone i wykorzystywane obiekty bezpośrednio odpowiadają strukturze wiadomości CMS.

 

O ile całość sprawia wrażenie skomplikowanej operacji, bo tak na dobrą sprawę jest, to zakodowanie tego w tym momencie jest naprawdę proste:

 

ContentInfo info = new ContentInfo(inBytes);

SignedCms cms = new SignedCms(info);

CmsSigner signer = new CmsSigner(cert);

cms.ComputeSignature(signer);

outBytes = cms.Encode();

 

Najpierw tworzymy strukturę ContentInfo odpowiadającą za zawartość wiadomości, podając do niej bajty, jakie chcemy podpisać. Potem tworzony w oparciu o to jest obiekt definiujący podpisaną wiadomość oraz obiekt, w którym zwarty jest certyfikat z kluczem prywatnym. Z tego ostatniego korzystamy podpisując wiadomość. Wywołanie metody Encode() przekształca znajdujący się w pamięci dokument w BLOB.

 

Weryfikacja jest ściśle zintegrowana ze składami certyfikatów w naszym systemie. Przebiegnie ona pomyślnie, jeśli będziemy mieli zainstalowany certyfikat osoby podpisującej w składzie „Inne osoby” lub do metody CheckSignature przekażemy skład zawierający certyfikat klucza publicznego podmiotu, który podpisał wiadomość.

 

SignedCms signedCms = new SignedCms();

signedCms.Decode(inBytes);

signedCms.CheckSignature(true);

 

Metoda signedCms.CheckSignature(true) rzuci wyjątek, jeśli weryfikacja się nie powiedzie. Podając true nie wymagamy, aby certyfikat był weryfikowany pod kątem CRL czy zaufania do jego CA.

 

Niemalże bliźniaczo przedstawia się sytuacja z szyfrowaniem wiadomości.

 

ContentInfo info = new ContentInfo(inBytes);

EnvelopedCms evCms = new EnvelopedCms(info);

CmsRecipient recipl = new CmsRecipient(SubjectIdentifierType.IssuerAndSerialNumber,cert);

evCms.Encrypt(recipl);

outBytes = evCms.Encode();

 

Jedyna różnica “od strony kodu” polega na tym, że EnvelopedCms zastąpił SignedCms oraz musimy utworzyć obiekt odbiorcy, przekazać tam certyfikat z kluczem publicznym oraz ustawić sposób identyfikacji. Z obiektu odbiorcy korzystamy szyfrując wiadomość.

 

Jeżeli życzymy sobie, aby wiadomość mogła być przekazana wielu podmiotom, zamiast jednego obiektu odbiorcy przekazujemy ich kolekcję.

 

ContentInfo info = new ContentInfo(inBytes);

EnvelopedCms evCms = new EnvelopedCms(info);

CmsRecipientCollection rcpCol = new CmsRecipientCollection(SubjectIdentifierType.IssuerAndSerialNumber, col);

evCms.Encrypt(rcpCol);

outBytes = evCms.Encode();

 

Obiekt col w tym miejscu to kolekcja certyfikatów, które wykorzystane zostaną do szyfrowania, dla każdego odbiorcy zostanie utworzona jego własna kopia zaszyfrowanej wiadomości.

 

Odszyfrowanie zarówno dla jednego, jak i wielu odbiorców przebiega tak samo:

 

EnvelopedCms evCms = new EnvelopedCms();

evCms.Decode(inBytes);

evCms.Decrypt(evCms.RecipientInfos[0]);             

outBytes = evCms.Encode();

 

Aby odszyfrować wiadomość trzeba w składzie “Osobiste” należy posiadać odpowiedni certyfikat z kluczem prywatnym lub przekazać dodatkowy skład certyfikatów przy wywołaniu metody Decrypt.

 

Dużego problemu w tym momencie nie stanowi połączenie tych obu operacji i zaszyfrowanie wcześniej podpisanej wiadomości.

 

Jak łatwo się domyślić będziemy korzystali z 2 certyfikatów (jeden z kluczem publicznym, drugi zawierający klucz prywatny), a podpisaną wiadomość uczynimy punktem początkowym dla szyfrowania.

 

ContentInfo info = new ContentInfo(inBytes);

SignedCms cms = new SignedCms(info);

CmsSigner signer = new CmsSigner(certSign);

cms.ComputeSignature(signer);

 

Powyżej jest standardowa procedura podpisania. Kluczowy punkt znajduje się poniżej:

 

ContentInfo envInfo = new ContentInfo(cms.Encode());

 

Popisaną wiadomość przekazujemy do zaszyfrowania. Końcowe fragmenty kodu są już całkowicie znajome:

 

EnvelopedCms evCms = new EnvelopedCms(envInfo);

CmsRecipient recipl = new CmsRecipient(SubjectIdentifierType.IssuerAndSerialNumber, certEncrypt);

evCms.Encrypt(recipl);

outBytes = evCms.Encode();

 

Odszyfrowanie i późniejsza weryfikacja przebiegają analogicznie (chyba w tym miejscu już nie trzeba powtarzać, że potrzebne będą 2 certyfikaty, ale w odwrotnym układzie kluczy niż w poprzedniej operacji).

 

Najpierw przeprowadzamy deszyfrowanie:

 

EnvelopedCms envCms = new EnvelopedCms();

envCms.Decode(inBytes);

envCms.Decrypt(envCms.RecipientInfos[0]);

 

Gdy to się powiedzie następuje weryfikacja podpisu.

 

SignedCms sngCms = new SignedCms();

sngCms.Decode(envCms.Encode());

sngCms.CheckSignature(true);

outBytes = sngCms.ContentInfo.Content;

 

W drugiej linijce odszyfrowana wiadomość została przekazana do obiektu podpisu.

 

Ostatnia na uwaga, a w zasadzie ostrzeżenie, na zakończenie tego działu. Jeżeli będziemy wykorzystywali certyfikaty utworzone przy użyciu narzędzia makecert do tworzenia zaszyfrowanych wiadomości CMS, to przy operacji odszyfrowywania zostanie rzucony wyjątek typu CryptographicException. Aby te przykłady działały warto skorzystać z innych certyfikatów (np. utworzonych przy pomocy własnego CA w Windows 2003 lub jakiś innych testowych. Dla tych, którzy nie dysponują dostępem do poprawnych certyfikatów podaje link do freeware, który do celów testowych pozwala bardzo prosto tworzyć własne certyfikaty z kluczem prywatnym: abylon SELFCERT.

 

4.          Odsłona czwarta: Coś jeszcze ??

 

Z całą pewnością tak. Nowości w kolejnej odsłonie Framework’a jest jeszcze sporo. Ot chociażby, RSACryptoServiceProvider, który udostępnia teraz metody rsa.ImportCspBlob oraz rsa.ExportCspBlob, które pozwalają na eksport prywatnych parametrów klucza do postaci binarnej, na takich samych zasadach, jak znana z Framework 1.1 rsa.ToXmlString. Wystarczy podać true, aby włączyć składnik prywatny. Rzeczywiście przechowywanie elementów prywatnych na dysku w niezabezpieczonym obszarze to nic mądrego, ale skoro mogliśmy to robić w postaci XML, to dlaczego nie można było robić tego w postaci binarnej. Poza tym, udostępniono więcej funkcji HMAC: HMACSHA256, HMACSHA384...  Także wyczerpanie nawet tak relatywnie ograniczonego tematu wydaje się kłopotliwe.

 

5.          Zakończenie

 

Mam nadzieje, że udało mi się rzucić trochę światła na nowe zagadnienia, jakie pojawiły się w kryptograficznych przestrzeniach nazw .NET 2.0  i że przynajmniej w obranym przeze mnie zakresie udało mi się przyzwoicie przekazać temat. Więcej informacji związanych z prezentowanymi tutaj zagadnieniami można odnaleźć w bibliografii. W załączniku znajduje się kod z przykładami wykorzystanymi w artykule.

 

 

6.          Bibliografia

 

1.     XML Encryption Syntax and Processing

2.     Exclusive XML Canonicalization Version 1.0

3.     XML Signature Syntax and Processing

4.     Canonical XML Version 1.0

5.     RFC 3280 : Internet X.509 Public Key Infrastructure Certificate and CRL Profile  

6.     M. Atreya, Introduction to the PKCS standards

7.     M. Atreya , Digital Signatures & Digital Envelopes

8.     PKCS #7: Cryptographic Message Syntax Standard

9.     MSDN: System.Security.Cryptography.Xml Namespace

10. MSDN: System.Security.Cryptography.Pkcs Namespace

11. MSDN: System.Security.Cryptography Namespace

12. MSDN: System.Security.Cryptography.X509Certificates Namespace

13.  RFC 1943:  Building an X.500 Directory Service in the US

 

Załączniki:


Podobne artykuły

Komentarze 1 Masz uwagi do tej strony? Napisz

Dodaj komentarz

avatar

Zaloguj się lub Zarejestruj się aby wykonać tę czynność.

Autor CamlanEx
avatar
 

Załóż konto
CodeGuru to miejsce dla każdego programisty. Przez lata portal rozwijany był siłami społeczności i to właśnie społeczność programistów jest tutaj najważniejsza. CG od wielu lat gromadzi wokół siebie coraz większą grupę pasjonatów. Warto być jej częścią!

Dowiedz się więcej o CodeGuru

Geek Club - Windows Phone

 

MetroOne

Idź na górę strony