Elastyczność kryptograficzna

Autor: Bryan Sullivan

W historii ludzie używali różnych form szyfrowania do ukrywania informacji przed swoimi przeciwnikami. Juliusz Cezar wykorzystywał szyfr przesuwający o 3 miejsca (litera A była konwertowana na D, B na E, itd.) do oznajmiania planów bitew. Podczas II Wojny Światowej marynarka niemiecka wykorzystywała znacznie bardziej zaawansowany system – maszynę Enigma – do szyfrowania komunikatów przesyłanych do swoich łodzi podwodnych. Obecnie używamy jeszcze bardziej skomplikowanych mechanizmów szyfrujących jako części infrastruktury kluczy publicznych, która pomaga nam dokonywać bezpiecznych transakcji w Internecie.

Ale tak długo, jak kryptografowie tworzyli tajne kody, kryptoanalitycy próbowali je łamać i wykradać informacje, a czasami się to udawało. Algorytmy kryptograficzne kiedyś uważane za bezpieczne zostały złamane i są uznawane obecnie za bezużyteczne. Czasami drobne wady są znajdowane w algorytmach, a czasami jest to tylko kwestia dostępu atakujących do większej mocy obliczeniowej w celu przeprowadzenia ataków brutalnej siły.

Ostatnio badacze zabezpieczeń pokazali słabości algorytmu tworzenia skrótów MD5 wynikające z kolizji to znaczy faktu, że dwa komunikaty mogą mieć tę samą wyliczoną wartość skrótu MD5. Stworzyli dowód możliwości przeprowadzenia ataku wykorzystującego tę słabość skierowanego na infrastruktury kluczy publicznych, które chronią transakcje handlowe w sieci Web. Wykupując specjalnie przygotowany certyfikat witryny Web w urzędzie certyfikacji (CA), który wykorzystuje MD5 do podpisywania swoich certyfikatów, badacze byli w stanie utworzyć fałszywy certyfikat tego urzędu certyfikacji, który z powodzeniem można by potencjalnie wykorzystać do podszywania się pod dowolną witrynę w Internecie. Doszli do wniosku, że algorytm MD5 nie jest odpowiedni do podpisywania certyfikatów cyfrowych i że powinna być używana silniejsza alternatywa taka, jak jeden z algorytmów SHA-2 (jeśli ktoś jest zainteresowany lepszym poznaniem tych badań, może przeczytać ich dokumentację).

Te odkrycia są oczywiście powodem do niepokoju, ale nie powinny stanowić wielkiej niespodzianki. Teoretyczne słabości MD5 były demonstrowane od lat, a wykorzystanie MD5 w produktach Microsoft zostało zabronione przez standardy kryptograficzne Microsoft SDL w 2005 roku. Inne niegdyś popularne algorytmy takie jak SHA-1 i RC2 zostały podobnie zakazane. Rysunek 1 pokazuje kompletną listę algorytmów kryptograficznych zabronionych lub zaakceptowanych przez SDL. Lista algorytmów zatwierdzonych przez SDL jest aktualna na moment pisania tego artykułu, ale lista ta jest przeglądana i aktualizowana corocznie jako część procesu aktualizacji SDL.

Rysunek 1: Algorytmy kryptograficzne zatwierdzone przez SDL.

Typ algorytmu Zabronione (algorytmy, które powinny być zastąpione w istniejącym kodzie lub mogą być używane tylko do odszyfrowywania) Dopuszczalne (algorytmy dopuszczalne w istniejącym kodzie, poza poufnymi danymi) Zalecane (algorytmy do stosowania w nowym kodzie)
Symetryczne blokowe DES, DESX, RC2, SKIPJACK 3DES (2 lub 3 klucze) AES (>=128 bit)
Symetryczne strumieniowe SEAL, CYLINK_MEK, RC4 (<128bit) RC4 (>= 128bit) Żadne, lepiej stosować szyfry blokowe
Asymetryczne RSA (<2048 bit), Diffie-Hellman (<2048 bit) RSA (>=2048bit ), Diffie-Hellman (>=2048bit) RSA (>=2048bit), Diffie-Hellman (>=2048bit), ECC (>=256bit)
Skrótowe (obejmuje wykorzystanie HMAC) SHA-0 (SHA), SHA-1, MD2, MD4, MD5 SHA-2 SHA-2 (obejmuje: SHA-256, SHA-384, SHA-512)
Długości kluczy HMAC <112bit >= 112bit >= 128bit

