.NET 7 против российской криптографии. Часть 2. Штампы времени

от автора

Это вторая часть статьи про криптографические сложности в .NET 7. Предыдущая доступна здесь.

Практически неотъемлемой частью формирования электронной подписи стало формирование штампов времени (TS) на подпись. С их помощью обеспечивается доверенное подтверждение времени подписания документа. Со штампами времени в .NET 7 та же беда, что и с CMS-сообщениями — отсутствие нативной поддержки российских алгоритмов хэширования и электронной подписи на уровне фреймворка. Но, благо, старый добрый WinAPI и здесь поможет решить задачу.

Саму теорию TS описывать здесь не буду, за деталями отсылаю в RFC3161, российские выкрутасы на эту тему неплохо освещены в этой статье.

Получение штампа времени

В WinAPI вся работа по получению штампа времени выполняется одной функцией CryptRetrieveTimeStamp.

Как всегда, начинаем с кода:

/// <summary> /// Calculates a timestamp token for a given data /// </summary> /// <param name="data">A source binary data for timestamping</param> /// <param name="tspDigestOid">An OID of a message digest algorithm</param> /// <param name="nonce">A nonce value, can be empty</param> /// <param name="tsaUri">An URI of a TSA</param> /// <param name="timeout">A TSA request timeout</param> /// <returns>A timestamp token in DER encoding</returns> public static unsafe byte[] RetriveTimestamp(ReadOnlySpan<byte> data, Oid tspDigestOid, ReadOnlySpan<byte> nonce, string tsaUri, TimeSpan timeout) { var tspReq = new CRYPT_TIMESTAMP_PARA(); tspReq.fRequestCerts = true; fixed (byte* pData = data, pNonce = nonce) { if (nonce.Length > 0) { tspReq.Nonce.cbData = (uint)nonce.Length; tspReq.Nonce.pbData = (nint)pNonce; }  nint pTsContext; CryptRetrieveTimeStamp(tsaUri, TIMESTAMP_NO_AUTH_RETRIEVAL | TIMESTAMP_VERIFY_CONTEXT_SIGNATURE,  timeout.Milliseconds, tspDigestOid.Value, (nint)(&tspReq), (nint)pData, (uint)data.Length, (nint)(&pTsContext), 0, 0).VerifyWinapiTrue(); try { var tsContext = new ReadOnlySpan<CRYPT_TIMESTAMP_CONTEXT>(pTsContext.ToPointer(), 1); var tst = new ReadOnlySpan<byte>(tsContext[0].pbEncoded.ToPointer(), (int)tsContext[0].cbEncoded); return tst.ToArray(); } finally { if (pTsContext != 0) CryptMemFree(pTsContext); } } } 

Использование довольно прямолинейное:

  1. В качестве исходных данных передаются:

    1. data — данные, для которых вычисляется штамп времени.

    2. tspDigestOid — OID алгоритма хэширования. Так как в TSA отправляются не сами удостоверяемые данные, а их хэш, то этим аргументом задаётся алгоритм хэширования и, опционально, его параметры.

    3. nonce — последовательность случайных байтов, служит для защиты от атак повтором сообщения. Может быть пустой, но стандарт настоятельно рекомендует использовать. Я обычно использую 16 случайных байт.

    4. tsaUri — URI центра выдачи штампов времени (TSA).

    5. timeout — таймаут ожидания ответа TSA.

  2. Заполняем структуру CRYPT_TIMESTAMP_PARA параметрами запроса TS. Из всех полей этой структуры в подавляющем большинстве случаем нужны только два: Nonce и fRequestCerts. В первое сохраняем наш входной параметр nonce, второе ставим в true для того, чтобы TSA включил в штамп времени свой сертификат, без которого проверить достоверность штампа будет затруднительно.

  3. Вызываем функцию CryptRetrieveTimeStamp, которая выполняет за нас всю низкоуровневую работу: рассчитывает хэш исходных данных, формирует запрос к TSA, отправляет его по HTTP, читает и разбирает ответ TSA, проверяет валидность полученного токена и сертификата TSA (если задан флаг TIMESTAMP_VERIFY_CONTEXT_SIGNATURE).

  4. Извлекаем токен из ответа TSA (поле pbEncoded выходной структуры CRYPT_TIMESTAMP_CONTEXT).

  5. Освобождаем неуправляемый блок памяти c ответом TSA с помощью функции CryptMemFree.

Токен, полученный в п. 4, можно сохранить в файл, прикладывая его где нужно к подписанному сообщению. По такой простейшей технологии работает, например, система ЭТРАН ОАО «РЖД», где штамп времени хранится отдельно от электронной подписи в формате CMS.

Встраивание штампа времени в подписанное CMS-сообщение

Более интересный вариант — это когда полученный штамп времени нужно сохранить внутри исходного CMS-сообщения в качестве одного из неподписываемых атрибутов. Такой вариант, например, используется в формате подписи CAdES-T и его производных. WinAPI поможет решить и эту задачу, правда кода будет уже больше, так как необходимо повозиться с обновлением CMS-сообщения.

И вновь начинаем с готового кода, который, как известно, один из лучших способов документации:

/// <summary> /// Calculates and adds a timestamp token to a CMS message as an unsigned attribute /// </summary> /// <param name="cms">A target CMS message</param> /// <param name="detachedSignature">A flag of the detached signature in the CMS</param> /// <param name="signerIndex">An index of the CMS signer</param> /// <param name="tspDigestOid">An OID of a message digest algorithm</param> /// <param name="nonce">A nonce value, can be empty</param> /// <param name="tsaUri">An URI of a TSA</param> /// <param name="timeout">A TSA request timeout</param> /// <returns>A new CMS message with an injected timestamp token</returns> public static unsafe byte[] AddTimestampToCms(ReadOnlySpan<byte> cms, bool detachedSignature, uint signerIndex, Oid tspDigestOid, ReadOnlySpan<byte> nonce, string tsaUri, TimeSpan timeout) { var hMsg = CryptMsgOpenToDecode(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, detachedSignature ? CMSG_DETACHED_FLAG : 0U, 0, 0, 0, 0) .VerifyWinapiNonzero(); try { // load the CMS signed message fixed (byte* pCms = cms) CryptMsgUpdate(hMsg, (nint)pCms, (uint)cms.Length, true).VerifyWinapiTrue();  // extract the signature from the CMS message for the specified signerIndex var signatureLength = 0; CryptMsgGetParam(hMsg, CMSG_ENCRYPTED_DIGEST, signerIndex, 0, (nint)(&signatureLength)).VerifyWinapiTrue(); var signature = stackalloc byte[signatureLength]; CryptMsgGetParam(hMsg, CMSG_ENCRYPTED_DIGEST, signerIndex, (nint)signature, (nint)(&signatureLength)).VerifyWinapiTrue();  // receive timestamp on the extracted signature var tst = RetriveTimestamp(new ReadOnlySpan<byte>(signature, signatureLength), tspDigestOid, nonce, tsaUri, timeout);  // add a new unsigned attribute fixed (byte* pzdObjId = "1.2.840.113549.1.9.16.2.14"u8, pTst = tst) { var tstBlob = new CRYPT_INTEGER_BLOB(); tstBlob.cbData = (uint)tst.Length; tstBlob.pbData = (nint)pTst;  var tstAttr = new CRYPT_ATTRIBUTE(); tstAttr.pszObjId = (nint)pzdObjId; tstAttr.cValue = 1; tstAttr.rgValue = (nint)(&tstBlob);  // encode a timestamp attribute to DER var attr = (nint)0; var attrLen = 0U; CryptEncodeObjectEx(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, PKCS_ATTRIBUTE, (nint)(&tstAttr), CRYPT_ENCODE_ALLOC_FLAG, 0, (nint)(&attr), (nint)(&attrLen)).VerifyWinapiTrue(); try { // inject the encoded unsigned attribute to the SignerInfo var cmsAttr = new CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR_PARA(); cmsAttr.dwSignerIndex = signerIndex; cmsAttr.blob.cbData = attrLen; cmsAttr.blob.pbData = attr; CryptMsgControl(hMsg, 0, CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, (nint)(&cmsAttr)).VerifyWinapiTrue(); } finally { LocalFree(attr).VerifyWinapiZero(); } }  // extract the updated CMS message uint updatedCmsLength = 0; CryptMsgGetParam(hMsg, CMSG_ENCODED_MESSAGE, 0, 0, (nint)(&updatedCmsLength)).VerifyWinapiTrue(); var updatedCms = new byte[updatedCmsLength]; fixed (byte* pUpdatedCms = updatedCms) CryptMsgGetParam(hMsg, CMSG_ENCODED_MESSAGE, 0, (nint)pUpdatedCms, (nint)(&updatedCmsLength)).VerifyWinapiTrue(); return updatedCms; } finally { CryptMsgClose(hMsg); } } 

Теперь шаг за шагом подробнее разберём, что здесь происходит.

  1. Вызовом CryptMsgOpenToDecode создаём пустое CMS-сообщение для последующего декодирования.

  2. Загружаем исходное сообщение, вызывая CryptMsgUpdate.

  3. Извлекаем само значение электронной подписи для подписанта с индексом signerIndex (в общем случае в одном CMS-сообщений может быть много подписантов, штамп времени формируется для каждого отдельно): CryptMsgGetParam(hMsg, CMSG_ENCRYPTED_DIGEST, ...). Именно для этого значения будем запрашивать штамп времени в следующем шаге.

  4. С помощью ранее рассмотренного метода RetriveTimestamp запрашиваем штамп времени.

  5. С помощью функции CryptEncodeObjectEx кодируем штамп времени в CMS-атрибут id-aa-timeStampToken c OID=1.2.840.113549.1.9.16.2.14. Указатель на закодированный атрибут помещаем в структуру CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR_PARA.

  6. Вызовом CryptMsgControl(..., CMSG_CTRL_ADD_SIGNER_UNAUTH_ATTR, ...) добавляем закодированный атрибут в качестве неподписываемого для подписанта с индексом signerIndex.

  7. Освобождаем неуправляемую память, любезно выделенную нам системой при кодировании атрибута на шаге 5, вызывая привычную LocalFree.

  8. Извлекаем обновлённое CMS-сообщение с помощью вызывов CryptMsgGetParam(hMsg, CMSG_ENCODED_MESSAGE, ...) в виде байтового массива.

  9. Освобождаем неуправляемый объект CMS-сообщения, вызывая CryptMsgClose.

Полученным байтовым массивом следует заменить исходную подпись там, где это необходимо.

Заключение

Пользуясь нехитрыми операциями с WinAPI, мы успешно обошли ограничения фреймворка .NET 7 в виде отсутствия поддержки сторонних криптоалгоритмов. Но, очевидно, это будет работать только под Windows. Под Linux, скорее всего, придётся писать свои вызовы к OpenSSL; но это, как говорится, уже совсем другая история.

Полный код, включая необходимые классы P/Invoke доступны на GitHub.


ссылка на оригинал статьи https://habr.com/ru/articles/771162/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *