e-Szignó .NET-ből? Majdnem egyszerű!

Úgy adódott, hogy e-Szignó smartkártyával kell digitális aláírást készítenem .NET-ből. Ez nem lehet probléma, hiszen remek osztályok állnak rendelkezésre a cél eléréséhez a System.Security.Cryptography névtér alatt.

Az elmélet a következő:

A digitális aláíró kártyák képesek tárolni aláíró kulcsokat, és a kulcsokhoz készített tanúsítványokat. A Windows viszont azokat a tanúsítványokat kezeli jól, amelyek a Windows tanúsítványtárban vannak. Ezeket a tanúsítványokat meg lehet nézni például a CertMgr.exe programmal.

Azért, hogy a smartkártyák kényelmesen használhatóak legyenek Windows alól, bizonyos smartkártyához egy kis programot készítenek, amelyet telepítve az folyamatosan figyeli a smartkártya olvasóba helyezett kártyákat, és arról a tanúsítványokat a Windows tanúsítványtárába másolják. Ezenkívül feljegyzik a Windows számára, hogy a tanúsítványhoz a felhasználó rendelkezik privát kulccsal, és hogy a privát kulcsot hol találja meg a Windows.

Az e-Szignó aláíró kártyához van ilyen program. Ennek köszönhetően kényelmesen lehet küldeni digitálisan aláírt leveleket az Outlookkal, és kényelmesen lehet használni a kártyát .NET-ből. Elméletileg.

Egy próbaalkalmazás pár sorból elkészíthető. A feladat két részből áll. Először meg kell találni a tanúsítványtárban azt a tanúsítványt, ami a smartkártyáról származik. Ezután használni kell a tanúsítványhoz tartozó privát kulcsot. Igazság szerint a privát kulcsot a tanúsítvány nélkül is lehetne használni, de ehhez elég intim tudásra lenne szükség az adott smartkártyára vonatkozólag.

A tanúsítványt úgy a legegyszerűbb megszerezni, hogy a felhasználóra bízzuk a kiválasztását. Ha tudjuk, hogy milyen tanúsítványt keressünk, ki lehet hagyni a felhasználót, most a teszt kedvéért egyszerűbb megkérni, hogy válasszon. Az ehhez szükséges kód a következő:

// A tanúsítványtárat képviselő objektum.
// A tanúsítványtárnak több "bugyra" van,
// a személyes használatra való tanúsítványok
// a "Current User" / "My" részben vannak.
X509Store store =
  new X509Store(
        StoreName.My,
        StoreLocation.CurrentUser);

// A tanúsítványtár megnyitása. Csak olvasásra
// kell, és csak akkor, ha már létezik.
store.Open(
    OpenFlags.ReadOnly
  | OpenFlags.OpenExistingOnly);

// A tanúsítvány kiválasztását lehetővé tevő
// ablak megjelenítése a felhasználónak. Csak
// egy tanúsítvány kiválasztása lehetséges a
// felületen.
X509Certificate2Collection collection =
  X509Certificate2UI.SelectFromCollection(
    store.Certificates,
    "Valami ablak cím",
    "Valami üzenet",
    X509SelectionFlag.SingleSelection);

A kód lefutása után, ha volt megjeleníthető tanúsítvány, és abból a felhasználó választott, akkor a collection[0] objektum az X509Certificate2 típusnak egy példánya. Az X509Certificate2 típusnak van egy PrivateKey property-je, ami pedig egy AsymmetricAlgorithm típusból származó példányt ad vissza. Pont erre a példányra van szükség a digitális aláírás elkészítéséhez. A PrivateKey property elnevezés egyébként elég szerencsétlen, valójában nem kapjuk meg a privát kulcsot, csak egy algoritmust megvalósító objektumot, ami a privát kulcsot használja. Magának az AsymmetricAlgorithm típusnak nincs olyan metódusa, amelyik elvégzi a digitális aláírást, csak a leszármazott típusoknak. Sajnos az osztályhierarchiát nem sikerült túl jól eltalálnia a tervezőknek, nincsenek szép interfészek, hanem tudnunk kell, hogy milyen típusokkal lehet dolgunk. Például most tudjuk, hogy ha a tanúsítvány egy RSA kulcshoz szól, akkor a PrivateKey property egy RSACryptoServiceProvider típusú példányt ad vissza. Ez az a helyzet, amikor az elvileg primitívebb C jellegű Windows CryptoAPI segítségével általánosabban használható kódot írhatnánk. Most azonban ki kell használni a speciális tudásunkat. Éles helyzetben a megfelelő típusokra ellenőrizni kellene, ez a példaprogramból kimarad:

// A PrivateKey property vagy RSACryptoServiceProvider-t
// vagy  DSACryptoServiceProvider-t ad vissza. Az
// e-Szignó kártya RSA alapú.
RSACryptoServiceProvider rsaSigner =
  collection[0].PrivateKey as RSACryptoServiceProvider;

// Az RSACryptoServiceProvider már elvégzi
// a digitális aláírást. Az aláírás alatti
// hash algoritmus egy SHA1-lesz.
rsaSigner.SignData(
  Encoding.UTF8.GetBytes("Helló világ!"),
  new SHA1CryptoServiceProvider());

Ez a pár sor elég ahhoz, hogy a smartkártya segítségével digitálisan aláírjuk a „Helló világ!” szöveget. Eltekintve attól a csúnyaságtól, hogy tudnunk kell az algoritmusok típusát és az azt megvalósító osztályokat (mint RSACryptoServiceProvider), elég egyszerű a dolgunk. Próbáljuk hát ki!

A tapasztalat az, hogy a fenti kód nem működik. Kellene neki, de nem. A hibaüzenet a következő: “The keyset is not defined.”. Némi magyarázat kell ahhoz, hogy ez az üzenet miért vicces a maga módján:

A Windows a kriptografikus műveleteket úgynevezett kriptografikus szolgáltatókon (Cryptographic Service Provider, CSP) keresztül végzi. Ez azért van így, mert nagyban változhat, hogy a kriptografikus műveletekkel szemben milyen követelmények vannak. Változhatnak az algoritmusok, változhatnak az implementációk. Van, amikor a szoftveres számítás elég, van, amikor hardveres számítás kell, például pont az e-Szignó esetében. Ahhoz, hogy a Windows használni tudja az e-Szignó smartkártyát, egy CSP közreműködése szükséges. Az e-Szignó CSP-je tudja, hogyan kell elvégeztetni egy digitális aláírást a smartkártyával, a Windows pedig csak annyit tud, hogy általában a CSP-kkel hogyan kell elvégeztetni egy digitális aláírást.

A Windows az aszimmetrikus kriptográfiai algoritmusokkal kapcsolatban, mint amilyen az RSA, a következőket várja el a CSP-től:
A CSP úgynevezett kulcs konténereket (key containers) tart karban. Minden alkalmazás készíthet magának kulcs konténert, ahol a kulcsait tárolja. A konténerek névvel vannak azonosítva. Ha van például egy levelező program, amit MailBuzzzer-nek hívnak, és a szoftver gyártója BigEndian Ltd, akkor a kulcs konténer neve lehet például „BigEndian – MailBuzzzer”. Más szoftverek egy guid-ot, vagy más véletlen értéket generálnak névnek, hogy a névütközést elkerüljék.

Minden kulcs konténer két pár aszimmetrikus kulcsot tud tárolni, amennyiben a szóban forgó CSP támogatja az aszimmetrikus műveleteket. Az egyiket aláíró, a másikat kulcscsere kulcsnak nevezi. Ez a felosztás az RSA alapú algoritmusok miatt alakult így, ahol maga az RSA algoritmus két módon használható, és az egyiket digitalis aláírásra, a másikat adat titkosításra lehet használni. Természetesen nem kötelező, hogy mind az aláíró kulcs mind a kulcscsere kulcs létre legyen hozva egy konténeren belül. Valójában egy konténer lehet üres is. Egy CSP tehát általánosságban a következő módon néz ki:CSP és konténerek
Ha a tanúsítványtárban van egy tanúsítvány, és a tanúsítvány meg van jelölve, hogy a felhasználó rendelkezik a tanúsítványhoz privát kulccsal, az azt jelenti, hogy a Windows rendelkezik azzal az információval, hogy melyik provider melyik konténerének melyik kulcsa tartozik a tanúsítványhoz.

Amikor behelyezzük az e-Szignó smartkártyát a kártyaolvasóba, akkor egy előre telepített program leolvassa a kártyán lévő tanúsítványokat, elhelyezi őket a Windows tanúsítványtárában, és feljegyzi hozzájuk a CSP/Konténer/Kulcs információkat. Amikor a .NET-ben a X509Certificate2 példány PrivateKey tulajdonságát lekérdezzük, akkor a program megpróbál a CSP/Konténer/Kulcs információk alapján hozzáférni a kulcshoz, de ez nem sikerül. Lényegében az e-Szignó kártya szoftvere a saját maga által összegyűjtött információt nem tudja használni.

Egy minősített aláíró eszköz esetében azért az ember gyanakodik, hogy nem lehet rossz. Ami biztos, hogy például az Outlook tudja használni a kártyát, hogy a kimenő levelet digitálisan aláírja. Össze lehet dobni egy C programot, ami megpróbálja használni a kártyát. Ekkor a következő függvényeket lehet alkalmazni:

  • CertFindCertificateInStore – megkeres egy adott tanúsítványt a tárban, és visszaad a tanúsítványhoz egy kontextust. Alternatívaként lehet használni a CryptUIDlgSelectCertificate függvényt.
  • CryptAcquireCertificatePrivateKey – visszaad egy CSP handle-t, amelyik a tanúsítványhoz tartozó kulcsot tudja használni.
  • CryptCreateHash / CryptHashData / CryptSignHash – elkészíti a digitális aláírást. 

Egy tesztprogram a fenti WinAPI-s függvényekkel hibátlanul működik. Mi lehet akkor baj a .NET-tel? Némi reflektorozás után kiderül, hogy a X509Certificate2 példány PrivateKey tulajdonsága kicsit kacifántosabban működik, minthogy meghívná a CryptAcquireCertificatePrivateKey függvényt. A .NET az RSACryptoServiceProvider osztályt szeretné használni, annak pedig nincs olyan konstruktora, ami egy CSP handle-re ülne rá (amit például a CryptAcquireCertificatePrivateKey hívás ad vissza). Emiatt kénytelen a kód külön lekérni a CSP paramétereket a CertGetCertificateContextProperty WinAPI fügvénnyel, hogy aztán azokat egy .NET-es CspParameters osztálypéldányba töltse. A CspParameters példány ezután odaadható a RSACryptoServiceProvider osztály konstruktorának. A RSACryptoServiceProvider konstruktor ekkor a CryptAcquireContext Windows API függvényt hívja meg.

Gyorsan írható egy C program, ami a fenti séma szerint próbálja meg használni az e-Szignó CSP-t. Az eredmény? Így tényleg nem működik! A CryptAcquireContext Windows API függvény hibát ad vissza, ha az e-Szignó CSP-jét kell használnia!

Bár nem szép dolog, hogy a CryptAcquireContext nem működik, valahol kimagyarázható egy kicsit. Mivel a kulcsokat hordozó konténerek nevei random számok (vagy annak látszanak), illetve mivel a kártyát valószínűleg úgy tervezték, hogy a használója mindig a tanúsítványok felől indul, a CryptAcquireContext elsőre nem tűnik hasznos függvénynek. Akárhogy is, azzal, hogy ez a helyzet adott, a .NET-es próbálkozásokat jócskán megnehezítik.

Amit ebben a helyzetben lehet csinálni, hogy P/Invoke segítségével megírjuk a saját kódunkat, hogy egy tanúsítvány kulcsával írjon alá adatot. A szükséges natív függvények legtöbbje korábban már említésre került:

  • CryptAcquireCertificatePrivateKey – egy adott tanúsítványhoz tartozó kulcshoz szerez egy CSP handle-t. A CSP handle szükséges a többi kriptografikus művelethez. Egy digitális aláíráshoz például szükséges, hogy melyik CSP melyik kulcs konténerében levő aláíró kulcsot kell használni. Egy CSP handle egy használatra előkészített CSP/konténer konfiguráció elérését teszi lehetővé.
  • CryptReleaseContext – a CryptAcquireCertificatePrivateKey által létrehozott handle felszabadításához szükséges függvény.
  • CryptCreateHash– a digitális aláírás az adatok hash értékét szokta magába foglalni. A hash érték előállítását elvégző algoritmust inicializálja ez a függvény, amely algoritmust a továbbiakban a visszaadott handle-lel lehet elérni, például adatot tölteni bele.
  • CryptHashData – a CryptCreateHash által előkészített algoritmus számára tölti be az adatokat. A hash műveleteket általában egy utolsó lépésben le kell zárni. A mi esetünkben erre nem lesz szükség, mert a digitális aláírást végző művelet ezt a szükséges utolsó lépést megcsinálja.
  • CryptDestroyHash – a CryptCreateHash által létrehozott handle felszabadítása
  • CryptSignHash – egy feltöltött hash-t aláíró függvény.
  • CryptSetProvParam – adott CSP paramétereinek beállítását lehetővé tevő függvény. A smartkártya PIN-kódjának beállítására fogjuk használni.

Az alábbi diagramon látható, hogy hogyan érdemes használni a natív függvényeket:

 A felhasználó kódja egy SignData(certificate, data, pin) jellegű hívással indítja el az aláírás folyamatát. A certificate egy X509Certificate2 példány, kell hogy a felhasználó rendelkezzen hozzá tartozó privát kulccsal. A data egy byte tömb, az aláírandó adatot tartalmazza. A pin egy opcionális paraméter, a smartkártya pinkódját tartalmazza. Ezt a paramétert SecureString típusban érdemes tárolni, hogy ne maradjon memóriaszemétként a memóriában a pin kód. A smartkártyára épülő CSP-k általában rendelkeznek mechanizmussal, hogy a pin-kódot maguk kérjék be egy saját felületen. Az e-Szignó CSP ezen felül képes kezelni pin kód bevitelére alkalmas billentyűzettel rendelkező smart kártya olvasókat is, ami a pin kód bevitelének legbiztonságosabb móda. Ha a pin paraméter hiányzik, az e-Szignó CSP a saját mechanizmusát fogja használni a pin kód bekérésére.
Az aláírást megvalósító kódban három részre érdemes bontani a műveleteket. Az első rész inicializálja azt a CSP-t, ami a paraméterben átadott tanúsítvány privát kulcsát kezeli. Ezen felül, ha van megadott pin kód, azt is beállítja a CSP-n. A CSP inicializálást az aláíró osztályban egy AcquireKeyAndSetPin(certificate, pin) metódus valósítja meg. Ez első lépésben meghívja a Windows API CryptAcquireCertificatePrivateKey metódust, ami visszaadja annak a CSP-nek a handle-jét, ami a tanúsítvány privát kulcsát kezeli. Második lépésében a AcquireKeyAndSetPin beállítja a CSP-n a pin kódot, ha azt a hívó megadta. A pin kód beállítására a CryptSetProvParam Windows API függvény használható.

Második fő lépésben azt a hash értéket kell előállítani, ami a digitalis aláírás magábafoglal. Ezt a műveletet egy CalculateHash(cspHadnle, data) jellegű függvény végzi el. A hash érték számítását ugyanannak a CSP handle-nek a segítségével kell elvégezni, amivel az aláíró kulcsot fogjuk elérni, különbnen a CSP nem “látja” a hash értéket. Ezért kell a korábban megszerzett CSP handle. A Windows CryptoAPI-ban egy hash számítást egy hash algoritmus inicializálásával kell kezdeni, erre használható a CryptCreateHash függvény. A CryptCreateHash függvény egy hash handle-t ad vissza. A hash handle-n keresztül lehet feltölteni az algoritmust az adatokkal, aminek a hash értékét ki szeretnénk számolni. Az adatfeltöltés a CryptHashData függvénnyel történik. Normál esetben még szükség lenne egy harmadik, a hash érték számításának befejezését végrehajtó függvényhívásra is. Digitális aláírásnál azonban az aláírást elvégző Windows API függvény ezt az utolsó lépést maga elvégzi. Emiatt a hash számítás csak ebből a két lépésből, a CryptCreateHash / CryptHashData párosból áll.

Harmadik lépésben kell előállítani a digitális aláírást. Azt várná az ember, hogy ehhez szükség van a korábban megszerzett CSP handle értékére, de nem. A digitális aláíráshoz csak a hash handle-je kell. A hash handle-t egy adott CSP állította elő, emiatt a hash handle-n keresztül a Windows API azonosítani tudja a CSP-t is, amivel a digitális aláírást el kell végeztetni. A digitális aláírást a CryptSignHash metódus készíti el.