Nawet jeśli stosujemy te wszystkie standardy w swoim własnym kodzie używając tylko najbardziej bezpiecznych algorytmów i najdłuższych kluczy, to nie ma gwarancji, że kod pisany dzisiaj pozostanie bezpieczny w przyszłości. W istocie, jeśli historia ma być jakąś wskazówką, to prawdopodobnie nie pozostanie bezpieczny.

Planowanie pod kątem przyszłych luk

Możemy podejść do tego nieprzyjemnego scenariusza reaktywnie przeglądając bazy kodu naszych starych aplikacji, wskazując zastosowania algorytmów podatnych na złamanie, zastępując je nowymi algorytmami, przebudowując aplikacje, przepuszczając je przez testy, a następnie wydając łatki lub pakiety serwisowe dla użytkowników. To nie tylko ogrom pracy, ale też ryzyko dla użytkowników do czasu dostarczenia poprawek.

Lepszą alternatywą jest planowanie pod kątem tego scenariusza od samego początku. Zamiast sztywno kodować określone algorytmy kryptograficzne w naszym kodzie, należy korzystać z jednej z funkcji elastyczności kryptograficznej wbudowanych w Microsoft .NET Framework. Przyjrzyjmy się kilku fragmentom kodu C# zaczynając od najmniej elastycznego przykładu:

private static byte[] computeHash(byte[] buffer)
{
   using (MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider())
   {
      return md5.ComputeHash(buffer);
   }
}

Ten kod jest całkiem nieelastyczny. Jest powiązany z określonym algorytmem (MD5), a także konkretną implementacją tego algorytmu, klasą MD5CryptoServiceProvider. Zmodyfikowanie tej aplikacji w celu wykorzystania bezpiecznego algorytmu funkcji skrótu wymagałoby zmiany kodu i wydania poprawki. Oto nieco lepszy przykład:

private static byte[] computeHash(byte[] buffer)
{
   string md5Impl = ConfigurationManager.AppSettings["md5Impl"];
   if (md5Impl == null)
      md5Impl = String.Empty;

   using (MD5 md5 = MD5.Create(md5Impl))
   {
      return md5.ComputeHash(buffer);
   }
}

Ta funkcja wykorzystuje klasę System.Configuration.ConfigurationManager w celu pobrania niestandardowych ustawień aplikacji (ustawienia „md5Impl”) z pliku konfiguracyjnego aplikacji. W tym przypadku ustawienie to służy do przechowywania silnej nazwy klasy implementującej algorytm, z którego chcemy skorzystać. Ten kod przekazuje pobraną wartość tego ustawienia do funkcji statycznej MD5.Create w celu utworzenia wystąpienia żądanej klasy (System.Security.Cryptography.MD5 jest abstrakcyjną klasą bazową, z której wywodzić się muszą wszystkie implementacje algorytmu MD5). Na przykład, jeśli ustawienie md5Impl byłoby ustawione na łańcuch znaków "System.Security.Cryptography.MD5Cng, System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", to funkcja MD5.Create utworzyłaby wystąpienie klasy MD5Cng.

To podejście rozwiązuje jedynie połowę naszego problemu kryptoelastycznościowego, więc w istocie wcale nie stanowi rozwiązania. Możemy teraz określić implementację algorytmu MD5 bez konieczności zmiany kodu źródłowego, co może okazać się przydatne, jeśli jakaś usterka zostanie wykryta w określonej implementacji, na przykład w MD5Cng, ale nadal jesteśmy związani z wykorzystaniem MD5. Aby rozwiązać ten problem, musimy przenieść się na kolejny poziom abstrakcji:

private static byte[] computeHash(byte[] buffer)
{
   using (HashAlgorithm hash = HashAlgorithm.Create("MD5"))
   {
      return hash.ComputeHash(buffer);
   }
}

Na pierwszy rzut oka ten fragment kodu nie wygląda na znacząco różny od pierwszego przykładu. Wygląda jakbyśmy jeszcze raz zakodowali na sztywno algorytm MD5 w tej aplikacji poprzez wywołanie HashAlgorithm.Create("MD5"). Zaskakujące jednak, że ten kod jest znacząco bardziej elastyczny kryptograficznie niż oba pozostałe przykłady. Podczas gdy domyślnym zachowaniem wywołania metody HashAlgorithm.Create("MD5") – w .NET 3.5 – jest utworzenie wystąpienia klasy MD5CryptoServiceProvider, to zachowanie w czasie wykonywania można dostosować dokonując zmiany w pliku machine.config.

Zmieńmy zachowanie tego kodu, aby utworzyć wystąpienie algorytmu SHA512 zamiast MD5. W tym celu musimy dodać dwa elementy do pliku machine.config: element <cryptoClass> w celu zamapowania przyjaznej nazwy algorytmu do klasy implementującej algorytm, którego chcemy użyć; oraz element <nameEntry> w celu zamapowania przyjaznej nazwy starego algorytmu na nową nazwę przyjazną.

<configuration>
  <mscorlib>
    <cryptographySettings>
      <cryptoNameMapping>
        <cryptoClasses>
          <cryptoClass MyPreferredHash="SHA512CryptoServiceProvider, System.Core, Version=3.5.0.0, 
            Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
        </cryptoClasses>
        <nameEntry name="MD5" class="MyPreferredHash"/>
      </cryptoNameMapping>
    </cryptographySettings>
  </mscorlib>
</configuration>

Teraz gdy nasz kod wywoła HashAlgorithm.Create("MD5"), to CLR zajrzy do pliku machine.config i zobaczy, że łańcuch znaków "MD5" powinien zostać zamapowany na przyjazną nazwę algorytmu "MyPreferredHash". Następnie zobaczy, że nazwa "MyPreferredHash" jest zamapowana do klasy SHA512CryptoServiceProvider (zdefiniowanej w podzespole System.Core, z określoną wersją, kulturą i tokenem klucza publicznego) oraz utworzy wystąpienie tej klasy.

Ważne jest, aby zauważyć, że przemapowywanie odbywa się nie w czasie kompilacji, ale w czasie działania aplikacji: To plik machine.config użytkownika kontroluje to przemapowywanie, a nie programista. W efekcie ta technika rozwiązuje nasz dylemat związany z przypisaniem do określonego algorytmu, który mógłby zostać złamany w pewnym momencie w przyszłości. Poprzez unikanie sztywnego kodowania algorytmu kryptograficznego w aplikacji i zakodowanie w zamian tylko abstrakcyjnego typu algorytmu kryptograficznego, HashAlgorithm, tworzymy aplikację, w której użytkownik końcowy (a konkretniej ktoś z prawami administracyjnymi do edycji pliku machine.config na komputerze, gdzie aplikacja jest zainstalowana) może określić dokładnie, który algorytm i implementacja zostaną wykorzystane przez aplikację. Administrator mógłby wybrać zastąpienie algorytmu, który został niedawno złamany, takim, który nadal jest uważany za bezpieczny (na przykład zastąpić MD5 przez SHA-256) lub aby proaktywnie zastąpić bezpieczny algorytm alternatywą z dłuższym kluczem (zastąpić SHA-256 przez SHA-512).

Potencjalne problemy

Modyfikowanie pliku machine.config w celu zmiany mapowania domyślnych łańcuchów znaków dla typów algorytmów (takich jak "MD5" i "SHA1") może rozwiązać problemy z elastycznością kryptograficzną, ale może jednocześnie stworzyć problemy z kompatybilnością. Dokonanie zmian w machine.config wpływa na każdą aplikację .NET na danym komputerze. Inne aplikacje zainstalowane na komputerze mogą opierać się konkretnie na MD5 i zmiana algorytmów wykorzystywanych przez te aplikacje mogłaby złamać je w nieoczekiwany sposób, trudny do zdiagnozowania. Jako alternatywa do wymuszania zmian na całej maszynie, lepiej korzystać z niestandardowych, specyficznych dla aplikacji, przyjaznych nazw w swoim kodzie i mapować te nazwy na preferowane klasy w machine.config. Na przykład możemy zmienić "MD5" na "MyApplicationHash" w naszym przykładzie:

private static byte[] computeHash(byte[] buffer)
{
   using (HashAlgorithm hash = HashAlgorithm.Create("MyApplicationHash"))
   {
      return hash.ComputeHash(buffer);
   }
}

Następnie dodajemy wpis do pliku machine.coinfig, aby zamapować "MyApplicationHash" na klasę "MyPreferredHash":

<cryptoClasses>
   <cryptoClass MyPreferredHash="SHA512CryptoServiceProvider, 
     System.Core, Version=3.5.0.0, Culture=neutral, 
     PublicKeyToken=b77a5c561934e089"/>
</cryptoClasses>
<nameEntry name="MyApplicationHash" class="MyPreferredHash"/>

Możemy też mapować wiele przyjaznych nazw do tej samej klasy; na przykład moglibyśmy mieć jedną przyjazną nazwę dla każdej z naszych aplikacji i w ten sposób zmieniać zachowanie określonych aplikacji bez wpływania na każdą inną aplikację na danym komputerze.

<cryptoClasses>
   <cryptoClass MyPreferredHash="SHA512CryptoServiceProvider, 
     System.Core, Version=3.5.0.0, Culture=neutral, 
     PublicKeyToken=b77a5c561934e089"/>
</cryptoClasses>
<nameEntry name="MyApplicationHash" class="MyPreferredHash"/>
<nameEntry name="MyApplication2Hash" class="MyPreferredHash"/>
<nameEntry name="MyApplication3Hash" class="MyPreferredHash"/>

Jednakże nadal nie uporaliśmy się z kwestią problemów z kompatybilnością w naszej aplikacji. Musimy planować naprzód w związku z rozmiarem pamięci zarówno dla zmiennych lokalnych (pamięć przejściowa), jak i dla bazy danych i schematów XML (pamięć trwała). Na przykład skróty MD5 mają zawsze długość 128 bitów. Jeśli zaplanujemy dokładnie 128 bitów w naszym kodzie lub schemacie na przechowywanie wynikowych skrótów, to nie będziemy w stanie dokonać uaktualnienia do SHA-256 (dane wyjściowe długości 256 bitów) lub SHA-512 (dane wyjściowe długości 512 bitów).

W związku z tym nasuwa się pytanie, ile pamięci wystarczy. Czy 512 bitów wystarczy, czy należy użyć 1024, 2048, czy więcej? Nie mogę podać tutaj sztywnej reguły, ponieważ każda aplikacja ma inne wymagania, ale jako regułę ogólną zalecam, aby zakładać dwa razy więcej miejsca na skróty, niż obecnie wykorzystywane ich długości. Dla danych szyfrowanych symetrycznie i asymetrycznie można rezerwować dodatkowe 10 procent miejsca. Jest mało prawdopodobne, aby szeroko przyjęły się nowe algorytmy dające wyniki o rozmiarach znacznie większych niż istniejące algorytmy.

Jednakże aplikacje, które przechowują wartości skrótów lub szyfrowane dane w stanie trwałym (na przykład w bazie danych lub pliku) mają większe problemy niż rezerwowanie wystarczającej ilości miejsca. Jeśli zachowamy dane przy użyciu jednego algorytmu, a później będziemy próbować działać na tych danych przy użyciu innego algorytmu, to nie dostaniemy oczekiwanych wyników. Na przykład dobrze jest przechowywać skróty haseł zamiast ich pełnych wersji otwartym tekstem. Gdy użytkownik próbuje się zalogować, kod może porównać skrót hasła dostarczonego przez użytkownika ze skrótem przechowywanym w bazie danych. Jeśli skróty te do siebie pasują, użytkownik zostaje uwierzytelniony. Jednakże jeśli skrót jest przechowywany w jednym formacie (powiedzmy MD5), a aplikacja zostanie uaktualniona poprzez zastosowanie innego algorytmu (powiedzmy SHA-256), to użytkownicy nie będą mogli się logować, ponieważ wartości skrótów SHA-256 dla haseł będą zawsze różne od wartości skrótów MD5 tych samych haseł.

Można obejść ten problem w niektórych przypadkach zachowując pierwotny algorytm jako metadane razem z faktycznymi danymi. Następnie podczas działania na zachowanych danych, należy skorzystać z refleksji do utworzenia wystąpienia algorytmu użytego pierwotnie zamiast aktualnego algorytmu:

