Стандартный механизм в .NET по подписи через SignedCms, весьма симпатичный, но есть одна проблема: для подписи необходимо передать массив байт, т.е. если надо подписать большой файл, то вначале прочитать в память в .NET потом засунуть этот же объём системному API, что весьма затратно по памяти.
Конечно, в CryptoPro есть замечательный метод SignHash, который позволяет заранее посчитать хеш и его подписать (что идентично подписи самого файла), но это только у CryptoPro, стандартное 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); } } } }