gubus 2.0

2010. 09. 1.

Elektronikus aláírás készítése PDF állományra smart card támogatással .NET alatt

Kategória: .NET, dotnet — Címkék: — gubus @ 23:56

Update 2010. 11. 27. – A SignPdf() hívás elején nem volt helyes a tanúsítványlánc felépítése, ami az “if (chain.Length >= 2)” sornál levő OCSP ellenőrzés működéséhez kell.

A PDF állományok elektronikusan alárhatóak, melyet .NET platformon az iTextSharp (http://sourceforge.net/projects/itextsharp/files/) osztálykönyvtárral lehet elvégezni.
Az osztálykönyvtárhoz vannak példakódok a http://itextpdf.sourceforge.net/howtosign.html címen, egész jó tutorial-ok, vagy példaalkalmazások érhetőek el:

http://isafepdf.codeplex.com/

http://www.codeproject.com/KB/security/isafepdf.aspx?display=Print

Az aláírás mellett még az is szükséges, hogy az aláírásra időpecsét kerüljön, ugyanis ha a tanúsítvány lejár, vagy visszavonásra kerül, és ha nem állapítható meg hitelesen az aláírás időpontja, akkor nem lehet bizonyítani, hogy az elromlás előtt lett még az aláírás elkészítve. A fentiek miatt kell egy aláírói tanúsítványra az felhasználónak, és szükséges egy időpecsét szerver elérés is (előfizetés).

Ha felhasználó minősített elektronikus aláírást szeretne, akkor a tanúsítványát egy smard card kártyán kell, hogy tárolja. A kártya fontos tulajdonsága, hogy a tanúsítvány privát kulcsát nem lehet kiolvasni róla, csak meg lehet kérni a kártyát, hogy a kért kriptográfiai műveletet végezze el, és az eredményt adja vissza. A kriptográfiai API-knak van egy hagyomános működési módjuk is: ilyenkor az API a tanúsítvány privát kulcsát kiolvassa, és közvetlenül a kulcs felhasználásával számolja ki az eredményt.

A kártyáknak kétféle API-juk van:
- PKCS #11
- Microsoft™ CryptoAPI – gyakorlatilag egy Windows drivert jelent, segítségével a kártyára tett X509-es tanúsítványok egy az egyben megjelennek a Windows tanúsítványtárában, és így .NET-ből is könnyen elérhetőek.

A látott iTextSharp-os aláírást készítő példakódoknak két típusa van: az időpecsét nélküli, és időpecsétes.

1. Időpecsét nélküli, de smart card támogatással.
How to sign with a smartcard using an external signature dictionary with iTextSharp and .NET 2.0

http://itextpdf.sourceforge.net/howtosign.html#signextitextsharp2

Ez a verzió a .NET SignedCms osztályát használja fel az aláírásra, mely egy PKCS#7 formátumú adatcsomagot ad vissza. A SignedCms kezeli azt az esetet is, ha a tanúsítvány kártyán van.

2. Időpecsétes, de smart card támogatás nélkül.
How to sign with a timestamp and OCSP in C#

http://itextpdf.sourceforge.net/howtosign.html#signtsocspcs

Aláírás készítése időpecséttel. Itt bonyolultabb a működés: mivel a PKCS#7 csomagba még bele kell tenni az időpecsétre vonatkozó adatokat, ezért az iTextSharp mellé tett Bouncy Castle Crypto API (http://www.bouncycastle.org/) kézzel állítja elő az aláírás HASH-t, és rakja össze a PKCS#7 üzenetet. A Bouncy Castle viszont csak a privát kulcs birtokában tud dolgozni, ezért kártyán tárolt tanúsítvánnyal _nem_ működik a kód.

Első próbálkozásként megpróbáltam a .NET RSACryptoServiceProvider osztályával előállítani a SHA1withRSA aláírást:

X509Certificate2 card = GetCertificate();
RSACryptoServiceProvider rsa = (RSACryptoServiceProvider)card.PrivateKey;
byte[] signedHashValue1 = rsa.SignData(documentHash, new SHA1Managed());

Sajnos nem működik a kártyával, a card.PrivateKey -> RSACryptoServiceProvider konverzió elszáll:

System.Security.Cryptography.CryptographicException: Bad Key.
at System.Security.Cryptography.CryptographicException.ThrowCryptogaphicException(Int32 hr)
at System.Security.Cryptography.Utils._GetKeyParameter(SafeKeyHandle hKey, UInt32 paramID)
at System.Security.Cryptography.Utils.GetKeyPairHelper(CspAlgorithmType keyType, CspParameters parameters, Boolean randomKeyContainer, Int32 dwKeySize, SafeProvHandle& safeProvHandle, SafeKeyHandle& safeKeyHandle)
at System.Security.Cryptography.RSACryptoServiceProvider.GetKeyPair()
at System.Security.Cryptography.RSACryptoServiceProvider..ctor(Int32 dwKeySize, CspParameters parameters, Boolean useDefaultKeySize)
at System.Security.Cryptography.RSACryptoServiceProvider..ctor(CspParameters parameters)
at System.Security.Cryptography.X509Certificates.X509Certificate2.get_PrivateKey()

A card.PrivateKey -> RSACryptoServiceProvider konverzióra van elvileg egy támogatott módszer:

/// <summary>
/// private static RSACryptoServiceProvider PrivateKey(String providerName, String keyContainerName, KeyNumber keytype)
/// String providerName: "Microsoft Base Smart Card Crypto Provider" if your CSP implement a mini driver (Card Modules)
/// String keyContainerName: "DS0" for smart card Infocert type CNS 1204XXXX csp KeyContainerName first Digitale Signature Key Container
/// KeyNumber keytype: KeyNumber.Signature
/// </summary>
private static RSACryptoServiceProvider PrivateKey(String providerName, String keyContainerName, KeyNumber keytype)
{
CspParameters csparms = new CspParameters();
csparms.ProviderName = providerName; // "Microsoft Base Smart Card Crypto Provider";
csparms.KeyContainerName = keyContainerName; // "DS0";
csparms.KeyNumber = (int)keytype;
csparms.ProviderType = 1;
csparms.Flags = CspProviderFlags.UseUserProtectedKey; // not sure you need this!
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(csparms);
return rsa;
}

certSigner.PrivateKey = PrivateKey(providerName, keyContainerName, KeyNumber.Signature);

De nem sikerült sajnos behergelni, meg az se szép benne, hogy nem általános a módszer, hanem a providerName, keyContainerName paramétereket kell hajkurászni (ami meg kártyafüggő).

Végül Windows CryptoAPI hívásokkal sikerült az SHA1withRSA aláírást előállítani úgy, hogy támogatja a kártyán levő tanúsítványt is.

A CryptoAPI hívásoknál felhasználtam Alejandro Campos Magencio által készített pinvoke segédosztályt is:

http://blogs.msdn.com/b/alejacma/archive/2007/11/23/p-invoking-cryptoapi-in-net-c-version.aspx

Íme a végeredmény:


using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using iTextSharp.text.pdf;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Security;

namespace PdfSignerTest
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Console.WriteLine("Start");

                string inputFileName = @"..\..\..\Report.pdf";
                string outputFileName = @"..\..\..\Signed.pdf";
                string timeStampUrl = "http://www.xxxx.com/timestamp.cgi";

                using (FileStream inputPdfStream = new FileStream(inputFileName, FileMode.Open, FileAccess.Read))
                {
                    using (FileStream outputPdfStream = new FileStream(outputFileName, FileMode.Create, FileAccess.Write))
                    {
                        X509Certificate2 card = SelectCertificate();

                        SignPdf(card, inputPdfStream, outputPdfStream, false, "Test Reason", "Test Location", "Test Contact", timeStampUrl, "teszt", "teszt");
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
            finally
            {
                Console.WriteLine("Done.");
                Console.ReadLine();
            }
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="certificate">smart card certificate</param>
        /// <param name="inputPdfStream">input PDF</param>
        /// <param name="outputPdfStream">output PDF (signed)</param>
        /// <param name="append">append if <CODE>true</CODE> the signature and all the other content will be added as a new revision thus not invalidating existing signature</param>
        /// <param name="reason">Reason field of signature</param>
        /// <param name="location">Location field of signature</param>
        /// <param name="contact">Contact field of signature</param>
        /// <param name="tsaClientUrl">Timestamp server URL</param>
        /// <param name="tsaClientLogin">Login user for timestamp server (BASIC authentication)</param>
        /// <param name="tsaClientPwd">Login password for timestamp server</param>
        public static void SignPdf(X509Certificate2 certificate, Stream inputPdfStream, Stream outputPdfStream, bool append, string reason, string location, string contact, string tsaClientUrl, string tsaClientLogin, string tsaClientPwd)
        {
            // Building certificate chain for the OCSP verification
            X509Chain ch = new X509Chain();
            ch.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
            ch.Build(certificate);

            Org.BouncyCastle.X509.X509Certificate[] chain = new Org.BouncyCastle.X509.X509Certificate[ch.ChainElements.Count];
            Org.BouncyCastle.X509.X509CertificateParser cp = new Org.BouncyCastle.X509.X509CertificateParser();
            int i = 0;

            foreach (X509ChainElement element in ch.ChainElements)
            {
                Org.BouncyCastle.X509.X509Certificate chainElement = cp.ReadCertificate(element.Certificate.RawData);
                chain[i] = chainElement;
                i++;
            }

            //Org.BouncyCastle.X509.X509CertificateParser cp = new Org.BouncyCastle.X509.X509CertificateParser();
            //Org.BouncyCastle.X509.X509Certificate[] chain = new Org.BouncyCastle.X509.X509Certificate[] { cp.ReadCertificate(certificate.RawData) };

            PdfReader reader = new PdfReader(inputPdfStream);
            PdfStamper st = PdfStamper.CreateSignature(reader, outputPdfStream, '\0', null, append);
            PdfSignatureAppearance sap = st.SignatureAppearance;

            sap.SetCrypto(null, chain, null, PdfSignatureAppearance.SELF_SIGNED);

            sap.Reason = reason;
            sap.Contact = contact;
            sap.Location = location;

            sap.SetVisibleSignature(new iTextSharp.text.Rectangle(50, 50, 200, 100), 1, null);

            PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKLITE, new PdfName("adbe.pkcs7.detached"));
            dic.Reason = sap.Reason;
            dic.Location = sap.Location;
            dic.Contact = sap.Contact;
            dic.Date = new PdfDate(sap.SignDate);
            sap.CryptoDictionary = dic;

            int contentEstimated = 15000;

            Dictionary<PdfName, int> exc = new Dictionary<PdfName, int>();
            exc[PdfName.CONTENTS] = contentEstimated * 2 + 2;
            sap.PreClose(exc);

            PdfPKCS7 sgn = new PdfPKCS7(null, chain, null, "SHA1", false);
            IDigest messageDigest = DigestUtilities.GetDigest("SHA1");
            Stream data = sap.RangeStream;
            byte[] buf = new byte[8192];
            int n;
            while ((n = data.Read(buf, 0, buf.Length)) > 0)
            {
                messageDigest.BlockUpdate(buf, 0, n);
            }
            byte[] hash = new byte[messageDigest.GetDigestSize()];
            messageDigest.DoFinal(hash, 0);
            DateTime cal = DateTime.Now;
            byte[] ocsp = null;
            if (chain.Length >= 2)
            {
                String url = PdfPKCS7.GetOCSPURL(chain[0]);
                if (url != null && url.Length > 0)
                    ocsp = new OcspClientBouncyCastle(chain[0], chain[1], url).GetEncoded();

                //File.WriteAllBytes(@"..\..\..\ocsp.pdf", ocsp);
            }
            byte[] sh = sgn.GetAuthenticatedAttributeBytes(hash, cal, ocsp);

            // SHA1withRSA calculated by CAPI
            byte[] signedHashValue = SignSHA1withRSA(certificate, sh);
            sgn.SetExternalDigest(signedHashValue, hash, "RSA");

            byte[] paddedSig = new byte[contentEstimated];

            if (!string.IsNullOrEmpty(tsaClientUrl))
            {
                TSAClientBouncyCastle tsc = new TSAClientBouncyCastle(tsaClientUrl, tsaClientLogin, tsaClientPwd);
                byte[] encodedSigTsa = sgn.GetEncodedPKCS7(hash, cal, tsc, ocsp);
                System.Array.Copy(encodedSigTsa, 0, paddedSig, 0, encodedSigTsa.Length);
                if (contentEstimated + 2 < encodedSigTsa.Length)
                    throw new ApplicationException("Not enough space for signature");
            }
            else
            {
                byte[] encodedSig = sgn.GetEncodedPKCS7(hash, cal, null, ocsp);
                System.Array.Copy(encodedSig, 0, paddedSig, 0, encodedSig.Length);
                if (contentEstimated + 2 < encodedSig.Length)
                    throw new ApplicationException("Not enough space for signature");
            }

            PdfDictionary dic2 = new PdfDictionary();
            dic2.Put(PdfName.CONTENTS, new PdfString(paddedSig).SetHexWriting(true));
            sap.Close(dic2);
        }

        public static byte[] SignSHA1withRSA(X509Certificate2 certificate, byte[] input)
        {
            const Int32 CRYPT_ACQUIRE_USE_PROV_INFO_FLAG = 0x00000002;
            const Int32 CRYPT_ACQUIRE_COMPARE_KEY_FLAG = 0x00000004;

            IntPtr privateKeyHandle = IntPtr.Zero;
            bool isCallerNeedFreeKeyHandle = false;
            IntPtr hashHandle = IntPtr.Zero;
            byte[] result = null;

            try
            {
                IntPtr cardHandle = certificate.Handle;
                Int32 pdwKeySpec = Crypto.AT_SIGNATURE;

                if (!Crypto.CryptAcquireCertificatePrivateKey(cardHandle, CRYPT_ACQUIRE_USE_PROV_INFO_FLAG |
                                                     CRYPT_ACQUIRE_COMPARE_KEY_FLAG, IntPtr.Zero, ref privateKeyHandle, ref pdwKeySpec, ref isCallerNeedFreeKeyHandle))
                {
                    throw new CryptographicException(Marshal.GetLastWin32Error());
                }

                if (!Crypto.CryptCreateHash(privateKeyHandle, Crypto.CALG_SHA1, IntPtr.Zero, 0, ref hashHandle))
                {
                    throw new CryptographicException(Marshal.GetLastWin32Error());
                }

                MemoryStream streamInput = new MemoryStream(input);
                byte[] buffer = new byte[4096];

                while (true)
                {
                    int read = streamInput.Read(buffer, 0, buffer.Length);

                    if (read == 0)
                        break;

                    if (!Crypto.CryptHashData(hashHandle, buffer, read, 0))
                    {
                        throw new CryptographicException(Marshal.GetLastWin32Error());
                    }
                }

                int pwdSigLen = 0;
                if (!Crypto.CryptSignHash(hashHandle, pdwKeySpec, null, 0, null, ref pwdSigLen))
                {
                    throw new CryptographicException(Marshal.GetLastWin32Error());
                }

                result = new byte[pwdSigLen];

                if (!Crypto.CryptSignHash(hashHandle, pdwKeySpec, null, 0, result, ref pwdSigLen))
                {
                    throw new CryptographicException(Marshal.GetLastWin32Error());
                }

                // CAPI generated hash has a different indian order
                Array.Reverse(result);
            }
            finally
            {

                if (hashHandle != IntPtr.Zero)
                {
                    if (!Crypto.CryptDestroyHash(hashHandle))
                    {
                        throw new CryptographicException(Marshal.GetLastWin32Error());
                    }
                }

                if (isCallerNeedFreeKeyHandle && privateKeyHandle != IntPtr.Zero)
                {
                    if (!Crypto.CryptReleaseContext(privateKeyHandle, 0))
                    {
                        throw new CryptographicException(Marshal.GetLastWin32Error());
                    }
                }
            }

            return result;

        }

        public static X509Certificate2 SelectCertificate()
        {
            X509Store st = new X509Store(StoreName.My, StoreLocation.CurrentUser);
            st.Open(OpenFlags.ReadOnly);
            X509Certificate2Collection col = st.Certificates.Find(X509FindType.FindByKeyUsage, X509KeyUsageFlags.NonRepudiation, true);

            X509Certificate2 card = null;
            X509Certificate2Collection sel = X509Certificate2UI.SelectFromCollection(col, "Certificates", "Select one to sign", X509SelectionFlag.SingleSelection);
            if (sel.Count > 0)
            {
                X509Certificate2Enumerator en = sel.GetEnumerator();
                en.MoveNext();
                card = en.Current;
            }
            st.Close();
            return card;
        }
    }
}

Vélemény? »

Vélemény?

RSS hírcsatorna a bejegyzéshez kapcsolódó hozzászólásokról. TrackBack URI

MINDEN VÉLEMÉNY SZÁMÍT!

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 / Módosítás )

Twitter kép

You are commenting using your Twitter account. Log Out / Módosítás )

Facebook kép

You are commenting using your Facebook account. Log Out / Módosítás )

Kapcsolódás: %s

Sablon: Shocking Blue Green. Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.