Végül a létrehozott handle-ket fel kell szabadítani a CryptDestroyHash és CryptReleaseContext hívásokkal. A natív hívásokat leíró kód a következő:

  using System;
  using System.Runtime.InteropServices;

  /// <summary>
  /// A szükséges WinAPI-s függvényeket tartalmazó osztály
  /// </summary>
  internal static class NativeMethods
  {
    /// <summary>
    /// Szerez egy kriptografikus provider handle-t, amely a
    /// <paramref name="certificateContext"/> paraméterben megadott
    /// tanúsítványhoz tartozó privát kulcsot kezeli.
    /// </summary>
    /// <param name="certificateContext">A tanúsítvány windowsos
    /// handle-je, amelyhez a privát kulcsot kezelő CSP tartozik.</param>
    /// <param name="flags">A függvény működését befolyásoló kapcsolók,
    /// lásd crypto API dokumentáció.</param>
    /// <param name="reserved">Nem használt.</param>
    /// <param name="providerHandle">A tanúsítványhoz tartozó privát
    /// kulcsot kezelő kriptografikus provider handle-je.</param>
    /// <param name="keySpec">A kulcshasználatot leíró érték, aláíró
    /// vagy kulcs csere kulcs.</param>
    /// <param name="callerFree">Ha <c>true</c> a hívónak kell
    /// felszabadítani a visszaadott <paramref name="providerHandle"/>.
    /// </param>
    /// <returns>Ha <c>true</c>, akkor a függvény futása sikeres volt.
    /// </returns>
    [DllImport("crypt32.dll", SetLastError = true)]
    internal static extern bool CryptAcquireCertificatePrivateKey(
                                 IntPtr certificateContext,
                                 int flags,
                                 IntPtr reserved,
                                 ref IntPtr providerHandle,
                                 ref int keySpec,
                                 ref bool callerFree);

    /// <summary>
    /// Felszabadítja a CSP-hez tartozó handle-t.
    /// </summary>
    /// <param name="providerHandle">A felszabadítani kívánt CSP handle.
    /// </param>
    /// <param name="flags">A függvény működését befolyásoló kapcsolók,
    /// lásd crypto API dokumentáció.</param>
    /// <returns>Ha <c>true</c>, akkor a függvény futása sikeres volt.
    /// </returns>
    [DllImport("advapi32.dll")]
    internal static extern bool CryptReleaseContext(
                                 IntPtr providerHandle,
                                 int flags);

    /// <summary>
    /// Készit egy hash műveletekre alkalmas objektumot, és visszaadja a
    /// hozzá tartozó handle-t.
    /// </summary>
    /// <param name="providerHandle">Egy kriptografikus provider handle,
    /// amelyen belül a hash objektumot létre kell hozni.</param>
    /// <param name="algId">A hash műveletet leíró algoritmus, mint SHA1.
    /// </param>
    /// <param name="keyHandle">A kulcsolt hash műveletekhez használt
    /// kulcs.</param>
    /// <param name="flags">A függvény működését befolyásoló kapcsolók,
    /// lásd crypto API dokumentáció.</param>
    /// <param name="hashHandle">A létrehozott hash objektumhoz tartozó
    /// handle.</param>
    /// <returns>Ha <c>true</c>, akkor a függvény futása sikeres volt.
    /// </returns>
    [DllImport("advapi32.dll", SetLastError = true)]
    internal static extern bool CryptCreateHash(
                                 IntPtr providerHandle,
                                 int algId,
                                 IntPtr keyHandle,
                                 int flags,
                                 ref IntPtr hashHandle);

    /// <summary>
    /// Elkészíti a <paramref name="dataToHash"/> paraméterben átadott
    /// adat hash-ét. Az elkészített hash értékét külön függvényhívással
    /// lehet kiolvasni, vagy digitálisan aláíratni.
    /// </summary>
    /// <param name="hashHandle">A hash objektumhoz tartozó handle, amely
    /// a számítást végezni fogja.</param>
    /// <param name="dataToHash">Az adat, aminek a hash értékét el kell
    /// készíteni.</param>
    /// <param name="dataLen">Az adat hossza byteokban, amihez a hash
    /// értéket el kell készíteni.</param>
    /// <param name="flags">A függvény működését befolyásoló kapcsolók,
    /// lásd crypto API dokumentáció.</param>
    /// <returns>Ha <c>true</c>, akkor a függvény futása sikeres volt.
    /// </returns>
    [DllImport("advapi32.dll", SetLastError = true)]
    internal static extern bool CryptHashData(
                                 IntPtr hashHandle,
                                 byte[] dataToHash,
                                 int dataLen,
                                 int flags);

    /// <summary>
    /// Felszabadít egy hash objektumhoz tartozó handle-t, melyet előzőleg
    /// a <see cref="CryptCreateHash"/> függvénnyel foglaltak.
    /// </summary>
    /// <param name="hashHandle">Egy hash objektumhoz tartozó handle.
    /// </param>
    /// <returns>Ha <c>true</c>, akkor a függvény futása sikeres volt.
    /// </returns>
    [DllImport("advapi32.dll", SetLastError = true)]
    internal static extern bool CryptDestroyHash(
                                 IntPtr hashHandle);

    /// <summary>
    /// Digitális aláírást készít. Az aláírásba az átadott hash objektum
    /// által számolt értéket teszi. Az aláíráshoz használt privát
    /// kulcs a hash objektumot hordozo kontextus alatt levő kulcs.
    /// </summary>
    /// <param name="hashHandle">A hash objektumhoz tartozó handle, amely
    /// a digitális aláírásba kerül.</param>
    /// <param name="keySpec">A kulcshasználatot leíró érték, aláíró vagy
    /// kulcs csere kulcs. Az aláíró kulcsot van értelme megadni.</param>
    /// <param name="description">Nem használt, mindig null.</param>
    /// <param name="flags">A függvény működését befolyásoló kapcsolók,
    /// lásd crypto API dokumentáció.</param>
    /// <param name="signature">A digitális aláírást hordozó tömb. Ha
    /// értéke null, a függvény visszaadja az aláíráshoz szükséges
    /// tömbméretet a <paramref name="signatureLenght"/> paraméterben.
    /// </param>
    /// <param name="signatureLenght">A digitális aláírást hordozó tömb
    /// mérete byteokban.</param>
    /// <returns>Ha <c>true</c>, akkor a függvény futása sikeres volt.
    /// </returns>
    [DllImport("advapi32.dll", SetLastError = true)]
    internal static extern bool CryptSignHash(
                                 IntPtr hashHandle,
                                 int keySpec,
                                 IntPtr description,
                                 int flags,
                                 [Out] byte[] signature,
                                 [In, Out] ref int signatureLenght);

    /// <summary>
    /// Beállítja a CSP egy paraméterét. A függvény prototípusa a PIN-kód
    /// beállítására lett kihegyezve, ezért az eredeti paraméter lista
    /// helyett, ahol a <paramref name="data"/> paraméter esetében
    /// előnyösebb lett volna egy byte[] típus alkalmazása, IntPtr lett
    /// bevezetve. Ez a PIN-kód számára azért jobb, mert a PIN kód
    /// tipikusan <see cref="SecureString"/> osztály példányban tárolt,
    /// ezt pedig egy Marshal.ZeroFreeGlobalAllocUnicode() hívással lehet
    /// az unmanaged kódnak átadni. Emiatt egy IntPtr típusú referencia
    /// lesz az, amit a függvényhívásnál rendelkezésre áll.</summary>
    /// <param name="providerHandle">Egy kriptografikus provider handle,
    /// amit a paraméterét állítani kell.</param>
    /// <param name="param">A beállítandó paraméter kódja.</param>
    /// <param name="data">A beállítandó paraméter értéke.</param>
    /// <param name="flags">A függvény működését befolyásoló kapcsolók,
    /// lásd crypto API dokumentáció.</param>
    /// <returns>Ha <c>true</c>, akkor a függvény futása sikeres volt.
    /// </returns>
    [DllImport("advapi32.dll", SetLastError = true)]
    internal static extern bool CryptSetProvParam(
                                 IntPtr providerHandle,
                                 int param,
                                 IntPtr data,
                                 int flags);
  } // class NativeMethods

