Стандартный механизм в .NET по подписи через
SignedCms, весьма симпатичный, но есть одна проблема: для подписи необходимо передать массив байт, т.е. если надо подписать большой файл, то вначале прочитать в память в .NET потом засунуть этот же объём системному API, что весьма затратно по памяти.
Конечно, в CryptoPro есть замечательный метод
SignHash, который позволяет заранее посчитать хеш и его подписать (что идентично подписи самого файла), но это только у CryptoPro, стандартное API даже на низком уровне не позволяет провести такое.
Гугление выдало
код, который может шифровать в потоковом режиме, но у него обнаружилось несколько проблем:
- Почему-то используется FileStream, хотя достаточно просто Stream
- Реально всё равно читается всё содержимое (!) а потом подписывается в потоковом режиме
- Нет поддержки Detached подписи, хотя добавить её несложно
- Внаглую из сертификата берётся приватный ключ, который опять же считается RSA (наивные! совершенно не знают что в России творится)
В общем, я поправил этот код, и выкладываю его здесь, ибо взял из блога исходники, надо их таким же образом вернуть, чтобы людям было приятно.
Код, представляет из себя компиляцию того что было, с тем что надо, по-хорошему его надо причесать, но для демонстрации возможностей, пойдёт.
StreamCms.cs
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Runtime.InteropServices;
using System.ComponentModel;
namespace NS1
{
[ComVisible(false)]
public class StreamCms
{
// File stream to use in callback function
private Stream _callbackFile;
// Streaming callback function for encoding
private bool StreamOutputCallback(IntPtr pvArg, IntPtr pbData, int cbData, bool fFinal)
{
if (cbData == 0) return true;
// Write all bytes to encoded file
var bytes = new byte[cbData];
Marshal.Copy(pbData, bytes, 0, cbData);
_callbackFile.Write(bytes, 0, cbData);
if (fFinal)
{
// This is the last piece. Close the file
_callbackFile.Flush();
_callbackFile.Close();
_callbackFile = null;
}
return true;
}
// Encode StreamCms with streaming to support large data
public void Encode(X509Certificate2 cert, Stream inFile, Stream outFile, bool isDetached)
{
// Variables
IntPtr hProv = IntPtr.Zero;
IntPtr SignerInfoPtr = IntPtr.Zero;
IntPtr CertBlobsPtr = IntPtr.Zero;
IntPtr hMsg = IntPtr.Zero;
try
{
// Prepare stream for encoded info
_callbackFile = outFile;
// Get cert chain
var chain = new X509Chain();
chain.Build(cert);
var chainElements = new X509ChainElement[chain.ChainElements.Count];
chain.ChainElements.CopyTo(chainElements, 0);
// Get certs in chain
var certs = new X509Certificate2[chainElements.Length];
for (int i = 0; i < chainElements.Length; i++)
{
certs[i] = chainElements[i].Certificate;
}
// Get context of all certs in chain
var CertContexts = new Win32.CERT_CONTEXT[certs.Length];
for (int i = 0; i < certs.Length; i++)
{
CertContexts[i] = (Win32.CERT_CONTEXT)Marshal.PtrToStructure(certs[i].Handle, typeof(Win32.CERT_CONTEXT));
}
// Get cert blob of all certs
var CertBlobs = new Win32.BLOB[CertContexts.Length];
for (int i = 0; i < CertContexts.Length; i++)
{
CertBlobs[i].cbData = CertContexts[i].cbCertEncoded;
CertBlobs[i].pbData = CertContexts[i].pbCertEncoded;
}
// Get CSP of client certificate
Win32.CRYPT_KEY_PROV_INFO csp;
GetPrivateKeyInfo(GetCertContext(cert), out csp);
bool bResult = Win32.CryptAcquireContext(
ref hProv,
csp.pwszContainerName,
csp.pwszProvName,
(int)csp.dwProvType,
0);
if (!bResult)
{
throw new Exception("CryptAcquireContext error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
}
// Populate Signer Info struct
var SignerInfo = new Win32.CMSG_SIGNER_ENCODE_INFO();
SignerInfo.cbSize = Marshal.SizeOf(SignerInfo);
SignerInfo.pCertInfo = CertContexts[0].pCertInfo;
SignerInfo.hCryptProvOrhNCryptKey = hProv;
SignerInfo.dwKeySpec = (int)csp.dwKeySpec;
SignerInfo.HashAlgorithm.pszObjId = cert.SignatureAlgorithm.Value; // Win32.szOID_OIWSEC_sha1;
// Populate Signed Info struct
var SignedInfo = new Win32.CMSG_SIGNED_ENCODE_INFO();
SignedInfo.cbSize = Marshal.SizeOf(SignedInfo);
SignedInfo.cSigners = 1;
SignerInfoPtr = Marshal.AllocHGlobal(Marshal.SizeOf(SignerInfo));
Marshal.StructureToPtr(SignerInfo, SignerInfoPtr, false);
SignedInfo.rgSigners = SignerInfoPtr;
SignedInfo.cCertEncoded = CertBlobs.Length;
CertBlobsPtr = Marshal.AllocHGlobal(Marshal.SizeOf(CertBlobs[0]) * CertBlobs.Length);
for (int i = 0; i < CertBlobs.Length; i++)
{
Marshal.StructureToPtr(CertBlobs[i], new IntPtr(CertBlobsPtr.ToInt64() + (Marshal.SizeOf(CertBlobs[i]) * i)), false);
}
SignedInfo.rgCertEncoded = CertBlobsPtr;
// Populate Stream Info struct
var StreamInfo = new Win32.CMSG_STREAM_INFO
{
cbContent = (int)inFile.Length,
pfnStreamOutput = StreamOutputCallback
};
// Open message to encode
hMsg = Win32.CryptMsgOpenToEncode(
Win32.X509_ASN_ENCODING | Win32.PKCS_7_ASN_ENCODING,
isDetached ? Win32.CMSG_DETACHED_FLAG : 0,
Win32.CMSG_SIGNED,
ref SignedInfo,
null,
ref StreamInfo);
if (hMsg.Equals(IntPtr.Zero))
{
throw new Exception("CryptMsgOpenToEncode error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
}
// Process the whole message
ProcessMessage(hMsg, inFile);
}
finally
{
// Clean up
if (inFile != null)
{
inFile.Close();
}
if (_callbackFile != null)
{
_callbackFile.Close();
}
if (!CertBlobsPtr.Equals(IntPtr.Zero))
{
Marshal.FreeHGlobal(CertBlobsPtr);
}
if (!SignerInfoPtr.Equals(IntPtr.Zero))
{
Marshal.FreeHGlobal(SignerInfoPtr);
}
if (!hProv.Equals(IntPtr.Zero))
{
Win32.CryptReleaseContext(hProv, 0);
}
if (!hMsg.Equals(IntPtr.Zero))
{
Win32.CryptMsgClose(hMsg);
}
}
}
// Decode StreamCms with streaming to support large data
public void Decode(Stream dataFile, Stream signFile, Stream outFile, bool isDetached)
{
// Variables
IntPtr hMsg = IntPtr.Zero;
IntPtr pSignerCertInfo = IntPtr.Zero;
IntPtr pSignerCertContext = IntPtr.Zero;
IntPtr hStore = IntPtr.Zero;
try
{
// Get data to decode
// Prepare stream for decoded info
_callbackFile = outFile;
// Populate Stream Info struct
var StreamInfo = new Win32.CMSG_STREAM_INFO
{
cbContent = (int)signFile.Length,
pfnStreamOutput = StreamOutputCallback
};
// Open message to decode
hMsg = Win32.CryptMsgOpenToDecode(
Win32.X509_ASN_ENCODING | Win32.PKCS_7_ASN_ENCODING,
isDetached ? Win32.CMSG_DETACHED_FLAG : 0,
0,
IntPtr.Zero,
IntPtr.Zero,
ref StreamInfo);
if (hMsg.Equals(IntPtr.Zero))
{
throw new Exception("CryptMsgOpenToDecode error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
}
// Process the whole message
if (isDetached)
{
ProcessMessage(hMsg, signFile);
ProcessMessage(hMsg, dataFile);
}
else
{
ProcessMessage(hMsg, signFile);
}
// Get signer certificate info
int cbSignerCertInfo = 0;
bool bResult = Win32.CryptMsgGetParam(
hMsg,
Win32.CMSG_SIGNER_CERT_INFO_PARAM,
0,
IntPtr.Zero,
ref cbSignerCertInfo);
if (!bResult)
{
throw new Exception("CryptMsgGetParam error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
}
pSignerCertInfo = Marshal.AllocHGlobal(cbSignerCertInfo);
bResult = Win32.CryptMsgGetParam(
hMsg,
Win32.CMSG_SIGNER_CERT_INFO_PARAM,
0,
pSignerCertInfo,
ref cbSignerCertInfo);
if (!bResult)
{
throw new Exception("CryptMsgGetParam error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
}
// Open a cert store in memory with the certs from the message
hStore = Win32.CertOpenStore(
Win32.CERT_STORE_PROV_MSG,
Win32.X509_ASN_ENCODING | Win32.PKCS_7_ASN_ENCODING,
IntPtr.Zero,
0,
hMsg);
if (hStore.Equals(IntPtr.Zero))
{
throw new Exception("CertOpenStore error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
}
// Find the signer's cert in the store
pSignerCertContext = Win32.CertGetSubjectCertificateFromStore(
hStore,
Win32.X509_ASN_ENCODING | Win32.PKCS_7_ASN_ENCODING,
pSignerCertInfo);
if (pSignerCertContext.Equals(IntPtr.Zero))
{
throw new Exception("CertGetSubjectCertificateFromStore error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
}
// Set message for verifying
var SignerCertContext = (Win32.CERT_CONTEXT)Marshal.PtrToStructure(pSignerCertContext, typeof(Win32.CERT_CONTEXT));
bResult = Win32.CryptMsgControl(
hMsg,
0,
Win32.CMSG_CTRL_VERIFY_SIGNATURE,
SignerCertContext.pCertInfo);
if (!bResult)
{
throw new Exception("CryptMsgControl error #" + Marshal.GetLastWin32Error().ToString(), new Win32Exception(Marshal.GetLastWin32Error()));
}
}
finally
{
// Clean up
if (!pSignerCertContext.Equals(IntPtr.Zero))
{
Win32.CertFreeCertificateContext(pSignerCertContext);
}
if (!pSignerCertInfo.Equals(IntPtr.Zero))
{
Marshal.FreeHGlobal(pSignerCertInfo);
}
if (!hStore.Equals(IntPtr.Zero))
{
Win32.CertCloseStore(hStore, Win32.CERT_CLOSE_STORE_FORCE_FLAG);
}
if (dataFile != null)
{
dataFile.Close();
}
if (signFile != null)
{
signFile.Close();
}
if (_callbackFile != null)
{
_callbackFile.Close();
}
if (!hMsg.Equals(IntPtr.Zero))
{
Win32.CryptMsgClose(hMsg);
}
}
}
private void ProcessMessage(IntPtr hMsg, Stream dataStream)
{
long streamSize = dataStream.Length;
if (streamSize == 0)
throw new CryptographicException("Cannot encode zero length data");
var gchandle = new GCHandle();
const int ChunkSize = 1024 * 1024;
var dwSize = (int)((streamSize < ChunkSize) ? streamSize : ChunkSize);
var pbData = new byte[dwSize];
try
{
var dwRemaining = streamSize;
gchandle = GCHandle.Alloc(pbData, GCHandleType.Pinned);
var pbPtr = gchandle.AddrOfPinnedObject();
while (dwRemaining > 0)
{
dataStream.Read(pbData, 0, dwSize);
// Update message piece by piece
var bResult = Win32.CryptMsgUpdate(hMsg, pbPtr, dwSize, (dwRemaining <= dwSize));
if (!bResult)
{
throw new Exception(
"CryptMsgUpdate error #" + Marshal.GetLastWin32Error().ToString(),
new Win32Exception(Marshal.GetLastWin32Error()));
}
dwRemaining -= dwSize;
if (dwRemaining < dwSize)
dwSize = (int)dwRemaining;
// if (gchandle.IsAllocated)
// gchandle.Free();
}
}
finally
{
if (gchandle.IsAllocated)
{
gchandle.Free();
}
}
}
internal static Win32.CertHandle GetCertContext(X509Certificate2 certificate)
{
var handle = Win32.CertDuplicateCertificateContext(certificate.Handle);
GC.KeepAlive(certificate);
return handle;
}
internal static bool GetPrivateKeyInfo(Win32.CertHandle safeCertContext, out Win32.CRYPT_KEY_PROV_INFO parameters)
{
parameters = new Win32.CRYPT_KEY_PROV_INFO();
var invalidHandle = new Win32.SafeHandle(IntPtr.Zero);
uint pcbData = 0;
if (!Win32.CertGetCertificateContextProperty(safeCertContext, 2, invalidHandle.DangerousGetHandle(), ref pcbData))
{
if (Marshal.GetLastWin32Error() != -2146885628)
{
throw new CryptographicException(Marshal.GetLastWin32Error());
}
return false;
}
invalidHandle = Win32.LocalAlloc(0, new IntPtr(pcbData));
if (!Win32.CertGetCertificateContextProperty(safeCertContext, 2, invalidHandle.DangerousGetHandle(), ref pcbData))
{
if (Marshal.GetLastWin32Error() != -2146885628)
{
throw new CryptographicException(Marshal.GetLastWin32Error());
}
return false;
}
parameters = (Win32.CRYPT_KEY_PROV_INFO)Marshal.PtrToStructure(invalidHandle.DangerousGetHandle(), typeof(Win32.CRYPT_KEY_PROV_INFO));
invalidHandle.Dispose();
return true;
}
}
}
Win32.cs
using System;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
namespace NS1
{
[ComVisible(false)]
public class Win32
{
#region "CONSTS"
public const int X509_ASN_ENCODING = 0x00000001;
public const int PKCS_7_ASN_ENCODING = 0x00010000;
public const int CMSG_SIGNED = 2;
public const int CMSG_DETACHED_FLAG = 0x00000004;
public const int AT_KEYEXCHANGE = 1;
public const int AT_SIGNATURE = 2;
// public const string szOID_OIWSEC_sha1 = "1.3.14.3.2.26";
public const int CMSG_CTRL_VERIFY_SIGNATURE = 1;
public const int CMSG_CERT_PARAM = 12;
public const int CMSG_SIGNER_CERT_INFO_PARAM = 7;
public const int CERT_STORE_PROV_MSG = 1;
public const int CERT_CLOSE_STORE_FORCE_FLAG = 1;
#endregion
#region "STRUCTS"
[StructLayout(LayoutKind.Sequential)]
[ComVisible(false)]
public struct CRYPT_ALGORITHM_IDENTIFIER
{
public string pszObjId;
public BLOB Parameters;
}
[StructLayout(LayoutKind.Sequential)]
[ComVisible(false)]
public struct CERT_ID
{
public int dwIdChoice;
public BLOB IssuerSerialNumberOrKeyIdOrHashId;
}
[StructLayout(LayoutKind.Sequential)]
[ComVisible(false)]
public struct CMSG_SIGNER_ENCODE_INFO
{
public int cbSize;
public IntPtr pCertInfo;
public IntPtr hCryptProvOrhNCryptKey;
public int dwKeySpec;
public CRYPT_ALGORITHM_IDENTIFIER HashAlgorithm;
public IntPtr pvHashAuxInfo;
public int cAuthAttr;
public IntPtr rgAuthAttr;
public int cUnauthAttr;
public IntPtr rgUnauthAttr;
public CERT_ID SignerId;
public CRYPT_ALGORITHM_IDENTIFIER HashEncryptionAlgorithm;
public IntPtr pvHashEncryptionAuxInfo;
}
[StructLayout(LayoutKind.Sequential)]
[ComVisible(false)]
public struct CERT_CONTEXT
{
public int dwCertEncodingType;
public IntPtr pbCertEncoded;
public int cbCertEncoded;
public IntPtr pCertInfo;
public IntPtr hCertStore;
}
[StructLayout(LayoutKind.Sequential)]
[ComVisible(false)]
public struct BLOB
{
public int cbData;
public IntPtr pbData;
}
[StructLayout(LayoutKind.Sequential)]
[ComVisible(false)]
public struct CMSG_SIGNED_ENCODE_INFO
{
public int cbSize;
public int cSigners;
public IntPtr rgSigners;
public int cCertEncoded;
public IntPtr rgCertEncoded;
public int cCrlEncoded;
public IntPtr rgCrlEncoded;
public int cAttrCertEncoded;
public IntPtr rgAttrCertEncoded;
}
[StructLayout(LayoutKind.Sequential)]
[ComVisible(false)]
public struct CMSG_STREAM_INFO
{
public int cbContent;
public StreamOutputCallbackDelegate pfnStreamOutput;
public IntPtr pvArg;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
[ComVisible(false)]
internal struct CRYPT_KEY_PROV_INFO
{
internal string pwszContainerName;
internal string pwszProvName;
internal uint dwProvType;
internal uint dwFlags;
internal uint cProvParam;
internal IntPtr rgProvParam;
internal uint dwKeySpec;
}
#endregion
#region "DELEGATES"
[ComVisible(false)]
public delegate bool StreamOutputCallbackDelegate(IntPtr pvArg, IntPtr pbData, int cbData, Boolean fFinal);
#endregion
#region "API"
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[ComVisible(false)]
public static extern Boolean CryptAcquireContext(
ref IntPtr hProv,
String pszContainer,
String pszProvider,
int dwProvType,
int dwFlags);
[DllImport("Crypt32.dll", SetLastError = true)]
[ComVisible(false)]
public static extern IntPtr CryptMsgOpenToEncode(
int dwMsgEncodingType,
int dwFlags,
int dwMsgType,
ref CMSG_SIGNED_ENCODE_INFO pvMsgEncodeInfo,
String pszInnerContentObjID,
ref CMSG_STREAM_INFO pStreamInfo);
[DllImport("Crypt32.dll", SetLastError = true)]
[ComVisible(false)]
public static extern IntPtr CryptMsgOpenToDecode(
int dwMsgEncodingType,
int dwFlags,
int dwMsgType,
IntPtr hCryptProv,
IntPtr pRecipientInfo,
ref CMSG_STREAM_INFO pStreamInfo);
[DllImport("Crypt32.dll", SetLastError = true)]
[ComVisible(false)]
public static extern Boolean CryptMsgClose(IntPtr hCryptMsg);
[DllImport("Crypt32.dll", SetLastError = true)]
[ComVisible(false)]
public static extern Boolean CryptMsgUpdate(
IntPtr hCryptMsg,
Byte[] pbData,
int cbData,
Boolean fFinal);
[DllImport("Crypt32.dll", SetLastError = true)]
[ComVisible(false)]
public static extern Boolean CryptMsgUpdate(
IntPtr hCryptMsg,
IntPtr pbData,
int cbData,
Boolean fFinal);
[DllImport("Crypt32.dll", SetLastError = true)]
[ComVisible(false)]
public static extern Boolean CryptMsgGetParam(
IntPtr hCryptMsg,
int dwParamType,
int dwIndex,
IntPtr pvData,
ref int pcbData);
[DllImport("Crypt32.dll", SetLastError = true)]
[ComVisible(false)]
public static extern Boolean CryptMsgControl(
IntPtr hCryptMsg,
int dwFlags,
int dwCtrlType,
IntPtr pvCtrlPara);
[DllImport("advapi32.dll", SetLastError = true)]
[ComVisible(false)]
public static extern Boolean CryptReleaseContext(
IntPtr hProv,
int dwFlags);
[DllImport("Crypt32.dll", SetLastError = true)]
[ComVisible(false)]
public static extern IntPtr CertCreateCertificateContext(
int dwCertEncodingType,
IntPtr pbCertEncoded,
int cbCertEncoded);
[DllImport("Crypt32.dll", SetLastError = true)]
[ComVisible(false)]
public static extern bool CertFreeCertificateContext(IntPtr pCertContext);
[DllImport("Crypt32.dll", SetLastError = true)]
[ComVisible(false)]
public static extern IntPtr CertOpenStore(
int lpszStoreProvider,
int dwMsgAndCertEncodingType,
IntPtr hCryptProv,
int dwFlags,
IntPtr pvPara);
[DllImport("Crypt32.dll", SetLastError = true)]
[ComVisible(false)]
public static extern IntPtr CertGetSubjectCertificateFromStore(
IntPtr hCertStore,
int dwCertEncodingType,
IntPtr pCertId);
[DllImport("Crypt32.dll", SetLastError = true)]
[ComVisible(false)]
public static extern IntPtr CertCloseStore(
IntPtr hCertStore,
int dwFlags);
[DllImport("crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern CertHandle CertDuplicateCertificateContext([In] IntPtr pCertContext);
[DllImport("crypt32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern bool CertGetCertificateContextProperty([In] CertHandle pCertContext, [In] uint dwPropId, [In, Out] IntPtr pvData, [In, Out] ref uint pcbData);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern SafeHandle LocalAlloc([In] uint uFlags, [In] IntPtr sizetdwBytes);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr LocalFree(IntPtr handle);
#endregion
public class SafeHandle : SafeHandleZeroOrMinusOneIsInvalid
{
public SafeHandle(IntPtr handle) : base(true)
{
SetHandle(handle);
}
public SafeHandle()
: base(true)
{
}
protected override bool ReleaseHandle()
{
return (LocalFree(handle) == IntPtr.Zero);
}
}
public class CertHandle : SafeHandleZeroOrMinusOneIsInvalid
{
public CertHandle()
: base(true)
{
}
public CertHandle(bool ownsHandle)
: base(ownsHandle)
{
}
protected override bool ReleaseHandle()
{
return CertFreeCertificateContext(handle);
}
}
}
}