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;
}
}
}