private static bool checkPassword(string password, byte[] storedHash,
   string storedHashAlgorithm)
{
   using (HashAlgorithm hash = HashAlgorithm.Create(storedHashAlgorithm))
   {
      byte[] newHash = 
         hash.ComputeHash(System.Text.Encoding.Default.GetBytes(password));
      if (newHash.Length != storedHash.Length)
         return false;
      for (int i = 0; i < newHash.Length; i++)
         if (newHash[i] != storedHash[i])
            return false;
      return true;
   }
}

Niestety, jeśli kiedykolwiek będziemy musieli porównać dwa zachowane skróty, to muszą one być utworzone przy użyciu tego samego algorytmu. Nie ma po prostu sposobu, aby porównać skrót MD5 ze skrótem SHA-256 i określić, czy były one oba utworzone na podstawie tych samych danych pierwotnych. Nie ma dobrego rozwiązania krypto-elastycznego tego problemu, a najlepszą radą, jaką mogę zaoferować jest to, że należy wybrać najbezpieczniejszy algorytm obecnie dostępny, a następnie opracować plan uaktualnienia w przypadku, gdyby ten algorytm został później złamany. Ogólnie elastyczność kryptograficzna zdaje się działać dużo lepiej dla danych przejściowych niż dla danych trwałych.

Alternatywne użycie i składnia

Zakładając, że projekt naszej aplikacji pozwala korzystać z elastyczności kryptograficznej, przyjrzyjmy się dalej kilku alternatywnym zastosowaniom i składniom tej techniki. Jak dotąd w tym artykule skupialiśmy się niemal całkowicie na kryptograficznych algorytmach skrótów, ale elastyczność kryptograficzna działa również dla innych typów algorytmów kryptograficznych. Wystarczy wywołać statyczną metodę Create dla odpowiedniej abstrakcyjnej klasy bazowej: SymmetricAlgorithm dla algorytmów kryptografii symetrycznej (z tajnym kluczem) takich jak AES; AsymmetricAlgorithm dla algorytmów kryptografii asymetrycznej (z kluczem publicznym) takich jak RSA; KeyedHashAlgorithm dla funkcji skrótu z kluczem; oraz HMAC dla opartych na skrótach kodach uwierzytelniania komunikatów.

Można również wykorzystywać elastyczność kryptograficzną w celu zastąpienia jednej ze standardowych klas algorytmów kryptograficznych .NET przez niestandardową klasę algorytmu, na przykład przez jeden z algorytmów opracowanych przez zespół zabezpieczeń CLR i opublikowanych w witrynie CodePlex. Jednakże odradza się pisanie własnych, niestandardowych bibliotek kryptograficznych. Wykonany domowym sposobem algorytm składający się z szyfru ROT13, a następnie bitowego przesunięcia w lewo i operacji XOR z ciągiem znaków składającym się na imię kota może wydawać się bezpieczny, ale nie będzie stanowić większego wyzwania dla specjalisty od łamania szyfrów. Nie będąc ekspertem w dziedzinie kryptografii lepiej pozostawić projektowanie algorytmów profesjonalistom.

Należy też oprzeć się pokusie tworzenia własnych algorytmów albo przywracania do życia dziwnych i dawno nieużywanych, jak szyfr Vigenère, nawet w sytuacjach, gdzie nie potrzeba nam silnej kryptograficznie ochrony. Problem nie tyle polega na tym, co zrobimy z naszym szyfrem, ale co zrobią z nim programiści, którzy nadejdą po nas. Nowy programista, który znajdzie naszą klasę niestandardowego algorytmu w kodzie po latach, może uznać, że tego właśnie potrzebuje dla nowej logiki generowania kluczy aktywujących produkt.

Jak dotąd poznaliśmy jedną ze składni implementowania kodu elastycznego kryptograficznie, AlgorithmType.Create(algorithmName), ale w .NET Framework wbudowane są też dwa inne podejścia. Pierwszym jest wykorzystanie klasy System.Security.Cryptography.CryptoConfig:

private static byte[] computeHash(byte[] buffer)
{
   using (HashAlgorithm hash = (HashAlgorithm)CryptoConfig.CreateFromName("MyApplicationHash"))
   {
      return hash.ComputeHash(buffer);
   }
}

