Em conexão com a transição para o Linux, tornou-se necessário portar um de nossos sistemas de servidor escritos em C # para Mono. O sistema trabalha com assinaturas digitais aprimoradas; portanto, uma das tarefas que enfrentamos foi verificar o desempenho dos certificados GOST do CryptoPro em mono. O próprio CryptoPro implementou o CSP para Linux há algum tempo, mas a primeira tentativa de usá-lo mostrou que as classes de criptografia Mono nativas (semelhantes às da base .Net - X509Store, X509Certificate2, etc.) não apenas não funcionam com chaves de convidado, mas também não os veja em seus cofres. Devido a isso, o trabalho com criptografia teve que ser conectado diretamente através das bibliotecas CryptoPro.
Instalação de certificado
Antes de implementar o código, você deve instalar o certificado e garantir que ele funcione corretamente.
Instalação de certificadoO componente CryptoPro CSP versão 3.9 foi instalado no Centos 7 na pasta / opt / cprocsp. Para evitar conflitos entre os utilitários mono e CryptoPro com o mesmo nome (por exemplo, certmgr), o caminho para a pasta não foi inserido nas variáveis de ambiente e todos os utilitários foram chamados no caminho completo.
Primeiro, definimos uma lista de leitores:
/opt/cprocsp/bin/amd64/csptest -enum -info -type PP_ENUMREADERS | iconv -f cp1251
Se não houver um leitor da pasta no disco (HDIMAGE) na lista, coloque-o:
/opt/cprocsp/sbin/amd64/cpconfig -hardware reader -add HDIMAGE store
Em seguida, você pode criar contêineres no formato '\\. \ HDIMAGE \ {container name}', criando um novo contêiner com as chaves:
/opt/cprocsp/bin/amd64/csptest -keyset -provtype 75 -newkeyset -cont '\\.\HDIMAGE\test'
ou criando a pasta / var / opt / cprocsp / keys / root / {nome do contêiner} .000, que contém o conjunto padrão de arquivos de contêiner CryptoPro (* .key, * .mask etc.).
Depois disso, o certificado do contêiner pode ser instalado no armazenamento de certificados:
/opt/cprocsp/bin/amd64/certmgr -inst mMy -cont '\\.\HDIMAGE\{ }'
O certificado instalado pode ser visto com o seguinte comando:
/opt/cprocsp/bin/amd64/certmgr -list mMy
A operação do certificado pode ser verificada da seguinte maneira:
/opt/cprocsp/bin/amd64/cryptcp – sign -norev -thumbprint {} {} { }
/opt/cprocsp/bin/amd64/cryptcp – verify -norev { }
Se tudo estiver bem com o certificado, você poderá prosseguir para a conexão no código.
Conexão no código
Apesar do processo de portar para o Linux, o sistema deveria continuar funcionando no ambiente Windows; portanto, externamente, o trabalho com criptografia tinha que ser realizado através de métodos gerais no formato “byte [] SignData (byte [] _arData, X509Certificate2 _pCert)”, que deveria funcionar igualmente como no Linux e no Windows.
Uma análise dos métodos das bibliotecas de criptografia resultou bem-sucedida, porque o CryptoPro implementou a biblioteca “libcapi20.so”, que imita completamente as bibliotecas de criptografia padrão do Windows - “crypt32.dll” e “advapi32.dll”. Talvez, é claro, não inteiramente, mas todos os métodos necessários para trabalhar com criptografia estejam disponíveis lá, e quase todo o trabalho.
Portanto, formamos duas classes estáticas “WCryptoAPI” e “LCryptoAPI”, cada uma das quais importará o conjunto de métodos necessário da seguinte maneira:
[DllImport(LIBCAPI20, SetLastError = true)] internal static extern bool CertCloseStore(IntPtr _hCertStore, uint _iFlags);
A sintaxe de conexão de cada um dos métodos pode ser criada independentemente, ou usar o site pinvoke , ou copiar de fontes .Net (classe CAPISafe ). No mesmo módulo, você pode desenhar constantes e estruturas associadas à criptografia, cuja presença sempre facilita a vida ao trabalhar com bibliotecas externas.
E então formamos a classe estática “UCryptoAPI” que, dependendo do sistema, chamará o método de uma das duas classes:
internal static bool CertCloseStore(IntPtr _hCertStore, uint _iFlags) { if (fIsLinux) return LCryptoAPI.CertCloseStore(_hCertStore, _iFlags); else return WCryptoAPI.CertCloseStore(_hCertStore, _iFlags); } public static bool fIsLinux { get { int iPlatform = (int) Environment.OSVersion.Platform; return (iPlatform == 4) || (iPlatform == 6) || (iPlatform == 128); } }
Portanto, usando os métodos da classe UCryptoAPI, você pode implementar código quase uniforme para os dois sistemas.
Pesquisa de certificado
O trabalho com criptografia geralmente começa com uma pesquisa de certificado; para isso, no crypt32.dll, existem dois métodos CertOpenStore (abre o armazenamento de certificados especificado) e um CertOpenSystemStore simples (abre os certificados pessoais do usuário). Como o trabalho com certificados não se limita aos certificados de usuário pessoal, conectamos o primeiro:
Pesquisa de certificado public static int FindCertificateCP(string _pFindValue, out X509Certificate2 _pCert, ref string _sError, StoreLocation _pLocation = StoreLocation.CurrentUser, StoreName _pName = StoreName.My, X509FindType _pFindType = X509FindType.FindByThumbprint, bool _fVerify = false) { _pCert = null; IntPtr hCert = IntPtr.Zero; GCHandle hInternal = new GCHandle(); GCHandle hFull = new GCHandle(); IntPtr hSysStore = IntPtr.Zero; try {
A pesquisa ocorre em várias etapas:
- abertura de armazenamento;
- a formação da estrutura de dados pela qual estamos procurando
- pesquisa de certificado;
- se necessário, a verificação do certificado (descrita em uma seção separada);
- fechando o repositório e liberando a estrutura do ponto 2 (já que em todos os lugares há trabalho com memória .net não gerenciada, nada será feito para que possamos limpá-lo);
Existem alguns pontos sutis ao procurar certificados.
O CryptoPro no Linux funciona com seqüências de caracteres ANSI e no Windows com UTF8, portanto:
- ao conectar o método de abertura de armazenamento no Linux, é necessário indicar explicitamente o tipo de empacotamento [In, MarshalAs (UnmanagedType.LPStr)] para o parâmetro de código de armazenamento;
- passando a string de pesquisa (por exemplo, pelo nome de Subject), ela deve ser convertida em um conjunto de bytes com codificações diferentes;
- para todas as constantes criptográficas que possuem variação pelo tipo de sequência (por exemplo, CERT_FIND_SUBJECT_STR_A e CERT_FIND_SUBJECT_STR_W) no Windows, você deve selecionar * _W e no Linux * _A;
O método MapX509StoreFlags pode ser obtido diretamente de fontes da Microsoft sem alterações, simplesmente forma uma máscara final com base nos sinalizadores .Net.
O valor pelo qual a pesquisa é realizada depende do tipo de pesquisa (verifique no MSDN o CertFindCertificateInStore ); o exemplo mostra as duas opções mais usadas - para o formato da string (nomes Assunto, Emissor, etc.) e binário (impressão digital, número de série).
O processo de criação de um certificado do IntPtr no Windows e Linux é muito diferente. O Windows criará o certificado de uma maneira simples:
new X509Certificate2(hCert);
no Linux, você precisa criar um certificado em duas etapas:
X509Certificate2(new X509Certificate(hCert));
No futuro, precisamos acessar o hCert for work e ele deverá ser armazenado no objeto de certificado. No Windows, ele pode ser recuperado posteriormente a partir da propriedade Handle, mas o Linux converte a estrutura CERT_CONTEXT que segue o link hCert em um link para a estrutura x509_st (OpenSSL) e o registra no Handle. Portanto, vale a pena criar um herdador do X509Certificate2 (ISDP_X509Cert no exemplo), que armazenará o hCert nos dois sistemas em um campo separado.
Não esqueça que este é um link para uma área de memória não gerenciada e deve ser liberada após o término do trabalho. Porque no .Net 4.5 X509Certificate2 não é descartável - a limpeza usando o método CertFreeCertificateContext deve ser executada no destruidor.
Formação de Assinaturas
Ao trabalhar com certificados GOST, quase sempre são usadas assinaturas desconectadas com um signatário. Para criar essa assinatura, é necessário um bloco de código bastante simples:
Formação de Assinaturas public static int SignDataCP(byte[] _arData, X509Certificate2 _pCert, out byte[] _arRes, ref string _sError) { _arRes = new byte[0];
Durante o trabalho do método, uma estrutura com parâmetros é formada e o método de assinatura é chamado. A estrutura de parâmetros pode permitir que você salve certificados na assinatura para formar uma cadeia completa (os campos cMsgCert e rgpMsgCert, o primeiro armazena o número de certificados e a segunda lista de links para as estruturas desses certificados).
O método de assinatura pode receber um ou mais documentos para assinatura simultânea com uma assinatura. A propósito, isso não contradiz a Lei Federal 63 e é muito conveniente, pois é improvável que o usuário fique satisfeito com a necessidade de clicar no botão "assinar" várias vezes.
A principal estranheza desse método é que ele não funciona no modo de duas chamadas, o que é típico da maioria dos métodos de biblioteca que trabalham com grandes blocos de memória (o primeiro com nulo - retorna o comprimento do buffer necessário, o segundo preenche o buffer). Portanto, é necessário criar um buffer grande e reduzi-lo ao seu tamanho real.
O único problema sério é a pesquisa do OID do algoritmo de hash (Digest) usado ao entrar - de forma explícita, não está no certificado (existe apenas o algoritmo da própria assinatura). E se no Windows você pode especificá-lo com uma string vazia - ele será ativado automaticamente, mas o Linux se recusará a assinar se o algoritmo não for o mesmo.
Mas há um truque - nas informações sobre o algoritmo de assinatura (estrutura CRYPT_OID_INFO), o OID da assinatura é armazenado no pszOID e o identificador do algoritmo de hash é armazenado no Algid. E converter Algid em OID já é uma questão técnica:
Obtendo o OID do algoritmo de hash internal static int GetHashAlgoritmOID(IntPtr _hCertHandle, out string _sOID, ref string _sError) { _sOID = ""; IntPtr hHashAlgInfo = IntPtr.Zero; IntPtr hData = IntPtr.Zero; try { CERT_CONTEXT pContext = (CERT_CONTEXT)Marshal.PtrToStructure(_hCertHandle, typeof(CERT_CONTEXT)); CERT_INFO pCertInfo = (CERT_INFO)Marshal.PtrToStructure(pContext.pCertInfo, typeof(CERT_INFO));
Após a leitura cuidadosa do código, você pode se surpreender ao descobrir que o identificador do algoritmo é obtido de maneira simples (CertOIDToAlgId) e o Oid é complicado (CryptFindOIDInfo). Seria lógico supor o uso de métodos complexos ou simples, e no Linux ambas as opções funcionam com êxito. No entanto, no Windows, a opção difícil de obter um identificador e simplesmente obter um OID é instável; portanto, um híbrido tão estranho seria uma solução estável.
Verificação de Assinatura
A verificação da assinatura ocorre em duas etapas, no início, a própria assinatura é verificada e, em seguida, o certificado com o qual foi gerada é verificado (cadeia, data de assinatura, etc.).
Além de assinar, você deve especificar o conjunto de dados a serem assinados, os parâmetros de assinatura e a própria assinatura:
Verificação de Assinatura internal static CRYPT_VERIFY_MESSAGE_PARA GetStdSignVerifyPar() { CRYPT_VERIFY_MESSAGE_PARA pVerifyParams = new CRYPT_VERIFY_MESSAGE_PARA(); pVerifyParams.cbSize = (int)Marshal.SizeOf(pVerifyParams); pVerifyParams.dwMsgEncodingType = UCConsts.PKCS_7_OR_X509_ASN_ENCODING; pVerifyParams.hCryptProv = 0; pVerifyParams.pfnGetSignerCertificate = IntPtr.Zero; pVerifyParams.pvGetArg = IntPtr.Zero; return pVerifyParams; } public static int CheckSignCP(byte[] _arData, byte[] _pSign, out X509Certificate2 _pCert, ref string _sError, bool _fVerifyOnlySign = true, X509RevocationMode _pRevMode = X509RevocationMode.Online, X509RevocationFlag _pRevFlag = X509RevocationFlag.ExcludeRoot){ _pCert = null; IntPtr pHData = Marshal.AllocHGlobal(_arData.Length); GCHandle pCertContext = GCHandle.Alloc(IntPtr.Zero, GCHandleType.Pinned); try { Marshal.Copy(_arData, 0, pHData, _arData.Length); CRYPT_VERIFY_MESSAGE_PARA pVerParam = UCUtils.GetStdSignVerifyPar();
Por conveniência, o processo de formação de uma estrutura com parâmetros foi movido para um método separado (GetStdSignVerifyPar). Depois disso, a própria assinatura é verificada e o primeiro signatário é extraído (para o bem, seria necessário extrair tudo, mas uma assinatura contendo vários signatários ainda é exótica).
Após extrair o certificado do assinante, o converteremos em nossa classe e o verificaremos (se especificado nos parâmetros do método). Para verificação, é usada a data de assinatura do primeiro signatário (consulte a seção sobre extração de informações da assinatura e a seção sobre verificação de certificado).
Extrair informações de assinatura
Geralmente, os sistemas criptográficos exigem uma representação impressa da assinatura. Em cada caso, é diferente, portanto, é melhor criar uma classe de informações sobre a assinatura, que conterá informações de uma forma conveniente para uso e, com sua ajuda, fornecerá uma apresentação impressa. No .Net existe essa classe - o SignedCms, no entanto, seu analógico em mono com as assinaturas do CritiPro pro se recusa a trabalhar no primeiro, em segundo lugar, contém o modificador selado e, em terceiro lugar, quase todas as suas propriedades são protegidas contra gravação, então você terá que criar seu próprio analógico.
A assinatura em si contém dois elementos principais - uma lista de certificados e uma lista de signatários. A lista de certificados pode estar vazia ou pode conter todos os certificados para verificação, incluindo cadeias completas. A lista de signatários indica o número de assinaturas reais. A comunicação entre eles é realizada por número de série e editor (emissor). Teoricamente, em uma assinatura, pode haver dois certificados de diferentes editores com o mesmo número de série, mas, na prática, isso pode ser negligenciado e pesquisado apenas pelo número de série.
A leitura da assinatura é a seguinte:
Extrair informações de assinatura public int Decode(byte[] _arSign, ref string _sError) { IntPtr hMsg = IntPtr.Zero;
A assinatura é analisada em vários estágios, primeiro a estrutura da mensagem (CryptMsgOpenToDecode) é formada e, em seguida, os dados reais da assinatura (CryptMsgUpdate) são inseridos nela. Resta verificar se essa é uma assinatura real e primeiro obter uma lista de certificados e depois uma lista de signatários. A lista de certificados é recuperada sequencialmente:
Obtendo uma lista de certificados internal static X509Certificate2Collection GetSignCertificates(IntPtr _hMsg) { X509Certificate2Collection certificates = new X509Certificate2Collection(); uint iCnt = GetCryptMsgParam<uint>(_hMsg, UCConsts.CMSG_CERT_COUNT_PARAM); for (int i = 0; i < iCnt; i++) { IntPtr hInfo = IntPtr.Zero; IntPtr hCert = IntPtr.Zero; try { uint iLen = 0; if (!GetCryptMsgParam(_hMsg, UCConsts.CMSG_CERT_PARAM, out hInfo, out iLen)) continue; hCert = UCryptoAPI.CertCreateCertificateContext(UCConsts.PKCS_7_OR_X509_ASN_ENCODING, hInfo, iLen); if (hCert != IntPtr.Zero) { certificates.Add(new ISDP_X509Cert(hCert)); hCert = IntPtr.Zero; } } finally { if (hInfo != IntPtr.Zero) Marshal.FreeHGlobal(hInfo); if (hInfo != IntPtr.Zero) Marshal.FreeHGlobal(hCert); } } return certificates; }
Primeiro, o número de certificados é determinado a partir do parâmetro CMSG_CERT_COUNT_PARAM e, em seguida, as informações sobre cada certificado são recuperadas seqüencialmente. O processo de criação do contexto do certificado e com base no próprio certificado conclui o processo de criação.
Recuperar dados do assinante é mais difícil. Eles contêm uma indicação do certificado e uma lista de parâmetros de assinatura (por exemplo, a data da assinatura). O processo de extração de dados é o seguinte:
Recuperando informações do assinante public int Decode(IntPtr _hMsg, int _iIndex, ISDPSignedCms _pSignedCms, ref string _sError) {
, CMSG_SIGNER_INFO. . , .
, — ( , ).
internal static CryptographicAttributeObjectCollection ReadCryptoAttrsCollection(CRYPT_ATTRIBUTES _pAttrs) { CryptographicAttributeObjectCollection pRes = new CryptographicAttributeObjectCollection(); for (int i = 0; i < _pAttrs.cAttr; i++) { IntPtr hAttr = new IntPtr((long)_pAttrs.rgAttr + (i * Marshal.SizeOf(typeof(CRYPT_ATTRIBUTE)))); CRYPT_ATTRIBUTE pAttr = (CRYPT_ATTRIBUTE) Marshal.PtrToStructure(hAttr, typeof(CRYPT_ATTRIBUTE)); CryptographicAttributeObject pAttrInfo = new CryptographicAttributeObject(new Oid(pAttr.pszObjId), GetAsnEncodedDataCollection(pAttr)); pRes.Add(pAttrInfo); } return pRes; }
Oid – ( ASN.1). :
internal static Pkcs9AttributeObject Pkcs9AttributeFromOID(string _sName) { switch (_sName) { case UCConsts.S_SIGN_DATE_OID : return new Pkcs9SigningTime();
Pkcs9AttributeObject. , mono . Mono .
— — SignedCms, .
, , . (, , ).
public static int EncryptDataCP(byte[] _arInput, X509Certificate2 _pCert, out byte[] _arRes, ref string _sError) { _arRes = new byte[0]; try {
— , . , , .
, , .
. , ( ). :
internal static int GetEncodeAlgoritmOID(IntPtr _hCertHandle, out string _sOID, ref string _sError) { bool fNeedRelease = false; _sOID = ""; uint iKeySpec = 0; IntPtr hCrypto = IntPtr.Zero; try {
. ( , , , .), . (UCConsts.CRYPT_ENCRYPT_ALG_OID_GROUP_ID). — .
( ).
, , . . — , :
public static int DecryptDataCP(byte[] _arInput, out X509Certificate2 _pCert, out byte[] _arRes, ref string _sError) { _arRes = new byte[0]; _pCert = null; IntPtr hSysStore = UCryptoAPI.CertOpenSystemStore(IntPtr.Zero, UCConsts.AR_CRYPTO_STORE_NAME[(int)StoreName.My]); GCHandle GC = GCHandle.Alloc(hSysStore, GCHandleType.Pinned); IntPtr hOutCertL = IntPtr.Zero; IntPtr hOutCert = IntPtr.Zero; try {
, . , ( Linux ).
, , , , . , . :
- ( , , . .);
- — ;
- — ;
- , , (CRL);
, .
Como já ficou claro na introdução, verificar a validade do certificado é uma das tarefas mais difíceis. É por isso que a biblioteca possui muitos métodos para implementar cada um dos itens individualmente. Portanto, para simplificar, recorremos às fontes .Net do método X509Certificate2.Verify () e as tomamos como base.
A verificação consiste em dois estágios:- formar uma cadeia de certificados até a raiz;
- verifique cada um dos certificados nele (revogação, horário etc.);
Essa verificação deve ser realizada antes da assinatura e criptografia na data atual e no momento da verificação da assinatura na data da assinatura. O próprio método de verificação é pequeno:
Verificação de certificado internal static int VerifyCertificate (IntPtr _hCert, X509RevocationMode _iRevMode, X509RevocationFlag _iRevFlag, DateTime _rOnDate, TimeSpan _iCTLTimeout, IntPtr _hPolicy, ref string _sError) { if (_hCert == IntPtr.Zero) { _sError = UCConsts.S_CRYPTO_CERT_CHECK_ERR; return UConsts.E_NO_CERTIFICATE; } CERT_CHAIN_POLICY_PARA pPolicyParam = new CERT_CHAIN_POLICY_PARA(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_PARA))); CERT_CHAIN_POLICY_STATUS pPolicyStatus = new CERT_CHAIN_POLICY_STATUS(Marshal.SizeOf(typeof(CERT_CHAIN_POLICY_STATUS)));
Primeiro, uma cadeia é formada usando o método BuildChain e, em seguida, é verificada. Durante a formação da cadeia, a estrutura dos parâmetros, a data da verificação e os sinalizadores de verificação são formados:
internal static int BuildChain (IntPtr _hChainEngine, IntPtr _hCert, X509RevocationMode _iRevMode, X509RevocationFlag _iRevFlag, DateTime _rOnDate, TimeSpan _rCTLTimeOut, ref IntPtr _hChain, ref string _sError) {
, Microsoft. hCertPolicy hAppPolicy OID-, , . , , .
(, ).
MapRevocationFlags — .Net — uint .
Conclusão
:
- 10 ;
- ;
- byte[] {1, 2, 3, 4, 5};
- ;
- ;
- byte[] {1, 2, 3, 4, 5};
- ;
Windows Linux 1-, 10- 50- , Linux . Linux - - ( , ), «» . (deadlock-) ( «Access Violation»).
UCryptoAPI . fpCPSection object :
private static object fpCPSection = new object(); internal static bool CryptMsgClose(IntPtr _hCryptMsg) { lock (pCPSection) { if (fIsLinux) return LCryptoAPI.CryptMsgClose(_hCryptMsg); else return WCryptoAPI.CryptMsgClose(_hCryptMsg); } } public static object pCPSection { get { return fpCPSection;} }
, Linux- .
mono Issuer Subject . , , mono X500DistinguishedName . , mono ( ), (impl.issuerName impl.subjectName). (Reflection) X500DistinguishedName, CERT_CONTEXT .
Referências
- CAPILite
- c #
- .Net:
- CAPIBase
- X509Certificate2
- SignedCMS
- SignerInfo
- mono:
- X509Certificate2
- X509CertificateImplBtls