Welcome to Weblog @ rebex.cz Sign in | Help

Temná zákoutí RSA implementace v .NETu?

Martin Vobr mě dnes ráno vyplašil odkazem na blog Michala Bláhy, kde prý upozorňuje na nějaký zásadní problém v implementaci RSA v .NETu. Po blížším zkoumání jsem však zjistil, že žádný problém ani chyba neexistuje a senzace se nekoná.

Michal upozorňuje na to, že třída RSACryptoServiceProvider nedokáže zašifrovat zprávu soukromým klíčem a dešifrovat ji veřejným klíčem. Ostuda? Ani ne... Toto není ani chyba, ani opomenutí, jak vysvětlím později. Tuto operace totiž neumí nejen CryptoAPI (RSA v .NETu je jen wrapper), ale ani žádná jiná implementace standardu PKCS #1 (základní standard pro RSA kryptografii) a Microsoft se v tomto případě opravdu nemá za co stydět - snad jen za to, že toto nebyl schopen sepsat místo mě. A že ani Honza Šeda nedokázal najít v MS nikoho, kdo by problém uvedl na pravou míru.

Klíčem k pochopení situace je drobný nesoulad mezi teorií a (nikoliv drsnou) praxí RSA, který se ale po blížším zkoumání ukáže být naprosto samozřejmý. Michal má samozřejmě pravdu v tom, že zprávu zašifrovanou pomocí privátního klíče P může dešifrovat kdokoliv s veřejným klíčem V, a naopak zprávu zašifrovanou pomocí veřejného klíče V může dešifrovat pouze a jenom vlastník privátního klíče P. Tolik teorie.

Ale zásadní otázka je - k čemu by nám zašifrování zprávy soukromým klíčem vlastně bylo? Veřejný klíč má k dispozici každý (je veřejný), proto každý by mohl naši zprávu dešifrovat. Existuje pouze jediný rozumný důvod k zašifrování zprávy soukromým klíčem, a to dokázat vlastnictví soukromého klíče. Jenže k tomuto účelu je přeci určen elektronický podpis! Který - jak jinak - spočívá právě v zašifrování zprávy (resp. jejího hashe) soukromým klíčem...

A praxe této úvaze odpovídá - zašifrování pomocí privátního klíče a dešifrování pomocí veřejného se opravdu používá vyhradně pro účely elektronického podpisu.

Zbytek tohoto příspěvku už není třeba číst, vše podstatné již bylo řečeno. Další řádky jen blíže přibližují praxi.

Základním dokumentem, který definuje co a jak, je PKCS #1 (jinak též znám jako např. RFC 3447), jehož autorem je RSA Laboratories. A ten definuje pouze čtyři operace:

  1. RSAEP - zašifrování - zašifrování zprávy veřejným klíče
  2. RSADP - dešifrování - dešifrování zprávy soukromým klíče
  3. RSASP1 - podpis - vytvoření podpisu zprávy (hashe) soukromým klíčem (odpovídá zašifrování soukromým klíčem)
  4. RSAVP1 - ověření - získání původní zprávy (hashe) z podpisu (odpovídá dešifrování veřejným klíčem)

Následně jsou zde definována různá schémata formátování vstupních dat pro RSAEP a pro RSASP1, pomocí kterých se data připraví do podoby vhodné k zašifrování - v žádném případě není možné zašifrovat nebo dešifrovat neformátovaná data, a to z bezpečnostních důvodů, které zde nemá smysl podrobněji rozebírat. Operace RSADP a RSAVP1 zas naopak po dešifrování tato formátovaná data převedou do jejich původní podoby. Při podpisu se například nezašifruje pouze hash zprávy, ale i ID algoritmu, pomocí kterého byl tento hash vytvořen.

Za povšimnutí také stojí, že když cpete třídě RSACryptoServiceProvider privátní klíč, tak jí cpete zárověň veřejný klíč, protože ten je součástí té stejné struktury.

Abych to shrnul - žádná implementace RSA dle PKCS #1 / RFC 3447 operaci "zašifrování dat soukromým klíčem" umět nemůže, protože standard nic takového nedefinuje. A to z pochopitelných důvodů uvedených výše.

A na závěr otázka pro Michala Bláhu, na kterou bych opravdu rád znal odpověď: K čemu vlastně potřebujete šifrovat data soukromým klíčem? :-)

Posted by lukasp | 7 Comments
Filed under:

Je volání webových služeb přes SOAP z .NETu spolehlivé?

Dlouho jsem nevěřil, že HttpWebRequest je tak moc problémový, jak se proslýchalo. Sice jsem s ním měl také problémy, ale nezavrhl jsem ho. To, že se při stahování autentikované stránky posílají dva požadavky, z nichž první je zamítnut, není chyba - je to v souladu s RFC 1945, kde se definuje autentikační mechanismus jako challenge-response. WebRequest sice má property PreAuthenticate, která má mít za následek odesílání autorizačního headeru už s prvním požadavkem, ale to platí až pro požadavky odeslané po úspěšné autentikaci následující po první 401ce. V instalovatelné verzi MSDN Library je to velmi přesně popsáno, zatímco v online verzi jsem nenašel ani zmínku.

Avšak další užitečná property KeepAlive se opravdu chová divně. Přestože je defaultně nastavena na true, týká se zřejmě jen neautentikovaných požadavků. Když jsem potřeboval přes WebDAV a NTLM autentikaci přistupovat k datům SharePointu, velice brzy jsem narazil - NTLM autentikace není nejrychlejší, zvlášť když se úplně zbytečně provádí stále dokola. A to opravdu není omezení NTLM ani IIS, o čemž je možné se přesvědčit za pomoci odchytávače paketů během procházení WebDAVu SharePointím pluginem do Exploreru - KeepAlive funguje, autentikace proběhne jen jednou, spojení drží a veškerá komunikace probíhá bez jakéhokoliv zdržování. Nakonec jsem svůj boj s HttpWebRequestem vzdal a pomalost obešel použítím vlastní cache... Bylo to ošklivé řešení, ale psát HTTP komponentu s podporou nedokumentovaného NTLM se mi nechtělo a ani na to nebyl čas.

Ale po zkušenostech Vaška Bárty a Michala Bláhy s používáním WebRequestu z více threadů se už budu od té na první pohled užitečné třídy držet raději dál - její jednoduchost ani zdaleka nevyvažuje všechny problémy. Jestli bude třída FtpWebRequest (paradoxní název, že?) z příští verze .NETu stejně spolehlivá, tak potěš pánbu.

Nepoužívat WebRequest bohužel není tak jednoduché. Jednou ze základních vymožeností .NET Frameworku jsou Web services a SOAP. Klientský stub vygenerovaný kompilátorem WSDL sestává ze třídy zděděné z SoapHttpClientProtocol. A tato třída je jen obálka právě okolo WebRequestu! Potřebujete volat webové služby z více threadů najednou? Možná to fungovat bude, ale pro jistotu přeji hodně štěstí a pevné nervy.

Posted by lukasp | 7 Comments

Jak nastavit čas ve Windows CE z VB.NET

Václav Bárta ve čtvrtek při programování pro .NET Compact Framework (což je .NET Framework pro PocketPC a jiná zařízení s Windows CE) narazil na velmi nepříjemný rozdíl mezi C# a VB.NET. Konkrétně se jedná o neschopnost VB.NET volat funkce importované z unmanaged dynamické knihovny, pokud jsou jejich parametry ukazatelé a my přikážeme Basicu místo nich předávat struktury - VB.NET to umí. Jenže v Compact Framework to prostě nefunguje a výsledkem je pád se výjimkou NotSupportedException. Velká části Windows API je proto nepřístupná, mimo jiné i funkce SetSystemTime z coredll.dll. Z C# se postižené funkce nejspíš díky ukazatelům a možnosti označit kód jako unsafe dají volat bez problémů.

Přestože VB.NET nemám rád (hlavně z iracionálních důvodů), odmítl jsem se smířit s nemožností nastavit z tohoto jazyka systémový čas. Půjde to i bez ukazatelů! Součástí namespacu System.Runtime.InteropServices je přece třída Marshal a ta poskytuje metodu StructureToPtr, která dokáže zkopírovat obsah instance objektu do bloku paměti alokovaného třeba z heapu (český výraz hromada mi nikdy nepřirostl k srdci) a odkazovaného typem IntPtr, a dokonce nechybí ani v .NET CF! Kdyby v tom nebyl jeden drobný háček - v .NET CF chybí ve třídě Marshal metody na alokování paměti - řešení by bylo na světě. Ale budiž. Když neumí pamět alokovat Marshal, můžeme použít funkce API LocalAlloc a LocalFree (ano, ty se dají úspěšně volat z VB.NET - žádný jejich parametr není ukazatel na strukturu). Zkusil jsem to tedy takto.

A výsledek? Funguje to! Pomocí metody TimeCf.SetSystemTime se dá nastavit systémový čas na PocketPC i z VB.NET. Toto řešení je vhodné i pro C#, protože odpadá nutnost označit kód jako unsafe.

Import System
Import System.Runtime.InteropServices

Public Class TimeCf
    Private Class SystemTime
        Public Year As Short
        Public Month As Short
        Public DayOfWeek As Short
        Public Day As Short
        Public Hour As Short
        Public Minute As Short
        Public Second As Short
        Public MilliSecond As Short
    End Class

    Private Declare Function SetSystemTime Lib "coredll.dll" _
        (ByVal st As IntPtr) As Integer

    Private Declare Function GetLastError Lib "coredll.dll" _
        () As Integer

    Private Declare Function LocalAlloc Lib "coredll.dll" _
        (ByVal Flags As Integer, ByVal Bytes As Integer) As IntPtr

    Private Declare Function LocalFree Lib "coredll.dll" _
        (ByVal Ptr As IntPtr) As IntPtr

    Public Shared Sub SetSystemTime(ByVal dt As DateTime)
        Dim systime As New SystemTime
        systime.Year = dt.Year
        systime.Month = dt.Month
        systime.DayOfWeek = 0
        systime.Day = dt.Day
        systime.Hour = dt.Hour
        systime.Minute = dt.Minute
        systime.Second = dt.Second
        systime.MilliSecond = dt.Millisecond

        Dim bytes As Integer = Marshal.SizeOf(systime)
        Dim ptr As IntPtr = LocalAlloc(0, bytes)
        Try
            Marshal.StructureToPtr(systime, ptr, False)
            If SetSystemTime(ptr) = 0 Then
                Dim e As Integer = GetLastError()
                Dim m As String
                m = String.Format("Cannot set system time (error {0}).", e)
                Throw New Exception(m)
            End If
        Finally
            LocalFree(ptr)
        End Try
    End Sub

End Class
Posted by lukasp | 14 Comments