Ten kod wykonuje te same operacje, co nasz poprzedni przykład wykorzystujący HashAlgorithm.Create("MyApplicationHash"): infrastruktura CLR szuka w pliku machine.config mapowania dla łańcucha znaków "MyApplicationHash" i wykorzystuje zamapowaną klasę algorytmu, jeśli ją znajdzie. Trzeba zwrócić uwagę, że musimy rzutować wynik metody CryptoConfig.CreateFromName, ponieważ zwraca ona typ System.Object i może być używana do tworzenia typów SymmetricAlgorithm, AsymmetricAlgorithm lub dowolnego innego rodzaju obiektów.

Drugą składnią alternatywną jest wywołanie statycznej metody Create z naszego pierwotnego przykładu, ale bez parametrów, jak poniżej:

private static byte[] computeHash(byte[] buffer)
{
   using (HashAlgorithm hash = HashAlgorithm.Create())
   {
      return hash.ComputeHash(buffer);
   }
}

W tym kodzie po prostu prosimy platformę .NET o zapewnienie wystąpienia domyślnej implementacji algorytmu funkcji skrótu. Listę domyślnych algorytmów dla każdej abstrakcyjnej klasy bazowej z System.Security.Cryptography (według stanu dla wersji .NET 3.5) można znaleźć na Rysunku 2.

Rysunek 2: Domyślne algorytmy i implementacje w .NET Framework 3.5.

Abstrakcyjna klasa bazowa Algorytm domyślny Implementacja domyślna
HashAlgorithm SHA-1 SHA1CryptoServiceProvider
SymmetricAlgorithm AES (Rijndael) RijndaelManaged
AsymmetricAlgorithm RSA RSACryptoServiceProvider
KeyedHashAlgorithm SHA-1 HMACSHA1
HMAC SHA-1 HMACSHA1

Dla HashAlgorithm widać, że algorytmem domyślnym jest SHA-1, a klasą domyślnej implementacji jest SHA1CryptoServiceProvider. Jednakże wiemy, że SHA-1 został zakazany przez standardy kryptograficzne SDL. Na razie zignorujmy fakt, że potencjalne problemy z kompatybilnością sprawiają, że nierozsądne może być przemapowywanie wbudowanych nazw algorytmów jak „SHA1” i zmienianie pliku machine.config w celu powiązania "SHA1" z SHA512CryptoServiceProvider:

<cryptoClasses>
   <cryptoClass MyPreferredHash="SHA512CryptoServiceProvider, System.Core, Version=3.5.0.0, Culture=neutral, 
     PublicKeyToken=b77a5c561934e089"/>
</cryptoClasses>
<nameEntry name="SHA1" class="MyPreferredHash"/>

Teraz wstawmy wiersz związany z debugowaniem do funkcji computeHash, aby potwierdzić, że algorytm został poprawnie przemapowany, a następnie uruchommy aplikację:

private static byte[] computeHash(byte[] buffer)
{
   using (HashAlgorithm hash = HashAlgorithm.Create())
   {
      Debug.WriteLine(hash.GetType());
      return hash.ComputeHash(buffer);
   }
}

Dane wyjściowe przy debugowaniu tej metody wyglądają następująco:

System.Security.Cryptography.SHA1CryptoServiceProvider

Co się stało? Czy nie przemapowaliśmy SHA1 na SHA-512? W istocie nie. Przemapowaliśmy jedynie łańcuch tekstowy "SHA1" na klasę SHA512CryptoServiceProvider, ale nie przekazaliśmy łańcucha "SHA1" jako parametru w wywołaniu HashAlgorithm.Create.

Chociaż wydaje się, że Create nie ma parametrów tekstowych do przemapowania, to nadal możliwa jest zmiana typu tworzonego obiektu. Możemy to zrobić, ponieważ HashAlgorithm.Create() stanowi po prostu skrótową składnię dla HashAlgorithm.Create("System.Security.Cryptography.HashAlgorithm"). Dodajmy teraz jeszcze jeden wiersz do pliku machine.config, aby przemapować "System.Security.Cryptography.HashAlgorithm" na SHA512CryptoServiceProvider, a następnie ponownie uruchommy aplikację:

<cryptoClasses>

<cryptoClass MyPreferredHash="SHA512CryptoServiceProvider, System.Core, Version=3.5.0.0,

Culture=neutral, PublicKeyToken=b77a5c561934e089"/>

</cryptoClasses>

<nameEntry name="SHA1" class="MyPreferredHash"/>

<nameEntry name="System.Security.Cryptography.HashAlgorithm" class="MyPreferredHash"/>

<cryptoClasses>
   <cryptoClass MyPreferredHash="SHA512CryptoServiceProvider, System.Core, Version=3.5.0.0, 
     Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
</cryptoClasses>
<nameEntry name="SHA1" class="MyPreferredHash"/>
<nameEntry name="System.Security.Cryptography.HashAlgorithm" class="MyPreferredHash"/>

Dane wyjściowe podczas debugowania computeHash są teraz dokładnie takie, jakich oczekujemy:

System.Security.Cryptography.SHA512CryptoServiceProvider

Jednakże trzeba pamiętać, że przemapowywanie klas w ten sposób może tworzyć nieoczekiwane i trudne do debugowania problemy z kompatybilnością. Lepiej jest korzystać z przyjaznych nazw specyficznych dla danej aplikacji, które można przemapowywać z mniejszym ryzykiem spowodowania problemów.

Inna korzyść z elastyczności kryptograficznej

Oprócz umożliwienia nam zastępowania złamanych algorytmów w locie bez konieczności ponownego kompilowania, elastyczność kryptograficzną można wykorzystywać do zwiększania wydajności. Przyglądając się przestrzeni nazw System.Security.Cryptography można zauważyć, że często istnieje kilka różnych klas implementacyjnych dla danego algorytmu. Na przykład istnieją trzy różne implementacje SHA-512: SHA512Cng, SHA512CryptoServiceProvider i SHA512Managed.

Spośród tych klas, SHA512Cng zwykle oferuje najlepszą wydajność. Szybki test na moim laptopie (działającym pod kontrolą systemu Windows 7 release candidate) pokazuje, że klasy –Cng są w ogólności o około 10 procent szybsze niż klasy -CryptoServiceProvider i klasy -Managed. Moi koledzy z grupy Core Architecture poinformowali mnie, że w pewnych okolicznościach klasy –Cng mogą nawet działać 10 razy szybciej niż pozostałe!

Jasne więc, że lepiej korzystać z klas –Cng i moglibyśmy skonfigurować nasz plik machine.config tak, aby przemapować implementacje algorytmów na korzystanie z tych klas, ale klasy -Cng nie są dostępne w każdym systemie operacyjnym. Jedynie systemy Windows Vista, Windows Server 2008 i Windows 7 (oraz pewnie wersje późniejsze) obsługują –Cng. Próba utworzenia wystąpienia klasy –Cng w innym systemie operacyjnym wzbudzi wyjątek.

Podobnie rodzina –Managed klas kryptograficznych (AesManaged, RijndaelManaged, SHA256Managed, itd.) nie zawsze jest dostępna, ale z zupełnie innego powodu. Norma Federal Information Processing Standard 140 (FIPS) określa standardy dla algorytmów kryptograficznych i ich implementacji. Na dzień pisania tego artykułu klasy implementujące –Cng i –CryptoServiceProvider są certyfikowane przez FIPS, ale klasy –Managed nie. Co więcej możemy skonfigurować ustawienie zasad grupy, które pozwala na korzystanie jedynie z algorytmów zgodnych z FIPS. Niektóre agencje rządowe w USA i Kanadzie wymagają włączenia tego ustawienia zasad. Aby sprawdzić swój komputer, należy otworzyć okno edytora lokalnych zasad grupy – Local Group Policy Editor (gpedit.msc), przejść do węzła Computer Configuration (Konfiguracja komputera) / Windows Settings (Ustawienia systemu Windows) / Security Settings (Ustawienia zabezpieczeń) / Local Policies (Zasady lokalne) / Security Options (Opcje zabezpieczeń) i sprawdzić wartość ustawienia „System Cryptography: Use FIPS compliant algorithms for encryption, hashing, and signing (Kryptografia systemu: użyj zgodnych algorytmów FIPS dla celów szyfrowania, tworzenia skrótu i podpisywania)”. Jeśli ta zasada jest ustawiona jako Enabled (Włączona), to próba utworzenia wystąpienia klasy –Managed na tym komputerze wzbudzi wyjątek.

To sprawia, że jedynie rodzina –CryptoServiceProvider na pewno będzie działać na wszystkich platformach, ale z reguły te klasy mają najgorszą wydajność. Można przezwyciężyć ten problem implementując jedną z trzech składni elastyczności kryptograficznej omówionych wcześniej w tym artykule i dostosowując plik machine.config na komputerach docelowych w oparciu o ich systemy operacyjne i ustawienia. W przypadku komputerów działających pod kontrolą systemu Windows Vista lub nowszego możemy przemapować plik machine.config na korzystanie z implementacji klas –Cng:

<cryptoClasses>
   <cryptoClass MyPreferredHash="SHA512Cng, System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
</cryptoClasses>
<nameEntry name="MyApplicationHash" class="MyPreferredHash"/>

Dla komputerów działających pod kontrolą systemów wcześniejszych niż Windows Vista z wyłączoną zgodnością z FIPS, możemy przemapować plik machine.config na korzystanie z klas –Managed:

<cryptoClasses>
   <cryptoClass MyPreferredHash="SHA512Managed"/>
</cryptoClasses>
<nameEntry name="MyApplicationHash" class="MyPreferredHash"/>

Dla wszystkich pozostałych komputerów dokonujemy przemapowania na klasy –CryptoServiceProvider:

<cryptoClasses>
   <cryptoClass MyPreferredHash="SHA512CryptoServiceProvider, System.Core, Version=3.5.0.0, 
     Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
</cryptoClasses>
<nameEntry name="MyApplicationHash" class="MyPreferredHash"/>

Każde wywołanie HashAlgorithm.Create("MyApplicationHash") utworzy teraz najbardziej wydajną implementację klasy dostępną dla danego komputera. Co więcej, ponieważ algorytmy są identyczne, nie musimy martwić się o problemy z kompatybilnością lub współpracą. Skrót utworzony dla danej wartości wejściowej na jednym komputerze będzie taki sam, jak skrót utworzony dla tych samych danych wejściowych na innym komputerze nawet, jeśli klasy implementujące będą inne. Jest to spełnione również dla innych typów algorytmów: można zaszyfrować dane na jednym komputerze przy użyciu AesManaged i odszyfrować je z powodzeniem na innym komputerze przy użyciu AesCryptoServiceProvider.

Podsumowanie

Biorąc pod uwagę czas i koszt przekodowania naszej aplikacji w odpowiedzi na złamany algorytm kryptograficzny, nie wspominając o narażaniu użytkowników na niebezpieczeństwo do czasu wdrożenia nowej wersji, dobrze jest zaplanować taką sytuację i napisać aplikację w sposób elastyczny kryptograficznie. Fakt, że możemy również uzyskać lepszą wydajność dzięki kodowaniu w ten sposób jest dodatkowym bonusem.

Nigdy nie należy sztywno kodować określonych algorytmów lub implementacji tych algorytmów w aplikacji. Zawsze należy deklarować algorytmy kryptograficzne jako jedną z następujących abstrakcyjnych klas typów algorytmów: HashAlgorithm, SymmetricAlgorithm, AsymmetricAlgorithm, KeyedHashAlgorithm lub HMAC.

Uważam, że reguła FxCop, która sprawdzałaby elastyczność kryptograficzną kodu byłaby niezwykle przydatna. Jeśli ktoś napisze taką regułę i opublikuje ją w witrynie Codeplex lub innym publicznym repozytorium kodu, z chęcią o tym napiszę w blogu SDL.

Na koniec chciałbym podziękować Shawnowi Hernanowi z zespołu zabezpieczeń SQL Server i Shawnowi Farkasowi z zespołu zabezpieczeń CLR za ich eksperckie uwagi i pomoc w stworzeniu tego artykułu.

Swoje pytania i uwagi można przesyłać na adres briefs@microsoft.com.

Bryan Sullivan jest menedżerem programowym zabezpieczeń w zespole Microsoft Security Development Lifecycle specjalizującym się w problemach zabezpieczeń aplikacji WWW. Jest autorem książki Ajax Security (Addison-Wesley, 2007).