A natív hívások segítségével az aláírást elvégző osztály már könnyen összerakható. Két dologra kell figyelni. Az egyik a handle-k megfelelő kezelése hibaesetekben. A másik dolog a világ (amibe a .NET is beletartozik) inkompabilitása a Windows Crypto API-val. A gondot az okozza, hogy az RSA aláírás tulajdonképpen nem más, mint egy matematikai művelet egy hatalmas egész számon. Számot ábrázolni pedig több módon lehet, aki próbált együttműködésre bírni különböző architektúrájú számítógépeket, egészen biztos, hogy találkozott az „Endianness” problémájával. Nos, a Crypto API fordított sorrendben ábrázolja az aláírást műveleteként létrejött számot, mint a .NET, illetve a világ többi része. Emiatt az elkészített aláírást még át kell fordítani. Az aláírást végző osztály kódja a következő:

  using System;
  using System.ComponentModel;
  using System.Diagnostics;
  using System.Runtime.InteropServices;
  using System.Security;
  using System.Security.Cryptography.X509Certificates;
  using System.Security.Permissions;

  /// <summary>
  /// Olyan függvényeket összefoglaló osztály, amely egy már betöltött tanúsítványhoz
  /// tartozó privát kulccsal végez el műveleteket.
  /// </summary>
  public static class SignUsingCertificate
  {
    /// <summary>
    /// Algoritmus azonosító a <see cref="CryptCreateHash"/> függvényhez. Értéke azt
    /// adja meg, hogy a használni kivánt hash algoritmus az SHA1. A CryptoAPI-ban
    /// CALG_SHA1 néven szerepel a konstant.
    /// </summary>
    private const int CalgSha1 = 0x00008004;

    /// <summary>
    /// Paraméter kód a <see cref="CryptSetProvParam"/> függvényhez. Értéke azt
    /// adja meg, hogy a beállítani kívánt paraméter az aláíró PIN. A CryptoAPI-ban
    /// PP_SIGNATURE_PIN néven szerepel a konstant.
    /// </summary>
    private const int ParamSignaturePin = 33;

    /// <summary>
    /// Egy megadott adathalmazt ír alá digitálisan. Az aláírást az átadott tanúsítványhoz
    /// tartozó privátkulccsal készíti el.
    /// </summary>
    /// <param name="certificate">A tanúsítvány, amelyhez tartozó privát kulcsot használni kell.</param>
    /// <param name="data">Az adat, amelyet alá kell írni digitálisan.</param>
    /// <param name="pin">A PIN kód, amelyet az aláíráshoz meg kell adni. Ha értéke <c>null</c>, akkor
    /// a privátkulcsot kezelő CSP szükség esetén a saját dialógus ablakát megjelenítve kéri be a
    /// PIN kódot.</param>
    /// <returns>A digitális aláírás.</returns>
    [SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)]
    public static byte[] SignData(X509Certificate certificate, byte[] data, SecureString pin)
    {
      if (data == null)
      {
        throw new ArgumentNullException("data");
      } // if

      IntPtr providerHandle = IntPtr.Zero;                                     // Ez tárolja majd a handle-t a CSP-hez.
      IntPtr hashHandle = IntPtr.Zero;                                         // Ez tárolja a hash objektum handle-jét, ami alá lesz íratva.

      bool callerFree = false;                                                 // Én szabadítsam-e fel a visszaadott handle-t, vagy ne.
      byte[] signature;                                                        // Az aláírást fogadó tömb.

      try
      {
        int keySpec = 0;                                                       // A tanúsítványhoz tartozó kulcs aláíró vagy key exchange. A AcquireKeyAndSetPin() határozza meg.
        providerHandle = AcquireKeyAndSetPin(                                  // A privát kulcshoz tartozó CSP megszerzése:
                           certificate,                                        // Ehhez a tanúsítványhoz tartozó privát kulcshoz kell a CSP
                           pin,                                                // Pin kód a kulcshasználathoz.
                           out keySpec,                                        // A kulcs típusa, aláíró vagy kulcs csere kulcs.
                           out callerFree);                                    // A CSP handle-t fel kell-e szabadítani, vagy nem.
                                                                               // Most kell egy hash objektum, hogy a messagét lehasheljük.
        hashHandle = CalculateHash(                                            // Az adat hashének számítása. A visszatérési érték egy handle a hash objektumra,
                       providerHandle,                                         // ezt használja később az aláíró függvény. Bemenő paraméter a CSP handle, ami a tanúsítványhoz
                       data);                                                  // tartozó privát kulcsot kezeli. Ebben kell létrehozni a hast, különben nem
                                                                               // tudja rá alkalmazni a privát kulcsot.
        signature = CalculateSignature(                                        // A digitális aláírás elkészítése. Elég átadni a hash objektum handle-jét, mivel az ugyanabban a CSP-ben
                      hashHandle,                                              // van, mint a privát kulcs. A CSP két privátkulcsot tud kezelni, ezek közül adja meg a keySpec, hogy melyik kell.
                      keySpec);                                                // A keySpec paramétert korábban az AcquireKeyAndSetPin() hívás adta vissza
      }
      finally
      {
        if (hashHandle != IntPtr.Zero)
        {                                                                      // Ha van érvényes handle egy has objektumhoz,
          NativeMethods.CryptDestroyHash(hashHandle);                          // felszabadítani
        } // if

        if (callerFree == true && providerHandle != IntPtr.Zero)
        {                                                                      // Ha a CSP a hívóval szabadítatja fel a handle-t,
          NativeMethods.CryptReleaseContext(providerHandle, 0);                // és az érvényes, felszabadítani.
        } // if
      } // finally

      Array.Reverse(signature);                                                // A CryptoAPI fordítva tárolja az aláírás byte-it, mint a világ (meg mint a .NET)
      return signature;
    } // SignData()

    /// <summary>
    /// Megszerzi a <paramref name="certificate"/> paraméterben átadott tanúsítványhoz tartozó
    /// privát kulcsot, és ha a <paramref name="pin"/> paraméter nem null, beállítja a használathoz
    /// szükséges pin kódot.
    /// </summary>
    /// <param name="certificate">A tanúsítvány, amelyhez tartozó privát kulcs szükséges.</param>
    /// <param name="pin">A privát kulcs használatához szükséges pin kód. Ha értéke <c>null</c>, akkor
    /// a privát kulcsot tároló CSP fog kikérdezni a pin kódért, ha az szükséges.</param>
    /// <param name="keySpec">Kimenő paraméter, amely visszaadja, hogy a privát kulcs az aláíró kulcs vagy
    /// kulcs csere kulcs.</param>
    /// <param name="callerFree">if set to <c>true</c> [caller free].</param>
    /// <returns>A privát kulcsot kezelő CSP handle-je, a továbbiakban ezt lehet paraméterként
    /// átadni a privát kulcsot értintő műveletekhez. Használat után ezt a hívónak fel kell szabadítania.</returns>
    private static IntPtr AcquireKeyAndSetPin(X509Certificate certificate, SecureString pin, out int keySpec, out bool callerFree)
    {
      IntPtr providerHandle = IntPtr.Zero;                                    // Ebben lesz a CSP handle, a visszatérési érték.

      callerFree = false;                                                     // Alapból nem kell felszabadítani semmit.
      keySpec = 0;                                                            // Alapból nincs meghatározva a kulcs típusa.

      try
      {
        bool result;

        result = NativeMethods.CryptAcquireCertificatePrivateKey(              // Adjon CSP handle-t a tanúsítványhoz tartozó privát kulcshoz.
                                 certificate.Handle,                           // Ehhez a tanúsítványhoz szerezzen CSP-t.
                                 0,                                            // Nem kellenek flagek.
                                 IntPtr.Zero,                                  // reserved...
                                 ref providerHandle,                           // Ebbe jön a CSP handle.
                                 ref keySpec,                                  // Ebbe jön, hogy aláíró kulcs-e.
                                 ref callerFree);                              // Ebbe jön, hogy nekem kell-e felszabadítani a handle-t.

        if (result == false)
        {                                                                      // nem sikerült handle-t keresnni.
          throw new Win32Exception();                                          // ez kiolvassa a lasterrort, szóval nem kell átadni neki.
        } // if

        if (pin != null)
        {                                                                      // Ha van PIN kód, állítsa be programból a kikérdezés helyett.
          IntPtr marshaledPin = Marshal.SecureStringToGlobalAllocAnsi(pin);    // A PIN kód átadása az unmanaged résznek.

          result = NativeMethods.CryptSetProvParam(                            // CSP paraméter beállítás:
                                   providerHandle,                             // Használja a tanúsítvány privát kulcsának a CSP-jét.
                                   ParamSignaturePin,                          // Az aláíró PIN kódot állítjuk be.
                                   marshaledPin,                               // Az unmanaged részre tett PIN kód handle-je
                                   0);                                         // Nem kellenek flagek.

          Marshal.ZeroFreeGlobalAllocAnsi(marshaledPin);                       // Nullázza ki, majd szabadítsa fel a PIN kód helyét.

          if (result == false)
          {                                                                    // nem sikerült beállítani a PIN kódot.
            throw new Win32Exception();                                        // ez kiolvassa a lasterrort, szóval nem kell átadni neki.
          } // if
        } // if
      }
      catch
      {
        if (callerFree == true && providerHandle != IntPtr.Zero)
        {                                                                      // Ha a CSP a hívóval szabadítatja fel a handle-t,
          NativeMethods.CryptReleaseContext(providerHandle, 0);                // és az érvényes, felszabadítani.
        } // if

        throw;
      } // catch

      return providerHandle;
    } // AcquireKeyAndSetPin()

    /// <summary>
    /// Elkészíti a <paramref name="data"/> hash értékét SHA1 algoritmust
    /// használva, és visszaadja a CSP hash objektum handle-t, amit egyéb
    /// műveletekben, például digitális aláíráshoz lehet használni.
    /// </summary>
    /// <param name="providerHandle">A CSP handle, amelyen belül létre kell hozni
    /// a hash objektumot. Az a CSP handle kell, amely később a szükséges műveletet,
    /// például a digitális aláírást végzi, különben nem fogja látni a hash objektumot.</param>
    /// <param name="data">Az adat, aminek a hash értékét ki kell számolni.</param>
    /// <returns>A hash objektum handle-je. Használat után a hívónak ezt fel kell szabadítania.</returns>
    private static IntPtr CalculateHash(IntPtr providerHandle, byte[] data)
    {
      Debug.Assert(data != null, "data != null");

      IntPtr hashHandle = IntPtr.Zero;

      try
      {
        bool result;

        result = NativeMethods.CryptCreateHash(                                // Csináljon hash objektumot
                                 providerHandle,                               // Használja a tanúsítvány privát kulcsának a CSP-jét, aláíráshoz úgyis olyan handle kell, ami abban a CSP-ben van
                                 CalgSha1,                                     // SHA1-et megvalósító hash objektum kell.
                                 IntPtr.Zero,                                  // Nem keyed hash, nem kell kulcs.
                                 0,                                            // Nincs flag.
                                 ref hashHandle);                              // Ebbe jön a hash objektum handle-je.

        if (result == false)
        {                                                                      // nem sikerült hash objektumot csinálni.
          throw new Win32Exception();                                          // ez kiolvassa a lasterrort, szóval nem kell átadni neki.
        } // if

        result = NativeMethods.CryptHashData(                                  // Most le lehet hashelni az adatot:
                                 hashHandle,                                   // A hash objektum handle-je, ami a hash-elést végzi.
                                 data,                                         // A hashelni kívánt adat tartalma
                                 data.Length,                                  // A hashelni kívánt adat hossza
                                 0);                                           // Flag az nem kell.

        if (result == false)
        {                                                                      // nem sikerült hashelni.
          throw new Win32Exception();                                          // ez kiolvassa a lasterrort, szóval nem kell átadni neki.
        } // if
      }
      catch
      {
        if (hashHandle != IntPtr.Zero)
        {                                                                      // Ha van érvényes handle egy has objektumhoz,
          NativeMethods.CryptDestroyHash(hashHandle);                          // felszabadítani
        } // if

        throw;
      } // catch

      return hashHandle;
    } // CalculateHash()

    /// <summary>
    /// Elkészíti a <paramref name="hashHandle"/> objektum által tartott hash értékre
    /// a digitális aláírást. Az aláíráshoz azt a kulcsot használja, amelyiket az a
    /// CSP hordoz, amiben a hash objektum létre lett hozva. A <paramref name="keySpec"/>
    /// azt mondja meg, hogy az aláíró vagy a kulcs csere kulccsal legyen elkészítve
    /// az aláírás. Értelme az aláíró kulcsnak van.
    /// </summary>
    /// <param name="hashHandle">A hash értéket tároló objektum handle-je, amire az
    /// aláírás készül.</param>
    /// <param name="keySpec">A használni kívánt kulcs, aláíró vagy kulcs csere.</param>
    /// <returns>A digitális aláírást tároló tömb.</returns>
    private static byte[] CalculateSignature(IntPtr hashHandle, int keySpec)
    {
      int signatureLenght = 0;                                               // Meg kell tudni az aláírás tárolásához szükséges tömbméretet
      bool result;

      result = NativeMethods.CryptSignHash(
                               hashHandle,                                   // A hash objektum handle-je, amiből a hash érték az aláírásba kerül
                               keySpec,                                      // Melyik kulcsot használja (aláíró, kulcs csere).
                               IntPtr.Zero,                                  // Description mező nem használt
                               0,                                            // Flagek nincsenek.
                               null,                                         // Nincs hely az aláírásnak, csak a mérete kell.
                               ref signatureLenght);                         // Ebbe teszi a szükséges méretet.

      if (result == false)
      {                                                                      // nem sikerült elkérni a digitális aláírás hosszát.
        throw new Win32Exception();                                          // ez kiolvassa a lasterrort, szóval nem kell átadni neki.
      } // if

      byte[] signature = new byte[signatureLenght];                          // Megvan az aláírás mérete, terület foglalás neki.

      result = NativeMethods.CryptSignHash(                                  // Az aláírás elkészítése
                               hashHandle,                                   // A hash objektum handle-je, amiből a hash érték az aláírásba kerül
                               keySpec,                                      // Melyik kulcsot használja (aláíró, kulcs csere).
                               IntPtr.Zero,                                  // Description mező nem használt
                               0,                                            // Flagek nincsenek.
                               signature,                                    // Ebbe írja az aláírást
                               ref signatureLenght);                         // Az aláírást fogadó tömb mérete.

      if (result == false)
      {                                                                      // nem sikerült elkészíteni a digitális aláírást
        throw new Win32Exception();                                          // ez kiolvassa a lasterrort, szóval nem kell átadni neki.
      } // if

      return signature;                                                      // A digitális aláírás visszaadása a hívónak.
    } // CalculateSignature()
  } // SignUsingCertificate

A kód természetesen nem csak az e-Szingó kártya tanúsítványaival, hanem más tanúsítványokkal is. Továbbfejlesztési lehetőségek természetesen vannak, például a fenti kód beégetve mindig az SHA1 algoritmust használja a digitális aláírás elkészítéséhez.

  1. Leave a comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: